Skip to content

Commit 0e19973

Browse files
ernestognwAmxxarr00james-toussaint
authored
Add TrieProof library (#5826)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com> Co-authored-by: James Toussaint <33313130+james-toussaint@users.noreply.github.com>
1 parent 79e4988 commit 0e19973

File tree

14 files changed

+1096
-5
lines changed

14 files changed

+1096
-5
lines changed

.changeset/shaky-phones-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`TrieProof`: Add library for verifying Ethereum Merkle-Patricia trie inclusion proofs.

.changeset/tender-pans-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Bytes`: Add the `toNibbles` function that expands the nibbles (4 bits chunk) of a `bytes` buffer. Used for manipulating Patricia Merkle Trees keys and paths.

contracts/mocks/Stateless.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ import {SignedMath} from "../utils/math/SignedMath.sol";
5757
import {StorageSlot} from "../utils/StorageSlot.sol";
5858
import {Strings} from "../utils/Strings.sol";
5959
import {Time} from "../utils/types/Time.sol";
60+
import {TrieProof} from "../utils/cryptography/TrieProof.sol";
6061

6162
contract Dummy1234 {}

contracts/utils/Bytes.sol

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,48 @@ library Bytes {
202202
return result;
203203
}
204204

205+
/**
206+
* @dev Split each byte in `input` into two nibbles (4 bits each)
207+
*
208+
* Example: hex"01234567" → hex"0001020304050607"
209+
*/
210+
function toNibbles(bytes memory input) internal pure returns (bytes memory output) {
211+
assembly ("memory-safe") {
212+
let length := mload(input)
213+
output := mload(0x40)
214+
mstore(0x40, add(add(output, 0x20), mul(length, 2)))
215+
mstore(output, mul(length, 2))
216+
for {
217+
let i := 0
218+
} lt(i, length) {
219+
i := add(i, 0x10)
220+
} {
221+
let chunk := shr(128, mload(add(add(input, 0x20), i)))
222+
chunk := and(
223+
0x0000000000000000ffffffffffffffff0000000000000000ffffffffffffffff,
224+
or(shl(64, chunk), chunk)
225+
)
226+
chunk := and(
227+
0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff,
228+
or(shl(32, chunk), chunk)
229+
)
230+
chunk := and(
231+
0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff,
232+
or(shl(16, chunk), chunk)
233+
)
234+
chunk := and(
235+
0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff,
236+
or(shl(8, chunk), chunk)
237+
)
238+
chunk := and(
239+
0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f,
240+
or(shl(4, chunk), chunk)
241+
)
242+
mstore(add(add(output, 0x20), mul(i, 2)), chunk)
243+
}
244+
}
245+
}
246+
205247
/**
206248
* @dev Returns true if the two byte buffers are equal.
207249
*/

contracts/utils/Memory.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ library Memory {
112112
}
113113
}
114114

115+
/// @dev Returns true if the two slices contain the same data.
116+
function equal(Slice a, Slice b) internal pure returns (bool result) {
117+
Memory.Pointer ptrA = _pointer(a);
118+
Memory.Pointer ptrB = _pointer(b);
119+
uint256 lenA = length(a);
120+
uint256 lenB = length(b);
121+
assembly ("memory-safe") {
122+
result := eq(keccak256(ptrA, lenA), keccak256(ptrB, lenB))
123+
}
124+
}
125+
115126
/**
116127
* @dev Private helper: create a slice from raw values (length and pointer)
117128
*

contracts/utils/cryptography/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A collection of contracts and libraries that implement various signature validat
1111
* {SignatureChecker}: A library helper to support regular ECDSA from EOAs as well as ERC-1271 signatures for smart contracts.
1212
* {Hashes}: Commonly used hash functions.
1313
* {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs.
14+
* {TrieProof}: Library for verifying Ethereum Merkle-Patricia trie inclusion proofs.
1415
* {EIP712}: Contract with functions to allow processing signed typed structure data according to https://eips.ethereum.org/EIPS/eip-712[EIP-712].
1516
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
1617
* {WebAuthn}: Library for verifying WebAuthn Authentication Assertions.
@@ -38,6 +39,8 @@ A collection of contracts and libraries that implement various signature validat
3839

3940
{{MerkleProof}}
4041

42+
{{TrieProof}}
43+
4144
{{EIP712}}
4245

4346
{{ERC7739Utils}}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.27;
3+
4+
import {Math} from "../math/Math.sol";
5+
import {Bytes} from "../Bytes.sol";
6+
import {Memory} from "../Memory.sol";
7+
import {RLP} from "../RLP.sol";
8+
9+
/**
10+
* @dev Library for verifying Ethereum Merkle-Patricia trie inclusion proofs.
11+
*
12+
* The {traverse} and {verify} functions can be used to prove the following value:
13+
*
14+
* * Transaction against the transactionsRoot of a block.
15+
* * Event against receiptsRoot of a block.
16+
* * Account details (RLP encoding of [nonce, balance, storageRoot, codeHash]) against the stateRoot of a block.
17+
* * Storage slot (RLP encoding of the value) against the storageRoot of a account.
18+
*
19+
* Proving a storage slot is usually done in 3 steps:
20+
*
21+
* * From the stateRoot of a block, process the account proof (see `eth_getProof`) to get the account details.
22+
* * RLP decode the account details to extract the storageRoot.
23+
* * Use storageRoot of that account to process the storageProof (again, see `eth_getProof`).
24+
*
25+
* See https://ethereum.org/en/developers/docs/data-structures-and-encoding/patricia-merkle-trie[Merkle-Patricia trie]
26+
*
27+
* Based on https://github.com/ethereum-optimism/optimism/blob/ef970556e668b271a152124023a8d6bb5159bacf/packages/contracts-bedrock/src/libraries/trie/MerkleTrie.sol[this implementation from optimism].
28+
*/
29+
library TrieProof {
30+
using Bytes for *;
31+
using RLP for *;
32+
using Memory for *;
33+
34+
enum Prefix {
35+
EXTENSION_EVEN, // 0 - Extension node with even length path
36+
EXTENSION_ODD, // 1 - Extension node with odd length path
37+
LEAF_EVEN, // 2 - Leaf node with even length path
38+
LEAF_ODD // 3 - Leaf node with odd length path
39+
}
40+
41+
enum ProofError {
42+
NO_ERROR, // No error occurred during proof traversal
43+
EMPTY_KEY, // The provided key is empty
44+
INVALID_ROOT, // The validation of the root node failed
45+
INVALID_LARGE_NODE, // The validation of a large node failed
46+
INVALID_SHORT_NODE, // The validation of a short node failed
47+
EMPTY_PATH, // The path in a leaf or extension node is empty
48+
INVALID_PATH_REMAINDER, // The path remainder in a leaf or extension node is invalid
49+
EMPTY_EXTENSION_PATH_REMAINDER, // The path remainder in an extension node is empty
50+
INVALID_EXTRA_PROOF_ELEMENT, // A leaf value should be the last proof element
51+
EMPTY_VALUE, // The leaf value is empty
52+
MISMATCH_LEAF_PATH_KEY_REMAINDER, // The path remainder in a leaf node doesn't match the key remainder
53+
UNKNOWN_NODE_PREFIX, // The node prefix is unknown
54+
UNPARSEABLE_NODE, // The node cannot be parsed from RLP encoding
55+
INVALID_PROOF // General failure during proof traversal
56+
}
57+
58+
error TrieProofTraversalError(ProofError err);
59+
60+
/// @dev The radix of the Ethereum trie
61+
uint256 internal constant EVM_TREE_RADIX = 16;
62+
63+
/// @dev Number of items in a branch node (16 children + 1 value)
64+
uint256 internal constant BRANCH_NODE_LENGTH = EVM_TREE_RADIX + 1;
65+
66+
/// @dev Number of items in leaf or extension nodes (always 2)
67+
uint256 internal constant LEAF_OR_EXTENSION_NODE_LENGTH = 2;
68+
69+
/// @dev Verifies a `proof` against a given `key`, `value`, `and root` hash.
70+
function verify(
71+
bytes memory value,
72+
bytes32 root,
73+
bytes memory key,
74+
bytes[] memory proof
75+
) internal pure returns (bool) {
76+
(bytes memory processedValue, ProofError err) = tryTraverse(root, key, proof);
77+
return processedValue.equal(value) && err == ProofError.NO_ERROR;
78+
}
79+
80+
/**
81+
* @dev Traverses a proof with a given key and returns the value.
82+
*
83+
* Reverts with {TrieProofTraversalError} if proof is invalid.
84+
*/
85+
function traverse(bytes32 root, bytes memory key, bytes[] memory proof) internal pure returns (bytes memory) {
86+
(bytes memory value, ProofError err) = tryTraverse(root, key, proof);
87+
require(err == ProofError.NO_ERROR, TrieProofTraversalError(err));
88+
return value;
89+
}
90+
91+
/**
92+
* @dev Traverses a proof with a given key and returns the value and an error flag
93+
* instead of reverting if the proof is invalid. This function may still revert if
94+
* malformed input leads to RLP decoding errors.
95+
*/
96+
function tryTraverse(
97+
bytes32 root,
98+
bytes memory key,
99+
bytes[] memory proof
100+
) internal pure returns (bytes memory value, ProofError err) {
101+
if (key.length == 0) return (_emptyBytesMemory(), ProofError.EMPTY_KEY);
102+
103+
// Expand the key
104+
bytes memory keyExpanded = key.toNibbles();
105+
106+
bytes32 currentNodeId;
107+
uint256 currentNodeIdLength;
108+
109+
// Free memory pointer cache
110+
Memory.Pointer fmp = Memory.getFreeMemoryPointer();
111+
112+
// Traverse proof
113+
uint256 keyIndex = 0;
114+
uint256 proofLength = proof.length;
115+
for (uint256 i = 0; i < proofLength; ++i) {
116+
// validates the encoded node matches the expected node id
117+
bytes memory encoded = proof[i];
118+
if (keyIndex == 0) {
119+
// Root node must match root hash
120+
if (keccak256(encoded) != root) return (_emptyBytesMemory(), ProofError.INVALID_ROOT);
121+
} else if (encoded.length >= 32) {
122+
// Large nodes are stored as hashes
123+
if (currentNodeIdLength != 32 || keccak256(encoded) != currentNodeId)
124+
return (_emptyBytesMemory(), ProofError.INVALID_LARGE_NODE);
125+
} else {
126+
// Small nodes must match directly
127+
if (currentNodeIdLength != encoded.length || bytes32(encoded) != currentNodeId)
128+
return (_emptyBytesMemory(), ProofError.INVALID_SHORT_NODE);
129+
}
130+
131+
// decode the current node as an RLP list, and process it
132+
Memory.Slice[] memory decoded = encoded.decodeList();
133+
if (decoded.length == BRANCH_NODE_LENGTH) {
134+
// If we've consumed the entire key, the value must be in the last slot
135+
// Otherwise, continue down the branch specified by the next nibble in the key
136+
if (keyIndex == keyExpanded.length) {
137+
return _validateLastItem(decoded[EVM_TREE_RADIX], proofLength, i);
138+
} else {
139+
bytes1 branchKey = keyExpanded[keyIndex];
140+
(currentNodeId, currentNodeIdLength) = _getNodeId(decoded[uint8(branchKey)]);
141+
keyIndex += 1;
142+
}
143+
} else if (decoded.length == LEAF_OR_EXTENSION_NODE_LENGTH) {
144+
bytes memory path = decoded[0].readBytes().toNibbles(); // expanded path
145+
// The following is equivalent to path.length < 2 because toNibbles can't return odd-length buffers
146+
if (path.length == 0) {
147+
return (_emptyBytesMemory(), ProofError.EMPTY_PATH);
148+
}
149+
uint8 prefix = uint8(path[0]);
150+
Memory.Slice keyRemainder = keyExpanded.asSlice().slice(keyIndex); // Remaining key to match
151+
Memory.Slice pathRemainder = path.asSlice().slice(2 - (prefix % 2)); // Path after the prefix
152+
uint256 pathRemainderLength = pathRemainder.length();
153+
154+
// pathRemainder must not be longer than keyRemainder, and it must be a prefix of it
155+
if (
156+
pathRemainderLength > keyRemainder.length() ||
157+
!pathRemainder.equal(keyRemainder.slice(0, pathRemainderLength))
158+
) {
159+
return (_emptyBytesMemory(), ProofError.INVALID_PATH_REMAINDER);
160+
}
161+
162+
if (prefix <= uint8(Prefix.EXTENSION_ODD)) {
163+
// Eq to: prefix == EXTENSION_EVEN || prefix == EXTENSION_ODD
164+
if (pathRemainderLength == 0) {
165+
return (_emptyBytesMemory(), ProofError.EMPTY_EXTENSION_PATH_REMAINDER);
166+
}
167+
// Increment keyIndex by the number of nibbles consumed and continue traversal
168+
(currentNodeId, currentNodeIdLength) = _getNodeId(decoded[1]);
169+
keyIndex += pathRemainderLength;
170+
} else if (prefix <= uint8(Prefix.LEAF_ODD)) {
171+
// Eq to: prefix == LEAF_EVEN || prefix == LEAF_ODD
172+
//
173+
// Leaf node (terminal) - return its value if key matches completely
174+
// we already know that pathRemainder is a prefix of keyRemainder, so checking the length sufficient
175+
return
176+
pathRemainderLength == keyRemainder.length()
177+
? _validateLastItem(decoded[1], proofLength, i)
178+
: (_emptyBytesMemory(), ProofError.MISMATCH_LEAF_PATH_KEY_REMAINDER);
179+
} else {
180+
return (_emptyBytesMemory(), ProofError.UNKNOWN_NODE_PREFIX);
181+
}
182+
} else {
183+
return (_emptyBytesMemory(), ProofError.UNPARSEABLE_NODE);
184+
}
185+
186+
// Reset memory before next iteration. Deallocates `decoded` and `path`.
187+
Memory.setFreeMemoryPointer(fmp);
188+
}
189+
190+
// If we've gone through all proof elements without finding a value, the proof is invalid
191+
return (_emptyBytesMemory(), ProofError.INVALID_PROOF);
192+
}
193+
194+
/**
195+
* @dev Validates that we've reached a valid leaf value and this is the last proof element.
196+
* Ensures the value is not empty and no extra proof elements exist.
197+
*/
198+
function _validateLastItem(
199+
Memory.Slice item,
200+
uint256 trieProofLength,
201+
uint256 i
202+
) private pure returns (bytes memory, ProofError) {
203+
if (i != trieProofLength - 1) {
204+
return (_emptyBytesMemory(), ProofError.INVALID_EXTRA_PROOF_ELEMENT);
205+
}
206+
bytes memory value = item.readBytes();
207+
return (value, value.length == 0 ? ProofError.EMPTY_VALUE : ProofError.NO_ERROR);
208+
}
209+
210+
/**
211+
* @dev Extracts the node ID (hash or raw data based on size)
212+
*
213+
* For small nodes (encoded length < 32 bytes) the node ID is the node content itself,
214+
* For larger nodes, the node ID is the hash of the encoded node data.
215+
*
216+
* NOTE: Under normal operation, the input should never be exactly 32-byte inputs. If such an input is provided,
217+
* it will be used directly, similarly to how small nodes are processed. The following traversal check whether
218+
* the next node is a large one, and whether its hash matches the raw 32 bytes we have here. If that is the case,
219+
* the value will be accepted. Otherwise, the next step will return an {INVALID_LARGE_NODE} error.
220+
*/
221+
function _getNodeId(Memory.Slice node) private pure returns (bytes32 nodeId, uint256 nodeIdLength) {
222+
uint256 nodeLength = node.length();
223+
return nodeLength < 33 ? (node.load(0), nodeLength) : (node.readBytes32(), 32);
224+
}
225+
226+
function _emptyBytesMemory() private pure returns (bytes memory result) {
227+
assembly ("memory-safe") {
228+
result := 0x60 // mload(0x60) is always 0
229+
}
230+
}
231+
}

0 commit comments

Comments
 (0)