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.
+
+