# Verify Aere Network's post-quantum anchor yourself, signatures included

Three dependencies, one command, no account. This checks the claim that matters:
**chain 2800's block hash covers the digest of a post-quantum certificate of the validator set** (the digest, not the
certificate itself, is under the hash: a stripped or swapped certificate leaves the hash unchanged and fails against the
digest, aere-node SPEC.md 2.3; corrected 2026-10-27: this line said the hash covers the certificate), and **that every
seal in that certificate is a valid signature by a key the validator bound to itself with its own
consensus key**. Since 4 September 2026 those certificates are scheme-tagged: every one carries
Falcon-512 seals (lattice), and the **hybrid** ones carry SLH-DSA-SHA2-128s seals (hash-based) as well, so
a break in either family of mathematics leaves the other standing at those anchors. The hybrid form was on
every anchor from 2026-09-04 to 2026-09-29; since block 20,746,736 it is on every 128th block, with
Falcon-512 alone at the other anchors. Since block 24,148,752 (AIP-22 stage R, 2026-10-24) every certificate is v3: scheme-tagged as before, with every seal bound to the consensus round it was given in, so the tool also says whether a quorum of the validators of the parent sealed it in one round. Version 3 of this tool verifies all three forms. Nothing here asks Aere
to vouch for anything.

## Run it

```
npm install
node verify-anchor.mjs
```

Expected output (excerpt; run on 2026-10-26 against the live chain, anchors 24,453,712 to 24,454,320; the heights and counts move with the chain):

```
24454320  OK: bound under block hash (v3, round 0, AERE-PQ-ANCHOR-3), 10 seals (10 falcon-512), all valid over M_r of block 24454319 for round 0 and from distinct validators of that block, the set its own header lists (the registry v3 epoch 20836228); PARENT DECIDED IN ROUND 0 (10 members of 10, quorum 7): a QBFT quorum of that set sealed block 24454319 in that one round
24454256  OK: bound under block hash (v3, round 0, AERE-PQ-ANCHOR-3), 18 seals (9 falcon-512 + 9 slh-dsa-sha2-128s), all valid over M_r of block 24454255 for round 0 and from distinct validators of that block, the set its own header lists (the registry v3 epoch 20836228); PARENT DECIDED IN ROUND 0 (9 members of 10, quorum 7): a QBFT quorum of that set sealed block 24454255 in that one round
...
seals: 242/242 cryptographically valid and validator-bound   manifests: 3/3 self-consistent
VERIFIED: 20/20 anchor blocks carry a post-quantum certificate bound under the block hash, every seal a valid falcon-512 or slh-dsa-sha2-128s signature by a key that a distinct validator of the block it signs bound to itself with its own consensus key.
```

## What it actually does, leg by leg

For each of the 20 most recent anchor blocks (every 32nd height (every 128th from block 17,225,968 to block 20,746,736, 2026-09-05 to 2026-09-29)), and for the published key
manifests:

1. **Binding.** Reads the header's `extraData` over a public RPC, RLP-decodes it, takes element 5
   (counting from 0, the sixth element: the certificate) exactly as carried, recomputes the digest and checks it equals the 32-byte
   digest in the hashed part of the header. Alter the certificate by one bit and this fails.
   There are three certificate forms, and the tool tells you which one it read:
   - **v1**, up to block 17,047,568 (4 September 2026): element 5 is the list of Falcon seals `[[index, signature], ...]`, and
     `D = keccak256(RLP["AERE-PQ-ANCHOR-1", chainId, parentNumber, parentHash, certificate])`.
   - **v2 (scheme-tagged; SLH-DSA at every anchor until 2026-09-29, at every 128th block after)**, from block 17,047,600 (4 September 2026) to block 24,148,720, the last v2 anchor: element 5 is
     `RLP[2, [[scheme, index, signature], ...]]` with scheme `1` = Falcon-512 and `2` = SLH-DSA-SHA2-128s, and
     `D = keccak256(RLP["AERE-PQ-ANCHOR-2", chainId, parentNumber, parentHash, BYTES(RLP(certificate))])`.
     Each scheme must reach the enforced minimum on its own, and every SLH-DSA seal must sit at an
     index that also carries a valid Falcon seal, so one scheme cannot be quietly dropped.
   - **v3 (scheme-tagged, round-bound)**, live since block 24,148,752 (AIP-22 stage R, 2026-10-24): element 5 is
     `RLP[3, round, [[scheme, index, signature], ...]]`, and
     `D = keccak256(RLP["AERE-PQ-ANCHOR-3", chainId, parentNumber, parentHash, BYTES(RLP(certificate))])`; the schemes are
     judged as in v2, and the round is the one the seals were given in (leg 3).
