From 0ea4adfa35f58d49a108c3819284afdc7792d836 Mon Sep 17 00:00:00 2001 From: Xiliang Chen Date: Fri, 25 Oct 2024 11:25:26 +1300 Subject: [PATCH] Fix block production (#195) * add failing test * fix schedule block timing on short epoch --- .../Config/ProtocolConfig+Timing.swift | 5 ++- .../Blockchain/RuntimeProtocols/Runtime.swift | 20 ++++++--- .../Blockchain/Validator/BlockAuthor.swift | 21 +++++++--- .../Blockchain/Validator/DevKeyStore.swift | 1 + .../BlockchainTests/BlockchainServices.swift | 7 ++-- .../Tests/BlockchainTests/MockScheduler.swift | 1 + .../ValidatorServiceTests.swift | 42 +++++++++++++++++-- 7 files changed, 79 insertions(+), 18 deletions(-) diff --git a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Timing.swift b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Timing.swift index c7528702..755d2ba3 100644 --- a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Timing.swift +++ b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Timing.swift @@ -20,7 +20,10 @@ extension ProtocolConfigRef { } public var prepareEpochStartTimeDelta: TimeInterval { - -TimeInterval(value.slotPeriodSeconds) * 3 + if value.epochLength < 15 { + return -TimeInterval(value.slotPeriodSeconds) + } + return -TimeInterval(value.slotPeriodSeconds) * 3 } public func scheduleTimeForAuthoring(timeslot: TimeslotIndex) -> TimeInterval { diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift index 4fb14c64..545b6bd2 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift @@ -76,7 +76,7 @@ public final class Runtime { // offendersMarkers is validated at apply time by Disputes } - public func validateHeaderSeal(block: BlockRef, state: inout State) throws(Error) { + public func validateHeaderSeal(block: BlockRef, state: inout State, prevState: StateRef) throws(Error) { let vrfOutput: Data32 let blockAuthorKey = try Result { try Bandersnatch.PublicKey(data: state.currentValidators[Int(block.header.authorIndex)].bandersnatch) @@ -87,7 +87,7 @@ public final class Runtime { switch state.safroleState.ticketsOrKeys { case let .left(tickets): let ticket = tickets[Int(index)] - let vrfInputData = SigningContext.safroleTicketInputData(entropy: state.entropyPool.t3, attempt: ticket.attempt) + let vrfInputData = SigningContext.safroleTicketInputData(entropy: prevState.value.entropyPool.t3, attempt: ticket.attempt) vrfOutput = try Result { try blockAuthorKey.ietfVRFVerify( vrfInputData: vrfInputData, @@ -107,16 +107,17 @@ public final class Runtime { logger.debug("expected key: \(key.toHexString()), got key: \(blockAuthorKey.data.toHexString())") throw Error.invalidAuthorKey } - let vrfInputData = SigningContext.fallbackSealInputData(entropy: state.entropyPool.t3) + let vrfInputData = SigningContext.fallbackSealInputData(entropy: prevState.value.entropyPool.t3) vrfOutput = try Result { - try blockAuthorKey.ietfVRFVerify( + logger.trace("verifying ticket", metadata: ["key": "\(blockAuthorKey.data.toHexString())"]) + return try blockAuthorKey.ietfVRFVerify( vrfInputData: vrfInputData, auxData: encodedHeader, signature: block.header.seal ) }.mapError(Error.invalidBlockSeal).get() - entropyVRFInputData = SigningContext.fallbackSealInputData(entropy: state.entropyPool.t3) + entropyVRFInputData = SigningContext.fallbackSealInputData(entropy: prevState.value.entropyPool.t3) } _ = try Result { @@ -156,7 +157,14 @@ public final class Runtime { do { try updateSafrole(block: block, state: &newState) - try validateHeaderSeal(block: block, state: &newState) + if newState.ticketsOrKeys != prevState.value.ticketsOrKeys { + logger.trace("state tickets changed", metadata: [ + "old": "\(prevState.value.ticketsOrKeys)", + "new": "\(newState.ticketsOrKeys)", + ]) + } + + try validateHeaderSeal(block: block, state: &newState, prevState: prevState) try updateDisputes(block: block, state: &newState) diff --git a/Blockchain/Sources/Blockchain/Validator/BlockAuthor.swift b/Blockchain/Sources/Blockchain/Validator/BlockAuthor.swift index 1b440b33..86f638af 100644 --- a/Blockchain/Sources/Blockchain/Validator/BlockAuthor.swift +++ b/Blockchain/Sources/Blockchain/Validator/BlockAuthor.swift @@ -44,13 +44,13 @@ public final class BlockAuthor: ServiceBase2, @unchecked Sendable { public func createNewBlock( timeslot: TimeslotIndex, claim: Either<(TicketItemAndOutput, Bandersnatch.PublicKey), Bandersnatch.PublicKey> - ) async throws - -> BlockRef - { + ) async throws -> BlockRef { let parentHash = await dataProvider.bestHead.hash logger.trace("creating new block for timeslot: \(timeslot) with parent hash: \(parentHash)") + // TODO: verify we are indeed the block author + let state = try await dataProvider.getState(hash: parentHash) let epoch = timeslot.timeslotToEpochIndex(config: config) @@ -175,8 +175,15 @@ public final class BlockAuthor: ServiceBase2, @unchecked Sendable { tickets.value = [] let timeslot = epoch.epochToTimeslotIndex(config: config) - let bestHead = await dataProvider.bestHead.hash - let state = try await dataProvider.getState(hash: bestHead) + let bestHead = await dataProvider.bestHead + + let bestHeadTimeslot = bestHead.timeslot + let bestHeadEpoch = bestHeadTimeslot.timeslotToEpochIndex(config: config) + if bestHeadEpoch + 1 != epoch { + logger.warning("best head epoch \(bestHeadEpoch) is too far from current epoch \(epoch)") + } + + let state = try await dataProvider.getState(hash: bestHead.hash) // simulate next block to determine the block authors for next epoch let res = try state.value.updateSafrole( @@ -187,6 +194,10 @@ public final class BlockAuthor: ServiceBase2, @unchecked Sendable { extrinsics: .dummy(config: config) ) + logger.trace("expected safrole tickets", metadata: [ + "tickets": "\(res.state.ticketsOrKeys)", "epoch": "\(epoch)", "parentTimeslot": "\(bestHead.timeslot)", + ]) + await scheduleNewBlocks(ticketsOrKeys: res.state.ticketsOrKeys, timeslot: timeslot) } } diff --git a/Blockchain/Sources/Blockchain/Validator/DevKeyStore.swift b/Blockchain/Sources/Blockchain/Validator/DevKeyStore.swift index 4a8dd0cc..7d753c23 100644 --- a/Blockchain/Sources/Blockchain/Validator/DevKeyStore.swift +++ b/Blockchain/Sources/Blockchain/Validator/DevKeyStore.swift @@ -28,6 +28,7 @@ public final class DevKeyStore: KeyStore { await keystore.get(type, publicKey: publicKey) } + @discardableResult public func addDevKeys(seed: UInt32) async throws -> KeySet { var seedData = Data(repeating: 0, count: 32) seedData[0 ..< 4] = seed.encode() diff --git a/Blockchain/Tests/BlockchainTests/BlockchainServices.swift b/Blockchain/Tests/BlockchainTests/BlockchainServices.swift index 140b12c1..d7985125 100644 --- a/Blockchain/Tests/BlockchainTests/BlockchainServices.swift +++ b/Blockchain/Tests/BlockchainTests/BlockchainServices.swift @@ -7,7 +7,7 @@ class BlockchainServices { let dataProvider: BlockchainDataProvider let eventBus: EventBus let scheduler: MockScheduler - let keystore: KeyStore + let keystore: DevKeyStore let storeMiddleware: StoreMiddleware let genesisBlock: BlockRef let genesisState: StateRef @@ -20,7 +20,8 @@ class BlockchainServices { init( config: ProtocolConfigRef = .dev, - timeProvider: MockTimeProvider = MockTimeProvider(time: 988) + timeProvider: MockTimeProvider = MockTimeProvider(time: 988), + keysCount: Int = 12 ) async { self.config = config self.timeProvider = timeProvider @@ -35,7 +36,7 @@ class BlockchainServices { scheduler = MockScheduler(timeProvider: timeProvider) - keystore = try! await DevKeyStore() + keystore = try! await DevKeyStore(devKeysCount: keysCount) } deinit { diff --git a/Blockchain/Tests/BlockchainTests/MockScheduler.swift b/Blockchain/Tests/BlockchainTests/MockScheduler.swift index 9266ca47..596edaee 100644 --- a/Blockchain/Tests/BlockchainTests/MockScheduler.swift +++ b/Blockchain/Tests/BlockchainTests/MockScheduler.swift @@ -77,6 +77,7 @@ final class MockScheduler: Scheduler, Sendable { func advance(by interval: TimeInterval) async { let to = timeProvider.getTimeInterval() + interval while await advanceNext(to: to) {} + mockTimeProvider.advance(to: to) } func advanceNext(to time: TimeInterval) async -> Bool { diff --git a/Blockchain/Tests/BlockchainTests/ValidatorServiceTests.swift b/Blockchain/Tests/BlockchainTests/ValidatorServiceTests.swift index 9d83f727..90abe888 100644 --- a/Blockchain/Tests/BlockchainTests/ValidatorServiceTests.swift +++ b/Blockchain/Tests/BlockchainTests/ValidatorServiceTests.swift @@ -6,11 +6,17 @@ import Utils @testable import Blockchain struct ValidatorServiceTests { - func setup(time: TimeInterval = 988) async throws -> (BlockchainServices, ValidatorService) { - // setupTestLogger() + func setup( + config: ProtocolConfigRef = .dev, + time: TimeInterval = 988, + keysCount: Int = 12 + ) async throws -> (BlockchainServices, ValidatorService) { + setupTestLogger() let services = await BlockchainServices( - timeProvider: MockTimeProvider(time: time) + config: config, + timeProvider: MockTimeProvider(time: time), + keysCount: keysCount ) let validatorService = await ValidatorService( blockchain: services.blockchain, @@ -112,4 +118,34 @@ struct ValidatorServiceTests { #expect(blockAuthoredEvents.count == 25) } + + @Test + func makeManyBlocksWithSingleKey() async throws { + let (services, validatorService) = try await setup( + config: .minimal, + keysCount: 0 + ) + let genesisState = services.genesisState + let storeMiddleware = services.storeMiddleware + let config = services.config + let scheduler = services.scheduler + let keystore = services.keystore + + try await keystore.addDevKeys(seed: 0) + + await validatorService.on(genesis: genesisState) + + await storeMiddleware.wait() + + for _ in 0 ..< 50 { + await scheduler.advance(by: TimeInterval(config.value.slotPeriodSeconds)) + await storeMiddleware.wait() // let events to be processed + } + + let events = await storeMiddleware.wait() + + let blockAuthoredEvents = events.filter { $0 is RuntimeEvents.BlockAuthored } + + #expect(blockAuthoredEvents.count > 0) + } }