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

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 KeyPublished stage, 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 verify on the program contract.
  • The request field computeProviderParams goes to IE3Program.validate unchanged. 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

ComponentLocationRole
e3-compute-providercrates/program/compute-providerSecureProcess, ComputeProvider, ComputeManager, InputPolicy, input tree
e3-program-servercrates/program/serverHTTP server with POST /run_compute and GET /health. It calls a runner function.
e3-program-runnercrates/program/runnerrun(processor, policy): the dev runner and the OpenVM service, chosen by argument
e3-openvm-hostcrates/program/openvm-hostThe OpenVM runner: native run, guest input, worker selection, proof envelope
interfold-openvm-provercrates/program/openvm-proverThe worker that proves the guest. A CPU build and a CUDA build.
Guestguest/ in your projectThe OpenVM guest. It links your program/ crate.
Program runnerrunner/ in your projectThe program-runner binary. It passes your program/ crate to e3-program-runner
Coordination serverserver/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:

  1. 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.
  2. The policy's select runs over the records that remain: index, hash, stored commitment, metadata, and recomputed commitment.
  3. 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:

program/src/lib.rs
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):

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:

crates/program/runner/src/dev.rs
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:

runner/src/main.rs
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.

SettingDevelopment runnerOpenVM service
Bind address0.0.0.0:13151127.0.0.1:13151; set OPENVM_BIND_ADDR to change it
EndpointsPOST /run_compute, GET /healthThe same
Body limit10 MiB128 MiB; set OPENVM_MAX_REQUEST_BYTES to change it
Body deadline120 s120 s; set OPENVM_BODY_TIMEOUT_SECS to change it
Parallel jobs11; set MAX_CONCURRENT_COMPUTATIONS to change it
WebhookUp to five attemptsUp 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):

FieldTypeContent
e3_idstringThe E3 ID. Required.
chain_idnumberChain ID for the proof domain
interfold_addresshex stringInterfold contract address for the proof domain
encryption_scheme_idhex stringe3.encryptionSchemeId
committee_public_key_hashhex stringe3.committeePublicKey (a hash of the committee key)
paramshex stringparamSetRegistry[e3.paramSet] from the Interfold contract
ciphertext_inputsarray of [hex, number]Each input ciphertext and its on-chain index, sorted by index
input_commitmentsarray of hex stringsOptional. The commitment that the program stored for each input.
input_slotsarray of hex stringsOptional. The 20-byte slot of each input. Send it with input_commitments.
input_parentsarray of numbersOptional. The parent index plus one of each input, at most uint40.
callback_urlstringThe 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:

server/index.ts
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).

SettingWhat interfold program start runsProof
program.dev: truecargo run --bin program-runner -- dev: the dev runner, with your program/Mock proof. Only MockOpenVmReceiptVerifier accepts it.
program.dev: false, program.openvmcargo run --release --bin program-runner -- openvm: the OpenVM serviceOpenVM 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):

  1. Chain ID
  2. Interfold contract address
  3. E3 ID
  4. Encryption scheme ID
  5. Committee public key hash
  6. Output ciphertext hash
  7. Output SAFE commitment
  8. BFV parameter hash
  9. 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.toml gives 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-hashes feature of your program/ crate and to guest/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 --evm

cargo 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.toml

On 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.toml

Configure the program section

interfold.config.yaml
program:
  dev: false
  openvm:
    prover_bin: /opt/openvm/interfold-openvm-prover
    prover_bin_cuda: /opt/openvm/interfold-openvm-prover-cuda
    backend: auto
KeyDefaultMeaning
prover_binNoneThe CPU worker
prover_bin_cudaNoneThe CUDA worker
backendautoauto, cpu, or cuda
setup_dir~/.openvmThe 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

VariableDefaultMeaning
OPENVM_CHECK_TIMEOUT_SECS1800Deadline for the startup check
OPENVM_PROVE_TIMEOUT_SECS86400Deadline for one proof. The worker is stopped and the round fails.
OPENVM_SEGMENT_MEMORY_BYTES8 GiBRead 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.

Next steps