aere-contracts/contracts/interop/lib/MerklePatriciaProof.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

160 lines
6.3 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "./RLPReader.sol";
/**
* @title MerklePatriciaProof — hexary Merkle-Patricia trie inclusion/exclusion verifier.
*
* @notice Solidity port of the validated Rust verifier in
* `zk-interop/interop-core/src/mpt.rs` (used verbatim inside the AERE SP1
* interop guest). Given a trie `root`, a key as a nibble path, and the set of
* trie nodes along that path (as `eth_getProof` / a receipts-trie builder
* emits), it returns the trie value for an included key or the empty flag for a
* proven absent key. Byte-exact: a tampered node or wrong root fails rather
* than lies.
*
* Node model (keccak-hashed refs, hex-prefix paths):
* leaf [ HP(path, leaf), value ]
* extension [ HP(path, ext), child_ref ]
* branch [ c0, c1, ..., c15, value ]
*/
library MerklePatriciaProof {
using RLPReader for bytes;
/// @notice Verify inclusion of `key` under `root` given `nodes`.
/// @param root the 32-byte trie root (e.g. a header's receiptsRoot).
/// @param key the key bytes (for a receipts trie: rlp(transactionIndex)).
/// @param nodes the proof node set; order irrelevant (resolved by hash).
/// @return included true if the key is present, false if proven absent.
/// @return value the trie value bytes when included (empty otherwise).
function verify(bytes32 root, bytes memory key, bytes[] memory nodes)
internal
pure
returns (bool included, bytes memory value)
{
bytes memory path = _nibbles(key);
// Empty trie: root = keccak(rlp("")) = keccak(0x80). Any key is absent.
if (root == keccak256(hex"80")) {
return (false, "");
}
bytes memory node = _findByHash(nodes, root);
uint256 idx = 0;
// Bounded loop: a valid path is at most 64 nibbles (+ node hops); 512 is safe.
for (uint256 hop = 0; hop < 512; hop++) {
bytes[] memory items = node.splitList();
if (items.length == 2) {
(bool isLeaf, bytes memory frag) = _hpDecode(items[0].readBytes());
if (isLeaf) {
if (_eq(_slice(path, idx), frag)) {
return (true, items[1].readBytes());
}
return (false, ""); // diverges at leaf -> exclusion
}
// extension: remaining path must start with frag
bytes memory rem = _slice(path, idx);
if (rem.length < frag.length || !_startsWith(rem, frag)) {
return (false, "");
}
idx += frag.length;
node = _resolve(items[1], nodes);
} else if (items.length == 17) {
if (idx >= path.length) {
// value in slot 16
if (_isEmptyRef(items[16])) return (false, "");
return (true, items[16].readBytes());
}
uint256 nib = uint8(path[idx]);
idx += 1;
bytes memory child = items[nib];
if (_isEmptyRef(child)) return (false, "");
node = _resolve(child, nodes);
} else {
revert("MPT:bad arity");
}
}
revert("MPT:path too long");
}
// ---- node reference resolution ------------------------------------------
/// @dev Resolve a child reference: an inline (embedded) list node IS the node;
/// a 32-byte string is a hash to look up in the node set.
function _resolve(bytes memory ref, bytes[] memory nodes) private pure returns (bytes memory) {
if (RLPReader.isList(ref)) {
return ref; // embedded node
}
bytes memory content = ref.readBytes();
require(content.length == 32, "MPT:bad ref");
return _findByHash(nodes, _toBytes32(content));
}
function _findByHash(bytes[] memory nodes, bytes32 h) private pure returns (bytes memory) {
for (uint256 i = 0; i < nodes.length; i++) {
if (keccak256(nodes[i]) == h) return nodes[i];
}
revert("MPT:missing node");
}
function _isEmptyRef(bytes memory ref) private pure returns (bool) {
return ref.length == 0 || (ref.length == 1 && ref[0] == 0x80);
}
// ---- path / nibble helpers ----------------------------------------------
function _nibbles(bytes memory b) private pure returns (bytes memory out) {
out = new bytes(b.length * 2);
for (uint256 i = 0; i < b.length; i++) {
out[2 * i] = bytes1(uint8(b[i]) >> 4);
out[2 * i + 1] = bytes1(uint8(b[i]) & 0x0f);
}
}
/// @dev Hex-prefix ("compact") decode. First nibble: bit1=leaf, bit0=odd.
function _hpDecode(bytes memory enc) private pure returns (bool isLeaf, bytes memory nibbles) {
require(enc.length != 0, "MPT:empty hp");
uint8 flags = uint8(enc[0]) >> 4;
isLeaf = (flags & 0x2) != 0;
bool odd = (flags & 0x1) != 0;
uint256 count = (enc.length - 1) * 2 + (odd ? 1 : 0);
nibbles = new bytes(count);
uint256 k = 0;
if (odd) {
nibbles[k++] = bytes1(uint8(enc[0]) & 0x0f);
}
for (uint256 i = 1; i < enc.length; i++) {
nibbles[k++] = bytes1(uint8(enc[i]) >> 4);
nibbles[k++] = bytes1(uint8(enc[i]) & 0x0f);
}
}
function _slice(bytes memory b, uint256 start) private pure returns (bytes memory out) {
if (start >= b.length) return "";
out = new bytes(b.length - start);
for (uint256 i = 0; i < out.length; i++) out[i] = b[start + i];
}
function _startsWith(bytes memory a, bytes memory prefix) private pure returns (bool) {
if (a.length < prefix.length) return false;
for (uint256 i = 0; i < prefix.length; i++) {
if (a[i] != prefix[i]) return false;
}
return true;
}
function _eq(bytes memory a, bytes memory b) private pure returns (bool) {
return a.length == b.length && keccak256(a) == keccak256(b);
}
function _toBytes32(bytes memory b) private pure returns (bytes32 r) {
require(b.length == 32, "MPT:not b32");
assembly {
r := mload(add(b, 32))
}
}
}