From 361bd570739deb43b84a8cd403c894dde53e6ed0 Mon Sep 17 00:00:00 2001 From: Xiliang Chen Date: Thu, 24 Oct 2024 13:51:14 +1300 Subject: [PATCH] Block data provider refactor (#192) * blockchain data provider refactor * fix and tests * fix * swiftlint is too slow --- .githooks/pre-commit | 10 +- .../BlockchainDataProvider.swift | 101 ++++++---- .../BlockchainDataProviderProtocol.swift | 5 + .../InMemoryDataProvider.swift | 29 +++ .../Sources/Blockchain/Types/BlockRef.swift | 9 + .../Sources/Blockchain/Types/StateRef.swift | 14 ++ .../Blockchain/Validator/BlockAuthor.swift | 4 +- .../Validator/ExtrinsicPoolService.swift | 4 +- .../BlockchainDataProviderTests.swift | 189 ++++++++++++++++++ .../ExtrinsicPoolServiceTests.swift | 16 +- .../InMemoryDataProviderTests.swift | 91 --------- .../ValidatorServiceTests.swift | 4 +- .../NetworkingProtocol/NetworkManager.swift | 4 +- Node/Sources/Node/Node.swift | 2 +- .../DataSource/Blockchain+DataSource.swift | 2 +- 15 files changed, 335 insertions(+), 149 deletions(-) create mode 100644 Blockchain/Tests/BlockchainTests/BlockchainDataProviderTests.swift delete mode 100644 Blockchain/Tests/BlockchainTests/InMemoryDataProviderTests.swift diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 89e71bd1..0edc2da8 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,10 +1,10 @@ #!/bin/bash -if which swiftlint >/dev/null; then - swiftlint lint --fix --quiet -else - echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" -fi +# if which swiftlint >/dev/null; then +# swiftlint lint --fix --quiet +# else +# echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" +# fi git diff --diff-filter=d --staged --name-only | grep -e '\(.*\).swift$' | while read line; do swiftformat "${line}"; diff --git a/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProvider.swift b/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProvider.swift index ee0a9cb2..edde73cb 100644 --- a/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProvider.swift +++ b/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProvider.swift @@ -3,61 +3,55 @@ import Utils private let logger = Logger(label: "BlockchainDataProvider") -private struct BlockchainStorage: Sendable { - var bestHead: Data32 - var bestHeadTimeslot: TimeslotIndex - var finalizedHead: Data32 +public struct HeadInfo: Sendable { + public var hash: Data32 + public var timeslot: TimeslotIndex + public var number: UInt32 } -public final class BlockchainDataProvider: Sendable { - private let storage: ThreadSafeContainer +public actor BlockchainDataProvider: Sendable { + public private(set) var bestHead: HeadInfo + public private(set) var finalizedHead: HeadInfo private let dataProvider: BlockchainDataProviderProtocol public init(_ dataProvider: BlockchainDataProviderProtocol) async throws { let heads = try await dataProvider.getHeads() - var bestHead: (HeaderRef, Data32)? + var bestHead = HeadInfo(hash: dataProvider.genesisBlockHash, timeslot: 0, number: 0) for head in heads { let header = try await dataProvider.getHeader(hash: head) - if bestHead == nil || header.value.timeslot > bestHead!.0.value.timeslot { - bestHead = (header, head) + if header.value.timeslot > bestHead.timeslot { + let number = try await dataProvider.getBlockNumber(hash: head) + bestHead = HeadInfo(hash: head, timeslot: header.value.timeslot, number: number) } } - let finalizedHead = try await dataProvider.getFinalizedHead() - storage = ThreadSafeContainer(.init( - bestHead: bestHead?.1 ?? dataProvider.genesisBlockHash, - bestHeadTimeslot: bestHead?.0.value.timeslot ?? 0, - finalizedHead: finalizedHead - )) + self.bestHead = bestHead - self.dataProvider = dataProvider - } + let finalizedHeadHash = try await dataProvider.getFinalizedHead() - public var bestHead: Data32 { - storage.value.bestHead - } + finalizedHead = try await HeadInfo( + hash: finalizedHeadHash, + timeslot: dataProvider.getHeader(hash: finalizedHeadHash).value.timeslot, + number: dataProvider.getBlockNumber(hash: finalizedHeadHash) + ) - public var finalizedHead: Data32 { - storage.value.finalizedHead + self.dataProvider = dataProvider } public func blockImported(block: BlockRef, state: StateRef) async throws { try await add(block: block) try await add(state: state) - try await updateHead(hash: block.hash, parent: block.header.parentHash) + try await dataProvider.updateHead(hash: block.hash, parent: block.header.parentHash) - if block.header.timeslot > storage.value.bestHeadTimeslot { - storage.write { storage in - storage.bestHead = block.hash - storage.bestHeadTimeslot = block.header.timeslot - } + if block.header.timeslot > bestHead.timeslot { + let number = try await dataProvider.getBlockNumber(hash: block.hash) + bestHead = HeadInfo(hash: block.hash, timeslot: block.header.timeslot, number: number) } logger.debug("block imported: \(block.hash)") } } -// expose BlockchainDataProviderProtocol extension BlockchainDataProvider { public func hasBlock(hash: Data32) async throws -> Bool { try await dataProvider.hasBlock(hash: hash) @@ -71,6 +65,10 @@ extension BlockchainDataProvider { try await dataProvider.isHead(hash: hash) } + public func getBlockNumber(hash: Data32) async throws -> UInt32 { + try await dataProvider.getBlockNumber(hash: hash) + } + public func getHeader(hash: Data32) async throws -> HeaderRef { try await dataProvider.getHeader(hash: hash) } @@ -95,31 +93,56 @@ extension BlockchainDataProvider { try await dataProvider.getBlockHash(byTimeslot: timeslot) } + public func getBlockHash(byNumber number: UInt32) async throws -> Set { + try await dataProvider.getBlockHash(byNumber: number) + } + + // add forks of finalized head is not allowed public func add(block: BlockRef) async throws { logger.debug("adding block: \(block.hash)") + // require parent exists (i.e. not purged) and block is not fork of any finalized block + guard try await hasBlock(hash: block.header.parentHash), block.header.timeslot > finalizedHead.timeslot else { + throw BlockchainDataProviderError.uncanonical(hash: block.hash) + } + try await dataProvider.add(block: block) } + /// only allow to add state if the corresponding block is added public func add(state: StateRef) async throws { logger.debug("adding state: \(state.value.lastBlockHash)") + // if block exists, that means it passed the canonicalization check + guard try await hasBlock(hash: state.value.lastBlockHash) else { + throw BlockchainDataProviderError.noData(hash: state.value.lastBlockHash) + } + try await dataProvider.add(state: state) } + /// Also purge fork of all finalized blocks public func setFinalizedHead(hash: Data32) async throws { logger.debug("setting finalized head: \(hash)") - try await dataProvider.setFinalizedHead(hash: hash) - storage.write { storage in - storage.finalizedHead = hash - } - } + let oldFinalizedHead = finalizedHead + let number = try await dataProvider.getBlockNumber(hash: hash) - public func updateHead(hash: Data32, parent: Data32) async throws { - logger.debug("updating head: \(hash) with parent: \(parent)") + var hashToCheck = hash + var hashToCheckNumber = number + while hashToCheck != oldFinalizedHead.hash { + let hashes = try await dataProvider.getBlockHash(byNumber: hashToCheckNumber) + for hash in hashes where hash != hashToCheck { + logger.trace("purge block: \(hash)") + try await dataProvider.remove(hash: hash) + } + hashToCheck = try await dataProvider.getHeader(hash: hashToCheck).value.parentHash + hashToCheckNumber -= 1 + } - try await dataProvider.updateHead(hash: hash, parent: parent) + let header = try await dataProvider.getHeader(hash: hash) + finalizedHead = HeadInfo(hash: hash, timeslot: header.value.timeslot, number: number) + try await dataProvider.setFinalizedHead(hash: hash) } public func remove(hash: Data32) async throws { @@ -131,4 +154,8 @@ extension BlockchainDataProvider { public var genesisBlockHash: Data32 { dataProvider.genesisBlockHash } + + public func getBestState() async throws -> StateRef { + try await dataProvider.getState(hash: bestHead.hash) + } } diff --git a/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProviderProtocol.swift b/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProviderProtocol.swift index 012373a2..c7c5cdb9 100644 --- a/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProviderProtocol.swift +++ b/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProviderProtocol.swift @@ -2,6 +2,7 @@ import Utils public enum BlockchainDataProviderError: Error, Equatable { case noData(hash: Data32) + case uncanonical(hash: Data32) } public protocol BlockchainDataProviderProtocol: Sendable { @@ -9,6 +10,8 @@ public protocol BlockchainDataProviderProtocol: Sendable { func hasState(hash: Data32) async throws -> Bool func isHead(hash: Data32) async throws -> Bool + func getBlockNumber(hash: Data32) async throws -> UInt32 + /// throw BlockchainDataProviderError.noData if not found func getHeader(hash: Data32) async throws -> HeaderRef @@ -24,6 +27,8 @@ public protocol BlockchainDataProviderProtocol: Sendable { /// return empty set if not found func getBlockHash(byTimeslot timeslot: TimeslotIndex) async throws -> Set + /// return empty set if not found + func getBlockHash(byNumber number: UInt32) async throws -> Set func add(block: BlockRef) async throws func add(state: StateRef) async throws diff --git a/Blockchain/Sources/Blockchain/BlockchainDataProvider/InMemoryDataProvider.swift b/Blockchain/Sources/Blockchain/BlockchainDataProvider/InMemoryDataProvider.swift index d075be2b..8b90dd6c 100644 --- a/Blockchain/Sources/Blockchain/BlockchainDataProvider/InMemoryDataProvider.swift +++ b/Blockchain/Sources/Blockchain/BlockchainDataProvider/InMemoryDataProvider.swift @@ -4,6 +4,8 @@ public actor InMemoryDataProvider: Sendable { public private(set) var heads: Set public private(set) var finalizedHead: Data32 + private var hashByNumber: [UInt32: Set] = [:] + private var numberByHash: [Data32: UInt32] = [:] private var blockByHash: [Data32: BlockRef] = [:] private var stateByBlockHash: [Data32: StateRef] = [:] private var hashByTimeslot: [TimeslotIndex: Set] = [:] @@ -32,6 +34,13 @@ extension InMemoryDataProvider: BlockchainDataProviderProtocol { heads.contains(hash) } + public func getBlockNumber(hash: Data32) async throws -> UInt32 { + guard let number = numberByHash[hash] else { + throw BlockchainDataProviderError.noData(hash: hash) + } + return number + } + public func getHeader(hash: Data32) throws -> HeaderRef { guard let header = blockByHash[hash]?.header.asRef() else { throw BlockchainDataProviderError.noData(hash: hash) @@ -65,6 +74,10 @@ extension InMemoryDataProvider: BlockchainDataProviderProtocol { hashByTimeslot[timeslot] ?? Set() } + public func getBlockHash(byNumber number: UInt32) -> Set { + hashByNumber[number] ?? Set() + } + public func add(state: StateRef) { stateByBlockHash[state.value.lastBlockHash] = state hashByTimeslot[state.value.timeslot, default: Set()].insert(state.value.lastBlockHash) @@ -73,6 +86,13 @@ extension InMemoryDataProvider: BlockchainDataProviderProtocol { public func add(block: BlockRef) { blockByHash[block.hash] = block hashByTimeslot[block.header.timeslot, default: Set()].insert(block.hash) + let blockNumber = if let number = numberByHash[block.header.parentHash] { + number + 1 + } else { + UInt32(0) + } + numberByHash[block.hash] = blockNumber + hashByNumber[blockNumber, default: Set()].insert(block.hash) } public func setFinalizedHead(hash: Data32) { @@ -92,9 +112,18 @@ extension InMemoryDataProvider: BlockchainDataProviderProtocol { public func remove(hash: Data32) { let timeslot = blockByHash[hash]?.header.timeslot ?? stateByBlockHash[hash]?.value.timeslot stateByBlockHash.removeValue(forKey: hash) + blockByHash.removeValue(forKey: hash) if let timeslot { hashByTimeslot[timeslot]?.remove(hash) } + + let number = numberByHash.removeValue(forKey: hash) + + if let number { + hashByNumber[number]?.remove(hash) + } + + heads.remove(hash) } } diff --git a/Blockchain/Sources/Blockchain/Types/BlockRef.swift b/Blockchain/Sources/Blockchain/Types/BlockRef.swift index 22996b78..797cb871 100644 --- a/Blockchain/Sources/Blockchain/Types/BlockRef.swift +++ b/Blockchain/Sources/Blockchain/Types/BlockRef.swift @@ -32,3 +32,12 @@ extension BlockRef: Codable { try value.encode(to: encoder) } } + +extension BlockRef { + public static func dummy(config: ProtocolConfigRef, parent: BlockRef) -> BlockRef { + dummy(config: config).mutate { + $0.header.unsigned.parentHash = parent.hash + $0.header.unsigned.timeslot = parent.header.timeslot + 1 + } + } +} diff --git a/Blockchain/Sources/Blockchain/Types/StateRef.swift b/Blockchain/Sources/Blockchain/Types/StateRef.swift index 2ca1720c..ac907bf0 100644 --- a/Blockchain/Sources/Blockchain/Types/StateRef.swift +++ b/Blockchain/Sources/Blockchain/Types/StateRef.swift @@ -37,3 +37,17 @@ extension StateRef: Codable { try value.encode(to: encoder) } } + +extension StateRef { + public static func dummy(config: ProtocolConfigRef, block: BlockRef) -> StateRef { + dummy(config: config).mutate { + $0.recentHistory.items.safeAppend(RecentHistory.HistoryItem( + headerHash: block.hash, + mmr: MMR([]), + stateRoot: Data32(), + workReportHashes: try! ConfigLimitedSizeArray(config: config) + )) + $0.timeslot = block.header.timeslot + 1 + } + } +} diff --git a/Blockchain/Sources/Blockchain/Validator/BlockAuthor.swift b/Blockchain/Sources/Blockchain/Validator/BlockAuthor.swift index 728c11b2..1b440b33 100644 --- a/Blockchain/Sources/Blockchain/Validator/BlockAuthor.swift +++ b/Blockchain/Sources/Blockchain/Validator/BlockAuthor.swift @@ -47,7 +47,7 @@ public final class BlockAuthor: ServiceBase2, @unchecked Sendable { ) async throws -> BlockRef { - let parentHash = dataProvider.bestHead + let parentHash = await dataProvider.bestHead.hash logger.trace("creating new block for timeslot: \(timeslot) with parent hash: \(parentHash)") @@ -175,7 +175,7 @@ public final class BlockAuthor: ServiceBase2, @unchecked Sendable { tickets.value = [] let timeslot = epoch.epochToTimeslotIndex(config: config) - let bestHead = dataProvider.bestHead + let bestHead = await dataProvider.bestHead.hash let state = try await dataProvider.getState(hash: bestHead) // simulate next block to determine the block authors for next epoch diff --git a/Blockchain/Sources/Blockchain/Validator/ExtrinsicPoolService.swift b/Blockchain/Sources/Blockchain/Validator/ExtrinsicPoolService.swift index 7a03f478..604afb04 100644 --- a/Blockchain/Sources/Blockchain/Validator/ExtrinsicPoolService.swift +++ b/Blockchain/Sources/Blockchain/Validator/ExtrinsicPoolService.swift @@ -104,13 +104,13 @@ public final class ExtrinsicPoolService: ServiceBase, @unchecked Sendable { // Safrole VRF commitments only changes every epoch // and we should never receive tickets at very beginning and very end of an epoch // so it is safe to use best head state without worrying about forks or edge cases - let state = try await dataProvider.getState(hash: dataProvider.bestHead) + let state = try await dataProvider.getBestState() try await storage.update(state: state, config: config) await storage.add(tickets: tickets.items) } private func on(safroleTicketsReceived tickets: RuntimeEvents.SafroleTicketsReceived) async throws { - let state = try await dataProvider.getState(hash: dataProvider.bestHead) + let state = try await dataProvider.getBestState() try await storage.update(state: state, config: config) await storage.add(tickets: tickets.items, config: config) diff --git a/Blockchain/Tests/BlockchainTests/BlockchainDataProviderTests.swift b/Blockchain/Tests/BlockchainTests/BlockchainDataProviderTests.swift new file mode 100644 index 00000000..9d2933b6 --- /dev/null +++ b/Blockchain/Tests/BlockchainTests/BlockchainDataProviderTests.swift @@ -0,0 +1,189 @@ +import Testing +import TracingUtils +import Utils + +@testable import Blockchain + +struct BlockchainDataProviderTests { + let config = ProtocolConfigRef.mainnet + let genesisBlock: BlockRef + let genesisState: StateRef + let provider: BlockchainDataProvider + + init() async throws { + setupTestLogger() + + (genesisState, genesisBlock) = try State.devGenesis(config: config) + provider = try await BlockchainDataProvider(InMemoryDataProvider(genesisState: genesisState, genesisBlock: genesisBlock)) + } + + // MARK: - Initialization Tests + + @Test func testInitialization() async throws { + #expect(await provider.bestHead.hash == genesisBlock.hash) + #expect(await provider.finalizedHead.hash == genesisBlock.hash) + #expect(try await provider.getFinalizedHead() == genesisBlock.hash) + #expect(try await provider.getHeads() == [genesisBlock.hash]) + } + + // MARK: - Block Tests + + @Test func testBlockOperations() async throws { + // Test block addition + let block = BlockRef.dummy(config: config, parent: genesisBlock) + try await provider.add(block: block) + + // Verify block exists + #expect(try await provider.hasBlock(hash: block.hash)) + #expect(try await provider.getBlock(hash: block.hash) == block) + + // Verify header can be retrieved + let header = try await provider.getHeader(hash: block.hash) + #expect(header.value == block.header) + + // Test getting block by timeslot + let blocks = try await provider.getBlockHash(byTimeslot: block.header.timeslot) + #expect(blocks.contains(block.hash)) + } + + @Test func testBlockOperationsErrors() async throws { + let nonExistentHash = Data32.random() + + // Test getting non-existent block + await #expect(throws: BlockchainDataProviderError.noData(hash: nonExistentHash)) { + _ = try await provider.getBlock(hash: nonExistentHash) + } + + // Test getting non-existent header + await #expect(throws: BlockchainDataProviderError.noData(hash: nonExistentHash)) { + _ = try await provider.getHeader(hash: nonExistentHash) + } + + // Test adding block without parent + let invalidBlock = BlockRef.dummy(config: config).mutate { + $0.header.unsigned.parentHash = nonExistentHash + } + await #expect(throws: BlockchainDataProviderError.uncanonical(hash: invalidBlock.hash)) { + try await provider.add(block: invalidBlock) + } + } + + // MARK: - State Tests + + @Test func testStateOperations() async throws { + // Test state addition + let block = BlockRef.dummy(config: config, parent: genesisBlock) + let state = StateRef.dummy(config: config, block: block) + + try await provider.blockImported(block: block, state: state) + + // Verify state exists + #expect(try await provider.hasState(hash: block.hash)) + #expect(try await provider.getState(hash: block.hash).stateRoot == state.stateRoot) + + // Test getting best state + let bestState = try await provider.getBestState() + #expect(bestState.stateRoot == state.stateRoot) + } + + @Test func testStateOperationsErrors() async throws { + let nonExistentHash = Data32.random() + + // Test getting non-existent state + await #expect(throws: BlockchainDataProviderError.noData(hash: nonExistentHash)) { + _ = try await provider.getState(hash: nonExistentHash) + } + + let block = BlockRef.dummy(config: config, parent: genesisBlock) + + // Test adding state without corresponding block + let state = StateRef.dummy(config: config, block: block) + await #expect(throws: BlockchainDataProviderError.noData(hash: block.hash)) { + try await provider.add(state: state) + } + } + + // MARK: - Head Management Tests + + @Test func testHeadManagement() async throws { + // Create a chain of blocks + let block1 = BlockRef.dummy(config: config, parent: genesisBlock) + let block2 = BlockRef.dummy(config: config, parent: block1) + let state1 = StateRef.dummy(config: config, block: block1) + let state2 = StateRef.dummy(config: config, block: block2) + + // Add blocks and states + try await provider.blockImported(block: block1, state: state1) + try await provider.blockImported(block: block2, state: state2) + + // Verify head updates + #expect(await provider.bestHead.hash == block2.hash) + #expect(try await provider.isHead(hash: block2.hash)) + + // Test finalization + try await provider.setFinalizedHead(hash: block1.hash) + #expect(await provider.finalizedHead.hash == block1.hash) + + // Verify fork removal + let fork = BlockRef.dummy(config: config, parent: block1).mutate { + $0.header.unsigned.extrinsicsHash = Data32.random() // so it is different + } + try await provider.add(block: fork) + try await provider.setFinalizedHead(hash: block2.hash) + #expect(try await provider.hasBlock(hash: fork.hash) == false) + } + + @Test func testHeadManagementErrors() async throws { + let nonExistentHash = Data32.random() + + // Test setting non-existent block as finalized head + await #expect(throws: BlockchainDataProviderError.noData(hash: nonExistentHash)) { + try await provider.setFinalizedHead(hash: nonExistentHash) + } + } + + // MARK: - Block Number Tests + + @Test func testBlockNumberOperations() async throws { + let block1 = BlockRef.dummy(config: config, parent: genesisBlock) + let block2 = BlockRef.dummy(config: config, parent: block1) + + try await provider.add(block: block1) + try await provider.add(block: block2) + + // Verify block numbers + #expect(try await provider.getBlockNumber(hash: genesisBlock.hash) == 0) + #expect(try await provider.getBlockNumber(hash: block1.hash) == 1) + #expect(try await provider.getBlockNumber(hash: block2.hash) == 2) + + // Test getting blocks by number + let blocksAtNumber1 = try await provider.getBlockHash(byNumber: 1) + #expect(blocksAtNumber1.contains(block1.hash)) + } + + @Test func testBlockNumberErrors() async throws { + let nonExistentHash = Data32.random() + + // Test getting number of non-existent block + await #expect(throws: BlockchainDataProviderError.noData(hash: nonExistentHash)) { + _ = try await provider.getBlockNumber(hash: nonExistentHash) + } + } + + // MARK: - Removal Tests + + @Test func testRemovalOperations() async throws { + let block = BlockRef.dummy(config: config, parent: genesisBlock) + let state = StateRef.dummy(config: config, block: block) + + try await provider.add(block: block) + try await provider.add(state: state) + + // Test removal + try await provider.remove(hash: block.hash) + + // Verify block and state are removed + #expect(try await provider.hasBlock(hash: block.hash) == false) + #expect(try await provider.hasState(hash: block.hash) == false) + } +} diff --git a/Blockchain/Tests/BlockchainTests/ExtrinsicPoolServiceTests.swift b/Blockchain/Tests/BlockchainTests/ExtrinsicPoolServiceTests.swift index fe05c18b..9822e0ae 100644 --- a/Blockchain/Tests/BlockchainTests/ExtrinsicPoolServiceTests.swift +++ b/Blockchain/Tests/BlockchainTests/ExtrinsicPoolServiceTests.swift @@ -38,7 +38,7 @@ struct ExtrinsicPoolServiceTests { @Test func testAddAndRetrieveTickets() async throws { - let state = try await dataProvider.getState(hash: dataProvider.bestHead) + let state = try await dataProvider.getBestState() var allTickets = SortedUniqueArray() @@ -74,7 +74,7 @@ struct ExtrinsicPoolServiceTests { @Test func testAddAndInvalidTickets() async throws { - let state = try await dataProvider.getState(hash: dataProvider.bestHead) + let state = try await dataProvider.getBestState() var allTickets = SortedUniqueArray() @@ -117,7 +117,7 @@ struct ExtrinsicPoolServiceTests { @Test func testRemoveTicketsOnBlockFinalization() async throws { // Add some tickets to the pool - let state = try await dataProvider.getState(hash: dataProvider.bestHead) + let state: StateRef = try await dataProvider.getBestState() let validatorKey = state.value.currentValidators[0] let secretKey = try await keystore.get(Bandersnatch.self, publicKey: Bandersnatch.PublicKey(data: validatorKey.bandersnatch))! @@ -149,7 +149,10 @@ struct ExtrinsicPoolServiceTests { availability: ExtrinsicAvailability.dummy(config: config), reports: ExtrinsicGuarantees.dummy(config: config) ) - let block = BlockRef(Block(header: Header.dummy(config: config), extrinsic: extrinsic)) + let block = BlockRef(Block(header: Header.dummy(config: config), extrinsic: extrinsic)).mutate { + $0.header.unsigned.parentHash = state.value.lastBlockHash + $0.header.unsigned.timeslot = state.value.timeslot + 1 + } try await dataProvider.add(block: block) @@ -168,7 +171,7 @@ struct ExtrinsicPoolServiceTests { @Test func testUpdateStateOnEpochChange() async throws { // Insert some valid tickets - let state = try await dataProvider.getState(hash: dataProvider.bestHead) + let state = try await dataProvider.getBestState() let validatorKey = state.value.currentValidators[0] let secretKey = try await keystore.get(Bandersnatch.self, publicKey: Bandersnatch.PublicKey(data: validatorKey.bandersnatch))! @@ -197,9 +200,10 @@ struct ExtrinsicPoolServiceTests { // Simulate an epoch change with new entropy let nextTimeslot = state.value.timeslot + TimeslotIndex(config.value.epochLength) + let bestHead = await dataProvider.bestHead.hash let newBlock = BlockRef.dummy(config: config).mutate { $0.header.unsigned.timeslot = nextTimeslot - $0.header.unsigned.parentHash = dataProvider.bestHead + $0.header.unsigned.parentHash = bestHead } let oldEntropyPool = state.value.entropyPool diff --git a/Blockchain/Tests/BlockchainTests/InMemoryDataProviderTests.swift b/Blockchain/Tests/BlockchainTests/InMemoryDataProviderTests.swift deleted file mode 100644 index 46bf59b0..00000000 --- a/Blockchain/Tests/BlockchainTests/InMemoryDataProviderTests.swift +++ /dev/null @@ -1,91 +0,0 @@ -import Testing -import Utils - -@testable import Blockchain - -struct InMemoryDataProviderTests { - let config = ProtocolConfigRef.mainnet - - @Test func testInitialization() async throws { - let (genesis, block) = try State.devGenesis(config: config) - let provider = await InMemoryDataProvider(genesisState: genesis, genesisBlock: block) - - #expect(await (provider.getHeads()) == [block.hash]) - #expect(await (provider.getFinalizedHead()) == block.hash) - } - - @Test func testAddAndRetrieveBlock() async throws { - let (genesis, block) = try State.devGenesis(config: config) - - let provider = await InMemoryDataProvider(genesisState: genesis, genesisBlock: block) - - await provider.add(block: block) - - #expect(await (provider.hasBlock(hash: block.hash)) == true) - #expect(try await (provider.getBlock(hash: block.hash)) == block) - await #expect(throws: BlockchainDataProviderError.noData(hash: Data32())) { - try await provider.getBlock(hash: Data32()) - } - } - - @Test func testAddAndRetrieveState() async throws { - let (genesis, block) = try State.devGenesis(config: config) - - let provider = await InMemoryDataProvider(genesisState: genesis, genesisBlock: block) - - let state = StateRef(State.dummy(config: config)) - await provider.add(state: state) - - #expect(await (provider.hasState(hash: state.value.lastBlockHash)) == true) - #expect(try await (provider.getState(hash: state.value.lastBlockHash)) == state) - } - - @Test func testUpdateHead() async throws { - let (genesis, block) = try State.devGenesis(config: config) - let provider = await InMemoryDataProvider(genesisState: genesis, genesisBlock: block) - - let newBlock = BlockRef(Block.dummy(config: config)).mutate { - $0.header.unsigned.timeslot = 123 - } - - await provider.add(block: newBlock) - try await provider.updateHead(hash: newBlock.hash, parent: block.hash) - - #expect(await provider.isHead(hash: newBlock.hash) == true) - #expect(await provider.isHead(hash: block.hash) == false) - - let hash = Data32.random() - await #expect(throws: BlockchainDataProviderError.noData(hash: hash)) { - try await provider.updateHead(hash: newBlock.hash, parent: hash) - } - } - - @Test func testSetFinalizedHead() async throws { - let (genesis, block) = try State.devGenesis(config: config) - - let provider = await InMemoryDataProvider(genesisState: genesis, genesisBlock: block) - - await provider.add(block: block) - await provider.setFinalizedHead(hash: block.hash) - - #expect(await (provider.getFinalizedHead()) == block.hash) - } - - @Test func testRemoveHash() async throws { - let (genesis, block) = try State.devGenesis(config: config) - - let provider = await InMemoryDataProvider(genesisState: genesis, genesisBlock: block) - - let state = StateRef(State.dummy(config: ProtocolConfigRef.dev)) - let timeslotIndex = state.value.timeslot - await provider.add(state: state) - - #expect(await (provider.hasState(hash: state.value.lastBlockHash)) == true) - #expect(await (provider.getBlockHash(byTimeslot: timeslotIndex).contains(state.value.lastBlockHash)) == true) - - await provider.remove(hash: state.value.lastBlockHash) - - #expect(await (provider.hasState(hash: state.value.lastBlockHash)) == false) - #expect(await (provider.getBlockHash(byTimeslot: timeslotIndex).contains(state.value.lastBlockHash)) == false) - } -} diff --git a/Blockchain/Tests/BlockchainTests/ValidatorServiceTests.swift b/Blockchain/Tests/BlockchainTests/ValidatorServiceTests.swift index 07c74cc2..9d83f727 100644 --- a/Blockchain/Tests/BlockchainTests/ValidatorServiceTests.swift +++ b/Blockchain/Tests/BlockchainTests/ValidatorServiceTests.swift @@ -7,7 +7,7 @@ import Utils struct ValidatorServiceTests { func setup(time: TimeInterval = 988) async throws -> (BlockchainServices, ValidatorService) { - setupTestLogger() + // setupTestLogger() let services = await BlockchainServices( timeProvider: MockTimeProvider(time: time) @@ -79,7 +79,7 @@ struct ValidatorServiceTests { #expect(await keystore.contains(publicKey: publicKey)) // Check the blockchain head is updated - #expect(dataProvider.bestHead == block.hash) + #expect(await dataProvider.bestHead.hash == block.hash) // Check block is stored in database #expect(try await dataProvider.hasBlock(hash: block.hash)) diff --git a/Node/Sources/Node/NetworkingProtocol/NetworkManager.swift b/Node/Sources/Node/NetworkingProtocol/NetworkManager.swift index dc8cc320..e43cbc6c 100644 --- a/Node/Sources/Node/NetworkingProtocol/NetworkManager.swift +++ b/Node/Sources/Node/NetworkingProtocol/NetworkManager.swift @@ -19,9 +19,9 @@ public final class NetworkManager: Sendable { // Those peers will receive all the messages regardless the target private let devPeers: Set - public init(config: Network.Config, blockchain: Blockchain, eventBus: EventBus, devPeers: Set) throws { + public init(config: Network.Config, blockchain: Blockchain, eventBus: EventBus, devPeers: Set) async throws { let handler = HandlerImpl(blockchain: blockchain) - network = try Network( + network = try await Network( config: config, protocolConfig: blockchain.config, genesisHeader: blockchain.dataProvider.genesisBlockHash, diff --git a/Node/Sources/Node/Node.swift b/Node/Sources/Node/Node.swift index 807bcad0..c62904b9 100644 --- a/Node/Sources/Node/Node.swift +++ b/Node/Sources/Node/Node.swift @@ -48,7 +48,7 @@ public class Node { self.keystore = keystore - network = try NetworkManager( + network = try await NetworkManager( config: config.network, blockchain: blockchain, eventBus: eventBus, diff --git a/RPC/Sources/RPC/DataSource/Blockchain+DataSource.swift b/RPC/Sources/RPC/DataSource/Blockchain+DataSource.swift index 53f85ad1..8326b89e 100644 --- a/RPC/Sources/RPC/DataSource/Blockchain+DataSource.swift +++ b/RPC/Sources/RPC/DataSource/Blockchain+DataSource.swift @@ -3,7 +3,7 @@ import Utils extension Blockchain: DataSource { public func getBestBlock() async throws -> BlockRef { - try await dataProvider.getBlock(hash: dataProvider.bestHead) + try await dataProvider.getBlock(hash: dataProvider.bestHead.hash) } public func getBlock(hash: Data32) async throws -> BlockRef? {