Enclave is now The Interfold.
Build
E3 Program Anatomy
Verify the Compute Provider

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):

  1. The protocol ciphertext verifier of the encryption scheme. For BFV this is OpenVmBfvCiphertextVerifier (contracts/verifiers/bfv/OpenVmBfvCiphertextVerifier.sol).
  2. verify on 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.

WordOpenVmBfvCiphertextVerifierMyProgram.verify (template)
Chain IDblock.chainidblock.chainid
Interfold contractmsg.sender (the Interfold proxy)address(interfold)
E3 IDCall argumentCall argument
Encryption scheme IDe3.encryptionSchemeIde3.encryptionSchemeId
Committee public key hashe3.committeePublicKeye3.committeePublicKey
Output ciphertext hashCall argumentCall argument
Output SAFE commitmentCall argumentCall argument
BFV parameter hashRequest-time hash, compared with the proofHash that validate stored, compared with the proof
Input rootProof (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. bindCryptoConfig copies the current ciphertext verifier of the scheme and the parameter hash into a per-E3 record. A later setCiphertextVerifier changes only future requests.
  • The identity cannot change. OpenVmBfvCiphertextVerifier.imageId and openVmVerifier are immutable. 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 check passed
  • 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=1 on the local chain, the template deploys MockOpenVmReceiptVerifier. With CRISP_UNPROVED_TEST=1, CRISP deploys it on the local chain, and on Sepolia when ALLOW_SEPOLIA_UNPROVED_COMPUTE=true is 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, call imageId() on the receipt verifier. The mock returns keccak256("INTERFOLD_LOCAL_UNPROVED_TEST"). A real OpenVmReceiptVerifier returns the identity of its guest.
  • Audit. The compute path and the OpenVM verifier integration are not audited.

Next steps