Compute Provider
The compute provider (CP) runs the secure process off-chain and proves that it ran correctly. Each
Interfold project proves its own E3 program: an OpenVM guest and a proving service, both built from
the project's program/ crate, run on the operator's machine. There is no proving market, program
upload, or payment account. This page explains what the Interfold contract expects from a CP, how
the Rust crates fit together, and how to run a CP. It is for E3 program developers and for the
person who runs the CP.
What the Interfold contract expects
The Interfold contract has no registry of compute providers. It checks the result, not the party that made it.
- Any account can call
publishCiphertextOutput. The function has no caller restriction. - The call must come after the input window closes and before the compute deadline. The E3 must be
in the
KeyPublishedstage, and the output can be published only one time. - The committee of the E3 must still meet its threshold.
- The proof must pass the protocol ciphertext verifier of the encryption scheme and then
verifyon the program contract. - The request field
computeProviderParamsgoes toIE3Program.validateunchanged. The Interfold contract does not read it.
interfaces/IComputeProvider.sol defines validate(e3Id, seed, params), which returns a
decryption verifier. Interfold.sol does not call this interface. Only the test contract
MockComputeProvider implements it. The decryption verifier comes from setDecryptionVerifier
for the encryption scheme.
Components
| Component | Location | Role |
|---|---|---|
e3-compute-provider | crates/program/compute-provider | SecureProcess, ComputeProvider, ComputeManager, InputPolicy, input tree |
e3-program-server | crates/program/server | HTTP server with POST /run_compute and GET /health. It calls a runner function. |
e3-program-runner | crates/program/runner | run(processor, policy): the dev runner and the OpenVM service, chosen by argument |
e3-openvm-host | crates/program/openvm-host | The OpenVM runner: native run, guest input, worker selection, proof envelope |
interfold-openvm-prover | crates/program/openvm-prover | The worker that proves the guest. A CPU build and a CUDA build. |
| Guest | guest/ in your project | The OpenVM guest. It links your program/ crate. |
| Program runner | runner/ in your project | The program-runner binary. It passes your program/ crate to e3-program-runner |
| Coordination server | server/index.ts (template) | Sends the job, receives the webhook, and calls publishCiphertextOutput |
interfold program compile and interfold program start build and run the program-runner binary
in your project directory. interfold init pins the guest's Interfold crates to the template's
commit.
The secure process
SecureProcess (crates/program/compute-provider/src/secure_process.rs) is the computation that
every CP reproduces. It reads a round in two passes, one ciphertext at a time:
- Every ciphertext, in index order. For each one it recomputes the SAFE commitment, hashes the bytes with Keccak-256, and builds the input's leaf with your policy. Then it drops the bytes.
- The policy's
selectruns over the records that remain: index, hash, stored commitment, metadata, and recomputed commitment. - The selected ciphertexts, read again in index order. Each one is refused unless it hashes to the value from the first pass. Your processor receives them one at a time.
Only one ciphertext is held at a time, so the guest's 512 MiB of memory does not limit the number of
inputs. ComputeInput::run runs the same code over a round that is already in memory. The OpenVM
host uses that to predict the journal the guest must reveal. The guest build swaps in OpenVM's
accelerated hashing, so the prediction holds only while that code matches the host's. Check it by
running the guest without proving: the worker's execute compares the digest it reveals with the
host's journal.
The processor takes the selected ciphertexts as an iterator:
pub fn fhe_processor(fhe_inputs: FHEProcessorInput<'_>) -> Vec<u8> {
let mut sum = Ciphertext::zero(fhe_inputs.params);
for (bytes, _) in fhe_inputs.ciphertexts {
let ciphertext = Ciphertext::from_bytes(&bytes, fhe_inputs.params).unwrap();
sum += &ciphertext;
}
sum.to_bytes()
}The processor must read every item. Keep the state that it carries between items small.
The ComputeProvider trait
A CP that uses ComputeManager implements one method
(crates/program/compute-provider/src/ciphertext_output.rs):
pub trait ComputeProvider {
type Output: Send + Sync;
fn prove(&self, input: &ComputeInput, policy: InputPolicy) -> Self::Output;
}ComputeManager connects the provider, the inputs, and the processor. start(policy) runs the
secure process and then calls prove with the same policy. It returns the provider output and the
output ciphertext from the same input selection. Use with_published when the policy reads the
commitment or metadata of each input.
The dev runner (crates/program/runner/src/dev.rs) is the smallest complete CP. It runs your
processor, builds a mock proof, and serves it over HTTP:
pub(crate) async fn serve(processor: FHEProcessor, policy: InputPolicy) -> Result<()> {
let server = E3ProgramServer::builder(move |job| async move {
let mut manager = ComputeManager::with_published(
MockProofProvider { processor },
job.inputs,
job.published,
processor,
);
let (result, ciphertext) = manager.start(policy)?;
let proof = encode_mock_compute_proof(&[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5], &result)?;
Ok((proof, ciphertext))
})
.build()?;
server.run().await
}Your project passes its processor and policy to e3_program_runner::run from the runner/ crate:
fn main() -> anyhow::Result<()> {
e3_program_runner::run(e3_user_program::fhe_processor, e3_user_program::policy())
}The first command-line argument selects the mode: dev for the dev runner, openvm for the OpenVM
service. interfold program start passes it for you.
The OpenVM service (crates/program/runner/src/openvm.rs) has the same shape. Its runner is
e3_openvm_host::Prover::prove, which runs the secure process natively, writes the guest's input
stream, runs the worker, and returns the verified proof envelope.
The program server
The program server does not send transactions. It computes, then posts the result to the
callback_url of the request. The coordination server receives that webhook and calls
publishCiphertextOutput.
| Setting | Development runner | OpenVM service |
|---|---|---|
| Bind address | 0.0.0.0:13151 | 127.0.0.1:13151; set OPENVM_BIND_ADDR to change it |
| Endpoints | POST /run_compute, GET /health | The same |
| Body limit | 10 MiB | 128 MiB; set OPENVM_MAX_REQUEST_BYTES to change it |
| Body deadline | 120 s | 120 s; set OPENVM_BODY_TIMEOUT_SECS to change it |
| Parallel jobs | 1 | 1; set MAX_CONCURRENT_COMPUTATIONS to change it |
| Webhook | Up to five attempts | Up to five attempts |
| Response | {"status":"processing","e3_id":"<id>"} at once |
A request is admitted before its body is read. A request over the job limit gets HTTP 429, a body over the limit gets HTTP 413, and a body that does not arrive by the deadline gets HTTP 408. The webhook is retried with backoff after a network error or HTTP 5xx, 408, or 429.
Request fields
POST /run_compute takes a JSON body (crates/program/server/src/types.rs, ComputeRequest):
| Field | Type | Content |
|---|---|---|
e3_id | string | The E3 ID. Required. |
chain_id | number | Chain ID for the proof domain |
interfold_address | hex string | Interfold contract address for the proof domain |
encryption_scheme_id | hex string | e3.encryptionSchemeId |
committee_public_key_hash | hex string | e3.committeePublicKey (a hash of the committee key) |
params | hex string | paramSetRegistry[e3.paramSet] from the Interfold contract |
ciphertext_inputs | array of [hex, number] | Each input ciphertext and its on-chain index, sorted by index |
input_commitments | array of hex strings | Optional. The commitment that the program stored for each input. |
input_slots | array of hex strings | Optional. The 20-byte slot of each input. Send it with input_commitments. |
input_parents | array of numbers | Optional. The parent index plus one of each input, at most uint40. |
callback_url | string | The URL for the webhook. Required. |
The template server sends only the first seven fields and callback_url (server/runner.ts). The
default policy needs no per-input data. CRISP also sends the three optional fields.
Webhook payload
The webhook body is a tagged JSON object. On success:
{
"status": "completed",
"e3_id": "1",
"ciphertext": "0x...",
"ciphertext_commitment": "0x...",
"proof": "0x..."
}On failure: {"status":"failed","e3_id":"1","error":"..."}. The template server takes the success
body and calls publishCiphertextOutput with a CiphertextOutputReference:
await sdk.publishCiphertextOutput(BigInt(e3_id), {
contentHash,
ciphertextCommitment: ciphertext_commitment,
computeProof: proof,
// The local program verifies raw bytes as a deterministic mock receipt.
availabilityProof: ciphertext,
})The program server rejects a callback URL that points at a private or link-local address. Loopback
is allowed. Set ALLOW_PRIVATE_CALLBACKS=1 to allow private addresses for local development.
Proof modes
The program section of interfold.config.yaml selects the mode
(crates/cli/src/program_config.rs).
| Setting | What interfold program start runs | Proof |
|---|---|---|
program.dev: true | cargo run --bin program-runner -- dev: the dev runner, with your program/ | Mock proof. Only MockOpenVmReceiptVerifier accepts it. |
program.dev: false, program.openvm | cargo run --release --bin program-runner -- openvm: the OpenVM service | OpenVM Halo2 EVM proof, verified before it is returned |
--dev true on interfold program start or interfold program compile overrides the file, as does
E3_PROGRAM__DEV=true. The template's local scripts set it unless TEMPLATE_REAL_PROOFS=1, and
CRISP's unless CRISP_REAL_PROOFS=1. A configuration with the removed program.risc0 section still
loads, but interfold program refuses it.
Proof format
The guest reveals SHA-256 of nine 32-byte ABI words, 288 bytes (lib/OpenVmComputeProof.sol):
- Chain ID
- Interfold contract address
- E3 ID
- Encryption scheme ID
- Committee public key hash
- Output ciphertext hash
- Output SAFE commitment
- BFV parameter hash
- Input root
The CP returns the proof as abi.encode(bytes seal, bytes32 paramsHash, bytes32 inputRoot). The
seal is abi.encode(uint8(1), bytes(halo2ProofData)), 1,856 bytes. It does not repeat the journal:
the Halo2 proof's only public value is the journal digest. OpenVmComputeProof.decode reads the
envelope on-chain. The verifiers rebuild the other seven fields from on-chain state. See
Verify the Compute Provider.
The guest must match your E3 program
The guest and the service link your program/ crate, so they run your processor and your policy.
The contract, the host, and the guest must derive the same leaves, the same selection, the same
parameter hash, and the same journal.
- A changed program, policy, or
guest/openvm.tomlgives a new receipt identity: a new executable commitment and VM commitment. The protocol verifier and your program contract must both name it, so a changed guest needs new verifiers. - The Interfold contract keeps one ciphertext verifier for each encryption scheme. On one
deployment, every BFV E3 program must use the receipt identity that this verifier names. The
template deploys its own verifiers for its chain (
deploy/default.ts). - If your program hashes data inside the guest, add the hash to the
openvm-hashesfeature of yourprogram/crate and toguest/openvm.toml, as CRISP does for SHA-256.
Run a compute provider
This procedure builds and starts the OpenVM CP. Run it in an Interfold project that interfold init
made.
The program server does not authenticate callers. Any caller that can reach the port can start a computation that your machine proves. Keep it on loopback or behind authenticated admission control.
Install the tools
Install the Interfold CLI (see Installation) and cargo-openvm:
cargo install --locked --git https://github.com/openvm-org/openvm.git --tag v2.0.2 cargo-openvm
cargo openvm setup --evmcargo openvm setup --evm writes the Halo2 proving key, the KZG parameters, and the EVM verifier to
~/.openvm. Without --evm it writes only the recursion keys, and the compile step stops.
Generating the Halo2 key needs a large amount of memory; --download fetches OpenVM's prebuilt key
and verifier instead. Check their provenance before you use them.
Build the workers
Build the worker from the Interfold repository at the same revision as your project:
cargo build --locked --release --manifest-path crates/program/openvm-prover/Cargo.tomlOn a machine with an NVIDIA GPU, also build the CUDA worker into its own directory. It needs the CUDA toolkit:
CARGO_TARGET_DIR=target/cuda cargo build --locked --release --features cuda \
--manifest-path crates/program/openvm-prover/Cargo.tomlConfigure the program section
program:
dev: false
openvm:
prover_bin: /opt/openvm/interfold-openvm-prover
prover_bin_cuda: /opt/openvm/interfold-openvm-prover-cuda
backend: auto| Key | Default | Meaning |
|---|---|---|
prover_bin | None | The CPU worker |
prover_bin_cuda | None | The CUDA worker |
backend | auto | auto, cpu, or cuda |
setup_dir | ~/.openvm | The directory that cargo openvm setup --evm wrote |
Set at least one worker. Every path is absolute. With backend: auto the service uses the CUDA
worker when it is set and can open a GPU, and the CPU worker otherwise. cuda refuses to start
without a working GPU, and cpu never tries one.
Compile
Run interfold program compile. It builds guest/, generates the application proving key, runs the
worker's prepare for the aggregation key and the receipt identity, writes the worker configuration
to .interfold/caches/openvm/prover.json (or to OPENVM_PROVER_CONFIG when it is set; start and
the deploys read the same path), and builds the service. The keys are regenerated only when the
guest executable or guest/openvm.toml changes. The identity is in
.interfold/caches/openvm/prepared/identity.json.
Deploy the verifiers
Deploy the contracts. The template and CRISP deploys read the identity and the verifier artifact
from prover.json, unless you set OPENVM_APP_EXE_COMMIT and OPENVM_APP_VM_COMMIT with either
OPENVM_VERIFIER_ARTIFACT and OPENVM_VERIFIER_SHA256 or OPENVM_HALO2_VERIFIER and
OPENVM_HALO2_RUNTIME_CODE_HASH.
Start the program server
Run interfold program start. The service picks its worker and runs the worker's check, which
loads every key and both KZG parameter files and recomputes the receipt identity. Then it listens on
127.0.0.1:13151.
Check the server
Run curl http://localhost:13151/health. The server returns {"status":"healthy","e3_id":"0"}.
Deadlines and memory
| Variable | Default | Meaning |
|---|---|---|
OPENVM_CHECK_TIMEOUT_SECS | 1800 | Deadline for the startup check |
OPENVM_PROVE_TIMEOUT_SECS | 86400 | Deadline for one proof. The worker is stopped and the round fails. |
OPENVM_SEGMENT_MEMORY_BYTES | 8 GiB | Read by compile. The segment memory limit; 8 GiB fits a 16 GB GPU. |
The worker holds the guest's input stream in memory twice, about eight times the binary size of the round. Jobs are in memory: a restart of the service loses accepted jobs, and the operator must reconcile them.