diff --git a/fluffy/common/common_utils.nim b/fluffy/common/common_utils.nim index 134bd03ac..77ffbd95c 100644 --- a/fluffy/common/common_utils.nim +++ b/fluffy/common/common_utils.nim @@ -14,7 +14,7 @@ import stew/[io2, arrayops], eth/p2p/discoveryv5/enr -func init*(T: type KeccakHash, hash: openArray[byte]): T = +func fromBytes*(T: type KeccakHash, hash: openArray[byte]): T = doAssert(hash.len() == 32) KeccakHash(data: array[32, byte].initCopyFrom(hash)) diff --git a/fluffy/network/state/content/content_keys.nim b/fluffy/network/state/content/content_keys.nim new file mode 100644 index 000000000..e9bf29888 --- /dev/null +++ b/fluffy/network/state/content/content_keys.nim @@ -0,0 +1,112 @@ +# 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. + +# As per spec: +# https://github.com/ethereum/portal-network-specs/blob/master/state-network.md#content-keys-and-content-ids + +{.push raises: [].} + +import + nimcrypto/[hash, sha2, keccak], + results, + stint, + eth/common/eth_types, + ssz_serialization, + ./nibbles + +export ssz_serialization, common_types, hash, results + +type + NodeHash* = KeccakHash + CodeHash* = KeccakHash + Address* = EthAddress + + ContentType* = enum + # Note: Need to add this unused value as a case object with an enum without + # a 0 valueis not allowed: "low(contentType) must be 0 for discriminant". + # For prefix values that are in the enum gap, the deserialization will fail + # at runtime as is wanted. + # In the future it might be possible that this will fail at compile time for + # the SSZ Union type, but currently it is allowed in the implementation, and + # the SSZ spec is not explicit about disallowing this. + unused = 0x00 + accountTrieNode = 0x20 + contractTrieNode = 0x21 + contractCode = 0x22 + + AccountTrieNodeKey* = object + path*: Nibbles + nodeHash*: NodeHash + + ContractTrieNodeKey* = object + address*: Address + path*: Nibbles + nodeHash*: NodeHash + + ContractCodeKey* = object + address*: Address + codeHash*: CodeHash + + ContentKey* = object + case contentType*: ContentType + of unused: + discard + of accountTrieNode: + accountTrieNodeKey*: AccountTrieNodeKey + of contractTrieNode: + contractTrieNodeKey*: ContractTrieNodeKey + of contractCode: + contractCodeKey*: ContractCodeKey + +func init*(T: type AccountTrieNodeKey, path: Nibbles, nodeHash: NodeHash): T = + AccountTrieNodeKey(path: path, nodeHash: nodeHash) + +func init*( + T: type ContractTrieNodeKey, address: Address, path: Nibbles, nodeHash: NodeHash +): T = + ContractTrieNodeKey(address: address, path: path, nodeHash: nodeHash) + +func init*(T: type ContractCodeKey, address: Address, codeHash: CodeHash): T = + ContractCodeKey(address: address, codeHash: codeHash) + +func initAccountTrieNodeKey*(path: Nibbles, nodeHash: NodeHash): ContentKey = + ContentKey( + contentType: accountTrieNode, + accountTrieNodeKey: AccountTrieNodeKey.init(path, nodeHash), + ) + +func initContractTrieNodeKey*( + address: Address, path: Nibbles, nodeHash: NodeHash +): ContentKey = + ContentKey( + contentType: contractTrieNode, + contractTrieNodeKey: ContractTrieNodeKey.init(address, path, nodeHash), + ) + +func initContractCodeKey*(address: Address, codeHash: CodeHash): ContentKey = + ContentKey( + contentType: contractCode, contractCodeKey: ContractCodeKey.init(address, codeHash) + ) + +proc readSszBytes*(data: openArray[byte], val: var ContentKey) {.raises: [SszError].} = + mixin readSszValue + if data.len() > 0 and data[0] == ord(unused): + raise newException(MalformedSszError, "SSZ selector is unused value") + + readSszValue(data, val) + +func encode*(contentKey: ContentKey): ByteList = + doAssert(contentKey.contentType != unused) + ByteList.init(SSZ.encode(contentKey)) + +func decode*(T: type ContentKey, contentKey: ByteList): Result[T, string] = + decodeSsz(contentKey.asSeq(), T) + +func toContentId*(contentKey: ByteList): ContentId = + # TODO: Should we try to parse the content key here for invalid ones? + let idHash = sha256.digest(contentKey.asSeq()) + readUintBE[256](idHash.data) diff --git a/fluffy/network/state/content/content_values.nim b/fluffy/network/state/content/content_values.nim new file mode 100644 index 000000000..5652cfe79 --- /dev/null +++ b/fluffy/network/state/content/content_values.nim @@ -0,0 +1,97 @@ +# 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. + +# As per spec: +# https://github.com/ethereum/portal-network-specs/blob/master/state-network.md#content-keys-and-content-ids + +{.push raises: [].} + +import results, eth/common/eth_types, ssz_serialization, ../../../common/common_types + +export ssz_serialization, common_types, hash, results + +const + MAX_TRIE_NODE_LEN = 1024 + MAX_TRIE_PROOF_LEN = 65 + MAX_BYTECODE_LEN = 32768 + +type + TrieNode* = List[byte, MAX_TRIE_NODE_LEN] + TrieProof* = List[TrieNode, MAX_TRIE_PROOF_LEN] + Bytecode* = List[byte, MAX_BYTECODE_LEN] + + AccountTrieNodeOffer* = object + proof*: TrieProof + blockHash*: BlockHash + + AccountTrieNodeRetrieval* = object + node*: TrieNode + + ContractTrieNodeOffer* = object + storageProof*: TrieProof + accountProof*: TrieProof + blockHash*: BlockHash + + ContractTrieNodeRetrieval* = object + node*: TrieNode + + ContractCodeOffer* = object + code*: Bytecode + accountProof*: TrieProof + blockHash*: BlockHash + + ContractCodeRetrieval* = object + code*: Bytecode + + ContentValue* = + AccountTrieNodeOffer | ContractTrieNodeOffer | ContractCodeOffer | + AccountTrieNodeRetrieval | ContractTrieNodeRetrieval | ContractCodeRetrieval + +func init*(T: type AccountTrieNodeOffer, proof: TrieProof, blockHash: BlockHash): T = + AccountTrieNodeOffer(proof: proof, blockHash: blockHash) + +func init*(T: type AccountTrieNodeRetrieval, node: TrieNode): T = + AccountTrieNodeRetrieval(node: node) + +func init*( + T: type ContractTrieNodeOffer, + storageProof: TrieProof, + accountProof: TrieProof, + blockHash: BlockHash, +): T = + ContractTrieNodeOffer( + storageProof: storageProof, accountProof: accountProof, blockHash: blockHash + ) + +func init*(T: type ContractTrieNodeRetrieval, node: TrieNode): T = + ContractTrieNodeRetrieval(node: node) + +func init*( + T: type ContractCodeOffer, + code: Bytecode, + accountProof: TrieProof, + blockHash: BlockHash, +): T = + ContractCodeOffer(code: code, accountProof: accountProof, blockHash: blockHash) + +func init*(T: type ContractCodeRetrieval, code: Bytecode): T = + ContractCodeRetrieval(code: code) + +func toRetrievalValue*(offer: AccountTrieNodeOffer): AccountTrieNodeRetrieval = + AccountTrieNodeRetrieval.init(offer.proof[^1]) + +func toRetrievalValue*(offer: ContractTrieNodeOffer): ContractTrieNodeRetrieval = + ContractTrieNodeRetrieval.init(offer.storageProof[^1]) + +func toRetrievalValue*(offer: ContractCodeOffer): ContractCodeRetrieval = + ContractCodeRetrieval.init(offer.code) + +func encode*(value: ContentValue): seq[byte] = + SSZ.encode(value) + +func decode*(T: type ContentValue, bytes: openArray[byte]): Result[T, string] = + decodeSsz(bytes, T) diff --git a/fluffy/network/state/content/nibbles.nim b/fluffy/network/state/content/nibbles.nim new file mode 100644 index 000000000..546bd517e --- /dev/null +++ b/fluffy/network/state/content/nibbles.nim @@ -0,0 +1,101 @@ +# 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. + +# As per spec: +# https://github.com/ethereum/portal-network-specs/blob/master/state-network.md#content-keys-and-content-ids + +{.push raises: [].} + +import + nimcrypto/hash, + results, + stint, + eth/common/eth_types, + ssz_serialization, + ../../../common/common_types + +export ssz_serialization, common_types, hash, results + +const + MAX_PACKED_NIBBLES_LEN = 33 + MAX_UNPACKED_NIBBLES_LEN = 64 + +type Nibbles* = List[byte, MAX_PACKED_NIBBLES_LEN] + +func init*(T: type Nibbles, packed: openArray[byte], isEven: bool): T = + doAssert(packed.len() <= MAX_PACKED_NIBBLES_LEN) + + var output = newSeqOfCap[byte](packed.len() + 1) + if isEven: + output.add(0x00) + else: + doAssert(packed.len() > 0) + # set the first nibble to 1 and copy the second nibble from the input + output.add((packed[0] and 0x0F) or 0x10) + + let startIdx = if isEven: 0 else: 1 + for i in startIdx ..< packed.len(): + output.add(packed[i]) + + Nibbles(output) + +func encode*(nibbles: Nibbles): seq[byte] = + SSZ.encode(nibbles) + +func decode*(T: type Nibbles, bytes: openArray[byte]): Result[T, string] = + decodeSsz(bytes, T) + +func packNibbles*(unpacked: openArray[byte]): Nibbles = + doAssert( + unpacked.len() <= MAX_UNPACKED_NIBBLES_LEN, "Can't pack more than 64 nibbles" + ) + + if unpacked.len() == 0: + return Nibbles(@[byte(0x00)]) + + let isEvenLength = unpacked.len() mod 2 == 0 + + var + output = newSeqOfCap[byte](unpacked.len() div 2 + 1) + highNibble = isEvenLength + currentByte: byte = 0 + + if isEvenLength: + output.add(0x00) + else: + currentByte = 0x10 + + for i, nibble in unpacked: + if highNibble: + currentByte = nibble shl 4 + else: + output.add(currentByte or nibble) + currentByte = 0 + highNibble = not highNibble + + Nibbles(output) + +func unpackNibbles*(packed: Nibbles): seq[byte] = + doAssert(packed.len() <= MAX_PACKED_NIBBLES_LEN, "Packed nibbles length is too long") + + var output = newSeqOfCap[byte](packed.len() * 2) + + for i, pair in packed: + if i == 0 and pair == 0x00: + continue + + let + first = (pair and 0xF0) shr 4 + second = pair and 0x0F + + if i == 0 and first == 0x01: + output.add(second) + else: + output.add(first) + output.add(second) + + output diff --git a/fluffy/network/state/state_content.nim b/fluffy/network/state/state_content.nim index e36c657b9..b27446a68 100644 --- a/fluffy/network/state/state_content.nim +++ b/fluffy/network/state/state_content.nim @@ -5,273 +5,6 @@ # * 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. -# As per spec: -# https://github.com/ethereum/portal-network-specs/blob/master/state-network.md#content-keys-and-content-ids +import ./content/content_keys, ./content/content_values, ./content/nibbles -{.push raises: [].} - -import - nimcrypto/[hash, sha2, keccak], - results, - stint, - eth/common/eth_types, - ssz_serialization, - ../../common/common_types - -export ssz_serialization, common_types, hash, results - -const - MAX_PACKED_NIBBLES_LEN = 33 - MAX_UNPACKED_NIBBLES_LEN = 64 - - MAX_TRIE_NODE_LEN = 1024 - MAX_TRIE_PROOF_LEN = 65 - MAX_BYTECODE_LEN = 32768 - -type - NodeHash* = KeccakHash - CodeHash* = KeccakHash - Address* = EthAddress - - ContentType* = enum - # Note: Need to add this unused value as a case object with an enum without - # a 0 valueis not allowed: "low(contentType) must be 0 for discriminant". - # For prefix values that are in the enum gap, the deserialization will fail - # at runtime as is wanted. - # In the future it might be possible that this will fail at compile time for - # the SSZ Union type, but currently it is allowed in the implementation, and - # the SSZ spec is not explicit about disallowing this. - unused = 0x00 - accountTrieNode = 0x20 - contractTrieNode = 0x21 - contractCode = 0x22 - - Nibbles* = List[byte, MAX_PACKED_NIBBLES_LEN] - - TrieNode* = List[byte, MAX_TRIE_NODE_LEN] - TrieProof* = List[TrieNode, MAX_TRIE_PROOF_LEN] - - Bytecode* = List[byte, MAX_BYTECODE_LEN] - - AccountTrieNodeKey* = object - path*: Nibbles - nodeHash*: NodeHash - - ContractTrieNodeKey* = object - address*: Address - path*: Nibbles - nodeHash*: NodeHash - - ContractCodeKey* = object - address*: Address - codeHash*: CodeHash - - ContentKey* = object - case contentType*: ContentType - of unused: - discard - of accountTrieNode: - accountTrieNodeKey*: AccountTrieNodeKey - of contractTrieNode: - contractTrieNodeKey*: ContractTrieNodeKey - of contractCode: - contractCodeKey*: ContractCodeKey - - AccountTrieNodeOffer* = object - proof*: TrieProof - blockHash*: BlockHash - - AccountTrieNodeRetrieval* = object - node*: TrieNode - - ContractTrieNodeOffer* = object - storageProof*: TrieProof - accountProof*: TrieProof - blockHash*: BlockHash - - ContractTrieNodeRetrieval* = object - node*: TrieNode - - ContractCodeOffer* = object - code*: Bytecode - accountProof*: TrieProof - blockHash*: BlockHash - - ContractCodeRetrieval* = object - code*: Bytecode - - OfferContentValueType* = enum - accountTrieNodeOffer - contractTrieNodeOffer - contractCodeOffer - - OfferContentValue* = object - case contentType*: ContentType - of unused: - discard - of accountTrieNode: - accountTrieNode*: AccountTrieNodeOffer - of contractTrieNode: - contractTrieNode*: ContractTrieNodeOffer - of contractCode: - contractCode*: ContractCodeOffer - - RetrievalContentValue* = object - case contentType*: ContentType - of unused: - discard - of accountTrieNode: - accountTrieNode*: AccountTrieNodeRetrieval - of contractTrieNode: - contractTrieNode*: ContractTrieNodeRetrieval - of contractCode: - contractCode*: ContractCodeRetrieval - -func encode*(contentKey: ContentKey): ByteList = - doAssert(contentKey.contentType != unused) - ByteList.init(SSZ.encode(contentKey)) - -proc readSszBytes*(data: openArray[byte], val: var ContentKey) {.raises: [SszError].} = - mixin readSszValue - if data.len() > 0 and data[0] == ord(unused): - raise newException(MalformedSszError, "SSZ selector is unused value") - - readSszValue(data, val) - -func decode*(contentKey: ByteList): Opt[ContentKey] = - try: - Opt.some(SSZ.decode(contentKey.asSeq(), ContentKey)) - except SerializationError: - return Opt.none(ContentKey) - -func toContentId*(contentKey: ByteList): ContentId = - # TODO: Should we try to parse the content key here for invalid ones? - let idHash = sha2.sha256.digest(contentKey.asSeq()) - readUintBE[256](idHash.data) - -func toContentId*(contentKey: ContentKey): ContentId = - toContentId(encode(contentKey)) - -func initAccountTrieNodeKey*(path: Nibbles, nodeHash: NodeHash): ContentKey = - ContentKey( - contentType: accountTrieNode, - accountTrieNodeKey: AccountTrieNodeKey(path: path, nodeHash: nodeHash), - ) - -func initContractTrieNodeKey*( - address: Address, path: Nibbles, nodeHash: NodeHash -): ContentKey = - ContentKey( - contentType: contractTrieNode, - contractTrieNodeKey: - ContractTrieNodeKey(address: address, path: path, nodeHash: nodeHash), - ) - -func initContractCodeKey*(address: Address, codeHash: CodeHash): ContentKey = - ContentKey( - contentType: contractCode, - contractCodeKey: ContractCodeKey(address: address, codeHash: codeHash), - ) - -func offerContentToRetrievalContent*( - offerContent: OfferContentValue -): RetrievalContentValue = - case offerContent.contentType - of unused: - raiseAssert "Converting content with unused content type" - of accountTrieNode: - RetrievalContentValue( - contentType: accountTrieNode, - accountTrieNode: - AccountTrieNodeRetrieval(node: offerContent.accountTrieNode.proof[^1]), - ) # TODO implement properly - of contractTrieNode: - RetrievalContentValue( - contentType: contractTrieNode, - contractTrieNode: - ContractTrieNodeRetrieval(node: offerContent.contractTrieNode.storageProof[^1]), - ) # TODO implement properly - of contractCode: - RetrievalContentValue( - contentType: contractCode, - contractCode: ContractCodeRetrieval(code: offerContent.contractCode.code), - ) - -func encode*(content: RetrievalContentValue): seq[byte] = - case content.contentType - of unused: - raiseAssert "Encoding content with unused content type" - of accountTrieNode: - SSZ.encode(content.accountTrieNode) - of contractTrieNode: - SSZ.encode(content.contractTrieNode) - of contractCode: - SSZ.encode(content.contractCode) - -func init*(T: type Nibbles, packed: openArray[byte], isEven: bool): T = - doAssert(packed.len() <= MAX_PACKED_NIBBLES_LEN) - - var output = newSeqOfCap[byte](packed.len() + 1) - if isEven: - output.add(0x00) - else: - doAssert(packed.len() > 0) - # set the first nibble to 1 and copy the second nibble from the input - output.add((packed[0] and 0x0F) or 0x10) - - let startIdx = if isEven: 0 else: 1 - for i in startIdx ..< packed.len(): - output.add(packed[i]) - - Nibbles(output) - -func packNibbles*(unpacked: openArray[byte]): Nibbles = - doAssert( - unpacked.len() <= MAX_UNPACKED_NIBBLES_LEN, "Can't pack more than 64 nibbles" - ) - - if unpacked.len() == 0: - return Nibbles(@[byte(0x00)]) - - let isEvenLength = unpacked.len() mod 2 == 0 - - var - output = newSeqOfCap[byte](unpacked.len() div 2 + 1) - highNibble = isEvenLength - currentByte: byte = 0 - - if isEvenLength: - output.add(0x00) - else: - currentByte = 0x10 - - for i, nibble in unpacked: - if highNibble: - currentByte = nibble shl 4 - else: - output.add(currentByte or nibble) - currentByte = 0 - highNibble = not highNibble - - Nibbles(output) - -func unpackNibbles*(packed: Nibbles): seq[byte] = - doAssert(packed.len() <= MAX_PACKED_NIBBLES_LEN, "Packed nibbles length is too long") - - var output = newSeqOfCap[byte](packed.len() * 2) - - for i, pair in packed: - if i == 0 and pair == 0x00: - continue - - let - first = (pair and 0xF0) shr 4 - second = pair and 0x0F - - if i == 0 and first == 0x01: - output.add(second) - else: - output.add(first) - output.add(second) - - output +export content_keys, content_values, nibbles diff --git a/fluffy/network/state/state_gossip.nim b/fluffy/network/state/state_gossip.nim new file mode 100644 index 000000000..e4775ac69 --- /dev/null +++ b/fluffy/network/state/state_gossip.nim @@ -0,0 +1,97 @@ +# 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 + results, + chronos, + chronicles, + eth/common/eth_hash, + eth/common, + eth/p2p/discoveryv5/[protocol, enr], + ../../database/content_db, + ../history/history_network, + ../wire/[portal_protocol, portal_stream], + ./state_content + +export results + +logScope: + topics = "portal_state" + +proc gossipOffer*( + p: PortalProtocol, + maybeSrcNodeId: Opt[NodeId], + decodedKey: AccountTrieNodeKey, + decodedValue: AccountTrieNodeOffer, +): Future[void] {.async.} = + var + nibbles = decodedKey.path.unpackNibbles() + proof = decodedValue.proof + + # When nibbles is empty this means the root node was received. Recursive + # gossiping is finished. + if nibbles.len() == 0: + return + + # TODO: Review this logic. + # Removing a single nibble will not work for extension nodes with multiple prefix nibbles + discard nibbles.pop() + discard (distinctBase proof).pop() + let + updatedValue = AccountTrieNodeOffer(proof: proof, blockHash: decodedValue.blockHash) + updatedNodeHash = keccakHash(distinctBase proof[^1]) + encodedValue = SSZ.encode(updatedValue) + updatedKey = + AccountTrieNodeKey(path: nibbles.packNibbles(), nodeHash: updatedNodeHash) + encodedKey = + ContentKey(accountTrieNodeKey: updatedKey, contentType: accountTrieNode).encode() + + await p.neighborhoodGossipDiscardPeers( + maybeSrcNodeId, ContentKeysList.init(@[encodedKey]), @[encodedValue] + ) + +proc gossipOffer*( + p: PortalProtocol, + maybeSrcNodeId: Opt[NodeId], + decodedKey: ContractTrieNodeKey, + decodedValue: ContractTrieNodeOffer, +): Future[void] {.async.} = + # TODO: Recursive gossiping for contract trie nodes + return + +proc gossipOffer*( + p: PortalProtocol, + maybeSrcNodeId: Opt[NodeId], + decodedKey: ContractCodeKey, + decodedValue: ContractCodeOffer, +): Future[void] {.async.} = + # TODO: Recursive gossiping for bytecode? + return + +# proc gossipContent*( +# p: PortalProtocol, +# maybeSrcNodeId: Opt[NodeId], +# contentKey: ByteList, +# decodedKey: ContentKey, +# contentValue: seq[byte], +# decodedValue: OfferContentValue, +# ): Future[void] {.async.} = +# case decodedKey.contentType +# of unused: +# raiseAssert "Gossiping content with unused content type" +# of accountTrieNode: +# await recursiveGossipAccountTrieNode( +# p, maybeSrcNodeId, decodedKey, decodedValue.accountTrieNode +# ) +# of contractTrieNode: +# await recursiveGossipContractTrieNode( +# p, maybeSrcNodeId, decodedKey, decodedValue.contractTrieNode +# ) +# of contractCode: +# await p.neighborhoodGossipDiscardPeers( +# maybeSrcNodeId, ContentKeysList.init(@[contentKey]), @[contentValue] +# ) diff --git a/fluffy/network/state/state_network.nim b/fluffy/network/state/state_network.nim index c8c7594d2..433049766 100644 --- a/fluffy/network/state/state_network.nim +++ b/fluffy/network/state/state_network.nim @@ -16,7 +16,8 @@ import ../history/history_network, ../wire/[portal_protocol, portal_stream, portal_protocol_config], ./state_content, - ./state_validation + ./state_validation, + ./state_gossip export results @@ -35,228 +36,6 @@ type StateNetwork* = ref object func toContentIdHandler(contentKey: ByteList): results.Opt[ContentId] = ok(toContentId(contentKey)) -func decodeKV*( - contentKey: ByteList, contentValue: seq[byte] -): Opt[(ContentKey, OfferContentValue)] = - const empty = Opt.none((ContentKey, OfferContentValue)) - let - key = contentKey.decode().valueOr: - return empty - value = - case key.contentType - of unused: - return empty - of accountTrieNode: - let val = decodeSsz(contentValue, AccountTrieNodeOffer).valueOr: - return empty - OfferContentValue(contentType: accountTrieNode, accountTrieNode: val) - of contractTrieNode: - let val = decodeSsz(contentValue, ContractTrieNodeOffer).valueOr: - return empty - OfferContentValue(contentType: contractTrieNode, contractTrieNode: val) - of contractCode: - let val = decodeSsz(contentValue, ContractCodeOffer).valueOr: - return empty - OfferContentValue(contentType: contractCode, contractCode: val) - - Opt.some((key, value)) - -func decodeValue*( - contentKey: ContentKey, contentValue: seq[byte] -): Opt[RetrievalContentValue] = - const empty = Opt.none(RetrievalContentValue) - let value = - case contentKey.contentType - of unused: - return empty - of accountTrieNode: - let val = decodeSsz(contentValue, AccountTrieNodeRetrieval).valueOr: - return empty - RetrievalContentValue(contentType: accountTrieNode, accountTrieNode: val) - of contractTrieNode: - let val = decodeSsz(contentValue, ContractTrieNodeRetrieval).valueOr: - return empty - RetrievalContentValue(contentType: contractTrieNode, contractTrieNode: val) - of contractCode: - let val = decodeSsz(contentValue, ContractCodeRetrieval).valueOr: - return empty - RetrievalContentValue(contentType: contractCode, contractCode: val) - - Opt.some(value) - -proc validateContent*( - n: StateNetwork, contentKey: ContentKey, contentValue: RetrievalContentValue -): bool = - doAssert(contentKey.contentType == contentValue.contentType) - - let res = - case contentKey.contentType - of unused: - Result[void, string].err("Received content with unused content type") - of accountTrieNode: - validateFetchedAccountTrieNode( - contentKey.accountTrieNodeKey, contentValue.accountTrieNode - ) - of contractTrieNode: - validateFetchedContractTrieNode( - contentKey.contractTrieNodeKey, contentValue.contractTrieNode - ) - of contractCode: - validateFetchedContractCode(contentKey.contractCodeKey, contentValue.contractCode) - - res.isOkOr: - warn "Validation of fetched content failed: ", error - - res.isOk() - -proc getContent*(n: StateNetwork, key: ContentKey): Future[Opt[seq[byte]]] {.async.} = - let - keyEncoded = encode(key) - contentId = toContentId(key) - contentInRange = n.portalProtocol.inRange(contentId) - - # When the content id is in the radius range, try to look it up in the db. - if contentInRange: - let contentFromDB = n.contentDB.get(contentId) - if contentFromDB.isSome(): - return contentFromDB - - let content = await n.portalProtocol.contentLookup(keyEncoded, contentId) - - if content.isNone(): - return Opt.none(seq[byte]) - - let - contentResult = content.get() - decodedValue = decodeValue(key, contentResult.content).valueOr: - error "Unable to decode offered Key/Value" - return Opt.none(seq[byte]) - - if not validateContent(n, key, decodedValue): - return Opt.none(seq[byte]) - - # When content is found on the network and is in the radius range, store it. - if content.isSome() and contentInRange: - # TODO Add poke when working on state network - # TODO When working on state network, make it possible to pass different - # distance functions to store content - n.portalProtocol.storeContent(keyEncoded, contentId, contentResult.content) - - # TODO: for now returning bytes, ultimately it would be nice to return proper - # domain types. - return Opt.some(contentResult.content) - -proc getStateRootByBlockHash( - n: StateNetwork, hash: BlockHash -): Future[Opt[KeccakHash]] {.async.} = - if n.historyNetwork.isNone(): - warn "History network is not available. Unable to get state root by block hash" - return Opt.none(KeccakHash) - - let header = (await n.historyNetwork.get().getVerifiedBlockHeader(hash)).valueOr: - warn "Failed to get block header by hash", hash - return Opt.none(KeccakHash) - - Opt.some(header.stateRoot) - -proc validateContent*( - n: StateNetwork, contentKey: ContentKey, contentValue: OfferContentValue -): Future[Result[void, string]] {.async.} = - doAssert(contentKey.contentType == contentValue.contentType) - - case contentKey.contentType - of unused: - Result[void, string].err("Received content with unused content type") - of accountTrieNode: - let stateRoot = ( - await n.getStateRootByBlockHash(contentValue.accountTrieNode.blockHash) - ).valueOr: - return Result[void, string].err("Failed to get state root by block hash") - - validateOfferedAccountTrieNode( - stateRoot, contentKey.accountTrieNodeKey, contentValue.accountTrieNode - ) - of contractTrieNode: - let stateRoot = ( - await n.getStateRootByBlockHash(contentValue.contractTrieNode.blockHash) - ).valueOr: - return Result[void, string].err("Failed to get state root by block hash") - - validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValue.contractTrieNode - ) - of contractCode: - let stateRoot = ( - await n.getStateRootByBlockHash(contentValue.contractCode.blockHash) - ).valueOr: - return Result[void, string].err("Failed to get state root by block hash") - - validateOfferedContractCode( - stateRoot, contentKey.contractCodeKey, contentValue.contractCode - ) - -proc recursiveGossipAccountTrieNode( - p: PortalProtocol, - maybeSrcNodeId: Opt[NodeId], - decodedKey: ContentKey, - decodedValue: AccountTrieNodeOffer, -): Future[void] {.async.} = - var - nibbles = decodedKey.accountTrieNodeKey.path.unpackNibbles() - proof = decodedValue.proof - - # When nibbles is empty this means the root node was received. Recursive - # gossiping is finished. - if nibbles.len() == 0: - return - - discard nibbles.pop() - discard (distinctBase proof).pop() - let - updatedValue = AccountTrieNodeOffer(proof: proof, blockHash: decodedValue.blockHash) - updatedNodeHash = keccakHash(distinctBase proof[^1]) - encodedValue = SSZ.encode(updatedValue) - updatedKey = - AccountTrieNodeKey(path: nibbles.packNibbles(), nodeHash: updatedNodeHash) - encodedKey = - ContentKey(accountTrieNodeKey: updatedKey, contentType: accountTrieNode).encode() - - await neighborhoodGossipDiscardPeers( - p, maybeSrcNodeId, ContentKeysList.init(@[encodedKey]), @[encodedValue] - ) - -proc recursiveGossipContractTrieNode( - p: PortalProtocol, - maybeSrcNodeId: Opt[NodeId], - decodedKey: ContentKey, - decodedValue: ContractTrieNodeOffer, -): Future[void] {.async.} = - return - -proc gossipContent*( - p: PortalProtocol, - maybeSrcNodeId: Opt[NodeId], - contentKey: ByteList, - decodedKey: ContentKey, - contentValue: seq[byte], - decodedValue: OfferContentValue, -): Future[void] {.async.} = - case decodedKey.contentType - of unused: - raiseAssert "Gossiping content with unused content type" - of accountTrieNode: - await recursiveGossipAccountTrieNode( - p, maybeSrcNodeId, decodedKey, decodedValue.accountTrieNode - ) - of contractTrieNode: - await recursiveGossipContractTrieNode( - p, maybeSrcNodeId, decodedKey, decodedValue.contractTrieNode - ) - of contractCode: - await p.neighborhoodGossipDiscardPeers( - maybeSrcNodeId, ContentKeysList.init(@[contentKey]), @[contentValue] - ) - proc new*( T: type StateNetwork, baseProtocol: protocol.Protocol, @@ -290,34 +69,161 @@ proc new*( historyNetwork: historyNetwork, ) +# TODO: implement content lookups for each type +proc getContent*( + n: StateNetwork, contentKey: ContentKey +): Future[Opt[seq[byte]]] {.async.} = + let + contentKeyBytes = contentKey.encode() + contentId = contentKeyBytes.toContentId() + contentInRange = n.portalProtocol.inRange(contentId) + + # When the content id is in the radius range, try to look it up in the db. + if contentInRange: + let contentFromDB = n.contentDB.get(contentId) + if contentFromDB.isSome(): + return contentFromDB + + let + contentLookupResult = ( + await n.portalProtocol.contentLookup(contentKeyBytes, contentId) + ).valueOr: + return Opt.none(seq[byte]) + contentValueBytes = contentLookupResult.content + + case contentKey.contentType + of unused: + error "Received content with unused content type" + return Opt.none(seq[byte]) + of accountTrieNode: + let contentValue = AccountTrieNodeRetrieval.decode(contentValueBytes).valueOr: + error "Unable to decode AccountTrieNodeRetrieval content value" + return Opt.none(seq[byte]) + + validateRetrieval(contentKey.accountTrieNodeKey, contentValue).isOkOr: + error "Validation of retrieval content failed: ", error + return Opt.none(seq[byte]) + of contractTrieNode: + let contentValue = ContractTrieNodeRetrieval.decode(contentValueBytes).valueOr: + error "Unable to decode ContractTrieNodeRetrieval content value" + return Opt.none(seq[byte]) + + validateRetrieval(contentKey.contractTrieNodeKey, contentValue).isOkOr: + error "Validation of retrieval content failed: ", error + return Opt.none(seq[byte]) + of contractCode: + let contentValue = ContractCodeRetrieval.decode(contentValueBytes).valueOr: + error "Unable to decode ContractCodeRetrieval content value" + return Opt.none(seq[byte]) + + validateRetrieval(contentKey.contractCodeKey, contentValue).isOkOr: + error "Validation of retrieval content failed: ", error + return Opt.none(seq[byte]) + + # When content is in the radius range, store it. + if contentInRange: + # TODO Add poke when working on state network + # TODO When working on state network, make it possible to pass different + # distance functions to store content + n.portalProtocol.storeContent(contentKeyBytes, contentId, contentValueBytes) + + # TODO: for now returning bytes, ultimately it would be nice to return proper + # domain types. + return Opt.some(contentValueBytes) + +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.} = + if n.historyNetwork.isNone(): + warn "History network is not available. Unable to get state root by block hash" + return Opt.none(KeccakHash) + + let header = (await n.historyNetwork.get().getVerifiedBlockHeader(hash)).valueOr: + warn "Failed to get block header by hash", hash + return Opt.none(KeccakHash) + + Opt.some(header.stateRoot) + +proc processOffer[K, V]( + n: StateNetwork, + maybeSrcNodeId: Opt[NodeId], + contentKeyBytes: ByteList, + contentKey: K, + contentValue: V, +): Future[Result[void, string]] {.async.} = + mixin blockHash, validateOffer, toRetrievalValue, gossipOffer + + let stateRoot = (await n.getStateRootByBlockHash(contentValue.blockHash)).valueOr: + return err("Failed to get state root by block hash") + + let res = validateOffer(stateRoot, contentKey, contentValue) + if res.isErr(): + return err("Received offered content failed validation: " & res.error()) + + let contentId = n.portalProtocol.toContentId(contentKeyBytes).valueOr: + return err("Received offered content with invalid content key") + + n.portalProtocol.storeContent( + contentKeyBytes, contentId, contentValue.toRetrievalValue().encode() + ) + info "Received offered content validated successfully", contentKeyBytes + + await gossipOffer(n.portalProtocol, maybeSrcNodeId, contentKey, contentValue) + proc processContentLoop(n: StateNetwork) {.async.} = try: while true: let (maybeSrcNodeId, contentKeys, contentValues) = await n.contentQueue.popFirst() - for i, contentValue in contentValues: + for i, contentValueBytes in contentValues: let - contentKey = contentKeys[i] - (decodedKey, decodedValue) = decodeKV(contentKey, contentValue).valueOr: - error "Unable to decode offered Key/Value" + contentKeyBytes = contentKeys[i] + contentKey = decodeKey(contentKeyBytes).valueOr: + error "Unable to decode offered content key", contentKeyBytes continue - (await n.validateContent(decodedKey, decodedValue)).isOkOr: - error "Received offered content failed validation", contentKey, error - continue - - let - valueForRetrieval = decodedValue.offerContentToRetrievalContent().encode() - contentId = n.portalProtocol.toContentId(contentKey).valueOr: - error "Received offered content with invalid content key", contentKey + let offerRes = + case contentKey.contentType + of unused: + error "Received content with unused content type" continue + of accountTrieNode: + let contentValue = AccountTrieNodeOffer.decode(contentValueBytes).valueOr: + error "Unable to decode offered AccountTrieNodeOffer content value" + continue - n.portalProtocol.storeContent(contentKey, contentId, valueForRetrieval) - info "Received offered content validated successfully", contentKey + await processOffer( + n, maybeSrcNodeId, contentKeyBytes, contentKey.accountTrieNodeKey, + contentValue, + ) + of contractTrieNode: + let contentValue = ContractTrieNodeOffer.decode(contentValueBytes).valueOr: + error "Unable to decode offered ContractTrieNodeOffer content value" + continue - await gossipContent( - n.portalProtocol, maybeSrcNodeId, contentKey, decodedKey, contentValue, - decodedValue, - ) + await processOffer( + n, maybeSrcNodeId, contentKeyBytes, contentKey.contractTrieNodeKey, + contentValue, + ) + of contractCode: + let contentValue = ContractCodeOffer.decode(contentValueBytes).valueOr: + error "Unable to decode offered ContractCodeOffer content value" + continue + + await processOffer( + n, maybeSrcNodeId, contentKeyBytes, contentKey.contractCodeKey, + contentValue, + ) + if offerRes.isOk(): + info "Offered content processed successfully", contentKeyBytes + else: + error "Offered content processing failed", + contentKeyBytes, error = offerRes.error() except CancelledError: trace "processContentLoop canceled" diff --git a/fluffy/network/state/state_validation.nim b/fluffy/network/state/state_validation.nim index 0ded9cab3..b945e87f0 100644 --- a/fluffy/network/state/state_validation.nim +++ b/fluffy/network/state/state_validation.nim @@ -11,7 +11,7 @@ import ../../common/[common_types, common_utils], ./state_content -export results +export results, state_content # private functions @@ -31,7 +31,7 @@ proc isValidNextNode(thisNodeRlp: Rlp, rlpIdx: int, nextNode: TrieNode): bool = let hash = hashOrShortRlp.toBytes() if hash.len() != 32: return false - KeccakHash.init(hash) + KeccakHash.fromBytes(hash) nextNode.hashEquals(nextHash) @@ -48,6 +48,23 @@ proc decodePrefix(nodePrefixRlp: Rlp): (byte, bool, Nibbles) = (firstNibble.byte, isLeaf, nibbles) +proc rlpDecodeAccountTrieNode(accountNode: TrieNode): Result[Account, string] = + let accNodeRlp = rlpFromBytes(accountNode.asSeq()) + if accNodeRlp.isEmpty() or accNodeRlp.listLen() != 2: + return err("invalid account trie node - malformed") + + let accNodePrefixRlp = accNodeRlp.listElem(0) + if accNodePrefixRlp.isEmpty(): + return err("invalid account trie node - empty prefix") + + let (_, isLeaf, _) = decodePrefix(accNodePrefixRlp) + if not isLeaf: + return err("invalid account trie node - leaf prefix expected") + + decodeRlp(accNodeRlp.listElem(1).toBytes(), Account) + +# public functions + proc validateTrieProof*( expectedRootHash: KeccakHash, path: Nibbles, proof: TrieProof ): Result[void, string] = @@ -122,24 +139,7 @@ proc validateTrieProof*( else: ok() -proc rlpDecodeAccountTrieNode(accountNode: TrieNode): Result[Account, string] = - let accNodeRlp = rlpFromBytes(accountNode.asSeq()) - if accNodeRlp.isEmpty() or accNodeRlp.listLen() != 2: - return err("invalid account trie node - malformed") - - let accNodePrefixRlp = accNodeRlp.listElem(0) - if accNodePrefixRlp.isEmpty(): - return err("invalid account trie node - empty prefix") - - let (_, isLeaf, _) = decodePrefix(accNodePrefixRlp) - if not isLeaf: - return err("invalid account trie node - leaf prefix expected") - - decodeRlp(accNodeRlp.listElem(1).toBytes(), Account) - -# public functions - -proc validateFetchedAccountTrieNode*( +proc validateRetrieval*( trustedAccountTrieNodeKey: AccountTrieNodeKey, accountTrieNode: AccountTrieNodeRetrieval, ): Result[void, string] = @@ -148,7 +148,7 @@ proc validateFetchedAccountTrieNode*( else: err("hash of fetched account trie node doesn't match the expected node hash") -proc validateFetchedContractTrieNode*( +proc validateRetrieval*( trustedContractTrieNodeKey: ContractTrieNodeKey, contractTrieNode: ContractTrieNodeRetrieval, ): Result[void, string] = @@ -157,7 +157,7 @@ proc validateFetchedContractTrieNode*( else: err("hash of fetched contract trie node doesn't match the expected node hash") -proc validateFetchedContractCode*( +proc validateRetrieval*( trustedContractCodeKey: ContractCodeKey, contractCode: ContractCodeRetrieval ): Result[void, string] = if contractCode.code.hashEquals(trustedContractCodeKey.codeHash): @@ -165,7 +165,7 @@ proc validateFetchedContractCode*( else: err("hash of fetched bytecode doesn't match the expected code hash") -proc validateOfferedAccountTrieNode*( +proc validateOffer*( trustedStateRoot: KeccakHash, accountTrieNodeKey: AccountTrieNodeKey, accountTrieNode: AccountTrieNodeOffer, @@ -177,7 +177,7 @@ proc validateOfferedAccountTrieNode*( else: err("hash of offered account trie node doesn't match the expected node hash") -proc validateOfferedContractTrieNode*( +proc validateOffer*( trustedStateRoot: KeccakHash, contractTrieNodeKey: ContractTrieNodeKey, contractTrieNode: ContractTrieNodeOffer, @@ -198,7 +198,7 @@ proc validateOfferedContractTrieNode*( else: err("hash of offered contract trie node doesn't match the expected node hash") -proc validateOfferedContractCode*( +proc validateOffer*( trustedStateRoot: KeccakHash, contractCodeKey: ContractCodeKey, contractCode: ContractCodeOffer, 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 fda824fb6..9bb9ee182 100644 --- a/fluffy/tests/state_network_tests/all_state_network_tests.nim +++ b/fluffy/tests/state_network_tests/all_state_network_tests.nim @@ -10,6 +10,7 @@ import ./test_state_content_keys, ./test_state_content_values, + ./test_state_content_nibbles, ./test_state_network, #./test_state_network_gossip, ./test_state_validation, diff --git a/fluffy/tests/state_network_tests/test_state_content_keys.nim b/fluffy/tests/state_network_tests/test_state_content_keys.nim index 2164bb094..24d2649f4 100644 --- a/fluffy/tests/state_network_tests/test_state_content_keys.nim +++ b/fluffy/tests/state_network_tests/test_state_content_keys.nim @@ -14,90 +14,6 @@ import const testVectorDir = "./vendor/portal-spec-tests/tests/mainnet/state/serialization/" suite "State Content Keys": - test "Encode/decode empty nibbles": - const - expected = "00" - nibbles: seq[byte] = @[] - packedNibbles = packNibbles(nibbles) - unpackedNibbles = unpackNibbles(packedNibbles) - - let encoded = SSZ.encode(packedNibbles) - - check: - encoded.toHex() == expected - unpackedNibbles == nibbles - - test "Encode/decode zero nibble": - const - expected = "10" - nibbles: seq[byte] = @[0] - packedNibbles = packNibbles(nibbles) - unpackedNibbles = unpackNibbles(packedNibbles) - - let encoded = SSZ.encode(packedNibbles) - - check: - encoded.toHex() == expected - unpackedNibbles == nibbles - - test "Encode/decode one nibble": - const - expected = "11" - nibbles: seq[byte] = @[1] - packedNibbles = packNibbles(nibbles) - unpackedNibbles = unpackNibbles(packedNibbles) - - let encoded = SSZ.encode(packedNibbles) - - check: - encoded.toHex() == expected - unpackedNibbles == nibbles - - test "Encode/decode even nibbles": - const - expected = "008679e8ed" - nibbles: seq[byte] = @[8, 6, 7, 9, 14, 8, 14, 13] - packedNibbles = packNibbles(nibbles) - unpackedNibbles = unpackNibbles(packedNibbles) - - let encoded = SSZ.encode(packedNibbles) - - check: - encoded.toHex() == expected - unpackedNibbles == nibbles - - test "Encode/decode odd nibbles": - const - expected = "138679e8ed" - nibbles: seq[byte] = @[3, 8, 6, 7, 9, 14, 8, 14, 13] - packedNibbles = packNibbles(nibbles) - unpackedNibbles = unpackNibbles(packedNibbles) - - let encoded = SSZ.encode(packedNibbles) - - check: - encoded.toHex() == expected - unpackedNibbles == nibbles - - test "Encode/decode max length nibbles": - const - expected = "008679e8eda65bd257638cf8cf09b8238888947cc3c0bea2aa2cc3f1c4ac7a3002" - nibbles: seq[byte] = - @[ - 8, 6, 7, 9, 0xe, 8, 0xe, 0xd, 0xa, 6, 5, 0xb, 0xd, 2, 5, 7, 6, 3, 8, 0xc, 0xf, - 8, 0xc, 0xf, 0, 9, 0xb, 8, 2, 3, 8, 8, 8, 8, 9, 4, 7, 0xc, 0xc, 3, 0xc, 0, - 0xb, 0xe, 0xa, 2, 0xa, 0xa, 2, 0xc, 0xc, 3, 0xf, 1, 0xc, 4, 0xa, 0xc, 7, 0xa, - 3, 0, 0, 2, - ] - packedNibbles = packNibbles(nibbles) - unpackedNibbles = unpackNibbles(packedNibbles) - - let encoded = SSZ.encode(packedNibbles) - - check: - encoded.toHex() == expected - unpackedNibbles == nibbles - test "Encode/decode AccountTrieNodeKey": const file = testVectorDir & "account_trie_node_key.yaml" @@ -120,7 +36,7 @@ suite "State Content Keys": encoded.asSeq() == testCase.content_key.hexToSeqByte() encoded.toContentId().toBytesBE() == testCase.content_id.hexToSeqByte() - let decoded = encoded.decode() + let decoded = ContentKey.decode(encoded) check: decoded.isOk() decoded.value().contentType == accountTrieNode @@ -151,7 +67,7 @@ suite "State Content Keys": encoded.asSeq() == testCase.content_key.hexToSeqByte() encoded.toContentId().toBytesBE() == testCase.content_id.hexToSeqByte() - let decoded = encoded.decode() + let decoded = ContentKey.decode(encoded) check: decoded.isOk() decoded.value().contentType == contractTrieNode @@ -180,7 +96,7 @@ suite "State Content Keys": encoded.asSeq() == testCase.content_key.hexToSeqByte() encoded.toContentId().toBytesBE() == testCase.content_id.hexToSeqByte() - let decoded = encoded.decode() + let decoded = ContentKey.decode(encoded) check: decoded.isOk() decoded.value().contentType == contractCode @@ -189,24 +105,24 @@ suite "State Content Keys": test "Invalid prefix - 0 value": let encoded = ByteList.init(@[byte 0x00]) - let decoded = decode(encoded) + let decoded = ContentKey.decode(encoded) - check decoded.isNone() + check decoded.isErr() test "Invalid prefix - before valid range": let encoded = ByteList.init(@[byte 0x01]) - let decoded = decode(encoded) + let decoded = ContentKey.decode(encoded) - check decoded.isNone() + check decoded.isErr() test "Invalid prefix - after valid range": let encoded = ByteList.init(@[byte 0x25]) - let decoded = decode(encoded) + let decoded = ContentKey.decode(encoded) - check decoded.isNone() + check decoded.isErr() test "Invalid key - empty input": let encoded = ByteList.init(@[]) - let decoded = decode(encoded) + let decoded = ContentKey.decode(encoded) - check decoded.isNone() + check decoded.isErr() diff --git a/fluffy/tests/state_network_tests/test_state_content_nibbles.nim b/fluffy/tests/state_network_tests/test_state_content_nibbles.nim new file mode 100644 index 000000000..e219ff13a --- /dev/null +++ b/fluffy/tests/state_network_tests/test_state_content_nibbles.nim @@ -0,0 +1,93 @@ +# 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 unittest2, stew/byteutils, ../../network/state/state_content + +suite "State Content Nibbles": + test "Encode/decode empty nibbles": + const + expected = "00" + nibbles: seq[byte] = @[] + packedNibbles = packNibbles(nibbles) + unpackedNibbles = unpackNibbles(packedNibbles) + + let encoded = SSZ.encode(packedNibbles) + + check: + encoded.toHex() == expected + unpackedNibbles == nibbles + + test "Encode/decode zero nibble": + const + expected = "10" + nibbles: seq[byte] = @[0] + packedNibbles = packNibbles(nibbles) + unpackedNibbles = unpackNibbles(packedNibbles) + + let encoded = SSZ.encode(packedNibbles) + + check: + encoded.toHex() == expected + unpackedNibbles == nibbles + + test "Encode/decode one nibble": + const + expected = "11" + nibbles: seq[byte] = @[1] + packedNibbles = packNibbles(nibbles) + unpackedNibbles = unpackNibbles(packedNibbles) + + let encoded = SSZ.encode(packedNibbles) + + check: + encoded.toHex() == expected + unpackedNibbles == nibbles + + test "Encode/decode even nibbles": + const + expected = "008679e8ed" + nibbles: seq[byte] = @[8, 6, 7, 9, 14, 8, 14, 13] + packedNibbles = packNibbles(nibbles) + unpackedNibbles = unpackNibbles(packedNibbles) + + let encoded = SSZ.encode(packedNibbles) + + check: + encoded.toHex() == expected + unpackedNibbles == nibbles + + test "Encode/decode odd nibbles": + const + expected = "138679e8ed" + nibbles: seq[byte] = @[3, 8, 6, 7, 9, 14, 8, 14, 13] + packedNibbles = packNibbles(nibbles) + unpackedNibbles = unpackNibbles(packedNibbles) + + let encoded = SSZ.encode(packedNibbles) + + check: + encoded.toHex() == expected + unpackedNibbles == nibbles + + test "Encode/decode max length nibbles": + const + expected = "008679e8eda65bd257638cf8cf09b8238888947cc3c0bea2aa2cc3f1c4ac7a3002" + nibbles: seq[byte] = + @[ + 8, 6, 7, 9, 0xe, 8, 0xe, 0xd, 0xa, 6, 5, 0xb, 0xd, 2, 5, 7, 6, 3, 8, 0xc, 0xf, + 8, 0xc, 0xf, 0, 9, 0xb, 8, 2, 3, 8, 8, 8, 8, 9, 4, 7, 0xc, 0xc, 3, 0xc, 0, + 0xb, 0xe, 0xa, 2, 0xa, 0xa, 2, 0xc, 0xc, 3, 0xf, 1, 0xc, 4, 0xa, 0xc, 7, 0xa, + 3, 0, 0, 2, + ] + packedNibbles = packNibbles(nibbles) + unpackedNibbles = unpackNibbles(packedNibbles) + + let encoded = SSZ.encode(packedNibbles) + + check: + encoded.toHex() == expected + unpackedNibbles == nibbles diff --git a/fluffy/tests/state_network_tests/test_state_content_values.nim b/fluffy/tests/state_network_tests/test_state_content_values.nim index 935c45af6..5d5e05f16 100644 --- a/fluffy/tests/state_network_tests/test_state_content_values.nim +++ b/fluffy/tests/state_network_tests/test_state_content_values.nim @@ -30,11 +30,11 @@ suite "State Content Values": blockHash = BlockHash.fromHex(testCase.block_hash) proof = TrieProof.init(testCase.proof.map((hex) => TrieNode.init(hex.hexToSeqByte()))) - accountTrieNodeOffer = AccountTrieNodeOffer(blockHash: blockHash, proof: proof) + accountTrieNodeOffer = AccountTrieNodeOffer.init(proof, blockHash) - encoded = SSZ.encode(accountTrieNodeOffer) + encoded = accountTrieNodeOffer.encode() expected = testCase.content_value.hexToSeqByte() - decoded = SSZ.decode(encoded, AccountTrieNodeOffer) + decoded = AccountTrieNodeOffer.decode(encoded).get() check: encoded == expected @@ -54,9 +54,9 @@ suite "State Content Values": node = TrieNode.init(testCase.trie_node.hexToSeqByte()) accountTrieNodeRetrieval = AccountTrieNodeRetrieval(node: node) - encoded = SSZ.encode(accountTrieNodeRetrieval) + encoded = accountTrieNodeRetrieval.encode() expected = testCase.content_value.hexToSeqByte() - decoded = SSZ.decode(encoded, AccountTrieNodeRetrieval) + decoded = AccountTrieNodeRetrieval.decode(encoded).get() check: encoded == expected @@ -86,9 +86,9 @@ suite "State Content Values": blockHash: blockHash, storage_proof: storageProof, account_proof: accountProof ) - encoded = SSZ.encode(contractTrieNodeOffer) + encoded = contractTrieNodeOffer.encode() expected = testCase.content_value.hexToSeqByte() - decoded = SSZ.decode(encoded, ContractTrieNodeOffer) + decoded = ContractTrieNodeOffer.decode(encoded).get() check: encoded == expected @@ -111,9 +111,9 @@ suite "State Content Values": node = TrieNode.init(testCase.trie_node.hexToSeqByte()) contractTrieNodeRetrieval = ContractTrieNodeRetrieval(node: node) - encoded = SSZ.encode(contractTrieNodeRetrieval) + encoded = contractTrieNodeRetrieval.encode() expected = testCase.content_value.hexToSeqByte() - decoded = SSZ.decode(encoded, ContractTrieNodeRetrieval) + decoded = ContractTrieNodeRetrieval.decode(encoded).get() check: encoded == expected @@ -140,9 +140,9 @@ suite "State Content Values": contractCodeOffer = ContractCodeOffer(code: code, blockHash: blockHash, accountProof: accountProof) - encoded = SSZ.encode(contractCodeOffer) + encoded = contractCodeOffer.encode() expected = testCase.content_value.hexToSeqByte() - decoded = SSZ.decode(encoded, ContractCodeOffer) + decoded = ContractCodeOffer.decode(encoded).get() check: encoded == expected @@ -162,9 +162,9 @@ suite "State Content Values": code = Bytecode.init(testCase.bytecode.hexToSeqByte()) contractCodeRetrieval = ContractCodeRetrieval(code: code) - encoded = SSZ.encode(contractCodeRetrieval) + encoded = contractCodeRetrieval.encode() expected = testCase.content_value.hexToSeqByte() - decoded = SSZ.decode(encoded, ContractCodeRetrieval) + decoded = ContractCodeRetrieval.decode(encoded).get() check: encoded == expected diff --git a/fluffy/tests/state_network_tests/test_state_network.nim b/fluffy/tests/state_network_tests/test_state_network.nim index c44cf1e2f..65c174c4c 100644 --- a/fluffy/tests/state_network_tests/test_state_network.nim +++ b/fluffy/tests/state_network_tests/test_state_network.nim @@ -53,11 +53,8 @@ procSuite "State Network": contentKey = ContentKey( contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey ) - contentId = toContentId(contentKey) - value = RetrievalContentValue( - contentType: accountTrieNode, - accountTrieNode: AccountTrieNodeRetrieval(node: TrieNode.init(v)), - ) + contentId = toContentId(contentKey.encode()) + value = AccountTrieNodeRetrieval(node: TrieNode.init(v)) discard proto1.contentDB.put( contentId, value.encode(), proto1.portalProtocol.localNode.id @@ -72,7 +69,7 @@ procSuite "State Network": contentKey = ContentKey( contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey ) - contentId = toContentId(contentKey) + 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. @@ -129,11 +126,8 @@ procSuite "State Network": contentKey = ContentKey( contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey ) - contentId = toContentId(contentKey) - value = RetrievalContentValue( - contentType: accountTrieNode, - accountTrieNode: AccountTrieNodeRetrieval(node: TrieNode.init(v)), - ) + contentId = toContentId(contentKey.encode()) + value = AccountTrieNodeRetrieval(node: TrieNode.init(v)) discard proto2.contentDB.put( contentId, value.encode(), proto2.portalProtocol.localNode.id diff --git a/fluffy/tests/state_network_tests/test_state_network_gossip.nim b/fluffy/tests/state_network_tests/test_state_network_gossip.nim index 2636557cc..d9802fe07 100644 --- a/fluffy/tests/state_network_tests/test_state_network_gossip.nim +++ b/fluffy/tests/state_network_tests/test_state_network_gossip.nim @@ -13,7 +13,7 @@ import eth/p2p/discoveryv5/protocol as discv5_protocol, ../../network/wire/[portal_protocol, portal_stream], ../../network/history/[history_content, history_network], - ../../network/state/[state_content, state_network], + ../../network/state/[state_content, state_network, state_gossip], ../../database/content_db, .././test_helpers, ../../eth_data/yaml_utils @@ -74,7 +74,7 @@ procSuite "State Network Gossip": header: ByteList.init(headerRlp), proof: BlockHeaderProof.init() ) value = recursiveGossipSteps[0].content_value.hexToSeqByte() - decodedValue = SSZ.decode(value, AccountTrieNodeOffer) + decodedValue = AccountTrieNodeOffer.decode(value).get() contentKey = history_content.ContentKey .init(history_content.ContentType.blockHeader, decodedValue.blockHash) .encode() @@ -89,28 +89,22 @@ procSuite "State Network Gossip": nextNode = clients[i + 1] key = ByteList.init(pair.content_key.hexToSeqByte()) - decodedKey = state_content.decode(key).valueOr: + decodedKey = state_content.ContentKey.decode(key).valueOr: raiseAssert "Cannot decode key" nextKey = ByteList.init(recursiveGossipSteps[1].content_key.hexToSeqByte()) - decodedNextKey = state_content.decode(nextKey).valueOr: + decodedNextKey = state_content.ContentKey.decode(nextKey).valueOr: raiseAssert "Cannot decode key" value = pair.content_value.hexToSeqByte() - decodedValue = SSZ.decode(value, AccountTrieNodeOffer) - offerValue = - OfferContentValue(contentType: accountTrieNode, accountTrieNode: decodedValue) - + decodedValue = AccountTrieNodeOffer.decode(value).get() nextValue = recursiveGossipSteps[1].content_value.hexToSeqByte() - nextDecodedValue = SSZ.decode(nextValue, AccountTrieNodeOffer) - nextOfferValue = OfferContentValue( - contentType: accountTrieNode, accountTrieNode: nextDecodedValue - ) - nextRetrievalValue = nextOfferValue.offerContentToRetrievalContent().encode() + nextDecodedValue = AccountTrieNodeOffer.decode(nextValue).get() + nextRetrievalValue = nextDecodedValue.toRetrievalValue().encode() if i == 0: - await currentNode.portalProtocol.gossipContent( - Opt.none(NodeId), key, decodedKey, value, offerValue + await currentNode.portalProtocol.gossipOffer( + Opt.none(NodeId), decodedKey.accountTrieNodeKey, decodedValue ) await sleepAsync(100.milliseconds) #TODO figure out how to get rid of this sleep diff --git a/fluffy/tests/state_network_tests/test_state_validation.nim b/fluffy/tests/state_network_tests/test_state_validation.nim index b13935a11..94c976d63 100644 --- a/fluffy/tests/state_network_tests/test_state_validation.nim +++ b/fluffy/tests/state_network_tests/test_state_validation.nim @@ -60,16 +60,14 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for testData in testCase: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - let contentValueRetrieval = SSZ.decode( - testData.content_value_retrieval.hexToSeqByte(), AccountTrieNodeRetrieval - ) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentValueRetrieval = AccountTrieNodeRetrieval + .decode(testData.content_value_retrieval.hexToSeqByte()) + .get() check: - validateFetchedAccountTrieNode( - contentKey.accountTrieNodeKey, contentValueRetrieval - ) - .isOk() + validateRetrieval(contentKey.accountTrieNodeKey, contentValueRetrieval).isOk() test "Validate invalid AccountTrieNodeRetrieval nodes": const file = testVectorDir / "account_trie_node.yaml" @@ -78,16 +76,15 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for testData in testCase: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - var contentValueRetrieval = SSZ.decode( - testData.content_value_retrieval.hexToSeqByte(), AccountTrieNodeRetrieval - ) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + var contentValueRetrieval = AccountTrieNodeRetrieval + .decode(testData.content_value_retrieval.hexToSeqByte()) + .get() contentValueRetrieval.node[^1] += 1 # Modify node hash - let res = validateFetchedAccountTrieNode( - contentKey.accountTrieNodeKey, contentValueRetrieval - ) + let res = validateRetrieval(contentKey.accountTrieNodeKey, contentValueRetrieval) check: res.isErr() res.error() == @@ -100,16 +97,14 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for testData in testCase: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - let contentValueRetrieval = SSZ.decode( - testData.content_value_retrieval.hexToSeqByte(), ContractTrieNodeRetrieval - ) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentValueRetrieval = ContractTrieNodeRetrieval + .decode(testData.content_value_retrieval.hexToSeqByte()) + .get() check: - validateFetchedContractTrieNode( - contentKey.contractTrieNodeKey, contentValueRetrieval - ) - .isOk() + validateRetrieval(contentKey.contractTrieNodeKey, contentValueRetrieval).isOk() test "Validate invalid ContractTrieNodeRetrieval nodes": const file = testVectorDir / "contract_storage_trie_node.yaml" @@ -118,16 +113,15 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for testData in testCase: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - var contentValueRetrieval = SSZ.decode( - testData.content_value_retrieval.hexToSeqByte(), ContractTrieNodeRetrieval - ) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + var contentValueRetrieval = ContractTrieNodeRetrieval + .decode(testData.content_value_retrieval.hexToSeqByte()) + .get() contentValueRetrieval.node[^1] += 1 # Modify node hash - let res = validateFetchedContractTrieNode( - contentKey.contractTrieNodeKey, contentValueRetrieval - ) + let res = validateRetrieval(contentKey.contractTrieNodeKey, contentValueRetrieval) check: res.isErr() res.error() == @@ -140,14 +134,14 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for testData in testCase: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - let contentValueRetrieval = SSZ.decode( - testData.content_value_retrieval.hexToSeqByte(), ContractCodeRetrieval - ) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentValueRetrieval = ContractCodeRetrieval + .decode(testData.content_value_retrieval.hexToSeqByte()) + .get() check: - validateFetchedContractCode(contentKey.contractCodeKey, contentValueRetrieval) - .isOk() + validateRetrieval(contentKey.contractCodeKey, contentValueRetrieval).isOk() test "Validate invalid ContractCodeRetrieval nodes": const file = testVectorDir / "contract_bytecode.yaml" @@ -156,15 +150,15 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for testData in testCase: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - var contentValueRetrieval = SSZ.decode( - testData.content_value_retrieval.hexToSeqByte(), ContractCodeRetrieval - ) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + var contentValueRetrieval = ContractCodeRetrieval + .decode(testData.content_value_retrieval.hexToSeqByte()) + .get() contentValueRetrieval.code[^1] += 1 # Modify node hash - let res = - validateFetchedContractCode(contentKey.contractCodeKey, contentValueRetrieval) + let res = validateRetrieval(contentKey.contractCodeKey, contentValueRetrieval) check: res.isErr() res.error() == "hash of fetched bytecode doesn't match the expected code hash" @@ -178,34 +172,30 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for i, testData in testCase: - var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() let contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer) + AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() check: - validateOfferedAccountTrieNode( - stateRoot, contentKey.accountTrieNodeKey, contentValueOffer - ) + validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer) .isOk() if i == 1: continue # second test case only has root node and no recursive gossip - let contentKey = - decode(testData.recursive_gossip.content_key.hexToSeqByte().ByteList).get() - let contentValueOffer = SSZ.decode( - testData.recursive_gossip.content_value_offer.hexToSeqByte(), - AccountTrieNodeOffer, - ) + let contentKey = ContentKey + .decode(testData.recursive_gossip.content_key.hexToSeqByte().ByteList) + .get() + let contentValueOffer = AccountTrieNodeOffer + .decode(testData.recursive_gossip.content_value_offer.hexToSeqByte()) + .get() check: - validateOfferedAccountTrieNode( - stateRoot, contentKey.accountTrieNodeKey, contentValueOffer - ) - .isOk() + validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer).isOk() test "Validate invalid AccountTrieNodeOffer nodes - bad state roots": const file = testVectorDir / "account_trie_node.yaml" @@ -219,15 +209,15 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for i, testData in testCase: - var stateRoot = KeccakHash.init(stateRoots[i].hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(stateRoots[i].hexToSeqByte()) - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() let contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer) + AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() - let res = validateOfferedAccountTrieNode( - stateRoot, contentKey.accountTrieNodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer) check: res.isErr() res.error() == "hash of proof root node doesn't match the expected root hash" @@ -239,17 +229,17 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for i, testData in testCase: - var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer) + AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() contentValueOffer.proof[0][0] += 1.byte - let res = validateOfferedAccountTrieNode( - stateRoot, contentKey.accountTrieNodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer) check: res.isErr() res.error() == "hash of proof root node doesn't match the expected root hash" @@ -257,33 +247,33 @@ suite "State Validation": for i, testData in testCase: if i == 1: continue # second test case only has root node - var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer) + AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() contentValueOffer.proof[^2][^2] += 1.byte - let res = validateOfferedAccountTrieNode( - stateRoot, contentKey.accountTrieNodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer) check: res.isErr() "hash of next node doesn't match the expected" in res.error() for i, testData in testCase: - var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer) + AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() contentValueOffer.proof[^1][^1] += 1.byte - let res = validateOfferedAccountTrieNode( - stateRoot, contentKey.accountTrieNodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer) check: res.isErr() @@ -296,34 +286,31 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for i, testData in testCase: - var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - let contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentValueOffer = ContractTrieNodeOffer + .decode(testData.content_value_offer.hexToSeqByte()) + .get() check: - validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValueOffer - ) + validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer) .isOk() if i == 1: continue # second test case has no recursive gossip - let contentKey = - decode(testData.recursive_gossip.content_key.hexToSeqByte().ByteList).get() - let contentValueOffer = SSZ.decode( - testData.recursive_gossip.content_value_offer.hexToSeqByte(), - ContractTrieNodeOffer, - ) + let contentKey = ContentKey + .decode(testData.recursive_gossip.content_key.hexToSeqByte().ByteList) + .get() + let contentValueOffer = ContractTrieNodeOffer + .decode(testData.recursive_gossip.content_value_offer.hexToSeqByte()) + .get() check: - validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValueOffer - ) - .isOk() + validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer).isOk() test "Validate invalid ContractTrieNodeOffer nodes - bad state roots": const file = testVectorDir / "contract_storage_trie_node.yaml" @@ -336,15 +323,15 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for i, testData in testCase: - var stateRoot = KeccakHash.init(stateRoots[i].hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(stateRoots[i].hexToSeqByte()) - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() let contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer) + ContractTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() - let res = validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer) check: res.isErr() res.error() == "hash of proof root node doesn't match the expected root hash" @@ -356,73 +343,75 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for i, testData in testCase: - var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + var contentValueOffer = ContractTrieNodeOffer + .decode(testData.content_value_offer.hexToSeqByte()) + .get() contentValueOffer.accountProof[0][0] += 1.byte - let res = validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer) check: res.isErr() res.error() == "hash of proof root node doesn't match the expected root hash" block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + var contentValueOffer = ContractTrieNodeOffer + .decode(testData.content_value_offer.hexToSeqByte()) + .get() contentValueOffer.storageProof[0][0] += 1.byte - let res = validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer) check: res.isErr() res.error() == "hash of proof root node doesn't match the expected root hash" block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + var contentValueOffer = ContractTrieNodeOffer + .decode(testData.content_value_offer.hexToSeqByte()) + .get() contentValueOffer.accountProof[^1][^1] += 1.byte check: - validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValueOffer - ) + validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer) .isErr() block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + var contentValueOffer = ContractTrieNodeOffer + .decode(testData.content_value_offer.hexToSeqByte()) + .get() contentValueOffer.storageProof[^1][^1] += 1.byte check: - validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValueOffer - ) + validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer) .isErr() block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() - var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer) + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() + var contentValueOffer = ContractTrieNodeOffer + .decode(testData.content_value_offer.hexToSeqByte()) + .get() contentValueOffer.accountProof[^2][^2] += 1.byte check: - validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValueOffer - ) + validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer) .isErr() # Contract bytecode offer validation tests @@ -434,17 +423,15 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for i, testData in testCase: - var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() let contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer) + ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() check: - validateOfferedContractCode( - stateRoot, contentKey.contractCodeKey, contentValueOffer - ) - .isOk() + validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer).isOk() test "Validate invalid ContractCodeOffer nodes - bad state root": const file = testVectorDir / "contract_bytecode.yaml" @@ -455,15 +442,14 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for i, testData in testCase: - var stateRoot = KeccakHash.init(stateRoots[i].hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(stateRoots[i].hexToSeqByte()) - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() let contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer) + ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() - let res = validateOfferedContractCode( - stateRoot, contentKey.contractCodeKey, contentValueOffer - ) + let res = validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer) check: res.isErr() res.error() == "hash of proof root node doesn't match the expected root hash" @@ -475,59 +461,57 @@ suite "State Validation": raiseAssert "Cannot read test vector: " & error for i, testData in testCase: - var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer) + ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() contentValueOffer.accountProof[0][0] += 1.byte - let res = validateOfferedContractCode( - stateRoot, contentKey.contractCodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer) check: res.isErr() res.error() == "hash of proof root node doesn't match the expected root hash" block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer) + ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() contentValueOffer.code[0] += 1.byte - let res = validateOfferedContractCode( - stateRoot, contentKey.contractCodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer) check: res.isErr() res.error() == "hash of offered bytecode doesn't match the expected code hash" block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer) + ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() contentValueOffer.accountProof[^1][^1] += 1.byte check: - validateOfferedContractCode( - stateRoot, contentKey.contractCodeKey, contentValueOffer - ) - .isErr() + validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer).isErr() block: - let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get() + let contentKey = + ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get() var contentValueOffer = - SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer) + ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get() contentValueOffer.code[^1] += 1.byte - let res = validateOfferedContractCode( - stateRoot, contentKey.contractCodeKey, contentValueOffer - ) + let res = + validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer) check: res.isErr() res.error() == "hash of offered bytecode doesn't match the expected code hash" @@ -549,17 +533,15 @@ suite "State Validation": if i == 1: continue - var stateRoot = KeccakHash.init(stateRoots[i].hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(stateRoots[i].hexToSeqByte()) for kv in testData.recursive_gossip: - let contentKey = decode(kv.content_key.hexToSeqByte().ByteList).get() + let contentKey = ContentKey.decode(kv.content_key.hexToSeqByte().ByteList).get() let contentValueOffer = - SSZ.decode(kv.content_value.hexToSeqByte(), AccountTrieNodeOffer) + AccountTrieNodeOffer.decode(kv.content_value.hexToSeqByte()).get() check: - validateOfferedAccountTrieNode( - stateRoot, contentKey.accountTrieNodeKey, contentValueOffer - ) + validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer) .isOk() test "Validate valid ContractTrieNodeOffer recursive gossip nodes": @@ -572,15 +554,13 @@ suite "State Validation": if i != 1: continue - var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte()) + var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte()) for kv in testData.recursive_gossip: - let contentKey = decode(kv.content_key.hexToSeqByte().ByteList).get() + let contentKey = ContentKey.decode(kv.content_key.hexToSeqByte().ByteList).get() let contentValueOffer = - SSZ.decode(kv.content_value.hexToSeqByte(), ContractTrieNodeOffer) + ContractTrieNodeOffer.decode(kv.content_value.hexToSeqByte()).get() check: - validateOfferedContractTrieNode( - stateRoot, contentKey.contractTrieNodeKey, contentValueOffer - ) + validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer) .isOk() diff --git a/fluffy/tests/state_network_tests/test_state_validation_genesis.nim b/fluffy/tests/state_network_tests/test_state_validation_genesis.nim index 46ca8744b..ad135bb6f 100644 --- a/fluffy/tests/state_network_tests/test_state_validation_genesis.nim +++ b/fluffy/tests/state_network_tests/test_state_validation_genesis.nim @@ -33,18 +33,15 @@ template checkValidProofsForExistingLeafs( nodeHash: keccakHash(accountProof[^1].asSeq()), ) accountTrieOffer = AccountTrieNodeOffer(proof: accountProof) - proofResult = validateOfferedAccountTrieNode( - accountState.rootHash(), accountTrieNodeKey, accountTrieOffer - ) + proofResult = + validateOffer(accountState.rootHash(), accountTrieNodeKey, accountTrieOffer) check proofResult.isOk() let contractCodeKey = ContractCodeKey(address: address, codeHash: acc.codeHash) contractCode = ContractCodeOffer(code: Bytecode.init(account.code), accountProof: accountProof) - codeResult = validateOfferedContractCode( - accountState.rootHash(), contractCodeKey, contractCode - ) + codeResult = validateOffer(accountState.rootHash(), contractCodeKey, contractCode) check codeResult.isOk() if account.code.len() > 0: @@ -62,7 +59,7 @@ template checkValidProofsForExistingLeafs( contractTrieOffer = ContractTrieNodeOffer( storageProof: storageProof, accountProof: accountProof ) - proofResult = validateOfferedContractTrieNode( + proofResult = validateOffer( accountState.rootHash(), contractTrieNodeKey, contractTrieOffer ) check proofResult.isOk() @@ -85,9 +82,8 @@ template checkInvalidProofsWithBadValue( accountProof[^1][^1] += 1 # bad account leaf value let accountTrieOffer = AccountTrieNodeOffer(proof: accountProof) - proofResult = validateOfferedAccountTrieNode( - accountState.rootHash(), accountTrieNodeKey, accountTrieOffer - ) + proofResult = + validateOffer(accountState.rootHash(), accountTrieNodeKey, accountTrieOffer) check proofResult.isErr() let @@ -96,9 +92,7 @@ template checkInvalidProofsWithBadValue( code: Bytecode.init(@[1u8, 2, 3]), # bad code value accountProof: accountProof, ) - codeResult = validateOfferedContractCode( - accountState.rootHash(), contractCodeKey, contractCode - ) + codeResult = validateOffer(accountState.rootHash(), contractCodeKey, contractCode) check codeResult.isErr() if account.code.len() > 0: @@ -118,7 +112,7 @@ template checkInvalidProofsWithBadValue( contractTrieOffer = ContractTrieNodeOffer( storageProof: storageProof, accountProof: accountProof ) - proofResult = validateOfferedContractTrieNode( + proofResult = validateOffer( accountState.rootHash(), contractTrieNodeKey, contractTrieOffer ) check proofResult.isErr() diff --git a/fluffy/tests/state_network_tests/test_state_validation_trieproof.nim b/fluffy/tests/state_network_tests/test_state_validation_trieproof.nim index b9130cb98..d7106a88a 100644 --- a/fluffy/tests/state_network_tests/test_state_validation_trieproof.nim +++ b/fluffy/tests/state_network_tests/test_state_validation_trieproof.nim @@ -16,7 +16,7 @@ import stint, nimcrypto/hash, eth/trie/[hexary, db, trie_defs], - ../../network/state/state_validation, + ../../network/state/[state_content, state_validation], ./state_test_helpers proc getKeyBytes(i: int): seq[byte] = @@ -88,8 +88,8 @@ suite "MPT trie proof verification": check: res.isOk() - test "Validate proof bytes": - var trie = initHexaryTrie(newMemoryDB(), isPruning = false) + test "Validate proof bytes - 3 keys": + var trie = initHexaryTrie(newMemoryDB()) trie.put("doe".toBytes, "reindeer".toBytes) trie.put("dog".toBytes, "puppy".toBytes) @@ -101,35 +101,25 @@ suite "MPT trie proof verification": let key = "doe".toBytes proof = trie.getTrieProof(key) - res = validateTrieProof(rootHash, key.asNibbles(), proof) - - check: - res.isOk() + check validateTrieProof(rootHash, key.asNibbles(), proof).isOk() block: let key = "dog".toBytes proof = trie.getTrieProof(key) - res = validateTrieProof(rootHash, key.asNibbles(), proof) - - check: - res.isOk() + check validateTrieProof(rootHash, key.asNibbles(), proof).isOk() block: let key = "dogglesworth".toBytes proof = trie.getTrieProof(key) - res = validateTrieProof(rootHash, key.asNibbles(), proof) - - check: - res.isOk() + check validateTrieProof(rootHash, key.asNibbles(), proof).isOk() block: let key = "dogg".toBytes proof = trie.getTrieProof(key) res = validateTrieProof(rootHash, key.asNibbles(), proof) - check: res.isErr() res.error() == "not enough nibbles to validate node prefix" @@ -139,7 +129,6 @@ suite "MPT trie proof verification": key = "dogz".toBytes proof = trie.getTrieProof(key) res = validateTrieProof(rootHash, key.asNibbles(), proof) - check: res.isErr() res.error() == "path contains more nibbles than expected for proof" @@ -149,7 +138,6 @@ suite "MPT trie proof verification": key = "doe".toBytes proof = newSeq[seq[byte]]().asTrieProof() res = validateTrieProof(rootHash, key.asNibbles(), proof) - check: res.isErr() res.error() == "proof is empty" @@ -159,7 +147,53 @@ suite "MPT trie proof verification": key = "doe".toBytes proof = @["aaa".toBytes, "ccc".toBytes].asTrieProof() res = validateTrieProof(rootHash, key.asNibbles(), proof) - check: res.isErr() res.error() == "hash of proof root node doesn't match the expected root hash" + + test "Validate proof bytes - 4 keys": + var trie = initHexaryTrie(newMemoryDB()) + + let + # leaf nodes + kv1 = "0xa7113550".hexToSeqByte() + kv2 = "0xa77d3370".hexToSeqByte() + kv3 = "0xa7f93650".hexToSeqByte() + kv4 = "0xa77d3970".hexToSeqByte() + + kv5 = "".hexToSeqByte() # root/first extension node + kv6 = "0xa7".hexToSeqByte() # first branch node + + # leaf nodes without key ending + kv7 = "0xa77d33".hexToSeqByte() + kv8 = "0xa77d39".hexToSeqByte() + + # failure cases + kv9 = "0xa0".hexToSeqByte() + kv10 = "0xa77d".hexToSeqByte() + kv11 = "0xa71135".hexToSeqByte() + kv12 = "0xa711355000".hexToSeqByte() + kv13 = "0xa711".hexToSeqByte() + + trie.put(kv1, kv1) + trie.put(kv2, kv2) + trie.put(kv3, kv3) + trie.put(kv4, kv4) + + let rootHash = trie.rootHash + + check: + validateTrieProof(rootHash, kv1.asNibbles(), trie.getTrieProof(kv1)).isOk() + validateTrieProof(rootHash, kv2.asNibbles(), trie.getTrieProof(kv2)).isOk() + validateTrieProof(rootHash, kv3.asNibbles(), trie.getTrieProof(kv3)).isOk() + validateTrieProof(rootHash, kv4.asNibbles(), trie.getTrieProof(kv4)).isOk() + validateTrieProof(rootHash, kv5.asNibbles(), trie.getTrieProof(kv5)).isOk() + validateTrieProof(rootHash, kv6.asNibbles(), trie.getTrieProof(kv6)).isOk() + validateTrieProof(rootHash, kv7.asNibbles(), trie.getTrieProof(kv7)).isOk() + validateTrieProof(rootHash, kv8.asNibbles(), trie.getTrieProof(kv8)).isOk() + + validateTrieProof(rootHash, kv9.asNibbles(), trie.getTrieProof(kv9)).isErr() + validateTrieProof(rootHash, kv10.asNibbles(), trie.getTrieProof(kv10)).isErr() + validateTrieProof(rootHash, kv11.asNibbles(), trie.getTrieProof(kv11)).isErr() + validateTrieProof(rootHash, kv12.asNibbles(), trie.getTrieProof(kv12)).isErr() + validateTrieProof(rootHash, kv13.asNibbles(), trie.getTrieProof(kv13)).isErr()