// 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; }