Skip to main content

Aggregation

Submitting N Action proofs on-chain would cost roughly N × one verification. Aggregation collapses all N into one on-chain verification. Two binary circuits build the tree; finalize wraps the root.

The two binary circuits

For N Actions the tree is ⌈log₂ N⌉ layers deep above the leaves: N = 4 → leaves + 1 inner; N = 18 → leaves + 2 inner layers. Odd counts promote the trailing node unchanged; an odd Action at layer 0 pairs with itself.

recursive_2_leaf

The leaf does more than fold. Each Action's 19 canonical fields are passed as private witnesses (values_1, values_2); for each child the leaf:

  1. recomputes P = Poseidon2(values) — the Action circuit's single public output — and binds the Action proof to it via verify_honk_proof(action_vk, proof, [P], action_vk_hash) (so the 19 values are exactly what the Action attested, by Poseidon2 collision resistance);
  2. computes D = sha256_action_digest(values) — the SHA-256 per-action digest the contract reconstructs from calldata;
  3. folds sha256_node(D_1, D_2) into the tree.

Public inputs are [action_vk_hash, leaf_vk_hash, inner_vk_hash, output_digest]. The three VK hashes are carried as anchors so parent layers can assert them.

recursive_2_inner

Each inner node verifies two children, where each child is a leaf or an inner proof. Both child types share the identical 4-field public layout, so one INNER_PUB_INPUTS = 4 ABI works at every depth.

VK pinning

A naive recursive circuit would verify against whatever VK the prover supplies — a malicious bundler could pin a permissive "always-true" VK and aggregate fake Actions. Platus prevents this by pinning three VK hashes in the contract (PinnedVkHashes):

Odd batches

// layer 0: if N is odd, the last Action pairs with itself
const b = (2*i + 1 < n) ? actions[2*i + 1] : actions[2*i];

A duplicated Action appears twice in the proof tree but only once in calldata. The contract validates each nullifier independently and spends it once, so the duplication is harmless.