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>
This commit is contained in:
Aere Network 2026-10-02 06:23:56 +03:00
parent a406a193f6
commit 88a87df694
3 changed files with 157 additions and 80 deletions

81
tools/mpt.mjs Normal file
View File

@ -0,0 +1,81 @@
// 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;
}

View File

@ -54,6 +54,7 @@ import { secp256k1 } from '@noble/curves/secp256k1';
import fs from 'node:fs';
import path from 'node:path';
import { verifyFalcon512, fromHex } from './falcon512.mjs';
import { accountAt, slotAt } from './mpt.mjs';
// Early refusals stop through stop(1), not process.exit(1): on Windows (Node 24), process.exit() right after a fetch aborts node
// inside libuv while the HTTP sockets close, and the caller sees exit code 127 instead of 1 (measured 2026-09-27: 8 of 8 runs; with
// stop(), 6 of 6 runs exit 1). stop() prints nothing itself, sets the code, and throws a marker that the handler below swallows, so the
@ -264,7 +265,12 @@ for (const m of index.manifests) {
}
// leg 5: the on-chain pin. keccak256(addr20 || pk, row by row) must equal slot 0 of the immutable
// anchor contract, and that contract must be the 11-byte read-only getter (no SSTORE anywhere).
let pinOk = false, pinNote = 'no anchorContract in index';
// 2026-10-02 (AIP-24 C4 (b)): the slot is no longer read at 'latest' from the endpoint (eth_getStorageAt / eth_getCode, which an
// endpoint can answer as it likes). It is PROVEN below, after the anchors: eth_getProof of the anchor contract under the state root
// of the parent of the newest VERIFIED anchor, the header the certificate's seals signed, so a lying endpoint cannot hand this tool
// a pin of its choosing. Here the manifest is judged on what does not need the chain: sha256, rows, chain, format; `pin` is filled
// in below (true / false / null when it could not be measured).
let pinOk = null, pinNote = 'no anchorContract in index', want = null;
if (!m.anchorContract && m.pin === 'genesis') {
// testnetul pinuieste manifestul in configuratia genezei (pqRegistryHash), care nu se citeste prin RPC:
// legatura e in fisierul de geneza pe care il rulezi deja, nu una pe care unealta o poate verifica aici.
@ -272,21 +278,14 @@ for (const m of index.manifests) {
}
if (m.anchorContract) {
const pre = Buffer.concat(rows.flatMap(r => [hex(r.addr), Buffer.from(r.pk)]));
const want = '0x' + keccak(pre).toString('hex');
try {
const slot0 = await rpc('eth_getStorageAt', [m.anchorContract, '0x0', 'latest']);
const code = await rpc('eth_getCode', [m.anchorContract, 'latest']);
const getter = code.toLowerCase() === '0x60005460005260206000f3';
pinOk = slot0.toLowerCase() === want.toLowerCase() && getter;
pinNote = (slot0.toLowerCase() === want.toLowerCase() ? 'slot 0 matches' : 'slot 0 MISMATCH (' + slot0.slice(0, 10) + ' vs ' + want.slice(0, 10) + ')') +
(getter ? ', contract is the read-only getter' : ', contract code is NOT the known read-only getter');
} catch (e) { pinNote = 'pin unmeasured: ' + e.message; }
want = '0x' + keccak(pre).toString('hex');
pinNote = 'slot 0 of ' + m.anchorContract + ' judged below, under the state of the certified parent of the newest verified anchor';
}
const good = shaOk && pinOk && rowsOk === man.count && Number(man.chainId) === CHAIN_ID && Number(man.formatVersion) === FORMAT_VERSION;
const good = shaOk && rowsOk === man.count && Number(man.chainId) === CHAIN_ID && Number(man.formatVersion) === FORMAT_VERSION;
if (!good) manifestBad++;
console.log(' bindHeight ' + String(man.bindHeight).padEnd(10) + ' ' + m.file.padEnd(26) + ' rows ' + rowsOk + '/' + man.count +
' sha256 ' + (shaOk ? 'matches index' : 'MISMATCH') + ' on-chain pin: ' + pinNote + (rowsBad.length ? ' BAD rows: ' + rowsBad.join(' ') : '') + (good ? '' : ' <-- FAIL'));
manifests.push({ bindHeight: Number(man.bindHeight), count: man.count, rows, good });
manifests.push({ bindHeight: Number(man.bindHeight), count: man.count, rows, good, file: m.file, anchorContract: m.anchorContract || null, want, pin: pinOk });
}
manifests.sort((a, b) => a.bindHeight - b.bindHeight);
const manifestAt = (h) => [...manifests].reverse().find(m => m.bindHeight <= h) || null;
@ -498,6 +497,7 @@ let lapseNotEnforced = 0;
// The ANCHOR_REPORT keys below are unchanged (the AIP-23 verifier reads them).
let newestOk = null;
let newestParentHash = null; // the hash of block newestOk - 1 the verified seals signed (M(parent)), for ANCHOR_REPORT
let newestParent = null; // that parent's header as served, re-encoded above to the certified hash: its stateRoot is bound to the seals
// ce scheme au fost CHIAR verificate: rezumatul final nu are voie sa numeasca o schema pe care nu a vazut-o
const schemesSeen = new Set();
let cursor = anchor;
@ -633,7 +633,7 @@ for (let i = 0; i < COUNT; i++) {
&& unknownScheme.length === 0 && unknownIndex.length === 0
&& notValidator.length === 0 && noKey.length === 0 && unbound.length === 0 && duplicate.length === 0;
const summary = Object.entries(perScheme).map(([nm, c]) => c + ' ' + nm).join(' + ');
if (good) { ok++; if (newestOk === null) { newestOk = h; newestParentHash = String(parent.hash).toLowerCase(); } console.log(h + ' OK: bound under block hash' + (isV2 ? ' (v2, scheme-tagged)' : '') + ', ' + n + ' seals (' + summary + '), all valid and from distinct validators of block ' + (h - 1) + ', the set its own header lists (' + (h - 1 >= KEYS_FROM && v3EpochAt(h - 1) ? 'keys of the registry v3 epoch ' + v3EpochAt(h - 1).bindHeight : 'manifest ' + man.bindHeight) + ')'
if (good) { ok++; if (newestOk === null) { newestOk = h; newestParentHash = String(parent.hash).toLowerCase(); newestParent = parent; } console.log(h + ' OK: bound under block hash' + (isV2 ? ' (v2, scheme-tagged)' : '') + ', ' + n + ' seals (' + summary + '), all valid and from distinct validators of block ' + (h - 1) + ', the set its own header lists (' + (h - 1 >= KEYS_FROM && v3EpochAt(h - 1) ? 'keys of the registry v3 epoch ' + v3EpochAt(h - 1).bindHeight : 'manifest ' + man.bindHeight) + ')'
+ (lapseAt(h) ? '; judged at K=' + MIN_SEALS + ', the threshold in force in the dated window ' + lapseAt(h).first + '..' + lapseAt(h).last + ' (' + lapseAt(h).date + '), digest and order required as everywhere' : '')); }
else if (envBlocked) {
unmeasured++;
@ -656,6 +656,49 @@ for (let i = 0; i < COUNT; i++) {
}
}
// 5. ON-CHAIN PIN, proven under the certified state (2026-10-02, AIP-24 C4 (b)). For every key manifest the index binds to an anchor
// contract, eth_getProof of that contract at the PARENT of the newest verified anchor: the account must exist under that header's
// state root (the header re-encodes to the hash the seals signed, so the root is certified), its code hash must be the 11-byte
// read-only getter, and slot 0, proven under the account's storage root, must hold keccak256(addr || pk over the rows). An endpoint
// that keeps no state for that block makes the pin UNMEASURED (exit 2), never VERIFIED; a proof that does not hold, or a value that
// is not the manifest's hash, is a FAIL of that manifest.
const GETTER_CODEHASH = keccak(Buffer.from('60005460005260206000f3', 'hex')).toString('hex');
let pinUnmeasured = 0;
{
const judged = manifests.filter(mm => mm.anchorContract);
if (judged.length && newestOk === null) {
for (const mm of judged) { pinUnmeasured++; mm.pin = null; }
console.log('on-chain pin: not judged, no anchor verified in this run (the pin is proven under the state of a certified parent)');
}
for (const mm of judged) {
if (newestOk === null) break;
const X = newestOk - 1;
let note, okPin = false;
try {
const pr = await rpc('eth_getProof', [mm.anchorContract, ['0x0000000000000000000000000000000000000000000000000000000000000000'], '0x' + X.toString(16)]);
const acct = accountAt(newestParent.stateRoot, hex(mm.anchorContract.toLowerCase()), pr && pr.accountProof);
if (!acct) { note = 'FAIL: the state of block ' + X + ' has no account at ' + mm.anchorContract; }
else if (acct[3].toString('hex') !== GETTER_CODEHASH) { note = 'FAIL: the account at ' + mm.anchorContract + ' in the state of block ' + X + ' is not the read-only getter (code hash 0x' + acct[3].toString('hex').slice(0, 16) + '..)'; }
else {
const sp = (pr.storageProof || [])[0];
const v = slotAt('0x' + acct[2].toString('hex'), Buffer.alloc(32), sp && sp.proof);
const got = '0x' + (v ? Buffer.from(v).toString('hex').padStart(64, '0') : '0'.repeat(64));
okPin = got === mm.want.toLowerCase() /*PAZA:pin-under-certified-state*/;
note = okPin ? 'slot 0 matches, proven under the state of block ' + X + ' (the certified parent of anchor ' + newestOk + '), contract is the read-only getter'
: 'FAIL: slot 0 MISMATCH under the state of block ' + X + ' (' + got.slice(0, 10) + ' vs ' + mm.want.slice(0, 10) + ')';
}
mm.pin = okPin;
} catch (e) {
const text = String(e.message);
// a proof the endpoint served that does not hold is a finding; an endpoint that cannot serve the proof is this run's gap
if (/missing the node|does not hold|not a list|hex-prefix|branch child|extension child|deeper than any key|RLP/.test(text)) { mm.pin = false; note = 'FAIL: the proof served for ' + mm.anchorContract + ' at block ' + X + ' does not hold: ' + text; }
else { mm.pin = null; pinUnmeasured++; note = 'UNMEASURED: eth_getProof at block ' + X + ' failed (the endpoint may not keep that state): ' + text.slice(0, 120); }
}
if (mm.pin === false) { manifestBad++; mm.good = false; }
console.log(' pin ' + mm.file.padEnd(26) + ' ' + note);
}
}
// 6. PROPOSER SEALS, AIP-22 stage 5 (added 2026-09-24). From H_hdr every header carries element 6, [index, signature]: the
// proposer's Falcon-512 seal over keccak256(RLP["AERE-PQ-PROPOSAL-1", chainId, height, round, digest]), where digest is the hash
// of the header re-encoded in the EXCLUDE_COMMIT_SEALS form (SPEC 2.3: round kept, commit seals an empty list, the certificate
@ -801,7 +844,8 @@ console.log(propChecked === 0
+ ' or of the proposer of the round that re-proposed the block' + (propReproposed ? ' (' + propReproposed + ' re-proposal(s), named above)' : '')
+ (propUnmeasured ? ', ' + propUnmeasured + ' could not be checked here' : '') + (propAbsent ? ' (' + propAbsent + ' earlier headers carry none)' : ''));
console.log('seals: ' + sealsValid + '/' + sealsTotal + ' cryptographically valid and validator-bound manifests: ' + (manifests.length - manifestBad) + '/' + manifests.length + ' self-consistent');
const all = bad === 0 && manifestBad === 0 && unmeasured === 0 && setUnmeasured === 0 && propBad === 0 && propUnmeasured === 0;
const all = bad === 0 && manifestBad === 0 && unmeasured === 0 && setUnmeasured === 0 && propBad === 0 && propUnmeasured === 0 && pinUnmeasured === 0;
if (pinUnmeasured) console.log('UNMEASURED: the on-chain pin of ' + pinUnmeasured + ' manifest(s) could not be proven here (no verified anchor in this run, or the endpoint served no state proof for the certified parent). This says nothing about the chain; try another endpoint (RPC=...).');
// D-478: an anchor in the dated lapse where nothing was in force is NOT counted as verified. The VERIFIED line counts only the anchors
// actually judged, the lapse ones get their own line, and a run in which every anchor read fell in the lapse verified nothing: it says
// so and exits 2 (nothing measured), never 0 with a VERIFIED line over anchors where nothing was checked.

View File

@ -27,11 +27,13 @@ import crypto from 'node:crypto';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { keccak_256 } from '@noble/hashes/sha3';
import { pathToFileURL } from 'node:url';
import { rlpDecode, mptGet } from './mpt.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const NOTARY = { 2800: '0x4aB392c4Aca7D9D4C16c0b60a9514c5025bd58c7', 28001: '0x70099E62735500AA2F85B60C518551a57B202d54' };
const RPC_DEFAULT = { 2800: 'https://rpc.aere.network', 28001: 'https://testnet-rpc.aere.network' };
const MANIFESTS_DEFAULT = { 2800: 'https://aere.network/pq/manifests', 28001: 'https://testnet-rpc.aere.network/pq/manifests' };
export const NOTARY = { 2800: '0x4aB392c4Aca7D9D4C16c0b60a9514c5025bd58c7', 28001: '0x70099E62735500AA2F85B60C518551a57B202d54' };
export const RPC_DEFAULT = { 2800: 'https://rpc.aere.network', 28001: 'https://testnet-rpc.aere.network' };
export const MANIFESTS_DEFAULT = { 2800: 'https://aere.network/pq/manifests', 28001: 'https://testnet-rpc.aere.network/pq/manifests' };
// keccak256 of the AereNotary runtime code (the same bytes on 2800 and 28001): an address whose account proves another code hash
// is not the notary, whatever the endpoint says
const NOTARY_CODEHASH_FULL = '8eaa924081e9718fcee05ae64fadaa1815437ebbbcef5b87317c5879050452ce';
@ -44,35 +46,15 @@ const stop = (code) => { process.exitCode = code; throw new Stop(); };
const hexb = (h) => Buffer.from(String(h).replace(/^0x/, '').replace(/^(.(..)*)$/, '0$1'), 'hex');
const keccak = (b) => Buffer.from(keccak_256(b));
const encLen = (n, off) => { if (n < 56) return Buffer.from([off + n]); const l = Buffer.from(n.toString(16).replace(/^(.(..)*)$/, '0$1'), 'hex'); return Buffer.concat([Buffer.from([off + 55 + l.length]), l]); };
function rlpEncode(x) {
export function rlpEncode(x) {
if (Array.isArray(x)) { const body = Buffer.concat(x.map(rlpEncode)); return Buffer.concat([encLen(body.length, 0xc0), body]); }
const b = Buffer.from(x);
if (b.length === 1 && b[0] < 0x80) return b;
return Buffer.concat([encLen(b.length, 0x80), b]);
}
const scalar = (h) => { const n = BigInt(h); return n === 0n ? Buffer.alloc(0) : hexb(n.toString(16)); };
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 on-chain hash of a QBFT header: extraData re-encoded as [vanity, validators, vote, round 0, no commit seals], the certificate
// and the proposer seal left out (they are outside the hash). This mirrors the re-encoding in verify-anchor.mjs; if the two ever
// drifted, the check below would FAIL (the hash would not match), never pass.
function onchainHash(b) {
/** the hash the chain gives a header: QBFT strips the committed seals and zeroes the round before hashing (CLAUDE.md s.2) */
export function onchainHash(b) {
const e = rlpDecode(hexb(b.extraData));
const extra = rlpEncode([e[0], e[1], e[2], Buffer.alloc(0), []]);
const f = [hexb(b.parentHash), hexb(b.sha3Uncles), hexb(b.miner), hexb(b.stateRoot), hexb(b.transactionsRoot), hexb(b.receiptsRoot),
@ -86,45 +68,9 @@ function onchainHash(b) {
if (b.balHash != null) f.push(hexb(b.balHash));
return '0x' + keccak(rlpEncode(f)).toString('hex');
}
// A Merkle-Patricia proof (eth_getProof): the value at keccak(key) under root, or null when the proof shows it absent. Every node
// is looked up by its own hash, starting from the root, so a node the endpoint added or changed is simply never reached.
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');
}
// rlpDecode and mptGet live in mpt.mjs since 2026-10-02 (shared with verify-anchor.mjs, which proves the anchor contract's slot 0
// under the certified parent state, AIP-24 C4 (b)); re-exported here so that nothing that imported them from this file changes.
export { rlpDecode, mptGet };
const level = (name, state, detail) => ({ level: name, state, detail });
// ---------- levels 1 and 2 ----------
@ -158,7 +104,7 @@ function signature(env, text) {
}
// ---------- level 4 ----------
async function finality(env, o) {
export async function finality(env, o) {
if (!o.rpcGiven && !env.notarization) return level('finality', 'ABSENT', 'no notarization claimed and no --rpc given');
const rpc = async (method, params) => {
const r = await fetch(o.rpc, { method: 'POST', headers: { 'content-type': 'application/json', 'user-agent': 'aere-verify-proof/1' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
@ -219,6 +165,11 @@ async function finality(env, o) {
}
// ---------- main ----------
// 2026-10-02 (AIP-24, L1): the finality path and its helpers are exported so that verify-shield.mjs (tools/aere-quantum-shield)
// judges a period head H_n through the SAME function, on a live endpoint or on a captured package replayed locally, instead of a
// second copy of this logic. Run as a program, this file behaves exactly as before; imported, it runs nothing.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await main();
async function main() {
const args = process.argv.slice(2);
const opt = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : null; };
const file = args.find((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--') && args[i - 1] !== '--json'));
@ -245,3 +196,4 @@ else {
console.log(`VERDICT: ${verdict}`);
}
process.exitCode = failed ? 1 : unmeasured ? 2 : 0;
}