Dated corrections (2026-10-27): anchor/README.md, the number of Falcon-512 seals an anchor carried in practice is stated with its measured ranges and dates (up to five, most three to five, under the write cap of five until 2026-08-16; eight or nine at nine validators; eight to ten at ten validators, over 400 anchors measured 2026-10-27). tools/verify-anchor.mjs: the ROUND leg names where it is active (testnet 28001 since block 6,157,024; chain 2800 since block 24,148,752), element 6 is counted from 0, and the tool exits 2 (UNMEASURED, with the reason) when the endpoint serves another chain than CHAIN_ID or does not say which. Correction to the message of 9820765 (2026-10-27): it also listed 'the follower's rewrite bound', which belonged to the previous round, and said 'three to five' where the text says 'three or more'.

This commit is contained in:
Aere Network 2026-10-28 00:04:54 +02:00
parent 9820765ebb
commit cdc523ea64
2 changed files with 34 additions and 16 deletions

View File

@ -18,10 +18,11 @@ should not be confused:
replacement: a rewrite of anchored history must also forge the post-quantum signatures. 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 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 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 least one honest validator signed; up to five, most of them three to five, were carried under the write cap of five until
eight or nine at nine validators, and nine or ten at ten validators, measured 2026-10-27). A node started from the 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` 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 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. 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 From 2026-09-11 the set was ten and QBFT's quorum seven, so the same six was above f+1 and

View File

@ -24,16 +24,17 @@
// 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 // 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]) // 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 // 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 // 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); // 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 // 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. // 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. // 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 // 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 // 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 // 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 // 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) // 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 { keccak_256 } from '@noble/hashes/sha3';
import { sha256 } from '@noble/hashes/sha256'; 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 // 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. // 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 // 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 // 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. // 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'; 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). // 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. // 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_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)); : (/^(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); } 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 // 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), // 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, // 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) 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. // { 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) { function decodeV1V2(node, isV2, raw) {
const tn = (x) => (x.list ? x.item.map(tn) : Buffer.from(x.item)); 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 isUrl = /^https?:\/\//.test(MANIFESTS);
const join = (a, b) => isUrl ? a.replace(/\/$/, '') + '/' + b : path.join(a, b); 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; let index;
try { index = JSON.parse(await loadText(join(MANIFESTS, 'index.json'))); } 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 (!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=<the manifests of chain ' + CHAIN_ID + '>; nothing was measured'); stop(2); }
console.log('chain ' + CHAIN_ID + ' rpc ' + RPC + ' manifests ' + MANIFESTS); console.log('chain ' + CHAIN_ID + ' rpc ' + RPC + ' manifests ' + MANIFESTS);
console.log(''); 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.'); + ' is accepted as written; checked only that its ' + pairs.length + ' seal indices are distinct (' + (pairs.join(',') || 'no seals') + '). Not verified.');
continue; 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 // 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 // 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. // it is. Which code, if any, a client binary names for it is not measured here.
if (items.length >= 6 && !items[5].list) { 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'); 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 6 of extraData (the certificate) is present but is not an RLP list (' + items[5].item.length + ' byte string)'); 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; bad++; continue;
} }
if (items.length < 6 || items[5].item.length === 0) { 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.) // 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 (!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) { 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. // 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 rawCert = ex.subarray(items[4].next, items[5].next);
const v3 = { code: null, why: null, round: null, rows: [] }; const v3 = { code: null, why: null, round: null, rows: [] };