mirror of https://github.com/status-im/nim-eth.git
239 lines
6.7 KiB
Nim
239 lines
6.7 KiB
Nim
|
# proof verification
|
||
|
# Copyright (c) 2022 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: [Defect].}
|
||
|
|
||
|
import
|
||
|
std/[tables, options, sequtils],
|
||
|
stew/results,
|
||
|
nimcrypto/[keccak, hash],
|
||
|
".."/rlp,
|
||
|
"."/[trie_defs, nibbles, db]
|
||
|
|
||
|
type
|
||
|
NextNodeKind = enum
|
||
|
EmptyValue, HashNode, ValueNode
|
||
|
|
||
|
NextNodeResult = object
|
||
|
case kind: NextNodeKind
|
||
|
of EmptyValue:
|
||
|
discard
|
||
|
of HashNode:
|
||
|
nextNodeHash: seq[byte]
|
||
|
restOfTheKey: NibblesSeq
|
||
|
of ValueNode:
|
||
|
value: seq[byte]
|
||
|
|
||
|
MptProofVerificationKind* = enum
|
||
|
ValidProof, InvalidProof, MissingKey
|
||
|
|
||
|
MptProofVerificationResult* = object
|
||
|
case kind*: MptProofVerificationKind
|
||
|
of MissingKey:
|
||
|
discard
|
||
|
of InvalidProof:
|
||
|
errorMsg*: string
|
||
|
of ValidProof:
|
||
|
value*: seq[byte]
|
||
|
|
||
|
func missingKey(): MptProofVerificationResult =
|
||
|
return MptProofVerificationResult(kind: MissingKey)
|
||
|
|
||
|
func invalidProof(msg: string): MptProofVerificationResult =
|
||
|
return MptProofVerificationResult(kind: InvalidProof, errorMsg: msg)
|
||
|
|
||
|
func validProof(value: seq[byte]): MptProofVerificationResult =
|
||
|
return MptProofVerificationResult(kind: ValidProof, value: value)
|
||
|
|
||
|
func isValid*(res: MptProofVerificationResult): bool =
|
||
|
return res.kind == ValidProof
|
||
|
|
||
|
func isMissing*(res: MptProofVerificationResult): bool =
|
||
|
return res.kind == MissingKey
|
||
|
|
||
|
proc getListLen(rlp: Rlp): Result[int, string] =
|
||
|
try:
|
||
|
return ok(rlp.listLen)
|
||
|
except RlpError as e:
|
||
|
return err(e.msg)
|
||
|
|
||
|
proc getListElem(rlp: Rlp, idx: int): Result[Rlp, string] =
|
||
|
if not rlp.isList:
|
||
|
return err("rlp element is not a list")
|
||
|
|
||
|
try:
|
||
|
return ok(rlp.listElem(idx))
|
||
|
except RlpError as e:
|
||
|
return err(e.msg)
|
||
|
|
||
|
proc blobBytes(rlp: Rlp): Result[seq[byte], string] =
|
||
|
try:
|
||
|
return ok(rlp.toBytes)
|
||
|
except RlpError as e:
|
||
|
return err(e.msg)
|
||
|
|
||
|
func rawBytesSeq(b: openArray[byte]): seq[byte] =
|
||
|
toSeq(b)
|
||
|
|
||
|
proc getRawRlpBytes(rlp: Rlp): Result[seq[byte], string] =
|
||
|
try :
|
||
|
return ok(rawBytesSeq(rlp.rawData))
|
||
|
except RlpError as e:
|
||
|
return err(e.msg)
|
||
|
|
||
|
proc getNextNode(nodeRlp: Rlp, key: NibblesSeq): Result[NextNodeResult, string] =
|
||
|
var currNode = nodeRlp
|
||
|
var restKey = key
|
||
|
|
||
|
template handleNextRef(nextRef: Rlp, keyLen: int) =
|
||
|
if not nextRef.hasData:
|
||
|
return err("invalid reference")
|
||
|
|
||
|
if nextRef.isList:
|
||
|
let rawBytes = ? nextRef.getRawRlpBytes()
|
||
|
if len(rawBytes) > 32:
|
||
|
return err("Embedded node longer than 32 bytes")
|
||
|
else:
|
||
|
currNode = nextRef
|
||
|
restKey = restKey.slice(keyLen)
|
||
|
else:
|
||
|
let nodeBytes = ? nextRef.blobBytes()
|
||
|
if len(nodeBytes) == 32:
|
||
|
return ok(
|
||
|
NextNodeResult(
|
||
|
kind: HashNode,
|
||
|
nextNodeHash: nodeBytes,
|
||
|
restOfTheKey: restKey.slice(keyLen)
|
||
|
)
|
||
|
)
|
||
|
elif len(nodeBytes) == 0:
|
||
|
return ok(NextNodeResult(kind: EmptyValue))
|
||
|
else:
|
||
|
return err("reference rlp blob should have 0 or 32 bytes")
|
||
|
|
||
|
while true:
|
||
|
let listLen = ? currNode.getListLen()
|
||
|
|
||
|
case listLen
|
||
|
of 2:
|
||
|
let
|
||
|
firstElem = ? currNode.getListElem(0)
|
||
|
blobBytes = ? firstElem.blobBytes()
|
||
|
|
||
|
let (isLeaf, k) = hexPrefixDecode blobBytes
|
||
|
|
||
|
# Paths have diverged, return empty result
|
||
|
if len(restKey) < len(k) or k != restKey.slice(0, len(k)):
|
||
|
return ok(NextNodeResult(kind: EmptyValue))
|
||
|
|
||
|
let nextRef = ? currNode.getListElem(1)
|
||
|
|
||
|
if isLeaf:
|
||
|
let blobBytes = ? nextRef.blobBytes()
|
||
|
return ok(NextNodeResult(kind: ValueNode, value: blobBytes))
|
||
|
|
||
|
handleNextRef(nextRef, len(k))
|
||
|
of 17:
|
||
|
if len(restKey) == 0:
|
||
|
let value = ? currNode.getListElem(16)
|
||
|
|
||
|
if not value.hasData():
|
||
|
return err("expected branch terminator")
|
||
|
|
||
|
if value.isList():
|
||
|
return err("branch value cannot be list")
|
||
|
|
||
|
if value.isEmpty():
|
||
|
return ok(NextNodeResult(kind: EmptyValue))
|
||
|
else:
|
||
|
let bytes = ? value.blobBytes()
|
||
|
return ok(NextNodeResult(kind: ValueNode, value: bytes))
|
||
|
else:
|
||
|
let nextRef = ? currNode.getListElem(restKey[0].int)
|
||
|
|
||
|
handleNextRef(nextRef, 1)
|
||
|
else:
|
||
|
return err("Invalid list node ")
|
||
|
|
||
|
proc verifyProof(
|
||
|
db: TrieDatabaseRef,
|
||
|
rootHash: seq[byte],
|
||
|
key: seq[byte]): Result[Option[seq[byte]], string] =
|
||
|
var currentKey = initNibbleRange(key)
|
||
|
|
||
|
var currentHash = rootHash
|
||
|
|
||
|
while true:
|
||
|
let node = db.get(currentHash)
|
||
|
|
||
|
if len(node) == 0:
|
||
|
return err("missing expected node")
|
||
|
|
||
|
let next = ? getNextNode(rlpFromBytes(node), currentKey)
|
||
|
|
||
|
case next.kind
|
||
|
of EmptyValue:
|
||
|
return ok(none(seq[byte]))
|
||
|
of ValueNode:
|
||
|
return ok(some(next.value))
|
||
|
of HashNode:
|
||
|
currentKey = next.restOfTheKey
|
||
|
currentHash = next.nextNodeHash
|
||
|
|
||
|
proc verifyMptProof*(
|
||
|
branch: seq[seq[byte]],
|
||
|
rootHash: KeccakHash,
|
||
|
key: seq[byte],
|
||
|
value: seq[byte]): MptProofVerificationResult =
|
||
|
## Verifies provided proof of inclusion (trie branch) against provided trie
|
||
|
## root hash.
|
||
|
## Distinguishes 3 possible results:
|
||
|
## - proof is valid but key is not part of the trie
|
||
|
## - proof is invalid
|
||
|
## - proof is valid
|
||
|
## In case of valid proof, value is extracted from the leaf node and compared
|
||
|
## against provided value
|
||
|
##
|
||
|
## Main difference between this function and hexary.isValidBranch() is that
|
||
|
## this function is meant for dealing with input from untrusted sources so:
|
||
|
## - it should not have hidden assertion
|
||
|
## - it should not have surprising exceptions
|
||
|
## - it parses mpt nodes more strictly
|
||
|
##
|
||
|
## hexary.isValidBranch() is implemented via hexary trie `get` method which
|
||
|
## may contain some checks important for integrity of the trie therefore is
|
||
|
## is not really safe when receiving input from untrusted source.
|
||
|
|
||
|
if len(branch) == 0:
|
||
|
return invalidProof("empty branch")
|
||
|
|
||
|
var db = newMemoryDB()
|
||
|
for node in branch:
|
||
|
if len(node) == 0:
|
||
|
return invalidProof("empty mpt node in proof")
|
||
|
let nodeHash = keccakHash(node)
|
||
|
db.put(nodeHash.data, node)
|
||
|
|
||
|
let
|
||
|
hashBytes: seq[byte] = toSeq(rootHash.data)
|
||
|
proofVerificationResult = verifyProof(db, hashBytes, key)
|
||
|
|
||
|
if proofVerificationResult.isErr:
|
||
|
return invalidProof(proofVerificationResult.error)
|
||
|
|
||
|
let maybeProofValue = proofVerificationResult.get()
|
||
|
|
||
|
if maybeProofValue.isNone():
|
||
|
return missingKey()
|
||
|
|
||
|
let proofValue = maybeProofValue.unsafeGet()
|
||
|
|
||
|
if proofValue == value:
|
||
|
return validProof(proofValue)
|
||
|
else:
|
||
|
return invalidProof("proof does not contain expected value")
|