ProofBridge uses zk-SNARKs (specifically UltraHonk on BN254) as the cryptographic bridge between two chains that cannot read each other’s state. The proof is a succinct, on-chain-verifiable attestation — the destination chain confirms what happened on the source chain by checking a fixed-size proof, with no oracle, validator committee, or relayer signature in the trust path.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.
What problem ZK proofs solve here
Two practical constraints push ProofBridge toward SNARKs:- Each chain can only see itself. A Soroban contract cannot read Sepolia’s Merkle Mountain Range, and an EVM contract cannot read Stellar’s. There is no native cross-chain read primitive on either platform. The destination chain has to be told what happened on the source chain — by something the chain can verify cryptographically rather than something it has to trust.
- Naive verification is too expensive on-chain. A direct MMR inclusion proof for a deep tree means dozens of Poseidon2 hashes per verification call. Doing that on-chain on every settlement would burn through Soroban resource budgets and EVM gas. A SNARK compresses the entire MMR-inclusion + order-hash + nullifier check into one fixed-size 14,592-byte proof verified in two pairings plus an MSM.
What the proof actually attests
Each settlement consumes a single proof. The proof’s public inputs are visible on-chain on both chains:| Public input | What it asserts | Visibility |
|---|---|---|
order_hash | The exact order being settled (EIP-712 digest of the trade) | Already on-chain — committed to the MMR at deposit time |
target_root | The MMR root the proof is checked against | Already on-chain — read from MerkleManager of the opposite chain |
nullifier_hash | The one-time-use marker for this settlement | Public output of the proof, recorded on settlement |
ad_contract | Which side of the trade this proof unlocks (true = AdManager / destination, false = OrderPortal / source) | Public flag, prevents cross-side replay |
MMR inclusion
MMR inclusion
The
order_hash is included under target_root at some position in the Merkle Mountain Range. The circuit recomputes the Poseidon2 hash chain from leaf to root using the prover-supplied sibling and peak hashes. If the recomputed root doesn’t match target_root, the constraint fails.This is the cross-chain attestation: chain A’s MMR root is a public on-chain value; the proof says “an order_hash matching the trade is in this tree.”Nullifier derivation
Nullifier derivation
The
nullifier_hash is poseidon2(secret_half, order_hash) — whoever produced the proof knew a per-trade secret whose halves bind to this specific order. The on-chain contracts record nullifier_hash after a successful settlement and reject any future proof carrying the same value, which prevents the same deposit from being claimed twice.The secret is a uniqueness device — its job is to make each settlement attempt singular and non-replayable.Side binding
Side binding
The
ad_contract flag selects which half of the secret is hashed with the order — left half for the destination side, right half for the source side. This means the same proof cannot be replayed in the opposite direction; the AdManager and OrderPortal each accept exactly one of the two valid forms.order_hash and is committed to the MMR via the public EIP-712 struct.
How the cross-chain flow uses the proof
The destination chain (AdManager + Verifier) never reads the source chain directly. It receives:
- The source chain’s MMR root
R_A(a public on-chain value, brought across by the relayer). - A SNARK proof binding
R_A,order_hash, andnullifier_hashtogether.
R_A happened. The relayer cannot fabricate a valid proof for a deposit that doesn’t exist — because they would have to forge a Merkle inclusion path for a leaf that isn’t in the tree, which the SNARK constraints catch.
The same proof is then submitted to the source chain’s OrderPortal (with the ad_contract flag flipped) to release the Maker’s payout. Two independent verifications of the same statement, one per chain.
Why UltraHonk specifically
UltraHonk on BN254 was chosen for three reasons:- Compact, constant-size proofs. 456 BN254 scalars (14,592 bytes), independent of circuit size. Fits in a single Soroban transaction.
- Cheap on-chain verification. Two pairings + one G1 multi-scalar multiplication via Soroban’s native BN254 host functions — and
ecPairing/ecMul/ecAddprecompiles on Ethereum. - Same proof bytes verified by both chains. The
bbprover produces one proof; both verifiers consume the same bytes against the same SRS. See Soroban verifier internals for the full Soroban path and Smart contracts for the EVM Verifier.
End-to-end safety from the proof
Because the proof has to verify on-chain on both sides:- The relayer cannot fabricate a deposit (no Merkle inclusion).
- A Maker cannot claim settlement without a matching deposit (order_hash mismatch).
- Neither party can replay an old proof (nullifier already recorded).
- The proof for one side cannot be reused on the other side (
ad_contractflag binds the nullifier derivation).
Related references
- Merkle Mountain Range — how
target_rootis built and why both chains can reproduce the same Poseidon2 hashing. - Order hashing — exact construction of the EIP-712 digest that becomes
order_hash. - Soroban verifier internals — the Stellar-side proof-checking pipeline and BN254 host calls.
- Smart contracts — Verifier role, addresses, and EVM-side details.