From 725aeebb658fa92348b48519dcfa9ee851b90555 Mon Sep 17 00:00:00 2001 From: Aere Network Date: Thu, 1 Oct 2026 20:28:51 +0300 Subject: [PATCH] Anchor verifier: the two dated historical lapse windows of chain 2800 transcribed from the production client (not enforced at all, then a threshold of one), the fleet's threshold before 13,034,000, and index order required as in production --- tools/verify-anchor.mjs | 109 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 10 deletions(-) diff --git a/tools/verify-anchor.mjs b/tools/verify-anchor.mjs index c943dc9..fe5ad1f 100644 --- a/tools/verify-anchor.mjs +++ b/tools/verify-anchor.mjs @@ -95,7 +95,11 @@ const K6_HEIGHT = 14961456; // that did not know the step would call that anchor wrong; one that hides it would be lying. MIN_SEALS_SCHEDULE // ("h:k,h:k") overrides for experiments and for other chains. // 28001, 2026-09-22: step 2988864:4, the per-scheme minimum becomes the QBFT quorum (4 of 5), AIP-22 stage 1 rehearsal. -const K_DEFAULT = CHAIN_ID === 28001 ? [[64, 0], [128, 3], [168032, 0], [168064, 3], [2988864, 4]] : [[0, 3], [K6_HEIGHT, 6], [17102384, 0], [17102416, 6], [20715632, 7]]; +// 2800, corrected 2026-10-01 (D-478): the schedule is the fleet's own, transcribed from the published follower configuration +// (RUN-A-NODE.md and SPEC.md: -Daere.pq.anchorMinSeals=13014000:0,13034000:3,14961456:6,17102384:0,17102416:6,20715632:7). Until this +// date the tool asked for 3 from the first anchor, so the warm-up anchors 13,014,000 .. 13,033,999, where the fleet required no seal +// (K=0; some carry two), came out FAIL: a false accusation of the same family as the two dated windows below. +const K_DEFAULT = CHAIN_ID === 28001 ? [[64, 0], [128, 3], [168032, 0], [168064, 3], [2988864, 4]] : [[13014000, 0], [13034000, 3], [K6_HEIGHT, 6], [17102384, 0], [17102416, 6], [20715632, 7]]; const K_SCHEDULE = (process.env.MIN_SEALS_SCHEDULE || '').split(',').filter(Boolean).map(p => p.split(':').map(Number)).sort((a, b) => a[0] - b[0]); 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 @@ -119,7 +123,44 @@ 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()); +// D-478 (2026-10-01): the two DATED historical windows of chain 2800 in which the anchor rules were not (fully) in force, as the +// production client names them. TRANSCRIBED, not deduced, from the Besu-derived client's PqAnchorLapse (published in the node package: +// anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java, the WINDOWS list at lines +// 235-259, CHAIN_ID at :82, the spacing at :85, isDisarmed at :333, historicMinSeals at :357; the index rules it relaxes are +// PqAnchor.java hasAcceptableIndices :528, hasStrictlyIncreasingIndices :439, hasDistinctNonNegativeIndices :484): +// 1. [13,267,824, 13,268,944], 36 anchor heights, NOTHING in force (2026-08-10: the rules were switched off on every validator to +// restart a stopped chain; vanityData carries the client's version string, the indices are in arrival order). The nodes accept +// such a header as written, after one check that is not relaxed at any height: the certificate decodes and its indices are +// non-negative and DISTINCT. This tool says so: NOT ENFORCED, neither verified nor failed. +// 2. [13,268,976, 13,890,544], 19,425 anchor heights, the THRESHOLD ONLY lowered to 1 (the emergency ceiling then in force); the +// digest binding and the strictly increasing indices were in force and are required here exactly as elsewhere. +// Bound to chain 2800 (PqAnchorLapse.CHAIN_ID): another chain, a testnet included, inherits neither window. +const LAPSE_CHAIN = 2800; +const LAPSE_WINDOWS = [ + { first: 13267824, last: 13268944, anchors: 36, relax: 'everything', minSeals: 0, date: '2026-08-10' }, + { first: 13268976, last: 13890544, anchors: 19425, relax: 'threshold-only', minSeals: 1, date: '2026-08-10' }, +]; +// the same self-check the production class runs at load: ordered and disjoint, the span agrees with the bounds at spacing 32, +// no threshold where nothing was in force, at least 1 where only the threshold was lowered +for (let i = 0; i < LAPSE_WINDOWS.length; i++) { + const w = LAPSE_WINDOWS[i], prev = LAPSE_WINDOWS[i - 1]; + if ((prev && w.first <= prev.last) || (w.last - w.first) / 32 + 1 !== w.anchors || (w.relax === 'everything') !== (w.minSeals === 0) || (w.relax === 'threshold-only' && w.minSeals < 1)) { + console.error('FAIL: the dated lapse table of this tool contradicts itself at [' + w.first + ', ' + w.last + ']'); stop(1); + } +} +const lapseAt = (hh) => (CHAIN_ID === LAPSE_CHAIN ? LAPSE_WINDOWS.find(w => hh >= w.first && hh <= w.last) || null : null); +const lapseOff = (hh) => { const w = lapseAt(hh); return !!w && w.relax === 'everything'; }; +// for SPACING the grid only: the window's bounds are anchor heights, and the blocks between them and the neighbouring unaffected anchors +// (up to 31 on each side, the spacing in force: PqAnchorLapse.ANCHOR_SPACING) also carry a certificate element (measured 2026-10-01: +// 13,268,959 and 13,268,960 both do), so the lookback steps over [first - 31, last + 31] as well; no anchor is judged differently by this +const lapseOffForGrid = (hh) => CHAIN_ID === LAPSE_CHAIN && LAPSE_WINDOWS.some(w => w.relax === 'everything' && hh >= w.first - 31 && hh <= w.last + 31); +// the threshold an anchor is judged by: the schedule, or, inside a threshold-only window, the lower threshold then in force +// (the lower of the two, as the production rule takes it) +const minSealsAt = (hh) => { + const k = Number(process.env.MIN_SEALS || K_SCHEDULE.filter(([h]) => hh >= h).map(([, k]) => k).pop()); + const w = lapseAt(hh); + return w && w.relax === 'threshold-only' && w.minSeals < k ? w.minSeals : k; +}; const COUNT = Number(process.env.ANCHORS || 20); // ---- minimal RLP --------------------------------------------------------------------------- @@ -300,7 +341,14 @@ let DERIVED_INTERVAL = 0; if (!INTERVAL_SCHEDULE.length) { const seen = []; const LOOKBACK = Number(process.env.ANCHOR_LOOKBACK || 1100); // two 512-intervals and a margin - for (let h = top; h > top - LOOKBACK && h > ANCHOR_FROM && seen.length < 2; h--) { + // D-478: inside the dated lapse where nothing was in force (chain 2800 only) headers carry a certificate element on blocks off + // the grid too, so they cannot space it; those heights are stepped over and do not count toward the lookback + // and two headers on the two sides of such a range do not measure the spacing (their distance spans the range), so a header found + // above it is dropped when the lookback crosses it, and the spacing is taken from two consecutive headers on one side + let scanned = 0; + for (let h = top; scanned < LOOKBACK && h > ANCHOR_FROM && seen.length < 2; h--) { + if (lapseOffForGrid(h)) { if (seen.length === 1) seen.length = 0; continue; } + scanned++; if (await carriesCertificate(h)) seen.push(h); } if (seen.length < 2) { @@ -332,7 +380,9 @@ if (anchor < ANCHOR_FROM) { console.error('FAIL: no anchor at or below ' + top + { const b = await rpc('eth_getBlockByNumber', ['0x' + anchor.toString(16), false]); const vanity = rlpRead(hex(b.extraData), 0).item[0].item; - 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)'); stop(1); } + // D-478: inside the dated lapse where nothing was in force the vanity IS the client string, as the production client accepts it + if (lapseOff(anchor)) console.log('anchor ' + anchor + ' falls in a dated lapse of chain 2800 (' + lapseAt(anchor).date + ', ' + lapseAt(anchor).first + '..' + lapseAt(anchor).last + '): the anchor rules were not in force there'); + else 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)'); stop(1); } } // AIP-22 stage 3 (2026-09-23): from the identity height a chain's validator set holds post-quantum ids, not ECDSA addresses. @@ -436,6 +486,8 @@ let ok = 0, bad = 0, sealsTotal = 0, sealsValid = 0; let unmeasured = 0; // D-477: anchors whose parent header could not be read or did not re-encode to its own hash, so the set it lists was not read let setUnmeasured = 0; +// D-478: anchors in the dated lapse where nothing was in force: judged only by what the nodes still require there, never VERIFIED +let lapseNotEnforced = 0; // 2026-09-26: the most recent anchor whose certificate VERIFIED here (anchors are walked newest first). Its PARENT is the // block the certificate's seals signed (AIP-15/22: the certificate signs the parent's commit message); printed in the summary // so a stranger reads from this tool, not from our word, which block the newest verified certificate covers. Nodes did not serve @@ -456,6 +508,26 @@ for (let i = 0; i < COUNT; i++) { const parent = await rpc('eth_getBlockByNumber', ['0x' + (h - 1).toString(16), false]); const items = rlpRead(hex(b.extraData), 0).item; const vanity = Buffer.from(items[0].item); + // D-478, the lapse where nothing was in force (chain 2800, 2026-08-10): the nodes accept the header as written after ONE check that + // is not relaxed at any height, that the certificate (possibly absent: empty) decodes as [index, signature] pairs with non-negative, + // pairwise DISTINCT indices (PqAnchorSealsRule.acceptAsWrittenInsideLapse, PqAnchor.hasDistinctNonNegativeIndices). Nothing else + // is judged, so nothing is verified: the answer is NOT ENFORCED, counted apart from VERIFIED and from FAIL. + if (lapseOff(h)) { + const w = lapseAt(h); + let pairs = null; + try { + const c = items.length >= 6 ? items[5] : { list: true, item: [] }; + if (c.list && c.item.every(s => s.list && s.item.length === 2 && !s.item[0].list && !s.item[1].list && s.item[0].item.length <= 4)) { + pairs = c.item.map(s => (s.item[0].item.length ? parseInt(Buffer.from(s.item[0].item).toString('hex'), 16) : 0)); + } + } catch (e) { pairs = null; } + if (pairs === null) { bad++; console.log(h + ' FAIL: inside the dated lapse of ' + w.date + ' the certificate must still decode as [index, signature] pairs, and it does not'); continue; } + if (new Set(pairs).size !== pairs.length) { bad++; console.log(h + ' FAIL: inside the dated lapse of ' + w.date + ' the certificate repeats a validator index (' + pairs.join(',') + '); distinct indices are required at every height'); continue; } + lapseNotEnforced++; + console.log(h + ' NOT ENFORCED (dated lapse ' + w.date + '): the anchor rules were not in force at ' + w.first + '..' + w.last + ' on chain 2800, so this header' + + ' 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) { console.log(h + ' FAIL: no certificate element'); bad++; continue; } const cert = items[5]; const toNested = (node) => (node.list ? node.item.map(toNested) : Buffer.from(node.item)); @@ -498,6 +570,12 @@ for (let i = 0; i < COUNT; i++) { const rowsOf = sealNodes.map(s => isV2 ? { scheme: (s.item[0].item.length ? s.item[0].item[0] : 0), idx: (s.item[1].item.length ? s.item[1].item[0] : 0), sig: new Uint8Array(s.item[2].item) } : { scheme: 1, idx: (s.item[0].item.length ? s.item[0].item[0] : 0), sig: new Uint8Array(s.item[1].item) }); + // D-478: the one order the production client accepts, at every height outside the lapse where nothing was in force (the window + // where only the threshold was lowered included): v1, indices strictly increasing (PqAnchor.hasStrictlyIncreasingIndices, which also + // makes a repeated index unrepresentable); v2, (index, scheme) strictly increasing (PqAnchorV2.CANONICAL) + const orderOk = rowsOf.every((r, i) => i === 0 || (isV2 + ? (r.idx > rowsOf[i - 1].idx || (r.idx === rowsOf[i - 1].idx && r.scheme > rowsOf[i - 1].scheme)) + : r.idx > rowsOf[i - 1].idx)); for (const r of rowsOf) if (r.scheme === 1) falconIdx.add(r.idx); for (const r of rowsOf) schemesSeen.add(r.scheme); for (const { scheme, idx, sig } of rowsOf) { @@ -541,21 +619,22 @@ for (let i = 0; i < COUNT; i++) { 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; + const good = bound && man && man.good && schemesOk && valid === n && orderOk; // 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. // ce NU se poate verifica pe masina asta: fara @noble/post-quantum, orice schema in afara de falcon-512. // Sigiliile ei ajung in 'invalid' si schema ei in 'missingSchemes' - amandoua sunt lipsuri ale RULARII, nu // ale lantului, si numai ele au voie sa ramana pentru ca blocul sa fie NEMASURAT in loc de ESUAT. const unverifiableHere = (nm) => !slhDsa && nm !== 'falcon-512'; - const envBlocked = !good && isV2 && !slhDsa && rowsOf.some(r => r.scheme === 2) + const envBlocked = !good && orderOk && isV2 && !slhDsa && rowsOf.some(r => r.scheme === 2) && bound && man && man.good && falconValid >= MIN_SEALS && missingSchemes.every(unverifiableHere) && invalid.every(x => unverifiableHere(String(x).split('@')[0])) && 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(); } 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++; console.log(h + ' NOT VERIFIED HERE: bound under block hash, ' + falconValid + ' valid falcon-512 seals, but this' @@ -571,6 +650,8 @@ for (let i = 0; i < COUNT; i++) { (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(',') + ' (the set of block ' + (h - 1) + ', the block these seals sign)' : '') + (unknownIndex.length ? ' unknown index ' + unknownIndex.join(',') : '') + (duplicate.length ? ' the same validator sealed more than once in one scheme: ' + duplicate.join(',') + ' (counted once; the nodes refuse such a certificate)' : '') + + (orderOk ? '' : ' seals not in the one accepted order (' + (isV2 ? '(index, scheme)' : 'index') + ' strictly increasing): ' + rowsOf.map(r => (isV2 ? r.idx + '/' + r.scheme : r.idx)).join(',')) + + (lapseAt(h) ? ' (inside the dated window ' + lapseAt(h).first + '..' + lapseAt(h).last + ' only the threshold was lowered, to ' + lapseAt(h).minSeals + ')' : '') + (isV2 && !slhDsa && rowsOf.some(r => r.scheme === 2) ? ' (SLH-DSA verifier not installed: npm install @noble/post-quantum)' : '')); } } @@ -721,8 +802,16 @@ console.log(propChecked === 0 + (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; -console.log(all - ? 'VERIFIED: ' + ok + '/' + COUNT + ' anchor blocks carry a post-quantum certificate bound under the block hash, every seal a valid ' +// 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. +const nothingVerified = all && ok === 0 && lapseNotEnforced > 0; +if (lapseNotEnforced) console.log('LAPSE: ' + lapseNotEnforced + ' anchor(s) in a dated lapse of chain 2800 (2026-08-10), not verified: the anchor rules were not in force there,' + + ' as the production client names it (PqAnchorLapse); only their seal indices were checked distinct. They are not counted below.'); +console.log(nothingVerified + ? 'NOT VERIFIED: all ' + lapseNotEnforced + ' anchor(s) read fall in the dated lapse; there was nothing in force to verify. Run below or above it.' + : all + ? 'VERIFIED: ' + ok + '/' + (COUNT - lapseNotEnforced) + ' anchor blocks carry a post-quantum certificate bound under the block hash, every seal a valid ' + ([...schemesSeen].sort().map(x => SCHEME_NAMES[x] || ('scheme ' + x)).join(' or ') || 'post-quantum') + ' signature by a key that a distinct validator of the block it signs bound to itself with its own consensus key.' + (propOk ? ' And ' + propOk + ' of the ' + propOk + ' most recent headers checked carry a valid Falcon-512 proposal seal by the key of their coinbase,' @@ -763,4 +852,4 @@ if (process.env.ANCHOR_REPORT) { // three states, three exit codes: 0 verified, 1 the chain failed a check, 2 this machine could not measure. // exitCode, not process.exit(): on Windows, exiting while the HTTP sockets are still closing aborts node inside libuv // (0xC0000409), and the verdict printed above would reach the caller with a code that is neither 0, 1 nor 2. -process.exitCode = all ? 0 : (bad === 0 && manifestBad === 0 && propBad === 0 ? 2 : 1); +process.exitCode = all ? (nothingVerified ? 2 : 0) : (bad === 0 && manifestBad === 0 && propBad === 0 ? 2 : 1);