From 049f09c863fa286c6f7b1bf2a3353bd9efe7d97a Mon Sep 17 00:00:00 2001 From: Toby1009 <69885352+Toby1009@users.noreply.github.com> Date: Fri, 21 Aug 2026 04:52:43 +0800 Subject: [PATCH 1/4] fix(docs): correct stale API references in the support and building guides Four documents describe contract and crate APIs that have since changed. Each correction below was checked against origin/main, and nothing here reverts a fix already present upstream: all four files are byte-identical between origin/main and this branch's merge base. crates/support/README.md - The E3 request snippet used `threshold: [M, N]` and `e3ProgramParams`, neither of which exists. Rewritten against `E3RequestParams`, whose nine fields it now matches in name and order, with the `IInterfold.` / `IE3Program(...)` / `IERC20(...)` qualifiers the snippet needs to compile. - The Boundless "defaults shown" block listed six values that contradicted the table further down the same file. `build_offer()` uses 0.00005, 0.002, 600, 300, 60 and 2.0; both places now say that. - `e3_id` is `Option`, and the request field is `committee_public_key_hash`. The `/run_compute` example sent a number and the older field name, so it could not deserialize. It does now, verified by feeding the body to `ComputeRequest`. - The proof binds nine values, five identifying the context and four coming from the computation. The old text listed seven and omitted the parameter hash and the input root. - The container is built by `build_e3_support_risc0` in ci.yml and `build-e3-support-release` in releases.yml, not by a `support-docker.yml` that does not exist. - Two notes record that the Boundless auction fields are unusable today, per #1812: setting one makes `interfold program start` exit, and the environment variables never reach the container. - `fixtures/payload.json` still fails to deserialize, so the warning now lists the required fields instead of pointing at another example. - Step 2 did not say whose logic the guest carries. `methods/guest/Cargo.toml` pins `e3-user-program` to `crates/support/program`, and CRISP's crate takes the same package name, so the path alone decides which one a build gets. docs/pages/building-with-interfold.mdx - `getE3Quote` and `request` each take one `E3RequestParams`, not six positional arguments. - The `E3` struct gained `ciphertextCommitment`; all fifteen fields now match IE3.sol in order. `requestBlock` holds a timestamp despite its name. - `E3Requested`'s third parameter is `bytes32 indexed cryptoConfigId`; `CiphertextOutputPublished` carries `ciphertextCommitment`; the template's `InputPublished` has no `inputHash`. - The JavaScript example now quotes the fee and approves it before requesting, because `request` pulls the fee with `transferFrom` and reverts with `FeeExceedsMaximum` above `maxFee`. - The input root rules out substituting or dropping an input from the tree. It does not decide which inputs are computed over: since #1821 that is the program's input policy. crates/compute-provider/Readme.md - #1821 removed `use_parallel`, `batch_size` and the parallel path. `new` takes three arguments, `start` takes an `InputPolicy` and returns a `Result`, and `prove` receives the policy. Added sections on policies and `with_published`, because a program whose contract builds a leaf other than the ciphertext commitment cannot reach the right root without them. - The dependency line combined `git` with `path`, which cargo rejects, and used the pre-rename URL. - Every Rust block compiles against the crate. templates/default/interfold.config.yaml - The commented auction parameters are the built-in defaults, and setting any of them makes the launcher exit. Closes #1808 Co-Authored-By: Claude Opus 5 (1M context) --- crates/compute-provider/Readme.md | 116 ++++++++++++++---------- crates/support/README.md | 89 ++++++++++-------- docs/pages/building-with-interfold.mdx | 113 ++++++++++++----------- templates/default/interfold.config.yaml | 3 +- 4 files changed, 186 insertions(+), 135 deletions(-) diff --git a/crates/compute-provider/Readme.md b/crates/compute-provider/Readme.md index 13a956af60..f92f92d740 100644 --- a/crates/compute-provider/Readme.md +++ b/crates/compute-provider/Readme.md @@ -1,15 +1,15 @@ # 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 @@ -17,72 +17,94 @@ 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)> { - 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

