aere-research/research/specs/spec-recursive-aggregation-scale.md
Aere Network 4a0b48588c Initial public release
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.
2026-07-20 01:02:30 +03:00

186 lines
18 KiB
Markdown

# AERE Network: Recursive Proof Aggregation at Scale
**Chain:** AERE Network mainnet, chain ID 2800 (Hyperledger Besu QBFT).
**Status date:** 2026-07-11.
**Source of truth for addresses:** `sdk-js/src/addresses.ts`. Contract source: `contracts/contracts/zkverify/AereProofAggregator.sol`. Guest and prover source: `zk-circuits/recursive-aggregation-scale/`. On-chain records: `contracts/deployments/proof-aggregator.json` (n=3) and `contracts/deployments/proof-aggregator-scale.json` (n=10).
**Scope:** folding N heterogeneous SP1 proofs into a single Groth16 proof, verified once on-chain, and the gas-amortization argument that follows. This is a focused companion to `spec-zk-stack.md`, which covers the wider verification surface.
---
## 0. Honesty preamble (read this first)
Five points frame everything below.
1. **This is a proof of approach, not a production rollup batcher.** The largest run demonstrated on-chain folds ten inner proofs. That is enough to establish that the recursion, the composite binding, and the constant-cost on-chain verification all work end to end. It is not a claim of a high-throughput sequencing pipeline. n=16 is in progress and is not yet recorded on-chain.
2. **The chain is small and centralized.** AERE runs three QBFT validators under a single operator on a single client (Besu). There is no external security audit of the AERE-authored contracts. The aggregator is an ordinary EVM contract that any account can call; it does not touch consensus.
3. **Proving happens off-chain and is heavy.** The n=10 aggregation took roughly 1549 seconds of wall time on the prover box (CPU): about 988 seconds to produce the ten inner proofs and about 526 seconds for the final Groth16 wrap. Aggregation buys cheap, constant on-chain verification by spending real off-chain compute. It is not a low-latency path.
4. **The inner statements in the scale run are constructed, not organic user traffic.** The ELFs are the real production programs (their verifying keys are checked byte-for-byte against the on-chain vkeys before any proof enters the fold, and the run aborts if an ELF does not reproduce its published key). The proofs are genuinely produced and genuinely recursively verified. But the per-instance inputs (user addresses, attestation times, credential values) are deterministically derived for the demonstration so that each folded proof is a distinct statement. No claim is made that ten independent users generated these.
5. **The aggregator stores one composite, not each inner statement's payload.** The individual `(vkey, publicValues-hash)` pairs are bound into the composite and emitted in the verification event, but the contract does not persist each inner statement's semantic content. Consumers that need per-proof detail must read the event or re-supply the inner data.
---
## 1. The primitive
`AereProofAggregator` at `0x6a260238890E740dB12b371E0C5d17a2470F84C5` (canonical, `addresses.ts`; `owner` = Foundation `0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3`) is an on-chain anchor for a single recursive SP1 "proof of proofs." One Groth16 proof attests that N inner SP1 proofs, each from a real, distinct AERE production program, were each verified inside the zkVM. The contract verifies that one Groth16 proof through the live SP1 gateway, recomputes a composite digest from the supplied inner data to bind the proof to exactly which programs were folded, and records the aggregation.
The design goal is amortization: pay for one on-chain verification and one Groth16 wrap, regardless of how many application proofs are folded underneath.
The verifier plumbing is shared with the rest of the ZK stack:
- `SP1VerifierGateway`: `0x9ca479C8c52C0EbB4599319a36a5a017BCC70628` (the aggregator holds this stable gateway address, not a concrete verifier, so new prover versions route in without redeploying the aggregator).
- `SP1VerifierGroth16_v6_1_0`: `0xb5456d48bFdA70635c13b6CBE1Ad0310Dc0171aD` (the concrete verifier the Groth16 selector `0x4388a21c` routes to).
---
## 2. Architecture: recursion in the guest, bind-and-verify in the contract
There are two halves: an SP1 guest that does the recursive verification off-chain, and the Solidity contract that verifies the resulting Groth16 proof on-chain.
### 2.1 The aggregation guest (`recursive-aggregation-scale/agg-program/src/main.rs`)
The guest reads a vector of inner verifying-key digests and a vector of inner public-value byte-strings. For each inner proof it:
1. computes `pv_digest = sha256(publicValues[i])`, the exact digest the SP1 recursion verifier binds an inner proof to;
2. calls `sp1_zkvm::lib::verify::verify_sp1_proof(&vkeys[i], &pv_digest)`, which recursively verifies, inside this zkVM, that a valid inner SP1 proof exists for that verifying key and those public values. If any inner proof is missing or invalid, the aggregation proof cannot be produced at all;
3. folds `(koalabear_vkey_digest_i || pv_digest)` into a running SHA-256.
After the loop it commits a single 32-byte composite. The guest is unchanged from the n=3 version, which is why its verifying key is stable and already registered on-chain. The host asserts the rebuilt guest reproduces the registered aggregation vkey `0x003a70854f258b13c6612b0272421f2d7d97f90eee63e124864c80be61a2672c` and aborts otherwise, since the Foundation, not the prover operator, controls which aggregation vkey the contract will route.
Inner proofs are generated as compressed SP1 (STARK) proofs. The final aggregation is Groth16-wrapped for the on-chain gateway. This is the key economic move: N compressed inner proofs plus one Groth16 wrap of the recursion, rather than N separate Groth16 wraps.
### 2.2 The contract (`AereProofAggregator.sol`)
`recordAggregation(aggVKey, innerVKeyDigests[], innerPvHashes[], publicValues, proof)` is permissionless. It:
1. requires `aggVKey` to be a registered aggregation program (`isAggProgram`);
2. checks array lengths match, N is non-zero, and `publicValues` is exactly 32 bytes;
3. recomputes the composite from the supplied inner digests using the exact same byte layout as the guest, and requires it to equal the committed 32-byte `publicValues` (reverts `CompositeMismatch` otherwise);
4. calls `ISp1Verifier(SP1_VERIFIER).verifyProof(aggVKey, publicValues, proof)` inside a `try/catch`, translating the gateway's revert-on-invalid convention into a typed `InvalidProof()`;
5. counts how many folded inner programs are recognized (registered via `registerInnerProgram`) and stores the aggregation keyed by composite, incrementing `aggregationCount`.
`SP1_VERIFIER` is immutable, set at deploy. Recorded aggregations cannot be altered or removed. A `verifyAggregation(...)` view performs the same recompute-then-verify without recording, so any caller can re-check an aggregation via `eth_call`.
Because the contract recomputes the composite from caller-supplied inner data and requires it to match the proof's own committed output, a caller cannot lie about which programs were folded: changing the claimed inner set changes the recomputed composite, which then fails to equal what the proof committed. The `record-aggregation-scale.js` driver exercises exactly this: tampering the proof body reverts, and dropping one child (so the recomputed composite no longer matches the committed value) reverts. Both rejections are recorded in `proof-aggregator-scale.json`.
---
## 3. The composite binding and the two vkey encodings
The composite is byte-identical between guest and contract:
```
composite = sha256(
u32_be(n)
|| (koalabearVKeyDigest_0 || sha256(publicValues_0))
|| ...
|| (koalabearVKeyDigest_{n-1} || sha256(publicValues_{n-1}))
)
```
Each inner program is identified by two different encodings of the same key. The `koalabearVKeyDigest` is the SP1Field (KoalaBear) digest that `verify_sp1_proof` actually checks (`SP1VerifyingKey::hash_u32`, serialized big-endian). The `onChainVKey` is the BN254 `bytes32` the program publishes for gateway verification. These are not the same 32 bytes. The Foundation registers the mapping between them (`registerInnerProgram(innerVKeyDigest, onChainVKey, label)`), so each folded program can be labelled and cross-checked against its published on-chain key. Binding the KoalaBear digest, rather than the BN254 key, is the sound choice: it is the value the recursion verifier truly checked.
---
## 4. Heterogeneity: three different programs, one proof
The folded set is not N copies of one circuit. The n=10 run folds three distinct programs with three distinct verifying keys:
- **zkscreen** (allowlist / sanctions-screening membership proof), on-chain vkey `0x006a211015aee2b90b82d266bef3aa6944551b06c488d86e895489b1dde3e5b7`, paired with the live `AereZKScreen` verifier `0x3A097A459FD26aC79573aCB5adB51430e473C2f1`.
- **over18** (age-credential proof over a Merkle-committed birth year), on-chain vkey `0x005aa94ac711bc65d0755b3a94f3622a6e8c76e2cdd708c29869587f633343c5`.
- **zkml-mnist** (a 784 to 256 to 10 MLP MNIST classifier run inside the zkVM on a private image), on-chain vkey `0x0061443ce273455d0afcdaf1103ea0a52b970d979896dc83d989e71ba7c4bceb`, paired with the live `AereZKMLVerifier` `0xf1BF15d5018a35D21FBB4Ec868f062DF7C06783c`.
`verify_sp1_proof` verifies any inner SP1 proof regardless of which program produced it, so heterogeneous folding requires no special handling: the guest simply calls it once per inner proof with that proof's own vkey digest. This is the meaningful capability. One settlement event can attest a mixed batch of an identity screen, an age proof, and an ML inference, drawn from unrelated circuits, in a single on-chain verification.
---
## 5. Proven results
### 5.1 First aggregation, n=3 (`proof-aggregator.json`)
One zkscreen, one over18, one zkml-mnist folded into one Groth16 proof.
- composite `0x8e246396a2f40c837501d06c965163db644284ffc877d7cd0d38681b612b4e36`
- on-chain verify tx `0xde4c102ab86567bfb7b384a784c049bf6ded33378e0b71acc0bfe97b7b8d4b39`, block 8811943
- on-chain gas used: **387,858**, result `true`
- aggregation program registered via tx `0x223960cbd60299dc9e09d1609d9be6195067257e70894f1df4460b4bc5e4876c`; inner programs registered for zkscreen, over18, and zkml-mnist.
### 5.2 Aggregation at scale, n=10 (`proof-aggregator-scale.json`)
Four zkscreen, four over18, two zkml-mnist, each instance a distinct statement (distinct derived user address, attestation time, and credential value), folded into one Groth16 proof.
- composite `0x11ee0c089c223aca62a6db85a3299e9d8160c72bdeb83e84e44642a4a37aa1fb`, committed as the proof's 32-byte public values, proof selector `0x4388a21c`
- record tx `0xd1fd4d60635fbafe8b269ea156a6bbc65e2b6ff8b886eb7910569a9c9344d978`, block 8930005
- on-chain gas used: **393,844**, status 1
- read-back: `exists = true`, `count = 10`, `recognized = 10` (all ten inner programs registered), `aggregationCount` now 2
- tamper checks: tampered proof reverts; dropped child reverts
Off-chain proving timings for the n=10 run: inner proofs about 988 seconds total, the Groth16 aggregation about 526 seconds, total wall time about 1549 seconds. Per-program the cost is uneven: each zkscreen and over18 inner proof took roughly 71 to 75 seconds, while each zkml-mnist inner proof took roughly 200 to 205 seconds. The two ML proofs alone account for over 400 of the 988 inner-proving seconds, which is worth noting for anyone planning a batch mix.
---
## 6. Constant on-chain cost
The headline is that the on-chain cost of recording an aggregation is essentially flat in N. The two measured points bracket a 3.3x increase in N:
| N | on-chain gas | tx |
|---|---|---|
| 3 | 387,858 | `0xde4c102a...` |
| 10 | 393,844 | `0xd1fd4d60...` |
Going from three to ten folded proofs raised on-chain gas by about 5,986, roughly 1.5 percent, for more than triple the work folded underneath. The bulk of the cost, roughly 390k gas, is the fixed Groth16 verification through the gateway, which is independent of N. The small residual growth comes from the parts that do scale with N: the calldata for the two `bytes32[]` arrays, the SHA-256 recompute of the composite over N entries, and the loop that counts recognized programs. None of that is the proof check. As N grows further, the per-record gas stays anchored near the single Groth16 verify cost; the marginal on-chain cost of one more folded proof is a few hundred gas of calldata and hashing, not another proof verification. Every record is comfortably under the EIP-7825 (Fusaka) per-transaction gas cap of 2^24 = 16,777,216; the driver estimates gas and asserts it is under the cap before sending.
---
## 7. The amortization argument
The value of aggregation is that many proofs settle behind one verification, on two axes.
**On-chain gas.** The honest baseline is: how much would it cost to get the same N proofs verified on-chain individually? SP1 Groth16 verification through the gateway is on the order of 300k gas per proof. Ten separate verifications is therefore on the order of 3,000,000 gas, before per-record storage overhead. The single aggregated record cost 393,844 gas. That is roughly an 87 percent reduction at n=10, and the saved fraction grows with N, because the aggregated cost is fixed while the individual-verification baseline is linear. Put as a marginal cost: an individually verified proof costs about 300k gas; a folded proof costs a few hundred gas of extra calldata and hashing on top of a shared, one-time 390k.
**Off-chain proving.** The saving is not only gas. Inner proofs are produced as compressed STARKs; only Groth16 and Plonk proofs are gateway-verifiable on-chain. To verify each inner proof individually on-chain, each would need its own Groth16 wrap, and a Groth16 wrap is the expensive step (the single aggregation wrap here took about 526 seconds). Aggregation replaces N Groth16 wraps with exactly one wrap of the recursion. So the amortization is real on both sides: one Groth16 wrap and one on-chain verify stand in for N of each.
**The trade, stated plainly.** Aggregation moves cost from on-chain gas and per-proof settlement events to off-chain proving latency and to a single coarse-grained record. It is the right tool when many application proofs can share one settlement point and one verification event (a periodic attestation batch, a compliance roll-up, a mixed identity-and-inference bundle). It is the wrong tool when each proof must be independently and immediately queryable on-chain with its own stored record, because the aggregator persists one composite, not N per-statement payloads. The inner `(vkey, publicValues-hash)` pairs are recoverable from the `AggregationVerified` event's `innerVKeyDigests` array, but the statement semantics are not stored in contract state.
---
## 8. What is and is not proven
**Proven, on-chain, recorded in the repo:**
- Recursive verification of ten real, heterogeneous inner SP1 proofs (three distinct programs) folded into one Groth16 proof, verified once on-chain.
- The composite binding is sound and enforced: the contract recomputes it from caller-supplied inner data and rejects any mismatch; tampering the proof or altering the claimed inner set reverts.
- On-chain cost is flat in N to within about 1.5 percent across a 3.3x increase, anchored at the single Groth16 verify cost.
- Every folded program's ELF was checked against its published on-chain vkey before proving, and the aggregation guest was checked against its registered vkey.
**Not proven, and not claimed:**
- No run beyond n=10 is recorded on-chain yet. n=16 is in progress.
- The scale-run inner statements are deterministically derived for the demonstration, not organic traffic from ten distinct users.
- This is not a production rollup or a sequencing pipeline. It is an attestation aggregator for application-layer proofs. (AERE's separate rollup-validity work, the bounded-VM `AereRollupValidity` and the newer full-EVM `AereEVMValidity` anchor, is documented elsewhere and is distinct from this contract.)
- No external audit. Three-validator, single-operator chain.
---
## 9. Trust boundary
The cryptographic check is the immutable concrete Groth16 verifier the gateway routes to. Neither the aggregator nor the gateway can forge a verification; both revert on an invalid proof. Two centralization facts remain honest to state. First, the gateway is `Ownable` (Foundation), so route management is Foundation-controlled, though a route can only point at a real verifier, never fabricate a pass. Second, `registerAggProgram` and `registerInnerProgram` are owner-only, so the set of aggregation programs the contract will route, and the human labels attached to inner digests, are Foundation-curated. The `recognized` counter reflects that curation and nothing stronger: an unrecognized-but-cryptographically-valid aggregation would still verify and record, it would simply show `recognized < count`. `recordAggregation` itself is permissionless: anyone holding a valid aggregation proof can record it without a Foundation signature.
---
## 10. Post-quantum note
The aggregation soundness rests on Groth16 over BN254 and on the SP1 recursion, both of which rely on classical hardness assumptions (elliptic-curve pairings and the underlying STARK-to-SNARK stack). They are not post-quantum. This is the standard trade for succinct, constant-size on-chain verification today. AERE's post-quantum hedging lives in a separate signature-verification suite (the Falcon, ML-DSA, SLH-DSA, and XMSS verifiers) rather than in the proof-aggregation path. A future migration of the succinct verifier to a post-quantum SNARK would be a wholesale swap of the concrete verifier behind the gateway, which the route-by-selector design is built to accommodate without redeploying the aggregator.
---
## 11. Roadmap
- Record n=16 on-chain (in progress) and confirm the on-chain gas stays anchored near the single Groth16 verify cost.
- Fold additional registered programs already wired into the host (`eujur`, `accred`) to widen the heterogeneous mix beyond the current three.
- Add an optional per-statement event index for consumers that need per-proof queryability without re-supplying inner data.
- Promote a documented batch-attestation flow (a scheduled aggregation of a period's application proofs) once organic proof traffic exists to batch.