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:
- recomputes
P = Poseidon2(values)— the Action circuit's single public output — and binds the Action proof to it viaverify_honk_proof(action_vk, proof, [P], action_vk_hash)(so the 19 values are exactly what the Action attested, by Poseidon2 collision resistance); - computes
D = sha256_action_digest(values)— the SHA-256 per-action digest the contract reconstructs from calldata; - 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.