Measurements
Verification cost on Solana, the curve gap, why the wrapper cannot be built, proving times in a browser, wallet sync, and the infrastructure that is running.
Not one number on this page is computed from a published constant. Every figure came out of something running: a real SVM, a real proof, real mainnet blocks over the wire.
Where a figure is an extrapolation built on top of a measurement, the row says so.
Verification on Solana
A real Groth16 setup, a real proof, a real program on a real SVM. The harness also flips one byte of the proof and requires the transaction to fail, then replays the honest proof and requires it to pass.
A verifier that accepts anything is cheap and produces an equally attractive table.
These figures are still true and no longer on the path. Nothing can hand this verifier a proof, because the wrapper that would have produced one cannot be built. They are kept because the day a Pallas syscall exists, this is what the last step costs.
- 79,623CU
- Verification, one public input
- One four-pair pairing check plus fixed work, against a 1.4M budget.
- ~4,344CU
- Each additional public input
- One scalar multiplication and one point addition.
- 9.7×
- Headroom at sixteen inputs
- It also fits the 200k default, so a verified proof rides in an ordinary transaction with no compute-budget instruction at all.
- 20
- Ceiling on public inputs
- Transaction size binds before compute does. At sixteen inputs the verifier uses 10% of the budget and 82% of the 1,232-byte packet. The circuit uses five.
The curve gap
Pallas has a 255-bit base field and BN254 a 254-bit scalar field. That is close enough to look interchangeable and not close enough to be.
Every operation decomposes into limbs with carries, and that is where the cost is. These are slopes between two sample sizes, so none of them carries its own setup.
- 677constraints
- Field multiplication
- Against one constraint natively. The same 677 comes out for Vesta's base field and for Pallas's, measured by two separate crates, which is the cheapest check that both are measuring the same thing.
- 4,871constraints
- Point doubling
- A wrapper does not multiply field elements. It multiplies points, and this is the first of the units it spends.
- 10,910constraints
- Point addition
- The second unit.
- 15,860constraints
- One scalar-mul bit
- A doubling, an addition, and the conditional select that decides whether the addition counts. 4,871 plus 10,910 is 15,781, and the missing 79 is that select. Measuring it separately is what keeps the accounting honest.
The wrapper
Multiplied out, those units say the wrapper cannot be built. This is the project's largest negative result and the reason the architecture has three places rather than four.
- 2,219
- Terms in the verifier's MSM
- 2,048 reference-string terms at k = 11, plus the commitments the verifier accumulates. Halo 2's inner product argument ends by folding the whole reference string into one check, and every scalar in it is a nonzero product of challenges, so none can be skipped.
- 8.97B constraints
- Wrapper circuit, naive
- Double-and-add, and a 4.0 TB proving key with it.
- 1.55B constraints
- Wrapper circuit, floor
- A 4-bit windowed bound that charges one addition per point per window and charges nothing for building the tables or reading them. Not achievable, and not arguable below.
- 0.7TB
- Proving key at that floor
- Extrapolated from a measured slope of 448 bytes per constraint, taken from Groth16 setups run here at 2^14 and 2^16. Linear is the optimistic reading, because both stages run FFTs over a domain sized to the constraint count.
- 6.4ms
- Native Halo 2 verification
- What the wrapper was going to replace. A quorum can do this, publish the proof beside its signature, and let anybody redo it.
Browser proving
Halo 2 proving compiles to wasm32 and runs. The wall in an earlier note was the multicore feature rather than the circuit: halo2_proofs has no rayon to stand on without atomics, and turning multicore off is the whole fix.
These figures come from the shielding path, which is the one a user presses today.
- 16.5s
- Shield proof, in the browser
- Three consecutive runs at 16.50, 16.51 and 16.70 s. Native is 3.85 s on one thread, so the penalty is 4.3×.
- 11.8s
- Proving key, in the browser
- Built once and independent of the amount. Started when the wallet loads, so a first shield costs 16.5 s instead of 28 s.
- 2
- Actions in one shield
- BundleType::DEFAULT pads to a minimum of two actions, each with its own circuit instance. Every estimate made before this was measured reasoned from one action and was short by half.
- 8.3s
- Layer 1 proving in a browser
- Measured in a Web Worker, against an extrapolation of about 9 s from the 4.3× ratio. The proving key costs 18.6 s and is built on the first press rather than at load, so a first proof is 27 s and every one after it is 8.3 s.
Wallet sync
Ironwood begins at block 3,428,143, located by binary-searching tree state rather than by converting a fork date into a height.
That makes the full span tens of thousands of blocks instead of 3.4 million, which is why a Zolar wallet syncs in a different class from an ordinary Zcash wallet.
The wallet asks for two pools, not one. It can only prove Ironwood notes, but a holder who shielded into Orchard and did nothing wrong would otherwise be shown a confident zero, so the scanner reads Orchard as well and labels it as unprovable.
- ~93s
- Full restore in the browser
- 30,171 blocks, 287,551 actions, two viewing keys, so 575,102 trial decryptions. Native on one thread is 37.0 s.
- 158µs
- Trial decryption, per attempt
- In WebAssembly, against 64 µs natively. The hot path is variable-base scalar multiplication in pasta_curves, which compiler flags do not touch.
- seconds
- Second sync onward
- State is saved after every batch, and a resume rolls back 100 blocks before continuing. Rolling back too far costs about 3 seconds; not rolling back far enough costs a wrong balance permanently.
- 1.3s
- New wallet, birthday at the tip
- End to end, to a real balance. This is the common case, because a wallet created here is created now.
- 67 / 33%
- Pool composition over that span
- 192,591 Orchard actions against 94,430 Ironwood ones. The pool Zolar can prove is the smaller share of the work.
- ~1,152blocks/day
- Growth
- Today's restore cost is a statement about the fork being weeks old, not about Zolar. Anything built on the idea that syncing is cheap has to know that.
Shielding
One transparent balance moved into Ironwood from the browser, on mainnet. The transaction is version 6 under ZIP 229, and the fee follows ZIP 317.
Its anchor is taken 10 blocks below the tip, because an anchor from a block that gets reorganised away takes the transaction with it.
- 9,348B
- Shielded transaction, on-chain
- Block 3,459,140. For comparison, the plain transparent transfer that funded the address was 241 bytes.
- 1,015,000zatoshi
- Balance moved
- 0.01 ZEC plus a 15,000 zatoshi fee. The transparent balance went from 4,213,679 to 3,198,679, matching what the dialog printed before it was sent.
- 2.1MB
- Shield payload in the browser
- 873 KB gzipped, plus 20 KB of glue. The same Rust that the command-line tool runs, compiled for the browser rather than reimplemented for it.
Infrastructure
One relay, one binary, no database. It never sees a key, a seed or a witness. It does see an IP address and a query pattern, and that is written here rather than left to be discovered.
- served
- Ironwood tree state from public nodes
- Queried live against mainnet on two reachable public servers, not read off a specification. This is what makes an independent browser verifier possible without running a full node.
- 12,992 → 4,250B
- Pruning survives the relay
- The same five blocks, with and without a pool filter. A browser can ask for the pools it uses and pay for those.
- 346KB
- Wallet payload
- 176 KB gzipped, loaded lazily, and paid by everybody. The prover is a second module at 1.42 MB, 548 KB gzipped, and is fetched only when somebody proves.
- identical
- Native and browser derivation
- Pinned as a cross-target test vector. If these ever diverged, one recovery phrase would restore two different wallets and nobody would find out until they went looking for funds that were never there.