Repository navigation
Conversation
…xits) Pin the circuit crates to qp-zk-circuits main rev 080bc897 and qp-plonky2 1.6.0. Leaf proofs no longer carry exits or the fee; aggregate_proofs takes the batch's exit list and volume_fee_bps instead. Circuit binaries are generated at the production shape (10 burns, 32 exit slots) under a new v5.0.0 directory. Regenerated flutter_rust_bridge bindings. Co-authored-by: illuzen <illuzen@users.noreply.github.com>
Plans are now per private batch. Change in the last batch is paid as 1-2-5 series bills (min 1 token) plus one sub-bill remainder, each its own exit, so change leaves are interchangeable with other users' leaves instead of being identifiable by their unique amount. Input selection ranks candidates by change-output count, then input count, then change amount, within the exit-slot budget. Co-authored-by: illuzen <illuzen@users.noreply.github.com>
WormholeSendService submits batch-level exits validated against the circuit shape reported by CircuitManager (leaf proofs and exit slots). Encrypted accounts reserve one change index per change output and record all of them in the pending spend; older single-change pending-spend files still load. Co-authored-by: illuzen <illuzen@users.noreply.github.com>
Co-authored-by: illuzen <illuzen@users.noreply.github.com>
n13
left a comment
There was a problem hiding this comment.
Verdict: Request changes
Reviewer: GPT-6.1 Sol
Reviewed head 13dd998d6049f6605aabaf9eb27c8245b96f3552 against base a8f1661dde9df650714e59c54b5c5fd578e6d4d1.
-
[P1] Bound each payout to u32 before crossing the FFI. wormhole_coin_selection.dart:264–278 now combines a batch's recipient payment into one exit, while Rust
ExitOutput.amountremainsu32. Two valid inputs of3_000_000_000scaled units can produce a selected recipient exit of5_000_000_000.sendSpendsaccepts it, and the generatedsse_encode_u_32callsByteData.setUint32, which serializes that value as705_032_704. The circuit permits outputs below the fee-adjusted burn total, so this can prove and consume the full inputs while paying much less than the confirmed amount. The sibling reward-claim path has the same issue: those inputs generate one exit of5_997_600_000. Reproduced the oversized send plan, successful send validation, serializer truncation, and oversized claim output. Enforce per-exit u32 bounds before FFI and plan/split both send and claim batches accordingly. -
[P1] Keep change reservations within the mnemonic recovery gap. encrypted_account_service.dart:632–640 skips every index reserved by another send, but one denominated send can now reserve more than the discovery gap of 20. Reproduction: select an input of
1_000_400_161scaled units and send1; its change needs 22 addresses. Hold that send before submission, let a second send submit change at indices 22 and 23, then fail the first send. Indices 0–21 remain unused. Running the realdiscoverUsedIndicesBatchedagainst an indexer fixture containing the successful change at 22/23 returns an empty set. A fresh mnemonic restore therefore misses those funds, even though the original device's persisted index can find them. Serialize change-bearing sends until earlier reservations are committed or released, or otherwise guarantee that unused reservation ranges cannot exceed the recovery gap. -
[P2] Reconcile pending change per output. encrypted_account_service.dart:400–401 considers change arrived only when every output address is present, but lines 432–434 still add the entire record's change while any output is missing. When change outputs are 500 and 99 scaled units and only the 500-unit bill has arrived, that bill is already spendable and another 599 units are counted as pending: balance becomes 1,099 instead of 599. Reproduced through
EncryptedAccountService.load. Keep the all-outputs condition for pruning the record, but calculate pending balance from only the individual outputs that have not arrived.
Validation:
- Existing focused SDK suites: 76 passed; mobile encrypted-send strategy: 7 passed.
- Five additional isolated regression probes reproduced the three findings above.
timeout 60 cargo +nightly test --locked --offline --lib wormhole: 29 passed, 1 ignored after a warm-cache retry; the initial build reached the 60-second cap.- Changed Dart files passed read-only formatting checks;
cargo fmt --checkandgit diff --checkpassed. GitHub CI is green at the reviewed head.
Local Flutter is 3.44.4, while the repository pins 3.47.7, so locked Melos bootstrap failed on SDK-constrained dependencies. Focused Dart checks used scratch manifests with compatible dependency resolutions and unchanged PR sources; local analysis and an end-to-end on-chain proof submission were not performed. The matching chain-circuit rollout prerequisite in the PR description remains.
|
Made into draft |
qp-zk-circuits #210 moves the production private batch to 9 leaves (leaf FRI rate 6 / 19 queries). The Rust side reads the shape from the pinned crate; the Dart planning default follows the chain. The Rust pin cannot move to that rev yet: it pins serde_json =1.0.154 against qp-rusty-crystals-hdwallet's =1.0.150. Co-authored-by: illuzen <illuzen@users.noreply.github.com>
Do not merge until the chain verifies the new qp-zk-circuits (proof-of-burn leaf, batch-level exits, N=9 / M=32). This PR assumes those circuits are live. Switch the
Cargo.tomlgit pins to the published crates once that release is cut.What changes
Encrypted sends no longer pay
amount + one change output. Change in the last batch of a send is paid out as fixed-size bills (1-2-5 series, smallest bill 1 token: 1, 2, 5, 10, 20, 50, ...) plus one sub-bill remainder, each to its own fresh change address. Change leaves therefore look like everyone else's leaves instead of carrying a unique amount that subset-sum over the public leaf amounts could tie back to a spend.quantus_sdk/rust/src/api/wormhole.rs): leaf proofs are proofs of burn (no exits/fee on the leaf);aggregate_proofs(proof_bytes_list, exits, volume_fee_bps, bins_dir)takes the batch's exit list; binaries generated atproduction::PRIVATE_BATCH_SHAPEunder a newv5.0.0artifact dir.wormhole_compute_output_amountremoved. FRB bindings regenerated (library stem kept asrust_lib_quantus_wallet; FRB 2.12's bundledcargo_metadatacan't parse the edition-2024 crates now in the graph and would emitUNKNOWN).wormhole_coin_selection.dart): plans are per private batch (WormholeSpendPlan→WormholeBatchPlan{inputs, recipientScaled, billsScaled, remainderScaled});denominateChangeis greedy over the bill series within the exit-slot budget (1 recipient + bills + remainder ≤ 32). Candidate selections (largest-first, smallest sufficient single input, best-fit prefix + completer) are ranked by fewest change outputs, then fewest inputs, then least change. Planning default is the chain's N=9.wormhole_send_service.dart,circuit_manager.dart): batch-levelWormholeBatchSpend{burns, exits}validated against the shape inconfig.json(num_leaf_proofs,num_exit_slots); claims pay one net exit per batch.encrypted_account_service.dart): reserves one change index per change output, records all of them inPendingSpend.changeOutputs; legacy single-changeAddresspending-spend files still load.Open item: Rust pin is one rev behind the chain's shape
The Rust crates are pinned to qp-zk-circuits
080bc897(production N=10). The chain's shape is N=9 / M=32 from qp-zk-circuits #210 (dc68518), but that rev pinsserde_json = "=1.0.154"(#206) whileqp-rusty-crystals-hdwallet 4.1.1(crates.io and git main) pinsserde_json = "=1.0.150", so Cargo cannot resolve a graph containing both. Once either side relaxes its pin, bumping the eightrev =entries inquantus_sdk/rust/Cargo.tomlis the only change needed: the Rust side readsproduction::PRIVATE_BATCH_SHAPEfrom the crate, and the Dart side already plans for N=9 and validates against the generatedconfig.jsonat send time.Proving cost of the new circuits (measured)
wormhole-memproffrom qp-zk-circuits, release build, on this agent's 4-vCPU shared Intel Xeon VM with the machine otherwise idle. (An earlier version of this table was taken while cargo builds were running and overstated every phase ~2.5x.) The circuit dimensions match upstream (private_batch n=9 m=32 unpadded_gates=23469 degree_bits=15).080bc897(N=10, leaf rate 7)dc68518(N=9, leaf rate 6)This VM is a constant ~2.2x slower than the M4 in every phase, so the relative effects are the same as upstream measured: #210 halves the leaf cost (rate 7 → 6), and versus the v4.4.0 circuits the leaf is ~3x and the private-batch prove ~2x on the same hardware, with peak memory ~1.75x higher. The private-batch circuit build (~3 s here) is not part of the per-send path; the app loads the prover from the generated binaries.
Verification
cargo check,cargo fmt,cargo test --lib wormhole(29 passed) and the ignoredwormhole_proof_generatesrelease test pass against the pinned rev.flutter testinquantus_sdk: 742 passed (incl.native-tagged tests against a locally built release dylib);mobile-app: 730 passed.flutter analyze . --fatal-infosclean onquantus_sdk; the mobile-app analyzer hangs on this machine (known), soflutter testserved as the compile check there.dart format --line-length=120applied to all changed files.