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.
160 lines
6.3 KiB
Solidity
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))
|
|
}
|
|
}
|
|
}
|