Skip to content

Wormhole Aggregation Level 2#618

Open
illuzen wants to merge 3 commits into
mainfrom
illuzen/wormhole-aggregation-level-2
Open

Wormhole Aggregation Level 2#618
illuzen wants to merge 3 commits into
mainfrom
illuzen/wormhole-aggregation-level-2

Conversation

@illuzen

@illuzen illuzen commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Teaches pallet-wormhole to 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 would
normally 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 renamed
    verify_private_batch (formerly verify_aggregated_proof).

Segment-based exit bundles

  • Validation and execution are refactored around 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.
  • Segment-level denial: a segment whose nullifiers are already used (or collide
    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.
  • Inert (dummy) segments: dummy-padded segments (all-zero nullifiers) are
    skipped entirely — no mints, no fee accounting, no nullifier storage. Zero
    nullifiers are never written to UsedNullifiers.

Aggregator incentive

  • New config item 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_address instead of being burned. The miner share is unchanged, so
    aggregation is strictly fee-positive for the aggregator and strictly
    burn-reducing for users.

Transaction pool

  • validate_unsigned handles both call variants; priority is computed from the
    total value of valid, non-inert segments only, so a mostly-replayed bundle
    can't jump the queue.

Build / infra

  • build.rs generates and embeds the public-batch verifier binaries and exposes
    circuit 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 the qp-wormhole-* /
    qp-zk-circuits-common crates at the local ../qp-zk-circuits checkout.
    Must be switched to published crate versions before merge.
  • Placeholder weight for verify_public_batch (25s ref-time upper bound);
    real benchmarking to follow.

Test plan

  • cargo test -p pallet-wormhole --release: new public_batch_proof_tests cover
    PI parsing, wrong origin, malformed bytes, replay prevention, and a success path
    asserting the aggregator rebate is paid and dummy segments are skipped.
  • Fixtures regenerated (test-data/private_batch.hex, test-data/public_batch.hex)
    via the regenerate_*_fixture ignored tests.
  • End-to-end against --dev node via quantus-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) alongside verify_private_batch, which replaces verify_aggregated_proof. Naming and ZK crates move from layer-0/layer-1 to private batch / public batch, with [patch.crates-io] temporarily pointing qp-wormhole-* at a local circuits checkout until 3.1.0 ships.

build.rs now generates and embeds public-batch verifier binaries, passes QP_NUM_PRIVATE_BATCH_PROOFS (default 4), and emits NUM_LEAF_PROOFS / NUM_PRIVATE_BATCH_PROOFS for 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. SegmentsDenied is emitted when some segments are dropped. Pool validate_unsigned priority counts only valid, non-inert segment value.

VolumeFeesAggregatorRate redirects part of the volume-fee burn bucket to the proof’s aggregator_address on public batches (miner share unchanged). Errors and weights are generalized (VerifierNotAvailable, verify_public_batch placeholder ~25s ref-time). Tests and fixtures move to private_batch.hex / public_batch.hex with new public-batch coverage.

Reviewed by Cursor Bugbot for commit b7b043f. Configure here.

illuzen and others added 2 commits July 6, 2026 16:16
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Triggered by learned rule: Wormhole verify_aggregated_proof weight requires manual DB augmentation

Reviewed by Cursor Bugbot for commit b7b043f. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant