verify-anchor.mjs: per-scheme anchor intervals (AIP-22 stage 2: SCHEME_SCHEDULE entries like falcon-512/32+slh-dsa-sha2-128s/128; a scheme off its own grid at an anchor is surplus, verified but not required) and post-quantum validator identity (AIP-22 stage 3: IDENTITY_BLOCK + REGISTRY_V3 translate the validator set to ids from H_id); testnet 28001 default gains the stage-2 step 3070656. Proven 2026-09-23 12:07Z on 28001: 11 of 11 anchors after 3,070,656 accepted (3 hybrid on the 128 grid, 8 Falcon-only at 32), and the pre-stage-2 schedule refuses exactly the 8 Falcon-only; 20 of 20 on chain 2800 unchanged.

This commit is contained in:
Aere Network 2026-09-23 15:08:45 +03:00
parent 5c99d51a3c
commit 79e5dafd53

View File

@ -78,10 +78,24 @@ if (!K_SCHEDULE.length) K_SCHEDULE.push(...K_DEFAULT);
// D-338 (2026-09-04): the SCHEMES the certificate must carry, by height of the PARENT (the seals are made when the
// parent is committed), exactly as both clients read them. Without this the tool verified only the seals it found,
// so a certificate that dropped a whole scheme passed. SCHEME_SCHEDULE ("h:scheme+scheme,...") overrides.
const SCHEME_DEFAULT = CHAIN_ID === 28001 ? '168000:falcon-512+slh-dsa-sha2-128s' : '17047568:falcon-512+slh-dsa-sha2-128s';
// 28001, 2026-09-23: AIP-22 stage 2 rehearsal from 3,070,656 - Falcon-512 at every anchor (every 32nd block again),
// SLH-DSA only at every 128th; chain 2800 unchanged.
const SCHEME_DEFAULT = CHAIN_ID === 28001 ? '168000:falcon-512+slh-dsa-sha2-128s,3070656:falcon-512/32+slh-dsa-sha2-128s/128' : '17047568:falcon-512+slh-dsa-sha2-128s';
// AIP-22 stage 2 (2026-09-23): a scheme may carry its own interval, "h:falcon-512/32+slh-dsa-sha2-128s/128". Such a scheme
// is REQUIRED only at anchors on its own grid ((anchor - ANCHOR_FROM) % interval == 0); at the other anchors it is still
// NAMED, so its seals, when present, are verified but need not reach K (surplus), exactly as both clients judge it. A scheme
// without "/interval" is required at every anchor (the rule before stage 2). Intervals are positive multiples of 32.
const SCHEME_SCHEDULE = (process.env.SCHEME_SCHEDULE || SCHEME_DEFAULT).split(',').filter(Boolean)
.map(p => { const [h, list] = p.split(':'); return [Number(h), list.split('+').map(x => x.trim()).filter(Boolean)]; }).sort((a, b) => a[0] - b[0]);
const schemesAt = (hh) => { let out = ['falcon-512']; for (const [from, list] of SCHEME_SCHEDULE) { if (from <= hh) out = list; else break; } return out; };
.map(p => { const [h, list] = p.split(':'); return [Number(h), list.split('+').map(x => x.trim()).filter(Boolean).map(e => { const [name, iv] = e.split('/'); return { name, interval: iv === undefined ? 0 : Number(iv) }; })]; })
.sort((a, b) => a[0] - b[0]);
for (const [h, list] of SCHEME_SCHEDULE) for (const { name, interval } of list) {
if (!(interval === 0 || (Number.isInteger(interval) && interval > 0 && interval % 32 === 0))) { console.error('FAIL: SCHEME_SCHEDULE step ' + h + ' gives ' + name + ' the interval ' + interval + ', which is not a positive multiple of 32'); process.exit(1); }
}
const stepAt = (hh) => { let out = [{ name: 'falcon-512', interval: 0 }]; for (const [from, list] of SCHEME_SCHEDULE) { if (from <= hh) out = list; else break; } return out; };
const schemesAt = (hh) => stepAt(hh).map(s => s.name);
// the schemes an anchor at height a must carry at K: the step in force at the PARENT (seals are made on the parent's commit),
// filtered by each scheme's own grid; the grid is counted from ANCHOR_FROM, the same anchor block both clients use
const schemesRequiredForAnchor = (a) => stepAt(a - 1).filter(s => s.interval === 0 || (a - ANCHOR_FROM) % s.interval === 0).map(s => s.name);
const minSealsAt = (hh) => Number(process.env.MIN_SEALS || K_SCHEDULE.filter(([h]) => hh >= h).map(([, k]) => k).pop());
const COUNT = Number(process.env.ANCHORS || 20);
@ -294,9 +308,26 @@ if (anchor < ANCHOR_FROM) { console.error('FAIL: no anchor at or below ' + top +
if (vanity.length !== 32 || Buffer.from(vanity).includes(Buffer.from('besu'))) { console.error('FAIL: block ' + anchor + ' does not look like an anchor (vanity is the client string, not a digest)'); process.exit(1); }
}
// AIP-22 stage 3 (2026-09-23): from IDENTITY_BLOCK a chain's validator set holds post-quantum ids, not ECDSA addresses, while
// the Falcon/hybrid manifests above still name each index by its classical address (the first stage keeps the keys where they
// are and adds the identity). REGISTRY_V3 (a v3 epoch manifest, file or URL) supplies the link: the row whose ecdsa.addr is the
// manifest's address gives the id that must be in the set. This tool only READS the link; the v3 manifest's own proofs are
// verified by verifica-epoca-v3.mjs (the independent JS implementation of the registry). Without both variables nothing changes.
const IDENTITY_BLOCK = process.env.IDENTITY_BLOCK ? Number(process.env.IDENTITY_BLOCK) : Infinity;
let V3_LINK = null;
if (process.env.REGISTRY_V3 && Number.isFinite(IDENTITY_BLOCK)) {
const v3 = JSON.parse(await loadText(process.env.REGISTRY_V3));
if (v3.formatVersion !== 3 || v3.chainId !== CHAIN_ID) { console.error('FAIL: REGISTRY_V3 is not a v3 epoch of chain ' + CHAIN_ID); process.exit(1); }
V3_LINK = new Map();
for (let i = 0; i < v3.count; i++) { const r = v3[String(i)]; if (r && r.ecdsa) V3_LINK.set(r.ecdsa.addr.toLowerCase(), r.id.toLowerCase()); }
console.log('identity: post-quantum ids from block ' + IDENTITY_BLOCK + ', ' + V3_LINK.size + ' ecdsa links from the v3 epoch bound at ' + v3.bindHeight);
}
const memberOf = (addr, h) => (V3_LINK && h >= IDENTITY_BLOCK ? (V3_LINK.get(addr) ?? addr) : addr);
const validatorsCache = new Map();
async function validatorsAt(h) {
const key = Math.floor(h / 1000);
// the identity switch changes the form of the set; one cache window must never hold both forms
const key = Math.floor(h / 1000) + (h >= IDENTITY_BLOCK ? ':id' : ':addr');
if (!validatorsCache.has(key)) {
const v = await rpc('qbft_getValidatorsByBlockNumber', ['0x' + h.toString(16)]);
validatorsCache.set(key, new Set((v || []).map(a => a.toLowerCase())));
@ -350,7 +381,7 @@ for (let i = 0; i < COUNT; i++) {
if (!name) { unknownScheme.push(scheme); continue; }
const row = man && man.rows[idx];
if (!row) { unknownIndex.push(idx); continue; }
if (!vset.has(row.addr.toLowerCase())) { notValidator.push(idx); continue; }
if (!vset.has(memberOf(row.addr.toLowerCase(), h))) { notValidator.push(idx); continue; }
if (scheme === 1) {
if (!verifyFalcon512(row.pk, msg, sig)) { invalid.push('falcon@' + idx); continue; }
} else {
@ -365,12 +396,19 @@ for (let i = 0; i < COUNT; i++) {
const n = rowsOf.length;
const MIN_SEALS = minSealsAt(h);
const falconValid = perScheme['falcon-512'] || 0;
const shortSchemes = Object.entries(perScheme).filter(([, c]) => c < MIN_SEALS).map(([nm, c]) => nm + ':' + c);
// D-338: every scheme the schedule names at the parent's height must reach K on its own; a scheme with NO seal at
// all is named as missing, not silently skipped. When K is 0 at this height (a dated step) nothing is required.
const requiredSchemes = MIN_SEALS > 0 ? schemesAt(h - 1) : [];
// D-338: every scheme the schedule REQUIRES at this anchor must reach K on its own; a scheme with NO seal at all is named
// as missing, not silently skipped. When K is 0 at this height (a dated step) nothing is required. AIP-22 stage 2: a
// scheme that is NAMED at the parent's step but not required at this anchor (off its own grid) is SURPLUS - its seals are
// verified above like any other, but it need not reach K; a scheme present in the certificate that the step does not
// name at all is refused, as both clients refuse it.
const requiredSchemes = MIN_SEALS > 0 ? schemesRequiredForAnchor(h) : [];
const namedSchemes = schemesAt(h - 1);
const shortSchemes = requiredSchemes.filter(nm => (perScheme[nm] || 0) > 0 && perScheme[nm] < MIN_SEALS).map(nm => nm + ':' + perScheme[nm]);
const missingSchemes = requiredSchemes.filter(nm => !(perScheme[nm] > 0));
const schemesOk = falconValid >= MIN_SEALS && shortSchemes.length === 0 && missingSchemes.length === 0 && (!isV2 || slhDsa || !rowsOf.some(r => r.scheme === 2));
const unnamedSchemes = isV2 ? [...new Set(rowsOf.map(r => SCHEME_NAMES[r.scheme]).filter(nm => nm && !namedSchemes.includes(nm)))] : [];
const falconNeeded = requiredSchemes.includes('falcon-512') || requiredSchemes.length === 0;
const schemesOk = (!falconNeeded || falconValid >= MIN_SEALS) && shortSchemes.length === 0 && missingSchemes.length === 0
&& unnamedSchemes.length === 0 && (!isV2 || slhDsa || !rowsOf.some(r => r.scheme === 2));
const good = bound && man && man.good && schemesOk && valid === n;
// blocat DE MEDIU: tot ce se putea verifica aici e in regula, si singurul lucru care lipseste e verificatorul
// SLH-DSA. Orice alt defect (digest, manifest, sigiliu invalid, prag) il scoate din categoria asta.
@ -395,7 +433,8 @@ for (let i = 0; i < COUNT; i++) {
else {
bad++;
console.log(h + ' FAIL:' + (bound ? '' : ' digest mismatch') + (man ? (man.good ? '' : ' manifest not self-consistent') : ' no manifest for this height') +
(falconValid < MIN_SEALS ? ' only ' + falconValid + ' valid Falcon seals (K=' + MIN_SEALS + ')' : '') + (shortSchemes.length ? ' short schemes ' + shortSchemes.join(',') : '') + (missingSchemes.length ? ' missing scheme ' + missingSchemes.join(',') + ' (required at ' + (h - 1) + ')' : '') +
(falconNeeded && falconValid < MIN_SEALS ? ' only ' + falconValid + ' valid Falcon seals (K=' + MIN_SEALS + ')' : '') + (shortSchemes.length ? ' short schemes ' + shortSchemes.join(',') : '') + (missingSchemes.length ? ' missing scheme ' + missingSchemes.join(',') + ' (required at anchor ' + h + ', step in force at ' + (h - 1) + ')' : '') +
(unnamedSchemes.length ? ' scheme(s) not named by the schedule at ' + (h - 1) + ': ' + unnamedSchemes.join(',') : '') +
(invalid.length ? ' invalid seals ' + invalid.join(',') : '') + (unbound.length ? ' extra seals without a Falcon seal of the same index ' + unbound.join(',') : '') +
(noKey.length ? ' manifest has no key for ' + noKey.join(',') : '') + (unknownScheme.length ? ' unknown scheme tag ' + unknownScheme.join(',') : '') +
(notValidator.length ? ' keys not in validator set at index ' + notValidator.join(',') : '') + (unknownIndex.length ? ' unknown index ' + unknownIndex.join(',') : '') +