Verify the Compute Provider
An OpenVM proof shows which guest ran and what it revealed. This page explains how the Interfold contract checks that proof, which values each check binds, and how to confirm the receipt identity of a deployment. It is for E3 program developers, auditors, and operators who did not build the deployment. At the end, you can tie a deployed verifier to a guest and its source.
Two checks on one proof
publishCiphertextOutput checks the compute proof two times, in this order
(InterfoldLifecycle.publishCiphertext and _isValidCiphertextHash):
- The protocol ciphertext verifier of the encryption scheme. For BFV this is
OpenVmBfvCiphertextVerifier(contracts/verifiers/bfv/OpenVmBfvCiphertextVerifier.sol). verifyon the program contract of the E3.
Before these checks, the Interfold contract calls verifyDataAvailability on the program contract.
If a verifier returns false, the call reverts with InvalidOutput. After the program check, the
Interfold contract reads the stage again and checks committee viability again. Then it stores the
output hash and commitment and moves the E3 to CiphertextReady.
Both verifiers rebuild the same journal and call the same OpenVmReceiptVerifier:
openVmVerifier.verify(proof.seal, imageId, sha256(journal));OpenVmReceiptVerifier (contracts/verifiers/OpenVmReceiptVerifier.sol) checks that imageId is
its own identity, decodes the seal, and calls the Halo2 verifier with the digest as the public
values and its two application commitments. The Halo2 verifier reverts when the proof does not show
that the guest with those commitments revealed that digest.
What each check binds
The journal has nine 32-byte words (lib/OpenVmComputeProof.sol). The table shows where each
verifier gets each word. A call argument is a value from the caller of publishCiphertextOutput.
| Word | OpenVmBfvCiphertextVerifier | MyProgram.verify (template) |
|---|---|---|
| Chain ID | block.chainid | block.chainid |
| Interfold contract | msg.sender (the Interfold proxy) | address(interfold) |
| E3 ID | Call argument | Call argument |
| Encryption scheme ID | e3.encryptionSchemeId | e3.encryptionSchemeId |
| Committee public key hash | e3.committeePublicKey | e3.committeePublicKey |
| Output ciphertext hash | Call argument | Call argument |
| Output SAFE commitment | Call argument | Call argument |
| BFV parameter hash | Request-time hash, compared with the proof | Hash that validate stored, compared with the proof |
| Input root | Proof (not checked) | Root of the on-chain input tree, compared with the proof |
The caller supplies the output hash and the commitment, and the Interfold contract stores them in the E3. The proof binds them to this chain, this Interfold contract, this E3, this committee key, and these parameters. The prover cannot change these other words.
The protocol verifier does not check the input root. The program contract must compare the input
root in the proof with its own tree root. If it does not, a prover can publish an output that it
computed over inputs that nobody submitted. MyProgram.verify and CRISPProgram.verify both make
this comparison.
The guest derives the input root from the ciphertexts it reads. Its input has no root field
(e3_openvm_types::GuestHeader), and every ciphertext contributes a leaf whatever the policy
selects. The root in the journal is therefore a function of the inputs that the output used.
The receipt identity
OpenVmReceiptVerifier binds three values, all immutable and public:
verifier: the Halo2 verifier contract;appExeCommit: the commitment of the guest executable;appVmCommit: the commitment of the VM configuration (guest/openvm.toml).
Its imageId is
keccak256(abi.encode(keccak256("INTERFOLD_OPENVM_RECEIPT_V1"), verifier, appExeCommit, appVmCommit)).
The protocol verifier and the program contract must both name this imageId. The constructor
refuses a verifier address with no code and a commitment that is zero or not a canonical BN254
scalar.
These properties hold without any action from you:
- The verifier is fixed at request time.
bindCryptoConfigcopies the current ciphertext verifier of the scheme and the parameter hash into a per-E3 record. A latersetCiphertextVerifierchanges only future requests. - The identity cannot change.
OpenVmBfvCiphertextVerifier.imageIdandopenVmVerifierareimmutable. A rebuilt guest needs new verifiers. - One verifier for each scheme. The Interfold contract keeps one current ciphertext verifier for each encryption scheme. Every BFV E3 program on a deployment must use the identity that it names.
An E3 program can keep its own identity and receipt verifier. The template sets them in the
constructor and has no setter. CRISPProgram has setOpenVmVerifier, which only its owner can call
and which takes the new verifier's identity with it. Every constructor here refuses an identity that
its receipt verifier does not report. A change of verifier cannot replace an output that the
Interfold contract already accepted. It can make an E3 in progress fail, because verify then
checks the proof against a different identity.
Check a deployment
Use this procedure to compare a deployed verifier with a guest. You need Foundry (cast), an RPC
URL, and the Interfold contract address from Contract Addresses.
Read the verifier address
cast call <INTERFOLD_ADDRESS> "getCiphertextVerifier(bytes32)(address)" \
$(cast keccak "fhe.rs:BFV") --rpc-url <RPC_URL>The result is the ciphertext verifier for new BFV requests.
Read the identity
cast call <VERIFIER_ADDRESS> "imageId()(bytes32)" --rpc-url <RPC_URL>
cast call <VERIFIER_ADDRESS> "openVmVerifier()(address)" --rpc-url <RPC_URL>
cast call <RECEIPT_ADDRESS> "appExeCommit()(bytes32)" --rpc-url <RPC_URL>
cast call <RECEIPT_ADDRESS> "appVmCommit()(bytes32)" --rpc-url <RPC_URL>
cast call <RECEIPT_ADDRESS> "verifier()(address)" --rpc-url <RPC_URL>Compare with the guest
Build the guest from the source you trust with interfold program compile in its project. Compare
app_exe_commit and app_vm_commit in .interfold/caches/openvm/prepared/identity.json with the
two commitments from the chain. The values must be equal.
Check the program contract
Read imageId() on the program contract. Read verifier() on a template program or
openVmVerifier() on CRISPProgram. Compare them with the values from the previous steps.
Generate the provenance manifest
From the repository root, run:
pnpm provenance:manifest --project <PROJECT_DIR> --config <PROVER_JSON> --prover <WORKER> \
--rpc <RPC_URL> --verifier <VERIFIER_ADDRESS> --out manifest.json--project is required. It is the project whose guest the worker configuration describes, for
example a checkout of the CRISP repository (opens in a new tab).
Make sure that the manifest shows "complete": true. The unresolved list names each field that
the script did not resolve.
The manifest (scripts/openvm/provenance.ts) records these values:
- The source commit and the project's commit, whether both are clean, and the digests of the root
and worker lockfiles, the project's lockfile, the guest lockfile, and the guest's
openvm.toml - The SHA-256 of the application key, the executable, the aggregation key, the Halo2 key, the verifier artifact, the Halo2 parameter files, and the worker
- The two application commitments, and whether the worker's
checkpassed - The chain ID, the three verifier addresses and the hashes of their code, and whether the deployed identity and Halo2 runtime match the configured guest and artifact
The manifest is complete: false when a field is missing. An incomplete manifest records an
unfinished check. It is not a passing result.
Reproduce the identity from source
The manifest checks artifacts and the chain against each other. It does not rebuild the guest. To
check the commitments against the source, rebuild the guest from a clean checkout with the pinned
toolchains: cargo-openvm v2.0.2, its guest toolchain, and the guest's Cargo.lock
(OPENVM_BUILD_LOCKED=1). Then run the worker's prepare and compare the new identity.json.
The SHA-256 of the executable is not the executable commitment. SHA-256 checks that a file arrived intact. The OpenVM commitment comes from the program's memory image and the VM. The manifest records both values for these two different purposes.
Limits of these checks
- Proxy upgrades. The Interfold contract is an upgradeable proxy. The per-E3 verifier record
protects an E3 from
setCiphertextVerifier. It does not protect against an upgrade of the proxy. That control belongs to governance. - Program values. The protocol verifier and each program contract keep separate values. The procedure above reads both.
- Unproved mocks. With
TEMPLATE_UNPROVED_TEST=1on the local chain, the template deploysMockOpenVmReceiptVerifier. WithCRISP_UNPROVED_TEST=1, CRISP deploys it on the local chain, and on Sepolia whenALLOW_SEPOLIA_UNPROVED_COMPUTE=trueis also set. This verifier accepts every seal, and the dev runner returns a placeholder seal. Such an E3 proves nothing about the computation. To detect the mock, callimageId()on the receipt verifier. The mock returnskeccak256("INTERFOLD_LOCAL_UNPROVED_TEST"). A realOpenVmReceiptVerifierreturns the identity of its guest. - Audit. The compute path and the OpenVM verifier integration are not audited.