tools/verify-anchor.mjs: certificate form v3 (round-bound commit seal, AIP-22 erratum): dispatcher on the first element, refusal codes AERE-PQ-V3-01..08, validator set of the parent, 'PARENT DECIDED IN ROUND r' only at a QBFT quorum of verified members; v1/v2 verdicts unchanged (20/20 on chain 2800 and 28001 before and after)

This commit is contained in:
Aere Network 2026-10-07 17:11:26 +03:00
parent 97542b5f1e
commit c05b2ba97b

View File

@ -33,6 +33,16 @@
// at most 1024 and the coinbase is the proposer of some round 0..r at that height. // at most 1024 and the coinbase is the proposer of some round 0..r at that height.
// Where no header carries it (chain 2800 today) the tool says so and this leg does not // Where no header carries it (chain 2800 today) the tool says so and this leg does not
// change the verdict. // change the verdict.
// 7. ROUND (1(b), D-470, learned 2026-10-07, active on no chain yet) from a chain's round height H_r the certificate is
// v3, RLP[3, round, [[scheme, idx, sig], ...]] under "AERE-PQ-ANCHOR-3", and every seal in it is over
// keccak256(RLP["AERE-PQ-COMMIT-2", chainId, parentNumber, round, parentHash]) for the ONE round it names, so a
// seal made in another round, or over another block, does not verify. The tool dispatches on the certificate's
// first element (a list: v1; the scalar 2: v2; the scalar 3: v3), admits only v3 at or above H_r and refuses
// v3 below it, and names every refusal with the code both clients name (AERE-PQ-V3-01..08). It reports how many
// distinct validators of the parent's set sealed in that round, against that set's QBFT quorum: at or above the
// quorum the line says "PARENT DECIDED IN ROUND r (m members of n, quorum Q)", which is what it measured (a QBFT
// quorum of commit seals for one round IS the decision), below it "CERTIFIED (participation)". It claims nothing
// further: "finality" is the chain's rule, not a measurement of this tool.
// //
// What this still does NOT prove: who RUNS the validators (today: one operator, the Foundation; we // What this still does NOT prove: who RUNS the validators (today: one operator, the Foundation; we
// publish that ourselves), or that the RPC you use is honest about chain contents - bring your own // publish that ourselves), or that the RPC you use is honest about chain contents - bring your own
@ -83,6 +93,25 @@ let slhDsa = null;
try { slhDsa = (await import('@noble/post-quantum/slh-dsa.js')).slh_dsa_sha2_128s; } catch { slhDsa = null; } try { slhDsa = (await import('@noble/post-quantum/slh-dsa.js')).slh_dsa_sha2_128s; } catch { slhDsa = null; }
const verifySlhDsa = (pk, msg, sig) => { if (!slhDsa) return false; try { return slhDsa.verify(new Uint8Array(sig), new Uint8Array(msg), new Uint8Array(pk)); } catch { return false; } }; const verifySlhDsa = (pk, msg, sig) => { if (!slhDsa) return false; try { return slhDsa.verify(new Uint8Array(sig), new Uint8Array(msg), new Uint8Array(pk)); } catch { return false; } };
const COMMIT_DOMAIN = 'AERE-PQ-COMMIT-1'; const COMMIT_DOMAIN = 'AERE-PQ-COMMIT-1';
// v3 (1(b), D-470, PROIECT-1B 1.1/1.3/1.4, learned 2026-10-07): from a chain's round height H_r the commit seal is ROUND-BOUND,
// keccak256(RLP["AERE-PQ-COMMIT-2", chainId, n, r, hash(n)]), and the anchor certificate is RLP[3, r, [[scheme, idx, sig], ...]] with
// the round written once, under its own domain "AERE-PQ-ANCHOR-3". One certificate, one round: every seal in it is over M_r of the
// PARENT for that r. The tool dispatches on the first element of the certificate: a LIST is v1, the scalar 2 is v2, the scalar 3 is
// v3; at an anchor at or above H_r only v3 is admitted, below H_r a v3 is refused, and every refusal of a v3 anchor names the code
// both clients name (AERE-PQ-V3-01..08, in the fixed order of PROIECT-1B 1.4), so a stranger's verdict can be compared with theirs.
const COMMIT_DOMAIN_2 = 'AERE-PQ-COMMIT-2';
const ANCHOR_DOMAIN_V3 = 'AERE-PQ-ANCHOR-3';
const MAX_ROUND = 1024; // PqAnchorV3.MAX_ROUND in both clients; no QBFT round timer gets there
const MAX_V3_ROWS = 64;
// H_r per chain: UNSET on both chains at the time of writing (no v3 anchor exists on any live chain). When the fleet publishes H_r
// for a chain it is written here, in the same hour, before H_r (D-372: the new verifier is published BEFORE the first v3 anchor).
// COMMIT_ROUND_BLOCK overrides (a height, or "none"); cloud-gateway/orar-ancore.mjs carries the same value for the consumers.
const COMMIT_ROUND_DEFAULT = CHAIN_ID === 28001 ? null : null;
const COMMIT_ROUND_BLOCK = process.env.COMMIT_ROUND_BLOCK === undefined ? COMMIT_ROUND_DEFAULT
: (/^(none|unset|)$/i.test(process.env.COMMIT_ROUND_BLOCK) ? null : Number(process.env.COMMIT_ROUND_BLOCK));
if (COMMIT_ROUND_BLOCK !== null && !(Number.isInteger(COMMIT_ROUND_BLOCK) && COMMIT_ROUND_BLOCK > 0)) { console.error('FAIL: COMMIT_ROUND_BLOCK must be a positive block height or "none"'); stop(1); }
// the form the certificate of anchor a must have (PROIECT-1B 2.1, certificateFormAt): v3 from H_r; below it the bytes decide (v1 or v2)
const certificateFormAt = (a) => (COMMIT_ROUND_BLOCK !== null && a >= COMMIT_ROUND_BLOCK ? 3 : null);
const POP_DOMAIN = 'AERE-PQ-POP-1'; const POP_DOMAIN = 'AERE-PQ-POP-1';
const CLAIM_DOMAIN = 'AERE-PQ-CLAIM-1'; const CLAIM_DOMAIN = 'AERE-PQ-CLAIM-1';
const FORMAT_VERSION = 2; const FORMAT_VERSION = 2;
@ -201,6 +230,13 @@ async function rpc(method, params) {
// ---- the three messages, exactly as the node computes them ------------------------------------- // ---- the three messages, exactly as the node computes them -------------------------------------
const commitMessage = (parentNumber, parentHash) => const commitMessage = (parentNumber, parentHash) =>
keccak(rlpEncode([Buffer.from(COMMIT_DOMAIN, 'ascii'), scalar(CHAIN_ID), scalar(parentNumber), parentHash])); keccak(rlpEncode([Buffer.from(COMMIT_DOMAIN, 'ascii'), scalar(CHAIN_ID), scalar(parentNumber), parentHash]));
// M_r(b) = keccak256(RLP["AERE-PQ-COMMIT-2", chainId, n, r, hash(b)]) (PROIECT-1B 1.1; the round as a minimal scalar, 0 = the empty string)
const commitMessageRound = (parentNumber, round, parentHash) =>
keccak(rlpEncode([Buffer.from(COMMIT_DOMAIN_2, 'ascii'), scalar(CHAIN_ID), scalar(parentNumber), scalar(round), parentHash]));
// a scalar read from its RLP bytes as an unsigned integer (the canonical-form check is the round trip of the whole certificate below)
const scalarOf = (b) => (b.length ? BigInt('0x' + Buffer.from(b).toString('hex')) : 0n);
// QBFT quorum of a set of n validators, as both clients compute it (BftHelpers.calculateRequiredValidatorQuorum: ceil(2n/3))
const quorumOf = (n) => Math.floor((2 * n + 2) / 3);
// PqRegistryBinding.bindingPreimage // PqRegistryBinding.bindingPreimage
function bindingPreimage(bindHeight, count, index, address, publicKey) { function bindingPreimage(bindHeight, count, index, address, publicKey) {
return Buffer.concat([Buffer.from(POP_DOMAIN, 'ascii'), Buffer.from([FORMAT_VERSION]), u64(CHAIN_ID), u64(bindHeight), return Buffer.concat([Buffer.from(POP_DOMAIN, 'ascii'), Buffer.from([FORMAT_VERSION]), u64(CHAIN_ID), u64(bindHeight),
@ -498,6 +534,7 @@ let lapseNotEnforced = 0;
let newestOk = null; let newestOk = null;
let newestParentHash = null; // the hash of block newestOk - 1 the verified seals signed (M(parent)), for ANCHOR_REPORT 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 let newestParent = null; // that parent's header as served, re-encoded above to the certified hash: its stateRoot is bound to the seals
let newestForm = null; // the form of that certificate: { version: 1|2|3, round (v3 only), members, setSize, quorum }, named in the last line
// ce scheme au fost CHIAR verificate: rezumatul final nu are voie sa numeasca o schema pe care nu a vazut-o // 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(); const schemesSeen = new Set();
let cursor = anchor; let cursor = anchor;
@ -528,7 +565,12 @@ for (let i = 0; i < COUNT; i++) {
+ ' is accepted as written; checked only that its ' + pairs.length + ' seal indices are distinct (' + (pairs.join(',') || 'no seals') + '). Not verified.'); + ' is accepted as written; checked only that its ' + pairs.length + ' seal indices are distinct (' + (pairs.join(',') || 'no seals') + '). Not verified.');
continue; continue;
} }
if (items.length < 6 || !items[5].list || items[5].item.length === 0) { console.log(h + ' FAIL: no certificate element'); bad++; continue; } if (items.length < 6 || !items[5].list || items[5].item.length === 0) {
// 1(b): at or above H_r the absence of a certificate is the named refusal 02 (PROIECT-1B 1.4: "at a >= H_r the anchor carries no v3")
if (certificateFormAt(h) === 3) console.log(h + ' FAIL: AERE-PQ-V3-02 (certificate v3): no certificate element; from H_r=' + COMMIT_ROUND_BLOCK + ' every anchor must carry a v3 certificate');
else console.log(h + ' FAIL: no certificate element');
bad++; continue;
}
const cert = items[5]; const cert = items[5];
const toNested = (node) => (node.list ? node.item.map(toNested) : Buffer.from(node.item)); const toNested = (node) => (node.list ? node.item.map(toNested) : Buffer.from(node.item));
// D-477: the parent's header is the source of the set the seals are judged against, so it must be readable, must re-encode // D-477: the parent's header is the source of the set the seals are judged against, so it must be readable, must re-encode
@ -550,6 +592,139 @@ for (let i = 0; i < COUNT; i++) {
continue; continue;
} }
if (!hex(parent.hash).equals(hex(b.parentHash))) { bad++; console.log(h + ' FAIL: the header served for block ' + (h - 1) + ' (hash ' + parent.hash + ') is not the parent this anchor names (' + b.parentHash + ')'); continue; } if (!hex(parent.hash).equals(hex(b.parentHash))) { bad++; console.log(h + ' FAIL: the header served for block ' + (h - 1) + ' (hash ' + parent.hash + ') is not the parent this anchor names (' + b.parentHash + ')'); continue; }
const man = manifestAt(h);
const vset = await validatorsAt(h - 1, pItems);
// ---- the certificate's form, from its FIRST element: a list is v1, the scalar 2 is v2, the scalar 3 is v3 (1(b)) ----
// v3 is judged in the block below, with the codes of PROIECT-1B 1.4; v1 and v2 are judged further down exactly as before 1(b).
const versionScalar = !cert.item[0].list && cert.item[0].item.length === 1 ? cert.item[0].item[0] : null;
const version = cert.item[0].list ? 1 : versionScalar === 2 ? 2 : versionScalar === 3 ? 3 : 0;
const judgeV3 = certificateFormAt(h) === 3 || version === 3;
// An unreadable parent set: under v3 it is the named refusal 06 ("the set of the parent could not be resolved"), given in R2 in
// the order of PROIECT-1B 1.4 (after R1 and after the count 05, at the first Falcon row, as PqAnchorV3.judgeSeals does); for
// v1 / v2 it is the plain FAIL it always was. (2026-10-07, review of step 8: until then a v3 anchor failed here without a code.)
if (!vset && !judgeV3) { bad++; console.log(h + ' FAIL: the validator list in element 1 of the header of block ' + (h - 1) + ' (the block these seals sign) is empty or malformed, so no seal can be bound to a validator'); continue; }
if (judgeV3) {
// The bytes of element 6 as carried (element 6 starts where element 5 ends): D3 is over these bytes, and the canonical round trip
// below requires them to be exactly the re-encoding of what was read, so no second RLP form of the same certificate is admitted.
const ex = hex(b.extraData);
const rawCert = ex.subarray(items[4].next, items[5].next);
const v3 = { code: null, why: null, round: null, rows: [] };
const refuse = (code, why) => { v3.code = code; v3.why = why; return false; };
// R1: the form by height (01, 02), the decoding (03), the digest (04)
const parseV3 = () => {
if (certificateFormAt(h) !== 3) return refuse('AERE-PQ-V3-01', 'a v3 certificate at anchor ' + h + ', below the round height H_r'
+ (COMMIT_ROUND_BLOCK === null ? ' (H_r is not set for chain ' + CHAIN_ID + ' in this tool, so no v3 anchor is expected on it; if the fleet has published H_r, pass COMMIT_ROUND_BLOCK=<H_r>)' : '=' + COMMIT_ROUND_BLOCK));
if (version === 1 || version === 2) return refuse('AERE-PQ-V3-02', 'from H_r=' + COMMIT_ROUND_BLOCK + ' the certificate must be v3 (RLP[3, round, rows]); this one is ' + (version === 1 ? 'v1 (it opens with a list)' : 'v2 (it opens with the scalar 2)'));
if (version !== 3) return refuse('AERE-PQ-V3-03', 'the certificate opens with an unreadable version scalar (0x' + Buffer.from(cert.item[0].item).toString('hex') + ')');
if (cert.item.length !== 3) return refuse('AERE-PQ-V3-03', 'a v3 certificate has three elements [3, round, rows]; this one has ' + cert.item.length);
const rNode = cert.item[1], rowsNode = cert.item[2];
if (rNode.list) return refuse('AERE-PQ-V3-03', 'the round is a list, not a scalar');
const rb = Buffer.from(rNode.item);
if (rb.length && rb[0] === 0) return refuse('AERE-PQ-V3-03', 'the round has a leading zero byte (not a canonical scalar)');
if (rb.length > 2 || scalarOf(rb) > BigInt(MAX_ROUND)) return refuse('AERE-PQ-V3-03', 'round ' + (rb.length > 8 ? '0x' + rb.toString('hex') : scalarOf(rb)) + ' is above ' + MAX_ROUND);
const round = Number(scalarOf(rb));
if (!rowsNode.list) return refuse('AERE-PQ-V3-03', 'the rows element is not a list');
if (rowsNode.item.length > MAX_V3_ROWS) return refuse('AERE-PQ-V3-03', rowsNode.item.length + ' rows, above ' + MAX_V3_ROWS);
if (rowsNode.item.length === 0 && round !== 0) return refuse('AERE-PQ-V3-03', 'an empty certificate must name round 0, this one names ' + round);
const rows = [];
for (const [k, s] of rowsNode.item.entries()) {
if (!s.list || s.item.length !== 3 || s.item.some(x => x.list)) return refuse('AERE-PQ-V3-03', 'row ' + k + ' is not [scheme, index, signature]');
const sb = Buffer.from(s.item[0].item), ib = Buffer.from(s.item[1].item), sig = new Uint8Array(s.item[2].item);
if ((sb.length && sb[0] === 0) || (ib.length && ib[0] === 0)) return refuse('AERE-PQ-V3-03', 'row ' + k + ': a scalar with a leading zero byte');
if (sb.length > 1 || !SCHEME_NAMES[Number(scalarOf(sb))]) return refuse('AERE-PQ-V3-03', 'row ' + k + ': unknown scheme tag 0x' + (sb.toString('hex') || '00'));
if (ib.length > 4) return refuse('AERE-PQ-V3-03', 'row ' + k + ': the index does not fit 32 bits');
if (sig.length === 0) return refuse('AERE-PQ-V3-03', 'row ' + k + ': empty signature');
const row = { scheme: Number(scalarOf(sb)), idx: Number(scalarOf(ib)), sig };
const prev = rows[rows.length - 1];
if (prev && !(row.idx > prev.idx || (row.idx === prev.idx && row.scheme > prev.scheme))) return refuse('AERE-PQ-V3-03', 'rows not strictly increasing by (index, scheme): ' + rows.concat(row).map(x => x.idx + '/' + x.scheme).join(','));
rows.push(row);
}
const canon = rlpEncode([scalar(3), scalar(round), rows.map(x => [scalar(x.scheme), scalar(x.idx), Buffer.from(x.sig)])]);
if (!canon.equals(rawCert)) return refuse('AERE-PQ-V3-03', 'the certificate is not in canonical RLP form (its re-encoding differs from the bytes carried)');
v3.round = round; v3.rows = rows;
return true;
};
if (parseV3()) {
const digestV3 = keccak(rlpEncode([Buffer.from(ANCHOR_DOMAIN_V3, 'ascii'), scalar(CHAIN_ID), scalar(h - 1), hex(parent.hash), rawCert]));
if (!digestV3.equals(vanity)) refuse('AERE-PQ-V3-04', 'the digest in vanityData is not keccak256(RLP["AERE-PQ-ANCHOR-3", chainId, ' + (h - 1) + ', parentHash, certificate]) of the certificate carried (round ' + v3.round + ')');
}
// R2: the Falcon rows against K (05); then, row by row in canonical order, the binding (06) and the signature (07), an extra also
// 08; at the end every required scheme (05). All or nothing: the first failure names the code. Every seal is over M_r of the
// PARENT for the ONE round the certificate names, with the keys in force at the parent (the v3 epoch from KEYS_FROM, else the
// key manifest); the validator of a row must be in the set of the PARENT (D-477) and counts once per scheme.
const MIN_SEALS = minSealsAt(h);
const msgR = v3.round === null ? null : commitMessageRound(h - 1, v3.round, hex(parent.hash));
const v3k = h - 1 >= KEYS_FROM ? v3EpochAt(h - 1) : null; // v3: the epoch in force at the parent
const falconRows = v3.rows.filter(x => x.scheme === 1);
const falconIdx3 = new Set(falconRows.map(x => x.idx));
const requiredSchemes = MIN_SEALS > 0 ? schemesRequiredForAnchor(h) : [];
const namedSchemes = schemesAt(h - 1);
const perScheme = {}, taken = {};
const slhMissing = []; // rows of a scheme this machine cannot verify (no @noble/post-quantum): a gap of this run, never a refusal
if (!v3.code && falconRows.length < MIN_SEALS) refuse('AERE-PQ-V3-05', 'only ' + falconRows.length + ' Falcon-512 row(s) at K=' + MIN_SEALS + (falconRows.length === 0 ? ' (an empty certificate is valid only where K is 0)' : ''));
const keySource = v3k ? 'the registry v3 epoch ' + v3k.bindHeight : 'the key manifest ' + (man ? man.bindHeight : '?');
// The two doors to the parent (its set, its keys) are opened at the FIRST row, after the count 05, as PqAnchorV3.judgeSeals
// does: an empty certificate at K=0 is valid without either, and a certificate short of K is refused 05 before 06
// (2026-10-07, review of step 8: until then "no manifest" was refused 06 before 05, and on the empty certificate too).
const keysBound = v3k || (man && man.good);
for (const row of (v3.code ? [] : v3.rows)) {
const name = SCHEME_NAMES[row.scheme];
sealsTotal++; schemesSeen.add(row.scheme);
if (!vset) { refuse('AERE-PQ-V3-06', 'the validator set of the parent ' + (h - 1) + ' could not be resolved: the validator list in element 1 of its header is empty or malformed, so no seal can be bound to a validator'); break; }
if (!keysBound) { refuse('AERE-PQ-V3-06', man ? 'the key manifest in force at block ' + (h - 1) + ' (bindHeight ' + man.bindHeight + ') is not self-consistent, so no index can be bound to a key' : 'no key manifest is in force at block ' + (h - 1)); break; }
const src = v3k ? v3k.byIndex[row.idx] : man.rows[row.idx];
if (row.scheme === 1) {
if (!src) { refuse('AERE-PQ-V3-06', 'index ' + row.idx + ' names no row of ' + keySource + ' (in force at block ' + (h - 1) + ')'); break; }
const member = v3k ? src.id : memberOf(src.addr.toLowerCase(), h - 1);
if (!vset.has(member)) { refuse('AERE-PQ-V3-06', 'the Falcon-512 seal of index ' + row.idx + ' is bound to ' + member + ', which is not in the validator set of block ' + (h - 1) + ', the block the seals sign'); break; }
const been = taken[name] || (taken[name] = new Set());
if (been.has(member)) { refuse('AERE-PQ-V3-06', 'two indices of this certificate are bound to the same validator ' + member + ' (the second is index ' + row.idx + ')'); break; }
been.add(member);
if (!src.pk || !verifyFalcon512(src.pk, msgR, row.sig)) { refuse('AERE-PQ-V3-07', 'the Falcon-512 seal of index ' + row.idx + ' does not verify over M_r(' + (h - 1) + ') for round ' + v3.round + ' (AERE-PQ-COMMIT-2) with the key of ' + keySource); break; }
} else {
if (!namedSchemes.includes(name)) { refuse('AERE-PQ-V3-08', 'a ' + name + ' seal at index ' + row.idx + ', a scheme the schedule does not name at block ' + (h - 1)); break; }
if (!falconIdx3.has(row.idx)) { refuse('AERE-PQ-V3-08', 'the ' + name + ' seal of index ' + row.idx + ' rides without a Falcon-512 seal of the same index'); break; }
if (!src) { refuse('AERE-PQ-V3-06', 'index ' + row.idx + ' names no row of ' + keySource); break; }
const pk = src.keys && src.keys[name];
if (!pk) { refuse('AERE-PQ-V3-06', keySource + ' has no ' + name + ' key for index ' + row.idx); break; }
if (!slhDsa) { slhMissing.push(name + '@' + row.idx); continue; }
if (!verifySlhDsa(pk, msgR, row.sig)) { refuse('AERE-PQ-V3-07', 'the ' + name + ' seal of index ' + row.idx + ' does not verify over M_r(' + (h - 1) + ') for round ' + v3.round + ' (AERE-PQ-COMMIT-2)'); break; }
}
perScheme[name] = (perScheme[name] || 0) + 1; sealsValid++;
}
if (!v3.code) for (const nm of requiredSchemes) {
if (nm !== 'falcon-512' && !slhDsa) continue; // reported below as NOT VERIFIED HERE, not as a finding
if ((perScheme[nm] || 0) < MIN_SEALS) { refuse('AERE-PQ-V3-05', 'scheme ' + nm + ' has ' + (perScheme[nm] || 0) + ' valid seal(s), below K=' + MIN_SEALS + ' required at anchor ' + h + ' (step in force at ' + (h - 1) + ')'); break; }
}
const n3 = v3.rows.length, members = perScheme['falcon-512'] || 0, setSize = vset ? vset.size : 0, Q = setSize ? quorumOf(setSize) : null;
const summary3 = Object.entries(perScheme).map(([nm, c]) => c + ' ' + nm).join(' + ') || 'no seals';
if (!v3.code && slhMissing.length) {
unmeasured++;
console.log(h + ' NOT VERIFIED HERE: v3 certificate (round ' + v3.round + ') bound under block hash, ' + members + ' valid falcon-512 seals, but this'
+ ' machine has no SLH-DSA verifier, so the ' + slhMissing.length + ' slh-dsa seals were not checked (npm install @noble/post-quantum). This is a gap in THIS run, not a finding about the chain.');
continue;
}
if (!v3.code) {
ok++;
// PROIECT-1B 4.2 #1: "PARENT DECIDED IN ROUND r" only when at least the QBFT quorum of the PARENT's set sealed it in that one
// round (every seal verified above, over M_r, each member counted once), which is what QBFT calls a decision; below the quorum
// the line says CERTIFIED (participation). The machine-readable form is fixed (the collector of the live gate D-470,
// consensus-pqc/d412-reparatie/r1b/aduna-fapte-vii-2800.mjs, parses exactly it): "PARENT DECIDED IN ROUND r (m members of n,
// quorum Q)". It is still not a finality claim: finality is the chain's rule, not a measurement of this tool.
const decided = n3 > 0 && Q !== null && members >= Q;
if (newestOk === null) { newestOk = h; newestParentHash = String(parent.hash).toLowerCase(); newestParent = parent; newestForm = { version: 3, round: v3.round, members, setSize, quorum: Q, decided }; }
console.log(h + ' OK: bound under block hash (v3, round ' + v3.round + ', AERE-PQ-ANCHOR-3), ' + n3 + ' seals (' + summary3 + ')'
+ (n3 ? ', all valid over M_r of block ' + (h - 1) + ' for round ' + v3.round + ' and from distinct validators of that block, the set its own header lists (' + keySource + '); '
+ (decided
? 'PARENT DECIDED IN ROUND ' + v3.round + ' (' + members + ' members of ' + setSize + ', quorum ' + Q + '): a QBFT quorum of that set sealed block ' + (h - 1) + ' in that one round'
: 'CERTIFIED (participation): ' + members + ' members of ' + setSize + ' sealed it in round ' + v3.round + ', below the QBFT quorum ' + Q + ' of that set, so this does not show that the block was decided')
: ': the empty certificate, which is valid only where K is 0 (K=' + MIN_SEALS + ' here) and proves nothing about block ' + (h - 1)));
continue;
}
bad++;
console.log(h + ' FAIL: ' + v3.code + ' (certificate v3' + (v3.round !== null ? ', round ' + v3.round : '') + '): ' + v3.why);
continue;
}
// v1 opens with a LIST ([idx, sig]); v2 opens with the SCALAR 2. Nothing else is accepted. // v1 opens with a LIST ([idx, sig]); v2 opens with the SCALAR 2. Nothing else is accepted.
const isV2 = cert.list && cert.item.length === 2 && !cert.item[0].list; const isV2 = cert.list && cert.item.length === 2 && !cert.item[0].list;
if (isV2 && !(cert.item[0].item.length === 1 && cert.item[0].item[0] === 2)) { console.log(h + ' FAIL: certificate opens with an unknown version scalar'); bad++; continue; } if (isV2 && !(cert.item[0].item.length === 1 && cert.item[0].item[0] === 2)) { console.log(h + ' FAIL: certificate opens with an unknown version scalar'); bad++; continue; }
@ -558,10 +733,7 @@ for (let i = 0; i < COUNT; i++) {
: keccak(rlpEncode([Buffer.from(ANCHOR_DOMAIN, 'ascii'), scalar(CHAIN_ID), scalar(h - 1), hex(parent.hash), toNested(cert)])); : keccak(rlpEncode([Buffer.from(ANCHOR_DOMAIN, 'ascii'), scalar(CHAIN_ID), scalar(h - 1), hex(parent.hash), toNested(cert)]));
const bound = digest.equals(vanity); const bound = digest.equals(vanity);
const man = manifestAt(h);
const msg = commitMessage(h - 1, hex(parent.hash)); const msg = commitMessage(h - 1, hex(parent.hash));
const vset = await validatorsAt(h - 1, pItems);
if (!vset) { bad++; console.log(h + ' FAIL: the validator list in element 1 of the header of block ' + (h - 1) + ' (the block these seals sign) is empty or malformed, so no seal can be bound to a validator'); continue; }
let valid = 0, invalid = [], notValidator = [], unknownIndex = [], unknownScheme = [], unbound = [], noKey = [], duplicate = []; let valid = 0, invalid = [], notValidator = [], unknownIndex = [], unknownScheme = [], unbound = [], noKey = [], duplicate = [];
const perScheme = {}; // scheme name -> valid seals, each from a DISTINCT validator const perScheme = {}; // scheme name -> valid seals, each from a DISTINCT validator
const countedBy = {}; // scheme name -> the validators already counted (D-477: a validator counts once per scheme, as in both clients) const countedBy = {}; // scheme name -> the validators already counted (D-477: a validator counts once per scheme, as in both clients)
@ -633,7 +805,7 @@ for (let i = 0; i < COUNT; i++) {
&& unknownScheme.length === 0 && unknownIndex.length === 0 && unknownScheme.length === 0 && unknownIndex.length === 0
&& notValidator.length === 0 && noKey.length === 0 && unbound.length === 0 && duplicate.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(' + '); const summary = Object.entries(perScheme).map(([nm, c]) => c + ' ' + nm).join(' + ');
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) + ')' if (good) { ok++; if (newestOk === null) { newestOk = h; newestParentHash = String(parent.hash).toLowerCase(); newestParent = parent; newestForm = { version: isV2 ? 2 : 1, round: null }; } 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' : '')); } + (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) { else if (envBlocked) {
unmeasured++; unmeasured++;
@ -877,9 +1049,18 @@ console.log(nothingVerified
// 2026-10-01: the tail of this line said, on every chain, that a header between anchors carries only its proposer's seal. That is true only // 2026-10-01: the tail of this line said, on every chain, that a header between anchors carries only its proposer's seal. That is true only
// where AIP-22 stage 5 is active (testnet 28001 from block 3,131,000); on chain 2800 headers between anchors still carry the validators' // where AIP-22 stage 5 is active (testnet 28001 from block 3,131,000); on chain 2800 headers between anchors still carry the validators'
// classical ECDSA commit seals, and this tool printed the stage-5 sentence there on every run. It now says what it measured above. // classical ECDSA commit seals, and this tool printed the stage-5 sentence there on every run. It now says what it measured above.
if (newestOk !== null) console.log('POST-QUANTUM CERTIFIED: block ' + (newestOk - 1) + ', the parent of anchor ' + newestOk + ', the most recent anchor whose certificate verified here:' // 2026-10-07 (1(b)): the line names the FORM of the newest verified certificate. For v1 / v2 it says what it said: the seals carry no
+ ' the validators whose seals it carries sealed that block post-quantum. The seals carry no consensus round, so this does not by itself prove' // round. For v3 it says what was verified: the seals are bound to one consensus round, and how many distinct members of the parent's
+ ' that the block was decided (AIP-22 erratum, 2026-10-01). Between anchors no block carries a post-quantum certificate; ' // set sealed it in that round against that set's QBFT quorum. It still certifies what it measured and pronounces no finality.
if (newestOk !== null) console.log('POST-QUANTUM CERTIFIED: block ' + (newestOk - 1) + ', the parent of anchor ' + newestOk + ', the most recent anchor whose certificate verified here'
+ (newestForm && newestForm.version === 3 ? ' (certificate v3, round ' + newestForm.round + '):' : ':')
+ ' the validators whose seals it carries sealed that block post-quantum. '
+ (newestForm && newestForm.version === 3
? 'Every seal is bound to consensus round ' + newestForm.round + ' (AERE-PQ-COMMIT-2), and ' + newestForm.members + ' of the ' + newestForm.setSize + ' validators of that block sealed it in that round'
+ ' (QBFT quorum of that set: ' + newestForm.quorum + ')' + (newestForm.decided ? ': PARENT DECIDED IN ROUND ' + newestForm.round + '.' : ': below the quorum, so not shown decided.')
+ ' This tool certifies what it verified; it pronounces no finality.'
: 'The seals carry no consensus round, so this does not by itself prove that the block was decided (AIP-22 erratum, 2026-10-01).')
+ ' Between anchors no block carries a post-quantum certificate; '
+ (propChecked === 0 + (propChecked === 0
? 'a header between anchors carries the validators\' classical ECDSA commit seals (AIP-22 stage 5 is not active at these heights on this chain).' ? 'a header between anchors carries the validators\' classical ECDSA commit seals (AIP-22 stage 5 is not active at these heights on this chain).'
: 'a header between anchors carries only its proposer\'s seal.')); : 'a header between anchors carries only its proposer\'s seal.'));
@ -889,8 +1070,11 @@ if (!slhDsa) console.log('note: @noble/post-quantum is not installed, so a v2 ce
// re-encoding the header, without a second copy of the certificate check. Written whatever the verdict; a caller also needs exit code 0. // re-encoding the header, without a second copy of the certificate check. Written whatever the verdict; a caller also needs exit code 0.
if (process.env.ANCHOR_REPORT) { if (process.env.ANCHOR_REPORT) {
try { try {
// 1(b): `certificate` names the form of the newest verified certificate (v1 / v2 / v3 with its round, and for v3 the verified
// members of the parent's set, the set size, its quorum and `decided` = members >= quorum); the keys before it are unchanged
fs.writeFileSync(process.env.ANCHOR_REPORT, JSON.stringify({ chainId: CHAIN_ID, anchor: newestOk, parentNumber: newestOk === null ? null : newestOk - 1, fs.writeFileSync(process.env.ANCHOR_REPORT, JSON.stringify({ chainId: CHAIN_ID, anchor: newestOk, parentNumber: newestOk === null ? null : newestOk - 1,
parentHash: newestParentHash }) + '\n'); parentHash: newestParentHash, certificate: newestForm === null ? null : { version: newestForm.version, round: newestForm.round,
...(newestForm.version === 3 ? { members: newestForm.members, setSize: newestForm.setSize, quorum: newestForm.quorum, decided: newestForm.decided } : {}) } }) + '\n');
} catch (e) { console.error('note: ANCHOR_REPORT could not be written: ' + e.message); } } catch (e) { console.error('note: ANCHOR_REPORT could not be written: ' + e.message); }
} }
// three states, three exit codes: 0 verified, 1 the chain failed a check, 2 this machine could not measure. // three states, three exit codes: 0 verified, 1 the chain failed a check, 2 this machine could not measure.