Skip to content

Add opt-in public-batch aggregator to the node - #716

Open
illuzen wants to merge 9 commits into
mainfrom
feat/public-batch-aggregator
Open

illuzen wants to merge 9 commits into
mainfrom
feat/public-batch-aggregator

Conversation

@illuzen

@illuzen illuzen commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Depends on #711 (fix/wormhole-pool-admission-nullifier-reads, from a fork so this can't be stacked on it directly). This branch is cut from that branch and includes its commits; the aggregator-specific change is the top commit (d0aa8fb). Review that commit, or wait for #711 to land and this diff collapses to it.

What

An opt-in mode for quantus-node that turns it into a wormhole public-batch aggregator:

quantus-node --public-batch-aggregator --public-batch-aggregator-address <SS58> [...]

Private-batch proofs that reach this node (gossip or RPC) are intercepted at transaction-pool submission, pooled, proved into a public batch bound to the configured aggregator address, and the public batch is submitted instead. Claimed private batches never enter the transaction pool, so this node neither propagates nor authors them.

Admission

#711 moved the ZK verify and UsedNullifiers reads out of validate_unsigned, so pool admission alone no longer says anything about whether a private batch would make it into a block. The interceptor re-runs the author's checks before taking custody of a proof:

  1. runtime TaggedTransactionQueue::validate_transaction (structural + stateless binding checks),
  2. UsedNullifiers storage read for each nullifier at the current best block,
  3. ProofPool::push (full plonky2 verify, budget, pool-wide nullifier dedupe).

Anything that fails falls through to the normal pool untouched. TransactionSource::InBlock and submit_and_watch are never intercepted.

Custody and eviction

Buckets are keyed by (block_hash, asset_id, volume_fee_bps). On each new best block the worker evicts proofs whose nullifiers appear in ProofVerified events, drops proofs older than --public-batch-proof-expiry (default: the BlockHashCount acceptance window), and discards buckets anchored to a block that is no longer canonical. Before submitting a proved batch it re-checks each segment's nullifiers and skips the submission if nothing is left to settle.

Policy

A bucket is proved when it is full, or when it holds >= --public-batch-min-proofs and its oldest proof is older than --public-batch-max-wait. One proof at a time; a bucket snapshotted less than --public-batch-reprove-after ago is left alone.

Changes

  • client/transaction-pool: SubmissionInterceptor hook on TransactionPoolWrapper + Builder::with_submission_interceptor.
  • pallets/wormhole: build.rs emits a dummy private-batch proof for std builds; embedded_artifacts exposes the private-batch common/verifier/dummy bytes; UsedNullifiers is pub.
  • qp-wormhole-* / qp-zk-circuits-common bumped 4.3.0 → 4.4.0 for the reusable PublicBatchProver::new_from_bytes / prove_batch(&self). No circuit changes; embedded verifier bytes are unchanged.
  • node/src/public_batch_aggregator/: interceptor, worker (prover warm-up, eviction, policy, prove + submit), metrics (quantus_public_batch_*).
  • CLI flags + service wiring; docs in docs/wormhole-zk.md and MINING.md.

Caveats

  • Gossip interception is leaky: other nodes still relay and include private batches they hear about directly. This captures only the traffic that reaches the aggregator before inclusion.
  • Buckets are keyed by block_hash, so clients proving against different anchors won't batch together. A client-side anchor convention is a follow-up.
  • Prover build takes a while at startup; the node logs when proving is ready. A prover build failure shuts the node down (essential task) rather than silently pooling proofs.

Testing

  • ./clippy.sh clean (taplo not installed locally).
  • cargo test -p quantus-node -p sc-transaction-pool -p pallet-wormhole pass; new unit tests cover CLI config parsing/validation, extrinsic recognition, and the proving policy predicate.
  • cargo check -p quantus-runtime (wasm build, pallet no_std) passes.
  • Not exercised end to end against a live network; the wrapper splice path in submit_at has no unit test (the fork's test harness isn't wired for a custom client).

mnm458 and others added 7 commits October 11, 2026 13:39
Address review on #711 — the CI `cargo clippy -- -D warnings` job failed
with three errors the test/doctest jobs didn't surface:

- `validate_exit_bundle_common` became dead code in the non-benchmark lib
  build once both production callers inlined the stateful checks. Restore its
  shared production use: the full `pre_validate_{private,public}_batch_proof`
  call it again, and the structural validators are now parse-only, with
  `validate_unsigned` adding `validate_exit_bundle_stateless` itself. Each path
  still runs the stateless checks exactly once (no double work), and admission
  still performs no per-nullifier reads.
