Wormhole sends in the browser: explore multithreaded WASM proving, or a slow prover with an engaging UX #13
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Deferred. For now wormhole accounts are read-only in the extension: derive the addresses and show the balance, no send. This issue records what was measured, what can't be done, and the two routes worth exploring before wormhole sends come to the extension.
Part of #1.
What a wormhole send has to prove
A wormhole address is not a key. The path
m/44'/189189189'/n'/{0,1}'/k'yields a 32-byte secret;first_hash = poseidon(salt ‖ secret)and the address isposeidon(first_hash). Funds leave through an unsignedwormhole.verify_private_batch(ensure_none) carrying a ZK proof, in two layers (qp-wormhole-*4.3.0, the version the chain pins):Constraint: proving cannot be offloaded
qp-zk-circuits-common4.3.0,circuit.rs:wormhole_leaf_circuit_config()isstandard_recursion_config() // zero_knowledge: false.So whoever builds the private batch sees non-ZK leaf proofs and must be assumed able to spend. A hosted prover would be custody. Both layers run where the secret is, and the only layer that could safely leave the machine is the public batch above them, which isn't needed to spend.
Measurements (2026-09-16, one 20-core desktop)
One real leaf (genuine header hash over
test-helpers'test_inputs_0) padded to a batch of 8. Browser runs are single-threaded WASM in a Web Worker, built withwasm-bindgen0.2.128 (the@quantus/codectoolchain),getrandomwasm_js backends, anddefault-features = false(no rayon).PrivateBatchProver::new_from_bytes(rebuilds the circuit)aggregatecommon.bin,verifier.binanddummy_proof.bintotal about 100 KB and can be embedded. Nothing large needs downloading.Note: the upstream
aggregatorbench at v4.3.0 doesn't run. It feeds the same leaf proof twice and trips the pairwise-distinct nullifier check, so its wall times are circuit generation only. The numbers above come from a separate harness.Route A: multithreaded WASM
Natively, 4 threads gave 2.4× over one thread. If that carries over, this machine lands around 55–70s, and a 4-core laptop perhaps near 2 minutes. To find out:
wasm-bindgen-rayon(or equivalent) on nightly with-Z build-stdand+atomics,+bulk-memory, enabling the aggregator'smultithreadfeature and plonky2'sparallel.cross_origin_embedder_policy/cross_origin_opener_policymanifest keys. Unknown whether Firefox extensions do; this is the first thing to establish, since without it Firefox stays single-threaded.ebd3b4b2) only covers message traffic, not a 2-minute CPU job.Route B: accept a slow prover, make the wait engaging
Even single-threaded, a send finishes; the question is whether a 2–5 minute wait is acceptable when the UI never looks stalled.
new_from_bytesrebuilds the batch circuit (~34s). Keep it warm in the worker for the session, so later sends cost only aggregation.Where sends go meanwhile
Proving natively takes about 14s on this machine, so blackbeard/wallet (desktop, Tauri, in progress in a separate effort) is the proposed home for wormhole sends until this lands. Whether and when it supports them is tracked there, not here.
Reproducing
A harness crate added as a member of
Quantus-Network/qp-zk-circuitsat tagv4.3.0:generated-binswithqp_wormhole_circuit_builder::generate_all_circuit_binaries(dir, true, 8, None).RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo build -p wasm-spike --release --target wasm32-unknown-unknown, thenwasm-bindgen --target web.performance.now()deltas andmemory.buffer.byteLengthto the page.