Wormhole Aggregation Level 2#618
Conversation
…tch terminology Rename the extrinsic surface (verify_aggregated_proof -> verify_private_batch, errors, weights, embedded binary names) to match the circuits repo's new private/public batch terminology, and extract exit processing into process_exit_bundle operating on an ExitBundle of deniable segments. A private batch parses to a single segment today; public batches will map one segment per inner private batch, enabling partial acceptance when a segment's nullifiers were already spent. Point crates-io patches at the local qp-zk-circuits checkout until 3.1.0 is published. Co-authored-by: Cursor <cursoragent@cursor.com>
Embeds the public-batch verifier (build.rs now passes num_private_batch_proofs) and parses public-batch PIs into per-segment exit bundles. Dummy-padded segments (all-zero nullifiers) are inert: skipped during processing, excluded from priority, and their zero nullifiers never stored. For public batches, VolumeFeesAggregatorRate (50%) of the burn bucket is minted to the proof's aggregator address, leaving the miner share untouched. Includes a real public-batch test fixture and regeneration test. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b7b043f. Configure here.
| Weight::from_parts(25_000_000_000, 200000) | ||
| .saturating_add(T::DbWeight::get().reads(200_u64)) | ||
| .saturating_add(T::DbWeight::get().writes(170_u64)) | ||
| } |
There was a problem hiding this comment.
Public batch weight undercounts storage
Medium Severity
verify_public_batch reuses the private-batch hand-augmented storage tail (200 reads / 170 writes) sized for one inner batch, but default build.rs sizing allows up to four private-batch segments with seven leaves each. A full public-batch success can mint and record_transfer for many more exit slots than that model accounts for, so declared weight can be materially lower than actual work.
Triggered by learned rule: Wormhole verify_aggregated_proof weight requires manual DB augmentation
Reviewed by Cursor Bugbot for commit b7b043f. Configure here.


Summary
Teaches
pallet-wormholeto verify second-layer ("public batch") aggregation proofs.A public batch bundles M private-batch proofs into one transaction and names an
aggregator_address; as an incentive to aggregate, part of the fee that wouldnormally be burned is redirected to that address. Verification is segment-aware:
individual private-batch segments can be denied (replayed nullifiers) or skipped
(dummy padding) without rejecting the whole proof.
Changes
New extrinsic
verify_public_batch(proof)(unsigned, call index 3), alongside the renamedverify_private_batch(formerlyverify_aggregated_proof).Segment-based exit bundles
ExitBundle/ExitSegment:both extrinsics parse their proof's public inputs into a normalized bundle where
each segment corresponds to one private batch and is accepted or denied atomically.
with another segment in the same bundle) is denied; the rest of the bundle still
executes. This also closes the double-mint that would otherwise arise from
including the same private-batch proof in a public batch twice, since the circuit
forwards nullifiers without deduplication.
skipped entirely — no mints, no fee accounting, no nullifier storage. Zero
nullifiers are never written to
UsedNullifiers.Aggregator incentive
VolumeFeesAggregatorRate(runtime: 50%). For public batches,that share of the burn portion of the 0.1% volume fee is paid to the proof's
aggregator_addressinstead of being burned. The miner share is unchanged, soaggregation is strictly fee-positive for the aggregator and strictly
burn-reducing for users.
Transaction pool
validate_unsignedhandles both call variants; priority is computed from thetotal value of valid, non-inert segments only, so a mostly-replayed bundle
can't jump the queue.
Build / infra
build.rsgenerates and embeds the public-batch verifier binaries and exposescircuit sizing constants (
NUM_LEAF_PROOFS,NUM_PRIVATE_BATCH_PROOFS,overridable via
QP_NUM_PRIVATE_BATCH_PROOFS) for public-input parsing.[patch.crates-io]temporarily points theqp-wormhole-*/qp-zk-circuits-commoncrates at the local../qp-zk-circuitscheckout.Must be switched to published crate versions before merge.
verify_public_batch(25s ref-time upper bound);real benchmarking to follow.
Test plan
cargo test -p pallet-wormhole --release: newpublic_batch_proof_testscoverPI parsing, wrong origin, malformed bytes, replay prevention, and a success path
asserting the aggregator rebate is paid and dummy segments are skipped.
test-data/private_batch.hex,test-data/public_batch.hex)via the
regenerate_*_fixtureignored tests.--devnode viaquantus-cli wormhole multiround --public.Note
High Risk
Changes ZK proof verification, minting, fee burn/miner/aggregator splits, and nullifier handling on a security-critical pallet; segment denial and temporary local circuit patches add integration and supply-accounting risk until published crates and real benchmarks land.
Overview
Adds second-layer wormhole aggregation on-chain:
verify_public_batch(call index 3) alongsideverify_private_batch, which replacesverify_aggregated_proof. Naming and ZK crates move from layer-0/layer-1 to private batch / public batch, with[patch.crates-io]temporarily pointingqp-wormhole-*at a local circuits checkout until 3.1.0 ships.build.rsnow generates and embeds public-batch verifier binaries, passesQP_NUM_PRIVATE_BATCH_PROOFS(default 4), and emitsNUM_LEAF_PROOFS/NUM_PRIVATE_BATCH_PROOFSfor PI parsing at runtime.Validation and execution are refactored around
ExitBundle/ExitSegment: both extrinsics normalize proofs into per–private-batch segments. Segment-level denial skips whole segments when nullifiers are already used or collide within the bundle (partial success for public batches); inert dummy segments are ignored.SegmentsDeniedis emitted when some segments are dropped. Poolvalidate_unsignedpriority counts only valid, non-inert segment value.VolumeFeesAggregatorRateredirects part of the volume-fee burn bucket to the proof’saggregator_addresson public batches (miner share unchanged). Errors and weights are generalized (VerifierNotAvailable,verify_public_batchplaceholder ~25s ref-time). Tests and fixtures move toprivate_batch.hex/public_batch.hexwith new public-batch coverage.Reviewed by Cursor Bugbot for commit b7b043f. Configure here.