- `clippy::type_complexity` on both structural validators' return tuples:
  extract a shared `StructurallyValidatedProof` type alias, mirroring the
  existing `PreValidatedProof`.

Admission and inclusion behavior are unchanged. cargo clippy --locked -p
pallet-wormhole --lib -- -D warnings, scripts/fmt.sh --check, and the 85-test
lib suite all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dispatch reliance

Splitting the per-nullifier pass off pool admission also dropped the
stateless half of `ensure_any_segment_valid`. Restore it: admission now
rejects a bundle no segment of which could mint even with every nullifier
fresh (all-dummy padding, or real segments whose slots all exit zero),
without reading `UsedNullifiers`. Such junk carries fresh, distinct
nullifiers, so it would otherwise earn a distinct pool tag per blob and
reach the block author.

Also state in the rustdoc of `verify_private_batch` / `verify_public_batch`
that the body does not run the ZK verifier and relies on
`ValidateUnsigned::pre_dispatch` having done so for a bare unsigned
extrinsic.

New test `validate_unsigned_rejects_bundle_with_no_mintable_slot` zeroes
every exit-slot amount in the fixture's public inputs and asserts admission
rejects it.

Co-authored-by: Cursor <cursoragent@cursor.com>
…patch

The settlement dispatchables do not run the ZK verifier; it runs in
`ValidateUnsigned::pre_dispatch`, which the executive invokes only for a bare
unsigned extrinsic. That makes "no mint without verify" a runtime property, so
pin it at the runtime layer with the pallet's real private-batch fixture:

- a genuine proof settles through `Executive::apply_extrinsic` (control);
- the same proof with corrupted proof bytes and intact public inputs — the
  shape that passes every structural check — is rejected by the executive
  with `InvalidTransaction::Call`, mints nothing, and marks no nullifier;
- dispatched directly with a `None` origin (bypassing `pre_dispatch`) the
  tampered proof mints, documenting why no in-runtime path may supply `None`;
- `Root` and `Signed` origins are refused, `batch_all` cannot deliver `None`
  from either a `None` or `Root` outer origin, and the tech-referenda lane
  rejects `None` as a proposal origin;
- an exhaustive match on `pallet_utility::Call` stops compiling if a
  `dispatch_as`-style combinator is reintroduced.

Adds `hex` and `qp-wormhole-verifier` as runtime dev-dependencies to parse the
fixture's public inputs.

Co-authored-by: Cursor <cursoragent@cursor.com>
…marker

`verify_private_batch` / `verify_public_batch` do not run the ZK verifier;
only `ValidateUnsigned::pre_dispatch` does, and the executive invokes that
solely for bare unsigned extrinsics. Until now "no mint without verify"
therefore depended on no other `None`-origin dispatch path existing in the
runtime.

Make it a pallet property instead:

- `pre_dispatch` writes `VerifiedProofHash = blake2_256(proof_bytes)` after
  a successful verify.
- Both dispatchables consume the marker right after `ensure_none` and fail
  with the new `Error::ProofNotVerified` (weight: one read, `Pays::No`) if
  it is missing or bound to different bytes.
- Weight accounts for the marker handoff (1 read, 2 writes).

Tests: pallet tests for missing marker, mismatched bytes, full
pre_dispatch -> dispatch flow consuming the marker, and that an
unverifiable proof cannot be settled by any route; runtime test asserting a
direct `None`-origin dispatch of the real fixture is refused. Docs updated.

Co-authored-by: Cursor <cursoragent@cursor.com>
… marker

Public items (ProofNotVerified, verify_*_batch) linked to the pub(super)
storage item, failing rustdoc::private_intra_doc_links under -D warnings.

Co-authored-by: Cursor <cursoragent@cursor.com>
A node started with --public-batch-aggregator intercepts private-batch
wormhole proofs at transaction-pool submission, re-runs the block author's
admission checks (runtime validate_transaction, UsedNullifiers, full ZK
verify), pools the survivors by (block_hash, asset_id, volume_fee_bps) and
proves them into a public batch bound to the configured aggregator address.
Claimed proofs never enter the transaction pool, so they are neither
propagated nor authored by this node.

- forked sc-transaction-pool: SubmissionInterceptor hook on the wrapper/builder
- pallet-wormhole: embed private-batch common/verifier bytes and a dummy
  private-batch proof for std builds; UsedNullifiers made pub
