How the proof is built
FX settlement needs one number that both sides can trust: the rate at the close, averaged over a short window so a single bad tick can move nothing. CRX builds that number off-chain and proves it on-chain. Nothing is taken on faith — the price came from Pyth, the average covers the right window, and a Solidity contract checks the proof before it reads a single mark.
The pipeline has five movements. A stream of signed ticks goes in; one TWAP, one breaker reading, and a zero-knowledge proof come out.
1 — Pyth publishes a signed tick
Pyth publishes one update roughly every 30 seconds. Each update is a Merkle accumulator: a single root, and under it the price leaves for every feed, each with the proof that binds it to that root. The root is what Pyth's guardians sign — so a leaf is only as trustworthy as its proof of membership in a signed root.
One root. Many leaves. One signature over the root.
2 — The host pulls a 5-minute window
The host service asks Hermes for the window that ends at the settlement close: five minutes of updates, about ten samples. It does no math on them. It collects the signed ticks, in order, and hands the whole window to the guest.
The host is untrusted. It selects the window; it never decides the price.
3 — The guest walks the window
Inside the SP1 zkVM, twap-core::walk reads the window one sample at a time. For each sample it runs the same four checks, and a failure on any of them aborts the proof:
- Prove the leaf. Re-hash the price leaf under Pyth's Keccak160 scheme and prove its membership in that update's root. A leaf that does not hash into its root is rejected.
- Authenticate the root. Verify the root against the pinned Wormhole guardian set — a secp256k1 quorum, guardian set index 6, baked into the verifying key. The guardian set is not an input the host can swap; it is part of what the proof commits to.
- Pin the feed. Parse the price, pin the feed id, and pin the exponent. The walk fixes which instrument it is reading and the scale it is read at, so no sample can quietly change feed or decimals mid-window.
- Pin the cadence. Enforce monotone, evenly-cadenced publish times. The window must move forward in even steps — no reordering, no gaps, no duplicated timestamps.
What survives the walk is a window of samples that are provably Pyth's, provably in order, and provably the same feed throughout.
4 — One walk, two reductions
The same single walk feeds two reductions, so both readings see the identical authenticated window:
- The TWAP lane computes the time-weighted mean — the settlement rate, alongside its EMA.
- The breaker lane runs a confidence check — the guard that flags a window too wide or too uncertain to settle against.
One pass over the data, two answers. The price and the guard can never disagree about which ticks they saw.
5 — Commit, verify, decode
The guest commits its result as ABI-encoded public values: closeTime, windowSecs, and a marks[] book. Each mark is a tuple:
marks[] = (pairId, feedId, ema, twap, expo, n)
— the pair, its feed, the EMA, the TWAP, the exponent it was read at, and n, the number of samples that backed it.
SP1 wraps the proof to Groth16. On-chain, the Solidity contract verifies that Groth16 proof first, and only then decodes the public values and reads the marks. The verify is the gate; the decode is what comes after it.
The contract never trusts the number. It trusts the proof, and the number rides in behind it.