Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 69 additions & 47 deletions crates/compute-provider/Readme.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,110 @@
# FHE Compute Manager

This project provides a flexible and efficient framework for managing Secure Programs (SP) of the
[Interfold Protocol](https://theinterfold.com). It supports both sequential and parallel processing,
with the ability to integrate various compute providers.
This project provides a framework for managing Secure Programs (SP) of the
[Interfold Protocol](https://theinterfold.com), with the ability to integrate various compute
providers.

## Features

- Support for both sequential and parallel FHE computations
- Flexible integration of different compute providers
- Merkle tree generation for input verification
- Ciphertext hashing for output verification
- Per-program input policies that decide the leaf layout and which inputs the computation sees

## Installation

To use this library, add it to your `Cargo.toml`:

```toml
[dependencies]
e3-compute-provider = { git = "https://github.com/gnosisguild/interfold.git", path = "crates/compute-provider"}
e3-compute-provider = { git = "https://github.com/theinterfold/interfold.git" }
```

## Usage

To use the library, follow these steps:

1. Create an instance of the `ComputeManager` with your desired configuration.
2. Call the `start` method to begin the computation process.
3. The method will return the computed ciphertext and the corresponding proof.
1. Create an instance of the `ComputeManager` with your compute provider and inputs.
2. Call the `start` method with your E3 program's `InputPolicy`.
3. The method returns the provider output together with the computed ciphertext bytes.

```rust
use anyhow::Result;
use e3_compute_provider::{ComputeInput, ComputeManager, ComputeProvider, ComputeResult, FHEInputs};
use voting_core::fhe_processor;

// Define your Risc0Provider struct and implement the ComputeProvider trait
pub fn run_compute(params: FHEInputs) -> Result<(Risc0Output, Vec<u8>)> {
let risc0_provider = Risc0Provider;
let mut provider = ComputeManager::new(risc0_provider, params, fhe_processor, false, None);
let output = provider.start();
Ok(output)
use e3_compute_provider::{ComputeError, ComputeManager, ComputeProvider, FHEInputs, InputPolicy};
use my_program::fhe_processor;

pub fn run_compute<P>(params: FHEInputs, provider: P) -> Result<(P::Output, Vec<u8>), ComputeError>
where
P: ComputeProvider + Send + Sync,
{
let mut manager = ComputeManager::new(provider, params, fhe_processor);
manager.start(InputPolicy::default())
}
```

## Risc0 Example
`fhe_processor` is your own function. It must match the exported `FHEProcessor` alias,
`fn(&FHEInputs) -> Vec<u8>`.

Here's a more detailed example of how to use the Compute Manager with Risc0:
## Input policies

`InputPolicy` carries the two answers that differ between E3 programs:

- `leaf` builds a tree leaf. It must equal what the E3 program builds on chain for the same input.
- `select` chooses which inputs the computation runs over, by index.

`InputPolicy::default()` is the behaviour every E3 program had before policies existed. The leaf is
the ciphertext's own SAFE commitment, and every input is computed over. A program whose contract
inserts something else, or that treats a second input from one participant as a replacement,
supplies its own.

A policy cannot supply a root or drop an input from the tree. Leaves are derived from the
ciphertexts the Secure Process consumed, and every published input contributes one.

When your E3 program publishes a commitment or other data alongside each ciphertext, build the
manager with `with_published` so the policy can read it:

```rust
let mut manager = ComputeManager::with_published(provider, params, published, fhe_processor);
```

## Implementing a provider

`ComputeProvider` has one method and one associated type. Everything else is yours to choose:

```rust
use e3_compute_provider::{ComputeInput, ComputeManager, ComputeProvider, ComputeResult, FHEInputs};
use methods::VOTING_ELF;
use risc0_ethereum_contracts::groth16;
use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts, VerifierContext};
use serde::{Deserialize, Serialize};

pub struct Risc0Provider;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Risc0Output {
pub result: ComputeResult,
pub seal: Vec<u8>,
use e3_compute_provider::{ComputeInput, ComputeProvider, InputPolicy};

pub struct MyProvider;

pub struct MyOutput {
pub proof: Vec<u8>,
}

impl ComputeProvider for Risc0Provider {
type Output = Risc0Output;
fn prove(&self, input: &ComputeInput) -> Self::Output {
// Implementation details
impl ComputeProvider for MyProvider {
type Output = MyOutput;

fn prove(&self, input: &ComputeInput, policy: InputPolicy) -> Self::Output {
// Prove that `input` produced its committed result under `policy`, however your
// backend does that, and return whatever the caller needs.
MyOutput { proof: Vec::new() }
}
}
pub fn run_compute(params: FHEInputs) -> Result<(Risc0Output, Vec<u8>)> {
let risc0_provider = Risc0Provider;
let mut provider = ComputeManager::new(risc0_provider, params, fhe_processor, false, None);
let output: (Risc0Output, Vec<u8>) = provider.start();
Ok(output)
}
```

This example demonstrates how to create a Risc0Provider, use it with the ComputeManager, and measure
the execution time of the computation.
`prove` receives the policy rather than choosing one. A prover that picked its own would select a
different input set from the one `start` returned the ciphertext for.

The repository's RISC Zero and Boundless providers live in `e3-support-host`. That crate is in a
separate workspace, so the dependency above does not pull it in. Inside an Interfold checkout, its
`run_risc0_compute` and `run_compute` entry points wrap the two backends, and
`crates/support/host/src/lib.rs` is the reference implementation to read.

## Configuration

The `ComputeManager::new()` function takes several parameters:
`ComputeManager::new()` takes three parameters:

- `provider`: An instance of your compute provider (e.g., `Risc0Provider`)
- `provider`: An instance of your compute provider (e.g., `MyProvider`)
- `fhe_inputs`: The FHE inputs for the computation
- `fhe_processor`: A function to process the FHE inputs
- `use_parallel`: A boolean indicating whether to use parallel processing
- `batch_size`: An optional batch size for parallel processing, must be a power of 2

`ComputeManager::with_published()` takes the same three, plus `published`: one `PublishedData` entry
per ciphertext, in the same order as `fhe_inputs.ciphertexts`.
84 changes: 48 additions & 36 deletions crates/support/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ graph TD
fulfillment.
- **`types/`** — Shared request, webhook, proof-domain, guest-input, and journal types.
- **`methods/`** — RISC Zero build crate. Compiles the guest program.
- **`guest/`** — The RISC Zero zkVM guest program. Runs `fhe_processor` (homomorphic ciphertext
summation) and commits the domain-bound `ComputeJournal`.
- **`methods/guest/`** — The RISC Zero zkVM guest program. Runs `fhe_processor` (homomorphic
ciphertext summation) and commits the domain-bound `ComputeJournal`.
- **`program/`** — The FHE processor (`fhe_processor`): sums BFV ciphertexts homomorphically.

## Webhook Payload Format
Expand All @@ -57,7 +57,7 @@ The callback server receives a tagged-enum JSON payload:
```json
{
"status": "completed",
"e3_id": 123,
"e3_id": "123",
"ciphertext": "0x...",
"ciphertext_commitment": "0x...",
"proof": "0x..."
Expand All @@ -67,7 +67,7 @@ The callback server receives a tagged-enum JSON payload:
**Failure:**

```json
{ "status": "failed", "e3_id": 123, "error": "Computation failed: ..." }
{ "status": "failed", "e3_id": "123", "error": "Computation failed: ..." }
```

This matches the format expected by CRISP and `E3ProgramServer` in `crates/program-server`.
Expand Down Expand Up @@ -98,13 +98,14 @@ program:
pinata_jwt: '${PINATA_JWT}'
program_url: 'https://gateway.pinata.cloud/ipfs/Qm...' # after upload (Step 3)
onchain: true
# Optional — custom auction params (defaults shown):
# min_price_eth: 0.001
# max_price_eth: 0.03
# timeout_secs: 1200
# lock_timeout_secs: 600
# ramp_up_secs: 120
# lock_collateral_zkc: 5.0
# Built-in auction defaults, shown for reference. Leave these fields unset.
# If you set one, `interfold program start` fails. See #1812.
# min_price_eth: 0.00005
# max_price_eth: 0.002
# timeout_secs: 600
# lock_timeout_secs: 300
# ramp_up_secs: 60
# lock_collateral_zkc: 2.0
```

### Step 2: Compile the RISC Zero Guest Program
Expand Down Expand Up @@ -180,28 +181,33 @@ This boots the ciphernodes, which listen for E3 requests, perform DKG, and await
interfold program start
```

This starts the Docker container running `e3-support-app` on port 13151. If Boundless config is
present, it will submit proofs to the Boundless market. Otherwise it falls back to dev mode.
This starts the Docker container that runs `e3-support-app` on port 13151. The `risc0_dev_mode`
value selects the proving backend, as shown in Step 1. `0` submits proofs to the Boundless market.
`1` returns fake proofs. The default is `1` when the field is unset. A Boundless request with
missing credentials fails instead of using dev mode.

### Step 6: Submit an E3 Request

The E3 request is submitted on-chain by the instigator (e.g., CRISP coordination server):

```solidity
// On-chain: Interfold.request(params)
interfold.request(E3RequestParams({
threshold: [M, N],
interfold.request(IInterfold.E3RequestParams({
committeeSize: IInterfold.CommitteeSize.Minimum,
inputWindow: [start, end],
e3Program: crispProgramAddress,
e3ProgramParams: encodedParams,
e3Program: IE3Program(crispProgramAddress),
paramSet: paramSetIndex, // registered via setParamSet
computeProviderParams: "",
customParams: ""
customParams: encodedRoundConfig, // CRISPProgram decodes seven values from this
expectedFeeToken: IERC20(feeTokenAddress),
expectedCryptoConfigId: cryptoConfigId,
maxFee: maxFee
}));
```

This triggers:

1. Fee payment (1 USDC)
1. Payment of the quoted fee in the active fee token
2. Committee selection via sortition
3. DKG (C0-C5 proofs) → committee public key published on-chain
4. Stage → `KeyPublished`
Expand All @@ -215,11 +221,11 @@ server:
curl -X POST http://localhost:13151/run_compute \
-H "Content-Type: application/json" \
-d '{
"e3_id": 1,
"e3_id": "1",
"chain_id": 31337,
"interfold_address": "0x1111111111111111111111111111111111111111",
"encryption_scheme_id": "0x...",
"committee_public_key": "0x...",
"committee_public_key_hash": "0x...",
"params": "0x...",
"ciphertext_inputs": [["0x...", 0], ["0x...", 1]],
"callback_url": "http://host.local:4000/state/add-result"
Expand All @@ -228,12 +234,15 @@ curl -X POST http://localhost:13151/run_compute \

The program server:

1. Returns `{"status":"processing","e3_id":1}` immediately
1. Returns `{"status":"processing","e3_id":"1"}` immediately
2. Runs FHE computation (homomorphic sum) locally → ciphertext output
3. Submits proof request to Boundless market
4. Waits for a prover to fulfill the request
5. Sends webhook callback with
`{"status":"completed","e3_id":1,"ciphertext":"0x...","ciphertext_commitment":"0x...","proof":"0x..."}`
`{"status":"completed","e3_id":"1","ciphertext":"0x...","ciphertext_commitment":"0x...","proof":"0x..."}`
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Steps 3 and 4 belong to the Boundless path that Step 1 configures. With `risc0_dev_mode: 1` the
server runs the same computation and returns a fake proof instead.

### Step 8: Webhook Handler Publishes On-Chain

Expand All @@ -243,9 +252,11 @@ The callback server (e.g., CRISP) receives the webhook and calls:
interfold.publishCiphertextOutput(e3Id, ciphertextOutput, ciphertextCommitment, proof);
```

The proof binds the chain, Interfold contract, E3, encryption scheme, committee key, output, and
SAFE commitment. The protocol verifier checks these fields before the application verifier. Both
checks must pass before the E3 can remain in `CiphertextReady`.
The proof binds nine values. Five identify the context: the chain, the Interfold contract, the E3,
the encryption scheme, and the committee key hash. Four come from the computation: the output hash,
the SAFE commitment, the parameter hash, and the input root. The protocol verifier checks these
fields before the E3 program verifier. Both checks must pass before the E3 can remain in
`CiphertextReady`.

### Step 9: Decryption & Completion

Expand All @@ -257,7 +268,7 @@ rewards distributed.

## Boundless Offer Parameters

All parameters are configurable via environment variables (or `interfold.config.yaml`). Defaults:
`build_offer()` reads these environment variables. Defaults:

| Parameter | Env Var | Default | Description |
| ------------ | ------------------------------- | --------- | ---------------------------- |
Expand All @@ -268,15 +279,12 @@ All parameters are configurable via environment variables (or `interfold.config.
| Ramp-up | `BOUNDLESS_RAMP_UP_SECS` | `60` | Price ramp-up period (sec) |
| Collateral | `BOUNDLESS_LOCK_COLLATERAL_ZKC` | `2.0` | ZKC locked per request |

These can also be set in `interfold.config.yaml` under `program.risc0.boundless`:
`interfold program start` always uses these defaults. Neither route to change them works today: the
`program.risc0.boundless` fields make the launcher exit, and the environment variables never reach
the container. See #1812.

```yaml
boundless:
min_price_eth: 0.002
max_price_eth: 0.05
timeout_secs: 1800
# ...
```
To use other values, open a shell in the container, export the variables there, and start
`e3-support-app` yourself. `./scripts/dev.sh` opens such a shell.

---

Expand All @@ -290,7 +298,8 @@ boundless:
./scripts/build.sh --push
```

The container is also built by the GitHub workflow at `.github/workflows/support-docker.yml`.
CI builds the container in the `build_e3_support_risc0` job of `.github/workflows/ci.yml`. The
`build-e3-support-release` job in `.github/workflows/releases.yml` builds release images.

## Development

Expand All @@ -314,6 +323,9 @@ cargo run --bin e3-support-app
./curl_test.sh
```

`fixtures/payload.json` is out of date and the request fails to deserialize. Use the Step 7 body
until the fixture is refreshed.

NOTE: This is outside of the main workspace because it needs to be run within its own context in
order to isolate risc0.

Expand Down
Loading
Loading