- bump qp-wormhole-* / qp-zk-circuits-common to 4.4.0 for the reusable
  PublicBatchProver (no circuit changes)
- eviction on new best block from ProofVerified events, expiry, and
  non-canonical anchors; pre-submit live-segment check
- Prometheus metrics under quantus_public_batch_*

Co-authored-by: Cursor <cursoragent@cursor.com>

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Request changes
Reviewer: GPT-6.1 Sol

Reviewed head d0aa8fb0b1bfdf9c98c189eb855516850ccc57f5, including the inherited #711 changes. The current base is 9e6d01f8bbefd126d87be6325e8cfc1f09825ae9; the diff merge base is 482c5b9e02bec0adc70797eade0a67f60baf2619.

  1. [P2] Keep repeat submissions of claimed proofs intercepted. node/src/public_batch_aggregator/interceptor.rs:117–138. After a proof is claimed, submitting the identical extrinsic again before settlement passes the runtime and on-chain-nullifier checks, but ProofPool::push rejects its already-staged nullifiers. This branch converts that rejection to Interception::Pass, and both wrapper submission methods then admit the private batch to the ordinary pool. The first submission never entered that pool, so its duplicate checks do not prevent admission. A normal RPC retry or subsequent gossip therefore makes this node propagate and potentially author a private batch it already claimed, defeating aggregation and wasting the public-batch proving work/rebate. The gossip handler's known-hash cache does not suppress incoming resubmissions; only concurrently pending imports are coalesced. Track the exact extrinsic hashes currently in custody and keep returning Claimed for repeats, synchronizing that tracking with eviction. Cover sequential retries through both wrapper submission paths. With the real valid private-batch fixture, I reproduced first-push success followed by the duplicate-nullifier error on the second push; source tracing confirms its fall-through to the ordinary pool.

  2. [P2] Evict proofs when their anchor leaves the runtime acceptance window. node/src/public_batch_aggregator/worker.rs:117–124 and 175–180. Expiry measures time since pool admission, while the other eviction check only asks whether the anchor is a canonical node-database header. Such a header can remain canonical after frame_system::BlockHash has pruned it, at which point the runtime rejects every proof referencing it. For example, a still-valid, unspent proof anchored at #100 can be admitted near best #4196 and become unusable a few blocks later, yet stay pooled for the entire default 49,152 seconds after admission. It remains eligible for repeated expensive proving, followed by rejected submissions; enough such proofs occupy capacity and consume the single prover while useful work waits. An isolated probe using the real proof pool and the unchanged canonicality/eligibility helpers, with a mocked canonical-header lookup, confirmed that an anchor at #100 is retained and eligible at best #4200. Check the anchor against the current runtime acceptance window during eviction and before proving, and remove buckets that can no longer settle; admission age alone cannot enforce this bound.

Validation at the reviewed head, with production circuit dimensions (7 leaves, 53 private batches):

  • SKIP_WASM_BUILD=1 cargo test --locked --offline -p quantus-node -p sc-transaction-pool -p pallet-wormhole: 259 passed, 0 failed, 3 ignored.
  • SKIP_WASM_BUILD=1 cargo test --locked --offline -p quantus-runtime --test mod --features fast-governance: 68 passed, 0 failed, 1 ignored, including the executive settlement/verification tests.
  • SKIP_WASM_BUILD=1 cargo clippy --locked --offline -p quantus-node -p sc-transaction-pool -p pallet-wormhole -- -D warnings: passed.
  • Pinned Rust formatting for the four affected crates, Taplo on the three changed manifests, and git diff --check: passed.
  • An additional --all-targets Clippy run failed on existing transaction-pool test lint violations in unchanged files; this is separate from the passing production check above.
  • Both targeted proof-pool/worker probes reproduced the findings above. No live-network aggregation run or fresh local Wasm build was performed.

GitHub CI's formatting and Clippy/documentation jobs passed at this head; the Linux and macOS build/test jobs were still running when checked.

@n13 n13 removed the bot-review label Oct 26, 2026
Nearly all private batches arrive via author_submitAndWatchExtrinsic
(subxt, quantus-cli), which bypassed the interceptor, so the aggregator
saw nothing in practice. Watched submissions are now claimable; the
status stream follows the proof's nullifiers (Ready, InBlock/Finalized
via the settling extrinsic, Dropped on unsettled eviction).

