Skip to content

Commit 166d9dc

Browse files
authored
Fix verify proof function in aristo proof module (#3562)
* Fix verify proof function in aristo proof module and update tests to use. Remove usages of eth/trie in tests. * Return error when proof is empty and add aristo proof test.
1 parent 0946ffb commit 166d9dc

File tree

8 files changed

+249
-95
lines changed

8 files changed

+249
-95
lines changed

execution_chain/db/aristo/aristo_desc/desc_error.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ type
8787
PartChnLeafPathMismatch
8888
PartChnNodeConvError
8989
PartTrkEmptyPath
90+
PartTrkEmptyProof
9091
PartTrkFollowUpKeyMismatch
9192
PartTrkGarbledNode
9293
PartTrkLeafPfxMismatch

execution_chain/db/aristo/aristo_proof.nim

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ proc trackRlpNodes(
9494
## Verify rlp-encoded node chain created by `chainRlpNodes()`.
9595
if path.len == 0:
9696
return err(PartTrkEmptyPath)
97+
if chain.len() == 0:
98+
return err(PartTrkEmptyProof)
9799

98100
# Verify key against rlp-node
99101
let digest = chain[0].digestTo(HashKey)
@@ -127,7 +129,11 @@ proc trackRlpNodes(
127129

128130
let nextKey = HashKey.fromBytes(link).valueOr:
129131
return err(PartTrkLinkExpected)
130-
chain.toOpenArray(1,chain.len-1).trackRlpNodes(nextKey, path.slice nChewOff)
132+
133+
if chain.len() > 1:
134+
chain.toOpenArray(1, chain.len() - 1).trackRlpNodes(nextKey, path.slice nChewOff)
135+
else:
136+
err(PartTrkLinkExpected)
131137

132138
proc makeProof(
133139
db: AristoTxRef;

execution_chain/db/core_db/base.nim

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ proc stateBlockNumber*(db: CoreDbTxRef): BlockNumber =
130130

131131
rc.BlockNumber
132132

133-
proc verify*(
133+
proc verifyProof*(
134134
db: CoreDbRef;
135135
proof: openArray[seq[byte]];
136136
root: Hash32;
@@ -221,21 +221,6 @@ proc hasKey*(kvt: CoreDbTxRef; key: openArray[byte]): bool =
221221

222222
# ----------- accounts ---------------
223223

224-
proc proof*(
225-
acc: CoreDbTxRef;
226-
accPath: Hash32;
227-
): CoreDbRc[(seq[seq[byte]],bool)] =
228-
## On the accounts MPT, collect the nodes along the `accPath` interpreted as
229-
## path. Return these path nodes as a chain of rlp-encoded blobs followed
230-
## by a bool value which is `true` if the `key` path exists in the database,
231-
## and `false` otherwise. In the latter case, the chain of rlp-encoded blobs
232-
## are the nodes proving that the `key` path does not exist.
233-
##
234-
let rc = acc.aTx.makeAccountProof(accPath).valueOr:
235-
return err(error.toError("", ProofCreate))
236-
237-
ok(rc)
238-
239224
proc fetch*(
240225
acc: CoreDbTxRef;
241226
accPath: Hash32;
@@ -307,6 +292,21 @@ proc getStateRoot*(acc: CoreDbTxRef): CoreDbRc[Hash32] =
307292

308293
ok(rc)
309294

295+
proc proof*(
296+
acc: CoreDbTxRef;
297+
accPath: Hash32;
298+
): CoreDbRc[(seq[seq[byte]],bool)] =
299+
## On the accounts MPT, collect the nodes along the `accPath` interpreted as
300+
## path. Return these path nodes as a chain of rlp-encoded blobs followed
301+
## by a bool value which is `true` if the `key` path exists in the database,
302+
## and `false` otherwise. In the latter case, the chain of rlp-encoded blobs
303+
## are the nodes proving that the `key` path does not exist.
304+
##
305+
let rc = acc.aTx.makeAccountProof(accPath).valueOr:
306+
return err(error.toError("", ProofCreate))
307+
308+
ok(rc)
309+
310310
proc multiProof*(
311311
acc: CoreDbTxRef;
312312
paths: Table[Hash32, seq[Hash32]];

tests/all_tests.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import
2222
test_forkid,
2323
test_genesis,
2424
test_getproof_json,
25+
test_aristo_proof,
2526
test_jwt_auth,
2627
test_kvt,
2728
test_ledger,

tests/proof_helpers.nim

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Nimbus
2+
# Copyright (c) 2018-2025 Status Research & Development GmbH
3+
# Licensed under either of
4+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
5+
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
6+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
7+
8+
import
9+
stew/byteutils,
10+
eth/rlp,
11+
eth/common/keys,
12+
web3/eth_api,
13+
../execution_chain/db/aristo/aristo_proof
14+
15+
template toHash32*(hash: untyped): Hash32 =
16+
fromHex(Hash32, hash.toHex())
17+
18+
proc verifyAccountLeafExists*(trustedStateRoot: Hash32, res: ProofResponse): bool =
19+
let
20+
accPath = keccak256(res.address.data)
21+
value = rlp.encode(Account(
22+
nonce: res.nonce.uint64,
23+
balance: res.balance,
24+
storageRoot: res.storageHash.toHash32(),
25+
codeHash: res.codeHash.toHash32()))
26+
27+
let accLeaf = verifyProof(seq[seq[byte]](res.accountProof), trustedStateRoot, accPath).expect("valid proof")
28+
accLeaf.isSome() and accLeaf.get() == value
29+
30+
proc verifyAccountLeafMissing*(trustedStateRoot: Hash32, res: ProofResponse): bool =
31+
let
32+
accPath = keccak256(res.address.data)
33+
value = rlp.encode(Account(
34+
nonce: res.nonce.uint64,
35+
balance: res.balance,
36+
storageRoot: res.storageHash.toHash32(),
37+
codeHash: res.codeHash.toHash32()))
38+
39+
let accLeaf = verifyProof(seq[seq[byte]](res.accountProof), trustedStateRoot, accPath).expect("valid proof")
40+
accLeaf.isNone()
41+
42+
proc verifySlotLeafExists*(trustedStorageRoot: Hash32, slot: StorageProof): bool =
43+
let
44+
slotPath = keccak256(toBytesBE(slot.key))
45+
value = rlp.encode(slot.value)
46+
47+
let slotLeaf = verifyProof(seq[seq[byte]](slot.proof), trustedStorageRoot, slotPath).expect("valid proof")
48+
slotLeaf.isSome() and slotLeaf.get() == value
49+
50+
proc verifySlotLeafMissing*(trustedStorageRoot: Hash32, slot: StorageProof): bool =
51+
let
52+
slotPath = keccak256(toBytesBE(slot.key))
53+
value = rlp.encode(slot.value)
54+
55+
let slotLeaf = verifyProof(seq[seq[byte]](slot.proof), trustedStorageRoot, slotPath).expect("valid proof")
56+
slotLeaf.isNone()

tests/test_aristo_proof.nim

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# proof verification
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed and distributed under either of
4+
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
5+
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
6+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
7+
8+
{.used.}
9+
10+
{.push raises: [].}
11+
12+
import
13+
std/sequtils,
14+
unittest2,
15+
stint,
16+
results,
17+
stew/byteutils,
18+
eth/trie/[hexary, db, trie_defs],
19+
../execution_chain/db/aristo/aristo_proof
20+
21+
proc getKeyBytes(i: int): seq[byte] =
22+
@(u256(i).toBytesBE())
23+
24+
suite "Aristo proof verification":
25+
26+
test "Validate proof for existing value":
27+
let numValues = 1000
28+
29+
var db = newMemoryDB()
30+
var trie = initHexaryTrie(db)
31+
32+
for i in 1..numValues:
33+
let
34+
key = getKeyBytes(i).keccak256()
35+
value = getKeyBytes(i)
36+
trie.put(key.data, value)
37+
38+
for i in 1..numValues:
39+
let
40+
key = getKeyBytes(i).keccak256()
41+
value = getKeyBytes(i)
42+
proof = trie.getBranch(key.data)
43+
root = trie.rootHash()
44+
45+
let proofRes = verifyProof(proof, root, key).expect("valid proof")
46+
check:
47+
proofRes.isSome()
48+
proofRes.get() == value
49+
50+
test "Validate proof for non-existing value":
51+
let numValues = 1000
52+
var db = newMemoryDB()
53+
var trie = initHexaryTrie(db)
54+
55+
for i in 1..numValues:
56+
let
57+
key = getKeyBytes(i).keccak256()
58+
value = getKeyBytes(i)
59+
trie.put(key.data, value)
60+
61+
let
62+
nonExistingKey = toSeq(toBytesBE(u256(numValues + 1))).keccak256()
63+
proof = trie.getBranch(nonExistingKey.data)
64+
root = trie.rootHash()
65+
66+
let proofRes = verifyProof(proof, root, nonExistingKey).expect("valid proof")
67+
check:
68+
proofRes.isNone()
69+
70+
# The following test cases were copied from the Rust hexary trie implementation.
71+
# See here: https://github.com/citahub/cita_trie/blob/master/src/tests/mod.rs#L554
72+
test "Validate proof for empty trie":
73+
let db = newMemoryDB()
74+
var trie = initHexaryTrie(db)
75+
76+
let
77+
proof = trie.getBranch("not-exist".toBytes.keccak256().data)
78+
res = verifyProof(proof, trie.rootHash, "not-exist".toBytes.keccak256())
79+
80+
check:
81+
trie.rootHash == keccak256(emptyRlp)
82+
proof.len() == 1 # Note that the Rust implementation returns an empty list for this scenario
83+
proof == @[emptyRlp]
84+
res.isErr()
85+
86+
test "Validate proof for one element trie":
87+
let db = newMemoryDB()
88+
var trie = initHexaryTrie(db)
89+
90+
trie.put("k".toBytes.keccak256().data, "v".toBytes)
91+
92+
let
93+
rootHash = trie.rootHash
94+
proof = trie.getBranch("k".toBytes.keccak256().data)
95+
res = verifyProof(proof, rootHash, "k".toBytes.keccak256()).expect("valid proof")
96+
97+
check:
98+
proof.len() == 1
99+
res.isSome()
100+
res.get() == "v".toBytes
101+
102+
test "Validate proof bytes":
103+
let db = newMemoryDB()
104+
var trie = initHexaryTrie(db)
105+
106+
trie.put("doe".toBytes.keccak256().data, "reindeer".toBytes)
107+
trie.put("dog".toBytes.keccak256().data, "puppy".toBytes)
108+
trie.put("dogglesworth".toBytes.keccak256().data, "cat".toBytes)
109+
110+
block:
111+
let
112+
rootHash = trie.rootHash
113+
proof = trie.getBranch("doe".toBytes.keccak256().data)
114+
res = verifyProof(proof, rootHash, "doe".toBytes.keccak256()).expect("valid proof")
115+
116+
check:
117+
res.isSome()
118+
res.get() == "reindeer".toBytes
119+
120+
block:
121+
let
122+
rootHash = trie.rootHash
123+
proof = trie.getBranch("dogg".toBytes.keccak256().data)
124+
res = verifyProof(proof, rootHash, "dogg".toBytes.keccak256()).expect("valid proof")
125+
126+
check res.isNone()
127+
128+
block:
129+
let
130+
proof = newSeq[seq[byte]]()
131+
res = verifyProof(proof, trie.rootHash, "doe".toBytes.keccak256())
132+
133+
check res.isErr()
134+
135+
block:
136+
let
137+
proof = @["aaa".toBytes, "ccc".toBytes]
138+
res = verifyProof(proof, trie.rootHash, "doe".toBytes.keccak256())
139+
140+
check res.isErr()

tests/test_getproof_json.nim

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,16 @@
88
import
99
std/os,
1010
unittest2,
11-
stew/byteutils,
1211
web3/eth_api,
13-
nimcrypto/[keccak, hash],
14-
eth/common/[keys, eth_types_rlp],
15-
eth/[rlp, trie/hexary_proof_verification],
1612
../execution_chain/db/[ledger, core_db],
1713
../execution_chain/common/chain_config,
18-
../execution_chain/rpc/server_api
14+
../execution_chain/rpc/server_api,
15+
./proof_helpers
1916

2017
type
2118
Hash32 = eth_types.Hash32
2219
Address = primitives.Address
2320

24-
template toHash32(hash: untyped): Hash32 =
25-
fromHex(Hash32, hash.toHex())
26-
27-
proc verifyAccountProof(trustedStateRoot: Hash32, res: ProofResponse): MptProofVerificationResult =
28-
let
29-
key = keccak256(res.address.data)
30-
value = rlp.encode(Account(
31-
nonce: res.nonce.uint64,
32-
balance: res.balance,
33-
storageRoot: res.storageHash.toHash32(),
34-
codeHash: res.codeHash.toHash32()))
35-
36-
verifyMptProof(
37-
seq[seq[byte]](res.accountProof),
38-
trustedStateRoot,
39-
key.data,
40-
value)
41-
42-
proc verifySlotProof(trustedStorageRoot: Hash32, slot: StorageProof): MptProofVerificationResult =
43-
let
44-
key = keccak256(toBytesBE(slot.key))
45-
value = rlp.encode(slot.value)
46-
47-
verifyMptProof(
48-
seq[seq[byte]](slot.proof),
49-
trustedStorageRoot,
50-
key.data,
51-
value)
52-
5321
proc getGenesisAlloc(filePath: string): GenesisAlloc =
5422
var cn: NetworkParams
5523
if not loadNetworkParams(filePath, cn):
@@ -89,14 +57,14 @@ proc checkProofsForExistingLeafs(
8957
proofResponse.balance == account.balance
9058
proofResponse.codeHash.toHash32() == accDB.getCodeHash(address)
9159
proofResponse.storageHash.toHash32() == accDB.getStorageRoot(address)
92-
verifyAccountProof(stateRoot, proofResponse).isValid()
60+
verifyAccountLeafExists(stateRoot, proofResponse)
9361
slotProofs.len() == account.storage.len()
9462

9563
for i, slotProof in slotProofs:
9664
check:
9765
slotProof.key == slots[i]
9866
slotProof.value == account.storage[slotProof.key]
99-
verifySlotProof(proofResponse.storageHash.toHash32(), slotProof).isValid()
67+
verifySlotLeafExists(proofResponse.storageHash.toHash32(), slotProof)
10068

10169
proc checkProofsForMissingLeafs(
10270
genAccounts: GenesisAlloc,
@@ -106,7 +74,7 @@ proc checkProofsForMissingLeafs(
10674
let
10775
missingAddress = Address.fromHex("0x999999cf1046e68e36E1aA2E0E07105eDDD1f08E")
10876
proofResponse = getProof(accDB, missingAddress, @[])
109-
check verifyAccountProof(stateRoot, proofResponse).isMissing()
77+
check verifyAccountLeafMissing(stateRoot, proofResponse)
11078

11179
for address, account in genAccounts:
11280
let
@@ -116,12 +84,19 @@ proc checkProofsForMissingLeafs(
11684

11785
check slotProofs.len() == 1
11886
if account.storage.len() > 0:
119-
check verifySlotProof(proofResponse2.storageHash.toHash32(), slotProofs[0]).isMissing()
120-
87+
check verifySlotLeafMissing(proofResponse2.storageHash.toHash32(), slotProofs[0])
12188

12289
suite "Get proof json tests":
12390

124-
let genesisFiles = ["berlin2000.json", "chainid1.json", "chainid7.json", "merge.json", "devnet4.json", "devnet5.json", "holesky.json"]
91+
let genesisFiles = [
92+
"berlin2000.json",
93+
"chainid1.json",
94+
"chainid7.json",
95+
"merge.json",
96+
"devnet4.json",
97+
"devnet5.json",
98+
"holesky.json"
99+
]
125100

126101
test "Get proofs for existing leafs":
127102
for file in genesisFiles:

0 commit comments

Comments
 (0)