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
|
// 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
|
// from the spec (falcon512.mjs), against the manifest key of its index, over
|
||||||
// keccak256(RLP["AERE-PQ-COMMIT-1", chainId, parentNumber, parentHash]).
|
// 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
|
// 4. VALIDATORS the validator bound to each sealing key was in the QBFT validator set of the PARENT block, the block the
|
||||||
// that height (qbft_getValidatorsByBlockNumber), so the seals are the validators'.
|
// 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
|
// 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
|
// 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.
|
// 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;
|
return e.link.get(addr) ?? addr;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validatorsCache = new Map();
|
// 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
|
||||||
async function validatorsAt(h) {
|
// 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,
|
||||||
// the identity switch changes the form of the set; one cache window must never hold both forms
|
// 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
|
||||||
const key = Math.floor(h / 1000) + (h >= IDENTITY_BLOCK ? ':id' : ':addr');
|
// 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
|
||||||
if (!validatorsCache.has(key)) {
|
// 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
|
||||||
const v = await rpc('qbft_getValidatorsByBlockNumber', ['0x' + h.toString(16)]);
|
// 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
|
||||||
validatorsCache.set(key, new Set((v || []).map(a => a.toLowerCase())));
|
// 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.
|
||||||
return validatorsCache.get(key);
|
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;
|
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
|
// 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.
|
// nemeritat: se numara separat si iese cu alt cod.
|
||||||
let unmeasured = 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;
|
||||||
// 2026-09-26: the most recent anchor whose certificate VERIFIED here (anchors are walked newest first). Its PARENT is the
|
// 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
|
// 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
|
// 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; }
|
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 cert = items[5];
|
||||||
const toNested = (node) => (node.list ? node.item.map(toNested) : Buffer.from(node.item));
|
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.
|
// 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;
|
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; }
|
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 man = manifestAt(h);
|
||||||
const msg = commitMessage(h - 1, hex(parent.hash));
|
const msg = commitMessage(h - 1, hex(parent.hash));
|
||||||
const vset = await validatorsAt(h);
|
const vset = await validatorsAt(h - 1, pItems);
|
||||||
let valid = 0, invalid = [], notValidator = [], unknownIndex = [], unknownScheme = [], unbound = [], noKey = [];
|
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; }
|
||||||
const perScheme = {}; // scheme name -> valid seals
|
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 falconIdx = new Set();
|
||||||
const sealNodes = isV2 ? cert.item[1].item : cert.item;
|
const sealNodes = isV2 ? cert.item[1].item : cert.item;
|
||||||
const rowsOf = sealNodes.map(s => isV2
|
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 v3k = h - 1 >= KEYS_FROM ? v3EpochAt(h - 1) : null;
|
||||||
const row = v3k ? v3k.byIndex[idx] : (man && man.rows[idx]);
|
const row = v3k ? v3k.byIndex[idx] : (man && man.rows[idx]);
|
||||||
if (!row) { unknownIndex.push(idx); continue; }
|
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 (scheme === 1) {
|
||||||
if (!row.pk || !verifyFalcon512(row.pk, msg, sig)) { invalid.push('falcon@' + idx); continue; }
|
if (!row.pk || !verifyFalcon512(row.pk, msg, sig)) { invalid.push('falcon@' + idx); continue; }
|
||||||
} else {
|
} else {
|
||||||
@ -520,9 +553,9 @@ for (let i = 0; i < COUNT; i++) {
|
|||||||
&& missingSchemes.every(unverifiableHere)
|
&& missingSchemes.every(unverifiableHere)
|
||||||
&& invalid.every(x => unverifiableHere(String(x).split('@')[0]))
|
&& invalid.every(x => unverifiableHere(String(x).split('@')[0]))
|
||||||
&& unknownScheme.length === 0 && unknownIndex.length === 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(' + ');
|
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) {
|
else if (envBlocked) {
|
||||||
unmeasured++;
|
unmeasured++;
|
||||||
console.log(h + ' NOT VERIFIED HERE: bound under block hash, ' + falconValid + ' valid falcon-512 seals, but this'
|
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(',') : '') +
|
(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(',') : '') +
|
(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(',') : '') +
|
(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)' : ''));
|
(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)' : '')
|
+ ' 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)' : ''));
|
+ (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');
|
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
|
console.log(all
|
||||||
? 'VERIFIED: ' + ok + '/' + COUNT + ' anchor blocks carry a post-quantum certificate bound under the block hash, every seal a valid '
|
? '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')
|
+ ([...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,'
|
+ (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).' : '')
|
+ ' or of the proposer of the round that re-proposed the block (AIP-22 stage 5).' : '')
|
||||||
: (bad === 0 && manifestBad === 0 && propBad === 0
|
: (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'
|
+ ' verifier is missing. Nothing here says the chain is wrong; install it and run again:\n'
|
||||||
+ ' npm install @noble/post-quantum'
|
+ ' 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=...).' : '')
|
+ ' 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.'
|
: '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
|
// 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'
|
// 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.
|
// 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