On-chain gas and fees
Measured cost of verifying one UltraHonk proof through the deployed Soroban verifier contract on the Stellar testnet, what drives it, and the levers we control (and the ones we do not).
Per-transaction cost (measured on testnet)
Contract: CCS6Z3VVCKV4F5BCH7VXJLKKWMDROUWOTZYROJ4T26CM7R45SE4IFYI2
(transfer-circuit VK — see docs/soroban-verification.md).
| Call | Outcome | minResourceFee | Fee actually charged | Host instructions |
|---|---|---|---|---|
verify_proof — valid transfer proof | ✅ verified | 149,208 stroops (0.014921 XLM) | 134,418 stroops (0.013442 XLM) | 95,297,789 |
verify_proof — tampered proof | ❌ rejected (VerificationFailed) | 0 (never lands) | — | early exit before crypto |
verify_proof — wrong-length proof | ❌ rejected (ProofParseError) | 0 (never lands) | — | early exit before crypto |
verify_proof — wrong public inputs | ❌ rejected | 0 (never lands) | — | early exit before crypto |
The simulated minResourceFee (149,208) is the RPC’s own fee estimate
and tracks the actual charge (134,418) closely; the estimate carries a
safety buffer, and the ledger charges only the resources actually used.
Because verify_proof is read-only, the simulation estimate is also the
exact cost any submission would carry — you never need to submit to know
the price.
Where the fee goes
Soroban fees are resource-based: instructions, ledger I/O, and memory. The measured resource footprint of one verification:
| Resource | Measured | Notes |
|---|---|---|
| Instructions | 95.3 M | ~100% of the fee. The UltraHonk verification: 28 sumcheck rounds × 26 subrelations, the Shplemini batch opening (65-entry MSM), and the final KZG pairing, all on BN254 via the host’s bn254 crypto functions. |
| Disk read | 0 B | Verification is read-only. |
| Write | 0 B | The verifier writes nothing. |
| Ledger keys | 2 read | The VK (1,760 B) read from instance storage. |
There is no ledger I/O cost to optimize — the fee is almost entirely CPU (the 95 M instructions above). The proof itself (14,592 B) and public inputs (288 B) travel as invocation arguments; at ~10⁷ stroops per 100 M instructions, argument bytes are noise next to the crypto.
The cheap-fail property (the caller-side optimization that matters)
The contract validates before it computes: proof length is checked first
(ProofParseError), then the VK is loaded, then — and only then — the
95 M-instruction cryptography runs. Consequences:
- Tampered, truncated, or malformed proofs cost nothing — they are rejected in simulation for free and, if submitted, fail at base fee only.
- Pre-validate before submitting. The live client
(
LiveSorobanClient::verify_proof_with_cost) already resolves the verdict by simulation, so the standard verification path pays exactly zero. Submission is only needed when an on-chain consumer must witness the proof, and only a proof that already simulated as valid should ever be submitted. - Budget with the oracle.
verify_proof_with_costreturns the RPC’sminResourceFeeand resource breakdown alongside the verdict, so callers can budget per-transaction cost without spending anything.
What we already do (and should not change)
- Minimal calldata. The client sends the raw wire bytes — 288 B of
public inputs, 14,592 B proof — with no envelope overhead. The
SorobanPayloadversion/count prefix exists only for local round-tripping and never reaches the contract. - One op, no auth. A verification is a single
InvokeHostFunctionoperation with an empty authorization vector. - No storage writes — the verifier never mutates state, so there is no write-footprint fee component.
What we cannot optimize (honest boundary)
The 95 M instructions are the fixed cost of any on-chain UltraHonk
verification using this verifier: the contract is Nethermind’s audited
implementation with an immutable VK and no admin path. Rewriting its
cryptography (e.g. hand-optimizing the MSM or transcript) to chase gas
would void the audit and the byte-for-byte agreement with bb, and is
explicitly out of scope. The levers that could actually move this number
are upstream:
- A newer audited verifier tracking a Barretenberg version with a more
efficient proof/verifier layout (the current one is pinned to the
bb
0.87.0byte format the contract was audited against). - Recursive/aggregated proofs (verify many transfers with one UltraHonk proof) — a proving-system change in the circuits, not a contract change.
- Protocol-level host improvements (faster BN254 MSM/pairing in the Soroban host).
Re-benchmarking
The benchmark is a network-gated live test; it needs no funded account and pays nothing (simulation only):
CRUCIBLE_SOROBAN_LIVE=1 cargo test -p crucible-soroban-adapter --test live \
-- --nocapture onchain_cost_report
Expected output:
on-chain cost report (testnet, transfer circuit):
valid proof : verified · minResourceFee 149208 stroops (0.014921 XLM) · 95297789 instructions · 0 B read · 0 B written · 2 read keys
tampered proof: rejected (fee 0, 0 instructions before early exit)
The numbers above were captured 2026-09-08 on the Stellar public testnet (ledger ~4,569,705). They will move as the network’s fee parameters and host pricing evolve; re-run the benchmark before quoting them.
Related
- docs/soroban-verification.md — the deployed contract, the on-chain proof-format pin, and the live test setup.
- docs/performance.md — local (proving-side) performance.
- docs/verification.md — the verifier service model.