tools/verify-anchor.mjs: a certificate in v3 form below the commit-round height is refused with AERE-PQ-V3-01 before any decoding (form on height first, as both clients do); an index wider than 31 bits and an element 6 that is not a list are named refusals; v1/v2 verdicts unchanged (20 anchors on chain 2800 and 20 on 28001, identical rows)

This commit is contained in:
Aere Network 2026-10-10 23:38:49 +03:00
parent 9fe4dac73e
commit 8de1ab2184

View File

@ -99,6 +99,10 @@ const COMMIT_DOMAIN = 'AERE-PQ-COMMIT-1';
// 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.
// 2026-10-09: three margins of the v3 rule, each with its note where it applies: R2 in two passes (every Falcon row, then every extra
// row), a row index above 2^31-1 refused 03 at the decoding, and an element 6 that is present but is not a list (03 from H_r).
// They follow the fixed order of the codes named above (R1: the form by height 01 / 02, then the decoding 03, then the digest 04; R2: the
// Falcon rows, then the extra rows). This tool runs neither client: how a client binary answers these margins is not measured here.
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
@ -565,7 +569,16 @@ 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.');
continue;
}
if (items.length < 6 || !items[5].list || items[5].item.length === 0) {
// 2026-10-09: an element 6 that is PRESENT but is not an RLP list (a byte string, or the empty string 0x80) is not "no certificate":
// it is a certificate slot that does not decode, so from H_r this tool refuses it with 03 (the decoding), not 02. Until this date it
// was named 02 ("no certificate element"). Below H_r this tool names no v3 code for it: it stays a FAIL without a code, named for what
// it is. Which code, if any, a client binary names for it is not measured here.
if (items.length >= 6 && !items[5].list) {
if (certificateFormAt(h) === 3) console.log(h + ' FAIL: AERE-PQ-V3-03 (certificate v3): element 6 of extraData is present but is not an RLP list (' + items[5].item.length + ' byte string), so it does not decode as a certificate');
else console.log(h + ' FAIL: element 6 of extraData (the certificate) is present but is not an RLP list (' + items[5].item.length + ' byte string)');
bad++; continue;
}
if (items.length < 6 || 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');
@ -610,7 +623,14 @@ for (let i = 0; i < COUNT; i++) {
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)
// R1: the form by height (01: a v3 below H_r; 02: a v1 / v2 at or above H_r), then the decoding (03), then the digest (04). The form
// by height comes first: a certificate whose first element is the version scalar 3 at an anchor below H_r is 01 whatever the rest of
// its bytes, malformed or not.
// 2026-10-10: this is the order the specification fixes (the first failure returns its code; the form by height before the
// decoding), and both clients now give 01 here too, measured on the shared test vectors ([3, rows] and [3, [], rows] below H_r). For
// a few days an unpublished working copy of this tool gave the decoding first (03); every published copy kept this order. Not the same
// as the clients: this tool reads RLP leniently, so a version scalar written in a non-canonical form (0x8103 for 3) counts as 3 here,
// while both clients' strict readers refuse that framing first and give no code. A header the chain carries never has that form.
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));
@ -632,7 +652,11 @@ for (let i = 0; i < COUNT; i++) {
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'));
// 2026-10-09: a seal index is an integer 0..2^31-1 (a signed 32-bit integer) in at most 4 bytes, and anything else is refused
// with 03 at the decoding. Until this date only "at most 4 bytes" was required, so 0x80000000..0xffffffff passed the form and were
// refused only later, at the binding (06) or at the digest (04).
if (ib.length > 4) return refuse('AERE-PQ-V3-03', 'row ' + k + ': the index does not fit 32 bits');
if (scalarOf(ib) > 0x7fffffffn) return refuse('AERE-PQ-V3-03', 'row ' + k + ': the index ' + scalarOf(ib) + ' is above 2^31-1, the largest seal index (a signed 32-bit integer)');
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];
@ -648,10 +672,16 @@ for (let i = 0; i < COUNT; i++) {
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.
// R2: the Falcon rows against K (05); then EVERY FALCON ROW in canonical order, the binding (06) and the signature (07); then
// every extra row in canonical order, the scheme (08), its Falcon companion (08), its key (06) and its signature (07); 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.
// 2026-10-09: TWO PASSES, as the fixed order R2 says (every Falcon row, then every extra row). Until this date there was one pass over
// the rows in wire order (index, scheme): a certificate with two defects, a bad extra row at a LOW index and a bad or non-member Falcon
// seal at a HIGH index, was 08 here instead of 07 or 06. Same verdict, different code. The doors to the parent (its set, its keys)
// are opened at the first FALCON row, on the Falcon branch only; on the extra branch they were already opened by the Falcon pass (an
// extra row without a Falcon row of the same index is 08 first).
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
@ -663,17 +693,20 @@ for (let i = 0; i < COUNT; i++) {
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).
// The two doors to the parent (its set, its keys) are opened at the FIRST Falcon row (until 2026-10-09: at the first row of any
// scheme), 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)) {
// the order of R2: every Falcon row (canonical order), then every extra row (canonical order)
const ordineR2 = v3.code ? [] : [...falconRows, ...v3.rows.filter(x => x.scheme !== 1)];
for (const row of ordineR2) {
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 (!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 (!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; }
@ -684,6 +717,9 @@ for (let i = 0; i < COUNT; i++) {
} 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; }
// the doors were opened by the Falcon pass (this row has a Falcon companion that passed them); kept as a guard
if (!keysBound) { refuse('AERE-PQ-V3-06', 'no key set is in force at block ' + (h - 1) + ' for the ' + name + ' seal of index ' + row.idx); break; }
const src = v3k ? v3k.byIndex[row.idx] : man.rows[row.idx];
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; }