Verify a compute-integrity receipt on-chain, with one eth_call
Every AINumbers tool that carries a compute proof publishes a risc0 zkVM receipt: a Groth16 seal over the claimed journal output. RISC Zero deploys an immutable Groth16 verifier behind a VerifierRouter on Ethereum mainnet and several L2s. This page is the complete recipe for checking one of our receipts against that deployed contract from your own machine, spending nothing, signing nothing, and trusting no code we wrote: the cryptography runs in the deployed contract, and the answer is a yes or a revert. The same seal is also verified directly in the browser: the Agent Work Ledger now runs the in-house pure-JS BN254 Groth16 reference verifier over each receipt locally (parity-gated against the kernel-side verifier), so the on-chain eth_call below serves as the independent second opinion alongside that in-page cryptographic check.
§1What you are holding
A published receipt is a JSON object shaped by the OpenChainGraph standard, §18 (compute-integrity proof). The fields that matter on-chain:
{
"type": "ZkVmReceipt",
"system": "risc0",
"receiptFormat": "groth16-bn254",
"imageId": "sha256:93c746e79afcf4b27f6d2a6da6cd142a4dd0da31f33d942f92b97344188526c5",
"seal": "JprnquQV+W8PZGrVb3psyDFR3XLhdjfIXqv6BPQl4toS…", // base64, 256 bytes
"journal": { …the committed output object… }
}
imageIdis the SHA-256 identity of the exact guest program (a Rust port of the tool'scompute()) that ran in the zkVM.sealis the 256-byte Groth16 proof: three elliptic curve points (A in G1, B in G2, C in G1) over the BN254 curve, each coordinate a 32-byte big-endian integer.journalis the output the guest committed. The seal proves knowledge of an execution ofimageIdwhoseReceiptClaimbinds exactly this journal.
The journal byte contract (standard §18.7)
The on-chain verifier does not take the journal object; it takes its SHA-256 digest. The digest is defined over the RFC 8785 (JCS) canonical serialization of the journal object, encoded UTF-8: compact, keys sorted at every nesting depth, no insignificant whitespace. That byte string is what the guest committed, so it is the only serialization a genuine seal verifies against. Hash anything else and an honest receipt fails, which is a feature: it is how you know you hashed the right bytes.
§2Derive the three on-chain inputs
2.1 journalDigest
Canonicalize the journal per RFC 8785, encode UTF-8, SHA-256 it. Two dependency-free ways:
- Use our published canonicalizer (it is the same one the standard's §4 hash path uses, gated by the §15 test suite): clone
PostOakLabs/ainumbers, save the receipt's journal object asjournal.jsonnext to the repo root, and runnode -e "const c=require('crypto'),fs=require('fs');import('./chaingraph/kernels/_hash.mjs').then(m=>{const j=JSON.parse(fs.readFileSync('journal.json','utf8'));console.log(c.createHash('sha256').update(Buffer.from(JSON.stringify(m.cgCanon(j)),'utf8')).digest('hex'))})" - Or use any RFC 8785 implementation you trust (several exist for every mainstream language), then SHA-256 the bytes. Caveat from the standard: our published corpus exercises only finite, small JSON numbers; if your journal could carry number-serialization edge cases (negative zero, exponent thresholds, integers beyond the IEEE-754 safe range), treat RFC 8785 itself as authoritative over any implementation's shortcut.
2.2 imageId
Strip the sha256: prefix; the remaining 32-byte hex is the on-chain value.
2.3 seal, with the selector prefix
Base64-decode the seal to 256 raw bytes. The on-chain call needs the 4-byte verifier selector in front: 73c457ba for the risc0 v3.0.x Groth16 parameters our receipts are proven under. So the bytes argument is 260 bytes: 73 45 c7 ba followed by the decoded seal, hex-encoded for the RPC.
Where the selector comes from
The selector is derived from the verifier parameters (the risc0 control root, the BN254 control ID, and the Groth16 verifying-key digest), not from a crate version string. It is computed as bytes4(sha256(tag ‖ control_root ‖ bn254_control_id ‖ verifying_key_digest ‖ 0x0300)) under risc0's tagged-struct hashing. Our receipts are proven with cargo-risczero 3.0.5 against the default Groth16ReceiptVerifierParameters (control root a54dc85a…c1f56), the same parameter set risc0-ethereum shipped as its 3.0.0 verifier generation, whose selector is 0x73c457ba. Patch releases within the 3.0.x series did not change the circuits or the verifying key, so the selector, which commits to exactly those, is unchanged. Section 4 shows you how to confirm the routing live before you verify, with a one-word getVerifier call.
§3The calldata
The router exposes verify(bytes seal, bytes32 imageId, bytes32 journalDigest), whose ABI function selector is 0xab750e75 (keccak-256 of the signature, first 4 bytes). ABI-encode: offset word 0x60, then imageId, then journalDigest, then the seal length (0x104 = 260) and the zero-padded seal. Layout of the 420-byte payload:
| Offset | Bytes | Value |
|---|---|---|
0x00 | 4 | ab750e75 (verify function selector) |
0x04 | 32 | 0x…60 (offset of the dynamic bytes argument) |
0x24 | 32 | imageId |
0x44 | 32 | journalDigest |
0x64 | 32 | 0x104 (260, the seal length) |
0x84 | 260+28 pad | 73c457ba ‖ 256-byte Groth16 seal |
Useful constants for reading responses:
getVerifier(bytes4)selector:0x3cadf449- Custom error
VerificationFailed(): reverts with data0x439cc0cd - Custom error
SelectorUnknown(bytes4): reverts with data0xe4ea6542… - Custom error
SelectorMismatch(bytes4,bytes4): reverts with data0xb8b38d4c…
§4Confirm routing, then verify
Optional but recommended first step: ask the router what verifier a selector maps to. Calldata is 3cadf449 plus the bytes4 73c457ba left-aligned in one 32-byte word:
curl -s -X POST https://ethereum-rpc.publicnode.com \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0x8EaB2D97Dfce405A1692a21b3ff3A172d593D319","data":"0x3cadf44973c457ba00000000000000000000000000000000000000000000000000000000"},"latest"]}'
→ {"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000009f9994eb4cb5200198fefb470f8b50301662e696"}
A returned address means the selector is routed. A revert with SelectorUnknown means the router has no verifier for it (and SelectorRemoved means it was removed and can never be re-registered). Then the verification itself:
curl -s -X POST https://ethereum-rpc.publicnode.com \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0x8EaB2D97Dfce405A1692a21b3ff3A172d593D319","data":"<CALLDATA>"},"latest"]}'
| Chain | Router | Free public RPC examples |
|---|---|---|
| Ethereum mainnet | 0x8EaB2D97Dfce405A1692a21b3ff3A172d593D319 | https://ethereum-rpc.publicnode.com, https://eth.drpc.org |
| Base | 0x0b144e07a0826182b6b59788c34b32bfa86fb711 | https://base-rpc.publicnode.com, https://mainnet.base.org |
Reading the answer:
- PASS
"result":"0x"(empty).verifyreturns nothing on success; it reverts on failure. The pairing check passed for your exact (imageId, journalDigest) under the routed verifier. - FAIL
"error":{"code":3,"message":"execution reverted","data":"0x439cc0cd"}.VerificationFailed(): the seal is not a valid proof over the claim you supplied (wrong journal bytes, wrong imageId, tampered seal, or a receipt from a different parameter set). - ROUTING revert data starting
0xe4ea6542or0xb8b38d4c: the selector is unknown or removed at the router, or the routed verifier's own selector check mismatched. This is a routing problem, not a statement about your seal.
§5Worked example: a published estate receipt
observed 2026-09-03 Receipt: chaingraph/kernels/fixtures/compute-proof/art-04-agent-identity-attestation-checker.receipt.json from the public repository. This is the one receipt in the published corpus whose journal carries unsorted nested keys, so it discriminates the JCS byte contract: canonical bytes and stored-order bytes genuinely differ for it.
The full call, verbatim as run:
curl -s -X POST https://ethereum-rpc.publicnode.com \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0x8EaB2D97Dfce405A1692a21b3ff3A172d593D319","data":"0xab750e75000000000000000000000000000000000000000000000000000000000000006093c746e79afcf4b27f6d2a6da6cd142a4dd0da31f33d942f92b97344188526c53a089ca010da4cc939c482b41b52b659a6ff177a2b82df91c64a774164bcd4be000000000000000000000000000000000000000000000000000000000000010473c457ba269ae7aae415f96f0f646ad56f7a6cc83151dd72e17637c85eabfa04f425e2da121193c6cc3010316f6f92d8c63e28f36022742d55ae0f840369eef960154de72898be54f9c57e5c91eb0eed5c27409437732fda9e6472b95f9f46a24036ff3e2b52d1d4208bf8b5c3b3839fa99da5caeacd483223f619a63a92d29a5489192c0cb98bcdc1bb9a0fddb62caf0c7f4fca70d2fc98a40aa604c9d982d0d2a865402f062a5c00f9e1d3319f7d61349398bfec179a0af8871ac32258382ed4688e2920c30556da14ee2eb1ba2e43038b5410d8d9f19506dfe2066bdd49ab6aa312c927fd595ad6ebb24b399f26fe0b4442401c414c5cbc1a5cbda505bc5b895e2e1800000000000000000000000000000000000000000000000000000000"},"latest"]}'
→ {"jsonrpc":"2.0","id":1,"result":"0x"}
Observed results, 2026-09-03 (four endpoints, two chains)
Ethereum mainnet via ethereum-rpc.publicnode.com and via eth.drpc.org: both returned "result":"0x" (verified). Base via base-rpc.publicnode.com and via mainnet.base.org: both returned "result":"0x" (verified). Chain IDs pinned on each endpoint before the calls: 0x1 for both mainnet RPCs, 0x2105 for both Base RPCs.
5.1 Tamper control: flip one byte of the seal
Same call with the final byte of the seal flipped from 0x18 to 0x19 (one bit):
→ {"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted","data":"0x439cc0cd"}}
5.2 Serialization control: the wrong journal bytes
Same seal, same imageId, but the journal digest computed over compact stored-insertion-order bytes instead of JCS canonical bytes (0xa92cceb5da47a360d4097c7585584aefac5036df9dd0d3be28b9cf0475243136):
→ {"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted","data":"0x439cc0cd"}}
Both directions on the one artifact that can separate them. The genuine receipt passes only under the canonical serialization, and the deployed contract agrees with the standard's §18.7 byte contract.
5.3 Reproducing a tool run from a URL
Every registered tool page also accepts a fragment-only deep link of the form #p=v1.<base64url(gzip(JSON policy_parameters))>, optionally followed by &run=1 to execute after prefill. The payload lives entirely in the URL fragment, so it is never transmitted to any server or written to any access log. The page decodes it with the same gzip codec the ledger page uses for #a=v1. artifact links, validates it against the tool's declared input schema, prefills the form, and (with run=1) runs the same computation an agent would reach through the tool's WebMCP registration, producing the identical execution_hash. Pastes larger than 30 KB compressed are rejected with the site's privacy banner and are never truncated or executed.
§6What this proves, and what it does not
A pass attests
That the 256-byte seal is a valid Groth16 proof, under the verifying key committed to by selector 0x73c457ba, of the risc0 ReceiptClaim built from your imageId and your journalDigest: an execution of that exact program image that halted cleanly with no assumptions, committing a journal whose SHA-256 is exactly the digest you supplied. The contract is the deployed, immutable risc0-ethereum verifier; the check is the same one an on-chain settlement would rely on.
A pass does NOT attest (read this part)
- Policy freshness. The proof says the guest computed this output at proving time. It does not say the output reflects the current published policy text, current tool version, or current source digest. Freshness is a separate question you answer by comparing the journal's committed output against the tool's current published output for the same inputs, and the journal's
kernel_digestagainst the current kernel source digest. - Endorsement of the image. You supply the imageId, and the proof binds to whatever you supplied. Binding "this image is the estate's art-04 guest" is done by the estate's published receipt corpus and Graph Index: take the imageId from the published receipt or manifest, not from the party whose receipt you are auditing.
- What the tool page showed you. The receipt proves the guest's committed journal. Whether a human-facing page faithfully displays that journal is a display-layer question; compare the page's output against the journal's
outputmember yourself. - That the RPC told the truth. See §7.
§7The RPC trust caveat
An eth_call is answered by whoever runs the RPC
Everything here is read-only: no keys, no funds, no transactions, nothing signed. But the verdict arrives as an RPC response, and a broken or hostile endpoint can fabricate "result":"0x" just as easily as it can serve a stale block. Treat any single RPC's answer as one witness. Minimum diligence: run the identical calldata against two independent RPC providers (different companies, different infrastructure) and require both to agree, pinning eth_chainId on each endpoint first so you know where you asked. For stronger assurance, run your own node, or move the same calldata into a real transaction you can observe in a block. The tamper controls in §5.1 and §5.2 are also worth re-running on your chosen endpoints: an endpoint that fails to reject the tampered seal is telling you something.
§8Addresses and further reading
dated observation 2026-09-03 Contract states are observations, and routers are admin-managed mappings; derive the live state rather than relying on this table's age.
| What | Chain | Address | Role |
|---|---|---|---|
| VerifierRouter | Ethereum mainnet | 0x8EaB2D97Dfce405A1692a21b3ff3A172d593D319 | selector routing, TimelockController-governed additions |
| VerifierRouter | Base | 0x0b144e07a0826182b6b59788c34b32bfa86fb711 | same design |
Routed verifier (selector 0x73c457ba) | mainnet + Base | 0x9F9994Eb4Cb5200198FEfb470f8b50301662e696 | emergency-stop proxy fronting the immutable Groth16 verifier |
| Base Groth16 verifier behind the proxy | mainnet + Base + other chains | 0x2a098988600d87650Fb061FfAff08B97149Fa84D | stateless, immutable; carries the v3.0.x control root and verifying key |
Design notes worth knowing: verifier implementations are immutable and stateless; each sits behind an emergency-stop proxy whose guardian can permanently disable it if a critical vulnerability is proven; a selector removed from a router can never be re-registered, so each selector maps to at most one implementation across time. The authoritative live list of routers and registered selectors is risc0-ethereum's contracts/deployment.toml on GitHub; our PR evidence for this page cross-checked that table against live getVerifier calls on both chains above.
- OpenChainGraph standard, §18 and §18.7 (receipt envelope and journal byte contract)
- How the estate reached full compute-proof coverage (narrated walkthrough)
- Verification Desk (all published verification surfaces)
- risc0-ethereum repository:
contracts/version-management-design.mdandcontracts/deployment.toml(deployment and upgrade design)
This page is static documentation: it makes zero network requests, sets no cookies, and stores nothing. The curl commands above are for you to run from your own machine against your own choice of RPC. Recipe and copy by Post Oak Labs, CC BY 4.0. The underlying verifier contracts are RISC Zero's (risc0-ethereum, Apache-2.0 / GPL-3.0 as noted per contract); this page documents them and contains no risc0-ethereum source.