Also enable qp-plonky2's rand feature, without which building the
public-batch prover panics at startup, and cover the v4 bare extrinsic
encoding that clients actually send.

Co-authored-by: Cursor <cursoragent@cursor.com>
@illuzen

illuzen commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Pushed e3692c2 after an end-to-end run against a --dev node with the stress-test harness:

  • Watched submissions are now claimed. subxt's submit() and quantus-cli go through author_submitAndWatchExtrinsic, which the interceptor deliberately skipped, so in practice the aggregator captured nothing (all 5 test batches settled as private batches). SubmissionInterceptor gained intercept_watched; the aggregator answers with a status stream that follows the proof's nullifiers: Ready on claim, InBlock/Finalized pointing at the settling extrinsic (the public batch), Dropped if the proof is evicted unsettled, Retracted on a lost settlement block.
  • qp-plonky2 needs the rand feature in the node; without it PublicBatchProver::new_from_bytes panics (Cannot use zero_knowledge without rand feature). Added an ignored test that builds the real prover.
  • Unit test for the legacy v4 (0x04) bare extrinsic encoding clients actually send.

Result after the fix: 12 two-leaf private batches fired → 12 claimed → one public batch (84 nullifiers) proved in 78s → all 12 settled in one block, none leaked.

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Request changes
Reviewer: GPT-6.1 Sol

Reviewed head e3692c2ff3c2eb376f5bfeeee8e8907c66468b36, including the inherited #711 changes. Base: 9e6d01f8bbefd126d87be6325e8cfc1f09825ae9; diff merge base: 482c5b9e02bec0adc70797eade0a67f60baf2619. The watched-submission change fixes interception, but introduces client compatibility and reorg notification failures. Both previously reported findings also remain.

  1. [P1] Preserve the extrinsic identity promised by the standard watch RPC. watchers.rs:80–87, reached through interceptor.rs:168–171. A claimed private extrinsic never appears in the reported block: only the different public-batch extrinsic does. The legacy author_submitAndWatchExtrinsic wire format serializes InBlock((hash, index)) as just {"inBlock": hash}, so the replacement index is lost and clients receive no replacement transaction hash. Subxt retains the original hash and searches the reported block for it when fetching success/events. I executed the real Subxt 0.44.3 wait_for_finalized_success() against a mocked block containing only the replacement; it returns Transaction(BlockNotFound). quantus-cli's wait_tx_inclusion / check_execution_success likewise searches for the original hash and returns “Extrinsic hash not found in reported block.” Thus aggregation can settle funds while ordinary clients report failure. Keep standard watched submissions on a path that preserves their identity, or introduce an explicit aggregation-aware RPC/client protocol carrying settlement identity and update consumers before intercepting their standard watches. Cover the actual client success/event-fetch path, not only receipt of status messages.

  2. [P2] Keep retries of proofs already in custody intercepted. interceptor.rs:122–143. A second identical submission before settlement passes runtime and on-chain-nullifier checks, but ProofPool::push rejects its already-staged nullifiers. This becomes None / Pass, admitting the private batch to the ordinary pool through submit_one, submit_at, or now submit_and_watch. The first submission never entered that pool, so its duplicate checks cannot stop the leak. A normal RPC retry or subsequent gossip makes this node propagate and potentially author a proof it already claimed. Reproduced with the valid production fixture: first push succeeds; the second reports “a nullifier in this proof is already staged in the pool.” Track exact extrinsic identities in custody and keep claiming identical retries, attaching repeat watchers to the existing claim; synchronize identity tracking with eviction and test all three submission paths.

  3. [P2] Evict anchors that leave the runtime acceptance window. worker.rs:136–143 and 210–216. Expiry measures time since admission, while canonicality checks the node's persistent header database. A proof admitted near the end of its anchor's 4,096-block window soon becomes invalid when runtime BlockHash storage prunes that anchor, although the header remains canonical. It can then occupy capacity and repeatedly consume the single expensive prover for up to the default 49,152 seconds after admission. A probe using the real proof pool and unchanged worker helpers retained an anchor at #1 and considered it eligible at best #4101 with a mocked canonical header. Check current runtime anchor acceptance during eviction and before proving; drop buckets that can no longer settle.

  4. [P2] Process watch retractions before replacement settlements. watchers.rs:93–103. Retractions are detected only once finality reaches the old settlement height. Until then, the watcher remains in in_block, while on_settled examines only pending. Sequence: settle in A#200; reorg; settle again in B#201 before finality reaches #200; finalize the replacement chain. The B settlement is ignored, then finality moves the watcher back to pending for the already-completed spend. Running this sequence against the unchanged watcher module produces Retracted(A), no Finalized(B), and eventually Dropped after expiry despite successful settlement. Handle retractions and enacted settlements when the best chain changes, or reconcile against retained settlement records/current chain state. Include replacement settlement before old-height finality in the lifecycle test.

