diff --git a/CITATIONS-UNRESOLVED.md b/CITATIONS-UNRESOLVED.md index 36e6bec..a5cbe15 100644 --- a/CITATIONS-UNRESOLVED.md +++ b/CITATIONS-UNRESOLVED.md @@ -33,3 +33,5 @@ Unresolvable distinct paths in this repository: **18**. - `testnet-public/CITESTE-MA.md` cited in: aere-node/SPEC.md - `testnet-public/D-311-DOVADA-2026-09-02.md` cited in: aere-node/SPEC.md - `VERIFICARE-FORMALA-2026-08-24.md` cited in: aere-node/SPEC.md +- `nethermind-pqc/nethermind-intree/patches/osaka-antet-besu.sh` cited in: aere-node/SPEC.md (operator repository, 2026-09-10: the Osaka header patch for the second client and its gate; not part of the node package) +- `scripts/deschise/antetul-osaka-e-al-lantului.sh` cited in: aere-node/SPEC.md (operator repository, 2026-09-10: the Osaka header patch for the second client and its gate; not part of the node package) diff --git a/anchor/README.md b/anchor/README.md index 1bf6667..2b5939d 100644 --- a/anchor/README.md +++ b/anchor/README.md @@ -227,7 +227,7 @@ the pure-Java path made one anchor signature cost 1.4 to 2.7 seconds on the cons (measured 2026-09-04). Signatures are byte-identical to the original in deterministic mode, which `SlhDsaFastEngineTest` pins with cross-verification in both directions. The package is regenerated from the published Bouncy Castle sources by a script, not edited by hand, and carries the Bouncy -Castle licence next to it (`slhdsa/LICENSE-BouncyCastle.txt`). What is ours is the framing, the +Castle licence next to it (`consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/LICENSE-BouncyCastle.txt`). What is ours is the framing, the registry that maps a validator to its keys, the digest, the scheme schedule and the validation rules. --- diff --git a/tools/verify-anchor.mjs b/tools/verify-anchor.mjs new file mode 100644 index 0000000..336346b --- /dev/null +++ b/tools/verify-anchor.mjs @@ -0,0 +1,420 @@ +#!/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= 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));