Dated corrections (2026-10-28): SPEC.md, SECURITY.md, AUDITS.md and anchor/README.md state what an anchor certificate is bound by (its digest sits in the hashed part of the header, so a node that validates the header refuses a detached or replaced certificate, while the block hash does not change); tools/verify-proof.mjs and tools/verify-anchor.mjs are the same files as those served at aere.network/tools
This commit is contained in:
parent
cdc523ea64
commit
521210c71b
10
AUDITS.md
10
AUDITS.md
@ -4,7 +4,7 @@
|
||||
purpose, and it stays here until it is no longer true. Everything below describes what we have
|
||||
verified ourselves, which is a weaker thing, and we do not want the difference blurred.
|
||||
|
||||
Last updated: 2026-09-01.
|
||||
Last updated: 2026-10-28 (the sentence on the block hash and the certificate corrected that day).
|
||||
|
||||
---
|
||||
|
||||
@ -12,7 +12,7 @@ Last updated: 2026-09-01.
|
||||
|
||||
| scope | auditor | date | report |
|
||||
|---|---|---|---|
|
||||
| — | none | — | — |
|
||||
| none | none | none | none |
|
||||
|
||||
There is nothing to put in this table yet. When there is, the full report goes here, including any
|
||||
findings we chose not to fix and why.
|
||||
@ -40,9 +40,11 @@ findings we chose not to fix and why.
|
||||
The post-quantum anchor is designed so you do not have to take our word for it: from block
|
||||
13,014,000 every 32nd block (every 128th from block 17,225,968 to block 20,746,736) carries a
|
||||
post-quantum certificate (Falcon-512, and from block 17,047,600 also SLH-DSA-SHA2-128s, at every
|
||||
128th block since block 20,746,736), and the block hash covers it. Since block 20,715,632 that
|
||||
128th block since block 20,746,736), and the block hash covers its 32-byte digest: the certificate itself is not under
|
||||
the hash, and a swapped or stripped one no longer matches the digest, so a node that validates refuses the anchor (SPEC.md
|
||||
2.3; corrected 2026-10-28: this said the block hash covers the certificate). Since block 20,715,632 that
|
||||
certificate requires at least seven valid seals of each scheme, as many as the quorum of the ten validators (six from block 14,961,456, six of nine validators then); below block 24,148,752 the seals proved those validators signed the block, not that it was decided (AIP-22 erratum, 2026-10-01), and from that block a certificate that carries the quorum of one round proves the decision. The verifier is
|
||||
public, the signatures are in the headers, and the check runs against any node — ours or your own.
|
||||
public, the signatures are in the headers, and the check runs against any node, ours or your own.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
|
||||
@ -38,10 +38,10 @@ pointer is <https://aere.network/.well-known/security.txt>.
|
||||
every 32nd block (every 128th from block 17,225,968 to block 20,746,736) carries a post-quantum
|
||||
certificate under the block hash (Falcon-512, and from block 17,047,600 also SLH-DSA-SHA2-128s, at
|
||||
every 128th block since block 20,746,736), requiring at least seven valid seals of each scheme
|
||||
since block 20,715,632, as many as the quorum of the ten validators (six from block 14,961,456, six of nine validators then); below block 24,148,752 the seals proved those validators signed the block, not that it was decided (AIP-22 erratum, 2026-10-01), and from that block a certificate that carries the quorum of one round proves the decision. Blocks between anchors rest on ECDSA. We correct
|
||||
since block 20,715,632, as many as the quorum of the ten validators (six from block 14,961,456, six of nine validators then); below block 24,148,752 the seals proved those validators signed the block, not that it was decided (AIP-22 erratum, 2026-10-01), and from that block a certificate that carries the quorum of one round proves the decision. Blocks between anchors carry no certificate of their own and are decided by QBFT, whose votes every validator attributes, since block 24,300,000 (AIP-22 stage 4), only through their verified post-quantum seals (headers are still admitted on their ECDSA commit seals until AIP-22 stage 5); between anchors a node that does not take part in consensus checks only their headers (this sentence corrected on 2026-10-28: it said that blocks between anchors rest on ECDSA). We correct
|
||||
over-claims publicly when we find them, and we have done so before.
|
||||
|
||||
## Fixes
|
||||
|
||||
When a report leads to a fix, we publish the finding, the fix, and the window during which the
|
||||
issue was live — including anything we chose not to fix, and why.
|
||||
issue was live, including anything we chose not to fix, and why.
|
||||
|
||||
5
SPEC.md
5
SPEC.md
@ -580,8 +580,9 @@ Correction added 2026-10-07: the sentence above ("protected by ECDSA alone, perm
|
||||
than what holds for a node with a reference point and, for a node without one, an overstatement:
|
||||
ECDSA is exactly what this adversary holds. History below block 13,014,000 carries no certificates
|
||||
of its own and the anchor cannot reach backwards; what covers it is the hash chain. Every block hash
|
||||
covers its parent's hash, and an anchor's hash covers its certificate, whose seals sign the parent's
|
||||
hash, so a rewrite at ANY height changes every hash above it. A node that holds, from outside the
|
||||
covers its parent's hash, and an anchor's hash covers the 32-byte digest of its certificate (section
|
||||
2.3; this clause corrected on 2026-10-28: it said the hash covers the certificate), whose seals sign
|
||||
the parent's hash, so a rewrite at ANY height changes every hash above it. A node that holds, from outside the
|
||||
sync, the height and block hash of one recent honest anchor (a reference point) therefore refuses
|
||||
every history that diverges at or below that anchor, since no such history reaches the reference
|
||||
with the same hash; above the reference the bound of this section applies (at most 32 blocks, the
|
||||
|
||||
@ -85,8 +85,10 @@ section) (Note added 2026-10-26: from block 24,148,752, AIP-22 stage R, a certif
|
||||
The threat it addresses is not "harvest now, decrypt later". A signature is public; there is nothing
|
||||
to harvest. The threat is **retroactive rewriting**: validator keys recovered later can be used to
|
||||
re-sign old blocks, and a chain whose history is authenticated only by ECDSA cannot distinguish the
|
||||
rewrite from the original. Binding a post-quantum certificate into the hash of every anchored header
|
||||
means a rewrite must also forge the post-quantum signatures.
|
||||
rewrite from the original. Binding the 32-byte digest of a post-quantum certificate into the hash of
|
||||
every anchored header, so that a node that validates refuses a certificate that does not match it,
|
||||
means a rewrite must also forge the post-quantum signatures (corrected 2026-10-28: this said the
|
||||
certificate itself is bound into the hash; it is its digest, SPEC.md 2.3).
|
||||
|
||||
The threat is not ours to claim. It is the long-range attack, and the peer-reviewed treatment is
|
||||
Azouvi, Danezis and Nikolaenko, "Winkle: Foiling Long-Range Attacks in Proof-of-Stake Systems"
|
||||
@ -166,7 +168,7 @@ now pinned: change one byte of it and the digest no longer matches, and the head
|
||||
|
||||
Updated 2026-08-29. The first publication of this directory, on 2026-08-11, was staged by hand and
|
||||
was never refreshed: by 29 August, 63 of the 78 files here had changed in the tree we actually run
|
||||
and 50 files had never been published at all. That is fixed at the root rather than by one copy —
|
||||
and 50 files had never been published at all. That is fixed at the root rather than by one copy:
|
||||
`aerenew/publish-bundle/stage-anchor.cjs` in the operator repository derives this directory from the authoritative overlay, refuses
|
||||
to run if the two files this README tells you to read first are missing, deletes what we removed
|
||||
rather than letting the published set grow monotonically, and stops on the secret-scanner's verdict
|
||||
@ -185,7 +187,7 @@ This matters more than the file list, so it is stated before it.
|
||||
|---|---|---|
|
||||
| Anchor certificate under the block hash | yes | **yes**, since block 13,014,000 |
|
||||
| Enforced minimum seals at an anchor height | yes | **yes**, 7 of 10 per scheme since 20,715,632 (6 from 14,961,456) |
|
||||
| Legacy per-block Falcon rule (`aere.falcon.forkBlock`) | yes | **no** — retired at the anchor block |
|
||||
| Legacy per-block Falcon rule (`aere.falcon.forkBlock`) | yes | **no**, retired at the anchor block |
|
||||
| Post-quantum seal on PREPARE, emission | yes | **yes**, since block 16,690,000 |
|
||||
| Post-quantum seal on PREPARE, enforcement | yes | **armed for block 17,700,000** (set 2026-09-05) |
|
||||
| Post-quantum seal on the PROPOSAL, emission | yes | **yes**, since block 16,620,000 |
|
||||
@ -205,7 +207,7 @@ This matters more than the file list, so it is stated before it.
|
||||
|
||||
Everything not yet in force is **disarmed by absence or by height, never by a flag**: the properties that arm them
|
||||
are unset, and unset means never. Each refuses loudly on a value it cannot parse rather than booting
|
||||
a node that believes itself armed — a node that disarms itself because of a mistyped character looks
|
||||
a node that believes itself armed: a node that disarms itself because of a mistyped character looks
|
||||
exactly like a correctly configured one, right up to the day it matters. The tests for that
|
||||
behaviour are in this directory and they are the ones to read if you doubt the claim.
|
||||
|
||||
@ -223,20 +225,20 @@ proposal without a valid Falcon seal from its own proposer does not open a round
|
||||
its own domain over (chainId, height, round, digest), so an honest proposal seal cannot be replayed
|
||||
as a vote nor a vote seal as a proposal - both directions are tested. The ROUND-CHANGE message
|
||||
carries its own seal since 2026-08-31 (its embedded justifications are coupled to PREPARE
|
||||
enforcement). What remains classical on chain 2800: node-level devp2p authentication, the ECDSA commit seals that every header still carries until AIP-22 stage 5, and the ECDSA commit seal that a commit carries besides its post-quantum seal. The validators are named in
|
||||
enforcement). What remains classical on chain 2800: node-level devp2p authentication, the ECDSA commit seals that every header still carries until AIP-22 stage 5, the ECDSA commit seal that a commit carries besides its post-quantum seal, and the ECDSA envelope that every consensus message still carries on the wire, whose form both clients judge but which, since block 24,300,000 (AIP-22 stage 4), no longer names its author (this sentence corrected on 2026-10-28: it did not name the envelope; AIP-22, note of 2026-10-27 under section 5). The validators are named in
|
||||
consensus by the post-quantum ids of AIP-22 since block 20,836,228 (stage 3, 2026-09-30); that code is in this directory.
|
||||
(Corrected 2026-10-22: until that date this sentence also listed validator identity as classical and said that the
|
||||
post-quantum identity was unarmed on chain 2800 and ran only on testnet 28001.)
|
||||
|
||||
### The files
|
||||
|
||||
- `consensus/common/.../bft/` — the anchor itself: configuration, the digest, the seal cache and
|
||||
- `consensus/common/.../bft/`: the anchor itself: configuration, the digest, the seal cache and
|
||||
store, the producer that attaches seals, the Falcon registry that maps a validator to a key.
|
||||
- `consensus/qbft/.../headervalidationrules/` — the validation rules: the digest must match, the
|
||||
- `consensus/qbft/.../headervalidationrules/`: the validation rules: the digest must match, the
|
||||
seals must verify, and the rules must actually be wired into the validator chain.
|
||||
- `app/.../controller/` — where the rules are built and where the node refuses to start on a
|
||||
- `app/.../controller/`: where the rules are built and where the node refuses to start on a
|
||||
configuration that would produce headers its own fleet rejects.
|
||||
- tests — including the negative controls. A test that cannot fail is not a test, and several of
|
||||
- tests, including the negative controls. A test that cannot fail is not a test, and several of
|
||||
these exist specifically to prove the guards can fail.
|
||||
|
||||
`BASE.txt` names the upstream commit. Applying these files to any other tree overwrites whatever
|
||||
|
||||
@ -60,18 +60,26 @@
|
||||
// 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
|
||||
// Run it (corrected 2026-10-28: this said `npm install @noble/hashes @noble/curves`, with no versions; npm installs the 2.x releases
|
||||
// today, which no longer export the subpaths this tool imports, so the tool stopped at its first import with exit 1 and an
|
||||
// ERR_PACKAGE_PATH_NOT_EXPORTED line, before reading anything from the chain: that exit is not a verdict about the chain):
|
||||
// npm install # with the package.json published beside this file (aere.network/tools/package.json)
|
||||
// npm install @noble/hashes@1 @noble/curves@1 @noble/post-quantum@0.7 # or the same versions by hand
|
||||
// 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
|
||||
// node verify-anchor.mjs 13921040 # 20 anchors ending at a chosen height (the on-chain pin is proven under the state of
|
||||
// # that height, which needs an endpoint that keeps it, an archive node; on the public
|
||||
// # endpoints the pin is UNMEASURED and the exit is 2, the anchors still judged)
|
||||
// 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
|
||||
// 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 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.
|
||||
// Exit 0 only when every leg holds on every anchor checked. Any failure is the finding (exit 1). Exit 2: UNMEASURED, a line that
|
||||
// starts with "UNMEASURED:" or "NOT VERIFIED" says why, and it is not a finding about the chain: an on-chain pin that cannot be
|
||||
// proven on this endpoint (one that does not keep the state of that block: a historical height needs an archive endpoint, and the
|
||||
// anchors themselves can still have been judged OK), no anchor verified at all, or, since 2026-10-27, an endpoint of another chain
|
||||
// than CHAIN_ID or one that does not say which, or manifests that do not load or are of another chain, and then nothing about the
|
||||
// chain was judged (corrected 2026-10-28: this said exit 2 exists since 2026-10-27 and named only those last reasons).
|
||||
|
||||
import { keccak_256 } from '@noble/hashes/sha3';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
@ -123,8 +131,10 @@ const COMMIT_DOMAIN_2 = 'AERE-PQ-COMMIT-2';
|
||||
const ANCHOR_DOMAIN_V3 = 'AERE-PQ-ANCHOR-3';
|
||||
const MAX_ROUND = 1024; // PqAnchorV3.MAX_ROUND in both clients; no QBFT round timer gets there
|
||||
const MAX_V3_ROWS = 64;
|
||||
// H_r per chain: UNSET on both chains at the time of writing (no v3 anchor exists on any live chain). When the fleet publishes H_r
|
||||
// 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).
|
||||
// H_r per chain: set on both chains, testnet 28001 from block 6,157,024 and chain 2800 from block 24,148,752 (reached on 2026-10-24);
|
||||
// when this line was first written neither was set and no v3 anchor existed on any live chain (corrected 2026-10-28: it still said
|
||||
// UNSET). When the fleet sets H_r for a chain it is written here, in the same hour, before H_r (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;
|
||||
// 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
|
||||
|
||||
@ -19,10 +19,25 @@
|
||||
// manifests); the header of the anchor's parent is re-encoded and must hash to the parent hash those seals signed;
|
||||
// that header's state root then anchors a Merkle proof (eth_getProof) that the AereNotary contract (its code hash
|
||||
// pinned below) holds this statementHash, and the time it was first seen. The endpoint is only a courier: a header,
|
||||
// a proof or a value it altered would not match the certified hash.
|
||||
// a proof or a value it altered would not match the certified hash. A courier of another chain (its eth_chainId is
|
||||
// not the chain checked) or one that does not say which chain it serves is UNMEASURED, by name (since 2026-10-27;
|
||||
// until then it made the anchor verifier accuse a healthy chain and this level FAILED)
|
||||
// 5 witness (2026-10-18, the Proof line: a Bitcoin witness for every notarization) present when the envelope carries
|
||||
// `bitcoinWitness` (written by tools/aere-quantum-shield/martor-notar-dovada.mjs), or when the file IS such a witness
|
||||
// (format aere-notar-martor-1): the leaf (this statementHash as the chain recorded its notarization) hashes as recorded,
|
||||
// its Merkle inclusion proof leads to the lot's root, the .ots bytes are an OpenTimestamps proof FOR that root, and the
|
||||
// proof's restricted evaluation leads to the Merkle root of a Bitcoin block. Offline the level is UNMEASURED
|
||||
// (ots-header-not-chain-checked: nothing dates anything without a header); PASSED needs --btc-antet <80-byte header of
|
||||
// that block> or --live (two public explorers that must agree). What PASSED proves: the notarization existed no later
|
||||
// than that Bitcoin block's time. The judge is martor-notar-verifica.mjs, looked for in AERE_MARTOR_VERIFICA, beside
|
||||
// this file, then ../tools/aere-quantum-shield/; when it is not here the level is UNMEASURED and names `ots verify`.
|
||||
//
|
||||
// Usage: node verify-proof.mjs <envelope.json> [--rpc <url>] [--chain 2800|28001] [--notary <address>] [--manifests <url|dir>] [--json]
|
||||
// Usage: node verify-proof.mjs <envelope.json> [--rpc <url>] [--chain 2800|28001] [--notary <address>] [--manifests <url|dir>]
|
||||
// [--live] [--btc-antet <80-byte hex>] [--offline] [--json]
|
||||
// Exit: 0 every level present passed; 1 a level present failed; 2 a level present could not be measured here (or is pending).
|
||||
// --offline (2026-10-18): nothing leaves this machine. Form, integrity and signature are judged as always; a notarization the
|
||||
// envelope claims is reported UNMEASURED by name (never dropped, never passed); the Bitcoin witness is judged as far as its local
|
||||
// judge can without a header. This is the mode the browser page runs (deploy/website/verify.html bundles judge() below).
|
||||
//
|
||||
// The notary stores the block TIME of the first sighting (block.timestamp), not a block number. The block that holds the first
|
||||
// sighting is located through the endpoint's logs and reported as such (it is not part of what is certified).
|
||||
@ -59,7 +74,7 @@ export function rlpEncode(x) {
|
||||
return Buffer.concat([encLen(b.length, 0x80), b]);
|
||||
}
|
||||
const scalar = (h) => { const n = BigInt(h); return n === 0n ? Buffer.alloc(0) : hexb(n.toString(16)); };
|
||||
/** the hash the chain gives a header: QBFT strips the committed seals and zeroes the round before hashing (CLAUDE.md s.2) */
|
||||
/** the hash the chain gives a header: QBFT strips the committed seals and zeroes the round before hashing (aere-node SPEC.md 2.3) */
|
||||
export function onchainHash(b) {
|
||||
const e = rlpDecode(hexb(b.extraData));
|
||||
const extra = rlpEncode([e[0], e[1], e[2], Buffer.alloc(0), []]);
|
||||
@ -80,7 +95,7 @@ export { rlpDecode, mptGet };
|
||||
const level = (name, state, detail, extra) => ({ level: name, state, detail, ...(extra || {}) });
|
||||
|
||||
// ---------- levels 1 and 2 ----------
|
||||
function local(env, notJson = null) {
|
||||
export function local(env, notJson = null) {
|
||||
const out = [];
|
||||
const obj = (x) => !!x && typeof x === 'object' && !Array.isArray(x);
|
||||
const formOk = obj(env) && obj(env.statement)
|
||||
@ -102,7 +117,7 @@ function local(env, notJson = null) {
|
||||
/** does this Node.js verify ML-DSA at all (OpenSSL 3.5)? asked once, by making a key: the answer is the runtime's, not a version string's */
|
||||
let mlDsaHere = null;
|
||||
const canMlDsa = () => { if (mlDsaHere === null) { try { crypto.generateKeyPairSync('ml-dsa-44'); mlDsaHere = true; } catch { mlDsaHere = false; } } return mlDsaHere; };
|
||||
function signature(env, text) {
|
||||
export function signature(env, text) {
|
||||
if (env.signature === undefined || env.signature === null) return level('signature', 'ABSENT', 'the envelope carries no signature');
|
||||
const s = env.signature;
|
||||
if (typeof s !== 'object' || Array.isArray(s)) return level('signature', 'FAILED', 'the signature field is not an object {scheme, publicKey, signature}');
|
||||
@ -130,20 +145,41 @@ function signature(env, text) {
|
||||
// ---------- level 4 ----------
|
||||
export async function finality(env, o) {
|
||||
if (!o.rpcGiven && !env.notarization) return level('finality', 'ABSENT', 'no notarization claimed and no --rpc given');
|
||||
// offline (2026-10-18): a claim is a claim; it is reported as not measured here, by name, never dropped and never passed
|
||||
if (o.offline) return level('finality', 'UNMEASURED', `offline: the notarization this envelope claims (chain ${(env.notarization && env.notarization.chainId) || o.chain}) was not checked; this level needs the chain: run the reference verifier with the network (node verify-proof.mjs <file>)`);
|
||||
const rpc = async (method, params) => {
|
||||
const r = await fetch(o.rpc, { method: 'POST', headers: { 'content-type': 'application/json', 'user-agent': 'aere-verify-proof/1' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
|
||||
const j = await r.json(); if (j.error) throw new Error(method + ': ' + (j.error.message || JSON.stringify(j.error))); return j.result;
|
||||
};
|
||||
// (0) 2026-10-27 (R9-S2): which chain the endpoint serves, read before anything else. A courier of ANOTHER chain says nothing about
|
||||
// chain o.chain: the level is UNMEASURED, named, never FAILED. Until this date the question was not asked, verify-anchor.mjs measured
|
||||
// the other chain's grid and reported a healthy chain as "not anchoring on a regular grid", and this level FAILED: the envelope
|
||||
// INVALID, on an endpoint that had only been given for the wrong chain.
|
||||
let served;
|
||||
try { served = Number(BigInt(await rpc('eth_chainId', []))); }
|
||||
catch (e) { return level('finality', 'UNMEASURED', `the endpoint ${o.rpc} did not say which chain it serves (eth_chainId: ${String(e && e.message).slice(0, 100)}); nothing about chain ${o.chain} was measured`); }
|
||||
if (served !== o.chain) return level('finality', 'UNMEASURED', `the endpoint ${o.rpc} serves chain ${served}, not chain ${o.chain}: nothing about chain ${o.chain} was measured there (give --rpc an endpoint of chain ${o.chain})`);
|
||||
// (a) the chain's most recent anchor, verified by verify-anchor.mjs, which reports the parent hash its seals signed
|
||||
const report = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aere-proof-')), 'anchor.json');
|
||||
const va = await new Promise((ok) => {
|
||||
// the report lives in a temporary directory that is removed on every path once it is read (2026-10-21: until then each online
|
||||
// finality check left one aere-proof-* directory behind; 296 of them after one stranger's pass)
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-proof-'));
|
||||
const report = path.join(tmpDir, 'anchor.json');
|
||||
let va, rep = null;
|
||||
try {
|
||||
va = await new Promise((ok) => {
|
||||
const c = spawn(process.execPath, [path.join(HERE, 'verify-anchor.mjs')], { cwd: HERE,
|
||||
env: { ...process.env, RPC: o.rpc, CHAIN_ID: String(o.chain), MANIFESTS: o.manifests, ANCHORS: '1', PROPOSER_HEADERS: '2', ANCHOR_REPORT: report } });
|
||||
let out = ''; c.stdout.on('data', (d) => { out += d; }); c.stderr.on('data', (d) => { out += d; });
|
||||
c.on('error', (e) => ok({ code: null, out: String(e.message) })); c.on('close', (code) => ok({ code, out }));
|
||||
});
|
||||
let rep = null; try { rep = JSON.parse(fs.readFileSync(report, 'utf8')); } catch { rep = null; }
|
||||
const tail = va.out.trim().split('\n').slice(-2).join(' | ').slice(0, 220);
|
||||
try { rep = JSON.parse(fs.readFileSync(report, 'utf8')); } catch { rep = null; }
|
||||
} finally { fs.rmSync(tmpDir, { recursive: true, force: true }); }
|
||||
// the last two MESSAGES of verify-anchor.mjs: a line that starts with blanks continues the one before it (its FAIL lines wrap), so it is
|
||||
// joined to it with a space, and only distinct messages are separated by ' | ' (2026-10-27, R9-S2: until then a wrapped sentence was
|
||||
// cut in the middle, "which is not a | multiple of 32")
|
||||
const mesaje = [];
|
||||
for (const l of va.out.replace(/\r/g, '').split('\n')) { if (!l.trim()) continue; if (/^\s/.test(l) && mesaje.length) mesaje[mesaje.length - 1] += ' ' + l.trim(); else mesaje.push(l.trim()); }
|
||||
const tail = mesaje.slice(-2).join(' | ').slice(0, 220);
|
||||
// exit 1 is a verdict only with verify-anchor's own verdict line: Node also exits 1 when the tool cannot start (a dependency not
|
||||
// installed: ERR_MODULE_NOT_FOUND), and that is not a failure of the chain (2026-09-29)
|
||||
const verdictLine = /^(FAIL:|PROBLEMS:)/m.test(va.out);
|
||||
@ -192,6 +228,65 @@ export async function finality(env, o) {
|
||||
}
|
||||
|
||||
// ---------- main ----------
|
||||
// ---------- level 5: the Bitcoin witness (martor-notar-verifica.mjs, one judge, never a second copy of it here) ----------
|
||||
export const FORMAT_MARTOR = 'aere-notar-martor-1';
|
||||
async function modulMartor() {
|
||||
const candidati = [process.env.AERE_MARTOR_VERIFICA, path.join(HERE, 'martor-notar-verifica.mjs'), path.join(HERE, '..', 'tools', 'aere-quantum-shield', 'martor-notar-verifica.mjs')].filter(Boolean);
|
||||
for (const c of candidati) { if (!fs.existsSync(c)) continue; try { const m = await import(pathToFileURL(c).href); if (typeof m.verificaMartor === 'function') return { m, cale: c }; } catch (e) { return { eroare: `${c}: ${String(e.message).slice(0, 120)}` }; } }
|
||||
return { lipsa: candidati };
|
||||
}
|
||||
export async function witness(w, statementHash, o) {
|
||||
const mod = await modulMartor();
|
||||
if (mod.eroare) return level('witness', 'UNMEASURED', 'the Bitcoin witness judge does not load here: ' + mod.eroare);
|
||||
if (!mod.m) return level('witness', 'UNMEASURED', `the Bitcoin witness judge (martor-notar-verifica.mjs) is not beside this verifier (looked in AERE_MARTOR_VERIFICA, ${HERE}, ../tools/aere-quantum-shield); the lot root can still be checked with the OpenTimestamps client: ots verify -d <rootHash without 0x> <lot .ots> (martor-notar-dovada.mjs --ots-separat writes it)`);
|
||||
let r; try { r = await mod.m.verificaMartor(w, { statementHash, live: !!o.live, btcAntet: o.btcAntet || null }); }
|
||||
catch (e) { return level('witness', 'UNMEASURED', 'the Bitcoin witness judge stopped: ' + String(e && e.message).slice(0, 160)); }
|
||||
return level('witness', r.state, (r.reason ? r.reason + ': ' : '') + r.detail, { reason: r.reason || null, bitcoin: r.bitcoin || null });
|
||||
}
|
||||
|
||||
// ---------- the judgement, one function ----------
|
||||
// 2026-10-18 (B12, the Proof line): the whole judgement moved out of main() into judge(), exported, so that the browser page
|
||||
// (deploy/website/verify.html, built by construieste-verificatorul-browser.mjs) runs THIS code, bundled, and not a second copy of
|
||||
// it that could diverge. main() is the command line around it and prints exactly what it printed before.
|
||||
/** a standalone witness file (format aere-notar-martor-1) is not an envelope: it is judged on its own level, and, since it names a
|
||||
* notarization, on finality for the hash it witnesses (the chain's certified state must hold that hash, or the witness covers nothing) */
|
||||
export const witnessFile = (env) => !!(env && typeof env === 'object' && !Array.isArray(env) && env.format === FORMAT_MARTOR && env.leaf && typeof env.leaf === 'object' && typeof env.leaf.hash === 'string');
|
||||
/** the options of one judgement. The chain: opt.chain if given; otherwise the chain the envelope's notarization declares, when it is a
|
||||
* known chain; otherwise 2800. (Before 2026-09-27 an envelope notarized on 28001, checked without --chain, was looked up on 2800 and
|
||||
* reported INVALID.) The notary stays the known contract of that chain (or opt.notary): an address written in the envelope is never
|
||||
* trusted as the notary. `offline`: nothing leaves this machine; the levels that need the chain are reported UNMEASURED by name. */
|
||||
export function options(env, opt = {}) {
|
||||
const declared = env && typeof env === 'object' && (witnessFile(env) ? Number(env.chainId) : env.notarization && Number(env.notarization.chainId));
|
||||
const chain = Number(opt.chain || (declared && NOTARY[declared] ? declared : 2800));
|
||||
if (!NOTARY[chain]) throw new Error('unknown chain ' + chain + ' (2800 or 28001)');
|
||||
if (opt.offline && (opt.rpc || opt.live)) throw new Error('--offline excludes --rpc and --live: offline, nothing is read from the chain or from Bitcoin');
|
||||
return { chain, rpcGiven: !!opt.rpc, rpc: opt.rpc || RPC_DEFAULT[chain], notary: (opt.notary || NOTARY[chain]).toLowerCase(),
|
||||
manifests: opt.manifests || MANIFESTS_DEFAULT[chain], live: !!opt.live, btcAntet: opt.btcAntet || null, offline: !!opt.offline };
|
||||
}
|
||||
/** every level present, the verdict, and the exit code main() would give: 0 all passed, 1 a level failed, 2 one could not be measured */
|
||||
export async function judge(env, o, notJson = null) {
|
||||
const martorSingur = witnessFile(env);
|
||||
let levels;
|
||||
if (martorSingur) {
|
||||
levels = [await witness(env, env.leaf.hash, o)];
|
||||
levels.push(await finality({ statementHash: String(env.leaf.hash), notarization: { chainId: o.chain, txHash: env.leaf.txHash } }, o));
|
||||
} else {
|
||||
const l = local(env, notJson);
|
||||
levels = [...l.out];
|
||||
if (l.text && env && typeof env === 'object') { levels.push(signature(env, l.text)); levels.push(await finality(env, o)); if (env.bitcoinWitness !== undefined) levels.push(await witness(env.bitcoinWitness, env.statementHash, o)); }
|
||||
}
|
||||
const failed = levels.some((x) => x.state === 'FAILED');
|
||||
const unmeasured = levels.some((x) => x.state === 'UNMEASURED' || x.state === 'PENDING');
|
||||
const verdict = failed ? 'INVALID' : unmeasured ? 'PARTIAL' : 'VALID';
|
||||
const shown = martorSingur ? String(env.leaf.hash) : env && typeof env === 'object' && typeof env.statementHash === 'string' ? env.statementHash : null;
|
||||
return { verdict, statementHash: shown, levels, exitCode: failed ? 1 : unmeasured ? 2 : 0 };
|
||||
}
|
||||
/** the text of a file, as an envelope: JSON (a leading byte order mark is skipped), or `notJson` naming why it is not */
|
||||
export function parseEnvelope(raw) {
|
||||
try { return { env: JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw), notJson: null }; }
|
||||
catch (e) { return { env: undefined, notJson: 'the file is not JSON (' + String(e.message).split('\n')[0].slice(0, 80) + ')' }; }
|
||||
}
|
||||
|
||||
// 2026-10-02 (AIP-24, L1): the finality path and its helpers are exported so that verify-shield.mjs (tools/aere-quantum-shield)
|
||||
// judges a period head H_n through the SAME function, on a live endpoint or on a captured package replayed locally, instead of a
|
||||
// second copy of this logic. Run as a program, this file behaves exactly as before; imported, it runs nothing.
|
||||
@ -199,31 +294,20 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const opt = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : null; };
|
||||
const file = args.find((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--') && args[i - 1] !== '--json'));
|
||||
if (!file) { console.error('usage: node verify-proof.mjs <envelope.json> [--rpc <url>] [--chain 2800|28001] [--notary <address>] [--manifests <url|dir>] [--json]'); stop(2); }
|
||||
const FARA_VALOARE = new Set(['--json', '--live', '--offline']);
|
||||
const file = args.find((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--') && !FARA_VALOARE.has(args[i - 1])));
|
||||
if (!file) { console.error('usage: node verify-proof.mjs <envelope.json> [--rpc <url>] [--chain 2800|28001] [--notary <address>] [--manifests <url|dir>] [--live] [--btc-antet <hex>] [--offline] [--json]'); stop(2); }
|
||||
let raw; try { raw = fs.readFileSync(file, 'utf8'); } catch (e) { console.error('the envelope cannot be read: ' + e.message); stop(2); }
|
||||
// a file that opens and is not JSON is not an envelope: refused at the form level, by name (a leading byte order mark is skipped)
|
||||
let env, notJson = null; try { env = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw); } catch (e) { env = undefined; notJson = 'the file is not JSON (' + String(e.message).split('\n')[0].slice(0, 80) + ')'; }
|
||||
// The chain: --chain if given; otherwise the chain the envelope's notarization declares, when it is a known chain; otherwise 2800.
|
||||
// (Before 2026-09-27 an envelope notarized on 28001, checked without --chain, was looked up on 2800 and reported INVALID.) The
|
||||
// notary stays the known contract of that chain (or --notary): an address written in the envelope is never trusted as the notary.
|
||||
const declared = env && typeof env === 'object' && env.notarization && Number(env.notarization.chainId);
|
||||
const chain = Number(opt('--chain') || (declared && NOTARY[declared] ? declared : 2800));
|
||||
if (!NOTARY[chain]) { console.error('unknown chain ' + chain + ' (2800 or 28001)'); stop(2); }
|
||||
const o = { chain, rpcGiven: !!opt('--rpc'), rpc: opt('--rpc') || RPC_DEFAULT[chain], notary: (opt('--notary') || NOTARY[chain]).toLowerCase(),
|
||||
manifests: opt('--manifests') || MANIFESTS_DEFAULT[chain] };
|
||||
const l = local(env, notJson);
|
||||
const levels = [...l.out];
|
||||
if (l.text && env && typeof env === 'object') { levels.push(signature(env, l.text)); levels.push(await finality(env, o)); }
|
||||
const failed = levels.some((x) => x.state === 'FAILED');
|
||||
const unmeasured = levels.some((x) => x.state === 'UNMEASURED' || x.state === 'PENDING');
|
||||
const verdict = failed ? 'INVALID' : unmeasured ? 'PARTIAL' : 'VALID';
|
||||
const shown = env && typeof env === 'object' && typeof env.statementHash === 'string' ? env.statementHash : null;
|
||||
if (args.includes('--json')) console.log(JSON.stringify({ verdict, statementHash: shown, levels }, null, 1));
|
||||
// a file that opens and is not JSON is not an envelope: refused at the form level, by name
|
||||
const { env, notJson } = parseEnvelope(raw);
|
||||
let o; try { o = options(env, { chain: opt('--chain'), rpc: opt('--rpc'), notary: opt('--notary'), manifests: opt('--manifests'), live: args.includes('--live'), btcAntet: opt('--btc-antet'), offline: args.includes('--offline') }); }
|
||||
catch (e) { console.error(e.message); stop(2); }
|
||||
const r = await judge(env, o, notJson);
|
||||
if (args.includes('--json')) console.log(JSON.stringify({ verdict: r.verdict, statementHash: r.statementHash, levels: r.levels }, null, 1));
|
||||
else {
|
||||
console.log(`AERE Proof Protocol - ${path.basename(file)}`);
|
||||
for (const x of levels) console.log(` [${x.state.padEnd(10)}] ${x.level}: ${x.detail}`);
|
||||
console.log(`VERDICT: ${verdict}`);
|
||||
for (const x of r.levels) console.log(` [${x.state.padEnd(10)}] ${x.level}: ${x.detail}`);
|
||||
console.log(`VERDICT: ${r.verdict}`);
|
||||
}
|
||||
process.exitCode = failed ? 1 : unmeasured ? 2 : 0;
|
||||
process.exitCode = r.exitCode;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ e citita din campul `katSource` al fiecarui fisier, scris 2026-09-02.
|
||||
|
||||
| fisier | schema | sursa declarata in fisier | sha256 (16) |
|
||||
|---|---|---|---|
|
||||
| `falcon1024_kat0.json` | NIST Falcon-1024 (round-3) — official KAT vector 0 and 1 | falcon1024-KAT.rsp (Falcon round-3 NIST submission) | `5f430ea31548fde0` |
|
||||
| `falcon1024_kat0.json` | NIST Falcon-1024 (round-3), official KAT vector 0 and 1 | falcon1024-KAT.rsp (Falcon round-3 NIST submission) | `5f430ea31548fde0` |
|
||||
| `falcon512_kat0.json` | NIST Falcon-512 (round-3), KAT oficial | fisierul nu poarta camp `katSource`; `README.md` (randul "official NIST Falcon KAT vector 0 for both parameter sets") il declara vectorul KAT 0 al trimiterii Falcon round-3, ca si perechea lui `falcon1024_kat0.json` | `5011efbb97f68027` |
|
||||
| `mldsa44-acvp-tg8.json` | ML-DSA-44 (Dilithium2), FIPS 204 | NIST ACVP ML-DSA-sigVer-FIPS204 test vectors | github.com/usnistgov/ACVP-Server | gen-val/json-files/ML-DSA-sigVer-FIPS204 | `d9850650abdf446c` |
|
||||
| `sphincs-sha2-128s-acvp-tg31.json` | SLH-DSA-SHA2-128s (SPHINCS+-SHA2-128s-simple), FIPS 205 | NIST ACVP SLH-DSA-sigVer-FIPS205 test vectors | github.com/usnistgov/ACVP-Server | gen-val/json-files/SLH-DSA-sigVer-FIPS205 | `943811677f3a2549` |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user