From ac5f13b79a76eb11bb3d7bfc766c39914f9d3f2a Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:34:41 +0100 Subject: [PATCH 1/4] docs: add tutorials --- docs/pages/_meta.json | 7 + docs/pages/tutorials/_meta.json | 31 ++ docs/pages/tutorials/custom-zk-circuits.mdx | 232 +++++++++++++++ docs/pages/tutorials/deploy-to-testnet.mdx | 205 +++++++++++++ docs/pages/tutorials/encrypt-and-submit.mdx | 232 +++++++++++++++ docs/pages/tutorials/manage-tickets.mdx | 153 ++++++++++ .../tutorials/operator-troubleshooting.mdx | 231 +++++++++++++++ docs/pages/tutorials/using-the-dashboard.mdx | 175 +++++++++++ docs/pages/tutorials/write-e3-program.mdx | 278 ++++++++++++++++++ 9 files changed, 1544 insertions(+) create mode 100644 docs/pages/tutorials/_meta.json create mode 100644 docs/pages/tutorials/custom-zk-circuits.mdx create mode 100644 docs/pages/tutorials/deploy-to-testnet.mdx create mode 100644 docs/pages/tutorials/encrypt-and-submit.mdx create mode 100644 docs/pages/tutorials/manage-tickets.mdx create mode 100644 docs/pages/tutorials/operator-troubleshooting.mdx create mode 100644 docs/pages/tutorials/using-the-dashboard.mdx create mode 100644 docs/pages/tutorials/write-e3-program.mdx diff --git a/docs/pages/_meta.json b/docs/pages/_meta.json index 1e7a1d4a19..a32bce0bff 100644 --- a/docs/pages/_meta.json +++ b/docs/pages/_meta.json @@ -44,6 +44,13 @@ "hello-world-tutorial": { "title": "Hello World" }, + "-- Tutorials": { + "type": "separator", + "title": "Tutorials" + }, + "tutorials": { + "title": "Tutorials" + }, "-- Tooling": { "type": "separator", "title": "Tooling" diff --git a/docs/pages/tutorials/_meta.json b/docs/pages/tutorials/_meta.json new file mode 100644 index 0000000000..df3d3123f5 --- /dev/null +++ b/docs/pages/tutorials/_meta.json @@ -0,0 +1,31 @@ +{ + "-- Build an E3": { + "type": "separator", + "title": "Build an E3" + }, + "write-e3-program": { + "title": "Build a Complete E3 Program" + }, + "custom-zk-circuits": { + "title": "Custom Noir Circuits" + }, + "encrypt-and-submit": { + "title": "Encrypt & Submit Data" + }, + "deploy-to-testnet": { + "title": "Deploy to Sepolia" + }, + "-- Operate a Node": { + "type": "separator", + "title": "Operate a Node" + }, + "using-the-dashboard": { + "title": "Node Dashboard" + }, + "manage-tickets": { + "title": "Manage Tickets" + }, + "operator-troubleshooting": { + "title": "Troubleshoot Your Node" + } +} diff --git a/docs/pages/tutorials/custom-zk-circuits.mdx b/docs/pages/tutorials/custom-zk-circuits.mdx new file mode 100644 index 0000000000..9841c27b29 --- /dev/null +++ b/docs/pages/tutorials/custom-zk-circuits.mdx @@ -0,0 +1,232 @@ +--- +title: 'Custom Noir Circuits for Input Validation' +description: + 'How CRISP uses Noir zero-knowledge circuits to prove vote validity on-chain — circuit structure, + proof composition, and on-chain verification' +--- + +# Custom Noir Circuits for Input Validation + +Every E3 program must validate that user inputs are correctly encrypted under the committee's +threshold public key — this is the baseline. On top of that, programs can add application-specific +checks. CRISP, for example, also proves that each vote is valid (within the voter's balance), that +the voter is eligible (Merkle membership), and — for mask votes — that the ciphertext is an +encryption of zero correctly added to the previous slot ciphertext. All without revealing the vote +itself. + +This tutorial walks through how CRISP's Noir circuits work, as a template for building your own +input validation circuits. + +> **What you'll learn:** How CRISP composes several Noir circuits into a single on-chain-verifiable +> proof, which library modules are reusable, and how to wire a custom circuit into `publishInput`. +> +> **Time:** ~25 minutes (reading) +> +> **Prerequisites:** Basic familiarity with [Noir](https://noir-lang.org/docs) and the +> [E3 program structure](./write-e3-program). + +--- + +## What the Circuits Prove + +CRISP's circuit system handles two distinct cases: + +**Actual vote:** + +1. **Voter eligibility** — the voter's address is in a Merkle tree of eligible participants +2. **Valid encryption** — the ciphertext is a well-formed BFV encryption +3. **Correct signature** — the submission was signed by the voter (ECDSA recovery) +4. **Within balance** — the vote amount does not exceed the voter's token balance + +**Mask vote** (for +[receipt-freeness](https://blog.theinterfold.com/vote-masking-receipt-freeness-secret-ballots/)): + +1. **Encryption of zero** — the ciphertext encrypts a zero-value vote +2. **Correct ciphertext addition** — the sum of the previous slot ciphertext and the zero ciphertext + is computed correctly (`sum = prev + 0`) + +--- + +## Circuit Structure + +CRISP's circuits live in `examples/CRISP/circuits/` and are organized into a main circuit plus +reusable library modules: + +```text +circuits/ +├── bin/crisp/src/main.nr # Main circuit — orchestrates all checks +└── lib/src/ + ├── merkle_tree.nr # Poseidon-based Merkle membership proof (depth 20) + ├── ecdsa.nr # secp256k1 signature validation + Ethereum address derivation + ├── ciphertext_addition.nr # Homomorphic addition verification (Schwartz-Zippel) + ├── utils.nr # Vote validation (binary coefficients, balance checks) + └── constants.nr # FHE parameters (N, L, Q, bit-widths) +``` + +### The Main Circuit + +The main circuit (`bin/crisp/src/main.nr`) takes all inputs and orchestrates the verification: + +```noir +fn main( + // Ciphertext components + prev_ct0is: [Polynomial; L], // Previous ciphertext (for updates) + prev_ct1is: [Polynomial; L], + prev_ct_commitment: pub Field, + sum_ct0is: [Polynomial; L], // Sum after addition + sum_ct1is: [Polynomial; L], + ct0is: [Polynomial; L], // New vote ciphertext + ct1is: [Polynomial; L], + + // ECDSA signature + public_key_x: [u8; 32], + public_key_y: [u8; 32], + signature: [u8; 64], + hashed_message: [u8; 32], + + // Merkle proof of eligibility + merkle_root: pub Field, + merkle_proof_indices: [u1; 20], + merkle_proof_siblings: [Field; 20], + + // Vote metadata + slot_address: pub Field, + balance: Field, + is_first_vote: pub bool, + is_mask_vote: bool, + num_options: pub u32, +) -> pub (Field, Field, Field) // (final_ct_commitment, ct_commitment, k1_commitment) +``` + +The circuit supports two modes controlled by `is_mask_vote`: + +- **Actual vote**: Verifies the signature matches the slot address, checks that the vote amount + doesn't exceed the voter's balance, and validates Merkle membership +- **Mask vote**: Verifies a zero-value vote. Anyone can submit mask votes to any slot, which + provides + [receipt-freeness](https://blog.theinterfold.com/vote-masking-receipt-freeness-secret-ballots/) — + voters cannot prove which ciphertext is theirs + +### Library Modules + +**`merkle_tree.nr`** — Poseidon-based Merkle membership proof with a 20-level tree. Proves that a +leaf (voter address) exists at a specific index without revealing other leaves. + +**`ecdsa.nr`** — secp256k1 ECDSA signature verification and Ethereum address derivation. Recovers +the signer address from the signature and verifies it matches the expected voter. + +**`ciphertext_addition.nr`** — Used in mask vote mode to prove that `sum = prev + new` for BFV +ciphertexts (where `new` is an encryption of zero). Uses the Schwartz-Zippel lemma with a +Fiat-Shamir challenge point to verify the polynomial identity at a single random point, avoiding the +cost of checking every coefficient. + +**`utils.nr`** — Vote validation helpers. Checks that vote coefficients are binary (0 or 1) and that +the total doesn't exceed the voter's balance. + +--- + +## Proof Composition + +A single vote submission requires proving multiple things, which means multiple circuit executions +composed together. CRISP uses recursive proof folding to produce a single EVM-verifiable proof: + +```text +ct0 circuit ─→ user_data_encryption ─┐ +ct1 circuit ─→ user_data_encryption ─┤ + ├─→ crisp circuit ─→ fold ─→ Single proof +merkle witness (verified inside crisp) ─┤ +ecdsa witness (verified inside crisp) ──┘ +``` + +The `ct0` and `ct1` circuits verify encryption of each ciphertext component. The main `crisp` +circuit takes those results plus the Merkle membership witness and ECDSA signature as private +inputs, verifying eligibility and signature correctness internally. The `fold` circuit recursively +aggregates everything into a single proof that can be verified on-chain. + +The TypeScript SDK orchestrates this in `packages/crisp-sdk/src/vote.ts`: + +```typescript +// 1. Generate circuit inputs (BFV encryption + Merkle proof + signature) +const { circuitInputs, encryptedVote } = await generateCircuitInputs(proofInputs) + +// 2. Execute circuits sequentially, each building on the last +const ct0Result = await executeCircuit(ct0Circuit, ct0Inputs) +const ct1Result = await executeCircuit(ct1Circuit, ct1Inputs) +const crispResult = await executeCircuit(crispCircuit, crispInputs) +const foldResult = await executeCircuit(foldCircuit, foldInputs) + +// 3. Generate the final Honk proof +const proof = await honkBackend.generateProof(foldResult.witness) + +// 4. Encode for Solidity +const solidityProof = encodeSolidityProof(proof, encryptedVote) +``` + +The Barretenberg WASM API (`@aztec/bb.js`) handles the UltraHonk proof generation. The SDK caches +the API instance and SRS (Structured Reference String) to avoid re-initialisation on every proof. + +--- + +## On-chain Verification + +The final proof is verified on-chain in `CRISPProgram.publishInput()` using an auto-generated Honk +verifier contract (`CRISPVerifier.sol`). This verifier is generated by Barretenberg during circuit +compilation and contains the verification key baked in. + +```solidity +// In publishInput(): +honkVerifier.verify(proof, publicInputs); +``` + +The public inputs include: + +- `prev_ct_commitment` — commitment to the previous ciphertext in the slot +- `merkle_root` — the eligibility Merkle root +- `slot_address` — which vote slot is being written to +- `is_first_vote` — whether this is the first vote in the slot +- `num_options` — number of voting options + +--- + +## Generating Circuit Inputs from WASM + +Circuit inputs require BFV encryption internals (polynomial coefficients, RNS representations) that +are implemented in Rust. To reuse the same code in the browser, CRISP compiles it to WASM via +`@crisp-e3/zk-inputs`: + +```typescript +import init from '@crisp-e3/zk-inputs/init' +import { ZKInputsGenerator } from '@crisp-e3/zk-inputs' + +await init() +const generator = ZKInputsGenerator.withDefaults() +const { encryptedVote, inputs } = generator.generateInputs( + previousCiphertext, + publicKey, + voteCoefficients, +) +``` + +The WASM module handles BFV encryption and produces the exact polynomial decompositions the Noir +circuits expect. See the +[crisp-zk-inputs README](https://github.com/gnosisguild/enclave/tree/main/examples/CRISP/packages/crisp-zk-inputs) +for the full API. + +--- + +## Building Your Own Circuits + +If your E3 program needs custom input validation: + +1. **Define what to prove** — eligibility? correct format? value bounds? +2. **Write Noir circuits** — use CRISP's library modules as building blocks (Merkle proofs, ECDSA, + ciphertext operations are all reusable) +3. **Generate a verifier** — compile your circuit with `nargo` and generate the Solidity verifier + with `bb` +4. **Call it from `publishInput`** — deploy the verifier and call it in your E3 program contract +5. **Build a TypeScript SDK** — wrap circuit input generation in a package your frontend can use + +For simpler cases (e.g. just checking a value is in range), you may not need custom circuits at all +— standard Solidity checks in `publishInput` may be sufficient. + +See [Noir Circuits](/noir-circuits) for toolchain setup and compilation instructions. diff --git a/docs/pages/tutorials/deploy-to-testnet.mdx b/docs/pages/tutorials/deploy-to-testnet.mdx new file mode 100644 index 0000000000..6b9d4ef92c --- /dev/null +++ b/docs/pages/tutorials/deploy-to-testnet.mdx @@ -0,0 +1,205 @@ +--- +title: 'Deploy to Sepolia' +description: + 'Move your E3 program from local development to the Sepolia testnet — contracts, server + configuration, and BFV parameter switching' +--- + +# Deploy Your E3 to Sepolia + +This tutorial takes you from a working local E3 (from [Quick Start](/quick-start) or CRISP) to a +live deployment on Sepolia testnet. + +> **What you'll build:** Your E3 program contract deployed to Sepolia, a coordination server pointed +> at a Sepolia RPC, and a verified end-to-end E3 round against the live testnet ciphernode +> committee. +> +> **Time:** ~30–45 minutes (most of it waiting for block confirmations and the committee to form) +> +> **Prerequisites:** A working local E3, Sepolia ETH for gas, a WebSocket RPC endpoint (e.g. +> Alchemy, Infura), and your deployer private key funded. + +--- + +## 1. Get Testnet Tokens + +You'll need: + +- **Sepolia ETH** — for gas. Any public Sepolia faucet works, e.g. + [sepoliafaucet.com](https://sepoliafaucet.com), + [Google Cloud faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia), or + [PoW faucet](https://sepolia-faucet.pk910.de). +- **Test ENCL** — for ciphernode bonding (only if you're running your own nodes). The mock ENCL + token is deployed at `0x24b28471AE7BdF1fdBcfDd183c73D13ff0689B99`. Reach out on the + [community Telegram](https://t.me/encaborations) for test ENCL. +- **Test USDC** — for E3 fees and tickets. Mock USDC at + `0x01AbD7D8e6547c943c2fE082C3Ce194fDCe57396`. Same channel for tokens. + +> The full list of deployed Sepolia addresses lives in +> [Ciphernode Operators → Sepolia Contract Addresses](/ciphernode-operators#sepolia-contract-addresses) +> and in `deployed_contracts.json` inside the repo. + +--- + +## 2. Configure Environment + +Create or update your `.env` file with Sepolia settings: + +```bash +# Sepolia RPC (WebSocket required for event listening) +WS_RPC_URL=wss://eth-sepolia.g.alchemy.com/v2/YOUR_KEY +HTTP_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY + +# Deployer private key (has Sepolia ETH) +PRIVATE_KEY=0x... + +# Chain +CHAIN_ID=11155111 + +# Deployed Interfold contract addresses (Sepolia) +ENCLAVE_ADDRESS=0x450015E41E1F6b6AfaEbf598E32a8d02a368c0A0 +CIPHERNODE_REGISTRY_ADDRESS=0xc8D2880c59D5e807eFFDee3451fb0Aa97f6aefDA +FEE_TOKEN_ADDRESS=0x01AbD7D8e6547c943c2fE082C3Ce194fDCe57396 +``` + +--- + +## 3. Deploy Your E3 Program Contract + +If you're deploying your own E3 program contract (not using an already-deployed one): + +```bash +# From your project root +pnpm deploy:contracts --network sepolia +``` + +This uses Hardhat to deploy your contracts to Sepolia. Make sure `hardhat.config.ts` has the Sepolia +network configured: + +```typescript +// hardhat.config.ts +networks: { + sepolia: { + url: process.env.HTTP_RPC_URL, + accounts: [process.env.PRIVATE_KEY], + }, +} +``` + +Note the deployed contract address — you'll need it for the E3 request and the server config. + +### Verify on Etherscan + +Publishing source makes your contract inspectable and lets users interact with it from Etherscan's +UI. Set an Etherscan API key and run: + +```bash +# .env +ETHERSCAN_API_KEY=your_key + +# Verify +pnpm hardhat verify --network sepolia +``` + +Constructor args are whatever you passed to your program's constructor — typically the Enclave +contract address and any verifier addresses. + +### Save the address + +Persist the deployed address in `.env` so your server and frontend pick it up automatically: + +```bash +# Append the fresh address — don't forget this step +echo "E3_PROGRAM_ADDRESS=" >> .env +``` + +--- + +## 4. Switch to Secure BFV Parameters + +On testnet, you should use the production BFV parameters. Update your SDK initialisation: + +```typescript +const sdk = new EnclaveSDK({ + // ... other config + thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192', +}) +``` + +The SDK will automatically use the secure parameters when requesting E3s: + +```typescript +const hash = await sdk.requestE3({ + threshold: [2, 3], + inputWindow: [startTime, endTime], + e3Program: YOUR_PROGRAM_ADDRESS, + e3ProgramParams: '0x...', + computeProviderParams: '0x...', +}) +``` + +Secure parameters use larger polynomial rings (N=8192 vs N=512) which makes proof generation slower +but provides real cryptographic security. Expect proof generation to take longer than in local dev. + +--- + +## 5. Configure the Server + +Update your coordination server to point to Sepolia: + +```bash +# .env for the server +WS_RPC_URL=wss://eth-sepolia.g.alchemy.com/v2/YOUR_KEY +HTTP_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY +ENCLAVE_ADDRESS=0x450015E41E1F6b6AfaEbf598E32a8d02a368c0A0 +E3_PROGRAM_ADDRESS= +CHAIN_ID=11155111 +``` + +Start the server: + +```bash +pnpm dev:server +``` + +The server will connect to Sepolia via WebSocket and begin listening for E3 events. + +--- + +## 6. Test End-to-End + +1. **Request an E3** — via the SDK or your frontend +2. **Wait for committee formation** — the Sepolia ciphernode committee will perform sortition and + DKG (this takes longer than local dev due to real block times) +3. **Submit encrypted inputs** — your frontend encrypts under the threshold public key and submits +4. **Wait for computation** — the compute provider runs the FHE program +5. **Check the result** — the committee decrypts and publishes the plaintext + +Monitor progress by watching for events: + +```typescript +sdk.onEnclaveEvent(EnclaveEventType.E3_REQUESTED, (e) => console.log('E3 requested')) +sdk.onEnclaveEvent(RegistryEventType.COMMITTEE_PUBLISHED, (e) => console.log('Committee ready')) +sdk.onEnclaveEvent(EnclaveEventType.CIPHERTEXT_OUTPUT_PUBLISHED, (e) => console.log('Output ready')) +sdk.onEnclaveEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, (e) => + console.log('Result:', e.data), +) +``` + +--- + +## Troubleshooting + +| Issue | Possible cause | Fix | +| --------------------------- | ------------------------------------------- | -------------------------------------------------------------------------- | +| E3 request reverts | Insufficient fee token approval | Call `sdk.approveFeeToken()` first | +| Committee never forms | Not enough active ciphernodes on Sepolia | Check the registry for active operators | +| Proof generation fails | Missing circuit artifacts for secure preset | Ensure `secure-8192` artifacts are built | +| Decryption produces garbage | BFV preset mismatch | Verify both SDK and E3 request use `SECURE_THRESHOLD_8192` / `paramSet: 1` | + +--- + +## Next Steps + +- [Manage Tickets](./manage-tickets) — if you're also running a ciphernode on Sepolia +- [Troubleshoot Your Node](./operator-troubleshooting) — debugging ciphernode issues on testnet diff --git a/docs/pages/tutorials/encrypt-and-submit.mdx b/docs/pages/tutorials/encrypt-and-submit.mdx new file mode 100644 index 0000000000..8f16f4bb62 --- /dev/null +++ b/docs/pages/tutorials/encrypt-and-submit.mdx @@ -0,0 +1,232 @@ +--- +title: 'Encrypt Data & Submit to an E3' +description: + 'Client-side flow for encrypting user inputs with FHE and submitting them to an E3 — BFV presets, + key generation, encryption, and result decoding' +--- + +# Encrypt Data & Submit to an E3 + +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. + +> **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. +> +> **Time:** ~15 minutes +> +> **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). + +--- + +## Where to find contract addresses + +The SDK needs three contract addresses: `enclave`, `ciphernodeRegistry`, and `feeToken`. + +| Environment | Where to get them | +| ----------- | ----------------------------------------------------------------------------------------------------- | +| Local dev | Printed by the deploy script; also in `packages/enclave-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 | + +Put them in your `.env` and read them from there rather than hard-coding in source: + +```bash +ENCLAVE_ADDRESS=0x... +CIPHERNODE_REGISTRY_ADDRESS=0x... +FEE_TOKEN_ADDRESS=0x... +``` + +--- + +## 1. Choose Your BFV Preset + +The BFV preset determines the encryption parameters. It must match the `paramSet` index used when +the E3 was requested: + +| Preset name | `paramSet` index | When to use | +| ------------------------ | ---------------- | -------------------------------------------- | +| `INSECURE_THRESHOLD_512` | `0` | Local development only — fast but not secure | +| `SECURE_THRESHOLD_8192` | `1` | Production — full security guarantees | + +```typescript +import { EnclaveSDK } from '@enclave-e3/sdk' + +const sdk = new EnclaveSDK({ + publicClient, + walletClient, + contracts: { + enclave: ENCLAVE_ADDRESS, + ciphernodeRegistry: REGISTRY_ADDRESS, + feeToken: FEE_TOKEN_ADDRESS, + }, + chain: sepolia, + // Must match the paramSet used when the E3 was requested + thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192', +}) +``` + +If you initialise the SDK with `INSECURE_THRESHOLD_512` but the E3 was requested with `paramSet: 1` +(secure), the ciphertext will be encrypted under the wrong parameters and decryption will fail +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) +``` + +The public key is only available after the committee is finalised. If you call this too early, it +will return `null`. + +--- + +## 3. Encrypt Your Data + +The SDK provides several encryption functions depending on your data type: + +### 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: + +```typescript +// Encrypt and generate a ZK proof that the encryption is valid +const { encryptedData, proof } = await sdk.encryptNumberAndGenProof(42n, publicKey) +``` + +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 '@enclave-e3/sdk/crypto' + +const pk = await generatePublicKey('SECURE_THRESHOLD_8192') +const ct = await encryptNumber(42n, pk, 'SECURE_THRESHOLD_8192') +``` + +--- + +## 4. Submit On-chain + +Submit the encrypted data to the E3 via the Enclave contract's `publishInput` function. The exact +method depends on your application architecture: + +```typescript +// Direct contract call via viem +const hash = await walletClient.writeContract({ + address: ENCLAVE_ADDRESS, + abi: enclaveAbi, + functionName: 'publishInput', + args: [e3Id, encodedData], +}) +const receipt = await publicClient.waitForTransactionReceipt({ hash }) +``` + +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. + +--- + +## 5. Listen for the Result + +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: + +```typescript +import { EnclaveEventType } from '@enclave-e3/sdk' + +// Listen for the decrypted result +sdk.onEnclaveEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, (event) => { + console.log('Result:', event.data) +}) + +// Or poll for it +const e3 = await sdk.getE3(e3Id) +if (e3.plaintextOutput) { + console.log('Decrypted result:', e3.plaintextOutput) +} +``` + +### 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: + +```typescript +import { decodeTally } from '@crisp-e3/sdk' + +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. + +--- + +## Browser Setup + +If using the SDK in a browser with Vite, you need to configure WASM support: + +```typescript +// vite.config.ts +import wasm from 'vite-plugin-wasm' +import topLevelAwait from 'vite-plugin-top-level-await' + +export default defineConfig({ + optimizeDeps: { + exclude: ['@enclave-e3/wasm'], + }, + plugins: [wasm(), topLevelAwait()], +}) +``` + +This is required because the SDK uses WASM for BFV encryption operations. + +--- + +## Common Pitfalls + +| 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 | + +--- + +## Next Steps + +- [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 diff --git a/docs/pages/tutorials/manage-tickets.mdx b/docs/pages/tutorials/manage-tickets.mdx new file mode 100644 index 0000000000..a723c6a57d --- /dev/null +++ b/docs/pages/tutorials/manage-tickets.mdx @@ -0,0 +1,153 @@ +--- +title: 'Manage Tickets & Optimise Selection' +description: + 'How to buy, burn, and manage your ciphernode tickets for optimal committee selection probability' +--- + +# Manage Tickets & Optimise Selection + +Tickets determine how often your ciphernode is selected for E3 committees. This tutorial covers the +practical aspects of managing your ticket balance. + +> **What you'll learn:** How ticket balances map to selection probability, how to buy and burn +> tickets from the CLI, the snapshot rule that decides which tickets count for a given E3 request, +> and a simple strategy for keeping your node eligible. +> +> **Time:** ~10 minutes +> +> **Prerequisites:** A registered ciphernode with a license bond (see +> [Registration](/ciphernode-operators/registration)). + +--- + +## How Tickets Affect Selection + +Each ticket gives you one entry in the sortition lottery. For each E3 request, your node computes a +score for every ticket it holds: + +```text +score = keccak256(your_address, ticket_number, e3Id, seed) +``` + +Only your **best** (lowest) score is submitted. More tickets means more draws from the lottery, +which means a better best score on average. + +**Selection probability scales linearly.** If the network has 1000 total tickets and you hold 100, +you'll be selected roughly 10% of the time (assuming no buffer effects). + +--- + +## Buying Tickets + +Deposit stablecoins (USDC or the configured fee token) to mint ticket tokens (ETK): + +```bash +enclave ciphernode tickets buy --amount 100 +``` + +This: + +1. Approves the ticket contract to spend your stablecoins (if needed) +2. Deposits the stablecoins +3. Mints equivalent ETK to your balance + +Your available ticket count is: + +```text +availableTickets = floor(etkBalance / ticketPrice) +``` + +Check `ticketPrice` with `enclave ciphernode status` — it's set by governance and may change. + +--- + +## Burning Tickets + +Withdraw the underlying stablecoins by burning tickets: + +```bash +enclave ciphernode tickets burn --amount 50 +``` + +Burning takes effect **immediately**. If it drops you below `minTicketBalance`, your node becomes +inactive and won't be selected for any further committees until you top up. + +--- + +## The Snapshot Rule + +Balances are snapshotted at **`requestBlock - 1`** — the block immediately before each E3 request is +included. This means: + +- Tickets added **before** the request block count for that round +- Tickets added **in the same block or later** do not count — they apply to future rounds +- You cannot front-run a request by depositing tickets at the last moment + +**Practical tip:** Add tickets well ahead of anticipated demand. Don't wait for a specific E3 +request. + +--- + +## Minimum Balance + +Your node must hold at least `minTicketBalance` tickets to be active. If your balance drops below +this (e.g. after burning or after a slashing penalty), your node transitions to **Inactive** and +stops being eligible for selection. + +Check the current minimum: + +```bash +enclave ciphernode status +``` + +--- + +## Strategy Tips + +### Keep a buffer + +Maintain a balance above the minimum — slashing deductions or governance changes to `ticketPrice` +could temporarily drop you below the threshold. A buffer of 2-3x the minimum is reasonable. + +### Add tickets before demand spikes + +If you know a high-value E3 is coming (or you see increased request frequency), add tickets one or +two requests ahead so they're captured in the snapshot. + +### Monitor the economics + +| Parameter | What it means | How to check | +| --------------------- | ----------------------------- | --------------------------- | +| `ticketPrice` | Stablecoins per ticket | `enclave ciphernode status` | +| `minTicketBalance` | Minimum to stay active | `enclave ciphernode status` | +| `availableTickets` | Your current ticket count | `enclave ciphernode status` | +| Total network tickets | Sum of all operators' tickets | On-chain query (registry) | + +### One network at a time + +Run separate CLI instances per chain. Ticket balances are per-chain — buying tickets on Sepolia +doesn't affect your Mainnet balance. + +--- + +## What Happens When You're Selected + +When the sortition algorithm picks your node: + +1. Your node auto-submits its winning ticket on-chain +2. After enough tickets are submitted, the committee is finalised +3. Your node participates in DKG → computation → decryption +4. On successful completion, you earn rewards + +If you miss the submission window (10s on Sepolia), your ticket isn't counted and another node takes +the slot. See [Troubleshooting](./operator-troubleshooting) for common causes. + +--- + +## Related + +- [Tickets & Sortition](/ciphernode-operators/tickets-and-sortition) — full reference on the ticket + system +- [Sortition Internals](/internals/sortition) — technical deep dive on the scoring algorithm +- [Exits & Slashing](/ciphernode-operators/exits-and-slashing) — what happens if something goes + wrong diff --git a/docs/pages/tutorials/operator-troubleshooting.mdx b/docs/pages/tutorials/operator-troubleshooting.mdx new file mode 100644 index 0000000000..11e71933af --- /dev/null +++ b/docs/pages/tutorials/operator-troubleshooting.mdx @@ -0,0 +1,231 @@ +--- +title: 'Troubleshoot Common Ciphernode Issues' +description: + 'Diagnose and fix the most common problems ciphernode operators encounter — selection failures, + connectivity, syncing, and key log messages' +--- + +# Troubleshoot Common Ciphernode Issues + +This guide covers the most frequent problems operators encounter and how to resolve them. + +> **What you'll learn:** How to diagnose a node that isn't being selected, isn't connecting to +> peers, isn't syncing, or is missing its submission window — and how to read the key log messages +> the node emits at each stage. +> +> **Time:** ~10–20 minutes depending on which section applies +> +> **Prerequisites:** A registered and running ciphernode. If you haven't set one up yet, see +> [Running a Ciphernode](/ciphernode-operators/running). + +--- + +## Not Being Selected for Committees + +**Symptom:** Your node logs `"This node was NOT selected for sortition"` on every E3 request. + +### Check your status + +```bash +enclave ciphernode status +``` + +Look for: + +| Field | Expected | Problem if | +| ---------------- | ----------------------- | ------------------------------------------- | +| `isActive` | `true` | `false` — you're not eligible for selection | +| `Ticket balance` | ≥ `minTicketBalance` | Below minimum — buy more tickets | +| `License bond` | ≥ `licenseRequiredBond` | Below minimum — top up your bond | +| `Exit pending` | `false` | `true` — you've requested deregistration | +| `Banned` | `false` | `true` — you've been slashed and banned | + +### Common causes + +**Low ticket count.** Selection probability scales linearly with tickets. If the network has 1000 +total tickets and you hold 10, you'll be selected ~1% of the time. Add more tickets: + +```bash +enclave ciphernode tickets buy --amount 100 +``` + +**Bond dropped below minimum.** If you were slashed, your bond may have fallen below +`licenseRequiredBond`. Top it up: + +```bash +enclave ciphernode license bond --amount 100 +``` + +**Inactive status.** Both bond AND tickets must be above their minimums for `isActive` to be `true`. +Check both. + +**Balance snapshot timing.** Balances are snapshotted at `requestBlock - 1`. If you added tickets in +the same block as the E3 request, they won't count for that round. They'll apply to the next one. + +--- + +## Missed Ticket Submissions + +**Symptom:** You were selected in the local sortition but the on-chain submission didn't go through. +Look for `"SubmissionWindowClosed"` errors or missing `TicketSubmitted` events. + +### Common causes + +**RPC latency.** The sortition submission window is short (10 seconds on Sepolia). If your RPC +endpoint is slow, the transaction may not be mined in time. + +**Fix:** Use a faster RPC endpoint, preferably WebSocket. Avoid free-tier endpoints that throttle +under load. + +**Nonce conflicts.** If your node is submitting other transactions simultaneously, nonce conflicts +can delay the ticket submission. + +**Gas price too low.** During network congestion, your transaction may sit in the mempool past the +deadline. + +--- + +## Peer Connectivity Issues + +**Symptom:** The node starts but doesn't receive events from peers. Look for: + +- `"No peers connected"` or low peer count +- Missing `EncryptionKeyReceived` events from other committee members + +### Check your networking + +The Interfold uses **QUIC** (UDP-based) for P2P networking. + +1. **Firewall**: Ensure your QUIC port (default from config) is open for **UDP** traffic, not just + TCP +2. **NAT**: If behind NAT, configure port forwarding for the QUIC port +3. **Cloud firewalls**: AWS Security Groups, GCP firewall rules, etc. must allow UDP inbound on your + QUIC port + +### Verify connectivity + +Check your node logs for: + +- `"Listening on /ip4/0.0.0.0/udp//quic-v1"` — confirms the listener started +- `"Peer connected: "` — confirms at least one peer is reachable + +If no peers connect, verify that your `quic_port` in `enclave.config.yaml` matches what's open in +your firewall. + +--- + +## Node Not Syncing + +**Symptom:** The node is running but missing on-chain events. E3 requests appear on-chain but your +node doesn't react. + +### Check your RPC endpoint + +**WebSocket required.** The node uses WebSocket subscriptions for real-time event listening. If your +RPC URL starts with `https://` instead of `wss://`, the node will fall back to polling which can +miss events. + +**Fix:** Use a WebSocket RPC endpoint: + +```yaml +# enclave.config.yaml +chains: + - name: sepolia + rpc_url: wss://eth-sepolia.g.alchemy.com/v2/YOUR_KEY +``` + +**Rate limiting.** Free-tier RPC endpoints may throttle your requests. If you see HTTP 429 errors in +logs, upgrade your RPC plan or switch providers. + +**Block lag.** Check that your RPC is returning recent blocks. Compare the block number in your logs +to a block explorer. + +--- + +## Reading the Logs + +### Verbosity + +The CLI uses `-v` flags, not `RUST_LOG`. Each additional `v` goes one level deeper: + +| Flag | Level | When to use | +| ------------- | ------- | --------------------------------------------------------------- | +| (default) | `WARN` | Normal operation | +| `-v` | `INFO` | Standard operator monitoring | +| `-vv` | `DEBUG` | Diagnosing a specific issue (protocol messages, state) | +| `-vvv` | `TRACE` | Deep diagnostics (extremely verbose — don't run long with this) | +| `-q, --quiet` | `ERROR` | Only log errors | + +```bash +enclave start -vv +``` + +### Log file location + +To find the current log file path: + +```bash +enclave config print log_file +``` + +The default lives under your platform's state directory (e.g. `~/.local/state/enclave/` on Linux, +`~/Library/Application Support/enclave/` on macOS). Tail it while reproducing an issue: + +```bash +tail -f "$(enclave config print log_file)" +``` + +--- + +## System Resources + +A mainnet-grade ciphernode should have: + +- **CPU:** 4+ modern cores. DKG and ZK proving are CPU-heavy. +- **Memory:** 8 GB minimum, 16 GB recommended. Proof generation during DKG peaks well above idle. +- **Disk:** 20 GB for the node data directory, more if you retain historical events. Use SSD — + spinning disks will miss submission windows under load. +- **Network:** Stable residential or cloud link. UDP must be unfiltered for QUIC (see below). + +If your node starts OOM-killing during DKG, that's a memory problem, not a bug — add RAM or move to +a bigger instance. + +--- + +## Key Log Messages + +| Log message | Meaning | Action | +| -------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------- | +| `"This node was SELECTED for sortition"` | You have a winning ticket for an E3 | Normal — node will auto-submit | +| `"This node was NOT selected for sortition"` | Your tickets didn't score low enough | Normal — try more tickets for higher odds | +| `"Performing Sortition with buffer"` | Sortition started (shows threshold_m, threshold_n, buffer) | Informational | +| `"Node is in finalized committee"` | Committee confirmed, DKG starting | Normal — node is participating | +| `"Ciphernode was not selected"` | Node wasn't in the final selection set | Normal | +| `"Ticket generated for score sortition"` | Winning ticket emitted to event bus | Normal | +| `"SubmissionWindowClosed"` | Transaction didn't land before the deadline | Check RPC latency | +| `"Failed to verify proof"` | A peer's ZK proof failed verification | The peer may be slashed — your node is working correctly | +| `"CommitmentConsistencyViolation"` | A peer's cross-circuit commitments don't match | The peer may be accused — your node detected dishonesty | + +--- + +## Dashboard + +For real-time monitoring, enable the [node dashboard](./using-the-dashboard): + +```yaml +# enclave.config.yaml +node: + dashboard_port: 8080 +``` + +Then open `http://localhost:8080` to see live events, filter by E3 ID or severity, and inspect error +details. + +--- + +## Still Stuck? + +- Check the [Tickets & Sortition](/ciphernode-operators/tickets-and-sortition) reference for + parameter details +- Join the community [Telegram group](https://t.me/encaborations) for help +- If you suspect a bug, open an issue on [GitHub](https://github.com/gnosisguild/enclave/issues) diff --git a/docs/pages/tutorials/using-the-dashboard.mdx b/docs/pages/tutorials/using-the-dashboard.mdx new file mode 100644 index 0000000000..7c6eb4b295 --- /dev/null +++ b/docs/pages/tutorials/using-the-dashboard.mdx @@ -0,0 +1,175 @@ +--- +title: 'Monitor Your Node with the Dashboard' +description: + 'Enable and use the embedded ciphernode dashboard for real-time monitoring — events, status, + filtering, and troubleshooting' +--- + +# Monitor Your Node with the Dashboard + +Every ciphernode ships with an embedded web dashboard for real-time monitoring. It runs as a +lightweight HTTP server alongside your node — no external dependencies, no separate installation. + +> **What you'll learn:** How to enable the dashboard, what each tab shows, how to use the event +> filters to trace a specific E3 or spot errors, and how to reach the dashboard securely from a +> remote machine. +> +> **Time:** ~10 minutes +> +> **Prerequisites:** A running ciphernode. If you haven't set one up, see +> [Running a Ciphernode](/ciphernode-operators/running). + +--- + +## Enabling the Dashboard + +Add `dashboard_port` to your node configuration: + +```yaml +# enclave.config.yaml +node: + dashboard_port: 8080 +``` + +Or set it via environment variable: + +```bash +E3_DASHBOARD_PORT=8080 +``` + +Restart your node. You'll see: + +```text +Dashboard available at http://0.0.0.0:8080 +``` + +Open `http://localhost:8080` (or `http://:8080` for remote access) in your browser. + +> **Security note:** The dashboard has no authentication and binds to all interfaces (`0.0.0.0`). If +> your node is on a public server, restrict access via firewall rules or an SSH tunnel. + +--- + +## Overview Tab + +The first tab shows your node's identity and configuration: + +- **Node name** — the name from your config +- **Ethereum address** — your operator address +- **Peer ID** — your libp2p identity on the P2P network +- **QUIC port** — the UDP port used for peer-to-peer communication +- **Ctrl port** — the local JSON-RPC port for CLI commands +- **Noir prover status** — whether the ZK prover is ready and which circuits are loaded + +Use this tab to verify your node started correctly and has the right identity. + +--- + +## Events Tab + +The events tab is where you'll spend most of your time. It shows a real-time stream of everything +happening on your node. + +### Event Stream + +Each row shows: + +| Column | Content | +| ------------- | -------------------------------------------------------------- | +| **Sequence** | Hybrid Logical Clock sequence number (ordering) | +| **Timestamp** | When the event occurred | +| **Type** | Event name (e.g. `E3Requested`, `CiphernodeSelected`) | +| **E3 ID** | Which E3 the event relates to | +| **Source** | Where the event came from: Local, Net (P2P), or Evm (on-chain) | + +Click any row to expand the **full JSON payload** — useful for debugging. + +### Filtering + +The events tab has powerful filtering to help you focus: + +- **Text filter by event type** — type `Selected` to show only selection events +- **Text filter by E3 ID** — isolate all events for a specific computation +- **Source filter** — show only Local / Net / Evm events +- **Severity filter**: + - **All** — everything + - **Errors** — `EnclaveError`, `E3Failed`, `ProofVerificationFailed`, `SignedProofFailed`, + `ThresholdShareCollectionFailed`, `EncryptionKeyCollectionFailed` + - **Warnings** — `AccusationVote`, `ProofFailureAccusation`, `CommitmentMismatch` + - **State changes** — `E3Requested`, `CiphernodeSelected`, `CommitteePublished`, + `CommitteeFinalized`, `CiphertextOutputPublished`, `PlaintextOutputPublished` + +Events are color-coded: red for errors, yellow for warnings, green for state changes. + +### Auto-Refresh + +Toggle the auto-refresh checkbox to poll for new events every 10 seconds. The event counter shows +how many events are loaded vs. how many match your current filters. + +--- + +## Status Tab + +Shows your node's operational state: + +- **Ciphernode status** — active/inactive, current committees, pending exits +- **Wallet** — ETH balance, ENCL bond, ticket token balance + +Use this to quickly check whether your node is in a healthy state. + +--- + +## Practical Patterns + +### Tracing an E3 Lifecycle + +1. Filter by E3 ID (e.g. `42`) +2. You'll see the full sequence: `E3Requested` → `CiphernodeSelected` (if you were picked) → + `EncryptionKeyCreated` → `ThresholdShareCreated` → `CommitteePublished` → inputs → + `CiphertextOutputPublished` → `PlaintextOutputPublished` +3. If the sequence stops at a specific point, that's where the problem is + +### Spotting Errors + +1. Set severity filter to **Errors** +2. Look for red events — these indicate something went wrong +3. Expand the event to see the error details (proof type, accused party, error message) + +### After an Upgrade + +1. Restart your node with the new version +2. Open the dashboard and check the **Overview** tab — verify Noir prover status shows the correct + circuits +3. Switch to **Events** and watch for normal activity (`E3Requested`, sortition events) +4. Set severity to **Errors** and check nothing new is failing + +### Checking Prover Health + +The Overview tab shows the Noir prover status. If the prover fails to initialise (e.g. missing +circuit artifacts), you'll see an error here before it affects any E3 participation. + +--- + +## Remote Access + +If your node runs on a remote server, you have two options: + +### SSH Tunnel (recommended) + +```bash +ssh -L 8080:localhost:8080 user@your-server +``` + +Then open `http://localhost:8080` locally. This keeps the dashboard private. + +### Direct Access + +If you open the port in your firewall, the dashboard is accessible at `http://:8080`. +Since there's no authentication, only do this on trusted networks. + +--- + +## Related + +- [Running a Ciphernode](/ciphernode-operators/running) — setup and deployment methods +- [Troubleshoot Your Node](./operator-troubleshooting) — common issues and fixes diff --git a/docs/pages/tutorials/write-e3-program.mdx b/docs/pages/tutorials/write-e3-program.mdx new file mode 100644 index 0000000000..65886257a3 --- /dev/null +++ b/docs/pages/tutorials/write-e3-program.mdx @@ -0,0 +1,278 @@ +--- +title: 'Build a Complete E3 Program' +description: + 'Walk through CRISP to understand how each component of an E3 program works — contracts, FHE + computation, and verification' +--- + +# Build a Complete E3 Program + +This tutorial walks through [CRISP](/CRISP/introduction) (Coercion-Resistant Impartial Selection +Protocol), a private voting application built on the Interfold, to show how each component of an E3 +program fits together. By the end, you'll understand the contract interface, FHE computation, and +verification flow well enough to build your own. + +> **What you'll learn:** How the three `IE3Program` entry points fit together, what the FHE +> computation looks like inside a RISC Zero guest, and how the final proof is verified on-chain. +> +> **Time:** ~20 minutes (reading) +> +> **Prerequisites:** Familiarity with Solidity and a basic understanding of +> [what an E3 is](/what-is-e3). + +--- + +## The IE3Program Interface + +Every E3 program implements the `IE3Program` interface from `@enclave-e3/contracts`. It has three +functions that the Enclave contract calls at different stages of the E3 lifecycle: + +```solidity +interface IE3Program { + function validate( + uint256 e3Id, + uint256 seed, + bytes calldata e3ProgramParams, + bytes calldata computeProviderParams, + bytes calldata customParams + ) external returns (bytes32 encryptionSchemeId); + + function publishInput(uint256 e3Id, bytes memory data) external; + + function verify( + uint256 e3Id, + bytes32 ciphertextOutputHash, + bytes memory proof + ) external returns (bool success); +} +``` + +| Function | When called | Purpose | +| -------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `validate` | When someone requests a new E3 | Validate parameters and initialise round state. Returns the encryption scheme ID (e.g. `keccak256("fhe.rs:BFV")`) | +| `publishInput` | Each time a user submits encrypted data | Validate the input (e.g. check a ZK proof, verify eligibility) and store it | +| `verify` | When the compute provider publishes the result | Verify the computation proof and confirm the ciphertext output is correct | + +--- + +## Step 1: validate — Initialise the Round + +When a new E3 is requested, the Enclave contract calls `validate` on your program. This is where you +parse parameters, set up state, and decide whether the request is valid. + +CRISP's implementation (`CRISPProgram.sol`): + +```solidity +function validate( + uint256 e3Id, + uint256, + bytes calldata e3ProgramParams, + bytes calldata, + bytes calldata customParams +) external returns (bytes32) { + if (msg.sender != address(enclave) && msg.sender != owner()) revert CallerNotAuthorized(); + if (e3Data[e3Id].paramsHash != bytes32(0)) revert E3AlreadyInitialized(); + + // Decode voting parameters from customParams + (, , uint256 numOptions, CreditMode creditMode, ) = abi.decode( + customParams, + (address, uint256, uint256, CreditMode, uint256) + ); + + // Store round configuration + e3Data[e3Id].numOptions = numOptions; + e3Data[e3Id].creditMode = creditMode; + e3Data[e3Id].paramsHash = keccak256(e3ProgramParams); + + // Initialise the Merkle tree that will track vote commitments + e3Data[e3Id].votes._init(TREE_DEPTH); + + return ENCRYPTION_SCHEME_ID; // keccak256("fhe.rs:BFV") +} +``` + +Key points: + +- Only the Enclave contract (or the program owner) can call `validate` +- `customParams` carries application-specific configuration — CRISP uses it for voting options and + credit mode +- The function returns an encryption scheme identifier so the Enclave contract knows which scheme + the ciphernodes should use + +--- + +## Step 2: publishInput — Validate User Inputs + +Each time a user submits encrypted data to the E3, the Enclave contract calls `publishInput`. This +is where you enforce application-specific rules. + +CRISP's `publishInput` does several things: + +1. **Checks the E3 stage** — only accepts inputs after the committee's public key is published +2. **Checks the input window** — rejects submissions before `inputWindow[0]` or after + `inputWindow[1]` +3. **Decodes the payload** — unpacks the Noir proof, slot address, commitment, and ciphertext +4. **Processes the vote slot** — inserts or overrides the voter's entry in the Merkle tree +5. **Verifies a ZK proof** — calls an on-chain Honk verifier to prove the vote was encrypted + correctly and the voter is eligible (see [Custom Noir Circuits](./custom-zk-circuits)) + +```solidity +function publishInput(uint256 e3Id, bytes memory data) external { + E3 memory e3 = enclave.getE3(e3Id); + + // Stage and window checks + if (enclave.getE3Stage(e3Id) != IEnclave.E3Stage.KeyPublished) revert KeyNotPublished(e3Id); + if (block.timestamp > e3.inputWindow[1]) revert InputDeadlinePassed(e3Id, e3.inputWindow[1]); + if (block.timestamp < e3.inputWindow[0]) revert E3NotAcceptingInputs(e3Id); + if (e3Data[e3Id].merkleRoot == 0) revert MerkleRootNotSet(); + if (data.length == 0) revert EmptyInputData(); + + ( + bytes memory noirProof, + address slotAddress, + bytes32 encryptedVoteCommitment, + bytes memory encryptedVote + ) = abi.decode(data, (bytes, address, bytes32, bytes)); + + (uint40 voteIndex, bytes32 previousCommitment) = _processVote( + e3Id, + slotAddress, + encryptedVoteCommitment + ); + + // Build the Noir public inputs (order must match the circuit) + bytes32[] memory publicInputs = new bytes32[](7); + publicInputs[0] = previousCommitment; + publicInputs[1] = bytes32(e3Data[e3Id].merkleRoot); + publicInputs[2] = bytes32(uint256(uint160(slotAddress))); + publicInputs[3] = bytes32(uint256(previousCommitment == bytes32(0) ? 1 : 0)); + publicInputs[4] = bytes32(e3Data[e3Id].numOptions); + publicInputs[5] = encryptedVoteCommitment; + publicInputs[6] = e3.committeePublicKey; + + if (!honkVerifier.verify(noirProof, publicInputs)) revert InvalidNoirProof(); + + emit InputPublished(e3Id, encryptedVote, voteIndex); +} +``` + +> Only the Enclave contract ever calls `publishInput` — the Enclave coordinator enforces that at the +> protocol level, so your program doesn't need an explicit `msg.sender` check here. + +Every E3 program should verify that the submitted ciphertext is correctly encrypted under the +committee's threshold public key. Beyond that baseline, your `publishInput` can add +application-specific checks — CRISP adds eligibility (Merkle membership), vote validity (balance +checks), and signature verification. Simpler programs may only need the encryption validity check. + +--- + +## Step 3: The FHE Computation + +After the input window closes, the compute provider runs the E3 program over the collected encrypted +inputs. For CRISP, this is a RISC Zero guest that sums all encrypted votes homomorphically: + +```rust +pub fn fhe_processor(fhe_inputs: &FHEInputs) -> Vec { + let params = decode_bfv_params_arc(&fhe_inputs.params).unwrap(); + + // Start with a zero ciphertext + let mut sum = Ciphertext::zero(¶ms); + + // Homomorphically add every encrypted vote — no decryption needed + for ciphertext_bytes in &fhe_inputs.ciphertexts { + let ciphertext = Ciphertext::from_bytes(&ciphertext_bytes.0, ¶ms).unwrap(); + sum += &ciphertext; + } + + // Return the encrypted tally + sum.to_bytes() +} +``` + +This is the core of what makes FHE powerful: the compute provider processes encrypted data without +ever seeing the plaintext. The `+=` operator performs homomorphic addition — when the ciphernode +committee later decrypts the sum, the result is the sum of all the original votes. + +The RISC Zero guest generates a ZK proof that this computation was performed correctly. In +development, set `RISC0_DEV_MODE=1` to skip real proof generation. + +--- + +## Step 4: verify — Confirm the Result + +After the compute provider publishes its result, the Enclave contract calls `verify` on your +program. CRISP's implementation checks the RISC Zero proof: + +```solidity +function verify( + uint256 e3Id, + bytes32 ciphertextOutputHash, + bytes memory proof +) external returns (bool) { + if (msg.sender != address(enclave)) revert CallerNotAuthorized(); + + // Reconstruct the expected journal from known values + bytes memory journal = abi.encodePacked( + ciphertextOutputHash, + e3Data[e3Id].paramsHash, + e3Data[e3Id].votes._root(TREE_DEPTH) + ); + + // Verify the RISC Zero proof + risc0Verifier.verify(proof, imageId, sha256(journal)); + + return true; +} +``` + +The journal reconstruction is critical — it binds the proof to the specific inputs (via the Merkle +root) and parameters (via the params hash) for this E3. A proof generated for different inputs or +parameters would fail verification. + +--- + +## Putting It Together + +The full lifecycle for a CRISP voting round: + +```mermaid +sequenceDiagram + participant U as User + participant E as Enclave contract + participant P as CRISPProgram + participant C as Ciphernode committee + participant CP as Compute provider + + U->>E: requestE3(params) + E->>P: validate(e3Id, params) + Note over P: store config,
init Merkle tree + C->>C: sortition → DKG + C->>E: publish threshold public key + U->>E: publishInput(e3Id, proof + ct) + E->>P: publishInput(e3Id, data) + Note over P: verify Noir proof,
insert into Merkle tree + Note over CP: input window closes + CP->>CP: fhe_processor()
homomorphic sum + RISC Zero proof + CP->>E: publishCiphertextOutput + E->>P: verify(e3Id, ctHash, proof) + C->>E: threshold-decrypt result + Note over E: plaintext tally on-chain +``` + +--- + +## Building Your Own + +To create a new E3 program: + +1. **Implement `IE3Program`** — start from the + [MockE3Program](https://github.com/gnosisguild/enclave/blob/main/packages/enclave-contracts/contracts/test/MockE3Program.sol) + for a minimal example, or study `CRISPProgram.sol` for a full implementation +2. **Write your FHE computation** — implement the `fhe_processor` function for your use case + (aggregation, comparison, etc.) +3. **Choose your validation strategy** — decide what `publishInput` should check (ZK proofs, token + balances, allow-lists, or nothing) +4. **Set up verification** — implement `verify` to check the compute provider's proof + +See [Writing the E3 Program Contract](/write-e3-contract) and +[Writing the Secure Process](/write-secure-program) for more details on each step. From ab221cadfee5dfe792d069b8f8720b57e45b1f94 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:58:40 +0100 Subject: [PATCH 2/4] docs: update tutorials --- docs/pages/ciphernode-operators/index.mdx | 2 +- docs/pages/tutorials/manage-tickets.mdx | 7 ------- docs/pages/tutorials/operator-troubleshooting.mdx | 9 ++++----- docs/pages/tutorials/write-e3-program.mdx | 6 ++---- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/docs/pages/ciphernode-operators/index.mdx b/docs/pages/ciphernode-operators/index.mdx index a59bb84b53..c6ab119885 100644 --- a/docs/pages/ciphernode-operators/index.mdx +++ b/docs/pages/ciphernode-operators/index.mdx @@ -102,7 +102,7 @@ Before operating a ciphernode, ensure you have: | **ENCL Tokens** | At least `100 ENCL` for the license bond (check `licenseRequiredBond()`) | | **Stablecoin** | USDC (or configured fee token) for tickets; minimum 1 ticket worth | | **ETH** | Gas for transactions on your target network | -| **Hardware** | Linux/macOS, 4+ cores, 8GB+ RAM, stable internet with open UDP port | +| **Hardware** | Linux/macOS, 8+ cores, 16GB+ RAM, stable internet with open UDP port | | **Software** | Interfold CLI installed, WebSocket RPC endpoint | ## Getting Started diff --git a/docs/pages/tutorials/manage-tickets.mdx b/docs/pages/tutorials/manage-tickets.mdx index a723c6a57d..0efd0dcf74 100644 --- a/docs/pages/tutorials/manage-tickets.mdx +++ b/docs/pages/tutorials/manage-tickets.mdx @@ -123,13 +123,6 @@ two requests ahead so they're captured in the snapshot. | `availableTickets` | Your current ticket count | `enclave ciphernode status` | | Total network tickets | Sum of all operators' tickets | On-chain query (registry) | -### One network at a time - -Run separate CLI instances per chain. Ticket balances are per-chain — buying tickets on Sepolia -doesn't affect your Mainnet balance. - ---- - ## What Happens When You're Selected When the sortition algorithm picks your node: diff --git a/docs/pages/tutorials/operator-troubleshooting.mdx b/docs/pages/tutorials/operator-troubleshooting.mdx index 11e71933af..593f28f080 100644 --- a/docs/pages/tutorials/operator-troubleshooting.mdx +++ b/docs/pages/tutorials/operator-troubleshooting.mdx @@ -74,8 +74,7 @@ Look for `"SubmissionWindowClosed"` errors or missing `TicketSubmitted` events. **RPC latency.** The sortition submission window is short (10 seconds on Sepolia). If your RPC endpoint is slow, the transaction may not be mined in time. -**Fix:** Use a faster RPC endpoint, preferably WebSocket. Avoid free-tier endpoints that throttle -under load. +**Fix:** Use a faster RPC endpoint. Avoid free-tier endpoints that throttle under load. **Nonce conflicts.** If your node is submitting other transactions simultaneously, nonce conflicts can delay the ticket submission. @@ -182,8 +181,8 @@ tail -f "$(enclave config print log_file)" A mainnet-grade ciphernode should have: - **CPU:** 4+ modern cores. DKG and ZK proving are CPU-heavy. -- **Memory:** 8 GB minimum, 16 GB recommended. Proof generation during DKG peaks well above idle. -- **Disk:** 20 GB for the node data directory, more if you retain historical events. Use SSD — +- **Memory:** 16 GB minimum, 32 GB recommended. Proof generation during DKG peaks well above idle. +- **Disk:** 30 GB for the node data directory, more if you retain historical events. Use SSD — spinning disks will miss submission windows under load. - **Network:** Stable residential or cloud link. UDP must be unfiltered for QUIC (see below). @@ -227,5 +226,5 @@ details. - Check the [Tickets & Sortition](/ciphernode-operators/tickets-and-sortition) reference for parameter details -- Join the community [Telegram group](https://t.me/encaborations) for help +- Join the community [Telegram group](https://t.me/enclave_e3) for help - If you suspect a bug, open an issue on [GitHub](https://github.com/gnosisguild/enclave/issues) diff --git a/docs/pages/tutorials/write-e3-program.mdx b/docs/pages/tutorials/write-e3-program.mdx index 65886257a3..fe93eaab36 100644 --- a/docs/pages/tutorials/write-e3-program.mdx +++ b/docs/pages/tutorials/write-e3-program.mdx @@ -156,9 +156,6 @@ function publishInput(uint256 e3Id, bytes memory data) external { } ``` -> Only the Enclave contract ever calls `publishInput` — the Enclave coordinator enforces that at the -> protocol level, so your program doesn't need an explicit `msg.sender` check here. - Every E3 program should verify that the submitted ciphertext is correctly encrypted under the committee's threshold public key. Beyond that baseline, your `publishInput` can add application-specific checks — CRISP adds eligibility (Merkle membership), vote validity (balance @@ -194,7 +191,8 @@ ever seeing the plaintext. The `+=` operator performs homomorphic addition — w committee later decrypts the sum, the result is the sum of all the original votes. The RISC Zero guest generates a ZK proof that this computation was performed correctly. In -development, set `RISC0_DEV_MODE=1` to skip real proof generation. +development, set `RISC0_DEV_MODE=1` to skip real proof generation, and ensure that the contracts +were deployed using full mock mode, otherwise a real proof will be needed. --- From fec2f01745595ea316df1eed5f86cea6294b829f Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:02:10 +0100 Subject: [PATCH 3/4] chore: updates --- docs/pages/tutorials/custom-zk-circuits.mdx | 15 ++++++--------- docs/pages/tutorials/deploy-to-testnet.mdx | 2 -- docs/pages/tutorials/encrypt-and-submit.mdx | 2 -- docs/pages/tutorials/manage-tickets.mdx | 2 -- docs/pages/tutorials/operator-troubleshooting.mdx | 2 -- docs/pages/tutorials/using-the-dashboard.mdx | 2 -- docs/pages/tutorials/write-e3-program.mdx | 2 -- 7 files changed, 6 insertions(+), 21 deletions(-) diff --git a/docs/pages/tutorials/custom-zk-circuits.mdx b/docs/pages/tutorials/custom-zk-circuits.mdx index 9841c27b29..b4d0ce9048 100644 --- a/docs/pages/tutorials/custom-zk-circuits.mdx +++ b/docs/pages/tutorials/custom-zk-circuits.mdx @@ -10,9 +10,8 @@ description: Every E3 program must validate that user inputs are correctly encrypted under the committee's threshold public key — this is the baseline. On top of that, programs can add application-specific checks. CRISP, for example, also proves that each vote is valid (within the voter's balance), that -the voter is eligible (Merkle membership), and — for mask votes — that the ciphertext is an -encryption of zero correctly added to the previous slot ciphertext. All without revealing the vote -itself. +the voter is eligible (Merkle membership), and for mask votes that the ciphertext is an encryption +of zero correctly added to the previous slot ciphertext. All without revealing the vote itself. This tutorial walks through how CRISP's Noir circuits work, as a template for building your own input validation circuits. @@ -20,8 +19,6 @@ input validation circuits. > **What you'll learn:** How CRISP composes several Noir circuits into a single on-chain-verifiable > proof, which library modules are reusable, and how to wire a custom circuit into `publishInput`. > -> **Time:** ~25 minutes (reading) -> > **Prerequisites:** Basic familiarity with [Noir](https://noir-lang.org/docs) and the > [E3 program structure](./write-e3-program). @@ -170,8 +167,8 @@ the API instance and SRS (Structured Reference String) to avoid re-initialisatio ## On-chain Verification The final proof is verified on-chain in `CRISPProgram.publishInput()` using an auto-generated Honk -verifier contract (`CRISPVerifier.sol`). This verifier is generated by Barretenberg during circuit -compilation and contains the verification key baked in. +verifier contract (`CRISPVerifier.sol`). This verifier is generated by Barretenberg and contains the +verification key baked in. ```solidity // In publishInput(): @@ -226,7 +223,7 @@ If your E3 program needs custom input validation: 4. **Call it from `publishInput`** — deploy the verifier and call it in your E3 program contract 5. **Build a TypeScript SDK** — wrap circuit input generation in a package your frontend can use -For simpler cases (e.g. just checking a value is in range), you may not need custom circuits at all -— standard Solidity checks in `publishInput` may be sufficient. +For simpler cases (e.g. just checking a value is in range), you may not need custom circuits at all, +you can reuse the user-data-encryption circuit and the standard Interfold SDK. See [Noir Circuits](/noir-circuits) for toolchain setup and compilation instructions. diff --git a/docs/pages/tutorials/deploy-to-testnet.mdx b/docs/pages/tutorials/deploy-to-testnet.mdx index 6b9d4ef92c..5aa26811c7 100644 --- a/docs/pages/tutorials/deploy-to-testnet.mdx +++ b/docs/pages/tutorials/deploy-to-testnet.mdx @@ -14,8 +14,6 @@ live deployment on Sepolia testnet. > at a Sepolia RPC, and a verified end-to-end E3 round against the live testnet ciphernode > committee. > -> **Time:** ~30–45 minutes (most of it waiting for block confirmations and the committee to form) -> > **Prerequisites:** A working local E3, Sepolia ETH for gas, a WebSocket RPC endpoint (e.g. > Alchemy, Infura), and your deployer private key funded. diff --git a/docs/pages/tutorials/encrypt-and-submit.mdx b/docs/pages/tutorials/encrypt-and-submit.mdx index 8f16f4bb62..f951e8aaa9 100644 --- a/docs/pages/tutorials/encrypt-and-submit.mdx +++ b/docs/pages/tutorials/encrypt-and-submit.mdx @@ -13,8 +13,6 @@ the committee's threshold public key, submitting it on-chain, and decoding the f > **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. > -> **Time:** ~15 minutes -> > **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) diff --git a/docs/pages/tutorials/manage-tickets.mdx b/docs/pages/tutorials/manage-tickets.mdx index 0efd0dcf74..06ddc60925 100644 --- a/docs/pages/tutorials/manage-tickets.mdx +++ b/docs/pages/tutorials/manage-tickets.mdx @@ -13,8 +13,6 @@ practical aspects of managing your ticket balance. > tickets from the CLI, the snapshot rule that decides which tickets count for a given E3 request, > and a simple strategy for keeping your node eligible. > -> **Time:** ~10 minutes -> > **Prerequisites:** A registered ciphernode with a license bond (see > [Registration](/ciphernode-operators/registration)). diff --git a/docs/pages/tutorials/operator-troubleshooting.mdx b/docs/pages/tutorials/operator-troubleshooting.mdx index 593f28f080..ae3a280371 100644 --- a/docs/pages/tutorials/operator-troubleshooting.mdx +++ b/docs/pages/tutorials/operator-troubleshooting.mdx @@ -13,8 +13,6 @@ This guide covers the most frequent problems operators encounter and how to reso > peers, isn't syncing, or is missing its submission window — and how to read the key log messages > the node emits at each stage. > -> **Time:** ~10–20 minutes depending on which section applies -> > **Prerequisites:** A registered and running ciphernode. If you haven't set one up yet, see > [Running a Ciphernode](/ciphernode-operators/running). diff --git a/docs/pages/tutorials/using-the-dashboard.mdx b/docs/pages/tutorials/using-the-dashboard.mdx index 7c6eb4b295..46196ec3fd 100644 --- a/docs/pages/tutorials/using-the-dashboard.mdx +++ b/docs/pages/tutorials/using-the-dashboard.mdx @@ -14,8 +14,6 @@ lightweight HTTP server alongside your node — no external dependencies, no sep > filters to trace a specific E3 or spot errors, and how to reach the dashboard securely from a > remote machine. > -> **Time:** ~10 minutes -> > **Prerequisites:** A running ciphernode. If you haven't set one up, see > [Running a Ciphernode](/ciphernode-operators/running). diff --git a/docs/pages/tutorials/write-e3-program.mdx b/docs/pages/tutorials/write-e3-program.mdx index fe93eaab36..86826f1882 100644 --- a/docs/pages/tutorials/write-e3-program.mdx +++ b/docs/pages/tutorials/write-e3-program.mdx @@ -15,8 +15,6 @@ verification flow well enough to build your own. > **What you'll learn:** How the three `IE3Program` entry points fit together, what the FHE > computation looks like inside a RISC Zero guest, and how the final proof is verified on-chain. > -> **Time:** ~20 minutes (reading) -> > **Prerequisites:** Familiarity with Solidity and a basic understanding of > [what an E3 is](/what-is-e3). From 4b0ec62d2f7f2ab2626a180a48f91493911ddb23 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:02:50 +0100 Subject: [PATCH 4/4] chore: coderabbit comments --- docs/pages/ciphernode-operators/index.mdx | 2 +- docs/pages/tutorials/deploy-to-testnet.mdx | 6 +++--- docs/pages/tutorials/operator-troubleshooting.mdx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/pages/ciphernode-operators/index.mdx b/docs/pages/ciphernode-operators/index.mdx index c6ab119885..064da3c17a 100644 --- a/docs/pages/ciphernode-operators/index.mdx +++ b/docs/pages/ciphernode-operators/index.mdx @@ -102,7 +102,7 @@ Before operating a ciphernode, ensure you have: | **ENCL Tokens** | At least `100 ENCL` for the license bond (check `licenseRequiredBond()`) | | **Stablecoin** | USDC (or configured fee token) for tickets; minimum 1 ticket worth | | **ETH** | Gas for transactions on your target network | -| **Hardware** | Linux/macOS, 8+ cores, 16GB+ RAM, stable internet with open UDP port | +| **Hardware** | Linux/macOS, 4+ cores, 16GB+ RAM, stable internet with open UDP port | | **Software** | Interfold CLI installed, WebSocket RPC endpoint | ## Getting Started diff --git a/docs/pages/tutorials/deploy-to-testnet.mdx b/docs/pages/tutorials/deploy-to-testnet.mdx index 5aa26811c7..8923c9a871 100644 --- a/docs/pages/tutorials/deploy-to-testnet.mdx +++ b/docs/pages/tutorials/deploy-to-testnet.mdx @@ -128,15 +128,15 @@ The SDK will automatically use the secure parameters when requesting E3s: ```typescript const hash = await sdk.requestE3({ - threshold: [2, 3], + committeeSize: CommitteeSize.Micro, inputWindow: [startTime, endTime], e3Program: YOUR_PROGRAM_ADDRESS, - e3ProgramParams: '0x...', computeProviderParams: '0x...', + paramSet: 0, // Insecure, }) ``` -Secure parameters use larger polynomial rings (N=8192 vs N=512) which makes proof generation slower +Secure parameters use polynomials of larger degrees (N=8192 vs N=512) which makes proof generation slower but provides real cryptographic security. Expect proof generation to take longer than in local dev. --- diff --git a/docs/pages/tutorials/operator-troubleshooting.mdx b/docs/pages/tutorials/operator-troubleshooting.mdx index ae3a280371..3bcd38a028 100644 --- a/docs/pages/tutorials/operator-troubleshooting.mdx +++ b/docs/pages/tutorials/operator-troubleshooting.mdx @@ -162,14 +162,14 @@ enclave start -vv To find the current log file path: ```bash -enclave config print log_file +enclave config get log_file ``` The default lives under your platform's state directory (e.g. `~/.local/state/enclave/` on Linux, `~/Library/Application Support/enclave/` on macOS). Tail it while reproducing an issue: ```bash -tail -f "$(enclave config print log_file)" +tail -f "$(enclave config get log_file)" ``` ---