Aggregation Pipeline
Binary tree aggregation from N images to a single root proof.
The previous section described how Apertrue generates two proofs per image — ProofA and ProofB — running in parallel. But a user uploading 8 images would produce 16 individual proofs. Sending all of them to the backend and blockchain individually would be expensive: 16 verification calls, 8 separate on-chain transactions, and 8 entries in the smart contract.
The aggregation pipeline solves this by combining all proofs into a single proof. One aggregated proof covers the entire batch — one backend verification, one blockchain transaction, one on-chain record — regardless of how many images the batch contains.
Two-stage architecture
Aggregation happens in two stages, each using a dedicated Noir circuit:
- Image aggregation. For each image, the
ImageAggregatorcircuit takes that image's ProofA and ProofB, verifies them recursively, checks the link commitment match, and produces a singleimage_commitment— one field element representing a fully verified image. - Tree aggregation. The
TreeAggregatorcircuit takes pairs of proofs and combines them into a binary tree. At each level, two child commitments are hashed together. This repeats until a singleroot_commitmentremains at the top — the final proof covering all images in the batch.
Image aggregator
The image aggregator is the first stage. It takes one image's split proofs and produces a single commitment:
- Verify ProofA recursively. The circuit calls
verify_honk_proofon ProofA's 500-field ZK proof, verification key, and 55 public values. This performs UltraHonk verification inside the circuit. - Verify ProofB recursively. Same process for ProofB's proof, VK, and 9 public values.
- Assert shared inputs match. Both proofs must agree on
trust_list_root,content_hash,nullifier,proof_timestamp, andclaim_hash_commitment. A mismatch means the proofs were generated for different images. - Check link commitment. Assert that ProofA's link commitment output equals ProofB's link commitment output. This is the cryptographic binding that prevents proof substitution attacks.
- Compute image commitment. Hash nine values together using Poseidon2:
The image_commitment is a single field element that uniquely represents a verified image. It carries forward all the security properties from both split proofs — trust list membership, certificate chain, hash chain, COSE signature, time and location range proofs — compressed into 32 bytes.
Tree aggregator
The tree aggregator is a single circuit reused at every level of the binary tree. It takes two child proofs and produces one combined proof:
- Verify left child. Recursively verifies the left child's proof (either an image aggregator proof or a previous tree aggregator proof).
- Verify right child. Same for the right child.
- Combine commitments. Extract each child's commitment and hash them together:
The tree aggregator has a uniform public interface: 9 padding inputs (all zeros) plus 1 output (the combined commitment). This makes the interface identical to the image aggregator's layout, so verify_honk_proof works the same way at every tree level — a tree aggregator can verify either image aggregator proofs or other tree aggregator proofs without special handling.
Logarithmic scaling
| Images | Image agg proofs | Tree levels | Tree agg proofs | Total proofs |
|---|---|---|---|---|
| 1 | 1 | 1 (self-pair) | 1 | 2 |
| 2 | 2 | 1 | 1 | 3 |
| 4 | 4 | 2 | 3 | 7 |
| 8 | 8 | 3 | 7 | 15 |
| 10 | 10 | 4 | 9 | 19 |
The tree depth grows as O(log N). Doubling the number of images adds just one more tree level. This makes aggregation practical even for large batches.
Handling odd counts
When a tree level has an odd number of proofs, the last proof is promoted to the next level without being paired. It carries its existing proof, verification key, and commitment unchanged. This avoids duplicating a node (which would corrupt the commitment structure by making one image appear twice in the tree).
The tree aggregator accepts separate verification keys for left and right children, specifically to handle this case — a promoted image aggregator proof might be paired with a tree aggregator proof at the next level, requiring different VKs.
Single-image self-pairing
When a batch contains only one image, the tree aggregator still runs — pairing the image commitment with itself: Poseidon2([image_commitment, image_commitment], 2). This ensures the final proof is always a tree aggregator proof, giving the backend a uniform verification interface regardless of batch size.
The root commitment
The root_commitment is the single field element at the top of the aggregation tree. It is the public output of the final tree aggregator proof and represents the entire batch of verified images.
- The backend stores it as the primary key for the batch — one row in the database covers all images.
- The Aztec smart contract receives it as the sole data payload — one private transaction, one on-chain record.
- Individual image provenance can still be verified via Merkle paths from each image commitment to the root.
Memory management
Aggregation workers are expensive — each requires ~350MB of physical memory for its Barretenberg WASM instance and circuit data. Split-proof workers cost ~400MB each. Running both simultaneously would exhaust the browser's memory budget on most devices.
The system uses a phased approach:
- Phase 1 — split proofs. All split-proof workers run, generating ProofA and ProofB for each image. On mid and low-end devices, no aggregation workers exist during this phase.
- Release. After all split proofs complete, the prover pool terminates all split workers, freeing ~400MB per worker.
- Phase 2 — aggregation. Aggregation workers spin up using the freed memory. The number of workers depends on the hardware tier and available budget.
The number of workers in each phase depends on the device's hardware tier and memory budget. On high-end devices, the system supports pipelining — starting image aggregation as soon as each image's split proofs finish, overlapping Phase 1 and Phase 2. The full hardware tier table and worker allocation strategy are in Browser Proving Engine.
Recursive proof format
The aggregation circuits use recursive verification — they verify ZK proofs inside other ZK proofs. This is what makes aggregation possible: instead of the backend verifying 16 individual proofs, it verifies one proof that internally verified all 16.
The proof format is UltraHonk ZK (500 field elements, 16,000 bytes). Each field element is 32 bytes. The verification key is 115 field elements. When a split worker generates proofs with the recursive flag, it produces these artefacts:
- Proof bytes chunked into 500 field elements
- Verification key (115 fields)
- VK hash (for binding the specific circuit)
- All public values (inputs + outputs)
The verify_honk_proof function in Noir takes these as inputs and performs full UltraHonk verification inside the circuit. This is computationally cheap relative to the original proof generation — verification is roughly 100x faster than proving.
Self-verification
Every proof is self-verified immediately after generation, before being passed to the next stage. The split worker verifies each ProofA and ProofB locally. The aggregator worker verifies each image aggregation and tree aggregation result. Self-verification catches format errors, field count mismatches, or circuit bugs before a corrupted proof propagates up the tree.
Backend verification
The final aggregated proof is submitted to the backend as a single request containing the proof bytes, root commitment, image count, per-image content hashes, and per-image nullifiers. The backend's verification pipeline:
- Format validation. Root commitment must be 64 hex chars. Content hash and nullifier counts must match the declared image count. Maximum batch size: 1024 images.
- Trust list check. The proof's trust list root must match the Oracle's current bundle root.
- Cryptographic verification. The proof is forwarded to the ZK verifier sidecar, which runs Barretenberg's
verifywith the tree aggregator verification key. - Atomic storage. In a single database transaction: insert the aggregated proof record (keyed by root commitment), insert all nullifiers (for replay prevention). If any nullifier is already used, the entire transaction rolls back.
If blockchain submission is requested, the backend returns the tree aggregator verification key and VK hash, which the browser uses to submit an Aztec private transaction containing the root commitment. The on-chain record is covered in On-Chain Verification.
The next section explains how the browser proving engine manages all of this — the hardware detection, worker allocation, circuit loading, and orchestration that makes proof generation practical on consumer devices.