verify-anchor.mjs: step 6, the proposer's Falcon-512 seal on every recent header (AIP-22 stage 5), verified against the v3 registry key of its index and bound to the coinbase; proven on testnet 28001 with two planted forgeries rejected

This commit is contained in:
Aere Network 2026-09-24 23:01:51 +03:00
parent c0e6b62d71
commit 0f6ec82fa1

View File

@ -22,6 +22,11 @@
// 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.
// 6. PROPOSER (AIP-22 stage 5, 2026-09-24) on a chain where headers carry element 6, the proposer's
// Falcon-512 seal over keccak256(RLP["AERE-PQ-PROPOSAL-1", chainId, height, round, digest])
// verifies against the v3 registry key of its index, and that index is the header's
// coinbase, on the most recent headers (PROPOSER_HEADERS, default 20). Where no header
// carries it (chain 2800 today) the tool says so and this leg does not change the verdict.
// //
// What this still does NOT prove: who RUNS the validators (today: one operator, the Foundation; we // What this still does NOT prove: who RUNS the validators (today: one operator, the Foundation; we
// publish that ourselves), or that the RPC you use is honest about chain contents - bring your own // publish that ourselves), or that the RPC you use is honest about chain contents - bring your own
@ -510,19 +515,78 @@ for (let i = 0; i < COUNT; i++) {
} }
} }
// 6. PROPOSER SEALS, AIP-22 stage 5 (added 2026-09-24). From H_hdr every header carries element 6, [index, signature]: the
// proposer's Falcon-512 seal over keccak256(RLP["AERE-PQ-PROPOSAL-1", chainId, height, round, digest]), where digest is the hash
// of the header re-encoded in the EXCLUDE_COMMIT_SEALS form (SPEC 2.3: round kept, commit seals an empty list, the certificate
// and the proposer seal omitted). The index names a row of the v3 registry epoch in force at that height, and that row's id must
// be the header's coinbase. Each header is first re-encoded in the on-chain form (round 0) and must reproduce the hash the
// endpoint reports: that is the control of THIS header's encoding, and a header that fails it is not judged at all. Headers
// without element 6 (every header of a chain, or of heights, before stage 5) are counted and named, never judged.
const PROPOSER_HEADERS = Number(process.env.PROPOSER_HEADERS || 20);
const PROPOSAL_DOMAIN = 'AERE-PQ-PROPOSAL-1';
const nested = (node) => (node.list ? node.item.map(nested) : Buffer.from(node.item));
function headerRlp(b, extra) {
const f = [hex(b.parentHash), hex(b.sha3Uncles), hex(b.miner), hex(b.stateRoot), hex(b.transactionsRoot), hex(b.receiptsRoot),
hex(b.logsBloom), scalar(b.difficulty), scalar(b.number), scalar(b.gasLimit), scalar(b.gasUsed), scalar(b.timestamp), extra,
hex(b.mixHash), hex(b.nonce)];
if (b.baseFeePerGas != null) f.push(scalar(b.baseFeePerGas));
if (b.withdrawalsRoot != null) f.push(hex(b.withdrawalsRoot));
if (b.blobGasUsed != null) { f.push(scalar(b.blobGasUsed)); f.push(scalar(b.excessBlobGas)); }
if (b.parentBeaconBlockRoot != null) f.push(hex(b.parentBeaconBlockRoot));
if (b.requestsHash != null) f.push(hex(b.requestsHash));
if (b.balHash != null) f.push(hex(b.balHash));
return rlpEncode(f);
}
let propOk = 0, propBad = 0, propUnmeasured = 0, propAbsent = 0;
{
const headNow = parseInt(await rpc('eth_blockNumber', []), 16);
for (let k = 0; k < PROPOSER_HEADERS; k++) {
const h = headNow - 2 - k; if (h < 1) break;
const b = await rpc('eth_getBlockByNumber', ['0x' + h.toString(16), false]);
const items = rlpRead(hex(b.extraData), 0).item;
if (items.length < 7) { propAbsent++; continue; }
const onchain = keccak(headerRlp(b, rlpEncode([nested(items[0]), nested(items[1]), nested(items[2]), Buffer.alloc(0), []])));
if (!onchain.equals(hex(b.hash))) {
propUnmeasured++;
console.log(h + ' PROPOSER SEAL NOT CHECKED HERE: this header does not re-encode to the hash the endpoint reports, so its proposal digest cannot be computed with confidence');
continue;
}
const seal = items[6];
if (!seal.list || seal.item.length !== 2 || seal.item[0].list || seal.item[1].list) { propBad++; console.log(h + ' FAIL: the proposer seal is not [index, signature]'); continue; }
const idx = seal.item[0].item.length ? parseInt(Buffer.from(seal.item[0].item).toString('hex'), 16) : 0;
const digestP = keccak(headerRlp(b, rlpEncode([nested(items[0]), nested(items[1]), nested(items[2]), nested(items[3]), []])));
const msgP = keccak(rlpEncode([Buffer.from(PROPOSAL_DOMAIN, 'ascii'), scalar(CHAIN_ID), scalar(h), Buffer.from(items[3].item), digestP]));
const ep = v3EpochAt(h);
const row = ep && ep.byIndex[idx];
if (!row) { propBad++; console.log(h + ' FAIL: proposer seal index ' + idx + ' names no row of a published v3 registry epoch in force at this height'); continue; }
if (String(b.miner).toLowerCase() !== row.id) { propBad++; console.log(h + ' FAIL: proposer seal index ' + idx + ' is id ' + row.id + ', but the coinbase is ' + b.miner); continue; }
if (!row.pk || !verifyFalcon512(row.pk, msgP, new Uint8Array(seal.item[1].item))) { propBad++; console.log(h + ' FAIL: the proposer seal of index ' + idx + ' is not a valid Falcon-512 signature over the proposal message'); continue; }
propOk++;
}
}
const propChecked = propOk + propBad + propUnmeasured;
console.log(''); console.log('');
console.log(propChecked === 0
? 'proposer seals (AIP-22 stage 5): none of the ' + propAbsent + ' most recent headers carries one; stage 5 is not active at these heights on this chain, so there is nothing to verify'
: 'proposer seals (AIP-22 stage 5): ' + propOk + ' of ' + propChecked + ' recent headers carry a valid Falcon-512 proposal seal by the key of their coinbase'
+ (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; const all = bad === 0 && manifestBad === 0 && unmeasured === 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 the validator of that height bound to itself with its own consensus key.'
: (bad === 0 && manifestBad === 0 + (propOk ? ' And ' + propOk + ' of the ' + propOk + ' most recent headers checked carry a valid Falcon-512 proposal seal by the key of their coinbase (AIP-22 stage 5).' : '')
? 'UNMEASURED: ' + unmeasured + ' of ' + COUNT + ' anchors could not be fully checked ON THIS MACHINE because the SLH-DSA\n' : (bad === 0 && manifestBad === 0 && propBad === 0
+ ' verifier is missing. Nothing here says the chain is wrong; install it and run again:\n' ? (unmeasured
+ ' npm install @noble/post-quantum' ? 'UNMEASURED: ' + unmeasured + ' of ' + COUNT + ' anchors could not be fully checked ON THIS MACHINE because the SLH-DSA\n'
: 'PROBLEMS: ' + bad + ' of ' + COUNT + ' anchors and ' + manifestBad + ' manifests failed - do not take our word for anything; the failure list above is the finding.' + ' 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'
+ ' 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)' : '')));
if (!slhDsa) console.log('note: @noble/post-quantum is not installed, so a v2 certificate carrying SLH-DSA seals cannot be verified here (npm install @noble/post-quantum).'); if (!slhDsa) console.log('note: @noble/post-quantum is not installed, so a v2 certificate carrying SLH-DSA seals cannot be verified here (npm install @noble/post-quantum).');
// three states, three exit codes: 0 verified, 1 the chain failed a check, 2 this machine could not measure. // three states, three exit codes: 0 verified, 1 the chain failed a check, 2 this machine could not measure.
process.exit(all ? 0 : (bad === 0 && manifestBad === 0 ? 2 : 1)); process.exit(all ? 0 : (bad === 0 && manifestBad === 0 && propBad === 0 ? 2 : 1));