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

18 KiB

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.