diff --git a/crates/compute-provider/Readme.md b/crates/compute-provider/Readme.md index 13a956af6..34d51bad8 100644 --- a/crates/compute-provider/Readme.md +++ b/crates/compute-provider/Readme.md @@ -17,7 +17,7 @@ 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 @@ -26,56 +26,52 @@ 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. +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)> { - 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::{ComputeManager, ComputeProvider, FHEInputs}; +use my_program::fhe_processor; + +pub fn run_compute

(params: FHEInputs, provider: P) -> (P::Output, Vec) +where + P: ComputeProvider + Send + Sync, +{ + let mut manager = ComputeManager::new(provider, params, fhe_processor, false, None); + manager.start() } ``` -## Risc0 Example +`fhe_processor` is your own function. It must match the exported `FHEProcessor` alias, +`fn(&FHEInputs) -> Vec`. -Here's a more detailed example of how to use the Compute Manager with Risc0: +## 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, +use e3_compute_provider::{ComputeInput, ComputeProvider}; + +pub struct MyProvider; + +pub struct MyOutput { + pub proof: Vec, } -impl ComputeProvider for Risc0Provider { - type Output = Risc0Output; +impl ComputeProvider for MyProvider { + type Output = MyOutput; + fn prove(&self, input: &ComputeInput) -> Self::Output { - // Implementation details + // Prove that `input` produced its committed result, however your + // backend does that, and return whatever the caller needs. + MyOutput { proof: Vec::new() } } } -pub fn run_compute(params: FHEInputs) -> Result<(Risc0Output, Vec)> { - let risc0_provider = Risc0Provider; - let mut provider = ComputeManager::new(risc0_provider, params, fhe_processor, false, None); - let output: (Risc0Output, Vec) = 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. +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 @@ -85,4 +81,5 @@ The `ComputeManager::new()` function takes several parameters: - `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 +- `batch_size`: The number of ciphertexts per parallel chunk, read only when `use_parallel` is true. + It defaults to 2 when not supplied. diff --git a/crates/support/README.md b/crates/support/README.md index 4e5607a4d..6a93f5078 100644 --- a/crates/support/README.md +++ b/crates/support/README.md @@ -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 @@ -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..." @@ -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`. @@ -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 @@ -144,8 +145,10 @@ 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 @@ -153,19 +156,22 @@ The E3 request is submitted on-chain by the instigator (e.g., CRISP coordination ```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` @@ -179,11 +185,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" @@ -192,12 +198,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..."}` + +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 @@ -207,9 +216,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 @@ -221,7 +232,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 | | ------------ | ------------------------------- | --------- | ---------------------------- | @@ -232,15 +243,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. --- @@ -254,7 +262,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 @@ -278,6 +287,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. diff --git a/docs/pages/building-with-interfold.mdx b/docs/pages/building-with-interfold.mdx index 30979247c..fd26c8b0e 100644 --- a/docs/pages/building-with-interfold.mdx +++ b/docs/pages/building-with-interfold.mdx @@ -1,8 +1,9 @@ # Building with the Interfold The Interfold smart contract acts as the central coordinator for all E3 operations. It manages -computation requests, input validation, Ciphernode Committees (CiCos), and result publication while -maintaining the security and privacy guarantees of the protocol. +computation requests, Ciphernode Committees (CiCos), and result publication while maintaining the +security and privacy guarantees of the protocol. Each E3 program validates its own requests, inputs, +and outputs. ## The Interfold Smart Contract @@ -10,11 +11,13 @@ maintaining the security and privacy guarantees of the protocol. - Manage E3 computation requests - Coordinate Ciphernode Committees -- Handle encrypted input submission -- Manage Merkle trees for input verification +- Store E3 program references and invoke their request-validation and output-verification hooks - Publish computation results - Emit events for off-chain services +Input submission and the input Merkle tree belong to each E3 program, not to Interfold. See +`IE3Program.publishInput`. + ### Key State Variables ```solidity @@ -47,7 +50,7 @@ contract Interfold { // Mapping of E3 payment amounts. mapping(uint256 e3Id => uint256 amount) public e3Payments; - // Maximum allowed duration for an E3's input window. + // Maximum allowed worst-case request-to-decryption duration. uint256 public maxDuration; // Auto-incrementing E3 identifier. @@ -61,9 +64,9 @@ Each computation is tracked by an `E3` struct: ```solidity struct E3 { - uint256 seed; // Random seed for committee selection + uint256 seed; // Random seed for the E3 computation CommitteeSize committeeSize; // Committee size enum (Minimum, Micro, Small) - uint256 requestBlock; // Block when E3 was requested + uint256 requestBlock; // Request timestamp, despite the name uint256[2] inputWindow; // [start, end] timestamps for input acceptance bytes32 encryptionSchemeId; // Encryption scheme identifier IE3Program e3Program; // E3 Program contract address @@ -75,6 +78,7 @@ struct E3 { bytes32 ciphertextOutput; // Hash of encrypted output bytes plaintextOutput; // Decrypted final result address requester; // Entity that requested computation + bytes32 ciphertextCommitment; // SAFE commitment to the decoded BFV ciphertext } ``` @@ -82,15 +86,15 @@ struct E3 { Each E3 progresses through these stages: -| Stage | Value | Description | -| -------------------- | ----- | ----------------------------------------------- | -| `None` | 0 | E3 does not exist | -| `Requested` | 1 | E3 submitted, committee selection initiated | -| `CommitteeFinalized` | 2 | Sortition complete, DKG started | -| `KeyPublished` | 3 | Committee public key published, inputs accepted | -| `CiphertextReady` | 4 | Encrypted output published, awaiting decryption | -| `Complete` | 5 | Plaintext output published, rewards distributed | -| `Failed` | 6 | Timeout or fault detected, refund initiated | +| Stage | Value | Description | +| -------------------- | ----- | ------------------------------------------------------------------------ | +| `None` | 0 | E3 does not exist | +| `Requested` | 1 | E3 submitted, committee selection initiated | +| `CommitteeFinalized` | 2 | Sortition complete, DKG started | +| `KeyPublished` | 3 | Committee public key published; the E3 program controls input acceptance | +| `CiphertextReady` | 4 | Encrypted output published, awaiting decryption | +| `Complete` | 5 | Plaintext output published, rewards distributed | +| `Failed` | 6 | Failure recorded. `processE3Failure` calculates refunds | ### View Functions @@ -104,14 +108,7 @@ function getE3(uint256 e3Id) external view returns (E3 memory e3); function getE3Stage(uint256 e3Id) external view returns (E3Stage stage); // Estimate the fee for an E3 request -function getE3Quote( - CommitteeSize committeeSize, - uint256[2] calldata inputWindow, - IE3Program e3Program, - uint8 paramSet, - bytes calldata computeProviderParams, - bytes calldata customParams -) external view returns (uint256 fee); +function getE3Quote(E3RequestParams calldata e3Params) external view returns (uint256 fee); // Quotes are cost-plus: modeled ciphernode costs plus marginBps. On success, // the treasury receives protocolShareBps of the gross E3 fee and the active @@ -141,26 +138,22 @@ function getTimeoutConfig() external view returns (E3TimeoutConfig memory config ### Request Flow -1. Users submit the request parameters: committee size, timing windows, program references, BFV - parameter set, and optional custom params. +1. Users submit one `E3RequestParams` value. It carries the committee size, the input window, the + program reference, the BFV parameter set, the application params, and the fee bounds the + requester accepts. ```solidity function request( - CommitteeSize committeeSize, - uint256[2] calldata inputWindow, - IE3Program e3Program, - uint8 paramSet, - bytes calldata computeProviderParams, - bytes calldata customParams - ) external returns (uint256 e3Id); + E3RequestParams calldata requestParams + ) external returns (uint256 e3Id, E3 memory e3); ``` -2. Contract validates the parameters, estimates the fee, and stores an `E3` record with the shared - seed used for deterministic ticket scoring. +2. Contract validates the parameters, quotes the fee, and stores an `E3` record with a seed for the + computation. The registry derives a separate committee seed after the request is final. 3. The E3 program's `validate` hook returns the encryption scheme ID, which is persisted on the `E3` struct. 4. Committee selection is delegated to the `ciphernodeRegistry` via `requestCommittee`. -5. `E3Requested(e3Id, e3, e3ProgramAddress)` is emitted for off-chain watchers. +5. `E3Requested(e3Id, e3, cryptoConfigId)` is emitted for off-chain watchers. ## Committee and Ciphernode Management @@ -180,8 +173,10 @@ Distributed Key Generation (DKG) to produce a shared public key. When the aggreg published, the CiphernodeRegistry calls `onCommitteePublished()` on the Interfold contract, which transitions the E3 to the `KeyPublished` stage. -Once the E3 reaches the `KeyPublished` stage and the input window opens -(`block.timestamp >= inputWindow[0]`), Data Providers can submit inputs to the computation. +Interfold does not gate input submission. Each E3 program does, in its own `publishInput`. The +recommended gate is the `KeyPublished` stage plus both bounds of `inputWindow`. The default template +checks only `inputWindow[1]`, so it accepts inputs from the `Requested` stage onward. `CRISPProgram` +checks the stage and both bounds. ## Input Publication @@ -204,8 +199,9 @@ program implementation). This allows you to: ### Merkle Tree Construction -The Interfold uses a Lean Merkle Tree implementation. Each input is hashed using the PoseidonT3 Hash -function, and the root is updated with each new input. +The input tree belongs to the E3 program, not to Interfold. The default template uses +`InternalLazyIMT` from `@zk-kit/lazy-imt.sol`. It inserts each SAFE ciphertext commitment directly +as a leaf, and PoseidonT3 combines node pairs as the tree grows. Your E3 program should: @@ -236,11 +232,15 @@ the E3's public key. It is also recommended to bundle in proofs to validate: **Interfold Contract Events:** ```solidity -event E3Requested(uint256 e3Id, E3 e3, IE3Program indexed e3Program); +event E3Requested(uint256 e3Id, E3 e3, bytes32 indexed cryptoConfigId); event E3StageChanged(uint256 indexed e3Id, E3Stage previousStage, E3Stage newStage); -event CiphertextOutputPublished(uint256 indexed e3Id, bytes ciphertextOutput); +event CiphertextOutputPublished( + uint256 indexed e3Id, + bytes ciphertextOutput, + bytes32 ciphertextCommitment +); event PlaintextOutputPublished(uint256 indexed e3Id, bytes plaintextOutput, bytes proof); @@ -252,14 +252,15 @@ event RewardsDistributed(uint256 indexed e3Id, address[] nodes, uint256[] amount **Program Contract Events:** ```solidity -event InputPublished(uint256 indexed e3Id, bytes data, uint256 inputHash, uint256 index); +event InputPublished(uint256 indexed e3Id, bytes data, uint256 index); ``` ### Result Publication Flow -1. The Compute Provider submits a proof and ciphertext output. -2. The Interfold uses the E3 Program contract to verify the proof and emits - `CiphertextOutputPublished`. +1. The Compute Provider returns the proof and ciphertext output to a callback server, which submits + them to Interfold. +2. Interfold verifies the proof twice. The request-time protocol verifier runs first, then the E3 + program verifier. Both checks must pass before Interfold emits `CiphertextOutputPublished`. 3. Ciphernodes decrypt the ciphertext output. 4. The plaintext result is then published and the Decryption Verifier validates the decryption proof. @@ -274,15 +275,24 @@ event InputPublished(uint256 indexed e3Id, bytes data, uint256 inputHash, uint25 const interfoldContract = new ethers.Contract(interfoldAddress, interfoldAbi, signer) const requestParams = { - threshold: [3, 5], + committeeSize, inputWindow: [inputWindowStart, inputWindowEnd], e3Program: e3ProgramAddress, - e3ProgramParams, + paramSet, computeProviderParams, customParams: '0x', + expectedFeeToken, + expectedCryptoConfigId, + maxFee, } -const tx = await interfoldContract.request(requestParams) +// `request` pulls the fee with `transferFrom`, so quote it and approve it first. +const feeTokenContract = new ethers.Contract(expectedFeeToken, erc20Abi, signer) +const fee = await interfoldContract.getE3Quote(requestParams) +await (await feeTokenContract.approve(interfoldAddress, fee)).wait() + +// `request` reverts with `FeeExceedsMaximum` when the quote is above `maxFee`. +const tx = await interfoldContract.request({ ...requestParams, maxFee: fee }) const receipt = await tx.wait() const e3Id = receipt.logs .map((log) => interfoldContract.interface.parseLog(log)) diff --git a/templates/default/interfold.config.yaml b/templates/default/interfold.config.yaml index aed306284..ab2320def 100644 --- a/templates/default/interfold.config.yaml +++ b/templates/default/interfold.config.yaml @@ -26,11 +26,12 @@ program: # risc0_dev_mode: 0 # 0 = production (Boundless), 1 = dev mode (fake proofs) # boundless: # rpc_url: "https://sepolia.infura.io/v3/YOUR_KEY" - # private_key: "PRIVATE_KEY" # Use env vars for secrets - # pinata_jwt: "PINATA_JWT" # For uploading programs + # private_key: "${PRIVATE_KEY}" # Read from the environment + # pinata_jwt: "${PINATA_JWT}" # Read from the environment, for uploads # program_url: "https://gateway.pinata.cloud/ipfs/QmNMRAB7DW43JSmENfzGmD96G6sqaeBBNfTVrrq5WQae3D" # Pre-uploaded program # onchain: true # true = onchain requests, false = offchain - # Optional — custom auction parameters (defaults shown): + # Built-in auction defaults, shown for reference. Leave these unset. + # If you set one, `interfold program start` fails. See #1812. # min_price_eth: 0.00005 # max_price_eth: 0.002 # timeout_secs: 600