diff --git a/fluffy/network/state/state_network.nim b/fluffy/network/state/state_network.nim index c7d7077a1..df15f69f6 100644 --- a/fluffy/network/state/state_network.nim +++ b/fluffy/network/state/state_network.nim @@ -122,17 +122,6 @@ proc getContractCode*( ): Future[Opt[ContractCodeRetrieval]] {.inline.} = n.getContent(key, ContractCodeRetrieval) -# High level endpoints -# eth_getBalance -# eth_getStorageAt -# eth_getCode - -func decodeKey(contentKey: ByteList): Opt[ContentKey] = - let key = ContentKey.decode(contentKey).valueOr: - return Opt.none(ContentKey) - - Opt.some(key) - proc getStateRootByBlockHash( n: StateNetwork, hash: BlockHash ): Future[Opt[KeccakHash]] {.async.} = @@ -146,7 +135,7 @@ proc getStateRootByBlockHash( Opt.some(header.stateRoot) -proc processOffer( +proc processOffer*( n: StateNetwork, maybeSrcNodeId: Opt[NodeId], contentKeyBytes: ByteList, @@ -183,10 +172,11 @@ proc processContentLoop(n: StateNetwork) {.async.} = try: while true: let (srcNodeId, contentKeys, contentValues) = await n.contentQueue.popFirst() + for i, contentValueBytes in contentValues: let contentKeyBytes = contentKeys[i] - contentKey = decodeKey(contentKeyBytes).valueOr: + contentKey = ContentKey.decode(contentKeyBytes).valueOr: error "Unable to decode offered content key", contentKeyBytes continue @@ -219,8 +209,7 @@ proc processContentLoop(n: StateNetwork) {.async.} = trace "processContentLoop canceled" proc start*(n: StateNetwork) = - info "Starting Portal execution state network", - protocolId = n.portalProtocol.protocolId + info "Starting Portal State Network", protocolId = n.portalProtocol.protocolId n.portalProtocol.start() n.processContentLoop = processContentLoop(n) diff --git a/fluffy/tests/state_network_tests/all_state_network_tests.nim b/fluffy/tests/state_network_tests/all_state_network_tests.nim index 7df995e09..4f65e1af8 100644 --- a/fluffy/tests/state_network_tests/all_state_network_tests.nim +++ b/fluffy/tests/state_network_tests/all_state_network_tests.nim @@ -8,13 +8,14 @@ {.warning[UnusedImport]: off.} import - ./test_state_content_keys, + ./test_state_content_keys_vectors, ./test_state_content_nibbles, - ./test_state_content_values, - #./test_state_network_gossip, - ./test_state_network, - ./test_state_recursivegossip_genesis, - ./test_state_recursivegossip_vectors, + ./test_state_content_values_vectors, + ./test_state_gossip_getparent_genesis, + ./test_state_gossip_getparent_vectors, + ./test_state_gossip_gossipoffer_vectors, + ./test_state_network_getcontent_vectors, + ./test_state_network_offercontent_vectors, ./test_state_validation_genesis, ./test_state_validation_trieproof, ./test_state_validation_vectors diff --git a/fluffy/tests/state_network_tests/state_test_helpers.nim b/fluffy/tests/state_network_tests/state_test_helpers.nim index e6f053fa9..717f07a54 100644 --- a/fluffy/tests/state_network_tests/state_test_helpers.nim +++ b/fluffy/tests/state_network_tests/state_test_helpers.nim @@ -9,10 +9,17 @@ import std/[sugar, sequtils], + chronos, eth/[common, trie, trie/db], + eth/p2p/discoveryv5/protocol as discv5_protocol, + eth/p2p/discoveryv5/routing_table, + ../../network/wire/[portal_protocol, portal_stream, portal_protocol_config], ../../nimbus/common/chain_config, - ../../network/state/[state_content, state_utils], - ../../eth_data/yaml_utils + ../../network/history/[history_content, history_network], + ../../network/state/[state_content, state_utils, state_network], + ../../eth_data/yaml_utils, + ../../database/content_db, + ../test_helpers export yaml_utils @@ -129,3 +136,53 @@ proc toState*( accountTrie.put(key, value) (accountTrie, storageStates) + +type StateNode* = ref object + discoveryProtocol*: discv5_protocol.Protocol + stateNetwork*: StateNetwork + +proc newStateNode*( + rng: ref HmacDrbgContext, port: int +): StateNode {.raises: [CatchableError].} = + let + node = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(port)) + db = ContentDB.new("", uint32.high, inMemory = true) + sm = StreamManager.new(node) + hn = HistoryNetwork.new(node, db, sm, FinishedAccumulator()) + sn = StateNetwork.new(node, db, sm, historyNetwork = Opt.some(hn)) + + return StateNode(discoveryProtocol: node, stateNetwork: sn) + +proc portalProtocol*(sn: StateNode): PortalProtocol = + sn.stateNetwork.portalProtocol + +proc localNode*(sn: StateNode): Node = + sn.discoveryProtocol.localNode + +proc start*(sn: StateNode) = + sn.stateNetwork.start() + +proc stop*(sn: StateNode) {.async.} = + sn.stateNetwork.stop() + await sn.discoveryProtocol.closeWait() + +proc containsId*(sn: StateNode, contentId: ContentId): bool = + return sn.stateNetwork.contentDB.get(contentId).isSome() + +proc mockBlockHashToStateRoot*( + sn: StateNode, blockHash: BlockHash, stateRoot: KeccakHash +) = + let + blockHeader = BlockHeader(stateRoot: stateRoot) + headerRlp = rlp.encode(blockHeader) + blockHeaderWithProof = BlockHeaderWithProof( + header: ByteList.init(headerRlp), proof: BlockHeaderProof.init() + ) + contentKeyBytes = history_content.ContentKey + .init(history_content.ContentType.blockHeader, blockHash) + .encode() + contentId = history_content.toContentId(contentKeyBytes) + + sn.portalProtocol().storeContent( + contentKeyBytes, contentId, SSZ.encode(blockHeaderWithProof) + ) diff --git a/fluffy/tests/state_network_tests/test_state_content_keys.nim b/fluffy/tests/state_network_tests/test_state_content_keys_vectors.nim similarity index 100% rename from fluffy/tests/state_network_tests/test_state_content_keys.nim rename to fluffy/tests/state_network_tests/test_state_content_keys_vectors.nim diff --git a/fluffy/tests/state_network_tests/test_state_content_values.nim b/fluffy/tests/state_network_tests/test_state_content_values_vectors.nim similarity index 100% rename from fluffy/tests/state_network_tests/test_state_content_values.nim rename to fluffy/tests/state_network_tests/test_state_content_values_vectors.nim diff --git a/fluffy/tests/state_network_tests/test_state_recursivegossip_genesis.nim b/fluffy/tests/state_network_tests/test_state_gossip_getparent_genesis.nim similarity index 98% rename from fluffy/tests/state_network_tests/test_state_recursivegossip_genesis.nim rename to fluffy/tests/state_network_tests/test_state_gossip_getparent_genesis.nim index 5779c3f78..48e4c1fec 100644 --- a/fluffy/tests/state_network_tests/test_state_recursivegossip_genesis.nim +++ b/fluffy/tests/state_network_tests/test_state_gossip_getparent_genesis.nim @@ -16,7 +16,7 @@ import ../../network/state/[state_content, state_validation, state_gossip, state_utils], ./state_test_helpers -suite "State Recursive Gossip - Genesis JSON Files": +suite "State Gossip getParent - Genesis JSON Files": let genesisFiles = [ "berlin2000.json", "calaveras.json", "chainid1.json", "chainid7.json", "devnet4.json", "devnet5.json", "holesky.json", "mainshadow1.json", "merge.json", diff --git a/fluffy/tests/state_network_tests/test_state_recursivegossip_vectors.nim b/fluffy/tests/state_network_tests/test_state_gossip_getparent_vectors.nim similarity index 99% rename from fluffy/tests/state_network_tests/test_state_recursivegossip_vectors.nim rename to fluffy/tests/state_network_tests/test_state_gossip_getparent_vectors.nim index 497f0d07b..202e8080d 100644 --- a/fluffy/tests/state_network_tests/test_state_recursivegossip_vectors.nim +++ b/fluffy/tests/state_network_tests/test_state_gossip_getparent_vectors.nim @@ -15,7 +15,7 @@ import ../../network/state/[state_content, state_gossip], ./state_test_helpers -suite "State Recursive Gossip - Test Vectors": +suite "State Gossip getParent - Test Vectors": test "Check account trie node parent matches expected recursive gossip": const file = testVectorDir / "account_trie_node.yaml" diff --git a/fluffy/tests/state_network_tests/test_state_gossip_gossipoffer_vectors.nim b/fluffy/tests/state_network_tests/test_state_gossip_gossipoffer_vectors.nim new file mode 100644 index 000000000..57c809d1d --- /dev/null +++ b/fluffy/tests/state_network_tests/test_state_gossip_gossipoffer_vectors.nim @@ -0,0 +1,389 @@ +# Fluffy +# Copyright (c) 2021-2024 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import + std/os, + testutils/unittests, + chronos, + stew/byteutils, + eth/common, + eth/p2p/discoveryv5/protocol as discv5_protocol, + eth/p2p/discoveryv5/routing_table, + ../../common/common_utils, + ../../network/wire/[portal_protocol, portal_stream], + ../../network/state/[state_content, state_network, state_gossip], + ../../database/content_db, + ./state_test_helpers + +procSuite "State Gossip - Gossip Offer": + const STATE_NODE1_PORT = 20602 + const STATE_NODE2_PORT = 20603 + + let rng = newRng() + + asyncTest "Gossip account trie nodes": + const file = testVectorDir / "account_trie_node.yaml" + + let + testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + + for i, testData in testCase: + if i == 1: + continue # skip scenario with no parent + + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_offer.hexToSeqByte() + contentValue = AccountTrieNodeOffer.decode(contentValueBytes).get() + + parentContentKeyBytes = + testData.recursive_gossip.content_key.hexToSeqByte().ByteList + parentContentKey = ContentKey.decode(parentContentKeyBytes).get() + parentContentId = toContentId(parentContentKeyBytes) + parentContentValueBytes = + testData.recursive_gossip.content_value_offer.hexToSeqByte() + parentContentValue = AccountTrieNodeOffer.decode(parentContentValueBytes).get() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + stateNode2.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode2.containsId(contentId) + + await stateNode1.portalProtocol.gossipOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.accountTrieNodeKey, + contentValue, + ) + + # wait for offer to be processed by state node 2 + while not stateNode2.stateNetwork.contentQueue.empty(): + await sleepAsync(1.milliseconds) + await sleepAsync(100.milliseconds) + + # check that both the offer and parent were received by the second state instance + let res1 = + await stateNode2.stateNetwork.getAccountTrieNode(contentKey.accountTrieNodeKey) + check: + stateNode2.containsId(contentId) + res1.isOk() + res1.get() == contentValue.toRetrievalValue() + res1.get().node == contentValue.toRetrievalValue().node + + let res2 = await stateNode2.stateNetwork.getAccountTrieNode( + parentContentKey.accountTrieNodeKey + ) + check: + stateNode2.containsId(parentContentId) + res2.isOk() + res2.get() == parentContentValue.toRetrievalValue() + res2.get().node == parentContentValue.toRetrievalValue().node + + await stateNode1.stop() + await stateNode2.stop() + + asyncTest "Gossip contract trie nodes": + const file = testVectorDir / "contract_storage_trie_node.yaml" + + let + testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + + for i, testData in testCase: + if i == 1: + continue # skip scenario with no parent + + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_offer.hexToSeqByte() + contentValue = ContractTrieNodeOffer.decode(contentValueBytes).get() + + parentContentKeyBytes = + testData.recursive_gossip.content_key.hexToSeqByte().ByteList + parentContentKey = ContentKey.decode(parentContentKeyBytes).get() + parentContentId = toContentId(parentContentKeyBytes) + parentContentValueBytes = + testData.recursive_gossip.content_value_offer.hexToSeqByte() + parentContentValue = ContractTrieNodeOffer.decode(parentContentValueBytes).get() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + stateNode2.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode2.containsId(contentId) + + await stateNode1.portalProtocol.gossipOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.contractTrieNodeKey, + contentValue, + ) + + # wait for offer to be processed by state node 2 + while not stateNode2.stateNetwork.contentQueue.empty(): + await sleepAsync(1.milliseconds) + await sleepAsync(100.milliseconds) + + # check that both the offer and parent were received by the second state instance + let res1 = await stateNode2.stateNetwork.getContractTrieNode( + contentKey.contractTrieNodeKey + ) + check: + stateNode2.containsId(contentId) + res1.isOk() + res1.get() == contentValue.toRetrievalValue() + res1.get().node == contentValue.toRetrievalValue().node + + let res2 = await stateNode2.stateNetwork.getContractTrieNode( + parentContentKey.contractTrieNodeKey + ) + check: + stateNode2.containsId(parentContentId) + res2.isOk() + res2.get() == parentContentValue.toRetrievalValue() + res2.get().node == parentContentValue.toRetrievalValue().node + + await stateNode1.stop() + await stateNode2.stop() + + asyncTest "Gossip contract bytecode": + const file = testVectorDir / "contract_bytecode.yaml" + + let + testCase = YamlContractBytecodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + + for i, testData in testCase: + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_offer.hexToSeqByte() + contentValue = ContractCodeOffer.decode(contentValueBytes).get() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + stateNode2.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode2.containsId(contentId) + + await stateNode1.portalProtocol.gossipOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.contractCodeKey, + contentValue, + ) + + # wait for offer to be processed by state node 2 + while not stateNode2.stateNetwork.contentQueue.empty(): + await sleepAsync(1.milliseconds) + await sleepAsync(100.milliseconds) + + # check that both the offer and parent were received by the second state instance + let res1 = + await stateNode2.stateNetwork.getContractCode(contentKey.contractCodeKey) + check: + stateNode2.containsId(contentId) + res1.isOk() + res1.get() == contentValue.toRetrievalValue() + res1.get().code == contentValue.toRetrievalValue().code + + await stateNode1.stop() + await stateNode2.stop() + + asyncTest "Recursive gossip account trie nodes": + const file = testVectorDir / "recursive_gossip.yaml" + + let + testCase = YamlRecursiveGossipKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + stateNode2.portalProtocol().addNode(stateNode1.localNode()) == Added + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + (await stateNode2.portalProtocol().ping(stateNode1.localNode())).isOk() + + for i, testData in testCase: + if i == 1: + continue + + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + leafData = testData.recursive_gossip[0] + contentKeyBytes = leafData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = leafData.content_value.hexToSeqByte() + contentValue = AccountTrieNodeOffer.decode(contentValueBytes).get() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + stateNode2.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode1.containsId(contentId) + check not stateNode2.containsId(contentId) + + # offer the leaf node + await stateNode1.portalProtocol.gossipOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.accountTrieNodeKey, + contentValue, + ) + + # wait for recursive gossip to complete + await sleepAsync(1000.milliseconds) + + # check that all nodes were received by both state instances + for kv in testData.recursive_gossip: + let + expectedKeyBytes = kv.content_key.hexToSeqByte().ByteList + expectedKey = ContentKey.decode(expectedKeyBytes).get() + expectedId = toContentId(expectedKeyBytes) + expectedValue = + AccountTrieNodeOffer.decode(kv.content_value.hexToSeqByte()).get() + res1 = await stateNode1.stateNetwork.getAccountTrieNode( + expectedKey.accountTrieNodeKey + ) + res2 = await stateNode2.stateNetwork.getAccountTrieNode( + expectedKey.accountTrieNodeKey + ) + check: + stateNode1.containsId(expectedId) + stateNode2.containsId(expectedId) + res1.isOk() + res1.get() == expectedValue.toRetrievalValue() + res1.get().node == expectedValue.toRetrievalValue().node + res2.isOk() + res2.get() == expectedValue.toRetrievalValue() + res2.get().node == expectedValue.toRetrievalValue().node + + await stateNode1.stop() + await stateNode2.stop() + + asyncTest "Recursive gossip contract trie nodes": + const file = testVectorDir / "recursive_gossip.yaml" + + let + testCase = YamlRecursiveGossipKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + stateNode2.portalProtocol().addNode(stateNode1.localNode()) == Added + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + (await stateNode2.portalProtocol().ping(stateNode1.localNode())).isOk() + + for i, testData in testCase: + if i != 1: + continue + + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + leafData = testData.recursive_gossip[0] + contentKeyBytes = leafData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = leafData.content_value.hexToSeqByte() + contentValue = ContractTrieNodeOffer.decode(contentValueBytes).get() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + stateNode2.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode1.containsId(contentId) + check not stateNode2.containsId(contentId) + + # offer the leaf node + await stateNode1.portalProtocol.gossipOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.contractTrieNodeKey, + contentValue, + ) + + # wait for recursive gossip to complete + await sleepAsync(1000.milliseconds) + + # check that all nodes were received by both state instances + for kv in testData.recursive_gossip: + let + expectedKeyBytes = kv.content_key.hexToSeqByte().ByteList + expectedKey = ContentKey.decode(expectedKeyBytes).get() + expectedId = toContentId(expectedKeyBytes) + expectedValue = + ContractTrieNodeOffer.decode(kv.content_value.hexToSeqByte()).get() + res1 = await stateNode1.stateNetwork.getContractTrieNode( + expectedKey.contractTrieNodeKey + ) + res2 = await stateNode2.stateNetwork.getContractTrieNode( + expectedKey.contractTrieNodeKey + ) + check: + stateNode1.containsId(expectedId) + stateNode2.containsId(expectedId) + res1.isOk() + res1.get() == expectedValue.toRetrievalValue() + res1.get().node == expectedValue.toRetrievalValue().node + res2.isOk() + res2.get() == expectedValue.toRetrievalValue() + res2.get().node == expectedValue.toRetrievalValue().node + + await stateNode1.stop() + await stateNode2.stop() diff --git a/fluffy/tests/state_network_tests/test_state_network.nim b/fluffy/tests/state_network_tests/test_state_network.nim deleted file mode 100644 index df2d4e0b9..000000000 --- a/fluffy/tests/state_network_tests/test_state_network.nim +++ /dev/null @@ -1,158 +0,0 @@ -# Fluffy -# Copyright (c) 2021-2024 Status Research & Development GmbH -# Licensed and distributed under either of -# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). -# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). -# at your option. This file may not be copied, modified, or distributed except according to those terms. - -import - std/os, - nimcrypto/hash, - testutils/unittests, - chronos, - eth/keys, - eth/trie, - eth/common/[eth_types, eth_hash], - eth/p2p/discoveryv5/protocol as discv5_protocol, - eth/p2p/discoveryv5/routing_table, - ../../network/wire/[portal_protocol, portal_stream], - ../../network/state/[state_content, state_network], - ../../database/content_db, - ../test_helpers, - ./state_test_helpers - -procSuite "State Network": - let rng = newRng() - - asyncTest "Test Share Full State": - let - accounts = - getGenesisAlloc("fluffy" / "tests" / "custom_genesis" / "chainid7.json") - (trie, _) = accounts.toState() - node1 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20312)) - sm1 = StreamManager.new(node1) - node2 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20313)) - sm2 = StreamManager.new(node2) - proto1 = - StateNetwork.new(node1, ContentDB.new("", uint32.high, inMemory = true), sm1) - proto2 = - StateNetwork.new(node2, ContentDB.new("", uint32.high, inMemory = true), sm2) - - check proto2.portalProtocol.addNode(node1.localNode) == Added - - var keys: seq[seq[byte]] - for k, v in trie.replicate: - keys.add(k) - - var nodeHash: NodeHash - copyMem(nodeHash.data.addr, unsafeAddr k[0], sizeof(nodeHash.data)) - - let - # TODO: add stateRoot, and path eventually - accountTrieNodeKey = AccountTrieNodeKey(nodeHash: nodeHash) - contentKey = ContentKey( - contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey - ) - contentId = toContentId(contentKey.encode()) - value = AccountTrieNodeRetrieval(node: TrieNode.init(v)) - - discard proto1.contentDB.put( - contentId, value.encode(), proto1.portalProtocol.localNode.id - ) - - for key in keys: - var nodeHash: NodeHash - copyMem(nodeHash.data.addr, unsafeAddr key[0], sizeof(nodeHash.data)) - - let - accountTrieNodeKey = AccountTrieNodeKey(nodeHash: nodeHash) - contentKey = ContentKey( - contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey - ) - contentId = toContentId(contentKey.encode()) - - # Note: GetContent and thus the lookup here is not really needed, as we - # only have to request data to one node. - let accTrieNode = await proto2.getAccountTrieNode(accountTrieNodeKey) - check accTrieNode.isSome() - - let hash = keccakHash(accTrieNode.get().node.asSeq()) - check hash.data == key - - proto1.stop() - proto2.stop() - await node1.closeWait() - await node2.closeWait() - - asyncTest "Find content in the network via content lookup": - # TODO: Improve this test so it actually need to go through several - # findNodes request, to properly test the lookup call. - let - accounts = - getGenesisAlloc("fluffy" / "tests" / "custom_genesis" / "chainid7.json") - (trie, _) = accounts.toState() - node1 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20312)) - sm1 = StreamManager.new(node1) - node2 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20313)) - sm2 = StreamManager.new(node2) - node3 = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20314)) - sm3 = StreamManager.new(node3) - - proto1 = - StateNetwork.new(node1, ContentDB.new("", uint32.high, inMemory = true), sm1) - proto2 = - StateNetwork.new(node2, ContentDB.new("", uint32.high, inMemory = true), sm2) - proto3 = - StateNetwork.new(node3, ContentDB.new("", uint32.high, inMemory = true), sm3) - - # Node1 knows about Node2, and Node2 knows about Node3 which hold all content - check proto1.portalProtocol.addNode(node2.localNode) == Added - check proto2.portalProtocol.addNode(node3.localNode) == Added - - check (await proto2.portalProtocol.ping(node3.localNode)).isOk() - - var keys: seq[seq[byte]] - for k, v in trie.replicate: - keys.add(k) - - var nodeHash: NodeHash - copyMem(nodeHash.data.addr, unsafeAddr k[0], sizeof(nodeHash.data)) - - let - accountTrieNodeKey = AccountTrieNodeKey(nodeHash: nodeHash) - contentKey = ContentKey( - contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey - ) - contentId = toContentId(contentKey.encode()) - value = AccountTrieNodeRetrieval(node: TrieNode.init(v)) - - discard proto2.contentDB.put( - contentId, value.encode(), proto2.portalProtocol.localNode.id - ) - # Not needed right now as 1 node is enough considering node 1 is connected - # to both. - discard proto3.contentDB.put( - contentId, value.encode(), proto3.portalProtocol.localNode.id - ) - - # Get first key - var nodeHash: NodeHash - let firstKey = keys[0] - copyMem(nodeHash.data.addr, unsafeAddr firstKey[0], sizeof(nodeHash.data)) - - let - accountTrieNodeKey = AccountTrieNodeKey(nodeHash: nodeHash) - contentKey = - ContentKey(contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey) - - let accTrieNode = await proto1.getAccountTrieNode(accountTrieNodeKey) - check accTrieNode.isSome() - - let hash = keccakHash(accTrieNode.get().node.asSeq()) - check hash.data == firstKey - - proto1.stop() - proto2.stop() - await node1.closeWait() - await node2.closeWait() - await node3.closeWait() diff --git a/fluffy/tests/state_network_tests/test_state_network_getcontent_vectors.nim b/fluffy/tests/state_network_tests/test_state_network_getcontent_vectors.nim new file mode 100644 index 000000000..938040c37 --- /dev/null +++ b/fluffy/tests/state_network_tests/test_state_network_getcontent_vectors.nim @@ -0,0 +1,350 @@ +# Fluffy +# Copyright (c) 2021-2024 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import + std/os, + testutils/unittests, + chronos, + stew/byteutils, + eth/p2p/discoveryv5/protocol as discv5_protocol, + eth/p2p/discoveryv5/routing_table, + ../../network/wire/[portal_protocol, portal_stream], + ../../network/state/[state_content, state_network], + ../../database/content_db, + ./state_test_helpers + +procSuite "State Network - Get Content": + const STATE_NODE1_PORT = 20402 + const STATE_NODE2_PORT = 20403 + + let rng = newRng() + + # Single state instance tests + + asyncTest "Single state instance - Get existing account trie node": + const file = testVectorDir / "account_trie_node.yaml" + + let + testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + + stateNode1.start() + + for testData in testCase: + let + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_retrieval.hexToSeqByte() + expectedContentValue = AccountTrieNodeRetrieval.decode(contentValueBytes).get() + + stateNode1.portalProtocol().storeContent( + contentKeyBytes, contentId, contentValueBytes + ) + + let res = + await stateNode1.stateNetwork.getAccountTrieNode(contentKey.accountTrieNodeKey) + check: + res.isOk() + res.get() == expectedContentValue + res.get().node == expectedContentValue.node + + await stateNode1.stop() + + asyncTest "Single state instance - Get missing account trie node": + const file = testVectorDir / "account_trie_node.yaml" + + let + testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + + stateNode1.start() + + for testData in testCase: + let + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + + let res = + await stateNode1.stateNetwork.getAccountTrieNode(contentKey.accountTrieNodeKey) + check: + res.isNone() + + await stateNode1.stop() + + asyncTest "Single state instance - Get existing contract trie node": + const file = testVectorDir / "contract_storage_trie_node.yaml" + + let + testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + + stateNode1.start() + + for testData in testCase: + let + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_retrieval.hexToSeqByte() + expectedContentValue = ContractTrieNodeRetrieval.decode(contentValueBytes).get() + + stateNode1.portalProtocol().storeContent( + contentKeyBytes, contentId, contentValueBytes + ) + + let res = await stateNode1.stateNetwork.getContractTrieNode( + contentKey.contractTrieNodeKey + ) + check: + res.isOk() + res.get() == expectedContentValue + res.get().node == expectedContentValue.node + + await stateNode1.stop() + + asyncTest "Single state instance - Get missing contract trie node": + const file = testVectorDir / "contract_storage_trie_node.yaml" + + let + testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + + stateNode1.start() + + for testData in testCase: + let + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + + let res = await stateNode1.stateNetwork.getContractTrieNode( + contentKey.contractTrieNodeKey + ) + check: + res.isNone() + + await stateNode1.stop() + + asyncTest "Single state instance - Get existing contract bytecode": + const file = testVectorDir / "contract_bytecode.yaml" + + let + testCase = YamlContractBytecodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + + stateNode1.start() + + for testData in testCase: + let + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_retrieval.hexToSeqByte() + expectedContentValue = ContractCodeRetrieval.decode(contentValueBytes).get() + + stateNode1.portalProtocol().storeContent( + contentKeyBytes, contentId, contentValueBytes + ) + + let res = + await stateNode1.stateNetwork.getContractCode(contentKey.contractCodeKey) + check: + res.isOk() + res.get() == expectedContentValue + res.get().code == expectedContentValue.code + + await stateNode1.stop() + + asyncTest "Single state instance - Get missing contract bytecode": + const file = testVectorDir / "contract_bytecode.yaml" + + let + testCase = YamlContractBytecodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + + stateNode1.start() + + for testData in testCase: + let + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + + let res = + await stateNode1.stateNetwork.getContractCode(contentKey.contractCodeKey) + check: + res.isNone() + + await stateNode1.stop() + + # Two state instances tests + + asyncTest "Two state instances - Get existing account trie node": + const file = testVectorDir / "account_trie_node.yaml" + + let + testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + stateNode2.portalProtocol().addNode(stateNode1.localNode()) == Added + + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + (await stateNode2.portalProtocol().ping(stateNode1.localNode())).isOk() + + for testData in testCase: + let + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_retrieval.hexToSeqByte() + expectedContentValue = AccountTrieNodeRetrieval.decode(contentValueBytes).get() + + # only store the content in the first state instance + stateNode1.portalProtocol().storeContent( + contentKeyBytes, contentId, contentValueBytes + ) + check: + stateNode1.containsId(contentId) + not stateNode2.containsId(contentId) + + let + res1 = await stateNode1.stateNetwork.getAccountTrieNode( + contentKey.accountTrieNodeKey + ) + res2 = await stateNode2.stateNetwork.getAccountTrieNode( + contentKey.accountTrieNodeKey + ) + check: + stateNode1.containsId(contentId) + stateNode2.containsId(contentId) + res1.isOk() + res1.get() == expectedContentValue + res1.get().node == expectedContentValue.node + res2.isOk() + res2.get() == expectedContentValue + res2.get().node == expectedContentValue.node + + await stateNode1.stop() + await stateNode2.stop() + + asyncTest "Two state instances - Get existing contract trie node": + const file = testVectorDir / "contract_storage_trie_node.yaml" + + let + testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + stateNode2.portalProtocol().addNode(stateNode1.localNode()) == Added + + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + (await stateNode2.portalProtocol().ping(stateNode1.localNode())).isOk() + + for testData in testCase: + let + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_retrieval.hexToSeqByte() + expectedContentValue = ContractTrieNodeRetrieval.decode(contentValueBytes).get() + + # only store the content in the first state instance + stateNode1.portalProtocol().storeContent( + contentKeyBytes, contentId, contentValueBytes + ) + check: + stateNode1.containsId(contentId) + not stateNode2.containsId(contentId) + + let + res1 = await stateNode1.stateNetwork.getContractTrieNode( + contentKey.contractTrieNodeKey + ) + res2 = await stateNode2.stateNetwork.getContractTrieNode( + contentKey.contractTrieNodeKey + ) + check: + stateNode1.containsId(contentId) + stateNode2.containsId(contentId) + res1.isOk() + res1.get() == expectedContentValue + res1.get().node == expectedContentValue.node + res2.isOk() + res2.get() == expectedContentValue + res2.get().node == expectedContentValue.node + + await stateNode1.stop() + await stateNode2.stop() + + asyncTest "Two state instances - Get existing contract bytecode": + const file = testVectorDir / "contract_bytecode.yaml" + + let + testCase = YamlContractBytecodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + stateNode2.portalProtocol().addNode(stateNode1.localNode()) == Added + + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + (await stateNode2.portalProtocol().ping(stateNode1.localNode())).isOk() + + for testData in testCase: + let + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_retrieval.hexToSeqByte() + expectedContentValue = ContractCodeRetrieval.decode(contentValueBytes).get() + + # only store the content in the first state instance + stateNode1.portalProtocol().storeContent( + contentKeyBytes, contentId, contentValueBytes + ) + check: + stateNode1.containsId(contentId) + not stateNode2.containsId(contentId) + + let + res1 = await stateNode1.stateNetwork.getContractCode(contentKey.contractCodeKey) + res2 = await stateNode2.stateNetwork.getContractCode(contentKey.contractCodeKey) + check: + stateNode1.containsId(contentId) + stateNode2.containsId(contentId) + res1.isOk() + res1.get() == expectedContentValue + res1.get().code == expectedContentValue.code + res2.isOk() + res2.get() == expectedContentValue + res2.get().code == expectedContentValue.code + + await stateNode1.stop() + await stateNode2.stop() diff --git a/fluffy/tests/state_network_tests/test_state_network_gossip.nim b/fluffy/tests/state_network_tests/test_state_network_gossip.nim deleted file mode 100644 index 59c640ed2..000000000 --- a/fluffy/tests/state_network_tests/test_state_network_gossip.nim +++ /dev/null @@ -1,118 +0,0 @@ -# Fluffy -# Copyright (c) 2023-2024 Status Research & Development GmbH -# Licensed and distributed under either of -# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). -# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). -# at your option. This file may not be copied, modified, or distributed except according to those terms. - -import - std/os, - chronos, - testutils/unittests, - stew/[byteutils, results], - eth/p2p/discoveryv5/protocol as discv5_protocol, - ../../network/wire/[portal_protocol, portal_stream], - ../../network/history/[history_content, history_network], - ../../network/state/[state_network, state_gossip], - ../../database/content_db, - .././test_helpers, - ../../eth_data/yaml_utils - -const testVectorDir = "./vendor/portal-spec-tests/tests/mainnet/state/validation/" - -procSuite "State Network Gossip": - let rng = newRng() - - asyncTest "Test Gossip of Account Trie Node Offer": - const file = testVectorDir / "recursive_gossip.yaml" - - type YamlRecursiveGossipKV = object - content_key: string - content_value: string - - type YamlRecursiveGossipData = object - state_root: string - recursive_gossip: seq[YamlRecursiveGossipKV] - - type YamlRecursiveGossipKVs = seq[YamlRecursiveGossipData] - - let - testCase = YamlRecursiveGossipKVs.loadFromYaml(file).valueOr: - raiseAssert "Cannot read test vector: " & error - recursiveGossipSteps = testCase[0].recursive_gossip - numOfClients = recursiveGossipSteps.len() - 1 - - var clients: seq[StateNetwork] - - for i in 0 .. numOfClients: - let - node = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20400 + i)) - db = ContentDB.new("", uint32.high, inMemory = true) - sm = StreamManager.new(node) - hn = HistoryNetwork.new(node, db, sm, FinishedAccumulator()) - proto = StateNetwork.new(node, db, sm, historyNetwork = Opt.some(hn)) - proto.start() - clients.add(proto) - - for i in 0 .. numOfClients - 1: - let - currentNode = clients[i] - nextNode = clients[i + 1] - - check: - currentNode.portalProtocol.addNode(nextNode.portalProtocol.localNode) == Added - (await currentNode.portalProtocol.ping(nextNode.portalProtocol.localNode)).isOk() - - let - blockHeader = BlockHeader( - stateRoot: Hash256.fromHex( - "0x1ad7b80af0c28bc1489513346d2706885be90abb07f23ca28e50482adb392d61" - ) - ) - headerRlp = rlp.encode(blockHeader) - blockHeaderWithProof = BlockHeaderWithProof( - header: ByteList.init(headerRlp), proof: BlockHeaderProof.init() - ) - value = recursiveGossipSteps[0].content_value.hexToSeqByte() - decodedValue = AccountTrieNodeOffer.decode(value).get() - contentKey = history_content.ContentKey - .init(history_content.ContentType.blockHeader, decodedValue.blockHash) - .encode() - contentId = history_content.toContentId(contentKey) - - clients[i].contentDB.put(contentId, SSZ.encode(blockHeaderWithProof)) - - for i in 0 .. numOfClients - 1: - let - pair = recursiveGossipSteps[i] - currentNode = clients[i] - nextNode = clients[i + 1] - - key = ByteList.init(pair.content_key.hexToSeqByte()) - decodedKey = state_content.ContentKey.decode(key).valueOr: - raiseAssert "Cannot decode key" - - nextKey = ByteList.init(recursiveGossipSteps[1].content_key.hexToSeqByte()) - decodedNextKey = state_content.ContentKey.decode(nextKey).valueOr: - raiseAssert "Cannot decode key" - - value = pair.content_value.hexToSeqByte() - decodedValue = AccountTrieNodeOffer.decode(value).get() - nextValue = recursiveGossipSteps[1].content_value.hexToSeqByte() - nextDecodedValue = AccountTrieNodeOffer.decode(nextValue).get() - nextRetrievalValue = nextDecodedValue.toRetrievalValue() - - if i == 0: - await currentNode.portalProtocol.gossipOffer( - Opt.none(NodeId), key, value, decodedKey.accountTrieNodeKey, decodedValue - ) - - await sleepAsync(100.milliseconds) #TODO figure out how to get rid of this sleep - - check (await nextNode.getAccountTrieNode(decodedNextKey.accountTrieNodeKey)) == - Opt.some(nextRetrievalValue) - - for i in 0 .. numOfClients: - await clients[i].portalProtocol.baseProtocol.closeWait() - - # TODO Add tests for Contract Trie Node Offer & Contract Code Offer diff --git a/fluffy/tests/state_network_tests/test_state_network_offercontent_vectors.nim b/fluffy/tests/state_network_tests/test_state_network_offercontent_vectors.nim new file mode 100644 index 000000000..800279948 --- /dev/null +++ b/fluffy/tests/state_network_tests/test_state_network_offercontent_vectors.nim @@ -0,0 +1,390 @@ +# Fluffy +# Copyright (c) 2021-2024 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import + std/os, + testutils/unittests, + chronos, + stew/byteutils, + eth/common, + eth/p2p/discoveryv5/protocol as discv5_protocol, + eth/p2p/discoveryv5/routing_table, + ../../common/common_utils, + ../../network/wire/[portal_protocol, portal_stream], + ../../network/state/[state_content, state_network], + ../../database/content_db, + ./state_test_helpers + +procSuite "State Network - Offer Content": + const + STATE_NODE1_PORT = 20502 + STATE_NODE2_PORT = 20503 + + let rng = newRng() + + # Single state instance tests + + asyncTest "Single state instance - Offer account trie nodes": + const file = testVectorDir / "account_trie_node.yaml" + + let testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + + for testData in testCase: + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_offer.hexToSeqByte() + contentValue = AccountTrieNodeOffer.decode(contentValueBytes).get() + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + + stateNode1.start() + + # no state root yet + check ( + await stateNode1.stateNetwork.processOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.accountTrieNodeKey, + AccountTrieNodeOffer, + ) + ).isErr() + + # set bad state root + let badStateRoot = KeccakHash.fromBytes( + "0xBAD7b80af0c28bc1489513346d2706885be90abb07f23ca28e50482adb392d61".hexToSeqByte() + ) + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, badStateRoot) + check ( + await stateNode1.stateNetwork.processOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.accountTrieNodeKey, + AccountTrieNodeOffer, + ) + ).isErr() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode1.containsId(contentId) + + let processRes = await stateNode1.stateNetwork.processOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.accountTrieNodeKey, + AccountTrieNodeOffer, + ) + check processRes.isOk() + + let getRes = + await stateNode1.stateNetwork.getAccountTrieNode(contentKey.accountTrieNodeKey) + check: + stateNode1.containsId(contentId) + getRes.isOk() + getRes.get() == contentValue.toRetrievalValue() + getRes.get().node == contentValue.toRetrievalValue().node + + await stateNode1.stop() + + asyncTest "Single state instance - Offer contract trie nodes": + const file = testVectorDir / "contract_storage_trie_node.yaml" + + let testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + + for testData in testCase: + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_offer.hexToSeqByte() + contentValue = ContractTrieNodeOffer.decode(contentValueBytes).get() + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + + stateNode1.start() + + # no state root yet + check ( + await stateNode1.stateNetwork.processOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.contractTrieNodeKey, + ContractTrieNodeOffer, + ) + ).isErr() + + # set bad state root + let badStateRoot = KeccakHash.fromBytes( + "0xBAD7b80af0c28bc1489513346d2706885be90abb07f23ca28e50482adb392d61".hexToSeqByte() + ) + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, badStateRoot) + check ( + await stateNode1.stateNetwork.processOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.contractTrieNodeKey, + ContractTrieNodeOffer, + ) + ).isErr() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode1.containsId(contentId) + + let processRes = await stateNode1.stateNetwork.processOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.contractTrieNodeKey, + ContractTrieNodeOffer, + ) + check processRes.isOk() + + let getRes = await stateNode1.stateNetwork.getContractTrieNode( + contentKey.contractTrieNodeKey + ) + check: + stateNode1.containsId(contentId) + getRes.isOk() + getRes.get() == contentValue.toRetrievalValue() + getRes.get().node == contentValue.toRetrievalValue().node + + await stateNode1.stop() + + asyncTest "Single state instance - Offer contract bytecode": + const file = testVectorDir / "contract_bytecode.yaml" + + let testCase = YamlContractBytecodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + + for testData in testCase: + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_offer.hexToSeqByte() + contentValue = ContractCodeOffer.decode(contentValueBytes).get() + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + + stateNode1.start() + + # no state root yet + check ( + await stateNode1.stateNetwork.processOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.contractCodeKey, + ContractCodeOffer, + ) + ).isErr() + + # set bad state root + let badStateRoot = KeccakHash.fromBytes( + "0xBAD7b80af0c28bc1489513346d2706885be90abb07f23ca28e50482adb392d61".hexToSeqByte() + ) + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, badStateRoot) + check ( + await stateNode1.stateNetwork.processOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.contractCodeKey, + ContractCodeOffer, + ) + ).isErr() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode1.containsId(contentId) + + let processRes = await stateNode1.stateNetwork.processOffer( + Opt.none(NodeId), + contentKeyBytes, + contentValueBytes, + contentKey.contractCodeKey, + ContractCodeOffer, + ) + check processRes.isOk() + + let getRes = + await stateNode1.stateNetwork.getContractCode(contentKey.contractCodeKey) + check: + stateNode1.containsId(contentId) + getRes.isOk() + getRes.get() == contentValue.toRetrievalValue() + getRes.get().code == contentValue.toRetrievalValue().code + + await stateNode1.stop() + + # Two state instances tests - State node 1 offers content to state node 2 + + asyncTest "Two state instances - Offer account trie nodes": + const file = testVectorDir / "account_trie_node.yaml" + + let testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + + for testData in testCase: + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_offer.hexToSeqByte() + contentValue = AccountTrieNodeOffer.decode(contentValueBytes).get() + contentKV = ContentKV(contentKey: contentKeyBytes, content: contentValueBytes) + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + stateNode2.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode2.containsId(contentId) + + let offerResult = + await stateNode1.portalProtocol.offer(stateNode2.localNode(), @[contentKV]) + check offerResult.isOk() + + # wait for offer to be processed by state node 2 + while not stateNode2.stateNetwork.contentQueue.empty(): + await sleepAsync(1.milliseconds) + await sleepAsync(100.milliseconds) + + let getRes = + await stateNode2.stateNetwork.getAccountTrieNode(contentKey.accountTrieNodeKey) + check: + stateNode2.containsId(contentId) + getRes.isOk() + getRes.get() == contentValue.toRetrievalValue() + getRes.get().node == contentValue.toRetrievalValue().node + + await stateNode1.stop() + await stateNode2.stop() + + asyncTest "Two state instances - Offer contract trie nodes": + const file = testVectorDir / "contract_storage_trie_node.yaml" + + let testCase = YamlTrieNodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + + for testData in testCase: + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_offer.hexToSeqByte() + contentValue = ContractTrieNodeOffer.decode(contentValueBytes).get() + contentKV = ContentKV(contentKey: contentKeyBytes, content: contentValueBytes) + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + stateNode2.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode2.containsId(contentId) + + let offerResult = + await stateNode1.portalProtocol.offer(stateNode2.localNode(), @[contentKV]) + check offerResult.isOk() + + # wait for offer to be processed by state node 2 + while not stateNode2.stateNetwork.contentQueue.empty(): + await sleepAsync(1.milliseconds) + await sleepAsync(100.milliseconds) + + let getRes = await stateNode2.stateNetwork.getContractTrieNode( + contentKey.contractTrieNodeKey + ) + check: + stateNode2.containsId(contentId) + getRes.isOk() + getRes.get() == contentValue.toRetrievalValue() + getRes.get().node == contentValue.toRetrievalValue().node + + await stateNode1.stop() + await stateNode2.stop() + + asyncTest "Two state instances - Offer contract bytecode": + const file = testVectorDir / "contract_bytecode.yaml" + + let testCase = YamlContractBytecodeKVs.loadFromYaml(file).valueOr: + raiseAssert "Cannot read test vector: " & error + + for testData in testCase: + let + stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) + contentKeyBytes = testData.content_key.hexToSeqByte().ByteList + contentKey = ContentKey.decode(contentKeyBytes).get() + contentId = toContentId(contentKeyBytes) + contentValueBytes = testData.content_value_offer.hexToSeqByte() + contentValue = ContractCodeOffer.decode(contentValueBytes).get() + contentKV = ContentKV(contentKey: contentKeyBytes, content: contentValueBytes) + stateNode1 = newStateNode(rng, STATE_NODE1_PORT) + stateNode2 = newStateNode(rng, STATE_NODE2_PORT) + + stateNode1.start() + stateNode2.start() + + check: + stateNode1.portalProtocol().addNode(stateNode2.localNode()) == Added + (await stateNode1.portalProtocol().ping(stateNode2.localNode())).isOk() + + # set valid state root + stateNode1.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + stateNode2.mockBlockHashToStateRoot(contentValue.blockHash, stateRoot) + + check not stateNode2.containsId(contentId) + + let offerResult = + await stateNode1.portalProtocol.offer(stateNode2.localNode(), @[contentKV]) + check offerResult.isOk() + + # wait for offer to be processed by state node 2 + while not stateNode2.stateNetwork.contentQueue.empty(): + await sleepAsync(1.milliseconds) + await sleepAsync(100.milliseconds) + + let getRes = + await stateNode2.stateNetwork.getContractCode(contentKey.contractCodeKey) + check: + stateNode2.containsId(contentId) + getRes.isOk() + getRes.get() == contentValue.toRetrievalValue() + getRes.get().code == contentValue.toRetrievalValue().code + + await stateNode1.stop() + await stateNode2.stop()