From e2709bf5293893f884b509b2c1b3ca69194a8c1f Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 26 Sep 2026 18:35:41 +0500 Subject: [PATCH] chore(docs): rewrite Build integration pages and the encrypt tutorial Rewrite the Interfold contract, request, best-practice, SDK, and client-and-server pages, and the encrypt-and-submit tutorial. Corrections include: inputs go to the E3 program's publishInput; getE3PublicKey returns the key commitment; CiphertextOutputPublished is never emitted (the contract emits CiphertextOutputReferencePublished); failed-E3 slashed funds go to honest nodes or the treasury; refunds wait one day after the deadline; mainnet accepts only the small committee. Every SDK export and signature is checked against packages/interfold-sdk. --- docs/pages/build/best-practices.mdx | 223 +++-- docs/pages/build/client-and-server.mdx | 662 +++++++------- docs/pages/build/interfold-contract.mdx | 670 +++++++------- docs/pages/build/requesting-an-e3.mdx | 457 +++++++--- docs/pages/build/sdk.mdx | 841 ++++++++++-------- .../build/tutorials/encrypt-and-submit.mdx | 434 +++++---- 6 files changed, 1871 insertions(+), 1416 deletions(-) diff --git a/docs/pages/build/best-practices.mdx b/docs/pages/build/best-practices.mdx index 03c18f7db3..0533877a72 100644 --- a/docs/pages/build/best-practices.mdx +++ b/docs/pages/build/best-practices.mdx @@ -1,70 +1,161 @@ --- title: 'Best Practices' +description: + 'Design and operations practices for E3 programs, requesters, clients, and servers. Each practice + follows from a mechanism in the contracts, the SDK, or the template.' --- -# Design & Operations Best Practices - -Competitor docs such as Fhenix's "Key Considerations" and Ritual's architecture primers emphasize -clear guardrails for confidential compute. This guide distills the equivalent playbook for the -Interfold. - -## Parameter Strategy - -- **Committee configuration**: Choose a supported committee enum. Each enum fixes total members `N`, - the mutually ready DKG roster `H`, and decryption threshold `T`. Do not supply arbitrary threshold - values. -- **Timing windows**: Set `inputWindow[0]` after the worst-case VRF, ticket, and DKG setup time. Set - `inputWindow[1]` late enough for inputs and data-availability finalization. Include a transaction - buffer. -- **Custom Params**: Use `customParams` in the E3 request to tag jurisdiction, use case, or feature - toggles without redeploying contracts. - -## Input Validation - -- **Push logic to validators**: Implement `IE3Program.publishInput` so ZK proofs and policy checks - happen before the Secure Process. This mirrors how CoFHE tutorials offload range proofs from the - heavy computation path. -- **Replay protection**: Include round identifiers or nullifiers inside inputs before you hash them - into the Lean IMT. -- **Access control**: Gate inputs via registries (e.g., DAO membership) or staking requirements to - keep spam out of the queue. - -## Secure Process & Compute Providers - -- **Determinism**: Reconstruct the on-chain Merkle root inside the Secure Process, then assert - equality prior to any heavy computation. -- **Proof hooks**: When using RISC Zero or other verifiable CPs, keep witness blobs small and - reserve `computeProviderParams` for runtime toggles (precision, chunk size, etc.). -- **Failover**: Decide how your app reacts if `CiphertextOutputPublished` never arrives—e.g., allow - a guardian actor to cancel the round and refund deposits. See the - [Requestor Guide](/requestor-guide) for the full failure → refund map and the `markE3Failed` / - `claimRequesterRefund` flow. - -## Key Material & Ciphernodes - -- **Key hygiene**: `dev:setup` wipes `.interfold` directories for you; mimic that behavior in - production by rotating committee keys when nodes churn. -- **Monitoring**: Subscribe to `E3Activated` and `PlaintextOutputPublished` events to ensure the - CiCo publishes keys and decryptions on schedule. -- **Operator docs**: Keep the [Ciphernode Operators](/ciphernode-operators) runbooks close to your - app documentation so node providers can mirror your expectations. - -## Observability & Tooling - -- **Structured logs**: Adopt the logging schema from the CRISP server (JSON with `e3_id` and phase) - so you can stitch together end-to-end traces. -- **Synthetic tests**: Reuse `pnpm dev:up` + Playwright to run rehearsal rounds on CI before - shipping new circuits or SDK releases. -- **Dashboarding**: Track request fees, committee saturation, and mean time from `request` to - `PlaintextOutputPublished` just like Ritual exposes node specialization metrics. - -## Deployment Checklist - -1. Run `pnpm dev:build` to preflight the entire stack. -2. Execute integration tests that cover `request → publishInput → publishCiphertextOutput` with mock - proofs. -3. Verify wallet funding for any automation (interfold CLI, Hardhat deployers). -4. Update onboarding docs (MetaMask, scripts) when RPC URLs or ports change. -5. Publish a postmortem template so incidents have consistent follow-up. - -Pair these practices with the [Use Cases](/use-cases) catalog to craft predictable launch plans. +import { Callout } from 'nextra/components' +import { LinkCards, LinkCard } from '@/components/ui' + +# Best Practices + +This page lists design and operations practices for E3 programs and the applications around them. It +is for developers who design an E3 program, request E3s, or run a coordination server. Each practice +names the mechanism in the code that makes it necessary. At the end, you have a deployment +checklist. + +## Request parameters + +- **Choose a supported committee size.** The committee enum fixes N, H, and T in + `lib/ActiveCryptoConfig.sol`. On mainnet, the launch pricing sets `minCommitteeSize` to 19, so + only `Small` is accepted. `getE3Quote` reverts with `CommitteeSizeTooSmall` for a smaller size. +- **Use the secure parameter set for private data.** Mainnet accepts only `paramSet` 1 + (secure-8192). The insecure-512 preset has no production security. +- **Match the SDK preset to the parameter set.** The SDK does not compare them. A mismatch encrypts + inputs under the wrong parameters. +- **Place the input window after the worst-case key time.** The worst-case key time is the request + time plus `randomnessRequestTimeout`, `sortitionSubmissionWindow`, and `dkgWindow`. The committee + cannot publish its key after `inputWindow[1]` (`InputWindowClosedBeforeKeyPublication`). A window + that starts earlier can lose input time while DKG runs. In Avail mode, `CRISPProgram.validate` + refuses a voting start before the worst-case key time. +- **Add a start buffer.** `inputWindow[0]` must not be earlier than the block time when the request + executes. `calculateInputWindow` adds 15 seconds by default. +- **Keep the input window no longer than necessary.** The quote charges node availability from the + request to the end of the input window. The worst-case lifecycle must also stay inside + `maxDuration` (`InvalidDuration`). +- **Put per-E3 settings in `customParams`.** Interfold stores the bytes on the E3 and passes them to + `e3Program.validate`. `CRISPProgram` decodes its round settings from `customParams`. + +## Fees + +- **Quote right before the request.** For the same parameters and pricing, a later block gives the + same quote or a lower quote. An approval of the latest quote is enough. +- **Approve only the quoted amount.** `request` pulls exactly the quoted fee with `transferFrom`. A + larger approval stays open for later calls. +- **Set `maxFee`, `expectedFeeToken`, and `expectedCryptoConfigId`.** These fields make the request + revert when governance changes the fee, the fee token, or the circuit configuration after your + quote. `requestE3` in the SDK sets the last two for you. +- **Check `requestsPaused()` before you offer a request.** A new deployment starts paused, and + governance can pause requests again. + +## Program contract + +- **Advertise both interfaces.** `registerE3Program` probes `IE3Program` and + `IE3ProgramDataAvailability` through ERC-165. It rejects a program that does not advertise both + (`E3ProgramInterfaceMissing`). +- **Check the request in `validate`.** Interfold stores the E3 record before it calls `validate`, so + the program can read the input window and the frozen timeouts. A revert in `validate` cancels the + request before the fee transfer. +- **Gate `publishInput` yourself.** Interfold does not check inputs. The template program checks + only `inputWindow[1]`. `CRISPProgram` also checks a ballot proof, a commitment deadline, and a + signed availability attestation. +- **Bind each ciphertext to its commitment.** The template program inserts the commitment into its + input tree without a proof of the binding. A comment in `MyProgram.sol` states that production + programs must verify it, because an invalid input can stop the E3 from completing. +- **Check the input root in `verify`.** The template `verify` requires the compute proof to use the + stored parameter hash and the current root of the input tree (`InvalidComputeContext`). A program + without this check cannot prove which inputs the computation used. +- **Verify the arguments, not Interfold storage.** Interfold calls `verify` before it stores the + output. `IE3Program` states that `verify` must check the supplied hash, commitment, and proof. +- **Replace the mock data availability verifier.** The template `verifyDataAvailability` accepts any + bytes that hash to the content hash. It is for the local stack only. + +## Output and liveness + +- **Publish the ciphertext output inside its window.** Interfold accepts it only after + `inputWindow[1]` and not after the compute deadline. Read the deadline with `getDeadlines(e3Id)`. +- **Treat the compute deadline as a hard limit.** A missed compute deadline fails the E3 with + `ComputeTimeout`. The requester pays for that failure: 50% of the service escrow goes to the + honest members and 5% to the treasury. +- **Make publication safe to repeat.** A second publication reverts with + `CiphertextOutputAlreadyPublished`. Check the stage before you retry after a crash. +- **Store job state on disk.** The template server keeps its job state in memory and watches only + new key events. After a restart, it does not schedule an E3 whose key was published earlier. +- **Publish the ciphertext before you publish the reference.** The ciphernodes fetch the bytes from + the data availability layer with the content hash. On the local stack, they read + `GET /availability/objects/:contentHash` on the template server. + +## Clients and events + +- **Check the committee key against its commitment.** The key chunks are transport data. Use + `CommitteePublicKeyAssembler` and `validatePublicKeyCommitment`, as the template client and server + do. +- **Listen to the events that the contracts emit.** Use `CIPHERTEXT_OUTPUT_REFERENCE_PUBLISHED`, not + `CIPHERTEXT_OUTPUT_PUBLISHED`. Use `COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED`, not + `COMMITTEE_PUBLISHED`. The current contracts do not emit `CiphertextOutputPublished` or + `CommitteePublished`. +- **Filter every event by E3 ID.** The SDK watches all events of a type on the contract. The + template handlers compare `event.data.e3Id` with the current E3. +- **Read history with a start block.** `getHistoricalEvents` reads in ranges of 10,000 blocks when + you give `fromBlock`. Start at the contract deployment block, not at block 0. +- **Wait for confirmations before an irreversible action.** A reorganization can remove a log. + Ciphernodes apply logs after `ingestion_confirmations` blocks (default 1). The CRISP availability + service reads finalized blocks before it retires a job. + +## Failures and refunds + +- **Know who can end an overdue E3.** Ciphernodes submit `markE3Failed` and `processE3Failure`. + During `markFailedGracePeriod` (1 hour in the mainnet configuration), only the requester, the + owner, or an active committee member can call `markE3Failed`. After that, any account can. +- **Expect a delay before a refund.** For an E3 with a finalized committee, settlement opens one day + after `getE3LifecycleDeadline(e3Id)`. Until then, `processE3Failure` reverts with + `SettlementBlocked`. +- **Request from an account that can claim.** Only the requester address can call + `claimRequesterRefund`. A smart contract wallet must be able to make this call. + +For the full refund rules, see [Request an E3](/build/requesting-an-e3#when-an-e3-fails). + +## Testing + +- **Run a full E3 in CI.** `pnpm test:integration` in the template runs the server tests, then an E3 + from the request to the plaintext output (`tests/integration.spec.ts`). It needs an `interfold` + binary built with the `test-only-skip-proof-aggregation` feature. `pnpm dev:setup` builds it in + the monorepo. +- **Test the runner without a request.** With `TEST_MODE=1`, `GET /test` on the server sends + `server/payload.json` to the program runner. `pnpm dev:all` sets `TEST_MODE=1`. +- **Test the contracts.** `pnpm test` in the template runs the Hardhat tests. + +## Deployment checklist + +1. Deploy the E3 program. Ask the Interfold owner to register it with `registerE3Program`. +2. Replace the mock `verifyDataAvailability` with a real data availability verifier. +3. Set the chain, the RPC URL, and the preset in the server and the client. +4. Fund the server key with gas for `publishCiphertextOutput`. +5. Fund the requester with the fee token. Read the token from `feeToken()`. +6. Confirm that `getE3Quote` accepts your committee size and parameter set on the target chain. +7. Run `pnpm test:integration` against the new build. + + + On mainnet, the Interfold owner is the governance DAO. A program registration is a governance + action. + + +## Next steps + + + + Fees, deadlines, failures, and refunds from the requester side. + + + Write `validate`, `publishInput`, and `verify` for your program. + + + The template client and server that these practices refer to. + + diff --git a/docs/pages/build/client-and-server.mdx b/docs/pages/build/client-and-server.mdx index 28217ae127..5c41506039 100644 --- a/docs/pages/build/client-and-server.mdx +++ b/docs/pages/build/client-and-server.mdx @@ -1,362 +1,330 @@ --- -title: 'Setting Up the Client and Server' -description: 'Build client applications and coordination servers using the Interfold SDK' +title: 'Client and Server' +description: + 'How the default template client and coordination server work: configuration, event listening, the + coordination work of the server, and error handling.' --- -# Setting Up the Client and Server +import { Callout, FileTree } from 'nextra/components' +import { LinkCards, LinkCard } from '@/components/ui' -Building E3 applications involves creating client-side interfaces for users and coordination logic -for managing the E3 lifecycle. The Interfold SDK provides powerful TypeScript and React tools to -streamline development. +# Client and Server -## Overview - -Modern E3 applications typically consist of: - -1. **Client Application**: User interface with wallet integration and FHE encryption -2. **Coordination Logic**: Server-side or client-side coordination of E3 processes -3. **Event Management**: Real-time monitoring of E3 lifecycle events -4. **Contract Interactions**: Type-safe smart contract communication - -The Interfold SDK handles much of this complexity for you, providing: - -- Type-safe contract interactions -- Real-time event listening -- React hooks for easy frontend integration -- Comprehensive error handling - -## Setting Up a Client Application - -### Install the SDK - -For TypeScript/JavaScript applications, install the TypeScript SDK: - -```bash -pnpm add @interfold/sdk -``` - -For React applications add the hook helpers as well: - -```bash -pnpm add @interfold/sdk @interfold/react -``` - -### Basic TypeScript Client - -Create a type-safe client that can interact with Interfold contracts: - -```typescript -import { CommitteeSize, InterfoldSDK, InterfoldEventType } from '@interfold/sdk' -import { createPublicClient, createWalletClient, http, custom } from 'viem' -import { hardhat } from 'viem/chains' - -// Initialize clients -const publicClient = createPublicClient({ - chain: hardhat, - transport: http('http://localhost:8545'), -}) - -const walletClient = createWalletClient({ - transport: custom(window.ethereum), -}) - -// Create SDK instance (no separate initialize() call needed) -const sdk = new InterfoldSDK({ - publicClient, - walletClient, - contracts: { - interfold: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', - ciphernodeRegistry: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9', - feeToken: '0x5FbDB2315678afecb367f032d93F642f64180aa3', - }, -}) - -const now = BigInt(Math.floor(Date.now() / 1000)) -const requestParams = { - committeeSize: CommitteeSize.Minimum, - inputWindow: [now, now + 300n] as const, - e3Program: '0x9A676e781A523b5d0C0e43731313A708CB607508', - paramSet: 0, - computeProviderParams: '0x', -} -const quote = await sdk.getE3Quote(requestParams) -const approvalHash = await sdk.approveFeeToken(quote) -await sdk.waitForTransaction(approvalHash) -const hash = await sdk.requestE3({ ...requestParams, maxFee: quote }) - -console.log('E3 requested with hash:', hash) -``` - -### React Application with Hooks - -For React applications, use the `useInterfoldSDK` hook for seamless integration: - -```typescript -import React, { useEffect, useState } from 'react'; -import { useInterfoldSDK } from '@interfold/react'; -import { CommitteeSize } from '@interfold/sdk'; - -function E3Dashboard() { - const [e3Data, setE3Data] = useState(null); - - const { - sdk, - isInitialized, - error, - requestE3, - onInterfoldEvent, - off, - InterfoldEventType, - RegistryEventType - } = useInterfoldSDK({ - autoConnect: true, - contracts: { - interfold: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', - ciphernodeRegistry: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9', - feeToken: '0x5FbDB2315678afecb367f032d93F642f64180aa3', - }, - }); - - // Listen to E3 events - useEffect(() => { - if (!isInitialized) return; - - const handleE3Requested = (event) => { - console.log('New E3 request:', event.data); - setE3Data(event.data); - }; - - const handleCommitteePublished = (event) => { - console.log('Committee published:', event.data); - }; - - onInterfoldEvent(InterfoldEventType.E3_REQUESTED, handleE3Requested); - onInterfoldEvent(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublished); - - return () => { - off(InterfoldEventType.E3_REQUESTED, handleE3Requested); - off(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublished); - }; - }, [isInitialized, onInterfoldEvent, off, InterfoldEventType, RegistryEventType]); - - const handleRequestE3 = async () => { - try { - if (!sdk) return; - const now = BigInt(Math.floor(Date.now() / 1000)); - const requestParams = { - committeeSize: CommitteeSize.Minimum, - inputWindow: [now, now + 300n] as const, - e3Program: '0x9A676e781A523b5d0C0e43731313A708CB607508', - paramSet: 0, - computeProviderParams: '0x', - }; - const quote = await sdk.getE3Quote(requestParams); - const approvalHash = await sdk.approveFeeToken(quote); - await sdk.waitForTransaction(approvalHash); - const hash = await requestE3({ ...requestParams, maxFee: quote }); - console.log('E3 requested:', hash); - } catch (error) { - console.error('Failed to request E3:', error); - } - }; - - if (error) { - return
Error: {error}
; - } - - if (!isInitialized) { - return
Connecting to Interfold...
; - } - - return ( -
-

E3 Dashboard

- - {e3Data && ( -
-

Latest E3 Request

-
{JSON.stringify(e3Data, null, 2)}
-
- )} -
- ); -} - -export default E3Dashboard; -``` - -## Event-Driven Architecture +This page explains the off-chain parts of the default template (`templates/default`). It is for +developers who change the template or build their own client and server. It covers the files, the +configuration, the events that each part listens to, the coordination work of the server, and the +error handling. At the end of the page, you know which file to change for each part of the flow. -The SDK provides comprehensive event handling for the entire E3 lifecycle: - -### Interfold Events - -```typescript -// Listen to key E3 lifecycle events -sdk.onInterfoldEvent(InterfoldEventType.E3_REQUESTED, (event) => { - console.log('New E3 computation requested:', event.data) -}) - -sdk.onInterfoldEvent(InterfoldEventType.CIPHERTEXT_OUTPUT_PUBLISHED, (event) => { - console.log('Encrypted computation result available:', event.data) -}) - -sdk.onInterfoldEvent(InterfoldEventType.PLAINTEXT_OUTPUT_PUBLISHED, (event) => { - console.log('Decrypted plaintext output available:', event.data) -}) -``` - -### Registry Events - -```typescript -import { RegistryEventType } from '@interfold/sdk' - -// Monitor committee lifecycle events -sdk.onInterfoldEvent(RegistryEventType.COMMITTEE_REQUESTED, (event) => { - console.log('Committee requested for E3:', event.data) -}) - -sdk.onInterfoldEvent(RegistryEventType.COMMITTEE_PUBLISHED, (event) => { - console.log('Committee selection complete:', event.data) -}) - -sdk.onInterfoldEvent(RegistryEventType.COMMITTEE_FINALIZED, (event) => { - console.log('Committee finalized:', event.data) -}) -``` - -## Server-Side Coordination - -For server-side applications, you can create automated coordination services: - -```typescript -import { InterfoldSDK, InterfoldEventType } from '@interfold/sdk' -import { hardhat } from 'viem/chains' - -class E3CoordinationServer { - private sdk: InterfoldSDK - - constructor(privateKey: string, rpcUrl: string) { - // Use the static factory for convenience - this.sdk = InterfoldSDK.create({ - rpcUrl, - privateKey: privateKey as `0x${string}`, - chain: hardhat, - contracts: { - interfold: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', - ciphernodeRegistry: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9', - feeToken: '0x5FbDB2315678afecb367f032d93F642f64180aa3', - }, - }) - } - - async start() { - this.setupEventListeners() - console.log('E3 Coordination Server started') - } - - private setupEventListeners() { - // Monitor new E3 requests - this.sdk.onInterfoldEvent(InterfoldEventType.E3_REQUESTED, async (event) => { - const { e3Id } = event.data - console.log(`New E3 requested: ${e3Id}`) - }) - - // Monitor ciphertext output publication - this.sdk.onInterfoldEvent(InterfoldEventType.CIPHERTEXT_OUTPUT_PUBLISHED, async (event) => { - console.log('Ciphertext output published:', event.data) - }) - - // Monitor plaintext output publication - this.sdk.onInterfoldEvent(InterfoldEventType.PLAINTEXT_OUTPUT_PUBLISHED, async (event) => { - console.log('Plaintext output published:', event.data) - }) - } - - async stop() { - this.sdk.cleanup() - console.log('E3 Coordination Server stopped') - } -} - -// Usage -const server = new E3CoordinationServer(process.env.PRIVATE_KEY!, process.env.RPC_URL!) - -server.start() -``` +## Overview -## Error Handling - -The SDK includes comprehensive error handling: - -```typescript -import { SDKError } from '@interfold/sdk' - -try { - await sdk.requestE3(params) -} catch (error) { - if (error instanceof SDKError) { - console.error(`SDK Error (${error.code}): ${error.message}`) - - switch (error.code) { - case 'MISSING_PUBLIC_CLIENT': - // Handle missing client - break - case 'INVALID_ADDRESS': - // Handle invalid contract address - break - case 'TRANSACTION_FAILED': - // Handle transaction failure - break - default: - console.error('Unknown SDK error:', error) - } - } else { - console.error('Unexpected error:', error) - } -} +The template has three off-chain processes and a local chain. `pnpm dev:all` starts all of them +through `scripts/dev_all_concurrently.sh`. + +| Process | Started by | Local address | Role | +| ------------------- | ------------------- | ------------------------ | --------------------------------------------------------------------------- | +| Client | `pnpm dev:frontend` | `http://localhost:3000` | A React wizard. It requests an E3, encrypts two numbers, and shows the sum. | +| Coordination server | `pnpm dev:server` | `http://localhost:8080` | Sends inputs to the program runner and publishes the ciphertext output. | +| Program runner | `pnpm dev:program` | `http://localhost:13151` | Runs the FHE program (`interfold program start`) and calls the server back. | +| Local chain | `anvil` | `http://localhost:8545` | Chain ID 31337 with a 1-second block time. | + +`pnpm dev:all` also starts five local ciphernodes, and it starts the server with `TEST_MODE=1`. + +The client and the server both talk to the chain. The server never receives inputs from the client. +It reads them from chain events. + +```mermaid +sequenceDiagram + participant C as Client + participant I as Interfold and registry + participant P as MyProgram + participant S as Server + participant X as Program runner + participant N as Ciphernodes + C->>I: request(params) + N->>I: committee key chunks + C->>P: publishInput(e3Id, data) + S->>X: POST /run_compute + X->>S: POST / with the result + S->>I: publishCiphertextOutput + N->>S: GET /availability/objects/:contentHash + N->>I: publishPlaintextOutput + I-->>C: PlaintextOutputPublished ``` -## Configuration Management - -### Development Configuration - -For local development with the default template: - -```typescript -import { hardhat } from 'viem/chains' - -const devSdk = InterfoldSDK.create({ - rpcUrl: 'http://localhost:8545', - chain: hardhat, - contracts: { - interfold: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', - ciphernodeRegistry: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9', - feeToken: '0x5FbDB2315678afecb367f032d93F642f64180aa3', - }, +## Files + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +| File | Content | +| -------------------------------------- | --------------------------------------------------------------------------------------------------- | +| `client/src/main.tsx` | The wagmi, React Query, and ConnectKit providers. In development, the chains are Sepolia and Anvil. | +| `client/src/context/WizardContext.tsx` | Calls `useInterfoldSDK`, holds the wizard state, and saves it. | +| `client/src/pages/steps/*.tsx` | One component for each wizard step. | +| `client/src/utils/env-config.ts` | Reads and checks the `VITE_` environment variables. | +| `client/src/utils/sdk-config.ts` | Builds the `useInterfoldSDK` configuration. | +| `client/src/utils/input.ts` | `publishInput`: encodes an input and calls the E3 program. | +| `client/src/utils/persistence.ts` | Saves the wizard state in `localStorage` under `interfold-wizard-state`. | +| `server/index.ts` | The Express server: event listener, scheduler, webhook, and object store. | +| `server/runner.ts` | `callFheRunner`: posts a job to the program runner. | +| `server/utils.ts` | Reads and checks the server environment variables. | +| `server/testHandler.ts` | Sends `server/payload.json` to the runner when `TEST_MODE` is set. | + +## Configuration + +The contract addresses come from `interfold.config.yaml`. The dev scripts run `interfold print-env` +to turn the `localhost` chain entry into environment variables: + +- `scripts/dev_frontend.sh` runs `interfold print-env --vite --chain localhost` before it starts the + client. +- `scripts/dev_server.sh` runs `interfold print-env --chain localhost`. It also sets `PRIVATE_KEY` + to the first Anvil account key, `CHAIN_ID=31337`, and `RPC_URL=http://localhost:8545`. + +### Client variables + +`client/src/utils/env-config.ts` reads these variables. When one is missing, the client shows the +`EnvironmentError` screen instead of the wizard. + +| Variable | Default | +| --------------------------------------- | ------------------------ | +| `VITE_INTERFOLD_ADDRESS` | None | +| `VITE_REGISTRY_ADDRESS` | None | +| `VITE_BONDING_REGISTRY_ADDRESS` | None | +| `VITE_E3_PROGRAM_ADDRESS` | None | +| `VITE_FEE_TOKEN_ADDRESS` | None | +| `VITE_RPC_URL` | `http://localhost:8545` | +| `VITE_THRESHOLD_BFV_PARAMS_PRESET_NAME` | `INSECURE_THRESHOLD_512` | + +`client/src/main.tsx` also reads `VITE_WALLETCONNECT_PROJECT_ID` for ConnectKit. + +### Server variables + +`server/utils.ts` requires the first group. `server/index.ts` reads the others. + +| Variable | Required | Default | Use | +| ----------------------------- | -------- | ------------------------------ | ---------------------------------------------- | +| `RPC_URL` | Yes | None | The RPC URL for the SDK. | +| `INTERFOLD_ADDRESS` | Yes | None | The Interfold contract. | +| `REGISTRY_ADDRESS` | Yes | None | The `CiphernodeRegistry`. | +| `E3_PROGRAM_ADDRESS` | Yes | None | `MyProgram`, for the input events. | +| `FEE_TOKEN_ADDRESS` | Yes | None | The fee token. | +| `PRIVATE_KEY` | Yes | None | The key that signs `publishCiphertextOutput`. | +| `CHAIN_ID` | Yes | None | The chain ID. | +| `PROGRAM_RUNNER_URL` | No | `http://127.0.0.1:13151` | The program runner. | +| `CALLBACK_URL` | No | `http://127.0.0.1:8080` | The URL that the runner calls with the result. | +| `PORT` | No | `8080` | The server port. | +| `DATA_AVAILABILITY_DIRECTORY` | No | `.interfold/data-availability` | Where the server stores ciphertext objects. | +| `TEST_MODE` | No | Not set | Enables `GET /test`. | + +### Data availability + +The local chain entry in `interfold.config.yaml` sets `data_availability` to `mode: mock_http` with +`rpc_url: http://127.0.0.1:8080/availability`. The ciphernodes read ciphertext objects from the +server at that URL. A production chain uses `mode: avail` instead. + +## How the client works + +The client is a five-step wizard. `WizardContext.tsx` creates the SDK with `useInterfoldSDK` and a +memoized configuration from `sdk-config.ts`. The wizard advances by itself when the wallet connects +and when the SDK is ready. + +| Step | Component | What it does | +| ---- | ------------------------ | ------------------------------------------------------------------------------------------------- | +| 1 | `ConnectWallet.tsx` | Asks for a wallet connection. The ConnectKit button is in the navigation bar. | +| 2 | `RequestComputation.tsx` | Quotes the fee, approves it, and calls `requestE3`. Then it waits for the committee public key. | +| 3 | `EnterInputs.tsx` | Encrypts two numbers, computes their commitments, and sends each one to `MyProgram.publishInput`. | +| 4 | `EncryptSubmit.tsx` | Shows progress and waits for the plaintext output. | +| 5 | `Results.tsx` | Shows the sum, the E3 ID, the transaction hash, and the raw output. | + +### Events that the client listens to + +| Event type | File | Action | +| -------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- | +| `InterfoldEventType.E3_REQUESTED` | `RequestComputation.tsx` | Saves `e3Id` and `inputWindow[1]`. | +| `RegistryEventType.COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED` | `RequestComputation.tsx` | Adds the chunk to a `CommitteePublicKeyAssembler`. It keeps the key only when `validatePublicKeyCommitment` returns `true`. | +| `InterfoldEventType.CIPHERTEXT_OUTPUT_PUBLISHED` | `EncryptSubmit.tsx` | Marks the computation step as done. | +| `InterfoldEventType.PLAINTEXT_OUTPUT_PUBLISHED` | `EncryptSubmit.tsx` | Decodes the result with `decodePlaintextOutput` and goes to the results step. | + + + The current Interfold contract emits `CiphertextOutputReferencePublished` for a new ciphertext + output. It does not emit `CiphertextOutputPublished`, so the computation step in the template + progress list stays open until the plaintext arrives. The SDK key for the emitted event is + `InterfoldEventType.CIPHERTEXT_OUTPUT_REFERENCE_PUBLISHED`. + + +### Input submission + +`client/src/utils/input.ts` encodes the ciphertext and its commitment as `(bytes, bytes32)` and +calls `publishInput` on the E3 program: + +```ts +const data = encodeAbiParameters(parseAbiParameters('bytes, bytes32'), [ + input, + ciphertextCommitment, +]) + +return walletClient.writeContract({ + address: programAddress, + abi: myProgramAbi, + functionName: 'publishInput', + args: [e3Id, data], + chain: walletClient.chain, + account: sender, }) ``` -### Production Configuration - -For production deployments: - -```typescript -import { mainnet } from 'viem/chains' - -const prodSdk = InterfoldSDK.create({ - rpcUrl: process.env.RPC_URL!, - privateKey: process.env.PRIVATE_KEY! as `0x${string}`, - chain: mainnet, - contracts: { - interfold: process.env.INTERFOLD_CONTRACT_ADDRESS! as `0x${string}`, - ciphernodeRegistry: process.env.REGISTRY_CONTRACT_ADDRESS! as `0x${string}`, - feeToken: process.env.FEE_TOKEN_ADDRESS! as `0x${string}`, - }, +`EnterInputs.tsx` blocks submission after `inputWindow[1]`, because `MyProgram.publishInput` then +reverts with `InputDeadlineReached`. For the full flow, see +[Encrypt and submit](/build/tutorials/encrypt-and-submit). + +## How the server works + +The server in `server/index.ts` coordinates the compute step. It keeps only two in-memory sets for +idempotency: `scheduled` and `inFlight`. It reads the parameters and the inputs from the chain when +it runs a job. + +### Server steps + +1. **Create the SDK.** `createPrivateSDK` calls `InterfoldSDK.create` with `RPC_URL`, `PRIVATE_KEY`, + the `hardhat` chain, and the `INSECURE_THRESHOLD_512` preset. It creates the SDK one time. +2. **Listen for the key.** `setupEventListeners` watches + `RegistryEventType.COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED`. When a key is complete and matches its + commitment, the server calls `scheduleE3(e3Id)`. +3. **Wait for the input window.** `scheduleE3` reads `inputWindow[1]` from `getE3`. It polls the + latest block time every 500 ms until the window closes. If the window is already closed, it runs + at once. +4. **Collect the inputs.** `runProgram` reads the encoded parameters from + `Interfold.paramSetRegistry(e3.paramSet)`. It reads every `InputPublished` event of `MyProgram` + for the E3, from block 0. It skips the E3 when there is one input or none. +5. **Start the compute.** `callFheRunner` in `server/runner.ts` posts to + `PROGRAM_RUNNER_URL/run_compute`. The body has the E3 ID, the chain and contract domain, the + parameters, the ciphertext inputs, and `callback_url`. The domain is the chain ID, the Interfold + address, the encryption scheme ID, and the committee public key hash. +6. **Publish the output.** The runner calls `POST /` with `e3_id`, `ciphertext`, + `ciphertext_commitment`, and `proof`. The server stores the ciphertext under its Keccak-256 hash. + Then it calls `sdk.publishCiphertextOutput`. + +The webhook publishes the output with this reference: + +```ts +const ciphertextBytes = Buffer.from(ciphertext.slice(2), 'hex') +const contentHash = keccak256(ciphertext) +await storeAvailabilityObject(contentHash, ciphertextBytes) + +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 Interfold SDK abstracts away much of the complexity, allowing you to focus on your application's -unique business logic while ensuring robust, type-safe interaction with the Interfold protocol. +### Server endpoints + +| Endpoint | Content | +| ---------------------------------------- | --------------------------------------------------------------------------------- | +| `POST /` | The program runner callback. It publishes the ciphertext output. | +| `GET /availability/objects/:contentHash` | Returns a stored object as `application/octet-stream`. The ciphernodes read here. | +| `GET /test` | Only with `TEST_MODE`. Sends `server/payload.json` to the program runner. | + +## Error handling + +The template handles errors in these places: + +- **Webhook input.** `POST /` returns HTTP 400 when a field is missing or a value is not a hex + string. It returns HTTP 500 when the publication fails. +- **Runner failure.** `runProgram` removes the E3 from `inFlight` when `callFheRunner` throws. A + later call can then try again. `callFheRunner` throws when the runner returns a status that is not + OK. +- **Key chunks.** The server and the client log a chunk that they cannot use, then continue. They + reject a key whose commitment does not match. +- **Client state.** `WizardRoutes.tsx` shows `EnvironmentError` for missing variables and + `SDKErrorDisplay` when the SDK hook reports an error. `error-formatting.ts` maps common wallet and + revert messages to short text. +- **Input window.** `EnterInputs.tsx` compares the local time with `inputWindow[1]` and blocks a + late submission. `EncryptSubmit.tsx` shows a warning when the window closed before the inputs were + sent. + +For SDK error codes, see [SDK](/build/sdk#errors). + +## Before you leave local development + +The template server is for a local chain. Change these points before you use it on another network: + +- `server/index.ts` sets the `hardhat` chain and the `INSECURE_THRESHOLD_512` preset. Set the target + chain and the preset that matches your `paramSet`. +- `MyProgram.verifyDataAvailability` accepts the raw bytes as the receipt. Replace it with a real + data availability verifier, and publish the ciphertext to that layer. +- `runProgram` reads input events from block 0. Start from the program deployment block. +- The `scheduled` and `inFlight` sets are in memory. A restart loses them. Store the job state if + the server must recover after a restart. +- The server must publish the output before the compute deadline. A missed deadline fails the E3 + with `ComputeTimeout`, and the requester pays for completed work. + +## Next steps + + + + Encrypt an input and send it to the E3 program step by step. + + + Every SDK export, event type, and error code. + + + How the program runner and the compute proof work. + + + Move the template from the local chain to a testnet. + + diff --git a/docs/pages/build/interfold-contract.mdx b/docs/pages/build/interfold-contract.mdx index 86858213bb..6f9b1e8688 100644 --- a/docs/pages/build/interfold-contract.mdx +++ b/docs/pages/build/interfold-contract.mdx @@ -1,350 +1,374 @@ -# Building with the Interfold - -The Interfold smart contract acts as the central coordinator for all E3 operations. It manages -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 - -### Core Responsibilities - -- Manage E3 computation requests -- Coordinate Ciphernode Committees -- 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 +--- +title: 'The Interfold Contract' +description: + 'The public API of Interfold.sol for application builders: request an E3, publish outputs, read E3 + state, and handle events and custom errors.' +--- + +import { Callout } from 'nextra/components' +import { Flow, LinkCards, LinkCard } from '@/components/ui' + +# The Interfold Contract + +This page describes the public API of the Interfold contract (`Interfold.sol`) for application +builders. It covers the request function and its parameters, where inputs go, the two output +functions, the read functions, the events, and the custom errors. At the end of the page, you can +call the contract directly or read the calls that the SDK makes for you. For the stages and their +deadlines in order, see [E3 lifecycle](/learn/e3-lifecycle). + +## What the contract does + +The Interfold contract records each E3 and moves it through its stages. It does these tasks: + +- Accepts E3 requests and takes the fee. +- Asks the `CiphernodeRegistry` to select a committee. +- Records the committee public key commitment when the registry reports it. +- Accepts the ciphertext output after the E3 program and the protocol verifier accept the proof. +- Accepts the plaintext output after the decryption verifier accepts the proof. +- Pays rewards on success and sends failed E3s to refund settlement. + +The Interfold contract does not accept inputs. Each E3 program accepts its own inputs in its +`publishInput` function. See [Input publication](#input-publication). + + + +## Request an E3 + +The request function takes one struct and returns the new E3 ID and the stored `E3` record. ```solidity -contract Interfold { - // Address of the Ciphernode registry. - ICiphernodeRegistry public ciphernodeRegistry; - - // Address of the bonding registry. - IBondingRegistry public bondingRegistry; - - // Address of the slashing manager. - ISlashingManager public slashingManager; - - // Address of the refund manager. - IE3RefundManager public e3RefundManager; - - // Fee token used for E3 payments. - IERC20 public feeToken; - - // Mapping of allowed E3 Programs. - mapping(IE3Program e3Program => bool allowed) public e3Programs; - - // Mapping of E3s. - mapping(uint256 e3Id => E3 e3) public e3s; - - // Mapping of enabled encryption schemes. - mapping(bytes32 encryptionSchemeId => IDecryptionVerifier decryptionVerifier) - public decryptionVerifiers; - - // Mapping of E3 payment amounts. - mapping(uint256 e3Id => uint256 amount) public e3Payments; - - // Maximum allowed worst-case request-to-decryption duration. - uint256 public maxDuration; - - // Auto-incrementing E3 identifier. - uint256 public nexte3Id; -} +function request( + E3RequestParams calldata requestParams +) external returns (uint256 e3Id, E3 memory e3); ``` -### E3 Struct - -Each computation is tracked by an `E3` struct: +`request` pulls the fee with `transferFrom`. Before you call it, approve the Interfold contract to +spend the quoted fee in the fee token. For the fee formula and the refund rules, see +[Request an E3](/build/requesting-an-e3). + +### Request parameters + +`E3RequestParams` is defined in `interfaces/IInterfold.sol`. + +| Field | Type | Meaning | Rule | +| ------------------------ | ------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `committeeSize` | `CommitteeSize` (`uint8`) | `Minimum` (0), `Micro` (1), or `Small` (2). | The size must have thresholds configured. The pricing minimums must accept it. | +| `inputWindow` | `uint256[2]` | Start and end timestamps of the input window, in seconds. | The start must not be in the past. The end must not be before the start. | +| `e3Program` | `IE3Program` | The E3 program contract. | The contract owner must register the program first. | +| `paramSet` | `uint8` | BFV parameter set. `0` is insecure-512, `1` is secure-8192. | The chain must support the set, and the set must be registered. | +| `computeProviderParams` | `bytes` | Compute provider settings. | Interfold passes the bytes to `e3Program.validate`. | +| `customParams` | `bytes` | Application settings. | Interfold stores the bytes on the E3 and passes them to `e3Program.validate`. | +| `expectedFeeToken` | `IERC20` | The fee token that the requester accepts. | Must equal `feeToken()`. Otherwise the call reverts with `FeeTokenChanged`. | +| `expectedCryptoConfigId` | `bytes32` | The circuit configuration that the requester accepts. | Must equal the ID for `paramSet`. Otherwise the call reverts with `CryptoConfigChanged`. | +| `maxFee` | `uint256` | The largest total fee that the requester accepts. | The quote must not be more than `maxFee`. Otherwise the call reverts. | + +The configuration IDs for each parameter set are constants in `lib/ActiveCryptoConfig.sol` +(`INSECURE_CONFIG_ID` and `SECURE_CONFIG_ID`). The SDK fills `expectedFeeToken`, +`expectedCryptoConfigId`, and `maxFee` for you. See [SDK](/build/sdk#request-an-e3). + +### What `request` checks + +`request` runs these checks in order. The first check that fails reverts the call. + +1. Requests are not paused (`RequestsPaused`). A new deployment starts with requests paused. +2. The registry, bonding registry, slashing manager, refund manager, and node release registry form + one consistent set (`DependencyConfigurationMismatch`). +3. The current fee token is on the allow-list (`FeeTokenNotAllowed`). +4. The E3 program is registered (`E3ProgramNotAllowed`). +5. The quote succeeds. The quote checks the input window, the total duration, the parameter set, and + the committee size. +6. The current fee token, the configuration ID for `paramSet`, and the quote agree with + `expectedFeeToken`, `expectedCryptoConfigId`, and `maxFee`. +7. The E3 program `validate` call returns an encryption scheme ID. Interfold must have a decryption + verifier and a public key verifier for that scheme (`InvalidEncryptionScheme`). +8. The fee transfer delivers the exact amount (`AssetTransferMismatch`). +9. The registry accepts the committee request. The registry reverts when fewer than N eligible + ciphernodes (`InsufficientCiphernodes`) or fewer than N bond owners (`InsufficientBondOwners`) + are available. + +The total duration check uses the worst case. Interfold takes the later of the input window end and +the committee setup time, then adds the compute window and the decryption window. The committee +setup time is the randomness timeout, plus the sortition submission window, plus the DKG window. The +total must not be more than `maxDuration` (`InvalidDuration`). + +### What `request` stores and emits + +A successful request does these things: + +- Assigns the next E3 ID. The first ID is the Interfold address shifted left by 96 bits, so E3 IDs + are large numbers. +- Freezes the registry, bonding registry, refund manager, slashing manager, and timeout + configuration for this E3. +- Stores the service fee as escrow. It credits the flat randomness fee to the treasury. +- Sets the stage to `Requested`. +- Emits `E3Requested(e3Id, e3, cryptoConfigId)` and `E3StageChanged(e3Id, None, Requested)`. + +### The E3 record + +`getE3(e3Id)` returns the `E3` struct from `interfaces/IE3.sol`. + +| Field | Type | Content | +| ---------------------- | --------------------- | ----------------------------------------------------------------------- | +| `seed` | `uint256` | Seed for the computation. Committee selection uses a separate VRF seed. | +| `committeeSize` | `CommitteeSize` | The requested committee size. | +| `requestBlock` | `uint256` | The request timestamp. The name is kept for ABI compatibility. | +| `inputWindow` | `uint256[2]` | The requested input window. | +| `encryptionSchemeId` | `bytes32` | The scheme that `e3Program.validate` returned. | +| `e3Program` | `IE3Program` | The E3 program address. | +| `paramSet` | `uint8` | The BFV parameter set. | +| `customParams` | `bytes` | The application settings from the request. | +| `decryptionVerifier` | `IDecryptionVerifier` | The verifier for the decryption proof. | +| `pkVerifier` | `IPkVerifier` | The verifier for the DKG public key proof. | +| `committeePublicKey` | `bytes32` | The committee public key commitment. It is not the full key. | +| `ciphertextOutput` | `bytes32` | Keccak-256 of the serialized ciphertext output. | +| `plaintextOutput` | `bytes` | The decrypted result. | +| `requester` | `address` | The account that called `request`. | +| `ciphertextCommitment` | `bytes32` | The SAFE commitment to the ciphertext that the decryption proof uses. | + +## Input publication + +Inputs go to the E3 program. `IE3Program` defines the entry point: ```solidity -struct E3 { - uint256 seed; // Random seed for the E3 computation - CommitteeSize committeeSize; // Committee size enum (Minimum, Micro, Small) - 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 - uint8 paramSet; // BFV encryption parameter set index - bytes customParams; // Arbitrary application-defined params - IDecryptionVerifier decryptionVerifier; // Verifier for decryption proofs - IPkVerifier pkVerifier; // Verifier for public key publication proofs - bytes32 committeePublicKey; // Hash of committee's shared public key - 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 -} +function publishInput(uint256 e3Id, bytes memory data) external; ``` -### Lifecycle Stages +The E3 program decides the encoding of `data`, the checks, and the storage. Interfold does not check +the input window for inputs. Interfold enforces the window only on the output: it rejects a +ciphertext output before `inputWindow[1]`. The two programs in the repository use different input +paths. -Each E3 progresses through these stages: +| Program | Encoding of `data` | Where the ciphertext bytes go | +| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MyProgram` (`templates/default`) | `abi.encode(bytes ciphertext, bytes32 ciphertextCommitment)` | On-chain, in the `InputPublished(e3Id, data, index)` event. The contract inserts the commitment into a LazyIMT tree. | +| `CRISPProgram` (`examples/CRISP`) | A ballot proof, the voting slot, the ciphertext commitment and hash, the parent index, and a signed availability attestation | To Avail. `publishInput` reserves the tree leaf and emits `InputCommitted`. Later, `finalizeInput` checks the Avail receipt and emits `InputPublished`. | -| 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 | +The template program does not prove that the ciphertext matches its commitment. A comment in +`MyProgram.sol` states that a production program must verify this binding. For the program side, see +[Program contract](/build/e3-program/program-contract). -### View Functions +## Publish the ciphertext output -The Interfold contract exposes these read-only functions for querying E3 state: +After the compute provider runs, a server publishes a reference to the encrypted result. The +ciphertext bytes stay off Ethereum, on a data availability layer. ```solidity -// Fetch the complete E3 struct (reverts if e3Id doesn't exist) -function getE3(uint256 e3Id) external view returns (E3 memory e3); - -// Get current lifecycle stage -function getE3Stage(uint256 e3Id) external view returns (E3Stage stage); - -// Estimate the fee for an E3 request -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 -// committee receives the remainder. Launch uses marginBps = 1000 and -// protocolShareBps = 182, so treasury receives about 1.82% of gross fees, -// which is approximately 20% of the 10% margin. - -// Get the decryption verifier for an encryption scheme -function getDecryptionVerifier( - bytes32 encryptionSchemeId -) external view returns (IDecryptionVerifier); - -// Get failure reason (if E3 has failed) -function getFailureReason(uint256 e3Id) external view returns (FailureReason reason); - -// Get the address that requested the E3 -function getRequester(uint256 e3Id) external view returns (address requester); - -// Get timeline deadlines (dkgDeadline, computeDeadline, decryptionDeadline) -function getDeadlines(uint256 e3Id) external view returns (E3Deadlines memory deadlines); - -// Get global timeout configuration -function getTimeoutConfig() external view returns (E3TimeoutConfig memory config); +function publishCiphertextOutput(uint256 e3Id, bytes calldata encodedOutputReference) external; ``` -## Requesting Computation - -### Request Flow - -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( - E3RequestParams calldata requestParams - ) external returns (uint256 e3Id, E3 memory e3); - ``` - -2. Contract validates the parameters, quotes the service and flat randomness fees, and stores an - `E3` record with a seed for the computation. The registry requests VRF and derives a separate, - request-bound committee seed after a verified response. -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, cryptoConfigId)` is emitted for off-chain watchers. +`encodedOutputReference` is the ABI encoding of one `CiphertextOutputReference` tuple: -## Committee and Ciphernode Management +| Field | Type | Content | +| ---------------------- | --------- | ------------------------------------------------------------- | +| `contentHash` | `bytes32` | Keccak-256 of the exact serialized ciphertext bytes. | +| `ciphertextCommitment` | `bytes32` | The SAFE commitment to the decoded ciphertext. | +| `computeProof` | `bytes` | The compute proof. It binds `contentHash` and the commitment. | +| `availabilityProof` | `bytes` | The data availability receipt for `contentHash`. | -### Committee Selection +Any account can call the function. The call succeeds only when all of these conditions are true: -- An E3 request triggers a deterministic sortition process to select a Ciphernode Committee (CiCo) - from the global pool of available Ciphernodes. -- Selected nodes coordinate to produce a shared public key with the requested threshold. -- The public key is published to the Ciphernode Registry smart contract. -- For bonding, ticket economics, CLI commands, and operational responsibilities, see the - [Ciphernode Operators guide](/ciphernode-operators). +- The E3 is in `KeyPublished`. +- The current time is at or after `inputWindow[1]` (`InputDeadlineNotReached`). +- The current time is at or before the compute deadline (`CommitteeDutiesCompleted`). +- No output exists yet (`CiphertextOutputAlreadyPublished`), and `contentHash` is not zero. +- The committee still has enough active members (`ThresholdNotMet` from the registry). +- `e3Program.verifyDataAvailability(contentHash, availabilityProof)` returns a receipt for the same + `contentHash`. +- The request-time protocol ciphertext verifier and `e3Program.verify` both accept the proof. -## E3 Activation +A receipt for a different hash, or a proof that a verifier rejects, reverts with `InvalidOutput`. +The E3 program can also revert with its own error. On success, the E3 moves to `CiphertextReady` and +the decryption deadline starts. The contract emits +`CiphertextOutputReferencePublished(e3Id, contentHash, ciphertextCommitment, availabilityBlock, availabilityLeafIndex)` +and `E3StageChanged`. -After the committee is selected and finalized via sortition, the selected Ciphernodes perform a -Distributed Key Generation (DKG) to produce a shared public key. When the aggregated public key is -published, the CiphernodeRegistry calls `onCommitteePublished()` on the Interfold contract, which -transitions the E3 to the `KeyPublished` stage. + + `registerE3Program` accepts only a program that implements + `IE3ProgramDataAvailability.verifyDataAvailability` and advertises `IE3Program` and + `IE3ProgramDataAvailability` through ERC-165. Otherwise it reverts with + `E3ProgramInterfaceMissing`. The template version checks only that the proof bytes hash to + `contentHash`. It is for local development. + -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. +## Publish the plaintext output -## Input Publication - -Inputs are published to Program contracts' `publishInput()` function. +The committee decrypts the output, and one ciphernode publishes the result and the proof. ```solidity -function publishInput( - uint256 e3Id, - bytes memory data -) external +function publishPlaintextOutput( + uint256 e3Id, + bytes calldata plaintextOutput, + bytes calldata proof +) external returns (bool success); ``` -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 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 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: - -1. Reconstruct the Merkle tree from the given inputs -2. Verify the resulting root matches on-chain root - -### Input Validation - -As much as possible, you should aim to validate inputs via proofs generated by Data Providers, -rather than in your Secure Process. This pushes computation to the edges and allows you to reduce -the complexity of your FHE computation. - -Your E3 Program must include logic that validates user inputs. For simplicity, this can be included -inside the `publishInput` function of the E3 Program contract. - -At a minimum, this logic should validate a proof that the given ciphertext is a valid encryption to -the E3's public key. It is also recommended to bundle in proofs to validate: - -- The legitimacy of the Data Provider (e.g., ensuring they are listed in a registry of approved data - providers). -- Range checks, checksums, and/or other well-formedness criteria for the plaintext that was - encrypted. - -## Result Publication and Events - -### Key Events - -**Interfold Contract Events:** - -```solidity -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, - bytes32 ciphertextCommitment -); - -event PlaintextOutputPublished(uint256 indexed e3Id, bytes plaintextOutput, bytes proof); - -event E3Failed(uint256 indexed e3Id, E3Stage failedAtStage, FailureReason reason); - -event RewardsDistributed(uint256 indexed e3Id, address[] nodes, uint256[] amounts); -``` - -**Program Contract Events:** - -```solidity -event InputPublished(uint256 indexed e3Id, bytes data, uint256 index); -``` - -### Result Publication Flow - -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. -5. Rewards are distributed to active committee members. -6. Events are emitted for off-chain services. - -## Example Usage - -### Submitting a Request - -```javascript -const interfoldContract = new ethers.Contract(interfoldAddress, interfoldAbi, signer) - -const requestParams = { - committeeSize, - inputWindow: [inputWindowStart, inputWindowEnd], - e3Program: e3ProgramAddress, - paramSet, - computeProviderParams, - customParams: '0x', - expectedFeeToken, - expectedCryptoConfigId, - maxFee: 0n, // replaced below, once the quote is known -} - -// `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)) - .find((parsed) => parsed?.name === 'E3Requested')?.args?.e3Id -``` - -### Monitoring Results - -```javascript -interfoldContract.on('PlaintextOutputPublished', (e3Id, plaintext, proof) => { - console.log(`Computation ${e3Id} completed with result:`, plaintext, proof) -}) -``` - -### Submitting Inputs - -You can submit inputs by calling the `publishInput` function on your E3 Program contract: - -```javascript -const tx = await programContract.publishInput(e3Id, encryptedInput) -await tx.wait() -``` - -## Security Considerations - -1. **Committee Selection** - -The committee enum that you select fixes `N`, `H`, and `T`. It affects the cost, liveness margin, -and economic security of your E3. Larger supported committees require more coordination and proof -work, so they have a higher fee. Applications cannot choose arbitrary threshold values. - -2. **Input Validation** - -Your Program contract is critical to ensuring that inputs come from approved parties, are sanitized -for your computation, and truthfully correspond to any specific sources of truth. You can use your -Program contract to push computation to the edges, reducing the complexity of your FHE computation. - -3. **Result Verification** - -The Interfold protocol will accept and decrypt whatever output is reported by your Compute Provider. -In other words, your E3s inherit the trust assumptions of your Compute Provider. An incorrectly -implemented Secure Process in your E3 Program or a dishonest or compromised Compute Provider could -leak information. - -4. **Event Handling** - -If you are consuming Interfold outputs offchain, allow for sufficient block confirmations to be -confident in the finality of the outcome and to appropriately handle re-orgs. - -## Best Practices - -See the dedicated [Best Practices](/best-practices) guide for updated operational playbooks covering -parameter selection, validator design, compute provider trust, and observability. +The call succeeds only when the E3 is in `CiphertextReady` and the decryption deadline has not +passed. The proof must not be empty (`ProofRequired`). The committee must still be viable, and the +decryption verifier must accept the proof. + +On success, the E3 moves to `Complete`. The contract credits the treasury share and the committee +rewards. It emits `RewardsDistributed`, `PlaintextOutputPublished(e3Id, plaintextOutput, proof)`, +and `E3StageChanged`. Rewards are pull payments. Each recipient calls `claimReward(e3Id)` or +`claimRewards(e3Ids)`. + +## Failure functions + +These functions handle an E3 that does not finish. For the rules and the refund amounts, see +[Request an E3](/build/requesting-an-e3#when-an-e3-fails). + +| Function | Caller | Effect | +| ----------------------------- | ----------------------------- | ------------------------------------------------------------------------- | +| `checkFailureCondition(e3Id)` | Anyone (view) | Returns `(canFail, reason)` for the current stage deadline. | +| `markE3Failed(e3Id)` | Anyone after the grace period | Moves an overdue E3 to `Failed` with a timeout reason. | +| `cancelE3(e3Id)` | The requester only | Fails an E3 whose randomness request expired without a usable result. | +| `processE3Failure(e3Id)` | Anyone | Sends the service escrow to the refund manager and calculates the refund. | + +During `markFailedGracePeriod` after a deadline, only the requester, the contract owner, or an +active committee member can call `markE3Failed` (`MarkE3FailedInGracePeriod`). + +## Read functions + +These view functions are the ones that applications use most. + +| Function | Returns | +| ------------------------------------------- | ----------------------------------------------------------------------------- | +| `getE3(e3Id)` | The `E3` record. Reverts with `E3DoesNotExist` for an unknown ID. | +| `getE3Stage(e3Id)` | The `E3Stage` value. | +| `getFailureReason(e3Id)` | The `FailureReason` value. `None` when the E3 has not failed. | +| `getRequester(e3Id)` | The requester address. | +| `getDeadlines(e3Id)` | `E3Deadlines { dkgDeadline, computeDeadline, decryptionDeadline }`. | +| `getE3TimeoutConfig(e3Id)` | The timeout windows frozen at request time. | +| `getE3LifecycleDeadline(e3Id)` | The latest possible lifecycle deadline, set at request time. | +| `getTimeoutConfig()` | The current `E3TimeoutConfig { dkgWindow, computeWindow, decryptionWindow }`. | +| `getE3Quote(params)` | The total fee for an `E3RequestParams` value at the current block. | +| `getPricingConfig()` | The current `PricingConfig`. | +| `feeToken()`, `feeTokenDecimals()` | The current fee token and its expected decimals. | +| `isFeeTokenAllowed(token)` | Whether a token is on the fee-token allow-list. | +| `requestsPaused()` | Whether new requests revert. | +| `e3Programs(program)` | Whether a program is registered for new requests. | +| `paramSetRegistry(paramSet)` | The ABI-encoded BFV parameters. Empty when the set is not registered. | +| `committeeThresholds(size, index)` | Index `0` is H and index `1` is N for a committee size. | +| `e3CryptoConfigIds(e3Id)` | The configuration ID frozen for an E3. | +| `maxDuration()`, `markFailedGracePeriod()` | The duration limit and the failure grace period, in seconds. | +| `pendingReward(e3Id, account)` | The reward that an account can claim for an E3. | +| `ciphernodeRegistry()`, `e3RefundManager()` | The current registry and refund manager. | + +## Events + +The Interfold contract emits these events for the E3 lifecycle. + +| Event | Emitted when | +| -------------------------------------- | --------------------------------------------------------------------------------------------- | +| `E3Requested` | `request` succeeds. It carries the full `E3` record and the indexed configuration ID. | +| `E3StageChanged` | Any stage changes. It carries the previous stage and the new stage. | +| `CommitteeFinalized` | The registry finalizes the committee. | +| `CommitteeFormed` | The registry reports the committee public key, and the E3 moves to `KeyPublished`. | +| `CiphertextOutputReferencePublished` | `publishCiphertextOutput` succeeds. | +| `PlaintextOutputPublished` | `publishPlaintextOutput` succeeds. | +| `RewardsDistributed`, `RewardCredited` | Rewards are credited after success. | +| `TreasuryCredited` | The randomness fee is credited at request time, or the protocol share is credited at success. | +| `E3Failed` | An E3 fails. It carries the stage at failure and the `FailureReason`. | +| `E3FailureReclassified` | An expulsion changes a requester-paid failure reason to a ciphernode-paid reason. | +| `E3FailureProcessed` | `processE3Failure` sends the escrow to the refund manager. | + +`IInterfold` also declares `InputPublished` and `CiphertextOutputPublished`. The Interfold contract +does not emit them. E3 programs emit their own input events, and new outputs use +`CiphertextOutputReferencePublished`. + +The `CiphernodeRegistry` emits the committee events. Applications use these most: + +| Registry event | Content | +| ---------------------------------- | ------------------------------------------------------------------------------------ | +| `CommitteeRandomnessRequested` | The randomness request ID, the provider, and the randomness deadline. | +| `SortitionCommitteeFinalized` | The selected committee and the ticket scores. | +| `CommitteeFormationFailed` | Too few tickets or no usable randomness at finalization. | +| `CommitteeProofPublished` | The committee, the public key commitment, and the verified DKG proof. | +| `CommitteePublicKeyChunkPublished` | One chunk of the serialized public key. Clients join the chunks to get the full key. | + +## Custom errors + +These errors are the ones that an application sees most. All are declared in `IInterfold`, except +the registry errors. + +| Error | Cause | +| ------------------------------------------------------------------ | --------------------------------------------------------------------- | +| `RequestsPaused()` | The owner paused new requests. | +| `FeeTokenNotAllowed(token)` | The current fee token is not on the allow-list. | +| `E3ProgramNotAllowed(program)` | The program is not registered. | +| `InvalidInputDeadlineStart(start)`, `InvalidInputDeadlineEnd(end)` | The input window starts in the past or ends before it starts. | +| `InvalidDuration(duration)` | The worst-case lifecycle is longer than `maxDuration`. | +| `UnsupportedCryptoConfig()` | The chain does not support the parameter set or the committee shape. | +| `CommitteeSizeNotConfigured(size)` | No thresholds are set for the committee size. | +| `CommitteeSizeTooSmall(size)`, `ThresholdTooSmall(threshold)` | The committee size is below the pricing minimums. | +| `FeeTokenChanged(expected, actual)` | `expectedFeeToken` is not the current fee token. | +| `CryptoConfigChanged(expected, actual)` | `expectedCryptoConfigId` is not the ID for `paramSet`. | +| `FeeExceedsMaximum(fee, maximum)` | The quote is more than `maxFee`. | +| `InvalidEncryptionScheme(id)` | No verifier is set for the scheme that the program returned. | +| `InsufficientCiphernodes(requested, available)` | Registry: too few eligible ciphernodes for N. | +| `InputWindowClosedBeforeKeyPublication(e3Id, inputDeadline)` | The committee tried to publish its key after the input window closed. | +| `InvalidStage(e3Id, expected, actual)` | The call is not valid in the current stage. | +| `InputDeadlineNotReached(e3Id, inputDeadline)` | A ciphertext output arrived before the input window closed. | +| `CommitteeDutiesCompleted(e3Id, expiration)` | An output arrived after its deadline. | +| `CiphertextOutputAlreadyPublished(e3Id)` | The E3 already has a ciphertext output. | +| `InvalidOutput(output)` | The data availability receipt or the compute proof failed. | +| `ProofRequired()` | The plaintext proof is empty. | +| `FailureConditionNotMet(e3Id)` | `markE3Failed` was called before the stage deadline passed. | +| `E3NotCancellable(e3Id, stage)` | `cancelE3` was called outside its narrow condition. | + +## Security considerations + +- **Committee size.** The committee enum fixes N, H, and T. You cannot choose other thresholds. A + larger committee costs more because the quote grows with N and H. +- **Input validation.** Interfold does not see inputs. Your E3 program must check who can submit, + when, and what. It must also bind each ciphertext to its commitment. +- **Output verification.** Interfold accepts a ciphertext output only if your program's `verify` + accepts the proof. A weak `verify` gives a weak guarantee. Your E3s also depend on the trust model + of your compute provider. +- **Event finality.** A reorganization can remove an event that you already read. Wait for enough + confirmations before you act on an event that moves funds or ends a process. + +## Next steps + + + + Fees, fee tokens, failure handling, refunds, and committee choice. + + + Call these functions from TypeScript and React. + + + Write the E3 program that validates requests, inputs, and outputs. + + + Each stage, its deadline, and what happens on a timeout. + + diff --git a/docs/pages/build/requesting-an-e3.mdx b/docs/pages/build/requesting-an-e3.mdx index 1df4e6c226..ea24b2fb84 100644 --- a/docs/pages/build/requesting-an-e3.mdx +++ b/docs/pages/build/requesting-an-e3.mdx @@ -1,180 +1,375 @@ --- -title: 'Requestor Guide' +title: 'Request an E3' +description: + 'What a requester pays for an E3, how the contract computes the fee, what happens when an E3 + fails, how refunds work, and how to choose a committee size and parameter set.' --- -# Requestor Guide +import { Callout, Steps } from 'nextra/components' +import { Lifecycle, Stage, LinkCards, LinkCard } from '@/components/ui' -This page is for **anyone using an Interfold-powered application** — voting in a private DAO, -joining a confidential auction, contributing data to a private analytics round, or anything else -built on top of Encrypted Execution Environments (E3s). +# Request an E3 -If you're a developer integrating with the Interfold contracts directly, see -[Building with Interfold](/building-with-interfold) and the [SDK reference](/sdk). The page you're -reading now is the plain-English version: what you're paying for, what to do when something goes -wrong, and how to read the security knobs on a request. +This page is for requesters. A requester is the account that calls `request` on the Interfold +contract and pays for an E3. The page explains the fee and how the contract computes it, the fee +token on each network, and the lifecycle from the requester side. It also explains failures, +refunds, and the choice of committee size and parameter set. It ends with a checklist that you can +use before each request. -> Most of what an E3 does is invisible by design — your inputs are encrypted before they ever leave -> your device, and only the _result_ is revealed. This guide explains the few things that _are_ -> visible to you: the fee, the waiting period, and what happens if a round fails. +For the function signatures, see [The Interfold Contract](/build/interfold-contract). For the SDK +calls, see [SDK](/build/sdk). -## 1. What you're paying for +## What the requester pays -Every E3 request charges a one-time fee, paid in the protocol's configured fee token. The Ethereum -Mainnet launch uses USDS. The total quote has two parts: +The requester pays the total fee in one transfer when `request` succeeds. The total has two parts. -- A service fee for ciphernode work -- A flat randomness fee for the protocol-funded VRF request +| Part | Where it goes | Refund on failure | +| ------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------- | +| Service fee | Escrow in the Interfold contract (`e3Payments`) until the E3 ends. | Full or partial. See [Who pays for a failure](#who-pays-for-a-failure). | +| Flat randomness fee | Credited to the request-time treasury when the request succeeds. | None. | -The service fee covers these tasks: +The flat randomness fee (`randomnessFlatFee`) pays for the VRF request that selects the committee. +The margin does not apply to it. -1. **Selecting a Ciphernode committee** — a random subset of operators who will hold a share of the - decryption key for your round. -2. **Generating the shared encryption key** (DKG). This is the cryptographically expensive step and - is the largest line item on your bill. -3. **Running the actual computation** through the compute provider. -4. **Decrypting the result** and posting it on-chain. +When the E3 completes, the service escrow is split in two: -Pricing is cost-plus: the protocol models the ciphernode work, applies a configurable margin, then -splits the service fee between the protocol treasury and the operators who did work. Launch pricing -uses a 10% service margin and a 1.82% treasury share of the service fee. The Ethereum launch flat -randomness fee is 5 USDS. Ticket purchases are operator capacity deposits, not normal protocol -revenue. +- The treasury gets `protocolShareBps` of the service escrow. +- The active committee members share the rest in equal parts. One member gets the rounding dust. +- If no active member remains, the requester gets the service escrow back. -The total fee is **charged once, when you submit the request**. The service fee stays in escrow -until settlement. The flat randomness fee is credited to the request-time treasury when the request -succeeds. It is not part of service escrow and is not refundable after an accepted request. +Committee rewards are pull payments. The contract credits them, and each recipient claims them. -Mainnet deployments can keep E3 requests paused during operator onboarding. If requests are paused, -`request()` reverts until governance enables them. +## How the fee is computed -## 2. The lifecycle of a request +`getE3Quote(params)` returns the total fee. The math is in `lib/InterfoldPricing.sol`. It uses N and +H from `committeeThresholds` for the committee size and the `PricingConfig`. It also uses the +timeout windows, the sortition submission window, the input window, and the block time. -From the moment you submit until the result is published, every E3 walks through five phases: +Each node produces a fixed number of proofs: -| # | Phase | What happens | What you see | -| --- | --------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------- | -| 1 | **Request** | The service fee is locked; VRF supplies the seed for committee sortition. | A "request submitted" confirmation. | -| 2 | **Key generation** | The committee runs a distributed key-generation ceremony and publishes a public key. | Until the configured DKG deadline. | -| 3 | **Input window** | You (and possibly other participants) encrypt your inputs to that key and submit them. | Whatever input form the dApp shows. | -| 4 | **Compute** | The compute provider runs the program over the encrypted inputs and posts an encrypted result. | A waiting state — nothing for you to do. | -| 5 | **Decrypt & publish** | The committee jointly decrypts the result and publishes it on-chain. | The plaintext result appears in the dApp. | +$$ +P = 14 + 4(N - 1) +$$ -The application you're using will surface phase changes for you. If you want to inspect them -yourself, the Interfold contract emits an `E3StageChanged` event for every transition. +The base cost $B$ is the sum of these terms. Each coefficient is a `PricingConfig` field in raw +fee-token units. -## 3. When things go wrong +| Term | Formula | `PricingConfig` field | +| --------------------------- | --------------------- | --------------------------- | +| Key generation, fixed part | $k_f \cdot N$ | `keyGenFixedPerNode` | +| Key generation, proofs | $k_p \cdot N \cdot P$ | `keyGenPerEncryptionProof` | +| Key generation coordination | $c \cdot N(N-1)/2$ | `coordinationPerPair` | +| Proof verification | $v \cdot N \cdot P$ | `verificationPerProof` | +| Availability | $a \cdot N \cdot D$ | `availabilityPerNodePerSec` | +| Decryption | $d \cdot H$ | `decryptionPerNode` | +| Decryption coordination | $c \cdot H(H-1)/2$ | `coordinationPerPair` | +| Publication | $p$ | `publicationBase` | -A failed E3 has a specific `FailureReason`. Settlement first decides who caused the failure: +$D$ is the billable duration in seconds. The contract takes the larger of two pre-compute estimates, +then adds weighted compute and decryption time. It rounds $D$ down to whole seconds. -| Failure reasons | Service fee result | -| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `CommitteeFormationTimeout`, `InsufficientCommitteeMembers`, `DKGTimeout`, `DKGInvalidShares` | Ciphernode-side failure. The requester receives 100% of service fee escrow. | -| `DecryptionTimeout`, `DecryptionInvalidShares`, `VerificationFailed` | Ciphernode-side failure. The requester receives 100% of service fee escrow. | -| `NoInputsReceived`, `ComputeTimeout`, `ComputeProviderExpired`, `ComputeProviderFailed` | Requester or compute-side failure. Completed service work is paid, and the requester gets the rest. | -| `RequesterCancelled` | Retained only for historical E3 compatibility. New cancellations do not use this reason. | +$$ +D = \max\left(W_s + L + W_k u_k,\ E - t\right) + W_c u_c + W_d u_d +$$ -The flat randomness fee remains charged in every case because the VRF request was already funded. -Slashed bonds use a separate settlement ledger and can add to a requester's recovery. +In this formula: -### How a failed round actually gets marked failed +- $W_s$ is the sortition submission window, and $L$ is the input window length. +- $W_k$, $W_c$, and $W_d$ are the DKG, compute, and decryption windows. +- $u_k$, $u_c$, and $u_d$ are `dkgUtilizationBps`, `computeUtilizationBps`, and + `decryptUtilizationBps`, as fractions of 10,000. +- $E$ is the input window end, and $t$ is the block time. -Most failures are detected automatically by the protocol's slashing manager and mark the round -failed in the same transaction. A few timeout-based failures (the `*Timeout` reasons in the table) -need someone to _push_ the round across the finish line by calling `markE3Failed(e3Id)` on the -Interfold contract. **This call is permissionless** — the application you're using will usually do -it for you, but you can do it yourself from a block explorer if it's stalled. +The service fee is $S = B \cdot (10000 + m) / 10000$, where $m$ is `marginBps`. The total fee is $S$ +plus `randomnessFlatFee`. The contract uses integer division. -### Cancelling your request +These rules follow from the formula: -Cancellation is only an escape hatch for a VRF request that expired without a usable response. The -original requester can call `cancelE3(e3Id)` only when all these conditions are true: +- A larger committee costs more. The key generation and coordination terms grow with $N^2$. +- A later input window costs more for the same length. The $E - t$ term reserves node availability + from the request to the end of the input window. +- For the same parameters and pricing, a later block gives the same quote or a lower quote. + `request` computes the quote again, so a fee approved from an earlier quote is enough. -- The E3 is still in `Requested` -- The randomness deadline has passed -- The registry has no timely, usable VRF result +`getE3Quote` reverts when the request is not valid. The errors are `InvalidInputDeadlineStart`, +`InvalidInputDeadlineEnd`, `InvalidDuration`, `UnsupportedCryptoConfig`, +`CommitteeSizeNotConfigured`, `CommitteeSizeTooSmall`, `ThresholdTooSmall`, and `PaymentRequired`. +An unregistered parameter set reverts with the message `BFV param set not registered`. -A valid VRF result permanently disables requester cancellation. The requester cannot inspect a -committee, cancel it, and buy another draw. +## Fee token and pricing per network -Cancellation marks the E3 as `Failed` with `CommitteeFormationTimeout`. It releases committee and -bonding obligations. The requester receives 100% of service fee escrow after -`processE3Failure(e3Id)` succeeds. The 5 USDS randomness fee remains charged. Anyone can also call -`finalizeCommittee(e3Id)` after the expired deadline to complete the same failure path. +The deployment configuration sets the fee token and the pricing. Governance can change these values +with `setFeeAssetConfig` and `setRandomnessFlatFee`. Read the live values with `feeToken()`, +`feeTokenDecimals()`, and `getPricingConfig()`. -This cleanup also trips the randomness circuit breaker. It clears the provider for future E3 -requests. Governance must configure a working provider while requests are paused before request -processing can resume. +| Setting | Ethereum mainnet | Sepolia and local chains | +| ------------------- | --------------------------- | ------------------------ | +| Fee token | USDS, 18 decimals | `MockUSDC`, 6 decimals | +| `randomnessFlatFee` | 5 USDS | 1.00 MockUSDC | +| `marginBps` | 1000 (10%) | 1000 (10%) | +| `protocolShareBps` | 182 (1.82% of service fees) | 182 | +| `minCommitteeSize` | 19 | 0 (no minimum) | +| `minThreshold` | 2 | 0 (no minimum) | -### Claiming your refund +The mainnet values come from `deploy/protocol/mainnet-protocol.config.json`. The Sepolia and local +values come from `scripts/deployInterfold.ts`. For the contract addresses, see +[Contracts & Addresses](/reference/contracts). -Refunds are pull-based: once a round is marked failed and the distribution is calculated, you have -to claim. The flow is: +Each request freezes its fee token. Refunds and rewards for that E3 use the frozen token, also after +governance changes the fee token. -1. Confirm the round is in `Failed` state. The application will usually show this; you can also - check by reading `getE3Stage(e3Id)`. -2. Wait for the `RefundDistributionCalculated` event (it's emitted as soon as the slashing manager - finalises the round — usually the same transaction that marked it failed). -3. Call `claimRequesterRefund(e3Id)` on the `E3RefundManager` contract from the **same address that - originally submitted the request**. The refund is paid in the same token your original fee was - paid in, even if governance has rotated the fee token in the meantime. +## The lifecycle from the requester side -Most dApps will offer a "claim refund" button that does this for you. If you submitted from a -smart-contract wallet (e.g. Safe), use the same wallet to claim. + + -There's no deadline for claiming. Your refund sits in the refund manager indefinitely until you take -it. +The fee transfer succeeds. The registry requests VRF randomness and opens ticket submission. -## 4. Things to know before you submit + + -A few practical points that catch people out: +Sortition selects N ciphernodes. The committee starts DKG. The DKG deadline starts. -- **Approve only the exact fee.** Use the dApp's "estimate" / `getE3Quote` flow to read the exact - fee, and approve that amount. Don't grant unlimited approvals to the Interfold contract. -- **You can't top up later.** If the protocol's pricing changes after you submit, your locked fee is - what runs your round. Don't try to amend an in-flight request — submit a new one if needed. -- **Set realistic windows.** If you're configuring the round yourself rather than using a preset, - schedule the input start after the worst-case committee-key deadline. Give participants enough - time to submit, and give the compute window enough time for your worst observed program runtime. -- **Refunds don't expire**, but they also don't auto-claim. Set a reminder to come back, or use a - dApp that claims for you. + + -## 5. Choosing a secure committee +The committee public key is available. Data providers encrypt inputs and send them to the E3 program +until the input window ends. -If you're configuring an E3 yourself, you choose a supported committee enum. You do not choose -arbitrary threshold numbers. Each deployed circuit and verifier fixes three values: + + -- `N`: the number of operators finalized for the E3 -- `H`: the number of mutually ready dealer contributions that form the DKG key -- `T+1`: the number of partial decryptions needed for the result +The compute provider result passes verification. The decryption deadline starts. -| Committee | N | H | Required decryptions (`T+1`) | DKG omissions | Decryption omissions | -| --------- | --- | --- | ---------------------------- | ------------- | -------------------- | -| Minimum | 3 | 2 | 2 | 1 | 0 | -| Micro | 9 | 5 | 5 | 4 | 0 | -| Small | 19 | 14 | 10 | 5 | 4 | + + -A larger committee gives the random draw more operators and can provide more liveness margin, but it -also increases coordination and proof work sharply. C3 share encryption grows approximately with -`N × (N-1)`. Choose a configuration supported by the target deployment and appropriate for the -round's value. The application should obtain the live configuration ID and fee quote instead of -reconstructing these values locally. +T + 1 ciphernodes decrypt the result. The contract stores the plaintext and credits rewards. -## 6. At-a-glance checklist + + -Before you click "submit": +A deadline passes, or a slash leaves too few active members. Refund settlement starts. -- [ ] You've read the dApp's fee estimate and approved exactly that amount. -- [ ] The committee configuration is appropriate for the value of the round. -- [ ] The input window leaves enough time for everyone (including you) to submit. -- [ ] You have a way to receive a notification when the round completes or fails. -- [ ] You know which address to claim a refund from if the round fails. + + -After the round ends: +Each stage has a deadline. The contract sets each deadline when the E3 enters the stage. -- [ ] If it succeeded, you can read the result on-chain or in the dApp. -- [ ] If it failed, wait for the `RefundDistributionCalculated` event, then claim from the Refund - Manager using the same address that submitted the request. +| Stage | Deadline | Failure reason after the deadline | +| -------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------- | +| `Requested` | The registry committee deadline. The contract adds the DKG window when enough tickets arrive. | `CommitteeFormationTimeout` | +| `CommitteeFinalized` | `dkgDeadline`: the committee deadline plus the DKG window. | `DKGTimeout` | +| `KeyPublished` | `computeDeadline`: the later of key publication and the input window end, plus the compute window. | `ComputeTimeout` | +| `CiphertextReady` | `decryptionDeadline`: the ciphertext publication time plus the decryption window. | `DecryptionTimeout` | -For the design-time companion to this guide, see [Best Practices](/best-practices). For what the -operators on the other side of your fee are actually doing, see -[Ciphernode Operators](/ciphernode-operators). +The committee must publish its key before the DKG deadline and before the input window ends. A key +publication after the DKG deadline reverts with `DKGDeadlinePassed`. A key publication after the +input window end reverts with `InputWindowClosedBeforeKeyPublication`. Read the deadlines of an E3 +with `getDeadlines(e3Id)`. + +The mainnet deployment configuration sets these windows: + +| Window | Mainnet value | +| --------------------------- | ------------------- | +| Randomness request timeout | 3600 s (1 hour) | +| Sortition submission window | 600 s (10 minutes) | +| DKG window | 21600 s (6 hours) | +| Compute window | 604800 s (7 days) | +| Decryption window | 21600 s (6 hours) | +| `maxDuration` | 2592000 s (30 days) | +| `markFailedGracePeriod` | 3600 s (1 hour) | + +## When an E3 fails + +An E3 moves to `Failed` in one of these ways: + +- **Timeout.** After a stage deadline, `markE3Failed(e3Id)` records the timeout reason for that + stage. During `markFailedGracePeriod`, only the requester, the owner, or an active committee + member can call it. After the grace period, anyone can call it. +- **Committee formation.** `finalizeCommittee` on the registry records `CommitteeFormationTimeout` + when no usable randomness arrived. It records `InsufficientCommitteeMembers` when fewer than N + tickets arrived. +- **Slashing.** An expulsion that leaves fewer than H active members records + `InsufficientCommitteeMembers`. +- **Cancellation.** The requester calls `cancelE3`. See [Cancel a request](#cancel-a-request). + +Ciphernodes watch the deadlines and submit `markE3Failed` and `processE3Failure` for you. Any +account can also submit them. + +### Who pays for a failure + +`lib/FailurePayerLib.sol` assigns each failure reason to a payer. The payer decides the refund. + +| Payer | Failure reasons | Requester refund of the service escrow | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | +| Ciphernodes | `CommitteeFormationTimeout`, `InsufficientCommitteeMembers`, `DKGTimeout`, `DKGInvalidShares`, `DecryptionTimeout`, `DecryptionInvalidShares`, `VerificationFailed` | 100% | +| Requester | `NoInputsReceived`, `ComputeTimeout`, `ComputeProviderExpired`, `ComputeProviderFailed`, `RequesterCancelled` | The share for work that was not done | + +For a requester-paid failure, the refund manager uses the work allocation that was frozen at request +time. The default allocation is `committeeFormationBps` 1000, `dkgBps` 4000, `decryptionBps` 4500, +and `protocolBps` 500. The four requester-paid reasons that new E3s can record map to the +`KeyPublished` stage, so the split is: + +| Recipient | Share of the service escrow | +| ------------------------ | --------------------------- | +| Honest committee members | 50% | +| Requester | 45% | +| Treasury | 5% | + +If no honest member remains, the members' share goes to the requester. `RequesterCancelled` is kept +only for old E3s. New cancellations use `CommitteeFormationTimeout`. + + + An E3 that gets no ciphertext output before the compute deadline fails with `ComputeTimeout`. The + requester pays for that failure. Make sure that your compute provider and your server can publish + the output inside the compute window. + + +These rules apply in every case: + +- The flat randomness fee is not refunded. +- Slashed ciphernode collateral goes to the honest committee members, or to the treasury when no + honest member remains. It does not go to the requester. + +### Cancel a request + +`cancelE3(e3Id)` is a recovery path for a randomness request that did not produce a usable result. +The call succeeds only when all of these conditions are true: + +- The caller is the requester (`NotRequester`). +- The E3 is in `Requested`. +- The registry has no sortition seed for the E3. +- The randomness deadline has passed. + +Otherwise the call reverts with `E3NotCancellable`. A valid VRF result disables cancellation. A +requester therefore cannot see a committee and then cancel it to get a new draw. + +A cancellation fails the E3 with `CommitteeFormationTimeout` and releases the committee. The +requester gets 100% of the service escrow after settlement. The registry marks the randomness +provider as degraded and emits `RandomnessCircuitBreakerTripped`. This flag is advisory. It does not +stop new requests, and a new provider clears it. + +### Claim a refund + +Refunds are pull payments. Any account can do the first three steps. Do the last step from the +requester address. + + + +### Confirm that the E3 failed + +Read `getE3Stage(e3Id)` on the Interfold contract. The value must be `Failed` (6). The `E3Failed` +event also gives the reason. + +### Wait for settlement to open + +For an E3 with a finalized committee, settlement opens one day after `getE3LifecycleDeadline(e3Id)`. +It also waits until no open proposal can expel a committee member. An E3 without a finalized +committee settles at once. Before settlement opens, `processE3Failure` reverts with +`SettlementBlocked`. + +### Process the failure + +Call `processE3Failure(e3Id)` on the Interfold contract, if no other account has called it. The call +moves the service escrow to the refund manager, and the refund manager emits +`RefundDistributionCalculated`. `NoPaymentToRefund` means that another account already processed the +E3. + +### Claim the refund + +Call `claimRequesterRefund(e3Id)` on the `E3RefundManager` from the requester address. The contract +pays the refund in the fee token that was frozen for the E3. It emits `RefundClaimed`. + + + +Use the refund manager that was current when you made the request. `e3RefundManager()` on the +Interfold contract returns the current one. `getRefundDistribution(e3Id)` returns the calculated +split, and `hasRequesterClaimed(e3Id, account)` shows whether you already claimed. The refund +manager has no claim deadline. The claim reverts with `RefundNotCalculated`, `NotRequester`, +`AlreadyClaimed`, or `NoRefundAvailable` when it cannot pay. + +## Choose a committee size and parameter set + +The committee size is an enum. Each size fixes N, H, and T in `lib/ActiveCryptoConfig.sol`. You +cannot choose other values. + +| Size | Enum value | N (members) | H (honest count) | T (threshold) | Shares to decrypt (T + 1) | +| --------- | ---------- | ----------- | ---------------- | ------------- | ------------------------- | +| `Minimum` | 0 | 3 | 2 | 1 | 2 | +| `Micro` | 1 | 9 | 5 | 4 | 5 | +| `Small` | 2 | 19 | 14 | 9 | 10 | + +The committee stays viable while at least H members stay active. Decryption needs T + 1 shares. + +The parameter set selects the BFV parameters. The SDK preset must match it. + +| `paramSet` | SDK preset name | Ring degree | Use | +| ---------- | ------------------------ | ----------- | ------------------------------------------ | +| 0 | `INSECURE_THRESHOLD_512` | 512 | Tests only. It has no production security. | +| 1 | `SECURE_THRESHOLD_8192` | 8192 | Production. | + +Each network accepts different pairs: + +| Network | Parameter sets | Committee sizes | +| -------------------------------------- | -------------- | ----------------------------------------------------------------- | +| Ethereum mainnet | 1 only | `Small` only. The launch pricing sets `minCommitteeSize` to 19. | +| Sepolia, local chains (31337 and 1337) | 0 and 1 | All three sizes, when the deployment configures their thresholds. | + +A parameter set must also be registered on the chain. `paramSetRegistry(paramSet)` returns empty +bytes for an unregistered set. The local deploy script registers set 0 and configures thresholds +only for `Minimum`. + +Use these rules to choose: + +- Choose `paramSet` 1 for any value that must stay private. +- Choose a larger committee for more fault tolerance. It costs more. +- Make sure that the network has enough eligible ciphernodes and bond owners. The request reverts + when fewer than N are available. +- Call `getE3Quote` to test a pair before you request. It reverts for a pair that the chain does not + accept. + +## Checklist + +Before each request: + +- [ ] Confirm that `requestsPaused()` returns `false`. +- [ ] Confirm that `e3Programs(program)` returns `true`. +- [ ] Confirm that the chain accepts your committee size and parameter set. +- [ ] Set the SDK preset to match `paramSet`. +- [ ] Set the input window start a few seconds after the current block time. +- [ ] Set the input window end after the worst-case key publication time. +- [ ] Get the fee with `getE3Quote`. +- [ ] Approve exactly that amount for the Interfold contract. +- [ ] Set `maxFee` to the quoted amount. + +The worst-case key publication time is the request time plus the randomness timeout, the sortition +submission window, and the DKG window. On mainnet, that sum is 25,800 seconds. + +After the request: + +- [ ] Watch `E3StageChanged` and `E3Failed` for your E3 ID. +- [ ] Publish the ciphertext output before the compute deadline. +- [ ] If the E3 fails, process the failure and claim the refund from the requester address. + +## Next steps + + + + Function signatures, events, and custom errors. + + + Quote, approve, and request an E3 from TypeScript. + + + Design and operations practices for E3 programs. + + + The stages, the events, and the timeouts in detail. + + diff --git a/docs/pages/build/sdk.mdx b/docs/pages/build/sdk.mdx index 1cc2cf7194..32f1badaba 100644 --- a/docs/pages/build/sdk.mdx +++ b/docs/pages/build/sdk.mdx @@ -1,26 +1,74 @@ --- -title: 'Interfold SDK' -description: 'Use the TypeScript + React SDKs to drive E3 programs from clients and services' +title: 'SDK' +description: + 'Reference for @interfold/sdk and @interfold/react: the client, contract calls, BFV encryption, + events, enums, utilities, and error codes.' --- -# Interfold SDK Overview +import { Callout } from 'nextra/components' +import { Layers, LinkCards, LinkCard } from '@/components/ui' -The Interfold SDK (`@interfold/sdk`) and React helpers (`@interfold/react`) provide a -batteries-included client stack for requesting E3 computations, subscribing to protocol events, and -coordinating custom frontends or backend services. Both packages ship in this repository (see -`packages/interfold-sdk` and `packages/interfold-react`). +# SDK -## Installation +This page is the reference for the TypeScript SDK (`@interfold/sdk`) and the React hook package +(`@interfold/react`). It is for developers who write an E3 client or a coordination server. It +covers installation, the client class, requests, the committee public key, encryption, events, +enums, utilities, and errors. The samples come from the default template (`templates/default`) and +the SDK README. Every export on this page exists in version 0.18.0. + +## Packages + +Both packages are in this repository. + +| Package | Source | Import paths | +| ------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------- | +| `@interfold/sdk` | `packages/interfold-sdk/src` | `@interfold/sdk`, `@interfold/sdk/crypto`, `@interfold/sdk/contracts`, `@interfold/sdk/events` | +| `@interfold/react` | `packages/interfold-react/src` | `@interfold/react` | + + + +## Install + +Add the SDK. Add the React package only if you use React. ```bash -pnpm add @interfold/sdk # TypeScript SDK -pnpm add @interfold/react # React hooks (optional) +pnpm add @interfold/sdk +pnpm add @interfold/react ``` -For browser projects using WASM (encryption helpers, FHE keygen), configure Vite per the SDK README: +`@interfold/react` has two peer dependencies: `react` (`^18.2.0`) and `wagmi` (`^2.14.16`). -```ts -// vite.config.ts +The encryption functions use WebAssembly. In a Vite project, add `vite-plugin-wasm` and +`vite-plugin-top-level-await`, and exclude `@interfold/wasm` from dependency optimization. This +configuration comes from the SDK README: + +```ts filename="vite.config.ts" +import { defineConfig } from 'vite' import wasm from 'vite-plugin-wasm' import topLevelAwait from 'vite-plugin-top-level-await' @@ -32,294 +80,330 @@ export default defineConfig({ }) ``` -## Core client +The template client uses the same two plugins in `client/vite.config.ts`. + +## Create a client + +### With viem clients + +`new InterfoldSDK(config)` creates the client. No separate initialize call is necessary. + +| `SDKConfig` field | Type | Required | Content | +| ------------------------------ | ------------------------------ | -------- | ------------------------------------------------------------ | +| `publicClient` | `PublicClient` | Yes | A viem public client for reads and events. | +| `walletClient` | `WalletClient` | No | A viem wallet client with an account. Write calls need it. | +| `contracts` | `ContractAddresses` | Yes | `interfold`, `ciphernodeRegistry`, and `feeToken` addresses. | +| `chain` | `Chain` | No | The viem chain. | +| `thresholdBfvParamsPresetName` | `ThresholdBfvParamsPresetName` | No | The BFV preset. The default is `INSECURE_THRESHOLD_512`. | + +The constructor throws an `SDKError` with the code `MISSING_PUBLIC_CLIENT`, `INVALID_ADDRESS`, or +`INVALID_THRESHOLD_BFV_PARAMS_PRESET_NAME` when the configuration is not valid. ```ts -import { InterfoldSDK, InterfoldEventType, RegistryEventType } from '@interfold/sdk' -import { createPublicClient, createWalletClient, http, custom } from 'viem' +import { InterfoldSDK } from '@interfold/sdk' +import { createPublicClient, createWalletClient, custom, http } from 'viem' import { sepolia } from 'viem/chains' -const publicClient = createPublicClient({ +const publicClient = createPublicClient({ chain: sepolia, transport: http(RPC_URL) }) + +// The SDK signs with walletClient.account, so create the wallet client with an account. +const [account] = await createWalletClient({ chain: sepolia, - transport: http(import.meta.env.VITE_RPC_URL), -}) + transport: custom(window.ethereum!), +}).requestAddresses() const walletClient = createWalletClient({ + account, chain: sepolia, - transport: custom(window.ethereum), + transport: custom(window.ethereum!), }) const sdk = new InterfoldSDK({ publicClient, walletClient, contracts: { - interfold: import.meta.env.VITE_INTERFOLD_ADDRESS, - ciphernodeRegistry: import.meta.env.VITE_REGISTRY_ADDRESS, - feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS, + interfold: INTERFOLD_ADDRESS, + ciphernodeRegistry: REGISTRY_ADDRESS, + feeToken: FEE_TOKEN_ADDRESS, }, chain: sepolia, - // Match the parameter set selected by the target E3. - thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', + thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192', }) ``` -For server-side or simpler setups, use the factory method: +### With the factory + +`InterfoldSDK.create(options)` creates the viem clients for you. It is useful on a server. + +| Option | Required | Content | +| ------------------------------ | -------- | -------------------------------------------------------------------------- | +| `rpcUrl` | Yes | An `http://`, `https://`, `ws://`, or `wss://` URL. | +| `contracts` | Yes | `interfold`, `ciphernodeRegistry`, and `feeToken` addresses. | +| `chain` | Yes | The viem chain. | +| `privateKey` | No | A private key for a local signing account. Omit it for a read-only client. | +| `thresholdBfvParamsPresetName` | No | The BFV preset. | + +A `ws://` or `wss://` URL gives a WebSocket transport with a 30-second keep-alive and five reconnect +attempts at 2-second intervals. Other URLs give an HTTP transport. This sample comes from +`templates/default/server/index.ts`: ```ts import { InterfoldSDK } from '@interfold/sdk' -import { sepolia } from 'viem/chains' +import { hardhat } from 'viem/chains' const sdk = InterfoldSDK.create({ - rpcUrl: import.meta.env.VITE_RPC_URL, + rpcUrl: RPC_URL, + privateKey: PRIVATE_KEY as `0x${string}`, contracts: { - interfold: import.meta.env.VITE_INTERFOLD_ADDRESS, - ciphernodeRegistry: import.meta.env.VITE_REGISTRY_ADDRESS, - feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS, + interfold: INTERFOLD_CONTRACT as `0x${string}`, + ciphernodeRegistry: CIPHERNODE_REGISTRY_CONTRACT as `0x${string}`, + feeToken: FEE_TOKEN_CONTRACT as `0x${string}`, }, - chain: sepolia, - privateKey: '0x...', // optional — omit for read-only - // Match the parameter set selected by the target E3. + chain: hardhat, thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', }) ``` -### BFV Parameter Presets +## BFV presets -The `thresholdBfvParamsPresetName` field selects the BFV encryption parameters used by the client. -It must match the `paramSet` index registered in the on-chain `Interfold` contract: +The preset selects the BFV parameters that the client uses to encrypt. It must match the `paramSet` +of the E3. -| Preset Name | On-chain `paramSet` | Degree | CRT moduli (L) | Security | Use case | -| ------------------------ | ------------------- | ------ | -------------- | -------- | -------------------------- | -| `INSECURE_THRESHOLD_512` | `0` | 512 | 2 | ❌ None | Fast local or testnet work | -| `SECURE_THRESHOLD_8192` | `1` | 8192 | 3 | ✅ Full | Production-equivalent work | +| Preset name | `paramSet` | Ring degree | Where the contracts accept it | +| ------------------------ | ---------- | ----------- | -------------------------------- | +| `INSECURE_THRESHOLD_512` | 0 | 512 | Sepolia and local chains only | +| `SECURE_THRESHOLD_8192` | 1 | 8192 | Ethereum mainnet, Sepolia, local | -#### Which preset to use on which network +The insecure preset has no production security. Use it only for tests. +`ThresholdBfvParamsPresetNames` lists both names, and `DEFAULT_THRESHOLD_BFV_PARAMS_PRESET_NAME` is +`INSECURE_THRESHOLD_512`. -| Network | Supported presets | -| ----------------- | ---------------------------------------------------- | -| Local development | `INSECURE_THRESHOLD_512` and `SECURE_THRESHOLD_8192` | -| Sepolia testnet | `INSECURE_THRESHOLD_512` and `SECURE_THRESHOLD_8192` | -| Ethereum mainnet | `SECURE_THRESHOLD_8192` only | + + The SDK does not compare the preset with the `paramSet` of the E3. A mismatch encrypts inputs + under the wrong parameters, and the committee cannot decrypt the result. + -Use the preset that the target E3 selects. A Sepolia deployment can use the insecure preset for fast -tests or the secure preset for production-equivalent tests. Mainnet rejects the insecure preset. +## Request an E3 -**Use `SECURE_THRESHOLD_8192` in production.** The insecure preset uses a small polynomial ring -(N=512) with no cryptographic security guarantees. The secure preset (N=8192) meets standard FHE -security targets. It also needs `secure-8192` circuit artifacts on the prover and the verifier — see -[Proving: embedded circuits or your own](#proving-embedded-circuits-or-your-own). - -The `paramSet` index in the E3 request must match the preset used by the ciphernode network. If you -request an E3 with `paramSet: 1` but initialise the SDK with `INSECURE_THRESHOLD_512`, the -ciphertext will be encrypted under the wrong parameters and decryption will fail. - -### Requesting computations +The request takes three calls: quote the fee, approve the fee token, and send the request. This +sample is trimmed from `templates/default/client/src/pages/steps/RequestComputation.tsx`. It adds +`maxFee`. ```ts -const requestParams = { - committeeSize: 0, // CommitteeSize.Minimum (N=3) - inputWindow: [ - BigInt(Math.floor(Date.now() / 1000)), - BigInt(Math.floor(Date.now() / 1000) + 5 * 60), - ] as const, - e3Program: '0x...', - paramSet: 0, // ParamSet.Insecure512. Use 1 for a secure-8192 E3. - computeProviderParams: '0x', - customParams: '0x', -} -const quote = await sdk.getE3Quote(requestParams) -const approvalHash = await sdk.approveFeeToken(quote) -await sdk.waitForTransaction(approvalHash) -const hash = await sdk.requestE3({ ...requestParams, maxFee: quote }) -``` +import { + DEFAULT_COMPUTE_PROVIDER_PARAMS, + DEFAULT_E3_CONFIG, + calculateInputWindow, + encodeComputeProviderParams, +} from '@interfold/sdk' -### Event subscriptions +const publicClient = sdk.getPublicClient() +const inputWindow = await calculateInputWindow(publicClient, 100) // 100 seconds -```ts -const e3Handler = (event) => { - console.log('New request', event.data) +const requestParams = { + committeeSize: DEFAULT_E3_CONFIG.committeeSize, // CommitteeSize.Minimum + inputWindow, + e3Program: E3_PROGRAM_ADDRESS, + paramSet: 0, // insecure-512, for local development + computeProviderParams: encodeComputeProviderParams(DEFAULT_COMPUTE_PROVIDER_PARAMS), } -sdk.onInterfoldEvent(InterfoldEventType.E3_REQUESTED, e3Handler) - -sdk.onInterfoldEvent(RegistryEventType.COMMITTEE_REQUESTED, (event) => { - console.log('Committee requested', event.data) -}) +const fee = await sdk.getE3Quote(requestParams) +const approveTx = await sdk.approveFeeToken(fee) +await publicClient.waitForTransactionReceipt({ hash: approveTx }) -// Later, remember to clean up -sdk.off(InterfoldEventType.E3_REQUESTED, e3Handler) +const hash = await sdk.requestE3({ ...requestParams, maxFee: fee }) ``` -### React hook +`E3RequestParams` in the SDK has these fields: -```tsx -import { useInterfoldSDK } from '@interfold/react' - -export function Dashboard() { - const { - sdk, - isInitialized, - error, - requestE3, - getThresholdBfvParamsSet, - onInterfoldEvent, - off, - InterfoldEventType, - RegistryEventType, - } = useInterfoldSDK({ - autoConnect: true, - contracts: { - interfold: import.meta.env.VITE_INTERFOLD_ADDRESS, - ciphernodeRegistry: import.meta.env.VITE_REGISTRY_ADDRESS, - feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS, - }, - // 'INSECURE_THRESHOLD_512' for local dev and Sepolia; 'SECURE_THRESHOLD_8192' for production. - thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', - }) +| Field | Type | Default | +| ----------------------- | --------------------------- | ----------------- | +| `committeeSize` | `number` (`CommitteeSize`) | Required | +| `inputWindow` | `readonly [bigint, bigint]` | Required | +| `e3Program` | `` `0x${string}` `` | Required | +| `paramSet` | `number` | Required | +| `computeProviderParams` | `` `0x${string}` `` | Required | +| `customParams` | `` `0x${string}` `` | `'0x'` | +| `maxFee` | `bigint` | A fresh quote | +| `gasLimit` | `bigint` | Estimated by viem | - useEffect(() => { - if (!isInitialized) return - const handle = (event) => console.log('E3 Requested', event.data) - onInterfoldEvent(InterfoldEventType.E3_REQUESTED, handle) - return () => off(InterfoldEventType.E3_REQUESTED, handle) - }, [isInitialized]) +`requestE3` fills the fields that the contract needs and the SDK knows: - if (error) return
Error: {error}
+- `expectedFeeToken` is the `feeToken` address from the SDK configuration. +- `expectedCryptoConfigId` is the configuration ID for `paramSet` 0 or 1. Other values fail. +- `committeeSize` must be 0, 1, or 2. - return -} -``` +`requestE3` simulates the call first, then sends it, and returns the transaction hash. To get the E3 +ID, watch `InterfoldEventType.E3_REQUESTED` and read `event.data.e3Id`. For the contract rules +behind each field, see [The Interfold Contract](/build/interfold-contract#request-an-e3). -The hook uses wagmi's `usePublicClient` and `useWalletClient` internally, so your app must be inside -a wagmi provider. +## Get the committee public key -## Encryption functions +The registry does not store the full public key. Committee members publish the serialized key in +`CommitteePublicKeyChunkPublished` events. Each event carries the proven public key commitment +(`pkCommitment`). The client joins the chunks, then checks the key against the commitment. -The SDK includes standalone FHE encryption functions that can be called via the SDK instance or -imported directly: +This sample comes from `templates/default/server/index.ts`: ```ts -// Via SDK instance (uses the configured preset name automatically) -const publicKey = await sdk.generatePublicKey() -const encrypted = await sdk.encryptNumber(42n, publicKey) -const encryptedVec = await sdk.encryptVector(new BigUint64Array([1n, 2n, 3n]), publicKey) -const { encryptedData, proof } = await sdk.encryptNumberAndGenProof(42n, publicKey) -const { encryptedData: vecData, proof: vecProof } = await sdk.encryptVectorAndGenProof( - new BigUint64Array([1n, 2n]), - publicKey, +import { CommitteePublicKeyAssembler } from '@interfold/sdk' +import { RegistryEventType } from '@interfold/sdk/events' +import { hexToBytes } from 'viem' + +const committeeKeyAssembler = new CommitteePublicKeyAssembler() + +await sdk.onInterfoldEvent( + RegistryEventType.COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED, + async (event) => { + const assembled = committeeKeyAssembler.add(event.data) + if (!assembled) return + + const isBoundKey = await sdk.validatePublicKeyCommitment( + assembled.publicKey, + hexToBytes(assembled.pkCommitment), + ) + if (!isBoundKey) return + + committeeKeyAssembler.clear(assembled.e3Id) + // assembled.publicKey is the committee public key for assembled.e3Id. + }, ) +``` -// Generate circuit inputs without proof (useful for debugging) -const { encryptedData, circuitInputs } = await sdk.encryptNumberAndGenInputs(42n, publicKey) +`CommitteePublicKeyAssembler` works as follows: -// Standalone import (pass preset name explicitly) -import { generatePublicKey, encryptNumber } from '@interfold/sdk' +- `add(event.data)` returns an `AssembledCommitteePublicKey` (`e3Id`, `nodes`, `pkCommitment`, + `publicKey`) when a candidate is complete and its Keccak-256 hash matches. Otherwise it returns + `undefined`. +- It accepts only the first candidate from each publisher. +- It rejects keys larger than `MAX_COMMITTEE_PUBLIC_KEY_BYTES` (512 KiB). Each chunk is at most + `MAX_COMMITTEE_PUBLIC_KEY_CHUNK_BYTES` (90 KiB). +- It tracks 128 E3s by default. `clear(e3Id)` removes the state for one E3. -const pk = await generatePublicKey('INSECURE_THRESHOLD_512') -const ct = await encryptNumber(42n, pk, 'INSECURE_THRESHOLD_512') -``` +`sdk.getE3PublicKey(e3Id)` returns the 32-byte public key commitment from the registry. It is not +the key. The call fails with `GET_E3_PUBLIC_KEY_FAILED` before the committee publishes. -### Full encryption method reference +## Encrypt inputs -| Method | Description | -| ---------------------------------- | ----------------------------------------------------- | -| `generatePublicKey()` | Generate a BFV public key | -| `computePublicKeyCommitment(pk)` | Compute a commitment for a public key | -| `encryptNumber(n, pk)` | Encrypt a single bigint | -| `encryptVector(vec, pk)` | Encrypt a `BigUint64Array` | -| `encryptNumberAndGenInputs(n, pk)` | Encrypt and return circuit inputs for ZK verification | -| `encryptNumberAndGenProof(n, pk)` | Encrypt and generate a ZK proof of valid encryption | -| `encryptVectorAndGenInputs(v, pk)` | Vector version of `encryptNumberAndGenInputs` | -| `encryptVectorAndGenProof(v, pk)` | Vector version of `encryptNumberAndGenProof` | -| `getThresholdBfvParamsSet()` | Get the parameter set for the configured preset | -| `generateProof(circuitInputs)` | Generate a ZK proof from circuit inputs directly | +The SDK has each encryption function in two forms. The instance method uses the preset from the +configuration. The standalone function takes the preset name as its last argument. -### Proving: embedded circuits or your own +| Instance method | Standalone function | Returns | +| --------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------- | +| `generatePublicKey()` | `generatePublicKey(preset)` | A new BFV public key (`Uint8Array`). For tests only. | +| `computePublicKeyCommitment(pk)` | `computePublicKeyCommitment(pk, preset)` | The public key commitment (`Uint8Array`). | +| `validatePublicKeyCommitment(pk, c)` | None | `true` when the commitment of `pk` equals `c`. | +| `computeCiphertextCommitment(ct)` | `computeCiphertextCommitment(ct, preset)` | The SAFE commitment of a ciphertext (`Uint8Array`). | +| `encryptNumber(value, pk)` | `encryptNumber(value, pk, preset)` | The ciphertext of one `bigint` (`Uint8Array`). | +| `encryptVector(values, pk)` | `encryptVector(values, pk, preset)` | The ciphertext of a `BigUint64Array` (`Uint8Array`). | +| `encryptNumberAndGenInputs(value, pk)` | `encryptNumberAndGenInputs(value, pk, preset)` | `{ encryptedData, circuitInputs }` | +| `encryptVectorAndGenInputs(values, pk)` | `encryptVectorAndGenInputs(values, pk, preset)` | `{ encryptedData, circuitInputs }` | +| `encryptNumberAndGenProof(value, pk)` | `encryptNumberAndGenProof(value, pk, preset)` | `{ encryptedData, proof }` | +| `encryptVectorAndGenProof(values, pk)` | `encryptVectorAndGenProof(values, pk, preset)` | `{ encryptedData, proof }` | +| `getThresholdBfvParamsSet()` | `getThresholdBfvParamsSet(preset)` | `BfvParams` (`degree`, `plaintextModulus`, `moduli`, `error1Variance`) | +| None | `generateProof(circuitInputs)` | A `ProofData` proof from `@aztec/bb.js`. | -`generateProof()` — and the `*AndGenProof` helpers that call it — run the user-data-encryption (UDE) -circuits that are embedded in the package. Those artifacts are compiled with -`--preset insecure-512 --committee minimum` (see -`packages/interfold-sdk/scripts/compile-circuits.sh`), so they only match `INSECURE_THRESHOLD_512` -and a minimum-size committee. +A key from `generatePublicKey` is not a committee key. The committee cannot decrypt data that is +encrypted to it. For the full encrypt and submit flow, see +[Encrypt and submit](/build/tutorials/encrypt-and-submit). -This has two consequences: +### Encryption proofs -- **`generateProof()` is a convenience for the embedded insecure path.** It is enough for local - development, for Sepolia, and for tests. Secure (N=8192) artifacts do not ship in the SDK today. -- **The secure preset gives witnesses the embedded circuits cannot execute.** With - `SECURE_THRESHOLD_8192`, `encryptNumberAndGenInputs()` returns N=8192 circuit inputs, while the - embedded UDE circuits are N=512. Proof generation fails on the shape mismatch. +The `*AndGenProof` functions and `generateProof` use circuits that ship in the SDK. The SDK compiles +them with the `insecure-512` preset and the `minimum` committee +(`packages/interfold-sdk/scripts/compile-circuits.sh`). The proofs target recursive verification +(`noir-recursive-no-zk`), not a Solidity verifier. -**For on-chain Honk verification of encryption, bring your own compiled circuits.** Most E3 programs -already need a custom proving pipeline on top of Greco — an app circuit and a fold circuit that -wraps the UDE proof — so compile the UDE, app, and fold circuits for the preset and committee your -deployment uses, then emit the Solidity verifier from them. `examples/CRISP` shows the whole flow. -See [Noir Circuits](./noir-circuits) for the build commands and the verifier interface. +In Node.js, `generateProof` checks the circuit stamp file (`.active-preset.json`) when it finds one. +It fails with one of these codes when the stamp does not match: -Use `encryptNumberAndGenInputs()` (or `encryptVectorAndGenInputs()`) to get the circuit inputs, and -feed them to your own prover instead of `generateProof()`. +- `SDK_CIRCUIT_STAMP_MISSING` or `SDK_CIRCUIT_STAMP_INVALID`: the stamp file is missing or broken. +- `SDK_CIRCUIT_COMMITTEE_MISMATCH`: the circuits are not for the `minimum` committee. +- `SDK_CIRCUIT_PRESET_MISMATCH`: the circuits are not for `insecure-512`. -Secure artifacts may ship in the SDK in a later release. Until they do, treat your own compiled -circuits as the supported path for production. +For another preset, another committee size, or an on-chain verifier, compile your own circuits. Use +`encryptNumberAndGenInputs` or `encryptVectorAndGenInputs` to get the witness for your prover. See +[Noir circuits](/build/noir-circuits). `examples/CRISP` shows a complete custom pipeline. -## Modular submodule imports +## Submodule imports -The SDK is organized into three submodules (`contracts`, `events`, `crypto`) that can be imported -independently for tree-shaking: +Each subpath exports part of the SDK. The main entry point exports everything, except the type +`CiphertextOutputReferencePublishedData`, which only `@interfold/sdk/events` exports. + +| Import path | Exports | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@interfold/sdk/crypto` | The standalone encryption functions, `generateProof`, `ThresholdBfvParamsPresetNames`, and the types `BfvParams`, `ThresholdBfvParamsPresetName`, `VerifiableEncryptionResult`, `EncryptedValueAndPublicInputs` | +| `@interfold/sdk/contracts` | `ContractClient`, `E3Stage`, `FailureReason`, and the types `ContractClientConfig`, `ContractAddresses`, `E3`, `E3RequestParams` | +| `@interfold/sdk/events` | `EventListener`, `InterfoldEventType`, `RegistryEventType`, `RandomnessProviderEventType`, and the event data types | ```ts -import { generatePublicKey, encryptNumber } from '@interfold/sdk/crypto' +import { encryptNumber, generatePublicKey } from '@interfold/sdk/crypto' import { ContractClient } from '@interfold/sdk/contracts' import { EventListener, InterfoldEventType } from '@interfold/sdk/events' ``` -All exports are also available from the main `@interfold/sdk` entry point. - -## Event types - -The SDK provides three event type enums for subscribing to on-chain events: +## Events + +The SDK watches events with viem `watchContractEvent`. It sends registry-only event names to the +`ciphernodeRegistry` address and all other names to the `interfold` address. The shared names +`OwnershipTransferred` and `Initialized` therefore go to the `interfold` address, also for a +`RegistryEventType` key. + +| Method | Effect | +| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | +| `onInterfoldEvent(type, callback)` | Watches an Interfold or registry event. | +| `off(type, callback)` | Removes a callback. The watcher stops when no callback remains. | +| `once(type, callback)` | Runs the callback for the first event only. | +| `getHistoricalEvents(type, fromBlock?, toBlock?)` | Reads past logs. With `fromBlock`, it reads in ranges of 10,000 blocks. | +| `onRandomnessProviderEvent(provider, type, callback, fromBlock?)` | Watches a randomness provider. | +| `offRandomnessProviderEvent(provider, type, callback)` | Removes a randomness provider callback. | +| `getHistoricalRandomnessProviderEvents(provider, type, from, to?)` | Reads past provider logs. It needs `fromBlock`. | +| `startEventPolling()`, `stopEventPolling()` | Starts or stops a loop that reads the block number every 5 seconds. The loop does not deliver events. | +| `cleanup()` | Stops polling, stops every watcher, and removes every callback. | + +Each callback gets an `InterfoldEvent` with `type`, `data` (the decoded event arguments), `log`, +`timestamp`, `blockNumber`, and `transactionHash`. `timestamp` is the local time when the SDK got +the log. Live watchers skip a log that they already delivered. ### `InterfoldEventType` -| Enum Key | Solidity Event Name | -| ----------------------------- | --------------------------- | -| `E3_REQUESTED` | `E3Requested` | -| `CIPHERTEXT_OUTPUT_PUBLISHED` | `CiphertextOutputPublished` | -| `PLAINTEXT_OUTPUT_PUBLISHED` | `PlaintextOutputPublished` | -| `E3_PROGRAM_REGISTERED` | `E3ProgramRegistered` | -| `ENCRYPTION_SCHEME_ENABLED` | `EncryptionSchemeEnabled` | -| `CIPHERNODE_REGISTRY_SET` | `CiphernodeRegistrySet` | -| `MAX_DURATION_SET` | `MaxDurationSet` | -| `PARAM_SET_REGISTERED` | `ParamSetRegistered` | -| `OWNERSHIP_TRANSFERRED` | `OwnershipTransferred` | -| `INITIALIZED` | `Initialized` | +| Key | Event | Emitted by the current contracts | +| --------------------------------------- | ------------------------------------ | -------------------------------- | +| `E3_REQUESTED` | `E3Requested` | Yes | +| `CIPHERTEXT_OUTPUT_REFERENCE_PUBLISHED` | `CiphertextOutputReferencePublished` | Yes | +| `CIPHERTEXT_OUTPUT_PUBLISHED` | `CiphertextOutputPublished` | No. Use the reference event. | +| `PLAINTEXT_OUTPUT_PUBLISHED` | `PlaintextOutputPublished` | Yes | +| `E3_PROGRAM_REGISTERED` | `E3ProgramRegistered` | Yes | +| `ENCRYPTION_SCHEME_ENABLED` | `EncryptionSchemeEnabled` | Yes | +| `CIPHERNODE_REGISTRY_SET` | `CiphernodeRegistrySet` | Yes | +| `MAX_DURATION_SET` | `MaxDurationSet` | Yes | +| `PARAM_SET_REGISTERED` | `ParamSetRegistered` | Yes | +| `OWNERSHIP_TRANSFERRED` | `OwnershipTransferred` | Yes | +| `INITIALIZED` | `Initialized` | Yes | ### `RegistryEventType` -| Enum Key | Solidity Event Name | -| ------------------------------------ | --------------------------------- | -| `COMMITTEE_REQUESTED` | `CommitteeRequested` | -| `COMMITTEE_RANDOMNESS_REQUESTED` | `CommitteeRandomnessRequested` | -| `RANDOMNESS_CIRCUIT_BREAKER_TRIPPED` | `RandomnessCircuitBreakerTripped` | -| `COMMITTEE_PUBLISHED` | `CommitteePublished` | -| `COMMITTEE_FINALIZED` | `SortitionCommitteeFinalized` | -| `INTERFOLD_SET` | `InterfoldSet` | -| `OWNERSHIP_TRANSFERRED` | `OwnershipTransferred` | -| `INITIALIZED` | `Initialized` | - -`CommitteeRequested` is retained for pre-VRF registry logs. The ciphernode runtime creates its own -durable event with the same name after the registry accepts a VRF result. +| Key | Event | Emitted by the current contracts | +| -------------------------------------- | ---------------------------------- | -------------------------------- | +| `COMMITTEE_RANDOMNESS_REQUESTED` | `CommitteeRandomnessRequested` | Yes | +| `RANDOMNESS_CIRCUIT_BREAKER_TRIPPED` | `RandomnessCircuitBreakerTripped` | Yes | +| `COMMITTEE_FINALIZED` | `SortitionCommitteeFinalized` | Yes | +| `COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED` | `CommitteePublicKeyChunkPublished` | Yes | +| `INTERFOLD_SET` | `InterfoldSet` | Yes | +| `OWNERSHIP_TRANSFERRED` | `OwnershipTransferred` | Yes | +| `INITIALIZED` | `Initialized` | Yes | +| `COMMITTEE_REQUESTED` | `CommitteeRequested` | No. It is kept for old logs. | +| `COMMITTEE_PUBLISHED` | `CommitteePublished` | No. Use the chunk event. | ### `RandomnessProviderEventType` -| Enum Key | Solidity Event Name | -| ---------------------- | --------------------- | -| `RANDOMNESS_FULFILLED` | `RandomnessFulfilled` | +`RANDOMNESS_FULFILLED` maps to `RandomnessFulfilled`. The registry freezes a provider for each +request. Read `provider`, `requestId`, and `e3Id` from `CommitteeRandomnessRequested`, then watch +that provider: -The Registry freezes a provider address for each request. Use the address, request ID, and E3 ID -from `CommitteeRandomnessRequested` to watch and correlate the provider response. These methods are -available on the main `InterfoldSDK` object: +```ts +import { RandomnessProviderEventType } from '@interfold/sdk' -```typescript await sdk.onRandomnessProviderEvent( provider, RandomnessProviderEventType.RANDOMNESS_FULFILLED, @@ -329,172 +413,171 @@ await sdk.onRandomnessProviderEvent( ) ``` -`getHistoricalRandomnessProviderEvents` requires an explicit `fromBlock` unless the listener config -defines one. It reads fulfillment logs in bounded block ranges. To join history to a live watcher -without overlap, read through a fixed head and start the watcher at the next block: +A fulfillment shows only that the provider stored a response. The registry decides whether the +response was on time and usable. Callers that share one provider watcher must use the same +`fromBlock`, or omit it. A different explicit `fromBlock` fails with `INVALID_EVENT_CONFIG`. + +## Lifecycle enums + +| Enum | Values | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `E3Stage` | `None` (0), `Requested` (1), `CommitteeFinalized` (2), `KeyPublished` (3), `CiphertextReady` (4), `Complete` (5), `Failed` (6) | +| `FailureReason` | `None` (0), `CommitteeFormationTimeout` (1), `InsufficientCommitteeMembers` (2), `DKGTimeout` (3), `DKGInvalidShares` (4), `NoInputsReceived` (5), `ComputeTimeout` (6), `ComputeProviderExpired` (7), `ComputeProviderFailed` (8), `RequesterCancelled` (9), `DecryptionTimeout` (10), `DecryptionInvalidShares` (11), `VerificationFailed` (12) | +| `CommitteeSize` | `Minimum` (0), `Micro` (1), `Small` (2) | + +`validateCommitteeSize(value)` returns the value or throws `INVALID_COMMITTEE_SIZE`. Read the stage +with `sdk.getE3Stage(e3Id)` and the reason with `sdk.getFailureReason(e3Id)`. + +## Contract calls + +These `InterfoldSDK` methods call the contracts. The write methods need a wallet client with an +account. + +| Method | Contract call | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `getE3Quote(params)` | `Interfold.getE3Quote` | +| `approveFeeToken(amount)` | `approve(interfold, amount)` on the fee token | +| `requestE3(params)` | `Interfold.request` | +| `cancelE3(e3Id)` | `Interfold.cancelE3` | +| `publishCiphertextOutput(e3Id, outputReference, gasLimit?)` | `Interfold.publishCiphertextOutput` with an encoded `CiphertextOutputReference` | +| `getE3(e3Id)`, `getE3Stage(e3Id)`, `getFailureReason(e3Id)` | The matching Interfold read functions | +| `getE3PublicKey(e3Id)` | `CiphernodeRegistry.committeePublicKey` | +| `estimateGas(functionName, args, address, abi, value?)` | viem `estimateContractGas` | +| `waitForTransaction(hash)` | viem `waitForTransactionReceipt` with one confirmation | +| `getPublicClient()` | Returns the viem public client | + +The SDK does not call the E3 program. Call `publishInput` on your program with viem. See +[Encrypt and submit](/build/tutorials/encrypt-and-submit#submit-the-input). + +## Utilities + +The main entry point exports these utilities. + +| Export | Content | +| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `calculateInputWindow(publicClient, duration?, startBuffer?)` | `[now + startBuffer, now + startBuffer + duration]` from the latest block time. Defaults: 1800 s and 15 s. | +| `getCurrentTimestamp(publicClient)` | The timestamp of the latest block. | +| `encodeComputeProviderParams(params, mock?)` | The JSON of `params` as hex bytes. With `mock` set to `true`, 32 zero bytes. | +| `encodeCustomParams(params)` | The JSON of an object as hex bytes. | +| `encodeBfvParams(params)` | ABI-encoded BFV parameters. It throws `MISSING_ERROR1_VARIANCE` without `error1Variance`. | +| `decodePlaintextOutput(hex)` | The first 8 bytes as a little-endian unsigned integer, as a `number`. `null` when the input is too short. | +| `DEFAULT_COMPUTE_PROVIDER_PARAMS` | `{ name: 'risc0', parallel: false, batch_size: 2 }` | +| `DEFAULT_E3_CONFIG` | `{ committeeSize: 0, duration: 1800, payment_amount: '0' }` | +| `isValidAddress(value)`, `isValidHash(value)` | Format checks for a 20-byte address and a 32-byte hash. | +| `sleep(ms)`, `formatBigInt`, `parseBigInt`, `formatEventName`, `parseEventData`, `generateEventId` | Small helpers. | + +## Contract addresses + +`ContractAddresses` has three fields: `interfold`, `ciphernodeRegistry`, and `feeToken`. Create one +SDK instance for each chain. + +The template reads the addresses from `interfold.config.yaml`. +`interfold print-env --chain localhost` prints them as `INTERFOLD_ADDRESS`, `REGISTRY_ADDRESS`, +`E3_PROGRAM_ADDRESS`, `FEE_TOKEN_ADDRESS`, and more. With `--vite`, the names start with `VITE_`. +For live networks, see [Contracts & Addresses](/reference/contracts). + +## Errors + +The SDK throws `SDKError`, a subclass of `Error` with `name` set to `SDKError` and an optional +`code`. + +| Codes | Source | +| ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| `MISSING_PUBLIC_CLIENT`, `INVALID_ADDRESS`, `INVALID_THRESHOLD_BFV_PARAMS_PRESET_NAME` | The constructor | +| `NO_WALLET`, `NO_ACCOUNT` | A write method without a wallet or an account | +| `APPROVE_FEE_TOKEN_FAILED`, `REQUEST_E3_FAILED`, `CANCEL_E3_FAILED`, `PUBLISH_CIPHERTEXT_OUTPUT_FAILED` | The write methods | +| `GET_E3_FAILED`, `GET_E3_PUBLIC_KEY_FAILED`, `GET_E3_QUOTE_FAILED`, `GET_E3_STAGE_FAILED`, `GET_FAILURE_REASON_FAILED` | The read methods | +| `GAS_ESTIMATION_FAILED`, `TRANSACTION_WAIT_FAILED` | Gas estimation and receipts | +| `WATCH_EVENT_FAILED`, `WATCH_LOGS_FAILED`, `HISTORICAL_EVENTS_FAILED`, `POLLING_START_FAILED`, `INVALID_EVENT_CONFIG` | The event listener | +| `INVALID_COMMITTEE_SIZE`, `UNSUPPORTED_CRYPTO_CONFIG`, `MISSING_ERROR1_VARIANCE` | Parameter checks | +| `SDK_CIRCUIT_STAMP_MISSING`, `SDK_CIRCUIT_STAMP_INVALID`, `SDK_CIRCUIT_COMMITTEE_MISMATCH`, `SDK_CIRCUIT_PRESET_MISMATCH` | Proof generation in Node.js | + +A write method wraps the original error in its own code. The contract revert reason is in `message`. +`getE3Quote`, `getE3Stage`, and `getFailureReason` return the viem read directly. A contract revert +from these calls therefore reaches you as a viem error, not as an `SDKError`. -```typescript -const historicalHead = await sdk.getPublicClient().getBlockNumber({ cacheTime: 0 }) -const logs = await sdk.getHistoricalRandomnessProviderEvents( - provider, - RandomnessProviderEventType.RANDOMNESS_FULFILLED, - providerDeployBlock, - historicalHead, -) -await sdk.onRandomnessProviderEvent( - provider, - RandomnessProviderEventType.RANDOMNESS_FULFILLED, - handleFulfillment, - historicalHead + 1n, -) +```ts +import { SDKError } from '@interfold/sdk' + +try { + await sdk.requestE3({ ...requestParams, maxFee: fee }) +} catch (error) { + if (error instanceof SDKError && error.code === 'REQUEST_E3_FAILED') { + console.error(error.message) // Contains the revert, for example FeeExceedsMaximum + } +} ``` -A fulfillment means that the provider stored a response. Query the Registry to determine whether the -response was timely and usable. Live watchers also suppress repeated delivery of the same block, -transaction, and log index. Calls that share one provider and event watcher must use the same -explicit `fromBlock`; omit it to join the active watcher. The SDK rejects a conflicting explicit -start block instead of silently dropping it. - -## E3 lifecycle enums - -### `E3Stage` - -| Value | Stage | -| ----- | -------------------- | -| 0 | `None` | -| 1 | `Requested` | -| 2 | `CommitteeFinalized` | -| 3 | `KeyPublished` | -| 4 | `CiphertextReady` | -| 5 | `Complete` | -| 6 | `Failed` | - -### `FailureReason` - -| Value | Reason | -| ----- | ------------------------------ | -| 0 | `None` | -| 1 | `CommitteeFormationTimeout` | -| 2 | `InsufficientCommitteeMembers` | -| 3 | `DKGTimeout` | -| 4 | `DKGInvalidShares` | -| 5 | `NoInputsReceived` | -| 6 | `ComputeTimeout` | -| 7 | `ComputeProviderExpired` | -| 8 | `ComputeProviderFailed` | -| 9 | Reserved | -| 10 | `DecryptionTimeout` | -| 11 | `DecryptionInvalidShares` | -| 12 | `VerificationFailed` | - -## Utility exports - -The SDK also exports several utility functions and constants from the main entry point: +## React hook -```ts -import { - SDKError, - encodeBfvParams, - encodeComputeProviderParams, - encodeCustomParams, - calculateInputWindow, - decodePlaintextOutput, - getCurrentTimestamp, - DEFAULT_COMPUTE_PROVIDER_PARAMS, - DEFAULT_E3_CONFIG, -} from '@interfold/sdk' +`useInterfoldSDK(config)` creates an `InterfoldSDK` from the wagmi public client and wallet client. +The component must be inside a `WagmiProvider`. + +| `UseInterfoldSDKConfig` field | Content | +| ------------------------------ | ------------------------------------------------------------------------ | +| `contracts` | `interfold`, `ciphernodeRegistry`, and `feeToken` addresses. | +| `autoConnect` | When `true`, the hook creates the SDK as soon as a public client exists. | +| `thresholdBfvParamsPresetName` | The BFV preset. | + +The hook returns `sdk`, `isInitialized`, `error` (a string), `requestE3`, +`getThresholdBfvParamsSet`, `onInterfoldEvent`, `off`, `InterfoldEventType`, and +`RegistryEventType`. It creates the SDK again when the wallet client changes, and it calls +`cleanup()` on unmount. The helper functions throw `SDK not initialized` before the SDK exists. + +The template memoizes the configuration, because the hook creates a new SDK when the `contracts` +object changes. This sample follows `client/src/context/WizardContext.tsx` and +`client/src/utils/sdk-config.ts`: + +```tsx +import { useMemo } from 'react' +import { useInterfoldSDK } from '@interfold/react' + +export function useTemplateSdk() { + const sdkConfig = useMemo( + () => ({ + autoConnect: true, + contracts: { + interfold: import.meta.env.VITE_INTERFOLD_ADDRESS, + ciphernodeRegistry: import.meta.env.VITE_REGISTRY_ADDRESS, + feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS, + }, + thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512' as const, + }), + [], + ) + return useInterfoldSDK(sdkConfig) +} ``` -| Export | Description | -| --------------------------------- | ----------------------------------------------------------------- | -| `SDKError` | Custom error class with an error `code` property | -| `encodeBfvParams(params)` | ABI-encode BFV parameters for smart contract calls | -| `encodeComputeProviderParams` | Encode compute provider params (supports mock mode) | -| `encodeCustomParams(params)` | Encode arbitrary custom parameters as hex | -| `calculateInputWindow` | Compute an input window `[start, end]` from current on-chain time | -| `decodePlaintextOutput(hex)` | Decode a plaintext output hex string to a number | -| `getCurrentTimestamp(client)` | Fetch the current block timestamp from the chain | -| `DEFAULT_COMPUTE_PROVIDER_PARAMS` | Default compute provider configuration object | -| `DEFAULT_E3_CONFIG` | Default E3 config (threshold, duration, payment) | - -## Configuration contract map - -Provide the Interfold, CiphernodeRegistry, and FeeToken addresses. The template exposes these via -`.env` / `interfold.config.yaml`. For multi-chain apps, instantiate separate SDK instances per -chain. - -## Working with the template - -`interfold init` generates the default template (see [Project Template](/project-template)). The -client already wires `@interfold/sdk` and `@interfold/react`: - -- `client/src/sdk.ts` builds the shared SDK instance -- `client/src/hooks/useE3.ts` uses `useInterfoldSDK` to expose helper hooks throughout the UI -- `server/src/services/interfold.ts` instantiates the SDK with a server wallet using - `createWalletClient` - -When you add new contracts or E3 programs, update `client/.env` and `server/.env` with the new -addresses, then restart the dev server so the SDK reconnects. - -## Application-specific SDKs - -For complex E3 programs with specialized requirements, you may want to build an application-specific -SDK on top of or alongside the Interfold SDK. For example, -[CRISP](https://github.com/theinterfold/interfold/tree/main/examples/CRISP) uses `@crisp-e3/sdk`, -which provides: - -- Zero-knowledge proof generation for votes using Noir circuits -- Merkle tree utilities for voter eligibility verification -- Vote encryption helpers using FHE -- Round management and token operations -- CRISP server interaction APIs - -If your E3 program requires specialized cryptographic operations, domain-specific logic, or -application-level abstractions beyond basic protocol interactions, consider building a dedicated SDK -that wraps or complements the core Interfold SDK. - -## Advanced usage - -- **Historical events**: `sdk.getHistoricalEvents(type, fromBlock, toBlock)` fetches logs without a - WebSocket provider; useful for CRON jobs or health checks. -- **Publish ciphertext output**: - `sdk.publishCiphertextOutput(e3Id, { contentHash, ciphertextCommitment, computeProof, availabilityProof }, gasLimit?)` - submits a proof-backed reference to an aggregate ciphertext published on the configured DA layer. -- **Gas controls**: pass `gasLimit` to `requestE3`, `publishCiphertextOutput`, or other write - methods if you want fixed limits when interacting with Interfold on L2s. -- **Gas estimation**: `sdk.estimateGas(functionName, args, contractAddress, abi)` returns the - estimated gas for a contract call before sending. -- **Transaction confirmation**: `sdk.waitForTransaction(hash)` blocks until a transaction is mined - and returns the `TransactionReceipt`. -- **One-shot events**: `sdk.once(eventType, callback)` listens for an event and automatically - unsubscribes after the first occurrence. -- **Event polling**: `sdk.startEventPolling()` / `sdk.stopEventPolling()` are handy in serverless - environments where websockets are unavailable. -- **Custom transports**: on the server, call `createWalletClient({ account, transport: http(rpc) })` - and load the private key from Vaults/KMS. -- **Get E3 quote**: call `sdk.getE3Quote(requestParams)` to get the fee amount required for an E3 - request. -- **Fee token approval**: call `sdk.approveFeeToken(amount)` before `requestE3` to approve the fee - token spend. -- **Cancel an E3**: call `sdk.cancelE3(e3Id)` from the original requester account only after the VRF - deadline expires without a usable result. A valid result disables cancellation. The failed-E3 - claim returns all service fee escrow, while the flat randomness fee remains charged. -- **Query E3 data**: use `sdk.getE3(e3Id)` to read the full E3 struct, `sdk.getE3Stage(e3Id)` for - the current lifecycle stage (returns `E3Stage` enum), and `sdk.getFailureReason(e3Id)` if the E3 - has failed (returns `FailureReason` enum). -- **Public key retrieval**: `sdk.getE3PublicKey(e3Id)` returns the committee's aggregated public key - for a given E3. -- **Access the public client**: `sdk.getPublicClient()` returns the underlying viem `PublicClient`. -- **Cleanup**: call `sdk.cleanup()` when shutting down to unsubscribe all event listeners and stop - polling. - -## Troubleshooting - -| Symptom | Triage | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -| `MISSING_PUBLIC_CLIENT` errors | Ensure `createPublicClient` uses an HTTP or WebSocket RPC reachable from the app. | -| `INVALID_ADDRESS` | Double-check contract addresses passed into the SDK match the current chain. | -| Hooks never initialize | Confirm `autoConnect` is true, wallet is connected, and React component is inside a wagmi provider that renders on the client. | -| No events arrive | Switch to a WebSocket RPC or call `sdk.startEventPolling()` so logs are polled over HTTP. | - -For additional API surface details see `packages/interfold-sdk/README.md` inside this repo. +`@interfold/react` also exports `InterfoldEventType`, `RegistryEventType`, +`RandomnessProviderEventType`, and the common event data types from the SDK. + +## Application SDKs + +A complex E3 program can ship its own SDK next to the Interfold SDK. CRISP uses `@crisp-e3/sdk` +(`examples/CRISP/packages/crisp-sdk`). It exports `CrispSDK`, vote functions such as `encryptVote`, +`prepareBallot`, and `decodeTally`, and Merkle helpers such as `generateMerkleProof`. + +## Next steps + + + + Get the committee key, encrypt an input, and send it to the E3 program. + + + How the template client and coordination server use the SDK. + + + The contract functions that the SDK calls. + + diff --git a/docs/pages/build/tutorials/encrypt-and-submit.mdx b/docs/pages/build/tutorials/encrypt-and-submit.mdx index 9ea94526ce..05147d5a49 100644 --- a/docs/pages/build/tutorials/encrypt-and-submit.mdx +++ b/docs/pages/build/tutorials/encrypt-and-submit.mdx @@ -1,235 +1,329 @@ --- -title: 'Encrypt Data & Submit to an E3' +title: 'Encrypt and Submit an Input' description: - 'Client-side flow for encrypting user inputs with FHE and submitting them to an E3 — BFV presets, - key generation, encryption, and result decoding' + 'Request an E3, get the committee public key, encrypt an input with the SDK, submit it to the E3 + program, and read the result.' --- -# Encrypt Data & Submit to an E3 +import { Callout, Steps } from 'nextra/components' +import { Flow, LinkCards, LinkCard } from '@/components/ui' -This tutorial covers the full client-side flow: choosing BFV parameters, encrypting user data under -the committee's threshold public key, submitting it on-chain, and decoding the final result. +# Encrypt and Submit an Input -> **What you'll build:** A client that encrypts a value under a committee's threshold public key, -> submits it to an active E3, and decodes the plaintext result once the committee publishes it. -> -> **Prerequisites:** A running E3 (from [Quick Start](/quick-start) or -> [CRISP](/CRISP/introduction)), the [SDK](/sdk) installed, and the three contract addresses for -> your target chain (see [Where to find contract addresses](#where-to-find-contract-addresses) -> below). +This tutorial shows the client side of one E3 on the local template stack. You request an E3, get +the committee public key, encrypt two numbers, and submit them to the E3 program. Then you read the +decrypted sum. It is for developers who have the template running and want to write their own client +code. Every call on this page is a real SDK or viem call, and the input format is the format that +the template program `MyProgram` accepts. ---- - -## Where to find contract addresses + -The SDK needs three contract addresses: `interfold`, `ciphernodeRegistry`, and `feeToken`. +## Before you start -| Environment | Where to get them | -| ----------- | ----------------------------------------------------------------------------------------------------- | -| Local dev | Printed by the deploy script; also in `packages/interfold-contracts/deployed_contracts.json` | -| Sepolia | [Ciphernode Operators → Sepolia Contract Addresses](/ciphernode-operators#sepolia-contract-addresses) | -| Custom | Wherever your deploy script wrote them — typically `.env` or a JSON artifact | +You need these items: -Put them in your `.env` and read them from there rather than hard-coding in source: +- A running template stack. Run `pnpm dev:all` in the project. See + [Quick Start](/build/quick-start). +- `@interfold/sdk` and `viem` in your project. For a browser, also add the Vite plugins from + [SDK](/build/sdk#install). +- The addresses from `interfold print-env --chain localhost`: `INTERFOLD_ADDRESS`, + `REGISTRY_ADDRESS`, `FEE_TOKEN_ADDRESS`, and `E3_PROGRAM_ADDRESS`. +- A funded local account. The first Anvil account deploys the contracts and holds the mock fee + token. -```bash -INTERFOLD_ADDRESS=0x... -CIPHERNODE_REGISTRY_ADDRESS=0x... -FEE_TOKEN_ADDRESS=0x... -``` +The samples run in Node.js. They use `RPC_URL`, `PRIVATE_KEY`, and the four addresses as variables. ---- +## Procedure -## 1. Choose Your BFV Preset + -The BFV preset determines the encryption parameters. It must match the `paramSet` index used when -the E3 was requested: +### Create the SDK client -| Preset name | `paramSet` index | When to use | -| ------------------------ | ---------------- | ------------------------------------------------ | -| `INSECURE_THRESHOLD_512` | `0` | Local development and Sepolia — fast, not secure | -| `SECURE_THRESHOLD_8192` | `1` | Production — full security guarantees | +Create one SDK instance and one viem wallet client. The SDK sends Interfold calls. The wallet client +sends the E3 program call. -```typescript +```ts import { InterfoldSDK } from '@interfold/sdk' +import { createWalletClient, http } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { hardhat } from 'viem/chains' -const sdk = new InterfoldSDK({ - publicClient, - walletClient, +const sdk = InterfoldSDK.create({ + rpcUrl: RPC_URL, + privateKey: PRIVATE_KEY, contracts: { interfold: INTERFOLD_ADDRESS, ciphernodeRegistry: REGISTRY_ADDRESS, feeToken: FEE_TOKEN_ADDRESS, }, - chain: sepolia, - // Must match the paramSet used when the E3 was requested + chain: hardhat, + // Must match paramSet 0, which the request in the next step uses. thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', }) -``` - -Sepolia runs `INSECURE_THRESHOLD_512` / `paramSet: 0`. If the preset here and the `paramSet` of the -E3 disagree, the ciphertext is encrypted under the wrong parameters and decryption fails silently. ---- - -## 2. Get the Committee's Public Key - -After the ciphernode committee completes DKG, the threshold public key is published on-chain. The -SDK retrieves it automatically: - -```typescript -const e3Id = 0n // Your E3 ID -const publicKey = await sdk.getE3PublicKey(e3Id) +const walletClient = createWalletClient({ + account: privateKeyToAccount(PRIVATE_KEY), + chain: hardhat, + transport: http('http://localhost:8545'), +}) ``` -The public key is only available after the committee is finalised. If you call this too early, it -will return `null`. +The result is a client that can read, write, and watch events. ---- +### Request an E3 -## 3. Encrypt Your Data +Quote the fee, approve it, and send the request. Then read the E3 ID from the `E3Requested` log. -The SDK provides several encryption functions depending on your data type: +```ts +import { + DEFAULT_COMPUTE_PROVIDER_PARAMS, + InterfoldEventType, + calculateInputWindow, + encodeComputeProviderParams, + type E3RequestedData, +} from '@interfold/sdk' -### Single number - -```typescript -// Encrypt a single bigint value -const encrypted = await sdk.encryptNumber(42n, publicKey) -``` - -### Vector of numbers - -```typescript -// Encrypt multiple values as a vector -const data = BigUint64Array.from([1n, 2n, 3n, 4n]) -const encrypted = await sdk.encryptVector(data, publicKey) -``` - -### With ZK proof of valid encryption - -If your E3 program's `publishInput` verifies encryption correctness (like CRISP does), you need to -generate a proof alongside the ciphertext: +const publicClient = sdk.getPublicClient() +const requestParams = { + committeeSize: 0, // CommitteeSize.Minimum + inputWindow: await calculateInputWindow(publicClient, 300), // 300-second window + e3Program: E3_PROGRAM_ADDRESS, + paramSet: 0, + computeProviderParams: encodeComputeProviderParams(DEFAULT_COMPUTE_PROVIDER_PARAMS), +} -```typescript -// Encrypt and generate a ZK proof that the encryption is valid -const { encryptedData, proof } = await sdk.encryptNumberAndGenProof(42n, publicKey) +const fee = await sdk.getE3Quote(requestParams) +await sdk.waitForTransaction(await sdk.approveFeeToken(fee)) +const requestHash = await sdk.requestE3({ ...requestParams, maxFee: fee }) + +const receipt = await publicClient.waitForTransactionReceipt({ hash: requestHash }) +const logs = await sdk.getHistoricalEvents( + InterfoldEventType.E3_REQUESTED, + receipt.blockNumber, + receipt.blockNumber, +) +const requestLog = logs.find((log) => log.transactionHash === requestHash) +const e3Id = (requestLog as unknown as { args: E3RequestedData }).args.e3Id ``` -This uses the user-data-encryption circuits embedded in `@interfold/sdk`, which are compiled for -`INSECURE_THRESHOLD_512` and a minimum-size committee. For any other preset or committee, or for a -program that folds the encryption proof into an app circuit, compile your own circuits and call -`encryptNumberAndGenInputs()` to get the witness — see -[Proving: embedded circuits or your own](../sdk#proving-embedded-circuits-or-your-own). - -For CRISP specifically, the SDK handles this through `generateVoteProof()` which orchestrates -multiple circuit executions (see [Custom Noir Circuits](./custom-zk-circuits)). - -### Standalone imports (tree-shaking) - -If you only need encryption (not the full SDK), import directly: - -```typescript -import { generatePublicKey, encryptNumber } from '@interfold/sdk/crypto' - -const pk = await generatePublicKey('INSECURE_THRESHOLD_512') -const ct = await encryptNumber(42n, pk, 'INSECURE_THRESHOLD_512') +The result is the E3 ID. It is a large number, because the Interfold address is in its high bits. + +### Get the committee public key + +Wait for the committee to publish the key. Committee members publish the serialized key in +`CommitteePublicKeyChunkPublished` events. Join the chunks, and keep the key only if it matches the +proven commitment. This code follows `templates/default/server/index.ts`. + +```ts +import { CommitteePublicKeyAssembler, RegistryEventType } from '@interfold/sdk' +import { hexToBytes } from 'viem' + +const assembler = new CommitteePublicKeyAssembler() + +const publicKey = await new Promise((resolve) => { + void sdk.onInterfoldEvent( + RegistryEventType.COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED, + async (event) => { + if (event.data.e3Id !== e3Id) return + try { + const assembled = assembler.add(event.data) + if (!assembled) return + const isBoundKey = await sdk.validatePublicKeyCommitment( + assembled.publicKey, + hexToBytes(assembled.pkCommitment), + ) + if (isBoundKey) resolve(assembled.publicKey) + } catch (error) { + console.warn('Ignored an invalid key chunk:', error) + } + }, + ) +}) ``` ---- +The result is the committee public key as a `Uint8Array`. On the local stack, it arrives after the +committee completes DKG. -## 4. Submit On-chain +### Encrypt the input -Submit the encrypted data to the E3 via the Interfold contract's `publishInput` function. The exact -method depends on your application architecture: +Encrypt the value to the committee key. Then compute the SAFE commitment of the ciphertext. -```typescript -// Direct contract call via viem -const hash = await walletClient.writeContract({ - address: INTERFOLD_ADDRESS, - abi: interfoldAbi, - functionName: 'publishInput', - args: [e3Id, encodedData], -}) -const receipt = await publicClient.waitForTransactionReceipt({ hash }) +```ts +const ciphertext = await sdk.encryptNumber(20n, publicKey) +const commitment = await sdk.computeCiphertextCommitment(ciphertext) ``` -If your E3 program requires a ZK proof (like CRISP), encode the proof and ciphertext together in -`encodedData` — the exact encoding depends on your program's `publishInput` ABI. CRISP uses its -coordination server to handle submission; simpler E3 programs can submit directly from the frontend. +The result is the ciphertext bytes and a 32-byte commitment. To encrypt a list of values, use +`sdk.encryptVector(BigUint64Array.from([1n, 2n]), publicKey)`. ---- +### Submit the input -## 5. Listen for the Result +Encode the ciphertext and the commitment as `(bytes, bytes32)`. Then call `publishInput` on the E3 +program. -After the input window closes, the compute provider runs the FHE computation and the ciphernode -committee decrypts the result. You can listen for the final plaintext: +```ts +import { bytesToHex, encodeAbiParameters, parseAbi, parseAbiParameters } from 'viem' -```typescript -import { InterfoldEventType } from '@interfold/sdk' +const data = encodeAbiParameters(parseAbiParameters('bytes, bytes32'), [ + bytesToHex(ciphertext), + bytesToHex(commitment), +]) -// Listen for the decrypted result -sdk.onInterfoldEvent(InterfoldEventType.PLAINTEXT_OUTPUT_PUBLISHED, (event) => { - console.log('Result:', event.data) +const inputHash = await walletClient.writeContract({ + address: E3_PROGRAM_ADDRESS, + abi: parseAbi(['function publishInput(uint256 e3Id, bytes data)']), + functionName: 'publishInput', + args: [e3Id, data], }) - -// Or poll for it -const e3 = await sdk.getE3(e3Id) -if (e3.plaintextOutput) { - console.log('Decrypted result:', e3.plaintextOutput) -} +await sdk.waitForTransaction(inputHash) ``` -### Decoding the result - -The raw plaintext is a byte array. How you interpret it depends on your E3 program. CRISP decodes it -into vote tallies: +The result is a mined transaction with an `InputPublished(e3Id, data, index)` log from `MyProgram`. +`MyProgram` rejects the call after `inputWindow[1]` with `InputDeadlineReached`. -```typescript -import { decodeTally } from '@crisp-e3/sdk' +### Submit a second input -const tally = decodeTally(plaintextOutput, numOptions) -// tally = [42, 58] — 42 votes for option A, 58 for option B -``` - -For a basic E3 that sums numbers, the plaintext is the sum encoded as BFV coefficients. - ---- +Repeat the two previous steps with a second value, for example `22n`. The template server runs the +computation only when the E3 has at least two inputs. -## Browser Setup +### Read the result -If using the SDK in a browser with Vite, you need to configure WASM support: +Watch for the plaintext output of your E3. Decode it with `decodePlaintextOutput`. -```typescript -// vite.config.ts -import wasm from 'vite-plugin-wasm' -import topLevelAwait from 'vite-plugin-top-level-await' +```ts +import { decodePlaintextOutput } from '@interfold/sdk' -export default defineConfig({ - optimizeDeps: { - exclude: ['@interfold/wasm'], - }, - plugins: [wasm(), topLevelAwait()], +await sdk.onInterfoldEvent(InterfoldEventType.PLAINTEXT_OUTPUT_PUBLISHED, (event) => { + if (event.data.e3Id !== e3Id) return + console.log('Sum:', decodePlaintextOutput(event.data.plaintextOutput)) }) ``` -This is required because the SDK uses WASM for BFV encryption operations. +The result appears after the input window closes. The server sends the inputs to the program runner +and publishes the ciphertext output. Then the committee decrypts it. The output is the sum of the +two values. ---- + -## Common Pitfalls +To check the result later, read the stage and the record: -| Issue | Cause | Fix | -| ------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------ | -| Decryption produces garbage | BFV preset mismatch between SDK and E3 request | Ensure `thresholdBfvParamsPresetName` matches the `paramSet` index | -| `getE3PublicKey` returns null | Committee hasn't finished DKG yet | Wait for `CommitteePublished` event | -| Proof verification fails on-chain | Wrong circuit artifacts or parameters | Ensure circuit artifacts match the deployed verifier contract | -| Transaction reverts on `publishInput` | Input window expired or invalid proof | Check E3 stage and deadline before submitting | +```ts +import { E3Stage, decodePlaintextOutput } from '@interfold/sdk' ---- +if ((await sdk.getE3Stage(e3Id)) === E3Stage.Complete) { + const e3 = await sdk.getE3(e3Id) + console.log('Sum:', decodePlaintextOutput(e3.plaintextOutput)) +} +``` -## Next Steps +## Read the key after it is published + +A client that starts after the key publication does not get the live events. Read the chunks from +history instead. Give a `fromBlock` before the key publication. The SDK then reads in ranges of +10,000 blocks. + +```ts +import { + CommitteePublicKeyAssembler, + RegistryEventType, + type CommitteePublicKeyChunkPublishedData, +} from '@interfold/sdk' +import { hexToBytes } from 'viem' + +const assembler = new CommitteePublicKeyAssembler() +const chunkLogs = await sdk.getHistoricalEvents( + RegistryEventType.COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED, + fromBlock, +) + +for (const log of chunkLogs) { + const chunk = (log as unknown as { args: CommitteePublicKeyChunkPublishedData }).args + if (chunk.e3Id !== e3Id) continue + const assembled = assembler.add(chunk) + if ( + assembled && + (await sdk.validatePublicKeyCommitment(assembled.publicKey, hexToBytes(assembled.pkCommitment))) + ) { + // assembled.publicKey is the committee key. + break + } +} +``` -- [Build a Complete E3 Program](./write-e3-program) — understand the contract side -- [Custom Noir Circuits](./custom-zk-circuits) — add ZK proof validation to your inputs -- [Deploy to Sepolia](./deploy-to-testnet) — move from local dev to testnet +`sdk.getE3PublicKey(e3Id)` returns only the 32-byte commitment from the registry. It does not return +the key. + + + `sdk.generatePublicKey()` returns a new random BFV public key. It is useful to test encryption + without a committee. The committee cannot decrypt data that is encrypted to this key. + + +## Add an encryption proof + +A program can require a proof that the ciphertext is a valid encryption. +`sdk.encryptNumberAndGenProof(value, publicKey)` returns `{ encryptedData, proof }`. The circuits in +the SDK support only the `INSECURE_THRESHOLD_512` preset and the `Minimum` committee. For other +presets, use `encryptNumberAndGenInputs` and your own circuits. See +[SDK](/build/sdk#encryption-proofs) and [Custom Noir circuits](/build/tutorials/custom-zk-circuits). + +## How CRISP submits inputs + +CRISP stores ballot ciphertexts on Avail, not on Ethereum. Its input flow has three parts: + +1. The CRISP server checks the ballot proof, stores the exact bytes, and signs an availability + attestation. +2. `CRISPProgram.publishInput` checks the proof and the attestation, reserves the tree leaf, and + emits `InputCommitted`. +3. The server publishes the bytes to Avail. After the Avail range is bridged to Ethereum, any + account calls `finalizeInput` with the receipt. The program emits `InputPublished`. + +`CRISPProgram.verify` refuses the compute proof while an input is still pending. See +[CRISP](/CRISP/introduction). + +## Common problems + +| Symptom | Cause | Fix | +| -------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------- | +| The key promise never resolves | The watcher started after the key chunks were published. | Read the chunks from history. | +| `validatePublicKeyCommitment` returns `false` | The SDK preset does not match the `paramSet` of the E3. | Set the preset that matches `paramSet`. | +| `GET_E3_PUBLIC_KEY_FAILED` | The committee has not published yet. | Wait for the key chunk events. | +| `publishInput` reverts with `InputDeadlineReached` | The input window closed. | Request a new E3 with a longer window. | +| `publishInput` reverts with `EmptyInputData` | The ciphertext is empty. | Check the encryption step. | +| No plaintext output | The E3 has fewer than two inputs, so the template server skips it. | Submit two inputs before the window closes. | +| A WebAssembly error in the browser | The Vite plugins are missing. | Add the plugins from [SDK](/build/sdk#install). | + +## Next steps + + + + Every export, event type, and error code. + + + How the template client and server divide the work. + + + Add input proofs that your program verifies. + + + Run the same flow on a testnet. + +