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:
parent
e63135ed39
commit
d56a120462
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user