Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
22 KiB
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 ataerenew/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.solandAerePQAttestationKeyRegistry.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):
M' = H_msg( toByte(2,32) || R || root || toByte(idx,32) || M ), the randomized, root-bound message hash, whereMis the domain-bound block message (Section 4.3).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.leaf = ltree(pk_ots, SEED, LTree-address(idx)), the L-tree compression of the 67 WOTS+ public values to one n-byte leaf.node = compute_root(leaf, idx, auth, SEED, HashTree-address), folding the leaf with the h = 10 authentication path.- The signature is VALID for
(root, SEED)iffnode == 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
- RECOMPUTE the committee commitment. Rebuild
validatorSetRootfrom the witnessed set exactly asAerePQAttestationKeyRegistry.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. - 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. - 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.
- COUNT and THRESHOLD. Let
quorumCountbe the number of DISTINCT valid attestations. The circuit requiresquorumCount >= ceil(2N/3)(the on-chain_quorumThreshold(n) = (2n + 2) / 3), and ABORTS otherwise, so a proof cannot exist below quorum. - 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^hWOTS+ keys under one long-term root, giving2^hsafe signatures (here2^10 = 1024) before the key is exhausted. Both are STATEFUL: the signer must never reuse a leaf index. This is why attestation keys ROTATE, whichAerePQAttestationKeyRegistryalready models with a monotonic per-validatorkeyEpochand an append-only key history. A validator that spends its2^hleaves 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.solSHA-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
verifyProofgas 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
AerePQStarkVerifierline) 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): theAerePQStarkVerifierskeleton (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. SeeAERE-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/:
src/sha256.rs: a dependency-freeno_stdSHA-256 (self-checked against FIPS 180-4SHA256("abc")andSHA256("")). A production SP1 guest swaps this for the SP1-patchedsha2precompile crate; the bytes are identical.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.src/vectors.rs: the official vector, GENERATED byscripts/gen_vectors.mjsfrom the committedcontracts/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.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-coreinto 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 theabi.encodeframing 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, andcontracts/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).