(params: FHEInputs, provider: P) -> Result<(P::Output, Vec), 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`. -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. Every published ciphertext gets a leaf +built from its own bytes, whatever `select` then decides to compute over. + +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, +use e3_compute_provider::{ComputeInput, ComputeProvider, InputPolicy}; + +pub struct MyProvider; + +pub struct MyOutput { + pub proof: Vec, } -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)> { - 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. +`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`. diff --git a/crates/support/README.md b/crates/support/README.md index 3efc06cd69..ee8d6eddd9 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 @@ -116,6 +117,11 @@ interfold program compile This builds the guest ELF binary inside the Docker container. Output goes to `./target/riscv-guest/methods/guests/riscv32im-risc0-zkvm-elf/release/program.bin`. +The guest runs the `fhe_processor` and `policy` from `crates/support/program`, which +`methods/guest/Cargo.toml` names as `e3-user-program`. Another E3 program needs a guest built +against its own crate of that name: the policy decides the input-tree leaf, and a leaf that differs +from the one the program's contract built produces a root the round cannot publish. + ### Changing the guest, and the pin that decides which code it runs The guest does **not** compile `crates/compute-provider` from this tree. `crates/support` is a @@ -180,8 +186,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 @@ -189,19 +197,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` @@ -215,11 +226,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" @@ -228,12 +239,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 @@ -243,9 +257,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 @@ -257,7 +273,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 | | ------------ | ------------------------------- | --------- | ---------------------------- | @@ -268,15 +284,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. --- @@ -290,7 +303,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 @@ -314,6 +328,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 30979247cf..cf3acd9ff9 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 @@ -197,15 +192,17 @@ function publishInput( Inputs can be accumulated incrementally into a Merkle tree (LazyIMT, or ultimately chosen by the program implementation). This allows you to: -1. Use the published Merkle root as part of your E3 Program's Secure Process to ensure all published - inputs are processed by the Compute Provider. +1. Use the published Merkle root as part of your E3 Program's Secure Process to ensure no published + input is substituted or dropped from the tree the proof commits to. Which of those inputs the + computation runs over is a separate choice, made by the program's input policy. 2. Prevent input tampering or omission. 3. Anchor proofs for correct execution. ### 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 +233,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 +253,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 +276,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: 0n, // replaced below, once the quote is known } -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 aed306284b..9e364a4411 100644 --- a/templates/default/interfold.config.yaml +++ b/templates/default/interfold.config.yaml @@ -30,7 +30,8 @@ program: # pinata_jwt: "PINATA_JWT" # For uploading programs # 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 From 7aa9e4f80cf06e4102fb6927bf741c640ea0f022 Mon Sep 17 00:00:00 2001 From: Toby1009 <69885352+Toby1009@users.noreply.github.com> Date: Fri, 21 Aug 2026 05:37:43 +0800 Subject: [PATCH 2/4] fix(docs): make the policy section usable and keep the template uncommentable Two defects found by doing what the documents say rather than reading them. The "Input policies" section named `leaf` and `select` but gave no signatures, so the natural attempt does not compile: `leaf` returns `Result` holding hex already reduced into BN254, not the bytes a reader would reach for. Added the two type aliases, a `policy()` that compiles, and the fields `PublishedInput` carries. The worked example is shaped like `crates/support/program`'s, so it can be pasted into a program crate. The template's auction note sat at the same indentation as the commented fields around it. Uncommenting the block, which is the only way to use it, turned two sentences into YAML keys: could not find expected ':' at line 34 column 7 The line it replaced ended in a colon and survived. Commented the prose one level deeper so it stays a comment after the reader strips one. Every Rust block in the README compiles against the crate, and the template now parses both as shipped and fully uncommented. Co-Authored-By: Claude Opus 5 (1M context) --- crates/compute-provider/Readme.md | 32 +++++++++++++++++++++++++ templates/default/interfold.config.yaml | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/compute-provider/Readme.md b/crates/compute-provider/Readme.md index f92f92d740..c17e0e010b 100644 --- a/crates/compute-provider/Readme.md +++ b/crates/compute-provider/Readme.md @@ -51,6 +51,38 @@ where - `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. +Both are plain function pointers, so a policy is a value rather than a trait implementation: + +```rust +pub type LeafFn = fn(&PublishedInput) -> Result; +pub type SelectFn = fn(&[PublishedInput]) -> Vec; +``` + +A leaf is returned as hex, already reduced into the BN254 scalar field. `leaf_from_digest` does that +reduction, so a program hashing its own fields does not restate the modulus: + +```rust +use e3_compute_provider::policy::{all_inputs, leaf_from_digest, InputPolicy, PublishedInput}; +use e3_compute_provider::ComputeError; +use sha2::{Digest, Sha256}; + +fn my_leaf(input: &PublishedInput) -> Result { + let digest = Sha256::digest([input.ciphertext, input.metadata].concat()); + Ok(leaf_from_digest(&digest)) +} + +pub fn policy() -> InputPolicy { + InputPolicy { + leaf: my_leaf, + select: all_inputs, + } +} +``` + +`PublishedInput` carries the input's `index`, its `ciphertext` bytes, the `commitment` the program +stored when it stores one, whatever `metadata` it published, and `recomputed`, the commitment +derived from the bytes. `matches_commitment()` compares the last two. + `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, diff --git a/templates/default/interfold.config.yaml b/templates/default/interfold.config.yaml index 9e364a4411..f983ae2471 100644 --- a/templates/default/interfold.config.yaml +++ b/templates/default/interfold.config.yaml @@ -30,8 +30,8 @@ program: # pinata_jwt: "PINATA_JWT" # For uploading programs # program_url: "https://gateway.pinata.cloud/ipfs/QmNMRAB7DW43JSmENfzGmD96G6sqaeBBNfTVrrq5WQae3D" # Pre-uploaded program # onchain: true # true = onchain requests, false = offchain - # Built-in auction defaults, shown for reference. Leave these unset. - # If you set one, `interfold program start` fails. See #1812. + # # 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 From 0e1e4111f9c59103640ec516c6b0947e00f87f8c Mon Sep 17 00:00:00 2001 From: Toby1009 <69885352+Toby1009@users.noreply.github.com> Date: Fri, 21 Aug 2026 09:02:42 +0800 Subject: [PATCH 3/4] fix(docs): say the walkthrough needs a project before Step 2 `interfold program compile` is Step 2, and from a checkout of this repository it exits 1 with `Configuration file not found`. The `program` subcommands shell out to `.interfold/support/ctl`, which `interfold init` writes (crates/init/src/lib.rs:163-178), and Prerequisites never mentions either. Added the missing prerequisite and said which directory the steps run from. Co-Authored-By: Claude Opus 5 (1M context) --- crates/support/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/support/README.md b/crates/support/README.md index ee8d6eddd9..d8d5052e00 100644 --- a/crates/support/README.md +++ b/crates/support/README.md @@ -84,6 +84,10 @@ This matches the format expected by CRISP and `E3ProgramServer` in `crates/progr 4. **Boundless wallet** — an Ethereum private key with ETH (for gas) and ZKC (for collateral) on the Boundless-supported chain 5. **Interfold CLI** — `cargo install --locked --path ./crates/cli --bin interfold -f` +6. **An Interfold project** — `interfold init `, then work from that directory. The steps + below run against a project, not against a checkout of this repository. Without one, + `interfold program compile` exits with `Configuration file not found`, because `interfold init` + is what writes `.interfold/support/ctl`, the scripts every `program` subcommand shells out to. ### Step 1: Configure `interfold.config.yaml` From 1d5cf41f661857efa1ec6b75d5e59a38e50bf095 Mon Sep 17 00:00:00 2001 From: Toby1009 <69885352+Toby1009@users.noreply.github.com> Date: Fri, 21 Aug 2026 09:12:53 +0800 Subject: [PATCH 4/4] fix(docs): name the fields matches_commitment compares The sentence said "the last two", but the enumeration before it ends with `metadata` and `recomputed`, while `matches_commitment` compares `commitment` with `recomputed` (policy.rs:55-62). Named them. Co-Authored-By: Claude Opus 5 (1M context) --- crates/compute-provider/Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/compute-provider/Readme.md b/crates/compute-provider/Readme.md index c17e0e010b..3d7740c6d9 100644 --- a/crates/compute-provider/Readme.md +++ b/crates/compute-provider/Readme.md @@ -81,7 +81,7 @@ pub fn policy() -> InputPolicy { `PublishedInput` carries the input's `index`, its `ciphertext` bytes, the `commitment` the program stored when it stores one, whatever `metadata` it published, and `recomputed`, the commitment -derived from the bytes. `matches_commitment()` compares the last two. +derived from the bytes. `matches_commitment()` compares `commitment` against `recomputed`. `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