diff --git a/anchor/README.md b/anchor/README.md index 2eb206d..43f7dd9 100644 --- a/anchor/README.md +++ b/anchor/README.md @@ -18,10 +18,11 @@ should not be confused: replacement: a rewrite of anchored history must also forge the post-quantum signatures. 2. **The enforced minimum.** Since 2026-08-14 a validator does not finalize an anchor block unless its certificate holds at least three valid Falcon-512 seals (f+1 of nine, the guarantee that at - least one honest validator signed; five were carried in practice under the write cap of five until 2026-08-16, then - eight or nine at nine validators, and nine or ten at ten validators, measured 2026-10-27). A node started from the + least one honest validator signed; up to five, most of them three to five, were carried under the write cap of five until + 2026-08-16 (one or two on some anchors before 2026-08-14), then eight or nine at nine validators, and eight to ten at ten + validators, measured 2026-10-27 over 400 anchors). A node started from the published package (`run-node.sh`) lowers that count to one seal, at the cost stated in `SPEC.md` - section 6.4 (corrected 2026-10-27: this item said "a node", and "eight or nine are carried in practice" in the present tense). + section 6.4 (corrected 2026-10-27: this item said "a node", and "eight or nine are carried in practice" in the present tense; then, the same day, "five were carried in practice" under the write cap and "nine or ten" at ten validators). Raised at block 14,961,456 (August 21, 2026): the enforced minimum became **six**, which at that time was as many as the QBFT quorum of the nine-validator set, the same count QBFT itself required to agree on the block. From 2026-09-11 the set was ten and QBFT's quorum seven, so the same six was above f+1 and diff --git a/tools/verify-anchor.mjs b/tools/verify-anchor.mjs index a62df28..26ca504 100644 --- a/tools/verify-anchor.mjs +++ b/tools/verify-anchor.mjs @@ -24,16 +24,17 @@ // 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. -// 6. PROPOSER (AIP-22 stage 5, 2026-09-24) on a chain where headers carry element 6, the proposer's +// 6. PROPOSER (AIP-22 stage 5, 2026-09-24) on a chain where headers carry element 6 (counting from 0), 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, or, in a block re-proposed in a round r > 0, the proposer QBFT expects for // round r (2026-09-25, D-406), on the most recent headers (PROPOSER_HEADERS, default 20); // and, as the nodes judge every header from H_hdr, the round is a canonical integer // at most 1024 and the coinbase is the proposer of some round 0..r at that height. -// Where no header carries it (chain 2800 today) the tool says so and this leg does not +// Where no header carries it (chain 2800 below its stage 5 height) the tool says so and this leg does not // change the verdict. -// 7. ROUND (1(b), D-470, learned 2026-10-07, active on no chain yet) from a chain's round height H_r the certificate is +// 7. ROUND (1(b), D-470, learned 2026-10-07; active on testnet 28001 since block 6,157,024 and on chain 2800 since +// block 24,148,752, 2026-10-24) from a chain's round height H_r the certificate is // v3, RLP[3, round, [[scheme, idx, sig], ...]] under "AERE-PQ-ANCHOR-3", and every seal in it is over // keccak256(RLP["AERE-PQ-COMMIT-2", chainId, parentNumber, round, parentHash]) for the ONE round it names, so a // seal made in another round, or over another block, does not verify. The tool dispatches on the certificate's @@ -68,7 +69,9 @@ // RPC=http://127.0.0.1:8545 node verify-anchor.mjs // ANCHOR_V2_BLOCK=17047600 node verify-anchor.mjs # the v2 height V (default: the chain's own, from the table; "none" = never) // -// Exit 0 only when every leg holds on every anchor checked. Any failure is the finding. +// Exit 0 only when every leg holds on every anchor checked. Any failure is the finding. Exit 2 (since 2026-10-27): UNMEASURED, a line +// that starts with "UNMEASURED:" says why: the endpoint serves another chain than CHAIN_ID or does not say which, or the manifests do +// not load or are of another chain; nothing about the chain was judged, and that is not a finding about it. import { keccak_256 } from '@noble/hashes/sha3'; import { sha256 } from '@noble/hashes/sha256'; @@ -113,7 +116,7 @@ const COMMIT_DOMAIN = 'AERE-PQ-COMMIT-1'; // v3; at an anchor at or above H_r only v3 is admitted, below H_r a v3 is refused, and every refusal of a v3 anchor names the code // both clients name (AERE-PQ-V3-01..08, in the fixed order of PROIECT-1B 1.4), so a stranger's verdict can be compared with theirs. // 2026-10-09: three margins of the v3 rule, each with its note where it applies: R2 in two passes (every Falcon row, then every extra -// row), a row index above 2^31-1 refused 03 at the decoding, and an element 6 that is present but is not a list (03 from H_r). +// row), a row index above 2^31-1 refused 03 at the decoding, and an element 5 (the certificate slot, counting from 0) that is present but is not a list (03 from H_r). // They follow the fixed order of the codes named above (R1: the form by height 01 / 02, then the decoding 03, then the digest 04; R2: the // Falcon rows, then the extra rows). This tool runs neither client: how a client binary answers these margins is not measured here. const COMMIT_DOMAIN_2 = 'AERE-PQ-COMMIT-2'; @@ -124,7 +127,11 @@ const MAX_V3_ROWS = 64; // for a chain it is written here, in the same hour, before H_r (D-372: the new verifier is published BEFORE the first v3 anchor). // COMMIT_ROUND_BLOCK overrides (a height, or "none"); cloud-gateway/orar-ancore.mjs carries the same value for the consumers. const COMMIT_ROUND_DEFAULT = CHAIN_ID === 28001 ? 6157024 : 24148752; -const COMMIT_ROUND_BLOCK = process.env.COMMIT_ROUND_BLOCK === undefined ? COMMIT_ROUND_DEFAULT +// 2026-10-27: the line above names 28001 and gives every OTHER id the H_r of 2800 (its form is read by the activation tools, so it stays +// as it is); a chain that is neither has no H_r, which both clients read as never, as V below for another chain. From 2026-10-21 until +// this date every third chain got 2800's H_r and, with no V, the tool refused to start ("COMMIT_ROUND_BLOCK=24148752 needs the v2 height +// below it"): found by the probes of this tool on synthetic chains (verify-anchor-ferestre-proba.mjs, ALT-LANT). +const COMMIT_ROUND_BLOCK = process.env.COMMIT_ROUND_BLOCK === undefined ? (CHAIN_ID === 28001 || CHAIN_ID === 2800 ? COMMIT_ROUND_DEFAULT : null) : (/^(none|unset|)$/i.test(process.env.COMMIT_ROUND_BLOCK) ? null : Number(process.env.COMMIT_ROUND_BLOCK)); if (COMMIT_ROUND_BLOCK !== null && !(Number.isInteger(COMMIT_ROUND_BLOCK) && COMMIT_ROUND_BLOCK > 0)) { console.error('FAIL: COMMIT_ROUND_BLOCK must be a positive block height or "none"'); stop(1); } // V per chain (2026-10-19, AIP-25 C5, D-539): aere.pq.anchorV2Block, the height from which every anchor below H_r carries the @@ -274,7 +281,7 @@ const quorumOf = (n) => Math.floor((2 * n + 2) / 3); // re-encode to the bytes received; PqAnchorV2.decode: version 2, at most 64 rows, scheme tag within a byte and known, index not negative), // the second client through PqAnchor.DecodeCertificate (v1: no leading zero byte, the lone 0x00 included, index at most int.MaxValue, // signature not empty) and PqAnchorV2.Decode (v2: exactly [2, rows], rows of exactly three scalars, the same scalar rules, known tags, -// signature not empty, the canonical round trip). `node` is element 6 as read by rlpRead, `raw` its bytes as carried. Returns +// signature not empty, the canonical round trip). `node` is element 5 (the certificate, counting from 0) as read by rlpRead, `raw` its bytes as carried. Returns // { rows: [{ scheme, idx, sig }] } or { why }, the first refusal in the order below. The ORDER of the rows is judged by the caller, as before. function decodeV1V2(node, isV2, raw) { const tn = (x) => (x.list ? x.item.map(tn) : Buffer.from(x.item)); @@ -330,11 +337,21 @@ async function loadText(loc) { const isUrl = /^https?:\/\//.test(MANIFESTS); const join = (a, b) => isUrl ? a.replace(/\/$/, '') + '/' + b : path.join(a, b); +// 2026-10-27 (R9-S2): the endpoint is only a courier, and a courier of ANOTHER chain says nothing about this one. Until this date the +// tool never asked which chain the endpoint serves: pointed at an endpoint of another chain, it measured that chain's grid and +// reported "FAIL: ... The chain is not anchoring on a regular grid" about a healthy chain (exit 1), which verify-proof.mjs turned into +// an INVALID envelope. Now the chain is read FIRST: another chain, or no answer, is UNMEASURED (exit 2), named, and nothing is judged. +// The same for inputs that are not about this chain: a manifest index of another chain, or none that loads, measures nothing. +let servedChain = null; +try { servedChain = Number(BigInt(await rpc('eth_chainId', []))); } +catch (e) { console.error('UNMEASURED: the endpoint ' + RPC + ' did not say which chain it serves (eth_chainId: ' + String(e && e.message).slice(0, 120) + '); nothing about chain ' + CHAIN_ID + ' was measured'); stop(2); } +if (servedChain !== CHAIN_ID) { console.error('UNMEASURED: the endpoint ' + RPC + ' serves chain ' + servedChain + ', not chain ' + CHAIN_ID + ' (CHAIN_ID); nothing about chain ' + CHAIN_ID + ' was measured. Point RPC at an endpoint of chain ' + CHAIN_ID + ', or set CHAIN_ID=' + servedChain + ' with the manifests of that chain.'); stop(2); } + 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); stop(1); } +catch (e) { console.error('UNMEASURED: cannot load the manifest index from ' + MANIFESTS + ': ' + e.message + '; nothing about chain ' + CHAIN_ID + ' was measured'); stop(2); } if (!index || !Array.isArray(index.manifests) || !index.manifests.length) { console.error('FAIL: manifest index has no entries'); stop(1); } -if (Number(index.chainId) !== CHAIN_ID) { console.error('FAIL: manifest index is for chain ' + index.chainId); stop(1); } +if (Number(index.chainId) !== CHAIN_ID) { console.error('UNMEASURED: the manifest index at ' + MANIFESTS + ' is for chain ' + index.chainId + ', not chain ' + CHAIN_ID + ' (CHAIN_ID); pass MANIFESTS=; nothing was measured'); stop(2); } console.log('chain ' + CHAIN_ID + ' rpc ' + RPC + ' manifests ' + MANIFESTS); console.log(''); @@ -640,13 +657,13 @@ for (let i = 0; i < COUNT; i++) { + ' is accepted as written; checked only that its ' + pairs.length + ' seal indices are distinct (' + (pairs.join(',') || 'no seals') + '). Not verified.'); continue; } - // 2026-10-09: an element 6 that is PRESENT but is not an RLP list (a byte string, or the empty string 0x80) is not "no certificate": + // 2026-10-09: an element 5 (the certificate slot, counting from 0; corrected 2026-10-27: this said element 6) that is PRESENT but is not an RLP list (a byte string, or the empty string 0x80) is not "no certificate": // it is a certificate slot that does not decode, so from H_r this tool refuses it with 03 (the decoding), not 02. Until this date it // was named 02 ("no certificate element"). Below H_r this tool names no v3 code for it: it stays a FAIL without a code, named for what // it is. Which code, if any, a client binary names for it is not measured here. if (items.length >= 6 && !items[5].list) { - if (certificateFormAt(h) === 3) console.log(h + ' FAIL: AERE-PQ-V3-03 (certificate v3): element 6 of extraData is present but is not an RLP list (' + items[5].item.length + ' byte string), so it does not decode as a certificate'); - else console.log(h + ' FAIL: element 6 of extraData (the certificate) is present but is not an RLP list (' + items[5].item.length + ' byte string)'); + if (certificateFormAt(h) === 3) console.log(h + ' FAIL: AERE-PQ-V3-03 (certificate v3): element 5 of extraData (counting from 0, the certificate slot) is present but is not an RLP list (' + items[5].item.length + ' byte string), so it does not decode as a certificate'); + else console.log(h + ' FAIL: element 5 of extraData (counting from 0, the certificate) is present but is not an RLP list (' + items[5].item.length + ' byte string)'); bad++; continue; } if (items.length < 6 || items[5].item.length === 0) { @@ -688,7 +705,7 @@ for (let i = 0; i < COUNT; i++) { // v1 / v2 it is the plain FAIL it always was. (2026-10-07, review of step 8: until then a v3 anchor failed here without a code.) if (!vset && !judgeV3) { 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; } if (judgeV3) { - // The bytes of element 6 as carried (element 6 starts where element 5 ends): D3 is over these bytes, and the canonical round trip + // The bytes of element 5 as carried (the certificate, counting from 0; it starts where element 4 ends): D3 is over these bytes, and the canonical round trip // below requires them to be exactly the re-encoding of what was read, so no second RLP form of the same certificate is admitted. const rawCert = ex.subarray(items[4].next, items[5].next); const v3 = { code: null, why: null, round: null, rows: [] };