2. **Key manifest.** Loads the published Falcon key manifests (`/pq/manifests/`). For every row it
   verifies the proof of possession (a Falcon-512 signature by the row's key over the row) and the
   claim (an ECDSA signature, EIP-191 version 0, by the validator's own consensus key over the same
   row, which must recover to the row's validator address). So the manifest cannot have been
   invented by us: each key is vouched for by the validator's consensus key.
3. **Seals.** Verifies every seal in every certificate: Falcon-512 in plain JavaScript written from
   the specification (`falcon512.mjs`, about 200 lines, no native code, no Aere code path, no
   precompile) and SLH-DSA-SHA2-128s through the independent `@noble/post-quantum` library (not
   ours), each against the manifest key of its index, over `keccak256(RLP["AERE-PQ-COMMIT-1", chainId, parentNumber, parentHash])` for a v1 or v2 certificate and, for a v3 certificate, over
   `keccak256(RLP["AERE-PQ-COMMIT-2", chainId, parentNumber, round, parentHash])` for the one round it names; when the seals of that round reach the QBFT quorum of the validator set of the
   parent, the line says `PARENT DECIDED IN ROUND r`.
4. **Validators.** Checks that the validator bound to each sealing key was in the QBFT validator set
   of the parent block, the block the seals sign, read from that block's own header after the header
   re-encodes to the hash the seals sign, and that each validator counts once per scheme (as both
   clients judge it). Corrected 2026-10-01: until then the set was asked of the endpoint at the anchor's
   height and cached per window of 1,000 blocks, so across a change of the set the tool could judge
   seals on the wrong set, and a duplicated seal counted twice. From the identity height of a v3
   epoch (chain 2800: block 20,836,228) the set holds post-quantum validator ids, and the check is on the id
   that the v3 epoch gives the key's row.
5. **On-chain pin.** Recomputes each manifest's hash, `keccak256(addr || pk, row by row)`, and
   checks it equals slot 0 of the immutable anchor contract named in the index (`eth_getStorageAt`),
   and that the contract's code is the 11-byte read-only getter with no write path (`eth_getCode`).
   So the manifest you hold is the one the chain pinned, not a copy we could have swapped. A v3 epoch
   (`formatVersion` 3; chain 2800 from block 20,836,228) is not pinned in a contract: it is pinned in the
   node configuration by its registry hash, and the tool verifies it in full (every proof of possession,
   every ECDSA link, the Merkle registry hash) and requires every row to carry the keys and the address of
   the same index in the key manifest in force at its height, which is the one pinned on chain.

It reads only; it needs no key, no wallet, no account.

## What the tool requires, and where the rules come from

The tool applies the chain's own schedules, the same ones both clients run, so that it can say "missing" and
not just "valid":

- **Threshold schedule** (`MIN_SEALS_SCHEDULE`, `h:k,...`): how many valid seals of each scheme an anchor at height
  `h` must carry. Default for chain 2800: `13014000:0,13034000:3,14961456:6,17102384:0,17102416:6,20715632:7`, the fleet's own
  schedule as the published node configuration gives it (corrected 2026-10-01: the tool asked for 3 seals from the first anchor,
  so warm-up anchors below 13,034,000, where the chain required none, came out as failures). The steps at 17102384 are a dated
  exception: at anchor 17102384 the chain required no seals, and the tool reports that anchor as it is
  (9 Falcon-512 seals, no SLH-DSA), not as a failure and not as a hybrid one.
- **Scheme schedule** (`SCHEME_SCHEDULE`, `h:scheme+scheme,...`): which schemes are required from which height of
  the parent. Default for chain 2800: `17047568:falcon-512+slh-dsa-sha2-128s,20746736:falcon-512/32+slh-dsa-sha2-128s/128`. A certificate that drops a required
  scheme entirely fails with `missing scheme`, and you can prove the check is alive by pointing the schedule at a
  height where the chain did not yet require the scheme.
- **Anchor interval schedule** (`ANCHOR_INTERVAL_SCHEDULE`, `h:interval,...`): by default derived from the chain, as the distance between the two most recent certificates (32 today; 128 from block 17,225,968 to block 20,746,736); if the
  chain moves to a wider interval from some height, set it here and the tool enumerates the same anchors the
  chain does. The dated range below where nothing was in force is stepped over when the spacing is derived, because its headers
  carry a certificate element off the grid as well.
- **Two dated historical windows of chain 2800**, both dated 2026-08-10, named by the production client
  (`PqAnchorLapse`, in the node package's `anchor/` source) and transcribed into the tool, for chain 2800 only. From 13,267,824 to
  13,268,944 (36 anchors) the anchor rules were not in force: those headers carry the client's version string instead of the
  digest, and the tool reports each as `NOT ENFORCED`, neither verified nor failed, after checking only what the nodes still
  require there, that its seal indices are distinct. Such anchors are not counted as verified: a run that reads only them ends
  `NOT VERIFIED` with exit code 2. From 13,268,976 to 13,890,544 only the threshold was lowered, to 1; the digest, the order of
  the indices and their distinctness are required there as everywhere. Added 2026-10-01: until then the tool reported anchors in
  both windows as failures, although the chain accepted them legitimately.

## Remove even the last trust assumption

By default the script talks to `https://rpc.aere.network` and fetches the manifests from
`https://aere.network/pq/manifests/`. To depend on nothing of ours but the public genesis, run your
own node and keep your own copy of the manifests:

```
RPC=http://localhost:8545 MANIFESTS=./my-manifests node verify-anchor.mjs
```

A manifest row cannot be faked without the validator's ECDSA key, a seal cannot be faked without
the validator's Falcon key, and a swapped manifest cannot match the hash pinned in an immutable
contract, so a hostile copy of any of them fails leg 2, 3 or 5 on your machine, not on ours.

## Prove the check can fail

A verifier that always says OK proves nothing. This one has demonstrated red paths:

```
# demand more seals than there are validators -> every recent anchor FAILS ("only 10
# Falcon-512 row(s) at K=11"), exit code 1; the same run without the override ends VERIFIED, exit 0
MIN_SEALS_SCHEDULE=13014000:0,13034000:3,14961456:6,17102384:0,17102416:6,20715632:11 node verify-anchor.mjs

# a tampered domain string -> every digest fails to match (AERE-PQ-ANCHOR-3, the domain of every anchor
# since block 24,148,752; until 2026-10-26 this line tampered ANCHOR-2, which no anchor of the default run uses any more)
sed 's/AERE-PQ-ANCHOR-3/AERE-PQ-ANCHOR-X/' verify-anchor.mjs > broken.mjs && node broken.mjs

# a tampered manifest: flip one byte of one public key in your local copy of
# manifest-13600000.json -> that row fails possession AND claim, and every seal of that index
# fails; the tool exits 1 and names the row. Same for a flipped claim byte, or a swapped address.
```

Corrected 2026-10-15: the first red path above used to be `node verify-anchor.mjs 13879664` ("thin certificates
below today's enforced minimum"). Since the correction of 2026-10-01 the tool judges that anchor at the threshold
the chain had in force there (K=1, the dated window below 13,890,544) and says OK, which is right, so that line
no longer showed a failure. Both red paths above were re-run against the live chain on 2026-10-15.

Corrected 2026-10-26: since block 24,148,752 (AIP-22 stage R) every anchor carries a v3 certificate, judged under `AERE-PQ-ANCHOR-3`, so the domain line above tampers ANCHOR-3; the ANCHOR-2 form it had until that date touches no anchor of the default run and ends VERIFIED, exit 0. The first two red paths were re-run against the live chain on 2026-10-26 (anchors 24,453,776 to 24,454,416): each ends with 20 of 20 anchors failed, exit 1; the manifest path was not re-run that day.

The Falcon verifier itself was accepted only after it (a) validated every real seal of live anchor
blocks under both key manifests, (b) rejected six kinds of planted forgery (flipped signature byte,
flipped message bit, flipped nonce bit, wrong key, truncated signature, wrong header), and (c)
agreed with the chain's native verifier on the same inputs in both directions.

## Files

- `verify-anchor.mjs` - version 3, all five legs, all three certificate forms, both signature schemes.
- `mpt.mjs` - the Merkle-Patricia proof walker (`eth_getProof`) and the RLP decoder, shared with `verify-proof.mjs` since
  2026-10-02: the on-chain pin of every key manifest (slot 0 of its anchor contract) is proven under the state root of the
  certified parent of the newest verified anchor, not read from the endpoint at `latest`. Download it next to `verify-anchor.mjs`.
- `falcon512.mjs` - Falcon-512 signature verification from the public spec: `verifyFalcon512(pk, message, sig)`.
- `registry-v3.mjs` - the validator registry v3 of AIP-22 stage 3 (post-quantum validator ids derived from the
  Falcon-512 key), verified in plain JavaScript: every proof of possession, every ECDSA link, the rotation chain and
  the Merkle registry hash. `verify-anchor.mjs` loads it only when the manifest index lists a v3 epoch
  (`formatVersion` 3): today that is chain 2800, where the validator set holds post-quantum ids from block
  20,836,228 (`registry-v3-20836228.json`, 2026-09-30), and the public testnet 28001, from block 3,080,010
  (armed on all six testnet nodes on 2026-09-23). On the testnet:
  `RPC=https://testnet-rpc.aere.network MANIFESTS=https://testnet-rpc.aere.network/pq/manifests CHAIN_ID=28001 node verify-anchor.mjs`.
- `verify-anchor-v1.mjs` - the original binding-only check, kept so earlier references stay true.
- `verify-proof.mjs` - the reference verifier of the AERE Proof Protocol (AIP-23), added 2026-09-27: it checks a proof
  envelope (statement, statementHash, optional signature) and, with `--rpc`, when the chain first saw it, from a state that a
  post-quantum certificate covers. It runs `verify-anchor.mjs` on the most recent anchor, binds that anchor's parent header to the
  hash its seals signed, and checks a Merkle proof of the notary's storage under that header's state root, so the endpoint only
  carries data. `node verify-proof.mjs <envelope.json> [--rpc <url>] [--chain 2800|28001]`. Conformance envelopes with their
  expected verdicts are in the node package (`aere-node/tools/proof-vectors/`). It needs `verify-anchor.mjs`, `mpt.mjs`,
  `falcon512.mjs`, `registry-v3.mjs` and `package.json` (one `npm install`) from this folder for level 4 (finality runs the anchor
  verifier beside it; without them that level is reported unmeasured), and `martor-notar-verifica.mjs` for level 5.
- `martor-notar-verifica.mjs` - the judge of the Bitcoin witness of a notarization (format `aere-notar-martor-1`), added
  2026-10-18: a single file built from the committed sources of `aere-quantum-shield` (the leaf's canonical bytes and RFC 9162 hash,
  the inclusion proof to the lot root, the OpenTimestamps proof parsed and evaluated restrictively down to a Bitcoin Merkle root), the
  same judge `verify-proof.mjs` calls for level `witness` when it sits beside it. Offline it names the Bitcoin block and reports
  UNMEASURED until you bring that block's 80-byte header (`--btc-antet`); `--live` asks two public explorers; it never contacts a
  calendar. `node martor-notar-verifica.mjs sample-witness.json --live`. The sample is the first witnessed notarization of chain 2800
  (lot 1, confirmed in Bitcoin block 971932 on 2026-10-18); the official OpenTimestamps client reads its proof to the same block.

## What this does not prove

Who runs the validators (today: one operator, the Aere Foundation, stated on every page), or that
the RPC you use is honest about chain contents: use your own node. It checks exactly the binding of
a post-quantum certificate under the block hash and the validity and provenance of its signatures,
nothing more. The full stack, and what is live versus armed, is at https://aere.network/quantum.html.
