Proof System

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:

  1. Image aggregation. For each image, the ImageAggregator circuit takes that image's ProofA and ProofB, verifies them recursively, checks the link commitment match, and produces a single image_commitment — one field element representing a fully verified image.
  2. Tree aggregation. The TreeAggregator circuit takes pairs of proofs and combines them into a binary tree. At each level, two child commitments are hashed together. This repeats until a single root_commitment remains at the top — the final proof covering all images in the batch.
Two-stage aggregation for 4 images: bottom level has 4 image boxes each with ProofA + ProofB, ImageAgg produces image commitments IC1-IC4, Level 1 TreeAgg combines IC1+IC2 into C12 and IC3+IC4 into C34, Level 2 TreeAgg combines C12+C34 into the root_commitment. O(log N) depth: 4 images need 2 tree levels.

Image aggregator

The image aggregator is the first stage. It takes one image's split proofs and produces a single commitment:

  1. Verify ProofA recursively. The circuit calls verify_honk_proof on ProofA's 500-field ZK proof, verification key, and 55 public values. This performs UltraHonk verification inside the circuit.
  2. Verify ProofB recursively. Same process for ProofB's proof, VK, and 9 public values.
  3. Assert shared inputs match. Both proofs must agree on trust_list_root, content_hash, nullifier, proof_timestamp, and claim_hash_commitment. A mismatch means the proofs were generated for different images.
  4. 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.
  5. Compute image commitment. Hash nine values together using Poseidon2:
Image commitment computation
image_commitment = Poseidon2([
    trust_list_root,         // which trust list was used
    content_hash,            // SHA-256 of the image bytes
    nullifier,               // replay prevention identifier
    proof_timestamp,         // when the proof was generated
    location_commitment,     // Poseidon2 hash of exact GPS (from ProofA)
    time_commitment,         // Poseidon2 hash of exact capture timestamp (from ProofA)
    edit_time_commitment,    // Poseidon2 hash of edit/export timestamp (0 if no edit)
    link_commit,             // binding between ProofA and ProofB
    actions_hash_field       // Poseidon2 hash of C2PA actions assertion
], 9)

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:

  1. Verify left child. Recursively verifies the left child's proof (either an image aggregator proof or a previous tree aggregator proof).
  2. Verify right child. Same for the right child.
  3. Combine commitments. Extract each child's commitment and hash them together:
Tree commitment combination
combined_commitment = Poseidon2([commitment_left, commitment_right], 2)

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

ImagesImage agg proofsTree levelsTree agg proofsTotal proofs
111 (self-pair)12
22113
44237
883715
10104919

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.
Key Insight
The root commitment is a cryptographic commitment to every image in the batch. Changing a single pixel of a single image would change that image's content hash, which would change its image commitment, which would propagate up the tree and change the root commitment. The root is a tamper-evident seal over the entire batch.

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:

  1. 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.
  2. Release. After all split proofs complete, the prover pool terminates all split workers, freeing ~400MB per worker.
  3. 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:

  1. 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.
  2. Trust list check. The proof's trust list root must match the Oracle's current bundle root.
  3. Cryptographic verification. The proof is forwarded to the ZK verifier sidecar, which runs Barretenberg's verify with the tree aggregator verification key.
  4. 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.
Note
Nullifier uniqueness is enforced atomically with the proof record. If a nullifier has been seen before, the entire batch is rejected. The nullifier derivation and its privacy properties are detailed in Privacy Model.

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.