Fluffy state network refactor (#2200)
* Add additional getTrieProof test. * Refactor state network: Removed variant objects for values and split state_content into multiple files.
This commit is contained in:
parent
2629d412d7
commit
e566e89d03
|
@ -14,7 +14,7 @@ import
|
||||||
stew/[io2, arrayops],
|
stew/[io2, arrayops],
|
||||||
eth/p2p/discoveryv5/enr
|
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)
|
doAssert(hash.len() == 32)
|
||||||
KeccakHash(data: array[32, byte].initCopyFrom(hash))
|
KeccakHash(data: array[32, byte].initCopyFrom(hash))
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -5,273 +5,6 @@
|
||||||
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
# * 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.
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||||
|
|
||||||
# As per spec:
|
import ./content/content_keys, ./content/content_values, ./content/nibbles
|
||||||
# https://github.com/ethereum/portal-network-specs/blob/master/state-network.md#content-keys-and-content-ids
|
|
||||||
|
|
||||||
{.push raises: [].}
|
export content_keys, content_values, nibbles
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
@ -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]
|
||||||
|
# )
|
|
@ -16,7 +16,8 @@ import
|
||||||
../history/history_network,
|
../history/history_network,
|
||||||
../wire/[portal_protocol, portal_stream, portal_protocol_config],
|
../wire/[portal_protocol, portal_stream, portal_protocol_config],
|
||||||
./state_content,
|
./state_content,
|
||||||
./state_validation
|
./state_validation,
|
||||||
|
./state_gossip
|
||||||
|
|
||||||
export results
|
export results
|
||||||
|
|
||||||
|
@ -35,228 +36,6 @@ type StateNetwork* = ref object
|
||||||
func toContentIdHandler(contentKey: ByteList): results.Opt[ContentId] =
|
func toContentIdHandler(contentKey: ByteList): results.Opt[ContentId] =
|
||||||
ok(toContentId(contentKey))
|
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*(
|
proc new*(
|
||||||
T: type StateNetwork,
|
T: type StateNetwork,
|
||||||
baseProtocol: protocol.Protocol,
|
baseProtocol: protocol.Protocol,
|
||||||
|
@ -290,34 +69,161 @@ proc new*(
|
||||||
historyNetwork: historyNetwork,
|
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.} =
|
proc processContentLoop(n: StateNetwork) {.async.} =
|
||||||
try:
|
try:
|
||||||
while true:
|
while true:
|
||||||
let (maybeSrcNodeId, contentKeys, contentValues) = await n.contentQueue.popFirst()
|
let (maybeSrcNodeId, contentKeys, contentValues) = await n.contentQueue.popFirst()
|
||||||
for i, contentValue in contentValues:
|
for i, contentValueBytes in contentValues:
|
||||||
let
|
let
|
||||||
contentKey = contentKeys[i]
|
contentKeyBytes = contentKeys[i]
|
||||||
(decodedKey, decodedValue) = decodeKV(contentKey, contentValue).valueOr:
|
contentKey = decodeKey(contentKeyBytes).valueOr:
|
||||||
error "Unable to decode offered Key/Value"
|
error "Unable to decode offered content key", contentKeyBytes
|
||||||
continue
|
continue
|
||||||
|
|
||||||
(await n.validateContent(decodedKey, decodedValue)).isOkOr:
|
let offerRes =
|
||||||
error "Received offered content failed validation", contentKey, error
|
case contentKey.contentType
|
||||||
continue
|
of unused:
|
||||||
|
error "Received content with unused content type"
|
||||||
let
|
|
||||||
valueForRetrieval = decodedValue.offerContentToRetrievalContent().encode()
|
|
||||||
contentId = n.portalProtocol.toContentId(contentKey).valueOr:
|
|
||||||
error "Received offered content with invalid content key", contentKey
|
|
||||||
continue
|
continue
|
||||||
|
of accountTrieNode:
|
||||||
|
let contentValue = AccountTrieNodeOffer.decode(contentValueBytes).valueOr:
|
||||||
|
error "Unable to decode offered AccountTrieNodeOffer content value"
|
||||||
|
continue
|
||||||
|
|
||||||
n.portalProtocol.storeContent(contentKey, contentId, valueForRetrieval)
|
await processOffer(
|
||||||
info "Received offered content validated successfully", contentKey
|
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(
|
await processOffer(
|
||||||
n.portalProtocol, maybeSrcNodeId, contentKey, decodedKey, contentValue,
|
n, maybeSrcNodeId, contentKeyBytes, contentKey.contractTrieNodeKey,
|
||||||
decodedValue,
|
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:
|
except CancelledError:
|
||||||
trace "processContentLoop canceled"
|
trace "processContentLoop canceled"
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import
|
||||||
../../common/[common_types, common_utils],
|
../../common/[common_types, common_utils],
|
||||||
./state_content
|
./state_content
|
||||||
|
|
||||||
export results
|
export results, state_content
|
||||||
|
|
||||||
# private functions
|
# private functions
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ proc isValidNextNode(thisNodeRlp: Rlp, rlpIdx: int, nextNode: TrieNode): bool =
|
||||||
let hash = hashOrShortRlp.toBytes()
|
let hash = hashOrShortRlp.toBytes()
|
||||||
if hash.len() != 32:
|
if hash.len() != 32:
|
||||||
return false
|
return false
|
||||||
KeccakHash.init(hash)
|
KeccakHash.fromBytes(hash)
|
||||||
|
|
||||||
nextNode.hashEquals(nextHash)
|
nextNode.hashEquals(nextHash)
|
||||||
|
|
||||||
|
@ -48,6 +48,23 @@ proc decodePrefix(nodePrefixRlp: Rlp): (byte, bool, Nibbles) =
|
||||||
|
|
||||||
(firstNibble.byte, isLeaf, 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*(
|
proc validateTrieProof*(
|
||||||
expectedRootHash: KeccakHash, path: Nibbles, proof: TrieProof
|
expectedRootHash: KeccakHash, path: Nibbles, proof: TrieProof
|
||||||
): Result[void, string] =
|
): Result[void, string] =
|
||||||
|
@ -122,24 +139,7 @@ proc validateTrieProof*(
|
||||||
else:
|
else:
|
||||||
ok()
|
ok()
|
||||||
|
|
||||||
proc rlpDecodeAccountTrieNode(accountNode: TrieNode): Result[Account, string] =
|
proc validateRetrieval*(
|
||||||
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*(
|
|
||||||
trustedAccountTrieNodeKey: AccountTrieNodeKey,
|
trustedAccountTrieNodeKey: AccountTrieNodeKey,
|
||||||
accountTrieNode: AccountTrieNodeRetrieval,
|
accountTrieNode: AccountTrieNodeRetrieval,
|
||||||
): Result[void, string] =
|
): Result[void, string] =
|
||||||
|
@ -148,7 +148,7 @@ proc validateFetchedAccountTrieNode*(
|
||||||
else:
|
else:
|
||||||
err("hash of fetched account trie node doesn't match the expected node hash")
|
err("hash of fetched account trie node doesn't match the expected node hash")
|
||||||
|
|
||||||
proc validateFetchedContractTrieNode*(
|
proc validateRetrieval*(
|
||||||
trustedContractTrieNodeKey: ContractTrieNodeKey,
|
trustedContractTrieNodeKey: ContractTrieNodeKey,
|
||||||
contractTrieNode: ContractTrieNodeRetrieval,
|
contractTrieNode: ContractTrieNodeRetrieval,
|
||||||
): Result[void, string] =
|
): Result[void, string] =
|
||||||
|
@ -157,7 +157,7 @@ proc validateFetchedContractTrieNode*(
|
||||||
else:
|
else:
|
||||||
err("hash of fetched contract trie node doesn't match the expected node hash")
|
err("hash of fetched contract trie node doesn't match the expected node hash")
|
||||||
|
|
||||||
proc validateFetchedContractCode*(
|
proc validateRetrieval*(
|
||||||
trustedContractCodeKey: ContractCodeKey, contractCode: ContractCodeRetrieval
|
trustedContractCodeKey: ContractCodeKey, contractCode: ContractCodeRetrieval
|
||||||
): Result[void, string] =
|
): Result[void, string] =
|
||||||
if contractCode.code.hashEquals(trustedContractCodeKey.codeHash):
|
if contractCode.code.hashEquals(trustedContractCodeKey.codeHash):
|
||||||
|
@ -165,7 +165,7 @@ proc validateFetchedContractCode*(
|
||||||
else:
|
else:
|
||||||
err("hash of fetched bytecode doesn't match the expected code hash")
|
err("hash of fetched bytecode doesn't match the expected code hash")
|
||||||
|
|
||||||
proc validateOfferedAccountTrieNode*(
|
proc validateOffer*(
|
||||||
trustedStateRoot: KeccakHash,
|
trustedStateRoot: KeccakHash,
|
||||||
accountTrieNodeKey: AccountTrieNodeKey,
|
accountTrieNodeKey: AccountTrieNodeKey,
|
||||||
accountTrieNode: AccountTrieNodeOffer,
|
accountTrieNode: AccountTrieNodeOffer,
|
||||||
|
@ -177,7 +177,7 @@ proc validateOfferedAccountTrieNode*(
|
||||||
else:
|
else:
|
||||||
err("hash of offered account trie node doesn't match the expected node hash")
|
err("hash of offered account trie node doesn't match the expected node hash")
|
||||||
|
|
||||||
proc validateOfferedContractTrieNode*(
|
proc validateOffer*(
|
||||||
trustedStateRoot: KeccakHash,
|
trustedStateRoot: KeccakHash,
|
||||||
contractTrieNodeKey: ContractTrieNodeKey,
|
contractTrieNodeKey: ContractTrieNodeKey,
|
||||||
contractTrieNode: ContractTrieNodeOffer,
|
contractTrieNode: ContractTrieNodeOffer,
|
||||||
|
@ -198,7 +198,7 @@ proc validateOfferedContractTrieNode*(
|
||||||
else:
|
else:
|
||||||
err("hash of offered contract trie node doesn't match the expected node hash")
|
err("hash of offered contract trie node doesn't match the expected node hash")
|
||||||
|
|
||||||
proc validateOfferedContractCode*(
|
proc validateOffer*(
|
||||||
trustedStateRoot: KeccakHash,
|
trustedStateRoot: KeccakHash,
|
||||||
contractCodeKey: ContractCodeKey,
|
contractCodeKey: ContractCodeKey,
|
||||||
contractCode: ContractCodeOffer,
|
contractCode: ContractCodeOffer,
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import
|
import
|
||||||
./test_state_content_keys,
|
./test_state_content_keys,
|
||||||
./test_state_content_values,
|
./test_state_content_values,
|
||||||
|
./test_state_content_nibbles,
|
||||||
./test_state_network,
|
./test_state_network,
|
||||||
#./test_state_network_gossip,
|
#./test_state_network_gossip,
|
||||||
./test_state_validation,
|
./test_state_validation,
|
||||||
|
|
|
@ -14,90 +14,6 @@ import
|
||||||
const testVectorDir = "./vendor/portal-spec-tests/tests/mainnet/state/serialization/"
|
const testVectorDir = "./vendor/portal-spec-tests/tests/mainnet/state/serialization/"
|
||||||
|
|
||||||
suite "State Content Keys":
|
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":
|
test "Encode/decode AccountTrieNodeKey":
|
||||||
const file = testVectorDir & "account_trie_node_key.yaml"
|
const file = testVectorDir & "account_trie_node_key.yaml"
|
||||||
|
|
||||||
|
@ -120,7 +36,7 @@ suite "State Content Keys":
|
||||||
encoded.asSeq() == testCase.content_key.hexToSeqByte()
|
encoded.asSeq() == testCase.content_key.hexToSeqByte()
|
||||||
encoded.toContentId().toBytesBE() == testCase.content_id.hexToSeqByte()
|
encoded.toContentId().toBytesBE() == testCase.content_id.hexToSeqByte()
|
||||||
|
|
||||||
let decoded = encoded.decode()
|
let decoded = ContentKey.decode(encoded)
|
||||||
check:
|
check:
|
||||||
decoded.isOk()
|
decoded.isOk()
|
||||||
decoded.value().contentType == accountTrieNode
|
decoded.value().contentType == accountTrieNode
|
||||||
|
@ -151,7 +67,7 @@ suite "State Content Keys":
|
||||||
encoded.asSeq() == testCase.content_key.hexToSeqByte()
|
encoded.asSeq() == testCase.content_key.hexToSeqByte()
|
||||||
encoded.toContentId().toBytesBE() == testCase.content_id.hexToSeqByte()
|
encoded.toContentId().toBytesBE() == testCase.content_id.hexToSeqByte()
|
||||||
|
|
||||||
let decoded = encoded.decode()
|
let decoded = ContentKey.decode(encoded)
|
||||||
check:
|
check:
|
||||||
decoded.isOk()
|
decoded.isOk()
|
||||||
decoded.value().contentType == contractTrieNode
|
decoded.value().contentType == contractTrieNode
|
||||||
|
@ -180,7 +96,7 @@ suite "State Content Keys":
|
||||||
encoded.asSeq() == testCase.content_key.hexToSeqByte()
|
encoded.asSeq() == testCase.content_key.hexToSeqByte()
|
||||||
encoded.toContentId().toBytesBE() == testCase.content_id.hexToSeqByte()
|
encoded.toContentId().toBytesBE() == testCase.content_id.hexToSeqByte()
|
||||||
|
|
||||||
let decoded = encoded.decode()
|
let decoded = ContentKey.decode(encoded)
|
||||||
check:
|
check:
|
||||||
decoded.isOk()
|
decoded.isOk()
|
||||||
decoded.value().contentType == contractCode
|
decoded.value().contentType == contractCode
|
||||||
|
@ -189,24 +105,24 @@ suite "State Content Keys":
|
||||||
|
|
||||||
test "Invalid prefix - 0 value":
|
test "Invalid prefix - 0 value":
|
||||||
let encoded = ByteList.init(@[byte 0x00])
|
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":
|
test "Invalid prefix - before valid range":
|
||||||
let encoded = ByteList.init(@[byte 0x01])
|
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":
|
test "Invalid prefix - after valid range":
|
||||||
let encoded = ByteList.init(@[byte 0x25])
|
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":
|
test "Invalid key - empty input":
|
||||||
let encoded = ByteList.init(@[])
|
let encoded = ByteList.init(@[])
|
||||||
let decoded = decode(encoded)
|
let decoded = ContentKey.decode(encoded)
|
||||||
|
|
||||||
check decoded.isNone()
|
check decoded.isErr()
|
||||||
|
|
|
@ -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
|
|
@ -30,11 +30,11 @@ suite "State Content Values":
|
||||||
blockHash = BlockHash.fromHex(testCase.block_hash)
|
blockHash = BlockHash.fromHex(testCase.block_hash)
|
||||||
proof =
|
proof =
|
||||||
TrieProof.init(testCase.proof.map((hex) => TrieNode.init(hex.hexToSeqByte())))
|
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()
|
expected = testCase.content_value.hexToSeqByte()
|
||||||
decoded = SSZ.decode(encoded, AccountTrieNodeOffer)
|
decoded = AccountTrieNodeOffer.decode(encoded).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
encoded == expected
|
encoded == expected
|
||||||
|
@ -54,9 +54,9 @@ suite "State Content Values":
|
||||||
node = TrieNode.init(testCase.trie_node.hexToSeqByte())
|
node = TrieNode.init(testCase.trie_node.hexToSeqByte())
|
||||||
accountTrieNodeRetrieval = AccountTrieNodeRetrieval(node: node)
|
accountTrieNodeRetrieval = AccountTrieNodeRetrieval(node: node)
|
||||||
|
|
||||||
encoded = SSZ.encode(accountTrieNodeRetrieval)
|
encoded = accountTrieNodeRetrieval.encode()
|
||||||
expected = testCase.content_value.hexToSeqByte()
|
expected = testCase.content_value.hexToSeqByte()
|
||||||
decoded = SSZ.decode(encoded, AccountTrieNodeRetrieval)
|
decoded = AccountTrieNodeRetrieval.decode(encoded).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
encoded == expected
|
encoded == expected
|
||||||
|
@ -86,9 +86,9 @@ suite "State Content Values":
|
||||||
blockHash: blockHash, storage_proof: storageProof, account_proof: accountProof
|
blockHash: blockHash, storage_proof: storageProof, account_proof: accountProof
|
||||||
)
|
)
|
||||||
|
|
||||||
encoded = SSZ.encode(contractTrieNodeOffer)
|
encoded = contractTrieNodeOffer.encode()
|
||||||
expected = testCase.content_value.hexToSeqByte()
|
expected = testCase.content_value.hexToSeqByte()
|
||||||
decoded = SSZ.decode(encoded, ContractTrieNodeOffer)
|
decoded = ContractTrieNodeOffer.decode(encoded).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
encoded == expected
|
encoded == expected
|
||||||
|
@ -111,9 +111,9 @@ suite "State Content Values":
|
||||||
node = TrieNode.init(testCase.trie_node.hexToSeqByte())
|
node = TrieNode.init(testCase.trie_node.hexToSeqByte())
|
||||||
contractTrieNodeRetrieval = ContractTrieNodeRetrieval(node: node)
|
contractTrieNodeRetrieval = ContractTrieNodeRetrieval(node: node)
|
||||||
|
|
||||||
encoded = SSZ.encode(contractTrieNodeRetrieval)
|
encoded = contractTrieNodeRetrieval.encode()
|
||||||
expected = testCase.content_value.hexToSeqByte()
|
expected = testCase.content_value.hexToSeqByte()
|
||||||
decoded = SSZ.decode(encoded, ContractTrieNodeRetrieval)
|
decoded = ContractTrieNodeRetrieval.decode(encoded).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
encoded == expected
|
encoded == expected
|
||||||
|
@ -140,9 +140,9 @@ suite "State Content Values":
|
||||||
contractCodeOffer =
|
contractCodeOffer =
|
||||||
ContractCodeOffer(code: code, blockHash: blockHash, accountProof: accountProof)
|
ContractCodeOffer(code: code, blockHash: blockHash, accountProof: accountProof)
|
||||||
|
|
||||||
encoded = SSZ.encode(contractCodeOffer)
|
encoded = contractCodeOffer.encode()
|
||||||
expected = testCase.content_value.hexToSeqByte()
|
expected = testCase.content_value.hexToSeqByte()
|
||||||
decoded = SSZ.decode(encoded, ContractCodeOffer)
|
decoded = ContractCodeOffer.decode(encoded).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
encoded == expected
|
encoded == expected
|
||||||
|
@ -162,9 +162,9 @@ suite "State Content Values":
|
||||||
code = Bytecode.init(testCase.bytecode.hexToSeqByte())
|
code = Bytecode.init(testCase.bytecode.hexToSeqByte())
|
||||||
contractCodeRetrieval = ContractCodeRetrieval(code: code)
|
contractCodeRetrieval = ContractCodeRetrieval(code: code)
|
||||||
|
|
||||||
encoded = SSZ.encode(contractCodeRetrieval)
|
encoded = contractCodeRetrieval.encode()
|
||||||
expected = testCase.content_value.hexToSeqByte()
|
expected = testCase.content_value.hexToSeqByte()
|
||||||
decoded = SSZ.decode(encoded, ContractCodeRetrieval)
|
decoded = ContractCodeRetrieval.decode(encoded).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
encoded == expected
|
encoded == expected
|
||||||
|
|
|
@ -53,11 +53,8 @@ procSuite "State Network":
|
||||||
contentKey = ContentKey(
|
contentKey = ContentKey(
|
||||||
contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey
|
contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey
|
||||||
)
|
)
|
||||||
contentId = toContentId(contentKey)
|
contentId = toContentId(contentKey.encode())
|
||||||
value = RetrievalContentValue(
|
value = AccountTrieNodeRetrieval(node: TrieNode.init(v))
|
||||||
contentType: accountTrieNode,
|
|
||||||
accountTrieNode: AccountTrieNodeRetrieval(node: TrieNode.init(v)),
|
|
||||||
)
|
|
||||||
|
|
||||||
discard proto1.contentDB.put(
|
discard proto1.contentDB.put(
|
||||||
contentId, value.encode(), proto1.portalProtocol.localNode.id
|
contentId, value.encode(), proto1.portalProtocol.localNode.id
|
||||||
|
@ -72,7 +69,7 @@ procSuite "State Network":
|
||||||
contentKey = ContentKey(
|
contentKey = ContentKey(
|
||||||
contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey
|
contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey
|
||||||
)
|
)
|
||||||
contentId = toContentId(contentKey)
|
contentId = toContentId(contentKey.encode())
|
||||||
|
|
||||||
# Note: GetContent and thus the lookup here is not really needed, as we
|
# Note: GetContent and thus the lookup here is not really needed, as we
|
||||||
# only have to request data to one node.
|
# only have to request data to one node.
|
||||||
|
@ -129,11 +126,8 @@ procSuite "State Network":
|
||||||
contentKey = ContentKey(
|
contentKey = ContentKey(
|
||||||
contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey
|
contentType: accountTrieNode, accountTrieNodeKey: accountTrieNodeKey
|
||||||
)
|
)
|
||||||
contentId = toContentId(contentKey)
|
contentId = toContentId(contentKey.encode())
|
||||||
value = RetrievalContentValue(
|
value = AccountTrieNodeRetrieval(node: TrieNode.init(v))
|
||||||
contentType: accountTrieNode,
|
|
||||||
accountTrieNode: AccountTrieNodeRetrieval(node: TrieNode.init(v)),
|
|
||||||
)
|
|
||||||
|
|
||||||
discard proto2.contentDB.put(
|
discard proto2.contentDB.put(
|
||||||
contentId, value.encode(), proto2.portalProtocol.localNode.id
|
contentId, value.encode(), proto2.portalProtocol.localNode.id
|
||||||
|
|
|
@ -13,7 +13,7 @@ import
|
||||||
eth/p2p/discoveryv5/protocol as discv5_protocol,
|
eth/p2p/discoveryv5/protocol as discv5_protocol,
|
||||||
../../network/wire/[portal_protocol, portal_stream],
|
../../network/wire/[portal_protocol, portal_stream],
|
||||||
../../network/history/[history_content, history_network],
|
../../network/history/[history_content, history_network],
|
||||||
../../network/state/[state_content, state_network],
|
../../network/state/[state_content, state_network, state_gossip],
|
||||||
../../database/content_db,
|
../../database/content_db,
|
||||||
.././test_helpers,
|
.././test_helpers,
|
||||||
../../eth_data/yaml_utils
|
../../eth_data/yaml_utils
|
||||||
|
@ -74,7 +74,7 @@ procSuite "State Network Gossip":
|
||||||
header: ByteList.init(headerRlp), proof: BlockHeaderProof.init()
|
header: ByteList.init(headerRlp), proof: BlockHeaderProof.init()
|
||||||
)
|
)
|
||||||
value = recursiveGossipSteps[0].content_value.hexToSeqByte()
|
value = recursiveGossipSteps[0].content_value.hexToSeqByte()
|
||||||
decodedValue = SSZ.decode(value, AccountTrieNodeOffer)
|
decodedValue = AccountTrieNodeOffer.decode(value).get()
|
||||||
contentKey = history_content.ContentKey
|
contentKey = history_content.ContentKey
|
||||||
.init(history_content.ContentType.blockHeader, decodedValue.blockHash)
|
.init(history_content.ContentType.blockHeader, decodedValue.blockHash)
|
||||||
.encode()
|
.encode()
|
||||||
|
@ -89,28 +89,22 @@ procSuite "State Network Gossip":
|
||||||
nextNode = clients[i + 1]
|
nextNode = clients[i + 1]
|
||||||
|
|
||||||
key = ByteList.init(pair.content_key.hexToSeqByte())
|
key = ByteList.init(pair.content_key.hexToSeqByte())
|
||||||
decodedKey = state_content.decode(key).valueOr:
|
decodedKey = state_content.ContentKey.decode(key).valueOr:
|
||||||
raiseAssert "Cannot decode key"
|
raiseAssert "Cannot decode key"
|
||||||
|
|
||||||
nextKey = ByteList.init(recursiveGossipSteps[1].content_key.hexToSeqByte())
|
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"
|
raiseAssert "Cannot decode key"
|
||||||
|
|
||||||
value = pair.content_value.hexToSeqByte()
|
value = pair.content_value.hexToSeqByte()
|
||||||
decodedValue = SSZ.decode(value, AccountTrieNodeOffer)
|
decodedValue = AccountTrieNodeOffer.decode(value).get()
|
||||||
offerValue =
|
|
||||||
OfferContentValue(contentType: accountTrieNode, accountTrieNode: decodedValue)
|
|
||||||
|
|
||||||
nextValue = recursiveGossipSteps[1].content_value.hexToSeqByte()
|
nextValue = recursiveGossipSteps[1].content_value.hexToSeqByte()
|
||||||
nextDecodedValue = SSZ.decode(nextValue, AccountTrieNodeOffer)
|
nextDecodedValue = AccountTrieNodeOffer.decode(nextValue).get()
|
||||||
nextOfferValue = OfferContentValue(
|
nextRetrievalValue = nextDecodedValue.toRetrievalValue().encode()
|
||||||
contentType: accountTrieNode, accountTrieNode: nextDecodedValue
|
|
||||||
)
|
|
||||||
nextRetrievalValue = nextOfferValue.offerContentToRetrievalContent().encode()
|
|
||||||
|
|
||||||
if i == 0:
|
if i == 0:
|
||||||
await currentNode.portalProtocol.gossipContent(
|
await currentNode.portalProtocol.gossipOffer(
|
||||||
Opt.none(NodeId), key, decodedKey, value, offerValue
|
Opt.none(NodeId), decodedKey.accountTrieNodeKey, decodedValue
|
||||||
)
|
)
|
||||||
|
|
||||||
await sleepAsync(100.milliseconds) #TODO figure out how to get rid of this sleep
|
await sleepAsync(100.milliseconds) #TODO figure out how to get rid of this sleep
|
||||||
|
|
|
@ -60,16 +60,14 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for testData in testCase:
|
for testData in testCase:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
let contentValueRetrieval = SSZ.decode(
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
testData.content_value_retrieval.hexToSeqByte(), AccountTrieNodeRetrieval
|
let contentValueRetrieval = AccountTrieNodeRetrieval
|
||||||
)
|
.decode(testData.content_value_retrieval.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateFetchedAccountTrieNode(
|
validateRetrieval(contentKey.accountTrieNodeKey, contentValueRetrieval).isOk()
|
||||||
contentKey.accountTrieNodeKey, contentValueRetrieval
|
|
||||||
)
|
|
||||||
.isOk()
|
|
||||||
|
|
||||||
test "Validate invalid AccountTrieNodeRetrieval nodes":
|
test "Validate invalid AccountTrieNodeRetrieval nodes":
|
||||||
const file = testVectorDir / "account_trie_node.yaml"
|
const file = testVectorDir / "account_trie_node.yaml"
|
||||||
|
@ -78,16 +76,15 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for testData in testCase:
|
for testData in testCase:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
var contentValueRetrieval = SSZ.decode(
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
testData.content_value_retrieval.hexToSeqByte(), AccountTrieNodeRetrieval
|
var contentValueRetrieval = AccountTrieNodeRetrieval
|
||||||
)
|
.decode(testData.content_value_retrieval.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
contentValueRetrieval.node[^1] += 1 # Modify node hash
|
contentValueRetrieval.node[^1] += 1 # Modify node hash
|
||||||
|
|
||||||
let res = validateFetchedAccountTrieNode(
|
let res = validateRetrieval(contentKey.accountTrieNodeKey, contentValueRetrieval)
|
||||||
contentKey.accountTrieNodeKey, contentValueRetrieval
|
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() ==
|
res.error() ==
|
||||||
|
@ -100,16 +97,14 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for testData in testCase:
|
for testData in testCase:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
let contentValueRetrieval = SSZ.decode(
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
testData.content_value_retrieval.hexToSeqByte(), ContractTrieNodeRetrieval
|
let contentValueRetrieval = ContractTrieNodeRetrieval
|
||||||
)
|
.decode(testData.content_value_retrieval.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateFetchedContractTrieNode(
|
validateRetrieval(contentKey.contractTrieNodeKey, contentValueRetrieval).isOk()
|
||||||
contentKey.contractTrieNodeKey, contentValueRetrieval
|
|
||||||
)
|
|
||||||
.isOk()
|
|
||||||
|
|
||||||
test "Validate invalid ContractTrieNodeRetrieval nodes":
|
test "Validate invalid ContractTrieNodeRetrieval nodes":
|
||||||
const file = testVectorDir / "contract_storage_trie_node.yaml"
|
const file = testVectorDir / "contract_storage_trie_node.yaml"
|
||||||
|
@ -118,16 +113,15 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for testData in testCase:
|
for testData in testCase:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
var contentValueRetrieval = SSZ.decode(
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
testData.content_value_retrieval.hexToSeqByte(), ContractTrieNodeRetrieval
|
var contentValueRetrieval = ContractTrieNodeRetrieval
|
||||||
)
|
.decode(testData.content_value_retrieval.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
contentValueRetrieval.node[^1] += 1 # Modify node hash
|
contentValueRetrieval.node[^1] += 1 # Modify node hash
|
||||||
|
|
||||||
let res = validateFetchedContractTrieNode(
|
let res = validateRetrieval(contentKey.contractTrieNodeKey, contentValueRetrieval)
|
||||||
contentKey.contractTrieNodeKey, contentValueRetrieval
|
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() ==
|
res.error() ==
|
||||||
|
@ -140,14 +134,14 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for testData in testCase:
|
for testData in testCase:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
let contentValueRetrieval = SSZ.decode(
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
testData.content_value_retrieval.hexToSeqByte(), ContractCodeRetrieval
|
let contentValueRetrieval = ContractCodeRetrieval
|
||||||
)
|
.decode(testData.content_value_retrieval.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateFetchedContractCode(contentKey.contractCodeKey, contentValueRetrieval)
|
validateRetrieval(contentKey.contractCodeKey, contentValueRetrieval).isOk()
|
||||||
.isOk()
|
|
||||||
|
|
||||||
test "Validate invalid ContractCodeRetrieval nodes":
|
test "Validate invalid ContractCodeRetrieval nodes":
|
||||||
const file = testVectorDir / "contract_bytecode.yaml"
|
const file = testVectorDir / "contract_bytecode.yaml"
|
||||||
|
@ -156,15 +150,15 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for testData in testCase:
|
for testData in testCase:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
var contentValueRetrieval = SSZ.decode(
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
testData.content_value_retrieval.hexToSeqByte(), ContractCodeRetrieval
|
var contentValueRetrieval = ContractCodeRetrieval
|
||||||
)
|
.decode(testData.content_value_retrieval.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
contentValueRetrieval.code[^1] += 1 # Modify node hash
|
contentValueRetrieval.code[^1] += 1 # Modify node hash
|
||||||
|
|
||||||
let res =
|
let res = validateRetrieval(contentKey.contractCodeKey, contentValueRetrieval)
|
||||||
validateFetchedContractCode(contentKey.contractCodeKey, contentValueRetrieval)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of fetched bytecode doesn't match the expected code hash"
|
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
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for i, testData in testCase:
|
for i, testData in testCase:
|
||||||
var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte())
|
var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte())
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
let contentValueOffer =
|
let contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer)
|
AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedAccountTrieNode(
|
validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer)
|
||||||
stateRoot, contentKey.accountTrieNodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isOk()
|
.isOk()
|
||||||
|
|
||||||
if i == 1:
|
if i == 1:
|
||||||
continue # second test case only has root node and no recursive gossip
|
continue # second test case only has root node and no recursive gossip
|
||||||
|
|
||||||
let contentKey =
|
let contentKey = ContentKey
|
||||||
decode(testData.recursive_gossip.content_key.hexToSeqByte().ByteList).get()
|
.decode(testData.recursive_gossip.content_key.hexToSeqByte().ByteList)
|
||||||
let contentValueOffer = SSZ.decode(
|
.get()
|
||||||
testData.recursive_gossip.content_value_offer.hexToSeqByte(),
|
let contentValueOffer = AccountTrieNodeOffer
|
||||||
AccountTrieNodeOffer,
|
.decode(testData.recursive_gossip.content_value_offer.hexToSeqByte())
|
||||||
)
|
.get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedAccountTrieNode(
|
validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer).isOk()
|
||||||
stateRoot, contentKey.accountTrieNodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isOk()
|
|
||||||
|
|
||||||
test "Validate invalid AccountTrieNodeOffer nodes - bad state roots":
|
test "Validate invalid AccountTrieNodeOffer nodes - bad state roots":
|
||||||
const file = testVectorDir / "account_trie_node.yaml"
|
const file = testVectorDir / "account_trie_node.yaml"
|
||||||
|
@ -219,15 +209,15 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for i, testData in testCase:
|
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 =
|
let contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer)
|
AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
let res = validateOfferedAccountTrieNode(
|
let res =
|
||||||
stateRoot, contentKey.accountTrieNodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of proof root node doesn't match the expected root hash"
|
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
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for i, testData in testCase:
|
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 =
|
var contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer)
|
AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
contentValueOffer.proof[0][0] += 1.byte
|
contentValueOffer.proof[0][0] += 1.byte
|
||||||
|
|
||||||
let res = validateOfferedAccountTrieNode(
|
let res =
|
||||||
stateRoot, contentKey.accountTrieNodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of proof root node doesn't match the expected root hash"
|
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:
|
for i, testData in testCase:
|
||||||
if i == 1:
|
if i == 1:
|
||||||
continue # second test case only has root node
|
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 =
|
var contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer)
|
AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
contentValueOffer.proof[^2][^2] += 1.byte
|
contentValueOffer.proof[^2][^2] += 1.byte
|
||||||
|
|
||||||
let res = validateOfferedAccountTrieNode(
|
let res =
|
||||||
stateRoot, contentKey.accountTrieNodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
"hash of next node doesn't match the expected" in res.error()
|
"hash of next node doesn't match the expected" in res.error()
|
||||||
|
|
||||||
for i, testData in testCase:
|
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 =
|
var contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), AccountTrieNodeOffer)
|
AccountTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
contentValueOffer.proof[^1][^1] += 1.byte
|
contentValueOffer.proof[^1][^1] += 1.byte
|
||||||
|
|
||||||
let res = validateOfferedAccountTrieNode(
|
let res =
|
||||||
stateRoot, contentKey.accountTrieNodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
|
|
||||||
|
@ -296,34 +286,31 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for i, testData in testCase:
|
for i, testData in testCase:
|
||||||
var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte())
|
var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte())
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
let contentValueOffer =
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer)
|
let contentValueOffer = ContractTrieNodeOffer
|
||||||
|
.decode(testData.content_value_offer.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedContractTrieNode(
|
validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer)
|
||||||
stateRoot, contentKey.contractTrieNodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isOk()
|
.isOk()
|
||||||
|
|
||||||
if i == 1:
|
if i == 1:
|
||||||
continue # second test case has no recursive gossip
|
continue # second test case has no recursive gossip
|
||||||
|
|
||||||
let contentKey =
|
let contentKey = ContentKey
|
||||||
decode(testData.recursive_gossip.content_key.hexToSeqByte().ByteList).get()
|
.decode(testData.recursive_gossip.content_key.hexToSeqByte().ByteList)
|
||||||
let contentValueOffer = SSZ.decode(
|
.get()
|
||||||
testData.recursive_gossip.content_value_offer.hexToSeqByte(),
|
let contentValueOffer = ContractTrieNodeOffer
|
||||||
ContractTrieNodeOffer,
|
.decode(testData.recursive_gossip.content_value_offer.hexToSeqByte())
|
||||||
)
|
.get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedContractTrieNode(
|
validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer).isOk()
|
||||||
stateRoot, contentKey.contractTrieNodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isOk()
|
|
||||||
|
|
||||||
test "Validate invalid ContractTrieNodeOffer nodes - bad state roots":
|
test "Validate invalid ContractTrieNodeOffer nodes - bad state roots":
|
||||||
const file = testVectorDir / "contract_storage_trie_node.yaml"
|
const file = testVectorDir / "contract_storage_trie_node.yaml"
|
||||||
|
@ -336,15 +323,15 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for i, testData in testCase:
|
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 =
|
let contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer)
|
ContractTrieNodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
let res = validateOfferedContractTrieNode(
|
let res =
|
||||||
stateRoot, contentKey.contractTrieNodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of proof root node doesn't match the expected root hash"
|
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
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for i, testData in testCase:
|
for i, testData in testCase:
|
||||||
var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte())
|
var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte())
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
var contentValueOffer =
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer)
|
var contentValueOffer = ContractTrieNodeOffer
|
||||||
|
.decode(testData.content_value_offer.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
contentValueOffer.accountProof[0][0] += 1.byte
|
contentValueOffer.accountProof[0][0] += 1.byte
|
||||||
|
|
||||||
let res = validateOfferedContractTrieNode(
|
let res =
|
||||||
stateRoot, contentKey.contractTrieNodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of proof root node doesn't match the expected root hash"
|
res.error() == "hash of proof root node doesn't match the expected root hash"
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
var contentValueOffer =
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer)
|
var contentValueOffer = ContractTrieNodeOffer
|
||||||
|
.decode(testData.content_value_offer.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
contentValueOffer.storageProof[0][0] += 1.byte
|
contentValueOffer.storageProof[0][0] += 1.byte
|
||||||
|
|
||||||
let res = validateOfferedContractTrieNode(
|
let res =
|
||||||
stateRoot, contentKey.contractTrieNodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of proof root node doesn't match the expected root hash"
|
res.error() == "hash of proof root node doesn't match the expected root hash"
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
var contentValueOffer =
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer)
|
var contentValueOffer = ContractTrieNodeOffer
|
||||||
|
.decode(testData.content_value_offer.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
contentValueOffer.accountProof[^1][^1] += 1.byte
|
contentValueOffer.accountProof[^1][^1] += 1.byte
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedContractTrieNode(
|
validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer)
|
||||||
stateRoot, contentKey.contractTrieNodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isErr()
|
.isErr()
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
var contentValueOffer =
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer)
|
var contentValueOffer = ContractTrieNodeOffer
|
||||||
|
.decode(testData.content_value_offer.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
contentValueOffer.storageProof[^1][^1] += 1.byte
|
contentValueOffer.storageProof[^1][^1] += 1.byte
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedContractTrieNode(
|
validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer)
|
||||||
stateRoot, contentKey.contractTrieNodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isErr()
|
.isErr()
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
var contentValueOffer =
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractTrieNodeOffer)
|
var contentValueOffer = ContractTrieNodeOffer
|
||||||
|
.decode(testData.content_value_offer.hexToSeqByte())
|
||||||
|
.get()
|
||||||
|
|
||||||
contentValueOffer.accountProof[^2][^2] += 1.byte
|
contentValueOffer.accountProof[^2][^2] += 1.byte
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedContractTrieNode(
|
validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer)
|
||||||
stateRoot, contentKey.contractTrieNodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isErr()
|
.isErr()
|
||||||
|
|
||||||
# Contract bytecode offer validation tests
|
# Contract bytecode offer validation tests
|
||||||
|
@ -434,17 +423,15 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for i, testData in testCase:
|
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 =
|
let contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer)
|
ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedContractCode(
|
validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer).isOk()
|
||||||
stateRoot, contentKey.contractCodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isOk()
|
|
||||||
|
|
||||||
test "Validate invalid ContractCodeOffer nodes - bad state root":
|
test "Validate invalid ContractCodeOffer nodes - bad state root":
|
||||||
const file = testVectorDir / "contract_bytecode.yaml"
|
const file = testVectorDir / "contract_bytecode.yaml"
|
||||||
|
@ -455,15 +442,14 @@ suite "State Validation":
|
||||||
raiseAssert "Cannot read test vector: " & error
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for i, testData in testCase:
|
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 =
|
let contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer)
|
ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
let res = validateOfferedContractCode(
|
let res = validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer)
|
||||||
stateRoot, contentKey.contractCodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of proof root node doesn't match the expected root hash"
|
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
|
raiseAssert "Cannot read test vector: " & error
|
||||||
|
|
||||||
for i, testData in testCase:
|
for i, testData in testCase:
|
||||||
var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte())
|
var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte())
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
var contentValueOffer =
|
var contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer)
|
ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
contentValueOffer.accountProof[0][0] += 1.byte
|
contentValueOffer.accountProof[0][0] += 1.byte
|
||||||
|
|
||||||
let res = validateOfferedContractCode(
|
let res =
|
||||||
stateRoot, contentKey.contractCodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of proof root node doesn't match the expected root hash"
|
res.error() == "hash of proof root node doesn't match the expected root hash"
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
var contentValueOffer =
|
var contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer)
|
ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
contentValueOffer.code[0] += 1.byte
|
contentValueOffer.code[0] += 1.byte
|
||||||
|
|
||||||
let res = validateOfferedContractCode(
|
let res =
|
||||||
stateRoot, contentKey.contractCodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of offered bytecode doesn't match the expected code hash"
|
res.error() == "hash of offered bytecode doesn't match the expected code hash"
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
var contentValueOffer =
|
var contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer)
|
ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
contentValueOffer.accountProof[^1][^1] += 1.byte
|
contentValueOffer.accountProof[^1][^1] += 1.byte
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedContractCode(
|
validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer).isErr()
|
||||||
stateRoot, contentKey.contractCodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isErr()
|
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let contentKey = decode(testData.content_key.hexToSeqByte().ByteList).get()
|
let contentKey =
|
||||||
|
ContentKey.decode(testData.content_key.hexToSeqByte().ByteList).get()
|
||||||
var contentValueOffer =
|
var contentValueOffer =
|
||||||
SSZ.decode(testData.content_value_offer.hexToSeqByte(), ContractCodeOffer)
|
ContractCodeOffer.decode(testData.content_value_offer.hexToSeqByte()).get()
|
||||||
|
|
||||||
contentValueOffer.code[^1] += 1.byte
|
contentValueOffer.code[^1] += 1.byte
|
||||||
|
|
||||||
let res = validateOfferedContractCode(
|
let res =
|
||||||
stateRoot, contentKey.contractCodeKey, contentValueOffer
|
validateOffer(stateRoot, contentKey.contractCodeKey, contentValueOffer)
|
||||||
)
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of offered bytecode doesn't match the expected code hash"
|
res.error() == "hash of offered bytecode doesn't match the expected code hash"
|
||||||
|
@ -549,17 +533,15 @@ suite "State Validation":
|
||||||
if i == 1:
|
if i == 1:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var stateRoot = KeccakHash.init(stateRoots[i].hexToSeqByte())
|
var stateRoot = KeccakHash.fromBytes(stateRoots[i].hexToSeqByte())
|
||||||
|
|
||||||
for kv in testData.recursive_gossip:
|
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 =
|
let contentValueOffer =
|
||||||
SSZ.decode(kv.content_value.hexToSeqByte(), AccountTrieNodeOffer)
|
AccountTrieNodeOffer.decode(kv.content_value.hexToSeqByte()).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedAccountTrieNode(
|
validateOffer(stateRoot, contentKey.accountTrieNodeKey, contentValueOffer)
|
||||||
stateRoot, contentKey.accountTrieNodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isOk()
|
.isOk()
|
||||||
|
|
||||||
test "Validate valid ContractTrieNodeOffer recursive gossip nodes":
|
test "Validate valid ContractTrieNodeOffer recursive gossip nodes":
|
||||||
|
@ -572,15 +554,13 @@ suite "State Validation":
|
||||||
if i != 1:
|
if i != 1:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var stateRoot = KeccakHash.init(testData.state_root.hexToSeqByte())
|
var stateRoot = KeccakHash.fromBytes(testData.state_root.hexToSeqByte())
|
||||||
|
|
||||||
for kv in testData.recursive_gossip:
|
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 =
|
let contentValueOffer =
|
||||||
SSZ.decode(kv.content_value.hexToSeqByte(), ContractTrieNodeOffer)
|
ContractTrieNodeOffer.decode(kv.content_value.hexToSeqByte()).get()
|
||||||
|
|
||||||
check:
|
check:
|
||||||
validateOfferedContractTrieNode(
|
validateOffer(stateRoot, contentKey.contractTrieNodeKey, contentValueOffer)
|
||||||
stateRoot, contentKey.contractTrieNodeKey, contentValueOffer
|
|
||||||
)
|
|
||||||
.isOk()
|
.isOk()
|
||||||
|
|
|
@ -33,18 +33,15 @@ template checkValidProofsForExistingLeafs(
|
||||||
nodeHash: keccakHash(accountProof[^1].asSeq()),
|
nodeHash: keccakHash(accountProof[^1].asSeq()),
|
||||||
)
|
)
|
||||||
accountTrieOffer = AccountTrieNodeOffer(proof: accountProof)
|
accountTrieOffer = AccountTrieNodeOffer(proof: accountProof)
|
||||||
proofResult = validateOfferedAccountTrieNode(
|
proofResult =
|
||||||
accountState.rootHash(), accountTrieNodeKey, accountTrieOffer
|
validateOffer(accountState.rootHash(), accountTrieNodeKey, accountTrieOffer)
|
||||||
)
|
|
||||||
check proofResult.isOk()
|
check proofResult.isOk()
|
||||||
|
|
||||||
let
|
let
|
||||||
contractCodeKey = ContractCodeKey(address: address, codeHash: acc.codeHash)
|
contractCodeKey = ContractCodeKey(address: address, codeHash: acc.codeHash)
|
||||||
contractCode =
|
contractCode =
|
||||||
ContractCodeOffer(code: Bytecode.init(account.code), accountProof: accountProof)
|
ContractCodeOffer(code: Bytecode.init(account.code), accountProof: accountProof)
|
||||||
codeResult = validateOfferedContractCode(
|
codeResult = validateOffer(accountState.rootHash(), contractCodeKey, contractCode)
|
||||||
accountState.rootHash(), contractCodeKey, contractCode
|
|
||||||
)
|
|
||||||
check codeResult.isOk()
|
check codeResult.isOk()
|
||||||
|
|
||||||
if account.code.len() > 0:
|
if account.code.len() > 0:
|
||||||
|
@ -62,7 +59,7 @@ template checkValidProofsForExistingLeafs(
|
||||||
contractTrieOffer = ContractTrieNodeOffer(
|
contractTrieOffer = ContractTrieNodeOffer(
|
||||||
storageProof: storageProof, accountProof: accountProof
|
storageProof: storageProof, accountProof: accountProof
|
||||||
)
|
)
|
||||||
proofResult = validateOfferedContractTrieNode(
|
proofResult = validateOffer(
|
||||||
accountState.rootHash(), contractTrieNodeKey, contractTrieOffer
|
accountState.rootHash(), contractTrieNodeKey, contractTrieOffer
|
||||||
)
|
)
|
||||||
check proofResult.isOk()
|
check proofResult.isOk()
|
||||||
|
@ -85,9 +82,8 @@ template checkInvalidProofsWithBadValue(
|
||||||
accountProof[^1][^1] += 1 # bad account leaf value
|
accountProof[^1][^1] += 1 # bad account leaf value
|
||||||
let
|
let
|
||||||
accountTrieOffer = AccountTrieNodeOffer(proof: accountProof)
|
accountTrieOffer = AccountTrieNodeOffer(proof: accountProof)
|
||||||
proofResult = validateOfferedAccountTrieNode(
|
proofResult =
|
||||||
accountState.rootHash(), accountTrieNodeKey, accountTrieOffer
|
validateOffer(accountState.rootHash(), accountTrieNodeKey, accountTrieOffer)
|
||||||
)
|
|
||||||
check proofResult.isErr()
|
check proofResult.isErr()
|
||||||
|
|
||||||
let
|
let
|
||||||
|
@ -96,9 +92,7 @@ template checkInvalidProofsWithBadValue(
|
||||||
code: Bytecode.init(@[1u8, 2, 3]), # bad code value
|
code: Bytecode.init(@[1u8, 2, 3]), # bad code value
|
||||||
accountProof: accountProof,
|
accountProof: accountProof,
|
||||||
)
|
)
|
||||||
codeResult = validateOfferedContractCode(
|
codeResult = validateOffer(accountState.rootHash(), contractCodeKey, contractCode)
|
||||||
accountState.rootHash(), contractCodeKey, contractCode
|
|
||||||
)
|
|
||||||
check codeResult.isErr()
|
check codeResult.isErr()
|
||||||
|
|
||||||
if account.code.len() > 0:
|
if account.code.len() > 0:
|
||||||
|
@ -118,7 +112,7 @@ template checkInvalidProofsWithBadValue(
|
||||||
contractTrieOffer = ContractTrieNodeOffer(
|
contractTrieOffer = ContractTrieNodeOffer(
|
||||||
storageProof: storageProof, accountProof: accountProof
|
storageProof: storageProof, accountProof: accountProof
|
||||||
)
|
)
|
||||||
proofResult = validateOfferedContractTrieNode(
|
proofResult = validateOffer(
|
||||||
accountState.rootHash(), contractTrieNodeKey, contractTrieOffer
|
accountState.rootHash(), contractTrieNodeKey, contractTrieOffer
|
||||||
)
|
)
|
||||||
check proofResult.isErr()
|
check proofResult.isErr()
|
||||||
|
|
|
@ -16,7 +16,7 @@ import
|
||||||
stint,
|
stint,
|
||||||
nimcrypto/hash,
|
nimcrypto/hash,
|
||||||
eth/trie/[hexary, db, trie_defs],
|
eth/trie/[hexary, db, trie_defs],
|
||||||
../../network/state/state_validation,
|
../../network/state/[state_content, state_validation],
|
||||||
./state_test_helpers
|
./state_test_helpers
|
||||||
|
|
||||||
proc getKeyBytes(i: int): seq[byte] =
|
proc getKeyBytes(i: int): seq[byte] =
|
||||||
|
@ -88,8 +88,8 @@ suite "MPT trie proof verification":
|
||||||
check:
|
check:
|
||||||
res.isOk()
|
res.isOk()
|
||||||
|
|
||||||
test "Validate proof bytes":
|
test "Validate proof bytes - 3 keys":
|
||||||
var trie = initHexaryTrie(newMemoryDB(), isPruning = false)
|
var trie = initHexaryTrie(newMemoryDB())
|
||||||
|
|
||||||
trie.put("doe".toBytes, "reindeer".toBytes)
|
trie.put("doe".toBytes, "reindeer".toBytes)
|
||||||
trie.put("dog".toBytes, "puppy".toBytes)
|
trie.put("dog".toBytes, "puppy".toBytes)
|
||||||
|
@ -101,35 +101,25 @@ suite "MPT trie proof verification":
|
||||||
let
|
let
|
||||||
key = "doe".toBytes
|
key = "doe".toBytes
|
||||||
proof = trie.getTrieProof(key)
|
proof = trie.getTrieProof(key)
|
||||||
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
check validateTrieProof(rootHash, key.asNibbles(), proof).isOk()
|
||||||
|
|
||||||
check:
|
|
||||||
res.isOk()
|
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let
|
let
|
||||||
key = "dog".toBytes
|
key = "dog".toBytes
|
||||||
proof = trie.getTrieProof(key)
|
proof = trie.getTrieProof(key)
|
||||||
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
check validateTrieProof(rootHash, key.asNibbles(), proof).isOk()
|
||||||
|
|
||||||
check:
|
|
||||||
res.isOk()
|
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let
|
let
|
||||||
key = "dogglesworth".toBytes
|
key = "dogglesworth".toBytes
|
||||||
proof = trie.getTrieProof(key)
|
proof = trie.getTrieProof(key)
|
||||||
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
check validateTrieProof(rootHash, key.asNibbles(), proof).isOk()
|
||||||
|
|
||||||
check:
|
|
||||||
res.isOk()
|
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let
|
let
|
||||||
key = "dogg".toBytes
|
key = "dogg".toBytes
|
||||||
proof = trie.getTrieProof(key)
|
proof = trie.getTrieProof(key)
|
||||||
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "not enough nibbles to validate node prefix"
|
res.error() == "not enough nibbles to validate node prefix"
|
||||||
|
@ -139,7 +129,6 @@ suite "MPT trie proof verification":
|
||||||
key = "dogz".toBytes
|
key = "dogz".toBytes
|
||||||
proof = trie.getTrieProof(key)
|
proof = trie.getTrieProof(key)
|
||||||
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "path contains more nibbles than expected for proof"
|
res.error() == "path contains more nibbles than expected for proof"
|
||||||
|
@ -149,7 +138,6 @@ suite "MPT trie proof verification":
|
||||||
key = "doe".toBytes
|
key = "doe".toBytes
|
||||||
proof = newSeq[seq[byte]]().asTrieProof()
|
proof = newSeq[seq[byte]]().asTrieProof()
|
||||||
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "proof is empty"
|
res.error() == "proof is empty"
|
||||||
|
@ -159,7 +147,53 @@ suite "MPT trie proof verification":
|
||||||
key = "doe".toBytes
|
key = "doe".toBytes
|
||||||
proof = @["aaa".toBytes, "ccc".toBytes].asTrieProof()
|
proof = @["aaa".toBytes, "ccc".toBytes].asTrieProof()
|
||||||
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
res = validateTrieProof(rootHash, key.asNibbles(), proof)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
res.isErr()
|
res.isErr()
|
||||||
res.error() == "hash of proof root node doesn't match the expected root hash"
|
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()
|
||||||
|
|
Loading…
Reference in New Issue