aere-node/tools/verify-anchor.mjs

421 lines
30 KiB
JavaScript

#!/usr/bin/env node
// verify-anchor.mjs (v2) - independently verify Aere Network's post-quantum header anchor,
// INCLUDING the Falcon-512 signatures, from a public RPC endpoint and the published key manifest.
//
// v1 of this tool proved the BINDING: every 32nd header carries a certificate of Falcon-512 seals
// and a 32-byte digest in the hashed part of the header, and the digest equals
// keccak256(RLP["AERE-PQ-ANCHOR-1", chainId, parentNumber, parentHash, certificate]). It did NOT
// check that the seals are valid signatures; for that you had to ask the chain's own precompile,
// i.e. ask us. v2 removes that trust point. What v2 proves, in order, each leg independent:
//
// 1. BINDING as v1: strip or alter the certificate and the digest no longer matches.
// 2. KEY MANIFEST the published registry of Falcon public keys is self-consistent: every row's
// proof of possession (a Falcon signature over the row) verifies with the row's
// key, and every row's claim (an ECDSA signature by the validator's own key, EIP-191
// version 0) recovers to the row's validator address. So the manifest cannot have
// been invented by us: each row is signed by the validator's consensus key.
// 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'.
// 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.
//
// 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
// node and pass RPC=<your-url> to remove even that. The manifests can also be given from disk.
//
// Run it:
// npm install @noble/hashes @noble/curves
// node verify-anchor.mjs # 20 most recent anchors, manifests fetched from aere.network
// node verify-anchor.mjs 13921040 # 20 anchors ending at a chosen height
// ANCHORS=100 node verify-anchor.mjs # more anchors
// MANIFESTS=./manifeste node verify-anchor.mjs # manifests from a local directory instead of the site
// RPC=http://127.0.0.1:8545 node verify-anchor.mjs
//
// Exit 0 only when every leg holds on every anchor checked. Any failure is the finding.
import { keccak_256 } from '@noble/hashes/sha3';
import { sha256 } from '@noble/hashes/sha256';
import { secp256k1 } from '@noble/curves/secp256k1';
import fs from 'node:fs';
import path from 'node:path';
import { verifyFalcon512, fromHex } from './falcon512.mjs';
const RPC = process.env.RPC || 'https://rpc.aere.network';
const MANIFESTS = process.env.MANIFESTS || 'https://aere.network/pq/manifests';
// CHAIN_ID din mediu: 2800 (mainnet, implicit) sau 28001 (testnetul public, cu manifestele lui sub
// testnet-rpc.aere.network/sarcina/pq/manifests). Aceeasi unealta, acelasi drum de verificare.
const CHAIN_ID = Number(process.env.CHAIN_ID || 2800);
const ANCHOR_DOMAIN = 'AERE-PQ-ANCHOR-1';
// v2 (2026-09-03): the SCHEME-TAGGED certificate RLP[2, [[scheme, idx, sig], ...]] under its own domain.
// Wire tags: 0x01 falcon-512, 0x02 slh-dsa-sha2-128s. Every scheme the certificate carries must reach K, and
// every non-Falcon seal must ride with a Falcon seal of the same index (the binding both clients enforce).
const ANCHOR_DOMAIN_V2 = 'AERE-PQ-ANCHOR-2';
const SCHEME_NAMES = { 1: 'falcon-512', 2: 'slh-dsa-sha2-128s' };
let slhDsa = null;
try { slhDsa = (await import('@noble/post-quantum/slh-dsa.js')).slh_dsa_sha2_128s; } catch { slhDsa = null; }
const verifySlhDsa = (pk, msg, sig) => { if (!slhDsa) return false; try { return slhDsa.verify(new Uint8Array(sig), new Uint8Array(msg), new Uint8Array(pk)); } catch { return false; } };
const COMMIT_DOMAIN = 'AERE-PQ-COMMIT-1';
const POP_DOMAIN = 'AERE-PQ-POP-1';
const CLAIM_DOMAIN = 'AERE-PQ-CLAIM-1';
const FORMAT_VERSION = 2;
// pragul IMPUS de lant depinde de inaltime: 3 de la introducere (2026-08-14), 6 din blocul
// 14.961.456 (2026-08-21, cvorum 2f+1). MIN_SEALS din mediu suprascrie, pentru experimente.
const K6_HEIGHT = 14961456;
// orarul pragului per lant: 2800 = 3 de la introducere, 6 din K6_HEIGHT; 28001 = 0 de la 64, 3 din 128
// The chain's own threshold schedule, including the one dated exception: on 2026-09-04, after eleven minutes
// of halted chain (every node had restarted with an empty seal store), the fleet was unblocked with a step
// 17102384:0,17102416:6, so anchor 17102384 legitimately carries no SLH-DSA seal and 9 Falcon seals. A verifier
// 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.
const K_DEFAULT = CHAIN_ID === 28001 ? [[64, 0], [128, 3], [168032, 0], [168064, 3]] : [[0, 3], [K6_HEIGHT, 6], [17102384, 0], [17102416, 6]];
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
// parent is committed), exactly as both clients read them. Without this the tool verified only the seals it found,
// so a certificate that dropped a whole scheme passed. SCHEME_SCHEDULE ("h:scheme+scheme,...") overrides.
const SCHEME_DEFAULT = CHAIN_ID === 28001 ? '168000:falcon-512+slh-dsa-sha2-128s' : '17047568:falcon-512+slh-dsa-sha2-128s';
const SCHEME_SCHEDULE = (process.env.SCHEME_SCHEDULE || SCHEME_DEFAULT).split(',').filter(Boolean)
.map(p => { const [h, list] = p.split(':'); return [Number(h), list.split('+').map(x => x.trim()).filter(Boolean)]; }).sort((a, b) => a[0] - b[0]);
const schemesAt = (hh) => { let out = ['falcon-512']; for (const [from, list] of SCHEME_SCHEDULE) { if (from <= hh) out = list; else break; } return out; };
const minSealsAt = (hh) => Number(process.env.MIN_SEALS || K_SCHEDULE.filter(([h]) => hh >= h).map(([, k]) => k).pop());
const COUNT = Number(process.env.ANCHORS || 20);
// ---- minimal RLP ---------------------------------------------------------------------------
function rlpRead(buf, pos) {
const p = buf[pos];
if (p < 0x80) return { item: buf.subarray(pos, pos + 1), next: pos + 1, list: false };
if (p < 0xb8) { const l = p - 0x80; return { item: buf.subarray(pos + 1, pos + 1 + l), next: pos + 1 + l, list: false }; }
if (p < 0xc0) { const ll = p - 0xb7; let l = 0; for (let i = 0; i < ll; i++) l = l * 256 + buf[pos + 1 + i]; return { item: buf.subarray(pos + 1 + ll, pos + 1 + ll + l), next: pos + 1 + ll + l, list: false }; }
let ll = 0, l;
if (p < 0xf8) l = p - 0xc0; else { ll = p - 0xf7; l = 0; for (let i = 0; i < ll; i++) l = l * 256 + buf[pos + 1 + i]; }
const start = pos + 1 + ll, end = start + l, items = []; let c = start;
while (c < end) { const r = rlpRead(buf, c); items.push(r); c = r.next; }
return { item: items, next: end, list: true };
}
function rlpLen(n) { if (n < 56) return Buffer.from([n]); const b = []; let v = n; while (v > 0) { b.unshift(v & 0xff); v = Math.floor(v / 256); } return Buffer.from([b.length, ...b]); }
function rlpEncode(x) {
if (Buffer.isBuffer(x)) {
if (x.length === 1 && x[0] < 0x80) return x;
if (x.length < 56) return Buffer.concat([Buffer.from([0x80 + x.length]), x]);
const l = rlpLen(x.length); l[0] += 0xb7; return Buffer.concat([l, x]);
}
const body = Buffer.concat(x.map(rlpEncode));
if (body.length < 56) return Buffer.concat([Buffer.from([0xc0 + body.length]), body]);
const l = rlpLen(body.length); l[0] += 0xf7; return Buffer.concat([l, body]);
}
const scalar = (n) => { if (n === 0) return Buffer.alloc(0); const b = []; let v = BigInt(n); while (v > 0n) { b.unshift(Number(v & 0xffn)); v >>= 8n; } return Buffer.from(b); };
const hex = (s) => Buffer.from(s.slice(2), 'hex');
const u64 = (n) => { const b = Buffer.alloc(8); b.writeBigUInt64BE(BigInt(n)); return b; };
const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; };
const keccak = (b) => Buffer.from(keccak_256(b));
async function rpc(method, params) {
const r = await fetch(RPC, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
const j = await r.json(); if (j.error) throw new Error(method + ': ' + JSON.stringify(j.error)); return j.result;
}
// ---- the three messages, exactly as the node computes them -------------------------------------
const commitMessage = (parentNumber, parentHash) =>
keccak(rlpEncode([Buffer.from(COMMIT_DOMAIN, 'ascii'), scalar(CHAIN_ID), scalar(parentNumber), parentHash]));
// PqRegistryBinding.bindingPreimage
function bindingPreimage(bindHeight, count, index, address, publicKey) {
return Buffer.concat([Buffer.from(POP_DOMAIN, 'ascii'), Buffer.from([FORMAT_VERSION]), u64(CHAIN_ID), u64(bindHeight),
u32(count), u32(index), address, u32(publicKey.length), publicKey]);
}
const possessionDigest = (...a) => keccak(bindingPreimage(...a));
function claimDigest(...a) {
const ctx = keccak(Buffer.concat([Buffer.from(CLAIM_DOMAIN, 'ascii'), u64(CHAIN_ID)])).subarray(12, 32);
return keccak(Buffer.concat([Buffer.from([0x19, 0x00]), ctx, bindingPreimage(...a)]));
}
function recoverClaim(digest, sig65) {
if (sig65.length !== 65) return null;
let rec = sig65[64]; if (rec >= 27) rec -= 27;
if (rec !== 0 && rec !== 1) return null;
try {
const s = secp256k1.Signature.fromCompact(sig65.subarray(0, 64)).addRecoveryBit(rec);
const pub = s.recoverPublicKey(digest).toRawBytes(false); // 65 bytes, 0x04 || x || y
return keccak(Buffer.from(pub.subarray(1))).subarray(12, 32);
} catch { return null; }
}
// ---- manifests: fetch or read, then prove each one self-consistent ------------------------------
async function loadText(loc) {
if (/^https?:\/\//.test(loc)) { const r = await fetch(loc); if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + loc); return r.text(); }
return fs.readFileSync(loc, 'utf8');
}
const isUrl = /^https?:\/\//.test(MANIFESTS);
const join = (a, b) => isUrl ? a.replace(/\/$/, '') + '/' + b : path.join(a, b);
let index;
try { index = JSON.parse(await loadText(join(MANIFESTS, 'index.json'))); }
catch (e) { console.error('FAIL: cannot load manifest index from ' + MANIFESTS + ': ' + e.message); process.exit(1); }
if (!index || !Array.isArray(index.manifests) || !index.manifests.length) { console.error('FAIL: manifest index has no entries'); process.exit(1); }
if (Number(index.chainId) !== CHAIN_ID) { console.error('FAIL: manifest index is for chain ' + index.chainId); process.exit(1); }
console.log('chain ' + CHAIN_ID + ' rpc ' + RPC + ' manifests ' + MANIFESTS);
console.log('');
console.log('KEY MANIFESTS (leg 2): every row signed by its Falcon key (possession) AND by the validator\'s ECDSA key (claim)');
const manifests = [];
let manifestBad = 0;
for (const m of index.manifests) {
const text = await loadText(join(MANIFESTS, m.file));
const digest = Buffer.from(sha256(Buffer.from(text, 'utf8'))).toString('hex');
const man = JSON.parse(text);
const shaOk = digest === String(m.sha256).toLowerCase();
let rowsOk = 0, rowsBad = [];
const rows = [];
for (let i = 0; i < man.count; i++) {
const row = man[String(i)];
const addr = hex(row.addr), pk = fromHex(row.pk);
const pop = fromHex(row.pop), claim = hex(row.claim.startsWith('0x') ? row.claim : '0x' + row.claim);
const popOk = verifyFalcon512(pk, possessionDigest(man.bindHeight, man.count, i, addr, Buffer.from(pk)), pop);
const rec = recoverClaim(claimDigest(man.bindHeight, man.count, i, addr, Buffer.from(pk)), claim);
const claimOk = rec !== null && rec.equals(addr);
if (popOk && claimOk) rowsOk++; else rowsBad.push(i + (popOk ? '' : ':possession') + (claimOk ? '' : ':claim'));
// v2: a row may carry extra scheme keys ({"keys": {"slh-dsa-sha2-128s": "0x.."}}); absent = Falcon only
const keys = {};
if (row.keys && typeof row.keys === 'object') for (const [id, kh] of Object.entries(row.keys)) keys[id] = fromHex(kh);
rows.push({ index: i, addr: '0x' + addr.toString('hex'), pk, keys, popOk, claimOk });
}
// leg 5: the on-chain pin. keccak256(addr20 || pk, row by row) must equal slot 0 of the immutable
// anchor contract, and that contract must be the 11-byte read-only getter (no SSTORE anywhere).
let pinOk = false, pinNote = 'no anchorContract in index';
if (!m.anchorContract && m.pin === 'genesis') {
// testnetul pinuieste manifestul in configuratia genezei (pqRegistryHash), care nu se citeste prin RPC:
// legatura e in fisierul de geneza pe care il rulezi deja, nu una pe care unealta o poate verifica aici.
pinOk = true; pinNote = 'pinned in the genesis config (pqRegistryHash), not readable over RPC';
}
if (m.anchorContract) {
const pre = Buffer.concat(rows.flatMap(r => [hex(r.addr), Buffer.from(r.pk)]));
const want = '0x' + keccak(pre).toString('hex');
try {
const slot0 = await rpc('eth_getStorageAt', [m.anchorContract, '0x0', 'latest']);
const code = await rpc('eth_getCode', [m.anchorContract, 'latest']);
const getter = code.toLowerCase() === '0x60005460005260206000f3';
pinOk = slot0.toLowerCase() === want.toLowerCase() && getter;
pinNote = (slot0.toLowerCase() === want.toLowerCase() ? 'slot 0 matches' : 'slot 0 MISMATCH (' + slot0.slice(0, 10) + ' vs ' + want.slice(0, 10) + ')') +
(getter ? ', contract is the read-only getter' : ', contract code is NOT the known read-only getter');
} catch (e) { pinNote = 'pin unmeasured: ' + e.message; }
}
const good = shaOk && pinOk && rowsOk === man.count && Number(man.chainId) === CHAIN_ID && Number(man.formatVersion) === FORMAT_VERSION;
if (!good) manifestBad++;
console.log(' bindHeight ' + String(man.bindHeight).padEnd(10) + ' ' + m.file.padEnd(26) + ' rows ' + rowsOk + '/' + man.count +
' sha256 ' + (shaOk ? 'matches index' : 'MISMATCH') + ' on-chain pin: ' + pinNote + (rowsBad.length ? ' BAD rows: ' + rowsBad.join(' ') : '') + (good ? '' : ' <-- FAIL'));
manifests.push({ bindHeight: Number(man.bindHeight), count: man.count, rows, good });
}
manifests.sort((a, b) => a.bindHeight - b.bindHeight);
const manifestAt = (h) => [...manifests].reverse().find(m => m.bindHeight <= h) || null;
// ---- anchors --------------------------------------------------------------------------------
const top = process.argv[2] ? Number(process.argv[2]) : parseInt(await rpc('eth_blockNumber', []), 16);
console.log('');
console.log('ANCHORS: ' + COUNT + ' ending at or below ' + top);
// Anchor heights are arithmetic, not guessed from the header: on 2800 every 32nd block from 13,014,000
// (h mod 32 == 16, universal from 13,889,296); on 28001 every 32nd block from 64 (h mod 32 == 0). The header
// is then required to LOOK like an anchor (32-byte digest that is not the client's vanity string), so a wrong
// schedule fails loudly instead of silently picking a non-anchor. Measured 2026-09-03: the old vanity scan chose
// 63399 on the testnet, a non-anchor, because that client's vanity never contains 'besu'.
const ANCHOR_MOD = Number(process.env.ANCHOR_MOD || (CHAIN_ID === 28001 ? 0 : 16));
const ANCHOR_FROM = Number(process.env.ANCHOR_FROM || (CHAIN_ID === 28001 ? 64 : 13014000));
// D-336 (2026-09-04): the interval can change at scheduled heights ("h:interval,h:interval", ANCHOR_INTERVAL_SCHEDULE);
// every scheduled interval is a multiple of 32 and every change height is on the new grid, so the anchors of a later
// regime are a subset of the earlier ones. Default: no change (every 32nd block). Same rule as both clients.
// D-336 (2026-09-04): the interval can change at scheduled heights ("h:interval,h:interval"); every scheduled
// interval is a multiple of 32 and every change height is on the new grid, so the anchors of a later regime are
// a subset of the earlier ones. Same rule as both clients.
//
// 2026-09-06: THE SCHEDULE IS NO LONGER SOMETHING YOU HAVE TO KNOW. Until today it came only from the
// ANCHOR_INTERVAL_SCHEDULE environment variable and defaulted to "every 32nd block". Chain 2800 moved to every
// 128th on 2026-09-05, so this tool - run the way a stranger runs it, with no variables - reported
// "PROBLEMS: 15 of 20 anchors failed" against a perfectly healthy chain: the 15 were ordinary blocks it had
// picked off the old grid. A verifier that cries wolf on a healthy chain is worse than no verifier.
//
// The grid is still ARITHMETIC, never guessed per header (a vanity scan once picked a non-anchor on the
// testnet). Only the STEP is now measured on the chain itself: walk back from the tip to the two most recent
// headers that carry a certificate element, take their distance, and require it to be a multiple of 32 that
// divides the distance from ANCHOR_FROM. From there every sampled height is arithmetic, and a height on that
// grid WITHOUT a certificate is still a loud failure. A chain that stopped anchoring cannot produce two
// regularly spaced certificates, so it cannot buy itself a green run here: it fails to derive at all.
const INTERVAL_SCHEDULE = (process.env.ANCHOR_INTERVAL_SCHEDULE || '').split(',').filter(Boolean)
.map(p => p.split(':').map(x => Number(x.trim()))).sort((a, b) => a[0] - b[0]);
for (const [h, iv] of INTERVAL_SCHEDULE) {
if (!(iv > 0 && iv % 32 === 0) || h < ANCHOR_FROM || (h - ANCHOR_FROM) % iv !== 0) { console.error('FAIL: ANCHOR_INTERVAL_SCHEDULE entry ' + h + ':' + iv + ' is not a multiple of 32 on the grid from ' + ANCHOR_FROM); process.exit(1); }
}
// carries a certificate element? (RLP item 6 of extraData; enough to space the grid, not to trust it)
async function carriesCertificate(h) {
if (h < ANCHOR_FROM) return false;
const b = await rpc('eth_getBlockByNumber', ['0x' + h.toString(16), false]);
if (!b || !b.extraData) return false;
try { return rlpRead(hex(b.extraData), 0).item.length >= 6; } catch (e) { return false; }
}
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--) {
if (await carriesCertificate(h)) seen.push(h);
}
if (seen.length < 2) {
console.error('FAIL: only ' + seen.length + ' certificate-carrying header(s) in the ' + LOOKBACK + ' blocks below ' + top + ',');
console.error(' so the anchor spacing cannot be measured on this chain. Either anchoring has stopped, or this');
console.error(' endpoint is not serving chain ' + CHAIN_ID + '. Pass ANCHOR_INTERVAL_SCHEDULE=h:interval to state it explicitly.');
process.exit(1);
}
const step = seen[0] - seen[1];
if (!(step > 0 && step % 32 === 0) || (seen[0] - ANCHOR_FROM) % step !== 0) {
console.error('FAIL: the two most recent anchors (' + seen[0] + ', ' + seen[1] + ') are ' + step + ' apart, which is not a');
console.error(' multiple of 32 on the grid from ' + ANCHOR_FROM + '. The chain is not anchoring on a regular grid.');
process.exit(1);
}
DERIVED_INTERVAL = step;
INTERVAL_SCHEDULE.push([seen[0] - Math.floor((seen[0] - ANCHOR_FROM) / step) * step, step]);
}
const intervalAt = (h) => { let iv = 32; for (const [from, v] of INTERVAL_SCHEDULE) { if (from <= h) iv = v; else break; } return iv; };
const isAnchor = (h) => h >= ANCHOR_FROM && (h - ANCHOR_FROM) % intervalAt(h) === 0;
const prevAnchor = (h) => { let x = h - 1; while (x >= ANCHOR_FROM && !isAnchor(x)) x--; return x; };
let anchor = top; while (anchor >= ANCHOR_FROM && !isAnchor(anchor)) anchor--;
if (DERIVED_INTERVAL) {
console.log('anchor spacing MEASURED ON THIS CHAIN: every ' + DERIVED_INTERVAL + 'th block from ' + ANCHOR_FROM + ' (grid anchored at ' + INTERVAL_SCHEDULE[0][0] + ');');
console.log(' derived from the two most recent certificate-carrying headers, not from anything you have to take on trust.');
} else if (INTERVAL_SCHEDULE.length) {
console.log('interval schedule (given): ' + INTERVAL_SCHEDULE.map(([h, iv]) => h + ':' + iv).join(',') + ' (in force at ' + anchor + ': every ' + intervalAt(anchor) + 'th block)');
}
if (anchor < ANCHOR_FROM) { console.error('FAIL: no anchor at or below ' + top + ' (anchors start at ' + ANCHOR_FROM + ')'); process.exit(1); }
{
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)'); process.exit(1); }
}
const validatorsCache = new Map();
async function validatorsAt(h) {
const key = Math.floor(h / 1000);
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);
}
let ok = 0, bad = 0, sealsTotal = 0, sealsValid = 0;
// 2026-09-06: 'nu am putut verifica AICI' nu e 'ancora e invalida', si a fost raportat asa. Fara
// @noble/post-quantum instalat, o ancora v2 sanatoasa (9 Falcon + 9 SLH-DSA) iesea in lista drept FAIL si
// rezumatul spunea 'PROBLEMS: 20 of 20 anchors failed' - despre un lant perfect sanatos, catre un strain care
// 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;
// ce scheme au fost CHIAR verificate: rezumatul final nu are voie sa numeasca o schema pe care nu a vazut-o
const schemesSeen = new Set();
let cursor = anchor;
for (let i = 0; i < COUNT; i++) {
const h = cursor; if (i > 0 && h < ANCHOR_FROM) break;
cursor = prevAnchor(h);
const b = await rpc('eth_getBlockByNumber', ['0x' + h.toString(16), false]);
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);
if (items.length < 6) { 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));
// 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; }
const digest = isV2
? keccak(rlpEncode([Buffer.from(ANCHOR_DOMAIN_V2, 'ascii'), scalar(CHAIN_ID), scalar(h - 1), hex(parent.hash), rlpEncode(toNested(cert))]))
: keccak(rlpEncode([Buffer.from(ANCHOR_DOMAIN, 'ascii'), scalar(CHAIN_ID), scalar(h - 1), hex(parent.hash), toNested(cert)]));
const bound = digest.equals(vanity);
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 falconIdx = new Set();
const sealNodes = isV2 ? cert.item[1].item : cert.item;
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) });
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) {
sealsTotal++;
const name = SCHEME_NAMES[scheme];
if (!name) { unknownScheme.push(scheme); continue; }
const row = man && man.rows[idx];
if (!row) { unknownIndex.push(idx); continue; }
if (!vset.has(row.addr.toLowerCase())) { notValidator.push(idx); continue; }
if (scheme === 1) {
if (!verifyFalcon512(row.pk, msg, sig)) { invalid.push('falcon@' + idx); continue; }
} else {
if (!falconIdx.has(idx)) { unbound.push(name + '@' + idx); continue; }
const pk = row.keys && row.keys[name];
if (!pk) { noKey.push(name + '@' + idx); continue; }
if (!verifySlhDsa(pk, msg, sig)) { invalid.push(name + '@' + idx); continue; }
}
perScheme[name] = (perScheme[name] || 0) + 1;
valid++; sealsValid++;
}
const n = rowsOf.length;
const MIN_SEALS = minSealsAt(h);
const falconValid = perScheme['falcon-512'] || 0;
const shortSchemes = Object.entries(perScheme).filter(([, c]) => c < MIN_SEALS).map(([nm, c]) => nm + ':' + c);
// D-338: every scheme the schedule names at the parent's height must reach K on its own; a scheme with NO seal at
// all is named as missing, not silently skipped. When K is 0 at this height (a dated step) nothing is required.
const requiredSchemes = MIN_SEALS > 0 ? schemesAt(h - 1) : [];
const missingSchemes = requiredSchemes.filter(nm => !(perScheme[nm] > 0));
const schemesOk = falconValid >= MIN_SEALS && shortSchemes.length === 0 && missingSchemes.length === 0 && (!isV2 || slhDsa || !rowsOf.some(r => r.scheme === 2));
const good = bound && man && man.good && schemesOk && valid === n;
// 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)
&& 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;
const summary = Object.entries(perScheme).map(([nm, c]) => c + ' ' + nm).join(' + ');
if (good) { ok++; console.log(h + ' OK: bound under block hash' + (isV2 ? ' (v2, scheme-tagged)' : '') + ', ' + n + ' seals (' + summary + '), all valid and from validators of this height (manifest ' + man.bindHeight + ')'); }
else if (envBlocked) {
unmeasured++;
console.log(h + ' NOT VERIFIED HERE: bound under block hash, ' + falconValid + ' valid falcon-512 seals, but this'
+ ' machine has no SLH-DSA verifier, so the ' + rowsOf.filter(r => r.scheme === 2).length + ' slh-dsa seals were not checked'
+ ' (npm install @noble/post-quantum). This is a gap in THIS run, not a finding about the chain.');
}
else {
bad++;
console.log(h + ' FAIL:' + (bound ? '' : ' digest mismatch') + (man ? (man.good ? '' : ' manifest not self-consistent') : ' no manifest for this height') +
(falconValid < MIN_SEALS ? ' only ' + falconValid + ' valid Falcon seals (K=' + MIN_SEALS + ')' : '') + (shortSchemes.length ? ' short schemes ' + shortSchemes.join(',') : '') + (missingSchemes.length ? ' missing scheme ' + missingSchemes.join(',') + ' (required at ' + (h - 1) + ')' : '') +
(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(',') : '') +
(isV2 && !slhDsa && rowsOf.some(r => r.scheme === 2) ? ' (SLH-DSA verifier not installed: npm install @noble/post-quantum)' : ''));
}
}
console.log('');
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;
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.'
: (bad === 0 && manifestBad === 0
? 'UNMEASURED: ' + unmeasured + ' of ' + COUNT + ' anchors could not be fully checked ON THIS MACHINE because the SLH-DSA\n'
+ ' verifier is missing. Nothing here says the chain is wrong; install it and run again:\n'
+ ' npm install @noble/post-quantum'
: 'PROBLEMS: ' + bad + ' of ' + COUNT + ' anchors and ' + manifestBad + ' manifests failed - 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)' : '')));
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.
process.exit(all ? 0 : (bad === 0 && manifestBad === 0 ? 2 : 1));