259 lines
8.7 KiB
Nim
259 lines
8.7 KiB
Nim
# Fluffy
|
|
# Copyright (c) 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.
|
|
|
|
{.push raises: [].}
|
|
|
|
import results, eth/rlp, eth/common/hashes, ./state_content, ./state_utils
|
|
|
|
export results, state_content, hashes
|
|
|
|
from eth/common/eth_types_rlp import rlpHash
|
|
|
|
template hashEquals(value: TrieNode | Bytecode, expectedHash: Hash32): bool =
|
|
keccak256(value.asSeq()) == expectedHash
|
|
|
|
func isValidNextNode(
|
|
thisNodeRlp: Rlp, rlpIdx: int, nextNode: TrieNode
|
|
): bool {.raises: RlpError.} =
|
|
let hashOrShortRlp = thisNodeRlp.listElem(rlpIdx)
|
|
if hashOrShortRlp.isEmpty():
|
|
return false
|
|
|
|
let nextHash =
|
|
if hashOrShortRlp.isList():
|
|
# is a short node
|
|
rlpHash(hashOrShortRlp)
|
|
else:
|
|
let hash = hashOrShortRlp.toBytes()
|
|
if hash.len() != 32:
|
|
return false
|
|
Hash32.fromBytes(hash)
|
|
|
|
nextNode.hashEquals(nextHash)
|
|
|
|
# TODO: Refactor this function to improve maintainability
|
|
func validateTrieProof*(
|
|
expectedRootHash: Opt[Hash32],
|
|
path: Nibbles,
|
|
proof: TrieProof,
|
|
allowKeyEndInPathForLeafs = false,
|
|
): Result[void, string] =
|
|
if proof.len() == 0:
|
|
return err("proof is empty")
|
|
|
|
if expectedRootHash.isSome():
|
|
if not proof[0].hashEquals(expectedRootHash.get()):
|
|
return err("hash of proof root node doesn't match the expected root hash")
|
|
|
|
let nibbles = path.unpackNibbles()
|
|
if nibbles.len() == 0:
|
|
if proof.len() == 1:
|
|
return ok() # root node case, already validated above
|
|
else:
|
|
return err("empty path, only one node expected in proof")
|
|
|
|
var nibbleIdx = 0
|
|
for proofIdx, p in proof:
|
|
let
|
|
thisNodeRlp = rlpFromBytes(p.asSeq())
|
|
remainingNibbles = nibbles.len() - nibbleIdx
|
|
isLastNode = proofIdx == proof.high
|
|
|
|
if remainingNibbles == 0:
|
|
if isLastNode:
|
|
break
|
|
else:
|
|
return err("proof has more nodes then expected for given path")
|
|
|
|
try:
|
|
case thisNodeRlp.listLen()
|
|
of 2:
|
|
let nodePrefixRlp = thisNodeRlp.listElem(0)
|
|
if nodePrefixRlp.isEmpty():
|
|
return err("node prefix is empty")
|
|
|
|
let (prefix, isLeaf, prefixNibbles) = decodePrefix(nodePrefixRlp)
|
|
if prefix >= 4:
|
|
return err("invalid prefix in node")
|
|
|
|
if not isLastNode or (isLeaf and allowKeyEndInPathForLeafs):
|
|
let unpackedPrefix = prefixNibbles.unpackNibbles()
|
|
if remainingNibbles < unpackedPrefix.len():
|
|
return err("not enough nibbles to validate node prefix")
|
|
|
|
let nibbleEndIdx = nibbleIdx + unpackedPrefix.len()
|
|
if nibbles[nibbleIdx ..< nibbleEndIdx] != unpackedPrefix:
|
|
return err("nibbles don't match node prefix")
|
|
nibbleIdx += unpackedPrefix.len()
|
|
|
|
if not isLastNode:
|
|
if isLeaf:
|
|
return err("leaf node must be last node in the proof")
|
|
else: # is extension node
|
|
if not isValidNextNode(thisNodeRlp, 1, proof[proofIdx + 1]):
|
|
return
|
|
err("hash of next node doesn't match the expected extension node hash")
|
|
of 17:
|
|
if not isLastNode:
|
|
let nextNibble = nibbles[nibbleIdx]
|
|
if nextNibble >= 16:
|
|
return err("invalid next nibble for branch node")
|
|
|
|
if not isValidNextNode(thisNodeRlp, nextNibble.int, proof[proofIdx + 1]):
|
|
return err("hash of next node doesn't match the expected branch node hash")
|
|
|
|
inc nibbleIdx
|
|
else:
|
|
return err("invalid rlp node, expected 2 or 17 elements")
|
|
except RlpError as e:
|
|
return err(e.msg)
|
|
|
|
if nibbleIdx < nibbles.len():
|
|
err("path contains more nibbles than expected for proof")
|
|
else:
|
|
ok()
|
|
|
|
func validateRetrieval*(
|
|
key: AccountTrieNodeKey, value: AccountTrieNodeRetrieval
|
|
): Result[void, string] =
|
|
if value.node.hashEquals(key.nodeHash):
|
|
ok()
|
|
else:
|
|
err("hash of account trie node doesn't match the expected node hash")
|
|
|
|
func validateRetrieval*(
|
|
key: ContractTrieNodeKey, value: ContractTrieNodeRetrieval
|
|
): Result[void, string] =
|
|
if value.node.hashEquals(key.nodeHash):
|
|
ok()
|
|
else:
|
|
err("hash of contract trie node doesn't match the expected node hash")
|
|
|
|
func validateRetrieval*(
|
|
key: ContractCodeKey, value: ContractCodeRetrieval
|
|
): Result[void, string] =
|
|
if value.code.hashEquals(key.codeHash):
|
|
ok()
|
|
else:
|
|
err("hash of bytecode doesn't match the expected code hash")
|
|
|
|
func validateOffer*(
|
|
trustedStateRoot: Opt[Hash32], key: AccountTrieNodeKey, offer: AccountTrieNodeOffer
|
|
): Result[void, string] =
|
|
?validateTrieProof(trustedStateRoot, key.path, offer.proof)
|
|
|
|
validateRetrieval(key, offer.toRetrieval())
|
|
|
|
func validateOffer*(
|
|
trustedStateRoot: Opt[Hash32],
|
|
key: ContractTrieNodeKey,
|
|
offer: ContractTrieNodeOffer,
|
|
): Result[void, string] =
|
|
?validateTrieProof(
|
|
trustedStateRoot,
|
|
key.addressHash.toPath(),
|
|
offer.accountProof,
|
|
allowKeyEndInPathForLeafs = true,
|
|
)
|
|
|
|
let account = ?offer.accountProof.toAccount()
|
|
|
|
?validateTrieProof(Opt.some(account.storageRoot), key.path, offer.storageProof)
|
|
|
|
validateRetrieval(key, offer.toRetrieval())
|
|
|
|
func validateOffer*(
|
|
trustedStateRoot: Opt[Hash32], key: ContractCodeKey, offer: ContractCodeOffer
|
|
): Result[void, string] =
|
|
?validateTrieProof(
|
|
trustedStateRoot,
|
|
key.addressHash.toPath(),
|
|
offer.accountProof,
|
|
allowKeyEndInPathForLeafs = true,
|
|
)
|
|
|
|
let account = ?offer.accountProof.toAccount()
|
|
if not offer.code.hashEquals(account.codeHash):
|
|
return err("hash of bytecode doesn't match the code hash in the account proof")
|
|
|
|
validateRetrieval(key, offer.toRetrieval())
|
|
|
|
# Local validations that check the structure of the content keys and values.
|
|
# None of the validations below check if the data is canonical or not
|
|
|
|
func validateGetContentKey*(
|
|
keyBytes: ContentKeyByteList
|
|
): Result[(ContentKey, ContentId), string] =
|
|
let key = ?ContentKey.decode(keyBytes)
|
|
ok((key, toContentId(keyBytes)))
|
|
|
|
func validateRetrieval*(
|
|
key: ContentKey, contentBytes: seq[byte]
|
|
): Result[void, string] =
|
|
case key.contentType
|
|
of unused:
|
|
raiseAssert("ContentKey contentType: unused")
|
|
of accountTrieNode:
|
|
let retrieval = ?AccountTrieNodeRetrieval.decode(contentBytes)
|
|
validateRetrieval(key.accountTrieNodeKey, retrieval)
|
|
of contractTrieNode:
|
|
let retrieval = ?ContractTrieNodeRetrieval.decode(contentBytes)
|
|
validateRetrieval(key.contractTrieNodeKey, retrieval)
|
|
of contractCode:
|
|
let retrieval = ?ContractCodeRetrieval.decode(contentBytes)
|
|
validateRetrieval(key.contractCodeKey, retrieval)
|
|
|
|
func validateRetrievalGetOffer*(
|
|
key: ContentKey, contentBytes: seq[byte], parentContentBytes: seq[byte]
|
|
): Result[seq[byte], string] =
|
|
case key.contentType
|
|
of unused:
|
|
raiseAssert("ContentKey contentType: unused")
|
|
of accountTrieNode:
|
|
let
|
|
retrieval = ?AccountTrieNodeRetrieval.decode(contentBytes)
|
|
parentOffer = ?AccountTrieNodeOffer.decode(parentContentBytes)
|
|
offer = retrieval.toOffer(parentOffer)
|
|
?validateRetrieval(key.accountTrieNodeKey, retrieval)
|
|
?validateOffer(Opt.none(Hash32), key.accountTrieNodeKey, offer)
|
|
ok(offer.encode())
|
|
of contractTrieNode:
|
|
let
|
|
retrieval = ?ContractTrieNodeRetrieval.decode(contentBytes)
|
|
parentOffer = ?ContractTrieNodeOffer.decode(parentContentBytes)
|
|
offer = retrieval.toOffer(parentOffer)
|
|
?validateRetrieval(key.contractTrieNodeKey, retrieval)
|
|
?validateOffer(Opt.none(Hash32), key.contractTrieNodeKey, offer)
|
|
ok(offer.encode())
|
|
of contractCode:
|
|
let
|
|
retrieval = ?ContractCodeRetrieval.decode(contentBytes)
|
|
parentOffer = ?ContractCodeOffer.decode(parentContentBytes)
|
|
offer = retrieval.toOffer(parentOffer)
|
|
?validateRetrieval(key.contractCodeKey, retrieval)
|
|
?validateOffer(Opt.none(Hash32), key.contractCodeKey, offer)
|
|
ok(offer.encode())
|
|
|
|
func validateOfferGetRetrieval*(
|
|
key: ContentKey, contentBytes: seq[byte]
|
|
): Result[seq[byte], string] =
|
|
case key.contentType
|
|
of unused:
|
|
raiseAssert("ContentKey contentType: unused")
|
|
of accountTrieNode:
|
|
let offer = ?AccountTrieNodeOffer.decode(contentBytes)
|
|
?validateOffer(Opt.none(Hash32), key.accountTrieNodeKey, offer)
|
|
ok(offer.toRetrieval.encode())
|
|
of contractTrieNode:
|
|
let offer = ?ContractTrieNodeOffer.decode(contentBytes)
|
|
?validateOffer(Opt.none(Hash32), key.contractTrieNodeKey, offer)
|
|
ok(offer.toRetrieval.encode())
|
|
of contractCode:
|
|
let offer = ?ContractCodeOffer.decode(contentBytes)
|
|
?validateOffer(Opt.none(Hash32), key.contractCodeKey, offer)
|
|
ok(offer.toRetrieval.encode())
|