Anchor verifier: the validator set is read from the parent header the certificate seals (bound to its hash), with no 1000-block cache; distinct members counted once per scheme

This commit is contained in:
Aere Network 2026-10-01 20:03:55 +03:00
parent e63135ed39
commit d56a120462

View File

@ -17,8 +17,10 @@
// 3. SEALS every Falcon-512 seal in every certificate verifies, in plain JavaScript written
// from the spec (falcon512.mjs), against the manifest key of its index, over
// keccak256(RLP["AERE-PQ-COMMIT-1", chainId, parentNumber, parentHash]).
// 4. VALIDATORS the validator address bound to each sealing key was in the QBFT validator set at
// that height (qbft_getValidatorsByBlockNumber), so the seals are the validators'.
// 4. VALIDATORS the validator bound to each sealing key was in the QBFT validator set of the PARENT block, the block the
// seals sign, read from that block's own header (bound to its hash), and each counts once per scheme, so the
// seals are those of distinct validators of that block (D-477, 2026-10-01: until then the set was asked of
// the endpoint at the anchor's height and cached per window of 1000 blocks, and duplicates counted).
// 5. ON-CHAIN PIN each manifest's hash, keccak256(addr||pk over rows), equals slot 0 of the immutable
// anchor contract named in the index, and that contract's code is the 11-byte
// read-only getter with no SSTORE: the manifest you hold is the one the chain pinned.
@ -411,15 +413,18 @@ const memberOf = (addr, h) => {
return e.link.get(addr) ?? addr;
};
const validatorsCache = new Map();
async function validatorsAt(h) {
// 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())));
}
return validatorsCache.get(key);
// D-477 (2026-10-01): a certificate's seals sign M(p) for the PARENT p = h - 1, so they are judged against the validator set of
// p, as both clients judge them (the validators of the parent block). That set is read here from element 1 of p's OWN header,
// which the caller first re-encodes in its on-chain form (SPEC 2.3) to the hash the seals sign: an endpoint cannot hand this tool
// a set of its choosing. Until this date the set was asked of the endpoint at the ANCHOR's height and cached per window of 1000
// blocks, so across a set vote up to 1000 blocks were judged on the wrong set: the seal of a member removed later was refused (the
// tool accused a sound chain, the family of D-372) and the seal of a member added later was counted. There is no cache: the set is
// read for each parent. Before the identity height the list holds ECDSA addresses, from it post-quantum ids (AIP-22 stage 3),
// matched below through the same translation as before.
async function validatorsAt(p, pItems) {
const el = pItems && pItems[1];
if (!el || !el.list || el.item.length === 0 || el.item.some(x => x.list || x.item.length !== 20)) return null;
return new Set(el.item.map(x => '0x' + Buffer.from(x.item).toString('hex')));
}
let ok = 0, bad = 0, sealsTotal = 0, sealsValid = 0;
@ -429,6 +434,8 @@ let ok = 0, bad = 0, sealsTotal = 0, sealsValid = 0;
// tocmai urmase instructiunea noastra de pe site. Un NEMASURAT nemeritat e la fel de mincinos ca un verde
// nemeritat: se numara separat si iese cu alt cod.
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;
// 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
@ -452,6 +459,25 @@ for (let i = 0; i < COUNT; i++) {
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));
// 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
// (on-chain form: round 0, commit seals empty, certificate and proposer seal omitted) to the hash the endpoint reports for it,
// and that hash must be the parent this anchor names. The first two are this run's gap (another endpoint may serve it); the
// third is a header that is not this anchor's parent, so the anchor is not judged on it.
let pItems = null, pSelf = false;
if (parent && parent.extraData) {
try {
pItems = rlpRead(hex(parent.extraData), 0).item;
pSelf = keccak(headerRlp(parent, rlpEncode([toNested(pItems[0]), toNested(pItems[1]), toNested(pItems[2]), Buffer.alloc(0), []]))).equals(hex(parent.hash));
} catch (e) { pSelf = false; }
}
if (!pSelf) {
setUnmeasured++;
console.log(h + ' NOT VERIFIED HERE: the header served for block ' + (h - 1) + (parent ? ' does not re-encode to the hash the endpoint reports for it'
: ' could not be read') + ', so the validator set it lists (the set these seals are judged against) cannot be read with confidence.'
+ ' This is a gap in THIS run, not a finding about the chain; try another endpoint (RPC=...).');
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; }
// 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;
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; }
@ -462,9 +488,11 @@ for (let i = 0; i < COUNT; i++) {
const man = manifestAt(h);
const msg = commitMessage(h - 1, hex(parent.hash));
const vset = await validatorsAt(h);
let valid = 0, invalid = [], notValidator = [], unknownIndex = [], unknownScheme = [], unbound = [], noKey = [];
const perScheme = {}; // scheme name -> valid seals
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 = [];
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 falconIdx = new Set();
const sealNodes = isV2 ? cert.item[1].item : cert.item;
const rowsOf = sealNodes.map(s => isV2
@ -480,7 +508,12 @@ for (let i = 0; i < COUNT; i++) {
const v3k = h - 1 >= KEYS_FROM ? v3EpochAt(h - 1) : null;
const row = v3k ? v3k.byIndex[idx] : (man && man.rows[idx]);
if (!row) { unknownIndex.push(idx); continue; }
if (!vset.has(v3k ? row.id : memberOf(row.addr.toLowerCase(), h))) { notValidator.push(idx); continue; }
const member = v3k ? row.id : memberOf(row.addr.toLowerCase(), h - 1);
if (!vset.has(member)) { notValidator.push(idx); continue; }
// D-477: a second seal of the same validator in the same scheme is not counted again; the nodes refuse such a certificate
const seen = countedBy[name] || (countedBy[name] = new Set());
if (seen.has(member)) { duplicate.push(name + '@' + idx); continue; }
seen.add(member);
if (scheme === 1) {
if (!row.pk || !verifyFalcon512(row.pk, msg, sig)) { invalid.push('falcon@' + idx); continue; }
} else {
@ -520,9 +553,9 @@ for (let i = 0; i < COUNT; i++) {
&& 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;
&& 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 validators of this height (' + (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) + ')'); }
else if (envBlocked) {
unmeasured++;
console.log(h + ' NOT VERIFIED HERE: bound under block hash, ' + falconValid + ' valid falcon-512 seals, but this'
@ -536,7 +569,8 @@ for (let i = 0; i < COUNT; i++) {
(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(',') : '') +
(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)' : '') +
(isV2 && !slhDsa && rowsOf.some(r => r.scheme === 2) ? ' (SLH-DSA verifier not installed: npm install @noble/post-quantum)' : ''));
}
}
@ -686,11 +720,11 @@ console.log(propChecked === 0
+ ' or of the proposer of the round that re-proposed the block' + (propReproposed ? ' (' + propReproposed + ' re-proposal(s), named above)' : '')
+ (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 && propBad === 0 && propUnmeasured === 0;
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 '
+ ([...schemesSeen].sort().map(x => SCHEME_NAMES[x] || ('scheme ' + x)).join(' or ') || 'post-quantum')
+ ' signature by a key that the validator of that height bound to itself with its own consensus key.'
+ ' 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,'
+ ' or of the proposer of the round that re-proposed the block (AIP-22 stage 5).' : '')
: (bad === 0 && manifestBad === 0 && propBad === 0
@ -699,10 +733,14 @@ console.log(all
+ ' verifier is missing. Nothing here says the chain is wrong; install it and run again:\n'
+ ' npm install @noble/post-quantum'
: '')
+ (propUnmeasured ? (unmeasured ? '\n' : '') + 'UNMEASURED: ' + propUnmeasured + ' recent header(s) did not re-encode to the hash the endpoint reports, so their'
+ (setUnmeasured ? (unmeasured ? '\n' : '') + 'UNMEASURED: ' + setUnmeasured + ' of ' + COUNT + ' anchors: the header served for the parent could not be read or did'
+ ' not re-encode to its own hash, so the validator set the seals are judged against was not read. This says nothing about the chain;'
+ ' try another endpoint (RPC=...).' : '')
+ (propUnmeasured ? (unmeasured || setUnmeasured ? '\n' : '') + 'UNMEASURED: ' + propUnmeasured + ' recent header(s) did not re-encode to the hash the endpoint reports, so their'
+ ' proposer seals were not judged. This says nothing about the chain; try another endpoint (RPC=...).' : '')
: 'PROBLEMS: ' + bad + ' of ' + COUNT + ' anchors and ' + manifestBad + ' manifests failed' + (propBad ? ', and ' + propBad + ' proposer seals' : '') + ' - do not take our word for anything; the failure list above is the finding.'
+ (unmeasured ? ' (' + unmeasured + ' more could not be checked here: npm install @noble/post-quantum)' : '')));
+ (unmeasured ? ' (' + unmeasured + ' more could not be checked here: npm install @noble/post-quantum)' : '')
+ (setUnmeasured ? ' (' + setUnmeasured + ' more not judged: their parent header did not re-encode to its own hash on this endpoint)' : '')));
// 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'
// classical ECDSA commit seals, and this tool printed the stage-5 sentence there on every run. It now says what it measured above.