Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.pfbridge.xyz/llms.txt

Use this file to discover all available pages before exploring further.

The on-chain 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:
LayerPathRole
Soroban contractcontracts/stellar/contracts/verifier/src/lib.rsThin wrapper — stores the VK immutably at deployment, exposes verify_proof(public_inputs, proof_bytes).
UltraHonk implementationrs-soroban-ultrahonk crate, pinned at rev d762995d8c181c05fe9a15962793dba6641a79ba (MIT-licensed, separate repo)The full UltraHonk verifier — proof parsing, Fiat-Shamir transcript, sumcheck, Shplemini opening, BN254 host-function calls.
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, instantiates UltraHonkVerifier from the external crate, and forwards the proof and public inputs to it. Returns Ok(()) on a valid proof, a typed VerifierError otherwise.
  • get_vk() -> Option<Bytes> — view accessor so anyone can read the deployed VK and diff it against the bb artifacts in the monorepo’s circuits/ directory.

BN254 host functions used

The UltraHonk pairing-based verifier needs three primitive operations on the BN254 curve: scalar arithmetic in Fr, 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:
// ec.rs:71 — G1 multi-scalar multiplication
env.crypto().bn254().g1_msm(points, filtered_scalars)

// ec.rs:83 — final Shplonk pairing equation
env.crypto().bn254().pairing_check(g1s, g2s)
OperationSoroban host callWhere it is used
Scalar arithmeticFr::add / mul / inv (host-backed)Sumcheck rounds, Fiat-Shamir delta computation, Shplemini scalar prep
Multi-scalar multiplicationenv.crypto().bn254().g1_msm(...)Shplemini commitment folding (shplemini.rs:208)
Pairing product checkenv.crypto().bn254().pairing_check(...)Final Shplonk pairing equation (shplemini.rs:211)
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-level verify 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.
If the SRS ever needed to change (e.g., new prover version), it would require a fresh deployment of the Verifier contract on every chain — there is no on-chain mechanism to rotate it. This is by design: a mutable SRS would mean a mutable trust anchor.

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 init function.
  • 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 Address field, no role enum, no migration hook.
  • VK parsing is enforced at deploy time. If load_vk_from_bytes returns None, the constructor panics, the deploy fails, and the contract address is never reachable. There is no path that produces a deployed-but-unconfigured Verifier.
The VK can be read at any time via 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 from bb write_solidity_verifier, which uses Ethereum’s ecMul / ecAdd / ecPairing precompiles for the same primitives.
Because both verifiers consume the same proof bytes against the same VK and the same SRS, a settled cross-chain trade is gated by two independent on-chain verifications of the same cryptographic statement — one on Sepolia, one on Stellar Testnet.

Cost measurement

The rs-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:
  1. 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.)
  2. Pin the external crate at the rev above and read ultrahonk-soroban-verifier/src/ec.rs — confirm the only host calls are env.crypto().bn254().g1_msm and env.crypto().bn254().pairing_check.
  3. Read verifier.rs — confirm the six-step pipeline above.
  4. Diff RHS_G2_BYTES / LHS_G2_BYTES against the SRS values used by Aztec bb and by the EVM verifier — they must match.
  5. Call get_vk() on the deployed Soroban contract and check the bytes against the VK in circuits/ — they must match.