aere-docs/AERE-XMSS-AGGREGATION-CIRCUIT.md
Aere Network dd1410c266 The unpublished line of work joins the sanitized public line
The published line and the local line of this repository had no common
ancestor: the public one carried the hygiene pass (no host names, no internal
paths), the local one carried a month of corrections that never shipped. This
commit ports the local work onto the public line, keeping the public hygiene
wording wherever the two touched the same sentence, and keeping the public
version of AERE-CROSS-CLIENT-DETERMINISM.md entirely.

Carried: LICENSE/LICENSING corrections, VERIFY-POLICY.md,
CITATIONS-UNRESOLVED.md remeasured 2026-08-11, the 'audited' adjective removed
from next to Bouncy Castle, citation paths rewritten to published form, AIP-8,
the QA consolidation report, the second EIP validation pass, fork-height
corrections, the AereSink / threshold-factory correction, the forge test
floor, and the architecture-map updates.
2026-08-15 13:54:42 +03:00

396 lines
22 KiB
Markdown

# Aere Network Post-Quantum Finality: XMSS Aggregation Circuit Design
Research item #4, the off-chain half of the Aere Network Post-Quantum Finality Certificate.
Status header, read this first.
- IMPLEMENTED and TESTED: the per-validator VERIFICATION CORE. An RFC 8391
XMSS-SHA2_10_256 hash-based signature verifier (WOTS+ one-time signature plus the
XMSS Merkle authentication-path check that recovers the root public key), written
as a dependency-free, `no_std`, allocation-free, float-free, hash-only Rust crate
at `aerenew/pqc-fork/pq-finality-circuit/xmss-verify-core/`. It is validated
against the OFFICIAL github.com/XMSS/xmss-reference known-answer vector and passes
(see Section 7 for the real, run test result).
- NOT IMPLEMENTED, `[MEASURE]`, multi-week specialist work: the full zkVM
aggregation guest that runs this core over N validators, the SP1 proving pipeline,
and the end-to-end certificate. This document specifies that design. It does not
claim it works.
- UNCHANGED and fail-closed: the on-chain half
(`contracts/contracts/pqfinality/AereFinalityCertificateVerifier.sol` and
`AerePQAttestationKeyRegistry.sol`). Nothing in this work touches it. See Section 8.
Scope boundary, unchanged from the certificate design. This is the ADDITIVE
post-quantum finality-attestation READ path. Aere consensus is still classical ECDSA
QBFT. Validators keep signing ECDSA committed seals for block production and ALSO
sign a hash-based attestation over each finalized block. The certificate proves the
second, additive quorum. It does not replace, gate, or flip consensus, it needs no
re-genesis, and post-quantum finality is NOT live end to end. Aere consensus is not
post-quantum, and nothing here claims it is.
---
## 1. Where this sits
`AERE-PQ-FINALITY-CERTIFICATE.md` describes the whole feature. The on-chain verifier
consumes ONE succinct proof and binds its public values to the live attestation-key
registry. The proof is produced by an off-chain aggregation circuit that, inside a
zkVM, verifies that a quorum of validators hash-based-signed a finalized block.
That aggregation circuit has an inner obligation it discharges once PER VALIDATOR:
take a validator's committed hash-based public key and one signature, and confirm the
signature is a genuine hash-based signature under that key over the domain-bound block
message. For the XMSS-SHA2_10_256 scheme (RFC 8391, registry `SCHEME_XMSS`), that
inner obligation is exactly the code in `xmss-verify-core`: recover the XMSS root from
the signature and compare it to the validator's committed root.
This document specifies how the aggregation circuit wraps N instances of that inner
obligation into the single proof the on-chain verifier already accepts.
---
## 2. The inner obligation (IMPLEMENTED)
For one validator, XMSS-SHA2_10_256 verification (RFC 8391 sections 4.1.9 / 4.1.10,
single tree, height h = 10, n = 32, Winternitz w = 16, len = 67 chains, SHA-256):
1. `M' = H_msg( toByte(2,32) || R || root || toByte(idx,32) || M )`, the randomized,
root-bound message hash, where `M` is the domain-bound block message (Section 4.3).
2. `pk_ots = WOTS_PKFromSig(sig_ots, M', SEED, OTS-address(idx))`, completing each of
the 67 Winternitz chains from the signature value to its chain end.
3. `leaf = ltree(pk_ots, SEED, LTree-address(idx))`, the L-tree compression of the 67
WOTS+ public values to one n-byte leaf.
4. `node = compute_root(leaf, idx, auth, SEED, HashTree-address)`, folding the leaf
with the h = 10 authentication path.
5. The signature is VALID for `(root, SEED)` iff `node == root`.
All hashing uses the RFC 8391 keyed, address-domain-separated toolbox with
padding_len = n = 32: `F`, `H`, `H_msg`, and `PRF`, with per-address keys and bitmasks
derived by `PRF(SEED, ADRS)` over the 32-byte hash address. The reference crate is a
byte-for-byte model of the already-deployed on-chain `AereXmssVerifier.sol` and its
independent JS oracle, both validated against the same official vector.
Why XMSS verification is zkVM-tractable, honestly. It is hash-only: no floating point,
no FFT, no Gaussian sampling (the reason Falcon is hard in a circuit). Every step is a
SHA-256 call over fixed-size inputs, and SHA-256 is an SP1 precompile. The cost is a
LARGE but BOUNDED number of SHA-256 invocations (Section 6), not a novel arithmetic
gadget. That is what makes this the tractable first target.
Honest limits of the core. It verifies that a signature is valid for a given
`(root, SEED)`. It does NOT and cannot enforce the signer's one-time-per-leaf state:
XMSS is stateful, and reusing a leaf index breaks WOTS+ off-chain, not in the verifier.
The registry models rotation (Section 5); state discipline is the signer's duty.
---
## 3. The aggregation circuit (NOT implemented, `[MEASURE]`)
The guest program (SP1 zkVM in the reference design, matching the repo's existing
guests under `aerenew/zk-light-client/` and `aerenew/zk-circuits/`) takes a private
witness and commits public values.
### 3.1 Private witness
- `blockHash`, `blockHeight`: the finalized Aere block the certificate is about.
- `attestationDomain`: the 32-byte domain the on-chain verifier binds (Section 4.3).
- The ordered validator set: for each of the N enrolled validators (in the registry's
enrollment order, which fixes the root ordering): the validator address, its current
`keyEpoch`, and its current attestation public key bytes (for XMSS: `root || SEED`,
plus scheme id).
- For each validator that attested: its XMSS signature `(idx, R, wots[67], auth[10])`
over the domain-bound block message. Non-attesting validators carry a null marker.
### 3.2 What the circuit MUST enforce
1. RECOMPUTE the committee commitment. Rebuild `validatorSetRoot` from the witnessed
set exactly as `AerePQAttestationKeyRegistry.validatorSetRoot()` defines it
(Section 4.1), and ABORT unless it equals the committed root the circuit emits. This
is the membership check: a signature only counts if it is under a key the registry
committed. Because the flat commitment binds every validator's address, key-epoch,
and current key material, the circuit that reproduces it has proven each counted key
is a genuine member at the committed epoch.
2. VERIFY each counted attestation. For each attesting validator, run the Section 2
inner obligation against THAT validator's committed `(root, SEED)` over the message
of Section 4.3, and count it only if it recovers the committed root.
3. NEVER double-count. The circuit iterates the committed validator list in order and
admits at most one counted attestation per list position, so distinctness is
structural, not a runtime set-membership test.
4. COUNT and THRESHOLD. Let `quorumCount` be the number of DISTINCT valid attestations.
The circuit requires `quorumCount >= ceil(2N/3)` (the on-chain
`_quorumThreshold(n) = (2n + 2) / 3`), and ABORTS otherwise, so a proof cannot exist
below quorum.
5. EMIT the public values of Section 4.2.
The proof is succinct: its size and on-chain verification cost are fixed by the SP1
verifier, independent of N and of how many attestations were checked internally. That
independence lives entirely in the SP1 proof system, not in this circuit.
---
## 4. Binding to the on-chain verifier (the exact contract)
The circuit exists to satisfy the ALREADY-BUILT, UNCHANGED on-chain verifier. The
binding is exact and is the load-bearing honesty of the design.
### 4.1 The committee commitment (registry-defined)
`AerePQAttestationKeyRegistry.validatorSetRoot()` is a flat, ordered keccak commitment:
```
leaf_i = keccak256(abi.encode(validator_i, keyEpoch_i, keyHash_i))
root = keccak256(abi.encode(leaf_0, leaf_1, ..., leaf_{N-1}))
```
where `keyHash_i = keccak256(currentPubKey_i)`, or `bytes32(0)` if validator_i has no
key yet, and the ordering is enrollment order. The circuit MUST reproduce this
byte-for-byte, including the Solidity `abi.encode` framing.
`[VERIFY]` Two encoding details the guest implementation must match against the
deployed registry when built: (a) `abi.encode(validator_i, keyEpoch_i, keyHash_i)`
left-pads the 20-byte address and the `uint32` epoch to 32 bytes each, giving a 96-byte
pre-image; (b) `abi.encode(bytes32[] leaves)` prepends the ABI head for a single
dynamic array (a 32-byte offset word then a 32-byte length word) before the N leaves,
so `root = keccak256( 0x20-word || N-word || leaf_0 || ... || leaf_{N-1} )`. Both use
keccak-256, an SP1 precompile, distinct from the SHA-256 inside XMSS.
A Merkle tree is a drop-in alternative if per-leaf inclusion proofs are ever wanted
(the registry doc notes this). It is NOT needed here: because the circuit witnesses the
WHOLE set, recomputing the flat commitment is both sufficient and cheaper than N
logarithmic inclusion proofs. If N grows large enough that witnessing the whole set is
undesirable, switch the registry to a Merkle root and have the circuit check per-member
inclusion instead; the on-chain verifier is agnostic, it only compares roots.
### 4.2 The public values (fixed 192-byte layout)
The circuit MUST commit exactly the six-word layout
`AereFinalityCertificateVerifier._decode` expects:
```
abi.encode(
bytes32 blockHash, // the finalized Aere block hash
uint256 blockHeight, // its height
bytes32 validatorSetRoot, // recomputed IN the circuit per Section 4.1
uint256 validatorSetSize, // N committed under the root
uint256 quorumCount, // number of DISTINCT valid attestations, >= ceil(2N/3)
bytes32 attestationDomain // the domain tag of Section 4.3
)
```
The on-chain verifier then, in ONE call and independent of N, checks: the registry is
non-empty; `validatorSetRoot` equals the live registry root (a rotation invalidates old
certs); `validatorSetSize` equals the live N; `attestationDomain` equals its immutable
bound domain; `quorumCount >= ceil(2N/3)`; `blockHash != 0`; and the SP1 proof verifies
against the pinned circuit vkey. The circuit is trusted for the facts behind those six
words; the contract binds them to live state.
### 4.3 The attestation message (domain-bound)
Each validator signs `M`, the 32-byte value the on-chain verifier defines in
`attestationMessage`:
```
attestationDomain = keccak256(abi.encode(DOMAIN_PREFIX, chainId, registry))
M = keccak256(abi.encode(attestationDomain, blockHash, blockHeight))
```
`DOMAIN_PREFIX = keccak256("AerePQFinalityCertificate.v1")`. `M` is a keccak output;
the XMSS `H_msg` then hashes `M` (as its message bytes) with SHA-256 internally. Domain
binding blocks replay of a certificate to another chain, registry, or purpose. The
circuit computes `M` from the witnessed `attestationDomain`, `blockHash`, `blockHeight`
and checks every counted XMSS signature over it, then emits `attestationDomain` so the
contract can confirm it equals the domain it pinned at construction.
---
## 5. Choice of hash-based scheme, and the tradeoff
The registry already carries scheme labels: `SCHEME_XMSS` (RFC 8391 stateful few-time),
`SCHEME_WOTS` (Winternitz OTS+ one-time), `SCHEME_LEANSIG` (lean hash-based), and
`SCHEME_SLHDSA128S` (FIPS 205 stateless), plus `SCHEME_XMSSMT`. The circuit's inner
verifier is scheme-specific, so the scheme choice is a real design axis.
### 5.1 WOTS+ / XMSS (the implemented target)
- WOTS+ alone is ONE-TIME: a key signs exactly one message safely. XMSS layers a Merkle
tree of `2^h` WOTS+ keys under one long-term root, giving `2^h` safe signatures (here
`2^10 = 1024`) before the key is exhausted. Both are STATEFUL: the signer must never
reuse a leaf index. This is why attestation keys ROTATE, which
`AerePQAttestationKeyRegistry` already models with a monotonic per-validator
`keyEpoch` and an append-only key history. A validator that spends its `2^h` leaves
registers a successor key; the root changes; old certificates stop matching; new ones
bind to the new root. Rotation is expected, cheap, and fail-closed.
- Pro: standardized (RFC 8391, NIST SP 800-208), the most conservative post-quantum
assumption (security from SHA-256 collision and preimage resistance alone), and it
MATCHES the deployed on-chain `AereXmssVerifier.sol`, so the same bytes verify
on-chain and in-circuit.
- Con: SHA-256 is bit-oriented and comparatively EXPENSIVE to prove. The per-validator
cost is thousands of SHA-256 invocations (Section 6). With N small this is tractable;
it does not scale gracefully to thousands of validators.
`SCHEME_SLHDSA128S` (SLH-DSA, FIPS 205) is the STATELESS hash-based alternative: no
rotation discipline (a key signs many times safely), at the cost of larger signatures
and even more hashing per verify, so a heavier circuit. It is the right choice if
operators cannot be trusted to manage XMSS state; it trades circuit cost for removing
the stateful footgun.
### 5.2 leanSig / Poseidon2 (the field-native alternative)
leanSig is the hash-based signature line behind Ethereum's Lean Consensus / beam-chain
direction: a generalized-XMSS-style construction whose hash is Poseidon2 over a small
STARK-native field, chosen precisely for signature aggregation in a proof system.
- Pro: Poseidon2 is arithmetization-friendly. It is DRAMATICALLY cheaper inside a
FRI/STARK circuit than SHA-256 (which pays for bit operations the field does not model
natively). A leanSig aggregation circuit proves far faster per validator and scales to
much larger N, which is why Ethereum targets it for its very large validator set.
- Con: it is NEWER and less standardized than RFC 8391 XMSS, and it moves the hash off
SHA-256. To keep the on-chain binding exact, the registry key material and the
committee commitment would also need a consistent hashing story (either keep the
keccak commitment over Poseidon2-based public keys, or move the commitment to
Poseidon2 too); the on-chain `AereXmssVerifier.sol` SHA-256 path would not be the
in-circuit hash. It is the better ENDGAME for cost and scale; XMSS-SHA2 is the better
FIRST target for standards-alignment and for reusing the already-validated on-chain
verifier.
The tradeoff in one line: XMSS-SHA2 buys standardization, conservatism, and on-chain
reuse at a higher in-circuit hashing cost; leanSig/Poseidon2 buys a much cheaper,
more scalable circuit at the cost of novelty and a hashing story that diverges from the
deployed SHA-256 verifier. An ML-DSA-in-zkVM approach (FIPS 204, integer arithmetic) is
a further alternative with an easier circuit than SHA-256-XMSS, but it is LATTICE-based,
not hash-based, so it does NOT carry the hash-only quantum-safety argument and is out of
scope for the hash-based certificate.
Aere Network's live-first advantage is honest and specific: with a small validator set
(N = 7 today, N >= 9 planned) the XMSS-SHA2 aggregation verifies at most N hash-based
signatures per certificate, a tractable proving workload NOW, whereas Ethereum's
hundreds of thousands of validators force the leanSig research runway. The small set
that makes Aere's honest-quorum premise an operator assumption is the same property that
makes post-quantum finality aggregation buildable today.
---
## 6. Structural cost (derivable) and performance (`[MEASURE]`)
The following op counts are STRUCTURAL: they follow from the XMSS-SHA2_10_256 parameters
and the toolbox definitions, and are derivable without running anything. They are NOT a
performance measurement.
Per SHA-256-toolbox primitive: `PRF` and `F` hash 96-byte inputs; `H` hashes a 128-byte
input; each `F` step needs 2 `PRF` (key, mask) plus one `F` hash (3 SHA-256 calls); each
`H` node needs 3 `PRF` (key, two masks) plus one `H` hash (4 SHA-256 calls).
Per-validator XMSS-SHA2_10_256 verify (worst case, chains completed from the low end):
| Stage | Units | SHA-256 calls (worst case) |
| ----------------- | ------------------------------ | -------------------------- |
| `H_msg` | 1 hash over ~129 bytes | 1 |
| WOTS_PKFromSig | up to 67 chains x 15 `F` steps | up to ~3015 |
| L-tree | 66 `H` nodes | ~264 |
| Auth path (h=10) | 10 `H` nodes | ~40 |
| Per validator | | up to ~3320 SHA-256 calls |
For N = 7 that is on the order of ~23,000 SHA-256 invocations across the aggregation,
plus a handful of keccak-256 calls for the committee commitment and the message binding.
Typical (not worst) WOTS cost is roughly half, since the average completed chain length
is about w/2. These counts are exactly why SHA-256 is the cost driver and why
leanSig/Poseidon2 is the cheaper long-run hash.
`[MEASURE]` and NOT run here (each depends on the actual SP1 guest build, the sha256/
keccak precompile configuration, the prover hardware, and the proof mode):
- RISC-V cycle count per validator and per certificate inside the SP1 zkVM.
- Proving latency and memory for N = 7 and N = 9, on a given prover or the Succinct
prover network.
- On-chain `verifyProof` gas against the REAL Succinct gateway (the contract is
currently tested against a mock gateway).
- Native STARK vs Groth16-wrap tradeoff. A Groth16 wrap over BN254 shrinks the on-chain
proof but REINTRODUCES a Shor-breakable pairing leg, forfeiting the end-to-end
quantum-safety of Section 4 of the certificate doc. A native hash-based STARK proof
(verified by the `AerePQStarkVerifier` line) keeps the proof itself post-quantum. This
choice is a `[MEASURE]` cost-vs-quantum-safety decision, not settled here.
Scope caveat (2026-07-19 finding): the `AerePQStarkVerifier` skeleton (0x0AE8) built so
far is a BabyBear + FRI verifier that targets Aere's OWN Plonky3 STARKs. It does NOT
verify an SP1 6.1.0 proof, because the pinned SP1 6.1.0 is a Hypercube / BaseFold
(KoalaBear multilinear) system, not BabyBear FRI. So the native-STARK option here means
proving the aggregation as an Aere-native BabyBear/FRI STARK (or retargeting 0x0AE8 to
the SP1 Hypercube stack, a separate ~22 to 32 person-week effort), not verifying an SP1
zkVM proof directly. See `AERE-STARK-SP1-RECURSION-AIR-PORT-SPEC-SUMMARY.md`.
---
## 7. What was actually built and the REAL test result
Built at `aerenew/pqc-fork/pq-finality-circuit/xmss-verify-core/`:
- `aere-research/pq-finality-circuit/xmss-verify-core/src/sha256.rs`: a dependency-free `no_std` SHA-256 (self-checked against FIPS 180-4
`SHA256("abc")` and `SHA256("")`). A production SP1 guest swaps this for the
SP1-patched `sha2` precompile crate; the bytes are identical.
- `aere-research/pq-finality-circuit/xmss-verify-core/src/xmss.rs`: the RFC 8391 XMSS-SHA2_10_256 verification core (`chain_lengths`,
`wots_pk_from_sig`, `l_tree`, `compute_root`, `xmss_recover_root`, `xmss_verify`),
`no_std`, allocation-free, float-free, hash-only.
- `aere-research/pq-finality-circuit/xmss-verify-core/src/vectors.rs`: the official vector, GENERATED by `aere-research/pq-finality-circuit/scripts/gen_vectors.mjs` from the
committed `contracts/test/fixtures/xmss-sha2_10_256-kat.json`
(github.com/XMSS/xmss-reference, oid=1, idx=512, msg=0x25), so the Rust vector is
provably derived from the official fixture with no hand transcription.
- `aere-research/pq-finality-circuit/xmss-verify-core/tests/kat.rs`: the known-answer validation.
Real result of `cargo test --offline` (run, not asserted from memory): 12 tests
passed, 0 failed. The genuine official signature recovers the official root
`0x9d898033e37af48e6a116f8b15651cc26773467007ad19375d38c23c690c3483` and verifies; and
every tamper (a WOTS+ value, the checksum chain, an authentication-path node, the
message, the leaf index, the claimed root, the R randomizer) fails to verify. The two
SHA-256 self-checks match FIPS 180-4.
This validates the INNER obligation of Section 2 only. It is not a proof, not a zkVM
run, and not an aggregation.
---
## 8. On-chain side is unchanged and fail-closed
This work adds an off-chain reference crate and this document. It does NOT modify
`AereFinalityCertificateVerifier.sol` or `AerePQAttestationKeyRegistry.sol`, and does
not touch their tests. The on-chain verifier remains fail-closed exactly as before: it
rejects unless the registry is non-empty, the proven root and size match the live
registry, the domain matches, `quorumCount >= ceil(2N/3)`, the block hash is non-zero,
AND the SP1 proof verifies against the pinned vkey through the immutable gateway. Until
a real aggregation guest and a real proof exist, no certificate can pass the real
gateway, which is the correct fail-closed behavior.
---
## 9. Effort estimate for the remaining `[MEASURE]` work
Honest, and a range, not a promise. The verification core (done) is the small part.
The remaining full zkVM guest plus proving pipeline is multi-week specialist work:
- Guest program: port `xmss-verify-core` into an SP1 guest, add the keccak committee
commitment and message binding, the N-validator loop, distinct-count, and the
threshold, and wire the six-word public-values output. Roughly 1 to 2 weeks including
matching the `abi.encode` framing of Section 4.1 against the deployed registry.
- Host and witness assembly: collect attestations, build the private witness, run the
prover. Roughly 1 week.
- Proving, tuning, and MEASURE: cycle counts, latency, memory, real-gateway gas, and the
native-STARK vs Groth16 decision. Roughly 1 to 3 weeks, hardware-dependent.
- Audit of the guest before any mainnet vkey is pinned. Separate, founder-gated.
Total on the order of several weeks of focused specialist work before an end-to-end
certificate could be produced, plus audit. Do NOT describe the certificate as working
end-to-end until that work is done and measured.
---
## 10. References
- On-chain half (unchanged): `contracts/contracts/pqfinality/AereFinalityCertificateVerifier.sol`,
`contracts/contracts/pqfinality/AerePQAttestationKeyRegistry.sol`, and
`contracts/test/AerePQFinalityCertificate.test.js`.
- Feature doc: `aerenew/docs/AERE-PQ-FINALITY-CERTIFICATE.md`.
- Deployed on-chain XMSS verifier and its official vector:
`contracts/contracts/pqc/AereXmssVerifier.sol`, `contracts/test/xmssVerifier.test.js`,
`contracts/test/fixtures/xmss-sha2_10_256-kat.json`.
- SP1 guest / core-crate pattern this crate mirrors: `aerenew/zk-light-client/guest/`,
`aerenew/zk-light-client/qbft-finality/`, `aerenew/zk-circuits/recursive-aggregation-scale/`.
- Standards: RFC 8391 (XMSS), NIST SP 800-208 (stateful hash-based signatures), FIPS 205
(SLH-DSA), FIPS 180-4 (SHA-256).
```