A §18 proof is a small piece of cryptography attached to a result. It says the published code, given these inputs, produced this exact output. You can check that in a few milliseconds, on your own machine, with no network, without re-running the calculation, and without taking the operator's word for it. If you need to, the proof can confirm the output while keeping the inputs private.
This page explains why that matters for regulated financial work, walks through how the proof is built and checked one step at a time with the math and code for each step, and tells you what we got wrong on the way so your team can skip the same mistakes.
Every regulated decision has to be defensible later: a capital calculation, a sanctions screen, an IFRS 17 measurement, a margin or settlement instruction. Today, to trust someone else's number you either re-run their tool and trust your own copy of it, or you accept an attestation and trust their controls. OpenChainGraph already gives every result an execution_hash, a SHA-256 over the canonical inputs and outputs:
That hash lets two parties confirm their outputs are identical. It does not tell you which code produced the number, or that the code ran faithfully on the inputs it claims. In finance that gap is expensive and concrete. Auditors re-perform calculations by hand. Counterparties stand up duplicate infrastructure just to reconcile. Supervisors accept SOC reports and management attestations they cannot independently check. The trust is social, not mathematical.
A compute-integrity proof closes that gap. Here is what it buys you, specifically in a regulated setting:
The mechanism is a zero-knowledge virtual machine (a zkVM): a processor that runs a program and emits a proof that it ran it correctly. The work is in feeding it the right thing to run and making the result reproducible. Here is the whole pipeline, with the math and code at each step.
Each tool's logic is a pure function. The same inputs always return the same output, with no clock, no randomness, no I/O.
Why it matters: a proof of a computation is only meaningful if the computation is reproducible in the first place. Determinism is the foundation everything else sits on.
Take the SHA-256 of the kernel's source code. That hash is the kernel's identity, and it is published next to the tool so "the published model" becomes a precise, checkable claim rather than a label. The sha256-source entry in compute_images[] sits alongside the zkVM imageId: together they bind a published receipt to a specific kernel source, so a proof cannot be silently re-pointed at different code.
The guest is a small program compiled for the zkVM's instruction set (RISC-V, RV64IM). It embeds a JavaScript engine (QuickJS), takes the kernel source and the inputs, runs compute, and writes three things to its public output, the journal.
Why it matters: because the kernel source is an input rather than baked in, one guest covers every kernel, present and future. We do not maintain a separate proving program per tool.
Compiling the guest with a pinned toolchain produces a deterministic fingerprint of the program, the ImageID. Publish it.
Why it matters: anyone can rebuild the guest from public source and get the same ImageID. That is how a third party confirms exactly what program runs their data, trusting no one.
A prover runs the guest in the zkVM and produces a STARK receipt, then wraps it into a Groth16 proof over the BN254 curve, about 200 bytes. This is heavy and happens out of band, never in a browser and never on a request path. Always with development mode off, so the receipt is real.
Why it matters: a small Groth16 proof verifies with a couple of pairings, so checking is cheap, and keeping proving off-line means the live service stays fast and never holds private data.
The receipt rides inside the result's artifact and is excluded from the execution_hash preimage.
A self-contained pairing check confirms the proof against the published ImageID and the journal. No network, no toolchain, milliseconds. This is the part a regulator or counterparty runs.
That is the trust model in one line: rebuild the guest and you confirm what code runs; check the proof and you confirm it ran that code on those inputs to get that output. No hardware enclave, no consensus, no operator to trust.
We took the AINumbers suite from zero §18 coverage to a real proof on every clean kernel. None of it was faked (more on that below). Here is the honest account, so your team can plan around the same problems instead of discovering them.
The obvious first idea is to rewrite each kernel in the zkVM's native language and prove that. We considered it and walked away. A separate guest per kernel means a separate thing to maintain, and every one is a fresh chance to drift from the JavaScript the browser actually runs. Putting one JavaScript engine in the guest and passing the kernel source as input gave us a single program, a single ImageID, and no per-tool proving code. If you adopt this, do the same: one guest, source as input.
Because the guest takes the kernel source as its input, a kernel cannot pull in a shared helper by import. The guest loader resolves only the hashing helper; any other import resolves back to the kernel source and fails to instantiate. That was the cause of the lazy-import class described further below. The rule is firm: shared code a kernel needs, a number formatter, a math library, an encoder, must be inlined into the kernel file itself, so the source that hashes to the kernel digest is the source that runs. You cannot patch the guest to add a capability without changing the ImageID and invalidating every existing proof.
The arithmetic standard (IEEE-754) only guarantees the same bits across engines for the basic operations. The library functions do not carry that guarantee, and they can differ in the last unit in the last place between two correct engines. A handful of our kernels (the machine-learning and quantitative ones) produced guest output that differed from the browser by exactly that last bit, and a last-bit difference means the proof does not match the canonical result.
The fix was a shared deterministic math library, used identically in all three places. Those roughly twelve transcendental kernels were the last and hardest part of the entire effort. If your kernels do pricing, risk, or ML math, budget for this first, not last.
Only five operations are identical bit for bit across JavaScript engines: add, subtract, multiply, divide, and square root. Every transcendental function, exp, log, pow, and the trig family, is implementation-defined, and V8, a WebAssembly build of QuickJS, and the RISC-V guest each link a different math library, so their last bits diverge. The fix was to stop calling the engine math and call one pinned pure-JavaScript library instead, cross-checked against the same algorithm the guest toolchain links. A pure-JavaScript implementation has a second advantage: it is not subject to the fused-multiply-add contraction that makes even a portable C library non-portable in practice.
One kernel validated URLs with a spec-faithful parser. Inside the guest, that cost roughly 5.4 billion cycles, which is effectively un-provable at any sane price. We replaced it with a small deterministic check and used that same check on every path, so the output never diverged.
The lesson generalizes: faithful-inside-the-guest is sometimes the wrong target. The real requirement is deterministic and identical everywhere, and a simpler function that meets it beats an expensive one that does not.
Two more classes of bug had nothing to do with math. One kernel imported a helper module lazily, which let execution order vary; making the import eager fixed it. Another formatted numbers with the locale-aware formatter, which quietly picked up the host's locale and changed the output.
Neither looked like a determinism problem at a glance. The takeaway is that determinism is a property of the whole runtime, the imports and the formatting and the locale, not just the arithmetic.
The prover has a development mode (RISC0_DEV_MODE=1) that emits a fake receipt which still passes verification. A fake proof sitting in a standards repository is the worst outcome we could ship, worse than shipping no proof at all, because it looks real. We ran every single proof with RISC0_DEV_MODE=0, and the rule we held to was simple: if we could not produce a real proof, we shipped nothing for that kernel and said so. Hold the same line.
The full suite proved on a single consumer GPU (an RTX 3080) in about five and a half hours. The Groth16 wrap ran roughly 252 seconds per kernel. The first toolchain build took 5 to 15 minutes. Verifying any of the proofs is milliseconds. Proving is the investment and it is a one-time, off-line cost; verifying is free and universal. If you have no GPU, a proving network (such as Boundless or Succinct) is an option for the proving step, but keep it out of the verify path.
On a consumer GPU, proving can fail in a way that looks like a broken proof and is not. At the default segment size (segment_limit_po2 = 20), the succinct prove failed on a 10 GB card under sustained load with a segment-verification error. It was not out of memory and not a session limit; the executor ran the full computation fine. Lowering the segment size by one, to 19, produced smaller segments and the error went away. The segment size is a prover parameter, not part of the guest, so lowering it does not change the ImageID or invalidate any existing proof, and it changes the number of segments, not peak memory. If a large job fails to prove on a consumer card, lower the segment size before assuming the receipt is wrong.
A large computation does not need a large GPU. The prover breaks execution into fixed-size segments, proves each at the same peak memory, then recursively joins them, and the join is cheaper than proving a segment. The most expensive node in the suite ran a Monte Carlo loop of over a billion cycles and proved on the same 10 GB card as everything else, over a longer run. The right question about a heavy job is how much time it needs, not whether it fits in memory.
Before generating a single proof, sort your kernels into three buckets: the ones using only basic arithmetic are ready immediately, the ones using integer powers need a quick check that the exponent really is an integer, and the transcendental ones need the deterministic math work first. That sort is your project plan. We classified all of ours up front, and it told us exactly where the hard work was.
Measure before you prove. The host can run a kernel in the guest in execute mode, which reports the cycle count and produces no proof. A normal node runs in a few million cycles; the practical ceiling for proving one on a consumer GPU is around fifty million. Reading the cycle count first is what kept a faithful URL parser from ever reaching the prover: execute mode reported over five billion cycles, and the decision to find a cheaper predicate was made in seconds instead of after a failed overnight run.
The lessons above compress into one repeatable pipeline for a kernel too expensive to prove at default settings: measure its cycle count first, run the GPU batch overnight rather than live, step the segment size down the ladder if a card runs out of memory partway through, and if it is still too costly, reach for a second, native-compiled guest image instead of the universal JS one. The diagram walks that pipeline left to right.
compute_proof_ready:"deferred" flag until it does. This page will say so the day it changes, not before.Coverage started partial and honest. Nodes that could not yet be proven carried an explicit compute_proof_ready:"deferred" flag; no placeholder or fake proof ever shipped. A downward-only ratchet gate (check-compute-proof-coverage.mjs) ensured the deferred count could only ever go down, never up. Getting there meant working through five distinct failure classes. The current state: every deterministic gpu:false node carries a real groth16-bn254 proof except one, a content-identification kernel whose in-guest hashing interprets to over a billion cycles and is deferred for proving cost rather than for any doubt about its correctness. That single deferral is recorded openly with its reason, and the ratchet gate lets the deferred count only fall. The gpu:true nodes are out of scope by design.
The bulk of deterministic nodes were proven in a single pipeline run: GPU-accelerated succinct proving on an RTX 3080, then a Docker-based Groth16 wrap, about five and a half hours for the full batch. Every receipt was BN254-verified and every journal byte-identical to the V8 output. Six separate gated PRs, zero dev mode. One QuickJS-ng guest (ImageID a1a0bc89) covered every kernel; the kernel source travels as an input, so one compiled program handles all of them.
The proving step is split across two binaries for a concrete reason. A single build that does both the GPU work and the final Groth16 wrap crashes on a 10 GB card: the native CUDA Groth16 prover hits an illegal memory access right after the seal is produced, and the Groth16 backend is selected at compile time, so a CUDA build cannot fall back to the CPU path at run time. The working arrangement is one CUDA binary that produces a GPU-recursion-compressed succinct receipt, then a separate non-CUDA binary that wraps it to Groth16 through a Docker gnark step. The split is the fix, not a preference.
Several kernels imported a signing helper at module top level. The guest could not resolve it, and the kernel failed to load. The fix was to move each import inside the function body, where it is evaluated only on a branch that never runs in the proving path. Output was unchanged; execution hashes were unchanged.
Three kernels called toLocaleString() for formatted output. Locale-dependent in the browser; the guest has no locale at all. Each call was replaced with a pinned deterministic fmtEnUS formatter. Output-preserving; hashes unchanged, because the formatted strings were identical on every platform after the fix.
A change described as output-preserving has to be proven so, not asserted. When the locale-dependent formatter was replaced, the pure-JavaScript replacement was checked against the engine output over more than a hundred thousand values, integers, decimals, negatives, and rounding edges, with zero differences, before it shipped. An output-preserving rewrite is a claim about every input, so the test has to look like every input, not a handful of samples.
One node validated URLs via the browser URL global, which is absent in the guest. A faithful WHATWG parser was ported into the guest and run; the cost was over five billion cycles, effectively unprovable at any sane price. The resolution: a small deterministic isHttpsUrl check (HTTPS scheme and non-empty authority), used identically in the kernel, the guest, the browser tool, and the Worker. No engine-specific reference remains, so no verdict can flip between environments.
Do not parse inside the proof. Expensive parsing does not belong inside a zkVM execution. The same conclusion shows up across the field: match a pattern outside the circuit and verify the match inside it, validate a scheme against an allowlist rather than parse a whole URL. A billion-cycle parser in the guest is the anti-pattern. When an operation genuinely must be both expensive and trusted, there is a second pattern: let a trusted component run it and sign the result as a precondition, then have the zkVM prove only the decision logic over that signed input. The proof stays cheap and the expensive step stays outside it.
Four kernels used Math.exp, Math.log, or Math.pow, whose last bit is not guaranteed to be identical across engines by specification. A difference in the last unit in the last place means the journal does not match the canonical output, and the proof fails. Each transcendental call was replaced with a pinned pure-JS fdlibm port (_detmath.bundle.mjs) called identically on every surface, so V8, QuickJS, and the guest all return the same bits. This was a deliberate one-time re-baseline: the new last-bit values differ from V8's native Math.*, so the affected nodes' execution_hash values were re-pinned to the corrected results.
Four crypto kernels computed hashes using async crypto.subtle with TextEncoder, neither of which is available in the QuickJS guest. Each was rewritten to use a synchronous pure-JavaScript SHA-256 already used and proven elsewhere in the suite. Output-preserving; fixture hashes were unchanged on both V8 and the guest. These four were the final nodes in that push to receive a real proof, taking the deferred set down to the single high-cost content-identification kernel noted above.
An optional proof that an agent cannot assume is present is worthless as a credential. The deterministic-node proof profile (ocg-p18-deterministic, v0.6.1) makes it a MUST: every live gpu:false node carries a verifying proof or an explicit deferral. With the deferred set down to the one documented content-identification node, the baseline tracks it exactly; no new deterministic node ships without a real proof. The downward-only ratchet gate enforces this on every subsequent push, so the recorded deferral count can only fall.
A proof says "this exact program ran on these inputs and produced this output." That claim is only meaningful if the program gives the same output regardless of which JavaScript engine evaluates it. A cross-engine gate runs each kernel's fixture inputs through three engines: V8 (Node), JavaScriptCore (Bun), and the guest's QuickJS-ng; then fails if their canonical outputs differ by a single byte. It already caught a kernel that stored data with raw NUL sentinels, which V8 and QuickJS parse one way and JavaScriptCore another. The proof attests to execution; this gate attests that execution is the same everywhere, the property a decade-old proof depends on.
Step back from the mechanics and the whole construction has one shape, the OpenChainGraph receipt. A kernel runs once inside the universal guest (ImageID a1a0bc89), the guest commits the kernel digest and the output to its journal, and the proof binds that journal to that exact program. A reader who holds the receipt re-runs nothing: checking the seal is a replay the prover already performed, compressed to about 200 bytes and confirmed with one pairing test. The base execution hash offers the same guarantee the slow way, by actual re-execution, and the cross-engine gate above is what lets the two agree bit for bit, so a receipt and a hand recompute of the same artifact always land on the same output. Recompute-to-verify and prove-to-verify are two readings of one receipt, not two systems.
Most verifiable-compute approaches anchor trust in one of two places. OCG §18 uses neither in the verify path.
| Approach | What you have to trust | OCG §18 |
|---|---|---|
| Secure hardware (TEE, confidential compute) | A chip vendor's attestation and supply chain | No enclave. Trust comes from the reproducible re-execution and the proof. |
| Blockchain or token network | A consensus set and its economic incentives | No chain in the verify path. Verification is a local pairing check. |
| Operator attestation (SOC reports) | The operator's controls and an auditor's review of them | No operator trust. The proof binds the published code to this output. |
Proving can use heavy compute, and a proving network is one way to get it. Verification stays self-contained, offline, and chain-free. That split is the whole promise: proving may be expensive and optional, checking is cheap and for everyone.
You need only the artifact, the published ImageID, and the verifier. Take any OCG artifact that carries a compute_proof, look up that tool's ImageID in chaingraph.json, and run the self-contained verifier. It does one pairing check and returns true only for a real proof of that exact output.
If you want to generate proofs, or independently confirm that the published ImageID matches the published guest source, you run the prover. Rebuild the guest and confirm you get the same ImageID we published, then prove with development mode off.
Then check that the journal's output equals what the JavaScript kernel produces for the same inputs, and verify your own receipt with the same self-contained verifier. The exact field definitions are in the technical spec under §17 and §18.