Validation at this head, with production dimensions (7 leaves, 53 private batches):

  • SKIP_WASM_BUILD=1 cargo test --locked --offline -p quantus-node -p sc-transaction-pool -p pallet-wormhole: 264 passed, 0 failed, 4 ignored.
  • SKIP_WASM_BUILD=1 cargo test --locked --offline -p quantus-runtime --test mod --features fast-governance: 68 passed, 0 failed, 1 ignored.
  • Additionally ran the ignored build_prover_succeeds node test: passed, 287 seconds.
  • Strict production Clippy for the three affected crates, pinned formatting for all four affected crates, Taplo on the changed manifests, and git diff --check: passed.
  • Four isolated probes executed the real proof pool, unchanged watcher source, unchanged worker helpers with mocked header lookup, and legacy status serialization; all confirmed the behaviors above. The separate real Subxt probe confirmed the client error.
  • GitHub formatting, dependency cooldown, and Clippy/documentation checks passed; Linux/macOS build/test jobs were still running when checked. No live-network aggregation or fresh local Wasm build was performed.

@n13 n13 removed the bot-review label Oct 26, 2026
The node's own dependency graph only enabled rand and std; parallel was
present just in the build-script graph, so the public-batch prover ran
single-threaded. Proving a 12-segment batch in the node drops from ~78s
to ~16s on a 10-core M4; the prover build from ~31s to ~12s.

Co-authored-by: Cursor <cursoragent@cursor.com>
@illuzen

illuzen commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Pushed one more: qp-plonky2 in the node's runtime dependency graph only had rand+std (parallel was only reaching the build-script graph via pallet-wormhole's build.rs, which resolver v2 keeps separate), so the public-batch prover was single-threaded. With parallel: prover build 31s → 12s, proving a 12-segment public batch 78s → 16s on this machine; stress-test end-to-end latency 99s → 45s (20s of which is --public-batch-max-wait).

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Request changes
Reviewer: GPT-6.1 Sol

Reviewed head 0f4ba17b4a9f6970c90f2fbbe858861c69aede29, including the inherited #711 changes. Base: 9e6d01f8bbefd126d87be6325e8cfc1f09825ae9; diff merge base: 482c5b9e02bec0adc70797eade0a67f60baf2619. The latest commit correctly enables parallel proving in the node dependency graph, but all four previously reported correctness issues remain at this head.

  1. [P1] Preserve transaction identity for standard watched submissions. watchers.rs:80–87, reached through interceptor.rs:168–171. The claimed private extrinsic is absent from the reported block; only the different public-batch extrinsic appears. The actual legacy status serializer emits only {"inBlock": block_hash}, losing the replacement index and conveying no replacement transaction hash. Subxt 0.44.3's fetch_events() retains the original hash and searches the block for it, returning Transaction(BlockNotFound); quantus-cli's check_execution_success similarly returns “Extrinsic hash not found in reported block.” Thus a successful aggregated settlement is reported as failure by ordinary clients. Keep standard watched submissions on an identity-preserving path, or introduce an explicit aggregation-aware RPC/client protocol carrying settlement identity before intercepting those watches. Test the client's success/event-fetch path.

  2. [P2] Continue claiming identical retries while their proof is in custody. interceptor.rs:122–143. A repeat submission before settlement passes runtime and on-chain-nullifier checks, but ProofPool::push rejects its already-staged nullifiers. That rejection becomes None / Pass, so submit_one, submit_at, and submit_and_watch send the private extrinsic to the ordinary pool. Its first submission never entered that pool, so ordinary duplicate detection cannot prevent this leak. RPC retries can therefore make this node propagate and author a proof it already claimed, defeating aggregation and wasting proving work. With the real valid fixture, the first push succeeded and the second produced the duplicate-nullifier error. Track exact extrinsic identities in custody, keep claiming identical retries, attach repeat watchers to the existing claim, and synchronize this tracking with eviction. Cover sequential retries through all three wrapper paths.

  3. [P2] Evict anchors when runtime block-hash storage prunes them. worker.rs:136–143 and 210–216. Expiry measures age since admission, while canonicality checks the node's persistent header database. A proof admitted near the end of its anchor's 4,096-block acceptance window soon becomes invalid when runtime BlockHash storage prunes the anchor, although its header remains canonical. It then occupies capacity and remains eligible for repeated expensive proving for up to the default 49,152 seconds after admission. A fresh probe admitted the valid fixture anchored at #1, finalized runtime state at #4098 to prune that anchor, and confirmed runtime admission then rejects it. The real proof pool and unchanged worker helpers, with a mocked canonical-header lookup, still retained it and allowed proving. Check current runtime anchor acceptance during eviction and before proving, and remove buckets that cannot settle.

  4. [P2] Handle retractions before replacement settlements can be missed. watchers.rs:93–103. A watcher remains in in_block until finality reaches its old settlement height, while on_settled examines only pending. Sequence: settle in A#200; reorg; settle in B#201 before finality reaches #200; finalize B's chain. The replacement settlement is ignored, then finality returns the watcher to pending after the spend has already completed. Executing this sequence against the unchanged watcher source produced only Retracted(A), no Finalized(B), and subsequently Dropped. Process retractions and enacted settlements on best-chain changes, or reconcile against retained settlement records. Add this replacement-before-old-height-finality lifecycle test.

