Verifier on Stellar Testnet is a small Soroban contract that delegates UltraHonk proof verification to an open-source Rust crate. This page documents the implementation end-to-end so it can be audited independently — which BN254 host functions are called, how proofs are parsed, the verifier pipeline, the trusted setup baked into the binary, and the immutability guarantees on the verification key.
Source layout
The verifier is split across two crates:
The contract crate is 139 lines. Every cryptographic operation lives in the external
rs-soroban-ultrahonk crate, which is open-source and independently auditable.
What the contract exposes
Three entry points, no admin surface:__constructor(vk_bytes)— runs once at deployment, parses the VK (panicking on malformed input), and writes it into instance storage. There is no setter, no upgrade hook, and no role enum; the VK is permanent for the lifetime of the contract address.verify_proof(public_inputs, proof_bytes)— reads the VK from immutable storage, instantiatesUltraHonkVerifierfrom the external crate, and forwards the proof and public inputs to it. ReturnsOk(())on a valid proof, a typedVerifierErrorotherwise.get_vk() -> Option<Bytes>— view accessor so anyone can read the deployed VK and diff it against thebbartifacts in the monorepo’scircuits/directory.
BN254 host functions used
The UltraHonk pairing-based verifier needs three primitive operations on the BN254 curve: scalar arithmetic inFr, multi-scalar multiplication on G1, and a multi-pairing check. All three are exposed by Soroban’s soroban_sdk::crypto::bn254 module as native host functions.
There are exactly two host calls in the verifier, both in ec.rs:
Every BN254 group operation that would otherwise have to be reimplemented in WASM is handled by these host calls. The verifier crate never implements its own elliptic-curve arithmetic.
Verifier pipeline
The top-levelverify in verifier.rs runs six steps in order. A proof must pass all six or verify_proof returns VerifierError::VerificationFailed.
1
Parse proof bytes
load_proof decodes the proof into PROOF_FIELDS = 456 BN254 scalars (14,592 bytes total). Wrong-length proofs return ProofParseError.2
Validate public inputs
Public inputs must be a multiple of 32 bytes. The verifier subtracts the 16 fixed pairing-point inputs (
PAIRING_POINTS_SIZE) from the VK’s public_inputs_size and requires the caller-supplied count to match exactly.3
Generate Fiat–Shamir transcript
generate_transcript hashes proof rounds + public inputs into challenges using keccak as the random oracle. The proof is generated with --oracle_hash keccak, which keeps the on-chain hashing cheap on both EVM and Soroban.4
Compute public_inputs_delta
compute_public_input_delta folds the public inputs into a single Fr using the transcript’s beta / gamma challenges. This becomes part of the relation parameters for sumcheck.5
Sumcheck
verify_sumcheck runs the sumcheck protocol over CONST_PROOF_SIZE_LOG_N rounds. Each round checks a degree-bounded polynomial relation against the prover’s committed values.6
Shplemini opening
verify_shplemini constructs two G1 points P0 and P1 via g1_msm (the host MSM), then calls pairing_check(P0, P1) (the host pairing). If e(P0, [1]_2) · e(P1, [τ]_2) == 1, the proof is valid.Trusted setup / SRS
The Shplonk pairing check needs the standard KZG SRS — a G2 generator[1]_2 and a [τ]_2 element from a one-shot trusted ceremony. Both are hardcoded as 128-byte affine encodings in ec.rs lines 8–28 (RHS_G2_BYTES and LHS_G2_BYTES).
These are the same SRS values used by Aztec’s Barretenberg bb prover (the prover that produces the proofs the relayer submits) and the same values baked into the auto-generated EVM Solidity verifier at contracts/evm/src/Verifier.sol. Identical SRS on both chains means a single proof produced by bb is acceptable to both verifiers without any rebuilding or re-blinding.
VK immutability
The contract surface is intentionally minimal:- The constructor runs exactly once at deploy. Soroban does not re-run constructors on upgrade, and the contract has no manual
initfunction. - No setter for the VK exists in
lib.rs— there is no function that writes to the VK storage slot after construction. - No admin role is recorded. The contract has no
Addressfield, no role enum, no migration hook. - VK parsing is enforced at deploy time. If
load_vk_from_bytesreturnsNone, the constructor panics, the deploy fails, and the contract address is never reachable. There is no path that produces a deployed-but-unconfigured Verifier.
get_vk(), so anyone can independently confirm the testnet contract is bound to the expected verification key (matching the bb artifacts in circuits/).
Same proof, both chains
UltraHonk proofs have a single canonical encoding — 456 field elements, keccak Fiat-Shamir, BN254 SRS — and both chains’ verifiers consume that same encoding:- Soroban side: the contract above (Soroban WASM, BN254 host functions).
- EVM side:
contracts/evm/src/Verifier.sol, the auto-generated Solidity verifier frombb write_solidity_verifier, which uses Ethereum’secMul/ecAdd/ecPairingprecompiles for the same primitives.
Cost measurement
Thers-soroban-ultrahonk crate ships a cost-measurement harness that submits proofs against a localnet Soroban deployment and records CPU instructions, memory, host-call counts, and read/write entries. It is how we validate that a real ProofBridge proof fits inside Soroban’s transaction resource limits, and how a regression introduced by a future bb version would be caught.
Independent verification checklist
For an auditor asking “is the Soroban verifier actually doing what is claimed?”, the steps are:- Read
contracts/stellar/contracts/verifier/src/lib.rs— confirm it stores the VK once and only forwards to the external crate. (139 lines, no concealed state.) - Pin the external crate at the rev above and read
ultrahonk-soroban-verifier/src/ec.rs— confirm the only host calls areenv.crypto().bn254().g1_msmandenv.crypto().bn254().pairing_check. - Read
verifier.rs— confirm the six-step pipeline above. - Diff
RHS_G2_BYTES/LHS_G2_BYTESagainst the SRS values used by Aztecbband by the EVM verifier — they must match. - Call
get_vk()on the deployed Soroban contract and check the bytes against the VK incircuits/— they must match.
Related references
- Smart contracts — Verifier role and addresses on both chains.
- Zero-knowledge proofs — circuit-level explanation of what is being proved.
- Order hashing — how the order digest fed into public inputs is constructed.
- Off-chain signing — how the prover-side artifacts are produced.