aere-node/tools/mpt.mjs
Aere Network 88a87df694 tools: the on-chain pin proven under the certified state (mpt.mjs), shared by verify-anchor.mjs and verify-proof.mjs
Copies of aerenew/verificator-falcon at 2026-10-02 (AIP-24 C4 (b)): slot 0 of every anchor contract is proven with eth_getProof
under the state root of the certified parent of the newest verified anchor; nothing is read at `latest` any more. mpt.mjs is the
Merkle-Patricia proof walker both tools import: it must sit next to them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 06:23:56 +03:00

82 lines
4.4 KiB
JavaScript

// mpt.mjs - a Merkle-Patricia proof walker (eth_getProof) and the RLP decoder it needs, in ONE place for the two tools that prove
// state against a certified header: verify-proof.mjs (the AereNotary entry, AIP-23 section 4) and verify-anchor.mjs (slot 0 of the
// anchor contract under the state of the certified parent, AIP-24 C4 (b), 2026-10-02). Until this date each tool had its own copy, or
// read the slot at 'latest' from the endpoint.
//
// Every node of a proof is looked up by its own keccak hash, starting from the root, so a node the endpoint added or changed is simply
// never reached: a proof is accepted only when it leads, hash by hash, from the root the certified header states to the value.
import { keccak_256 } from '@noble/hashes/sha3';
export const hexb = (h) => Buffer.from(String(h).replace(/^0x/, '').replace(/^(.(..)*)$/, '0$1'), 'hex');
export const keccak = (b) => Buffer.from(keccak_256(b));
/** strict RLP decoding: nested lists become arrays, strings become Buffers; trailing bytes are an error */
export function rlpDecode(buf) {
const [v, end] = dec(buf, 0);
if (end !== buf.length) throw new Error('trailing bytes after an RLP item');
return v;
function dec(b, o) {
const p = b[o];
if (p === undefined) throw new Error('RLP runs past its input');
if (p < 0x80) return [b.subarray(o, o + 1), o + 1];
if (p < 0xb8) return [b.subarray(o + 1, o + 1 + p - 0x80), o + 1 + p - 0x80];
if (p < 0xc0) { const l = p - 0xb7; const n = Number(BigInt('0x' + b.subarray(o + 1, o + 1 + l).toString('hex'))); return [b.subarray(o + 1 + l, o + 1 + l + n), o + 1 + l + n]; }
let n, s;
if (p < 0xf8) { n = p - 0xc0; s = o + 1; } else { const l = p - 0xf7; n = Number(BigInt('0x' + b.subarray(o + 1, o + 1 + l).toString('hex'))); s = o + 1 + l; }
const out = []; let q = s;
while (q < s + n) { const [x, r] = dec(b, q); out.push(x); q = r; }
if (q !== s + n) throw new Error('RLP list length mismatch');
return [out, s + n];
}
}
/** the value at keccak(key) under root, or null when the proof shows it absent; throws when the proof does not hold */
export function mptGet(root, hashedKey, proof) {
const byHash = new Map(proof.map((p) => { const b = hexb(p); return [keccak(b).toString('hex'), b]; }));
const nib = (buf) => { const o = []; for (const x of buf) o.push(x >> 4, x & 15); return o; };
let key = nib(hashedKey), ref = hexb(root);
for (let depth = 0; depth < 70; depth++) {
let node;
if (Buffer.isBuffer(ref)) {
const raw = byHash.get(ref.toString('hex'));
if (!raw) throw new Error('the proof is missing the node ' + ref.toString('hex').slice(0, 16) + '..');
node = rlpDecode(raw);
} else node = ref;
if (!Array.isArray(node)) throw new Error('a proof node is not a list');
if (node.length === 17) {
if (key.length === 0) return node[16].length ? node[16] : null;
const next = node[key[0]]; key = key.slice(1);
if (Array.isArray(next)) { ref = next; continue; }
if (next.length === 0) return null;
if (next.length !== 32) throw new Error('a branch child is neither a hash nor an inline node');
ref = next; continue;
}
if (node.length === 2) {
const enc = nib(node[0]); const flag = enc[0];
if (flag > 3) throw new Error('bad hex-prefix flag');
const p = enc.slice(flag % 2 === 1 ? 1 : 2);
const matches = key.length >= p.length && p.every((n, i) => key[i] === n);
if (flag >= 2) return matches && key.length === p.length ? node[1] : null;
if (!matches) return null;
key = key.slice(p.length);
const next = node[1];
if (Array.isArray(next)) { ref = next; continue; }
if (next.length !== 32) throw new Error('an extension child is not a hash');
ref = next; continue;
}
throw new Error('a proof node with ' + node.length + ' items');
}
throw new Error('the proof is deeper than any key');
}
/** an account's [nonce, balance, storageRoot, codeHash] proven under a state root, or null when the account is absent */
export function accountAt(stateRoot, address20, accountProof) {
const a = mptGet(stateRoot, keccak(address20), accountProof || []);
return a ? rlpDecode(a) : null;
}
/** a storage slot's value (bytes, RLP-decoded, leading zeros stripped by the client) proven under an account's storage root */
export function slotAt(storageRoot, slot32, storageProof) {
const v = mptGet(storageRoot, keccak(slot32), storageProof || []);
return v ? rlpDecode(v) : null;
}