Validation at this head, using production dimensions (7 leaves, 53 private batches):

  • SKIP_WASM_BUILD=1 cargo test --locked --offline -j 4 -p quantus-node -p sc-transaction-pool -p pallet-wormhole: 264 passed, 0 failed, 4 ignored.
  • SKIP_WASM_BUILD=1 cargo test --locked --offline -j 4 -p quantus-runtime --test mod --features fast-governance: 68 passed, 0 failed, 1 ignored, including executive verification/settlement tests.
  • Standalone probes used the real proof pool, runtime block-hash pruning/admission, unchanged watcher source, unchanged worker helpers with mocked header lookup, and actual legacy status serialization; they reproduced the behaviors above. Client error paths were verified by source inspection; no new Subxt mock or live-network run was performed.
  • Pinned formatting for all four affected crates, Taplo for the three changed manifests, and git diff --check: passed. Cargo's node feature graph includes both parallel and rand for qp-plonky2.
  • The ignored real-prover constructor test, public_batch_aggregator::worker::tests::build_prover_succeeds, also passed (191.07 seconds, debug build).

GitHub's formatting, dependency cooldown, and Clippy/documentation checks passed at this head; the Linux and macOS build/test jobs were still running when checked. No fresh local Wasm build was performed.

@n13 n13 removed the bot-review label Oct 26, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Request changes for two issues in the watched-submission path. The on-chain nullifier checks prevent the overlapping private/public batch scenarios from minting twice, but the new status stream does not reliably confirm the submitted transaction's intended settlement.

  1. [P1] Match the actual settled segment before reporting inclusion or finalization.

    Watcher::settled_by uses find_map, so any one matching nullifier moves a watcher to InBlock; on_finalized subsequently reports Finalized for that block. For example, a watched proof P spending {a, b} is reported included when a conflicting proof Q spending {a, c} settles, even though P's intended payouts did not execute. A targeted test requiring partial overlap not to report inclusion fails on this revision.

    Track and verify the intended private segment's public statement, including its exit recipients and amounts, against the segment that actually settled. Report a conflicting spend separately from successful settlement. Checking all nullifiers instead of any one is not sufficient: an owner can construct conflicting proofs spending the same inputs to different recipients. Add regression coverage for both partial overlap and identical inputs with different payouts.

  2. [P1] Preserve transaction-watch semantics or provide an aggregation-aware confirmation API.

    intercept_watched claims the submitted private extrinsic and replaces its normal status stream with notifications for the settling public extrinsic. The original private extrinsic never enters the block. Existing clients still retain its original hash: Subxt's TxInBlock::fetch_events searches the reported block for that hash, and quantus-cli's find_extrinsic_event / collect_wormhole_events_for_extrinsic do the same. These confirmation paths therefore fail even when the replacement public batch settled successfully; returning the replacement's extrinsic index in the node-side status does not change the hash those clients search for.

    Provide explicit aggregation-aware tracking that identifies the replacement transaction and verifies the intended segment, with corresponding client support, or leave watched submissions on the normal transaction-pool path until that support exists. Add an end-to-end test using the existing Subxt/CLI submission and confirmation flow.

This branch has not been deployed

No deployments
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.

3 participants