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
This commit is contained in:
parent
d56a120462
commit
725aeebb65
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user