From 0aa3cedf16272d6ede47e4533c13c5f5a3319efd Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:02:53 +0100 Subject: [PATCH 1/6] docs: update docs --- README.md | 7 +- docs/pages/_meta.json | 7 + docs/pages/ciphernode-operators/index.mdx | 12 +- docs/pages/internals/_meta.json | 14 + .../internals/commitment-consistency.mdx | 209 +++++++++++++++ docs/pages/internals/dkg.mdx | 244 +++++++++++++++++ docs/pages/internals/sortition.mdx | 247 ++++++++++++++++++ docs/pages/internals/zk-proofs.mdx | 185 +++++++++++++ docs/pages/sdk.mdx | 33 ++- examples/CRISP/Readme.md | 4 +- .../src/pages/Landing/components/Hero.tsx | 2 +- .../CRISP/packages/crisp-zk-inputs/README.md | 52 +++- examples/CRISP/program/README.md | 60 ++++- packages/enclave-config/README.md | 39 ++- packages/enclave-contracts/README.md | 25 ++ packages/enclave-mcp/README.md | 11 +- packages/enclave-mcp/src/index.ts | 2 +- packages/enclave-sdk/README.md | 24 +- 18 files changed, 1123 insertions(+), 54 deletions(-) create mode 100644 docs/pages/internals/_meta.json create mode 100644 docs/pages/internals/commitment-consistency.mdx create mode 100644 docs/pages/internals/dkg.mdx create mode 100644 docs/pages/internals/sortition.mdx create mode 100644 docs/pages/internals/zk-proofs.mdx diff --git a/README.md b/README.md index 7c5e7feb6b..0f30f1f78d 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,15 @@ The monorepo provides several test scripts for different components: - **`pnpm sdk:test`** - Runs tests for the TypeScript SDK in `packages/enclave-sdk`. - **`pnpm noir:test`** - Runs tests for Noir circuits in the `circuits/` directory using - `nargo test`. + `nargo test`. Requires the + [Noir toolchain](https://noir-lang.org/docs/getting_started/installation) (`nargo`) and + [Barretenberg](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg) (`bb`) to + be installed and on your `PATH`. - **`pnpm test:integration`** - Runs integration tests from `tests/integration/`. These tests may require prebuilt binaries and can be run with `--no-prebuild` if binaries are already available. + When running with secure (production) BFV parameters, pre-built circuit artifacts for the + `secure-8192` preset must be present in the `circuits/` artifacts directory. #### Running Individual Test Suites diff --git a/docs/pages/_meta.json b/docs/pages/_meta.json index 959fcfcd09..1e7a1d4a19 100644 --- a/docs/pages/_meta.json +++ b/docs/pages/_meta.json @@ -96,6 +96,13 @@ "CRISP": { "title": "CRISP" }, + "-- Internals": { + "type": "separator", + "title": "Internals" + }, + "internals": { + "title": "Technical Deep Dives" + }, "-- Reference": { "type": "separator", "title": "Reference" diff --git a/docs/pages/ciphernode-operators/index.mdx b/docs/pages/ciphernode-operators/index.mdx index 47113e127e..a59bb84b53 100644 --- a/docs/pages/ciphernode-operators/index.mdx +++ b/docs/pages/ciphernode-operators/index.mdx @@ -109,11 +109,7 @@ Before operating a ciphernode, ensure you have: Follow these guides in order to become an active ciphernode operator: -1. **[Running a Ciphernode](./ciphernode-operators/running)** - Set up your node using DappNode, - Interfold CLI, or Docker -2. **[Registration & Licensing](./ciphernode-operators/registration)** - Bond your license, - register, and add tickets -3. **[Tickets & Sortition](./ciphernode-operators/tickets-and-sortition)** - Understand how - committee selection works -4. **[Exits, Rewards & Slashing](./ciphernode-operators/exits-and-slashing)** - Learn about rewards - and exit procedures +1. **[Running a Ciphernode](./running)** - Set up your node using DappNode, Interfold CLI, or Docker +2. **[Registration & Licensing](./registration)** - Bond your license, register, and add tickets +3. **[Tickets & Sortition](./tickets-and-sortition)** - Understand how committee selection works +4. **[Exits, Rewards & Slashing](./exits-and-slashing)** - Learn about rewards and exit procedures diff --git a/docs/pages/internals/_meta.json b/docs/pages/internals/_meta.json new file mode 100644 index 0000000000..d95183b82b --- /dev/null +++ b/docs/pages/internals/_meta.json @@ -0,0 +1,14 @@ +{ + "sortition": { + "title": "Sortition" + }, + "dkg": { + "title": "DKG & Threshold Cryptography" + }, + "zk-proofs": { + "title": "ZK Proof Pipeline" + }, + "commitment-consistency": { + "title": "Commitment Consistency" + } +} diff --git a/docs/pages/internals/commitment-consistency.mdx b/docs/pages/internals/commitment-consistency.mdx new file mode 100644 index 0000000000..6e40cc45e8 --- /dev/null +++ b/docs/pages/internals/commitment-consistency.mdx @@ -0,0 +1,209 @@ +--- +title: 'Commitment Consistency' +description: + 'How the Interfold protocol detects dishonest ciphernodes by cross-checking commitment values + across ZK circuit proofs — link types, scoping rules, and the C2→C4 check' +--- + +# Commitment Consistency — Technical Deep Dive + +The [ZK proof pipeline](./zk-proofs) ensures each individual circuit execution is correct. But a +dishonest node could still cheat by using **different inputs** across circuits — for example, +committing to one set of shares in C2 but using different shares in C4. Each proof would verify in +isolation, but the overall protocol output would be wrong. + +The **commitment consistency checker** closes this gap by cross-referencing public signals across +circuit proofs. If a commitment value produced by one circuit doesn't match the expected value in +another, the offending party is flagged and excluded. + +--- + +## Architecture + +The checker is a per-E3 actor (`CommitmentConsistencyChecker`) that: + +1. **Caches** verified proof outputs keyed by `(Address, ProofType)` +2. **Evaluates** a set of registered **commitment links** on every new proof arrival +3. **Emits** `CommitmentConsistencyViolation` events for the accusation pipeline on mismatch + +It operates in two modes: + +### Pre-ZK Gating (fast path) + +Before expensive ZK verification, the `ShareVerificationActor` sends all received proofs to the +checker in a batch via `CommitmentConsistencyCheckRequested`. The checker evaluates all links and +returns a set of `inconsistent_parties` that are **excluded from ZK verification entirely**. + +This is a significant optimization — a dishonest party caught by commitment checks never wastes +resources on ZK verification. + +### Post-ZK Cross-Check (defense in depth) + +After ZK verification passes, the checker receives `ProofVerificationPassed` events and re-evaluates +links. This catches cases where a proof from one phase arrives after the initial batch, or where a +cross-party link couldn't be evaluated earlier because the counterparty's proof hadn't arrived yet. + +--- + +## Link Types + +Each commitment link defines a relationship between a **source** circuit (which produces a +commitment value) and a **target** circuit (which must consume or agree with that value). + +```rust +trait CommitmentLink { + fn source_proof_type(&self) -> ProofType; + fn target_proof_type(&self) -> ProofType; + fn scope(&self) -> LinkScope; + fn extract_source_values(&self, public_signals: &[u8]) -> Vec; + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + src_party_id: u64, + tgt_party_id: u64, + ) -> bool; +} +``` + +### Scoping Rules + +The **scope** determines how source and target proofs are matched and where blame is attributed: + +| Scope | Matching rule | Blame | Example | +| ---------------------------- | ------------------------------------------------------------------- | ------ | --------------------------------------------------------------- | +| **SameParty** | Source and target must come from the same Ethereum address | Source | C1→C2: same node's sk commitment must match | +| **CrossParty** | Each source must match at least one target from any party | Source | C1→C5: per-node pk must appear in aggregator's C5 | +| **SourceMustExistInTargets** | Each source claims a value that must exist in some target's signals | Source | C2→C4: sender's share commitments must appear in recipient's C4 | + +Blame always falls on the **source** party. If C2's commitments don't match C4's expectations, the +C2 sender is accused — not the C4 recipient. + +--- + +## Registered Links + +The protocol registers 12 commitment links at startup. The `l` parameter (number of CRT moduli) is +derived from the E3's BFV preset: + +### DKG Phase Links + +| Link | Scope | What it checks | +| ------------- | ------------------------ | ---------------------------------------------------------------------- | +| **C1 → C2a** | SameParty | C1's `sk_commitment` matches C2a's `expected_secret_commitment` | +| **C1 → C2b** | SameParty | C1's `e_sm_commitment` matches C2b's `expected_secret_commitment` | +| **C1 → C5** | CrossParty | C1's `pk_commitment` appears in C5's expected pk inputs | +| **C3a → C0** | SourceMustExistInTargets | C3a's `expected_pk_commitment` must exist in some C0's `pk_commitment` | +| **C3b → C0** | SourceMustExistInTargets | Same for C3b | +| **C3a → C2a** | SameParty | C3a's `expected_message_commitment` matches C2a's share commitments | +| **C3b → C2b** | SameParty | Same for the smudging noise branch | +| **C2a → C4a** | SourceMustExistInTargets | C2a's L share commitments for recipient R match C4a's row for sender X | +| **C2b → C4b** | SourceMustExistInTargets | Same for smudging noise | + +### Threshold Decryption Links + +| Link | Scope | What it checks | +| ------------ | ---------- | -------------------------------------------------------------------------- | +| **C4a → C6** | SameParty | C4a's aggregated `sk_commitment` matches C6's `expected_sk_commitment` | +| **C4b → C6** | SameParty | C4b's aggregated `e_sm_commitment` matches C6's `expected_e_sm_commitment` | +| **C6 → C7** | CrossParty | C6's `d_commitment` matches C7's `expected_d_commitment` | + +--- + +## The C2→C4 Link (Most Complex) + +This link is the most technically interesting because it bridges two **different parties**: the +sender (who computed shares in C2) and the recipient/aggregator (who decrypted shares in C4). + +### Public Signal Layouts + +**C2 inner circuit** (row-major: party first, modulus second): + +``` +[expected_secret_commitment (32 B)] ← skipped +[party_0_mod_0] [party_0_mod_1] ... [party_0_mod_{L-1}] +[party_1_mod_0] ... +[party_{N-1}_mod_{L-1}] +``` + +**C4 public signals** (H honest parties, L moduli): + +``` +[expected_commitments[0][0]] ... [expected_commitments[0][L-1]] +[expected_commitments[1][0]] ... +[expected_commitments[H-1][L-1]] +[aggregated_commitment (32 B)] ← tail, excluded from check +``` + +### Check Logic + +Given C2 sender `X` (party_id) and C4 recipient `R` (party_id): + +1. **From C2_X**: Extract L commitments at slot R → `source_values[R*L .. (R+1)*L]` — these are the + L commitments that sender X computed for recipient R +2. **From C4_R**: Extract row at position X → `target_signals[X*L*32 .. (X+1)*L*32]` — these are the + L commitments that recipient R expects from sender X +3. **Compare**: All L values must match exactly + +If any modulus commitment differs, sender X is accused. + +### The L Parameter + +The block size `L` (number of CRT moduli) varies by BFV preset: + +| Preset | L | +| ------------------------ | --- | +| `INSECURE_THRESHOLD_512` | 2 | +| `SECURE_THRESHOLD_8192` | 4 | + +The `L` used in the check **must match the actual preset** for the E3 — otherwise every index +calculation is wrong and the check produces false positives. This is why the checker reads `L` from +`E3Meta.params_preset` at E3 startup rather than using a hardcoded default. + +--- + +## Deduplication + +The same proof can arrive via two paths: + +1. **Pre-ZK batch**: `CommitmentConsistencyCheckRequested` from `ShareVerificationActor` +2. **Post-ZK event**: `ProofVerificationPassed` after ZK verification + +The checker deduplicates by `data_hash` — a hash of the proof's public signals. If a proof with the +same hash is already cached, it's silently ignored. This prevents double-counting or re-triggering +link evaluations. + +--- + +## Accusation Flow + +When a commitment inconsistency is detected: + +``` +CommitmentConsistencyChecker detects mismatch + │ + ├─ Emits CommitmentConsistencyViolation { + │ accused_party_id, + │ accused_address, + │ proof_type, // which proof was inconsistent + │ data_hash, // identifies the specific proof + │ } + │ + ▼ +AccusationManager receives violation + │ + ├─ Initiates off-chain accusation quorum protocol + ├─ Other committee members independently verify + └─ If quorum reached → on-chain slashing via SlashingManager +``` + +Pre-ZK inconsistent parties are also excluded from the `CommitmentConsistencyCheckComplete` +response, preventing them from consuming ZK verification resources. + +--- + +## Related + +- [ZK Proof Pipeline](./zk-proofs) — the circuits whose commitments are being cross-checked +- [DKG & Threshold Cryptography](./dkg) — the protocol that generates the proofs +- [Sortition](./sortition) — committee selection before any of this begins diff --git a/docs/pages/internals/dkg.mdx b/docs/pages/internals/dkg.mdx new file mode 100644 index 0000000000..7a6cd59ef7 --- /dev/null +++ b/docs/pages/internals/dkg.mdx @@ -0,0 +1,244 @@ +--- +title: 'DKG & Threshold Cryptography' +description: + 'How ciphernodes collectively generate a threshold public key without any party learning the full + secret — BFV keypairs, Shamir sharing, TrBFV, and the DKG state machine' +--- + +# DKG & Threshold Cryptography — Technical Deep Dive + +After [sortition](./sortition) selects a committee, the selected ciphernodes run a Distributed Key +Generation (DKG) protocol to produce a **collective public key** that users encrypt to. No single +party ever holds the full secret key — instead, each node holds a Shamir share, and any +`threshold_m` of `N` committee members can collaboratively decrypt. + +The scheme is built on **Threshold Ring-BFV** (TrBFV), which extends the standard BFV +fully-homomorphic encryption scheme to a threshold setting. + +--- + +## Two Key Hierarchies + +A common source of confusion: there are **two separate keypairs** in the protocol. + +| Key type | Scheme | Purpose | Lifetime | +| ------------------ | ------------ | -------------------------------------------------------------------- | ------------------ | +| **Individual key** | Standard BFV | Encrypt Shamir shares in transit during DKG | One DKG session | +| **Threshold key** | TrBFV | The collective key users encrypt to; used for homomorphic operations | One E3 computation | + +The individual BFV key is proved correct in **C0**. The threshold key contributions are proved in +**C1**. They live in different polynomial rings (the individual key uses a smaller "DKG" parameter +set optimized for encryption). + +--- + +## DKG State Machine + +Each ciphernode runs a `ThresholdKeyshare` actor that progresses through a strict state machine: + +``` +Init + │ CiphernodeSelected arrives + ▼ +CollectingEncryptionKeys ← Generate BFV keypair; prove with C0; wait for all N peers + │ All N keys collected + ▼ +GeneratingThresholdShare ← Generate TrBFV contribution (C1); Shamir-split sk and e_sm; + │ encrypt shares (C3); prove share computation (C2) + │ All N shares collected + ▼ +AggregatingDecryptionKey ← Decrypt received shares; prove decryption (C4); + │ aggregate into local sk/e_sm shares + │ C4 proofs verified + ▼ +ReadyForDecryption ← Waiting for ciphertext output from the FHE evaluation + │ CiphertextOutputPublished arrives + ▼ +Decrypting ← Compute partial decryption share (C6) + │ + ▼ +GeneratingDecryptionProof ← [Aggregator only] Collect shares; Lagrange interpolation (C7) + │ + ▼ +Completed +``` + +Each transition requires both a cryptographic operation and successful ZK proof +generation/verification. If any step times out, the node can be expelled from the committee. + +--- + +## Phase 1: Individual Key Exchange (C0) + +``` +CiphernodeSelected event arrives at ThresholdKeyshare + │ + ├─ BFV::keygen(dkg_params) → (sk_bfv, pk_bfv) + │ This is the INDIVIDUAL node encryption key (NOT the threshold key) + │ + ├─ Encrypt sk_bfv at rest with local password (AES-GCM via Cipher) + │ + ├─ Publish EncryptionKeyPending { e3_id, party_id, pk_bfv } + │ → ZK proof actor picks this up and generates C0 + │ + └─ Create collectors: + ├─ EncryptionKeyCollector (waits for N parties, 60s timeout) + └─ ThresholdShareCollector (waits for N parties, 120s timeout) + → Start immediately so early-arriving peer data is buffered +``` + +**C0** proves the BFV keypair was generated correctly — specifically that `pk = (-a·sk + e, a)` for +a validly sampled secret `sk` and error `e`. Receiving nodes verify C0 before trusting the public +key. + +--- + +## Phase 2: Threshold Share Generation (C1, C2, C3) + +Once all N individual public keys are collected, each node generates its threshold contribution: + +### Step 1 — TrBFV Key Share (C1) + +``` +GenPkShareAndSkSss request: + ├─ TRBFV::gen_pk_share(sk, crp, params) → PublicKeyShare + │ pk0_share = -crp·sk + eek + r2·(γ^N+1) + r1·q + │ + ├─ ShareManager::create_shares(sk, M, N) → N Shamir shares of the secret key + │ + └─ Sample smudging noise e_sm with λ-bit statistical security + ShareManager::create_shares(e_sm, M, N) → N Shamir shares of e_sm +``` + +**C1** proves the public key share was correctly derived from the secret key, and that the smudging +noise has the correct distribution. The Common Random Polynomial (CRP) is a fixed deterministic +value derived from a default seed per BFV preset — all nodes using the same preset produce the same +CRP. + +### Step 2 — Share Computation Proof (C2a / C2b) + +C2 runs separately for the secret key shares (C2a) and smudging noise shares (C2b). Each C2 circuit: + +1. **Verifies Shamir structure** via a parity check: `H · y^T ≡ 0 (mod q_j)` for each CRT modulus, + where `H` is a Reed–Solomon parity matrix. This proves the shares lie on a polynomial of degree ≤ + M without revealing the polynomial +2. **Outputs per-party commitments**: `hash(share_polynomial)` for each recipient party and each CRT + modulus — producing an `N × L` grid of commitment values + +These commitments are the bridge to C4 (the recipient verifies they received the same shares that +were committed to). + +### Step 3 — Share Encryption (C3a / C3b) + +Each share is BFV-encrypted under the recipient's individual public key (from C0). C3 proves: + +``` +ct0 = pk0·u + e0 + message + r1·q + r2·(γ^N+1) +ct1 = pk1·u + e1 + p1·q + p2·(γ^N+1) +``` + +This generates `(N-1) × L` proofs per variant (one for each recipient × CRT modulus). In a 3-node +committee with L=4 CRT moduli, that's 8 C3a proofs and 8 C3b proofs per node. + +### Proof Publication + +All proofs for a node are generated in parallel on a multithread pool. Only when **all** proofs +complete does the node broadcast its `ThresholdShareCreated` message containing: + +- The public key share (from C1) +- Encrypted Shamir shares (from C3) +- All signed proofs (C1, C2a, C2b, C3a[], C3b[]) + +This atomic publication prevents partial data from reaching peers. + +--- + +## Phase 3: Share Decryption and Aggregation (C4) + +Each node decrypts the encrypted shares it received from all other honest parties using its +individual BFV secret key. C4 then: + +1. **Verifies commitment consistency**: Each decrypted share is hashed and compared against the + commitment the sender published in C2. If they don't match, the sender was dishonest +2. **Aggregates**: Sums all decrypted shares coefficient-wise per CRT modulus: + `agg_sk[ℓ][i] = Σ_h share_h[ℓ][i]` +3. **Normalizes**: Reduces mod `q_ℓ`, reverses coefficient order, and centers to match C6's + representation +4. **Outputs**: A single commitment to the aggregated share polynomial, consumed by C6 + +After C4, each node holds: + +- Its share of the threshold secret key (`agg_sk`) +- Its share of the smudging noise (`agg_e_sm`) + +These are the only long-lived secrets from the DKG. + +--- + +## Phase 4: Public Key Aggregation (C5) + +The aggregator (party_id=0) collects all N public key shares from C1 and sums them: + +``` +pk = Σ pk_share_i +``` + +C5 proves this addition was done correctly. The result is the **threshold public key** that users +encrypt their data to. It is published on-chain via `publishCommittee`. + +--- + +## Threshold Decryption (C6, C7) + +Multiple users submit BFV-encrypted inputs during the E3 input window. A compute provider then +evaluates the E3 program homomorphically over those ciphertexts (e.g. adding them for a tally, or +multiplying for other computations) and publishes the resulting ciphertext output on-chain. At that +point, each committee member computes a partial decryption share of the evaluated result: + +### C6 — Partial Decryption + +``` +d[ℓ](γ) = ct0[ℓ](γ) + ct1[ℓ](γ)·sk[ℓ](γ) + e_sm[ℓ](γ) + r2[ℓ](γ)·(γ^N+1) + r1[ℓ](γ)·q_ℓ +``` + +The smudging noise `e_sm` is added to prevent leaking information about the node's secret key share +through the partial decryption. C6 proves this computation was done correctly with the same `sk` and +`e_sm` committed in C4a/C4b. + +### C7 — Lagrange Reconstruction + +The aggregator collects ≥ `threshold_m` partial shares and reconstructs the plaintext: + +1. **Lagrange interpolation** at zero: `u[ℓ] = Σ_i d_i[ℓ] · L_i(0) (mod q_ℓ)` where + `L_i(0) = ∏_{j≠i} (-x_j) / (x_i - x_j)` +2. **CRT reconstruction**: Stitch `u[0..L-1]` into a single value via the Chinese Remainder Theorem +3. **Decoding**: Map from the ciphertext ring back to the plaintext space + +C7 proves all three steps were done correctly. The final plaintext is published on-chain. + +--- + +## BFV Parameter Presets + +All polynomial degrees, CRT moduli, noise bounds, and security levels are determined by the BFV +preset: + +| Parameter | Insecure (N=512) | Secure (N=8192) | +| ------------------------ | ---------------- | --------------- | +| Polynomial degree (N) | 512 | 8192 | +| Threshold CRT moduli (L) | 2 | 4 | +| DKG CRT moduli | 1 | 2 | +| Plaintext modulus (t) | Small | Large | +| Security | None | Full (λ=80) | + +The preset is selected per-E3 via the on-chain `paramSet` index. Circuit artifacts are compiled +separately for each preset, so both the prover and verifier must use matching parameters. + +--- + +## Related + +- [ZK Proof Pipeline](./zk-proofs) — how each circuit is proved, verified, and aggregated +- [Commitment Consistency](./commitment-consistency) — how cross-circuit commitments are enforced +- [Sortition](./sortition) — how the committee is selected before DKG begins +- [Cryptography](/cryptography) — mathematical foundations (user-facing overview) diff --git a/docs/pages/internals/sortition.mdx b/docs/pages/internals/sortition.mdx new file mode 100644 index 0000000000..c4613b0217 --- /dev/null +++ b/docs/pages/internals/sortition.mdx @@ -0,0 +1,247 @@ +--- +title: 'Sortition — Technical Deep Dive' +description: + 'How the Interfold sortition algorithm selects ciphernode committees — scoring function, seed + derivation, buffer strategy, and on-chain/off-chain determinism' +--- + +# Sortition — Technical Deep Dive + +Sortition is the mechanism that selects a subset of registered ciphernodes to form a committee for +each E3 request. The design goals are: + +- **Unpredictability** — no party can know the outcome before the request block +- **Proportionality** — selection probability scales linearly with ticket stake +- **Determinism** — every honest node independently computes the same result +- **Sybil-resistance** — splitting funds across many wallets provides no advantage +- **On-chain verifiability** — the contract enforces the same rules the nodes compute off-chain + +--- + +## Randomness Source + +The seed for every sortition round is derived from the EVM's `block.prevrandao` (EIP-4399) combined +with the E3 identifier: + +```solidity +seed = uint256(keccak256(abi.encode(block.prevrandao, e3Id))); +``` + +`prevrandao` is the RANDAO reveal from the beacon chain. It is unpredictable before the block is +proposed but publicly verifiable afterwards. Combining it with `e3Id` ensures different rounds +produce independent seeds even within the same block. + +> **Limitation:** `prevrandao` is weak against a block proposer who can selectively withhold their +> block. This is an accepted trade-off for now; future versions may use a commit-reveal scheme or an +> external VRF. + +The seed is emitted with `E3Requested` and used by all nodes to deterministically compute the same +committee. + +--- + +## Ticket Balances and Eligibility + +Each operator's weight in the lottery is determined by their available ticket count at the snapshot +block: + +``` +availableTickets = floor(ticketTokenBalance / ticketPrice) +``` + +Balances are snapshotted at **`requestBlock - 1`** — the block immediately before the E3 request was +included. This prevents operators from front-running a request by depositing tickets in the same +block. + +An operator is eligible if and only if: + +1. They are registered in `CiphernodeRegistry` and not banned +2. `isActive == true` (bond ≥ `licenseRequiredBond` AND tickets ≥ `minTicketBalance`) +3. No exit is pending + +--- + +## Scoring Function + +For each ticket an operator holds, a score is computed as a deterministic pseudo-random number: + +``` +score = keccak256(abi.encodePacked(node_address, ticket_number, e3Id, seed)) +``` + +The result is interpreted as an unsigned 256-bit integer. **Lower scores win.** + +Key properties: + +- The hash is computed identically on-chain (Solidity) and off-chain (Rust using `alloy`'s + `abi_encode_packed`), so both sides always agree +- Given the same inputs, the score is always the same — there is no per-invocation randomness +- `ticket_number` is a 0-based index into the operator's ticket balance. Ticket 0, ticket 1, etc. + +### One Best Ticket Per Node + +Each operator computes scores for **all** of their tickets but submits only the single best +(lowest-scoring) one. This is enforced off-chain by the node software and on-chain by the contract +(which rejects duplicate submissions from the same address). + +This design is critical for **sybil-resistance**: splitting 100 tickets across 100 wallets vs +holding 100 tickets in one wallet produces the same expected score distribution, because: + +- A single wallet takes `min(score_0, score_1, ..., score_99)` — the minimum of 100 draws +- 100 wallets each take `min(score_i)` with 1 draw each — 100 independent draws + +The minimum of N draws from a uniform distribution is dominated by the same statistics in both +cases, so there is no incentive to split. + +--- + +## Committee Size and Buffer + +The sortition selects more nodes than strictly required by the threshold parameters. For a request +with `threshold_m` (minimum signers) and `threshold_n` (nominal committee size), the total selection +size is: + +``` +buffer = (threshold_n - threshold_m) + safety_factor +total_selection_size = threshold_n + buffer +``` + +Where `safety_factor` depends on the `m/n` ratio: + +| `threshold_m / threshold_n` | Safety factor | Rationale | +| --------------------------- | ------------- | ------------------------------------------------------------------------ | +| ≥ 0.8 (high threshold) | 3 | Tight committees are vulnerable to even one dropout; more backups needed | +| ≥ 0.6 | 2 | Moderate risk | +| < 0.6 (loose threshold) | 1 | Plenty of slack; minimal buffer required | + +The extra nodes act as **standing backups**. If a selected node fails to respond during DKG, it can +be expelled and the next node in score order steps in without restarting the committee. + +--- + +## On-chain Submission Window + +After `E3Requested` is emitted, there is a fixed window (e.g. 10 seconds on Sepolia) during which +eligible operators submit their winning tickets: + +```solidity +submitTicket(e3Id, ticketNumber) +``` + +The contract maintains a sorted top-N list of the best submissions seen so far, using a linear scan +to find the insertion point. Each submission emits: + +```solidity +event TicketSubmitted(uint256 e3Id, address node, uint256 ticketId, uint256 score); +``` + +The contract enforces: + +- Only one submission per address per E3 +- The ticket number must be within the operator's balance at `requestBlock - 1` +- The score must be recomputed on-chain and match what the node claims + +--- + +## Committee Finalization + +After the submission window closes, anyone can call `finalizeCommittee(e3Id)`: + +- If ≥ `threshold_n` tickets were submitted → `CommitteeFinalized(e3Id, committee, scores)` is + emitted +- If fewer → `CommitteeFormationFailed` is emitted and the E3 fails with a refund + +`CommitteeFinalized` carries the committee as an ordered list of addresses. The Rust runtime then +**normalizes** this list by **sorting it by ascending score** to produce a canonical party ordering: + +``` +party_id = index in score-sorted committee (lowest score = 0) +``` + +This normalization is done in `CommitteeFinalized::sort_by_score()` and ensures every node +independently derives the same `party_id` for each member, regardless of the order the contract +happened to emit them. + +--- + +## Aggregator Assignment + +The **active aggregator** is the node with `party_id = 0` — the node that won the lowest overall +score. It is responsible for: + +- Collecting and verifying ZK proofs from all other committee members +- Aggregating decryption shares +- Submitting the final ciphertext output and proof on-chain + +If the aggregator is expelled, the next non-expelled node by `party_id` takes over. + +--- + +## Off-chain Pre-filtering + +To avoid every node sending a transaction on every E3 request (which would be expensive and noisy), +each node runs the full sortition algorithm locally before deciding whether to submit: + +```rust +// Pseudocode from crates/sortition/src/backends.rs +let eligible = nodes.filter(|n| n.is_active && n.available_tickets > 0); +let committee = ScoreSortition::new(total_selection_size).get_committee(eligible, e3id, seed); +if committee.contains(my_address) { + submit_winning_ticket(e3id, my_ticket_number); +} +``` + +Because the scoring function is deterministic and all inputs are public (on-chain balances at a +fixed block, the seed), every honest node computes the same committee. A node that the algorithm +says should submit but doesn't is simply dropping out — it may be expelled later. + +--- + +## Full Flow Summary + +```mermaid +sequenceDiagram + participant Chain + participant NodeA + participant NodeB + participant NodeC + + Chain->>NodeA: E3Requested(e3Id, seed, requestBlock) + Chain->>NodeB: E3Requested(e3Id, seed, requestBlock) + Chain->>NodeC: E3Requested(e3Id, seed, requestBlock) + + Note over NodeA,NodeC: Each node snapshots balances at requestBlock-1,
scores all tickets, keeps best + + NodeA->>Chain: submitTicket(e3Id, ticketId=3) + NodeB->>Chain: submitTicket(e3Id, ticketId=0) + Note over NodeC: NodeC's best score isn't low enough → doesn't submit + + Note over Chain: Submission window closes + Chain->>Chain: finalizeCommittee(e3Id) + Chain->>NodeA: CommitteeFinalized([NodeA, NodeB], [scoreA, scoreB]) + Chain->>NodeB: CommitteeFinalized([NodeA, NodeB], [scoreA, scoreB]) + + Note over NodeA,NodeB: Both sort by score → same party_ids
Lowest score = party_id 0 (aggregator) +``` + +--- + +## Security Properties + +| Property | Mechanism | +| ----------------------- | ------------------------------------------------------------------- | +| **Unpredictability** | `seed` derived from `prevrandao` — unknown until block proposal | +| **Proportionality** | Expected min-score over N tickets scales with N | +| **Sybil-resistance** | Splitting tickets across wallets doesn't improve expected min-score | +| **Determinism** | Hash function is identical on-chain (Solidity) and off-chain (Rust) | +| **Anti-frontrunning** | Balances snapshotted at `requestBlock - 1` | +| **Backup availability** | Buffer nodes ensure committee can absorb dropouts without restart | + +--- + +## Related + +- [Tickets & Sortition](../ciphernode-operators/tickets-and-sortition) — operator-facing guide on + managing tickets and participating in committees +- [Cryptography](../cryptography) — how the selected committee runs DKG and threshold decryption +- [E3 Computation Flow](../computation-flow) — end-to-end view of the E3 lifecycle diff --git a/docs/pages/internals/zk-proofs.mdx b/docs/pages/internals/zk-proofs.mdx new file mode 100644 index 0000000000..433c1eeb43 --- /dev/null +++ b/docs/pages/internals/zk-proofs.mdx @@ -0,0 +1,185 @@ +--- +title: 'ZK Proof Pipeline' +description: + 'The complete C0–C7 circuit pipeline — what each circuit proves, how proofs are generated with + Barretenberg, verified over P2P, and recursively aggregated' +--- + +# ZK Proof Pipeline — Technical Deep Dive + +Every cryptographic operation in the Interfold DKG and threshold decryption protocol is accompanied +by a zero-knowledge proof. These proofs let nodes verify each other's work without trusting anyone, +and allow on-chain contracts to verify correctness without seeing secret data. + +The pipeline uses **Noir** circuits compiled to **UltraHonk** proofs via +[Barretenberg](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg) (`bb`). + +--- + +## Circuit Map + +| ID | Circuit | Phase | Proves | Source → Target | +| ------- | ---------------------------- | ------ | --------------------------------------------------------------------------------- | -------------------------- | +| **C0** | `PkBfv` | P1 DKG | Individual BFV keypair was generated correctly | Each node → all peers | +| **C1** | `PkGeneration` | P1 DKG | TrBFV public key share is correctly derived from secret key | Each node → aggregator | +| **C2a** | `SkShareComputation` | P1 DKG | Secret key Shamir shares satisfy parity constraints | Each node → all peers | +| **C2b** | `ESmShareComputation` | P1 DKG | Smudging noise Shamir shares satisfy parity constraints | Each node → all peers | +| **C3a** | `ShareEncryption` (sk) | P1 DKG | BFV encryption of secret key shares is valid | Per sender → per recipient | +| **C3b** | `ShareEncryption` (e_sm) | P1 DKG | BFV encryption of smudging noise shares is valid | Per sender → per recipient | +| **C4a** | `DkgShareDecryption` (sk) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local threshold key share | Each node → all peers | +| **C4b** | `DkgShareDecryption` (e_sm) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local smudging noise share | Each node → all peers | +| **C5** | `PkAggregation` | P2 Agg | Sum of all pk shares equals the published threshold public key | Aggregator → on-chain | +| **C6** | `ThresholdShareDecryption` | P4 Dec | Partial decryption share uses correct sk and e_sm from C4 | Each node → aggregator | +| **C7** | `DecryptedSharesAggregation` | P4 Dec | Lagrange reconstruction + CRT + decoding is correct | Aggregator → on-chain | + +--- + +## Proof Count Per E3 + +For a committee of **N** nodes with **L** CRT moduli: + +| Circuit | Count per node | Total across committee | +| ------- | --------------------- | ---------------------- | +| C0 | 1 | N | +| C1 | 1 | N | +| C2a | 1 | N | +| C2b | 1 | N | +| C3a | (N-1) × L | N × (N-1) × L | +| C3b | (N-1) × L × ESI_count | N × (N-1) × L × ESI | +| C4a | 1 | N | +| C4b | 1 | N | +| C5 | 1 (aggregator only) | 1 | +| C6 | 1 | N | +| C7 | 1 (aggregator only) | 1 | + +C3 is by far the most proof-intensive circuit. With N=3 and L=4 (secure params), each node generates +8 C3a proofs alone. This is why proof generation runs on a multithread pool. + +--- + +## Proof Lifecycle + +### 1. Generation + +The `ProofRequestActor` dispatches ZK requests to the `ZkActor`: + +``` +ProofRequestActor receives event (e.g. EncryptionKeyPending) + │ + ├─ Builds witness data from event payload + ├─ Dispatches ComputeRequest::zk(ZkRequest::PkBfv { ... }) + │ + ▼ +ZkActor (IO layer): + ├─ Writes witness to temp directory + ├─ Spawns: bb prove -b circuit.json -w witness.gz -o proof/ + └─ Returns: Proof { data, public_signals } +``` + +### 2. Signing + +Every proof is ECDSA-signed by the generating node before broadcast: + +``` +digest = keccak256(abi.encode( + PROOF_PAYLOAD_TYPEHASH, + chainId, + e3Id, + proofType, // e.g. C0, C2a, C3b + keccak256(proof.data), + keccak256(proof.public_signals) +)) + +signature = ecSign(digest, operator_private_key) // 65-byte r||s||v +``` + +This binds the proof to a specific E3, chain, and proof type — preventing replay attacks. The +signature also identifies the prover, so verifiers know which committee member produced the proof. + +### 3. Broadcast + +Signed proofs are wrapped in protocol events and broadcast via **libp2p gossip**. Depending on the +proof type: + +- **C0**: Published as `EncryptionKeyCreated` — all peers receive and verify +- **C1–C3**: Bundled into `ThresholdShareCreated` — atomic publication of all DKG proofs +- **C6**: Published as `DecryptionShareCreated` — aggregator collects + +A node does **not** publish partial results. All proofs for a given phase complete before anything +is broadcast. + +### 4. Verification + +On receiving a signed proof, the `ProofVerificationActor`: + +``` +ProofVerificationActor receives signed proof from gossip + │ + ├─ Recover ECDSA signer address from signature + ├─ Validate signer is a known committee member + │ + ├─ Dispatch to ZkActor: bb verify -k vk -p proof.data + │ (uses the inner circuit's verification key) + │ + ├─ If PASS: + │ ├─ Publish ProofVerificationPassed (cached by CommitmentConsistencyChecker) + │ └─ Continue protocol + │ + └─ If FAIL: + └─ Publish SignedProofFailed → triggers accusation pipeline +``` + +### 5. Share Verification (Three-Phase Pipeline) + +For DKG shares (C1, C2, C3 together), there's a more sophisticated pipeline in +`ShareVerificationActor`: + +**Phase 1 — Lightweight ECDSA** (inline, fast): + +- Verify signature, recover signer, check consistency (all proofs from same address) +- Filter obviously invalid submissions + +**Phase 2 — Commitment Consistency** (dispatched to per-E3 checker): + +- Evaluate all registered [commitment links](./commitment-consistency) +- Exclude parties whose commitments are inconsistent across circuits +- This catches dishonest behavior **before** expensive ZK verification + +**Phase 3 — Heavy ZK Verification** (multithread): + +- Only consistency-passing parties reach this stage +- Each proof verified independently via `bb verify` +- Results aggregated into `ShareVerificationComplete { dishonest_parties }` + +--- + +## Circuit Dependencies + +The proofs form a directed acyclic graph through shared commitments: + +``` +C0 ──────────────────────────────────────→ C3 (pk used to encrypt) +C0 ──────────────────────────────────────→ C3 (pk_commitment verified) +C1 ──→ C2a (sk_commitment) +C1 ──→ C2b (e_sm_commitment) +C1 ──→ C5 (pk_commitment) +C2a ─→ C3a (share_commitment) +C2b ─→ C3b (share_commitment) +C2a ─→ C4a (expected_commitments for all L moduli) +C2b ─→ C4b (expected_commitments for all L moduli) +C4a ─→ C6 (sk_commitment) +C4b ─→ C6 (e_sm_commitment) +C6 ─→ C7 (d_commitment) +``` + +These dependencies are enforced by the [commitment consistency checker](./commitment-consistency), +which evaluates 12 cross-circuit links to detect any party submitting inconsistent values. + +--- + +## Related + +- [DKG & Threshold Cryptography](./dkg) — the protocol that generates the proofs +- [Commitment Consistency](./commitment-consistency) — how cross-circuit commitments are enforced +- [Noir Circuits](/noir-circuits) — toolchain setup and circuit compilation +- [Cryptography](/cryptography) — mathematical foundations diff --git a/docs/pages/sdk.mdx b/docs/pages/sdk.mdx index 61a321ac50..922c5b58b3 100644 --- a/docs/pages/sdk.mdx +++ b/docs/pages/sdk.mdx @@ -57,7 +57,8 @@ const sdk = new EnclaveSDK({ feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS, }, chain: sepolia, - thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', + // Use 'SECURE_THRESHOLD_8192' for production. 'INSECURE_THRESHOLD_512' is for local dev only. + thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192', }) ``` @@ -76,18 +77,29 @@ const sdk = EnclaveSDK.create({ }, chain: sepolia, privateKey: '0x...', // optional — omit for read-only - thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', + // Use 'SECURE_THRESHOLD_8192' for production. 'INSECURE_THRESHOLD_512' is for local dev only. + thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192', }) ``` ### BFV Parameter Presets -The SDK supports two BFV parameter presets: +The `thresholdBfvParamsPresetName` field selects the BFV encryption parameters used by the client. +It must match the `paramSet` index registered in the on-chain `Enclave` contract: -| Preset Name | Degree | Security | Use Case | -| ------------------------ | ------ | -------- | ---------------------------- | -| `INSECURE_THRESHOLD_512` | 512 | Insecure | Development and testing only | -| `SECURE_THRESHOLD_8192` | 8192 | Secure | Production deployments | +| Preset Name | On-chain `paramSet` | Degree | CRT moduli (L) | Security | Use case | +| ------------------------ | ------------------- | ------ | -------------- | -------- | ---------------------------------- | +| `INSECURE_THRESHOLD_512` | `0` | 512 | 2 | ❌ None | Local development and testing only | +| `SECURE_THRESHOLD_8192` | `1` | 8192 | 4 | ✅ Full | Production deployments | + +**Always use `SECURE_THRESHOLD_8192` in production.** The insecure preset uses a small polynomial +ring (N=512) with no cryptographic security guarantees — it exists solely to speed up local dev +cycles. The secure preset (N=8192) meets standard FHE security targets and is required for mainnet +and testnet deployments. + +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 @@ -145,7 +157,8 @@ export function Dashboard() { ciphernodeRegistry: import.meta.env.VITE_REGISTRY_ADDRESS, feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS, }, - thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', + // Use 'SECURE_THRESHOLD_8192' for production. 'INSECURE_THRESHOLD_512' is for local dev only. + thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192', }) useEffect(() => { @@ -186,8 +199,8 @@ const { encryptedData, circuitInputs } = await sdk.encryptNumberAndGenInputs(42n // Standalone import (pass preset name explicitly) import { generatePublicKey, encryptNumber } from '@enclave-e3/sdk' -const pk = await generatePublicKey('INSECURE_THRESHOLD_512') -const ct = await encryptNumber(42n, pk, 'INSECURE_THRESHOLD_512') +const pk = await generatePublicKey('SECURE_THRESHOLD_8192') +const ct = await encryptNumber(42n, pk, 'SECURE_THRESHOLD_8192') ``` ### Full encryption method reference diff --git a/examples/CRISP/Readme.md b/examples/CRISP/Readme.md index 666b8cdaea..22731193a0 100644 --- a/examples/CRISP/Readme.md +++ b/examples/CRISP/Readme.md @@ -6,7 +6,7 @@ cryptography (DTC) to enable verifiable secret ballots. Built with Enclave, CRIS democratic systems and decision-making applications against coercion, manipulation, and other vulnerabilities. To learn more about CRISP, you can read our [blog post](https://blog.enclave.gg/crisp-private-voting-secret-ballot-fhe-zkp-mpc/) or visit the -[documentation](https://docs.enclave.gg/CRISP/introduction). +[documentation](https://docs.theinterfold.com/CRISP/introduction). ## Project Structure @@ -29,7 +29,7 @@ CRISP/ ``` You can have an extended explanation of the single folders in the dedicated -[documentation](https://docs.enclave.gg/CRISP/introduction#project-structure). +[documentation](https://docs.theinterfold.com/CRISP/introduction#project-structure). ## Prerequisites diff --git a/examples/CRISP/client/src/pages/Landing/components/Hero.tsx b/examples/CRISP/client/src/pages/Landing/components/Hero.tsx index 60a1b85344..b35c32c0e4 100644 --- a/examples/CRISP/client/src/pages/Landing/components/Hero.tsx +++ b/examples/CRISP/client/src/pages/Landing/components/Hero.tsx @@ -50,7 +50,7 @@ const HeroSection: React.FC = () => {
This is a simple demonstration of CRISP technology.
Learn more.
diff --git a/examples/CRISP/packages/crisp-zk-inputs/README.md b/examples/CRISP/packages/crisp-zk-inputs/README.md index c275126eb2..463c936f77 100644 --- a/examples/CRISP/packages/crisp-zk-inputs/README.md +++ b/examples/CRISP/packages/crisp-zk-inputs/README.md @@ -1,29 +1,55 @@ -# Wasm bundle for crisp-zk-inputs +# @crisp-e3/zk-inputs -Here we export wasm functionality for consumption in TypeScript to enable us to share code between -Rust and TypeScript. +WASM bindings for generating CRISP ZK proof inputs, compiled from Rust and shared between the +server-side Node.js environment and the browser. This package lets the CRISP SDK produce the circuit +witness data needed for Noir-based vote-validity proofs without duplicating the logic in TypeScript. + +## What it generates + +The WASM module exposes functions for computing ZK circuit inputs for CRISP's two Noir circuits: + +- **Vote proof inputs** — proves a vote was cast by an eligible participant with a valid Merkle + membership witness. +- **Masked vote proof inputs** — same as above but with an additional blinding factor for additional + privacy. + +These inputs are then passed to `@noir-lang/noir_js` and `@aztec/bb.js` to generate the actual +proofs. ## Usage -This package exposes an `init` subpackage default function which should be used to universally load -the wasm module instead of exporting the default loader. +This package requires a universal init pattern because: + +- In **Node.js** (>=18) WASM can be loaded synchronously — no preloading needed. +- In the **browser** the WASM binary must be fetched and instantiated asynchronously. -This is because in modern node there is no need for preloading however in the browser we still need -to load the wasm bundle. +The `init` subpackage handles both environments transparently. -### ❌ DONT USE THE DEFAULT INIT +### ❌ Don't use the default export ```ts -// Bad! Because this uses the raw loader which doesn't exist in node contexts -import init, { bfvEncryptNumber } from '@crisp-e3/zk-inputs' +// Bad — the raw default loader doesn't work in Node.js contexts +import init, { generateVoteInputs } from '@crisp-e3/zk-inputs' ``` -### ✅ DO USE THE EXPORTED SUBMODULE +### ✅ Use the universal subpackage loader ```ts -// Good! Use the universal loader import init from '@crisp-e3/zk-inputs/init' +import { generateVoteInputs } from '@crisp-e3/zk-inputs' await init() -// other package imports here +const inputs = generateVoteInputs(/* ... */) +``` + +Call `init()` once before any other imports from `@crisp-e3/zk-inputs`. In browser environments +`init()` fetches the WASM binary; in Node.js it is a no-op. + +## Building + +The WASM bundle is compiled from the Rust source in `crates/crisp-zk-inputs` using `wasm-pack`: + +```bash +# From the CRISP root +pnpm build:wasm ``` diff --git a/examples/CRISP/program/README.md b/examples/CRISP/program/README.md index 63abac4784..986b1c4038 100644 --- a/examples/CRISP/program/README.md +++ b/examples/CRISP/program/README.md @@ -1,10 +1,10 @@ -# Program +# CRISP Program -This module does the following: +The program module runs the FHE computation at the heart of CRISP: it aggregates encrypted votes, +produces a Risc0 ZK proof of correct execution, and submits the result back to the on-chain CRISP +contract via the coordination server. -- Run an FHE computation given some inputs - -This is the program component for our overall CRISP architecture: +## Architecture ```mermaid graph TD @@ -32,3 +32,53 @@ graph TD end server -. "WebSocket listener" .-> evm ``` + +## What it computes + +CRISP uses BFV fully homomorphic encryption to tally votes without revealing individual inputs: + +1. The server collects BFV-encrypted vote ciphertexts from participants. +2. The program homomorphically adds all ciphertexts together to produce an encrypted tally. +3. A Risc0 guest program proves the aggregation was performed correctly. +4. The proof and ciphertext output are submitted on-chain; the Enclave ciphernode committee then + threshold-decrypts the result. + +## E3 Program Entry Points + +The CRISP Solidity contract implements `IE3Program` with three functions called by the Enclave +contract: + +| Function | When called | What it does | +| --------------- | ------------------------ | ------------------------------------------------------------------ | +| `validate` | On E3 request | Validates request parameters (e.g. duration, eligible voters) | +| `validateInput` | On each input submission | Checks that the submitted BFV ciphertext is well-formed | +| `verify` | On output publication | Verifies the Risc0 proof and that the ciphertext output is correct | + +## Proof Generation + +Proof generation is delegated to [Boundless](https://boundless.xyz) (a Risc0 proving service): + +- **Development / local:** The program can run the Risc0 guest directly (no Boundless needed). +- **Production:** Set the `BOUNDLESS_RPC_URL` and `BOUNDLESS_PRIVATE_KEY` environment variables to + submit jobs to the Boundless network. See [Boundless configuration](../README.md#configuration) in + the CRISP root README. + +## Environment Variables + +| Variable | Required | Description | +| ----------------------- | ---------- | ------------------------------------------------------------- | +| `RISC0_DEV_MODE` | Dev only | Set to `1` to skip real proof generation (fast local testing) | +| `BOUNDLESS_RPC_URL` | Production | RPC endpoint for Boundless proof submission | +| `BOUNDLESS_PRIVATE_KEY` | Production | Private key for paying Boundless proving fees | +| `PINATA_JWT` | Production | JWT for uploading the compiled guest binary to IPFS | +| `PROGRAM_URL` | Production | IPFS URL of the uploaded guest binary | + +## Running locally + +```bash +# From the CRISP root +pnpm dev:program +``` + +This starts the program HTTP server (default port 3001). The coordination server calls it when a new +E3 computation request arrives. diff --git a/packages/enclave-config/README.md b/packages/enclave-config/README.md index af6a6f834b..116011c78a 100644 --- a/packages/enclave-config/README.md +++ b/packages/enclave-config/README.md @@ -1,3 +1,38 @@ -# Enclave Config +# @enclave-e3/config -This is a package containing shared configuration for the Enclave monorepo. +Shared build tooling configuration for the Enclave monorepo. This is an **internal package** — it +does not export runtime code. It provides common `tsup`, TypeScript, and ESLint configurations +consumed by all other TypeScript packages. + +## Exported Configs + +| Export | File | Purpose | +| --------------------- | ------------------- | -------------------------------------------------- | +| `./tsup` | `tsup.config.js` | Shared tsup bundler config (ESM + CJS dual output) | +| `./tsconfig.json` | `tsconfig.json` | Base TypeScript config for library packages | +| `./dom.tsconfig.json` | `dom.tsconfig.json` | TypeScript config for browser/DOM packages | +| `./eslint.config.js` | `eslint.config.js` | Shared ESLint rules | + +## Usage + +Extend the shared TypeScript config in a package's `tsconfig.json`: + +```json +{ + "extends": "@enclave-e3/config/tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} +``` + +Import the shared tsup config in a package's `tsup.config.ts`: + +```ts +export { default } from '@enclave-e3/config/tsup' +``` + +## License + +LGPL-3.0-only diff --git a/packages/enclave-contracts/README.md b/packages/enclave-contracts/README.md index 8becd5eb25..77ba0839a8 100644 --- a/packages/enclave-contracts/README.md +++ b/packages/enclave-contracts/README.md @@ -1,5 +1,30 @@ # Enclave Smart Contracts +## Contract Overview + +| Contract | Description | +| ------------------------------- | ------------------------------------------------------------------------------------------------ | +| `Enclave.sol` | Main protocol coordinator — handles E3 requests, param sets, fee routing, and output publication | +| `CiphernodeRegistryOwnable.sol` | Ciphernode registration and committee selection | +| `BondingRegistry.sol` | ENCL token bonding for ciphernodes; tracks bond amounts and manages bond lifecycle | +| `EnclaveToken.sol` | ENCL governance/utility token | +| `EnclaveTicketToken.sol` | USDC-backed tickets used by ciphernodes for sortition entry | +| `SlashingManager.sol` | Fault attribution and slashing for dishonest ciphernodes (accusation → quorum → slash) | +| `E3RefundManager.sol` | Issues refunds to requesters when an E3 fails | +| `BfvDecryptionVerifier.sol` | On-chain ZK verifier for threshold decryption proofs (C6/C7) | +| `BfvPkVerifier.sol` | On-chain ZK verifier for public key generation proofs (C0/C1) | + +### Key Interfaces + +| Interface | Description | +| ------------------ | ----------------------------------------------------------------------------- | +| `IE3Program` | Implement this to write a custom E3 program (defines `validate` and `verify`) | +| `IEnclave` | External interface to the main Enclave contract | +| `IBondingRegistry` | Interface for bonding queries and management | +| `ISlashingManager` | Interface for accusation and slashing | +| `IE3RefundManager` | Interface for the refund manager | +| `IComputeProvider` | Interface for compute provider integration | + ## Importing the contracts, interfaces or types To install, run diff --git a/packages/enclave-mcp/README.md b/packages/enclave-mcp/README.md index be55f2a2fd..04274d1713 100644 --- a/packages/enclave-mcp/README.md +++ b/packages/enclave-mcp/README.md @@ -1,7 +1,8 @@ # @enclave-e3/mcp -MCP server for [Enclave](https://enclave.gg) documentation. Allows AI assistants to answer questions -about Enclave by fetching content directly from [docs.enclave.gg](https://docs.enclave.gg). +MCP server for [The Interfold](https://theinterfold.com) documentation. Allows AI assistants to +answer questions about The Interfold by fetching content directly from +[docs.theinterfold.com](https://docs.theinterfold.com). ## Requirements @@ -86,10 +87,10 @@ Edit `~/.codeium/windsurf/mcp_config.json`: Once configured, ask your AI assistant questions like: -- _"What is an E3 in Enclave?"_ +- _"What is an E3?"_ - _"How do I run a ciphernode?"_ -- _"Explain the Enclave architecture"_ -- _"Search the enclave docs for threshold encryption"_ +- _"Explain the Interfold architecture"_ +- _"Search the docs for threshold encryption"_ ## License diff --git a/packages/enclave-mcp/src/index.ts b/packages/enclave-mcp/src/index.ts index beaef3dab1..32e450f488 100644 --- a/packages/enclave-mcp/src/index.ts +++ b/packages/enclave-mcp/src/index.ts @@ -11,7 +11,7 @@ import pkg from '../package.json' with { type: 'json' } const { version } = pkg -const BASE_URL = 'https://docs.enclave.gg' +const BASE_URL = 'https://docs.theinterfold.com' const FETCH_TIMEOUT_MS = 10_000 interface DocPage { diff --git a/packages/enclave-sdk/README.md b/packages/enclave-sdk/README.md index 45b424c38f..2a8760ec4b 100644 --- a/packages/enclave-sdk/README.md +++ b/packages/enclave-sdk/README.md @@ -50,7 +50,8 @@ const sdk = new EnclaveSDK({ feeToken: '0x...', // Your ERC-20 fee token address }, chain: sepolia, - thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', + // Use 'SECURE_THRESHOLD_8192' for production; 'INSECURE_THRESHOLD_512' for local dev only + thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192', }) // Listen to events with the unified event system @@ -90,7 +91,8 @@ const sdk = EnclaveSDK.create({ }, chain: sepolia, privateKey: '0x...', // optional — omit for read-only - thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', + // Use 'SECURE_THRESHOLD_8192' for production; 'INSECURE_THRESHOLD_512' for local dev only + thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192', }) ``` @@ -208,7 +210,8 @@ function MyComponent() { feeToken: '0x...', }, autoConnect: true, - thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', + // Use 'SECURE_THRESHOLD_8192' for production; 'INSECURE_THRESHOLD_512' for local dev only + thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192', }) useEffect(() => { @@ -270,7 +273,8 @@ import { getThresholdBfvParamsSet, } from '@enclave-e3/sdk' -const presetName = 'INSECURE_THRESHOLD_512' +// Use 'SECURE_THRESHOLD_8192' for production; 'INSECURE_THRESHOLD_512' for local dev only +const presetName = 'SECURE_THRESHOLD_8192' const publicKey = await generatePublicKey(presetName) const encrypted = await encryptNumber(42n, publicKey, presetName) @@ -400,8 +404,16 @@ interface SDKConfig { } ``` -`thresholdBfvParamsPresetName` must be one of: `'INSECURE_THRESHOLD_512'` or -`'SECURE_THRESHOLD_8192'`. +`thresholdBfvParamsPresetName` selects the BFV parameter set used for encryption. It must match the +on-chain `paramSet` index registered in the Enclave contract: + +| Preset name | On-chain `paramSet` index | Use case | +| -------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `'INSECURE_THRESHOLD_512'` | `0` | Local development and testing only — small polynomial degree (N=512), fast but not cryptographically secure | +| `'SECURE_THRESHOLD_8192'` | `1` | Production — full security parameters (N=8192, L=4 CRT moduli) | + +Always use `'SECURE_THRESHOLD_8192'` in production. The insecure preset exists solely to speed up +local dev cycles. ## Error Handling From f93c3166795d3fdb58ddcf30305e1d6ff4368872 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:18:51 +0100 Subject: [PATCH 2/6] docs: cleanup --- README.md | 4 ++-- .../CRISP/packages/crisp-zk-inputs/README.md | 23 +++++++++++++------ packages/enclave-contracts/README.md | 2 -- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0f30f1f78d..6071550e26 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ The monorepo provides several test scripts for different components: - **`pnpm test:integration`** - Runs integration tests from `tests/integration/`. These tests may require prebuilt binaries and can be run with `--no-prebuild` if binaries are already available. - When running with secure (production) BFV parameters, pre-built circuit artifacts for the - `secure-8192` preset must be present in the `circuits/` artifacts directory. + Pre-built circuit artifacts for the configured BFV preset must be present in the `circuits/` + artifacts directory. #### Running Individual Test Suites diff --git a/examples/CRISP/packages/crisp-zk-inputs/README.md b/examples/CRISP/packages/crisp-zk-inputs/README.md index 463c936f77..9cc0ebce6b 100644 --- a/examples/CRISP/packages/crisp-zk-inputs/README.md +++ b/examples/CRISP/packages/crisp-zk-inputs/README.md @@ -6,15 +6,24 @@ witness data needed for Noir-based vote-validity proofs without duplicating the ## What it generates -The WASM module exposes functions for computing ZK circuit inputs for CRISP's two Noir circuits: +The WASM module wraps a `ZKInputsGenerator` class that performs BFV encryption and produces the +witness data needed for CRISP's Noir circuits. Two main proof types are supported: -- **Vote proof inputs** — proves a vote was cast by an eligible participant with a valid Merkle - membership witness. -- **Masked vote proof inputs** — same as above but with an additional blinding factor for additional - privacy. +- **Vote proof** (`generateInputs`) — encrypts a vote under the committee's threshold BFV public key + and produces a witness proving the vote is correctly encrypted and that the voter is eligible + (e.g. holds the required token balance, verified via a Merkle membership proof). -These inputs are then passed to `@noir-lang/noir_js` and `@aztec/bb.js` to generate the actual -proofs. +- **Vote update / mask proof** (`generateInputsForUpdate`) — same structure, but used for revotes or + masker contributions under the + [vote masking](https://blog.theinterfold.com/vote-masking-receipt-freeness-secret-ballots/) scheme + that provides receipt-freeness. Unlike the first-vote path, this preserves the real + `prev_ct_commitment` (rather than zeroing it) to chain updates together. + +The generator also exposes `encryptVote` / `decryptVote` for standalone BFV operations and +`generateKeys` for key generation. + +These witness objects are then passed to `@noir-lang/noir_js` and `@aztec/bb.js` to generate the +actual ZK proofs. ## Usage diff --git a/packages/enclave-contracts/README.md b/packages/enclave-contracts/README.md index 77ba0839a8..962dbe3b3c 100644 --- a/packages/enclave-contracts/README.md +++ b/packages/enclave-contracts/README.md @@ -11,8 +11,6 @@ | `EnclaveTicketToken.sol` | USDC-backed tickets used by ciphernodes for sortition entry | | `SlashingManager.sol` | Fault attribution and slashing for dishonest ciphernodes (accusation → quorum → slash) | | `E3RefundManager.sol` | Issues refunds to requesters when an E3 fails | -| `BfvDecryptionVerifier.sol` | On-chain ZK verifier for threshold decryption proofs (C6/C7) | -| `BfvPkVerifier.sol` | On-chain ZK verifier for public key generation proofs (C0/C1) | ### Key Interfaces From 6a19ec981664e79bc7d18ffcef58188f1c3794e4 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:25:36 +0100 Subject: [PATCH 3/6] ci: trigger From 3875062f81e50c351de900d31e0947dbb9379c54 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:28:33 +0100 Subject: [PATCH 4/6] chore: pr comments --- examples/CRISP/packages/crisp-zk-inputs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/CRISP/packages/crisp-zk-inputs/README.md b/examples/CRISP/packages/crisp-zk-inputs/README.md index 9cc0ebce6b..d11058bf3f 100644 --- a/examples/CRISP/packages/crisp-zk-inputs/README.md +++ b/examples/CRISP/packages/crisp-zk-inputs/README.md @@ -51,7 +51,7 @@ await init() const inputs = generateVoteInputs(/* ... */) ``` -Call `init()` once before any other imports from `@crisp-e3/zk-inputs`. In browser environments +Call `init()` once before using any other imports from `@crisp-e3/zk-inputs`. In browser environments `init()` fetches the WASM binary; in Node.js it is a no-op. ## Building From 5c008c5a5fda76cc41aee06d517039a6467d3265 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:16:39 +0100 Subject: [PATCH 5/6] chore: update terminology --- docs/pages/internals/dkg.mdx | 253 ++++++++++++------ docs/pages/internals/zk-proofs.mdx | 26 +- .../CRISP/packages/crisp-zk-inputs/README.md | 4 +- 3 files changed, 184 insertions(+), 99 deletions(-) diff --git a/docs/pages/internals/dkg.mdx b/docs/pages/internals/dkg.mdx index 7a6cd59ef7..aa5f015012 100644 --- a/docs/pages/internals/dkg.mdx +++ b/docs/pages/internals/dkg.mdx @@ -7,28 +7,47 @@ description: # DKG & Threshold Cryptography — Technical Deep Dive -After [sortition](./sortition) selects a committee, the selected ciphernodes run a Distributed Key -Generation (DKG) protocol to produce a **collective public key** that users encrypt to. No single -party ever holds the full secret key — instead, each node holds a Shamir share, and any -`threshold_m` of `N` committee members can collaboratively decrypt. +After [sortition](./sortition) selects a committee, the selected ciphernodes run a Publicly +Verifiable Distributed Key Generation (PVDKG) protocol to produce a **threshold public key** that +users encrypt to. No single party ever holds the full secret key — instead, each node holds a Shamir +share, and any `T+1` of `N` committee members can collaboratively decrypt (where `T` is the +threshold parameter, configurable per E3 program). The scheme is built on **Threshold Ring-BFV** (TrBFV), which extends the standard BFV fully-homomorphic encryption scheme to a threshold setting. --- +## Terminology + +Following the conventions from the PVDKG specification: + +| Term | Definition | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| **Individual key pair** | A BFV key pair each ciphernode holds independently, used to encrypt/decrypt shares exchanged during DKG | +| **Secret key contribution** | A single ciphernode's contribution to the threshold secret key | +| **Public key share** | The BFV public key corresponding to a secret key contribution | +| **Secret key share** | A ciphernode's _Shamir_ share of the threshold secret key (received from other nodes, aggregated locally) | +| **Threshold public key** | The jointly generated BFV public key; no single party holds the corresponding secret key | +| **Smudging noise contribution** | A ciphernode's contribution to the collective smudging noise (prevents information leakage during decryption) | +| **Smudging noise share** | A ciphernode's _Shamir_ share of the smudging noise | +| **Partial decryption** | A ciphernode's share over a decrypted value, computed before the rounding step | +| **Authorised set (A)** | The set of ciphernodes allowed to participate; starts as all N and shrinks as malicious nodes are detected | + +--- + ## Two Key Hierarchies -A common source of confusion: there are **two separate keypairs** in the protocol. +There are **two separate key pairs** in the protocol — a common source of confusion: | Key type | Scheme | Purpose | Lifetime | | ------------------ | ------------ | -------------------------------------------------------------------- | ------------------ | -| **Individual key** | Standard BFV | Encrypt Shamir shares in transit during DKG | One DKG session | +| **Individual key** | Standard BFV | Encrypt Shamir shares in transit during DKG (like a secure channel) | One DKG session | | **Threshold key** | TrBFV | The collective key users encrypt to; used for homomorphic operations | One E3 computation | -The individual BFV key is proved correct in **C0**. The threshold key contributions are proved in -**C1**. They live in different polynomial rings (the individual key uses a smaller "DKG" parameter -set optimized for encryption). +The individual public key is committed in **C0**. The threshold key contributions are proved correct +in **C1**. They live in different polynomial rings — the individual key uses a smaller "DKG" +parameter set optimised for share encryption. --- @@ -40,41 +59,42 @@ Each ciphernode runs a `ThresholdKeyshare` actor that progresses through a stric Init │ CiphernodeSelected arrives ▼ -CollectingEncryptionKeys ← Generate BFV keypair; prove with C0; wait for all N peers +CollectingEncryptionKeys ← Generate individual BFV keypair; commit with C0; wait for all N peers │ All N keys collected ▼ -GeneratingThresholdShare ← Generate TrBFV contribution (C1); Shamir-split sk and e_sm; - │ encrypt shares (C3); prove share computation (C2) +GeneratingThresholdShare ← Generate secret key contribution + public key share (C1); + │ Shamir-split and verify shares (C2); encrypt shares (C3) │ All N shares collected ▼ -AggregatingDecryptionKey ← Decrypt received shares; prove decryption (C4); - │ aggregate into local sk/e_sm shares +AggregatingDecryptionKey ← Decrypt received shares; verify against C2 commitments (C4); + │ aggregate into local secret key share and smudging noise share │ C4 proofs verified ▼ -ReadyForDecryption ← Waiting for ciphertext output from the FHE evaluation +ReadyForDecryption ← Waiting for ciphertext output from the FHE computation │ CiphertextOutputPublished arrives ▼ -Decrypting ← Compute partial decryption share (C6) +Decrypting ← Compute partial decryption (C6) │ ▼ -GeneratingDecryptionProof ← [Aggregator only] Collect shares; Lagrange interpolation (C7) +GeneratingDecryptionProof ← [Aggregator only] Collect partial decryptions; Lagrange interpolation (C7) │ ▼ Completed ``` Each transition requires both a cryptographic operation and successful ZK proof -generation/verification. If any step times out, the node can be expelled from the committee. +generation/verification. If any step times out, the node can be expelled from the committee and the +authorised set **A** is updated. --- -## Phase 1: Individual Key Exchange (C0) +## Phase 1: Individual Key Commitment (C0) ``` CiphernodeSelected event arrives at ThresholdKeyshare │ ├─ BFV::keygen(dkg_params) → (sk_bfv, pk_bfv) - │ This is the INDIVIDUAL node encryption key (NOT the threshold key) + │ This is the INDIVIDUAL key (NOT the threshold key) │ ├─ Encrypt sk_bfv at rest with local password (AES-GCM via Cipher) │ @@ -87,58 +107,72 @@ CiphernodeSelected event arrives at ThresholdKeyshare → Start immediately so early-arriving peer data is buffered ``` -**C0** proves the BFV keypair was generated correctly — specifically that `pk = (-a·sk + e, a)` for -a validly sampled secret `sk` and error `e`. Receiving nodes verify C0 before trusting the public -key. +**C0** takes the individual public key components (`pk_0`, `pk_1` — polynomial arrays over L CRT +moduli) and produces a cryptographic **commitment**. This commitment is consumed downstream by +**C3**, which verifies that the recipient's public key used for share encryption matches the one +committed in C0. C0 itself does not prove key generation correctness — its role is binding the +individual public key for later verification. --- ## Phase 2: Threshold Share Generation (C1, C2, C3) -Once all N individual public keys are collected, each node generates its threshold contribution: +Once all N individual public keys are collected, each node generates its threshold contribution. + +### Step 1 — Generation (C1) -### Step 1 — TrBFV Key Share (C1) +Each ciphernode generates its secret key contribution, the corresponding public key share, and its +smudging noise contribution. **C1** proves that the public key share was correctly derived from the +secret key contribution using the BFV key generation equation: ``` -GenPkShareAndSkSss request: - ├─ TRBFV::gen_pk_share(sk, crp, params) → PublicKeyShare - │ pk0_share = -crp·sk + eek + r2·(γ^N+1) + r1·q - │ - ├─ ShareManager::create_shares(sk, M, N) → N Shamir shares of the secret key - │ - └─ Sample smudging noise e_sm with λ-bit statistical security - ShareManager::create_shares(e_sm, M, N) → N Shamir shares of e_sm +pk_0 = -a · sk + e + r₂ · (X^N + 1) + r₁ · q +pk_1 = a ``` -**C1** proves the public key share was correctly derived from the secret key, and that the smudging -noise has the correct distribution. The Common Random Polynomial (CRP) is a fixed deterministic -value derived from a default seed per BFV preset — all nodes using the same preset produce the same -CRP. +where `a` is the Common Random Polynomial (CRP) — a fixed deterministic value derived from a default +seed per BFV preset. All nodes using the same preset produce the same CRP. The equation is verified +at a single random challenge point via the +[Schwartz-Zippel Lemma](https://en.wikipedia.org/wiki/Schwartz%E2%80%93Zippel_lemma), avoiding the +cost of checking every coefficient individually. + +The node then Shamir-splits its secret key contribution and smudging noise contribution into N +shares each (this happens outside the circuit). C1 outputs three commitments that feed downstream +circuits: -### Step 2 — Share Computation Proof (C2a / C2b) +1. `commit(sk_contribution)` → consumed by **C2a** +2. `commit(e_sm_contribution)` → consumed by **C2b** +3. `commit(pk_share)` → consumed by **C5** in P2 -C2 runs separately for the secret key shares (C2a) and smudging noise shares (C2b). Each C2 circuit: +### Step 2 — Share Computation (C2a / C2b) -1. **Verifies Shamir structure** via a parity check: `H · y^T ≡ 0 (mod q_j)` for each CRT modulus, - where `H` is a Reed–Solomon parity matrix. This proves the shares lie on a polynomial of degree ≤ - M without revealing the polynomial -2. **Outputs per-party commitments**: `hash(share_polynomial)` for each recipient party and each CRT - modulus — producing an `N × L` grid of commitment values +C2 runs separately for the secret key shares (C2a) and smudging noise shares (C2b). Each C2 circuit +verifies two properties: -These commitments are the bridge to C4 (the recipient verifies they received the same shares that -were committed to). +1. **Consistency with C1**: the shares were built from the correct secret (the committed + `sk_contribution` or `e_sm_contribution`) +2. **Valid secret sharing**: the shares form a valid Reed-Solomon codeword. A parity check matrix + `H` is constructed such that: `H_j · y_{i,j}^T ≡ 0 (mod q_j)` — guaranteeing the shares lie on a + polynomial of degree at most T without ever reconstructing the polynomial itself + +The circuit outputs a commitment for each party's share per CRT modulus — an `N × L` grid of +commitment values. These commitments are the bridge to C4: the recipient verifies the decrypted +shares match what was committed here. ### Step 3 — Share Encryption (C3a / C3b) -Each share is BFV-encrypted under the recipient's individual public key (from C0). C3 proves: +Each share must be encrypted before transmission. C3a handles secret key shares and C3b handles +smudging noise shares. The circuit verifies two commitment links simultaneously: -``` -ct0 = pk0·u + e0 + message + r1·q + r2·(γ^N+1) -ct1 = pk1·u + e1 + p1·q + p2·(γ^N+1) -``` +- The **plaintext** being encrypted matches the share commitment from C2 +- The **recipient's individual public key** matches the commitment from C0 -This generates `(N-1) × L` proofs per variant (one for each recipient × CRT modulus). In a 3-node -committee with L=4 CRT moduli, that's 8 C3a proofs and 8 C3b proofs per node. +Together these checks guarantee that the correct share is encrypted under the correct recipient's +key — without exposing the share in plaintext. The BFV encryption equations are verified via +Schwartz-Zippel at a Fiat-Shamir challenge point (same technique as C1). + +C3 produces no new commitments — its role is purely to verify correct encryption. It has the highest +proof count: `|A| × (|A| - 1)` proof instances per variant (C3a and C3b). ### Proof Publication @@ -149,72 +183,123 @@ complete does the node broadcast its `ThresholdShareCreated` message containing: - Encrypted Shamir shares (from C3) - All signed proofs (C1, C2a, C2b, C3a[], C3b[]) -This atomic publication prevents partial data from reaching peers. +This atomic publication prevents partial data from reaching peers. Any party whose proofs fail +verification is excluded from the authorised set **A**. --- ## Phase 3: Share Decryption and Aggregation (C4) -Each node decrypts the encrypted shares it received from all other honest parties using its +Each node decrypts the encrypted shares it received from all other authorised parties using its individual BFV secret key. C4 then: -1. **Verifies commitment consistency**: Each decrypted share is hashed and compared against the - commitment the sender published in C2. If they don't match, the sender was dishonest -2. **Aggregates**: Sums all decrypted shares coefficient-wise per CRT modulus: - `agg_sk[ℓ][i] = Σ_h share_h[ℓ][i]` -3. **Normalizes**: Reduces mod `q_ℓ`, reverses coefficient order, and centers to match C6's +1. **Verifies commitment consistency**: each decrypted share is hashed and compared against the + commitment the sender published in C2. Since commitments are binding, producing a different share + with the same commitment is computationally infeasible +2. **Aggregates**: sums all verified shares coefficient-wise per CRT modulus: + `agg[ℓ][i] = Σ_{a ∈ A} share[a][ℓ][i] (mod q_ℓ)`. This reflects the threshold key construction + directly — the secret key corresponding to the threshold public key equals the sum of all secret + key contributions (`sk = Σ sk_i`), so after aggregation each node holds a Shamir share of this + sum +3. **Normalizes**: reduces mod `q_ℓ`, reverses coefficient order, and centers to match C6's representation -4. **Outputs**: A single commitment to the aggregated share polynomial, consumed by C6 +4. **Outputs**: `commit(sk_agg)` for C4a and `commit(e_sm_agg)` for C4b — these two commitments form + the final link between P1 and P4 After C4, each node holds: -- Its share of the threshold secret key (`agg_sk`) -- Its share of the smudging noise (`agg_e_sm`) +- Its **secret key share** (`agg_sk`) — a Shamir share of the threshold secret key +- Its **smudging noise share** (`agg_e_sm`) — a Shamir share of the smudging noise -These are the only long-lived secrets from the DKG. +These are the only long-lived secrets from the DKG. Each variant (C4a and C4b) runs once per +ciphernode in **A**. --- -## Phase 4: Public Key Aggregation (C5) +## P2: Public Key Aggregation (C5) -The aggregator (party_id=0) collects all N public key shares from C1 and sums them: +The aggregator (party_id=0) collects all public key shares from C1 and sums them: ``` -pk = Σ pk_share_i +pk_threshold = Σ pk_share_i for i ∈ A ``` -C5 proves this addition was done correctly. The result is the **threshold public key** that users -encrypt their data to. It is published on-chain via `publishCommittee`. +Before performing the aggregation, C5 confirms that each public key share matches its commitment +from C1 — ensuring no party has substituted a different value since key generation. The circuit +outputs `commit(pk_threshold)`, which is posted on-chain as the encryption key commitment. Users +verify this commitment before encrypting their private inputs in P3. + +This circuit runs once, executed by the aggregator. + +--- + +## P3: User Encryption + +With the threshold public key published, users encrypt their private inputs using a specialisation +of the [GRECO](https://blog.theinterfold.com/enclave-cryptography-greco-fhe-zk/) circuit. This +proves each ciphertext was formed correctly under the threshold public key without revealing the +plaintext or encryption randomness. The circuit verifies `commit(pk_threshold)` from C5 before +validating the encryption. + +The resulting ciphertexts are collected and passed to the compute provider, which executes the E3 +program homomorphically over the encrypted data (e.g. a private vote tally). Only the encrypted +output, along with a proof of correct execution, is published on-chain. --- -## Threshold Decryption (C6, C7) +## P4: Threshold Decryption (C6, C7) -Multiple users submit BFV-encrypted inputs during the E3 input window. A compute provider then -evaluates the E3 program homomorphically over those ciphertexts (e.g. adding them for a tally, or -multiplying for other computations) and publishes the resulting ciphertext output on-chain. At that -point, each committee member computes a partial decryption share of the evaluated result: +After the homomorphic computation completes, the encrypted output must be decrypted. No single party +can do this alone — each authorised ciphernode independently computes a **partial decryption** from +its secret key share and smudging noise share. ### C6 — Partial Decryption ``` -d[ℓ](γ) = ct0[ℓ](γ) + ct1[ℓ](γ)·sk[ℓ](γ) + e_sm[ℓ](γ) + r2[ℓ](γ)·(γ^N+1) + r1[ℓ](γ)·q_ℓ +d[ℓ] = ct0[ℓ] + ct1[ℓ] · sk[ℓ] + e_sm[ℓ] + r₂[ℓ] · (X^N + 1) + r₁[ℓ] · q_ℓ ``` -The smudging noise `e_sm` is added to prevent leaking information about the node's secret key share -through the partial decryption. C6 proves this computation was done correctly with the same `sk` and -`e_sm` committed in C4a/C4b. +The smudging noise `e_sm` prevents information leakage — without it, the partial decryption share +would reveal information about the node's secret key share. C6 first verifies that `sk` and `e_sm` +match their commitments from C4a and C4b (range checks are not repeated — P1 already performed them, +and commitment binding guarantees they are unchanged). The decryption equation is verified via +Schwartz-Zippel at a Fiat-Shamir challenge point, following the same pattern as C1 and C3. + +The circuit outputs `commit(d)`, passed to C7 for aggregation. This circuit runs at least `T+1` +times — once for each ciphernode contributing a valid partial decryption. ### C7 — Lagrange Reconstruction -The aggregator collects ≥ `threshold_m` partial shares and reconstructs the plaintext: +The aggregator collects ≥ `T+1` valid partial decryptions and reconstructs the plaintext in three +steps: 1. **Lagrange interpolation** at zero: `u[ℓ] = Σ_i d_i[ℓ] · L_i(0) (mod q_ℓ)` where - `L_i(0) = ∏_{j≠i} (-x_j) / (x_i - x_j)` -2. **CRT reconstruction**: Stitch `u[0..L-1]` into a single value via the Chinese Remainder Theorem -3. **Decoding**: Map from the ciphertext ring back to the plaintext space + `L_i(0) = ∏_{j≠i} (-x_j) / (x_i - x_j)`. The Lagrange coefficients are computed in-circuit, + preventing any party from manipulating them +2. **CRT reconstruction**: stitch `u[0..L-1]` into a single global value `u_global` via the Chinese + Remainder Theorem: `u[ℓ] + r[ℓ] · q_ℓ = u_global` +3. **Decoding**: `message = -Q⁻¹ · (t · u_global)_Q mod t` where Q is the product of all CRT moduli + and t is the plaintext modulus. The decoding handles centered representation so coefficients are + correctly recovered within `[0, t)` + +C7 proves all three steps were done correctly. The output is the final plaintext — the result of the +encrypted computation, publicly verifiable by anyone. This circuit runs once, executed by the +aggregator. + +--- + +## Commitment Chain + +Circuits are not isolated — they are linked through cryptographic commitments using +[SAFE](https://hackmd.io/@7dpNYqjKQGeYC7wMlPxHtQ/ByIbpfX9c), a hashing framework built on Keccak256 +and Poseidon2. Each circuit commits to its outputs; downstream circuits take the expected commitment +as a public input and the underlying values as private inputs, recompute the commitment inside the +circuit, and verify that it matches. This is how, for example, C1 connects to C5: C5 verifies the +commitment produced by C1 without the raw polynomials ever appearing on-chain — only the compact +commitment is published. -C7 proves all three steps were done correctly. The final plaintext is published on-chain. +See [Commitment Consistency](./commitment-consistency) for how these cross-circuit links are +enforced at runtime. --- @@ -238,7 +323,7 @@ separately for each preset, so both the prover and verifier must use matching pa ## Related -- [ZK Proof Pipeline](./zk-proofs) — how each circuit is proved, verified, and aggregated +- [ZK Proof Pipeline](./zk-proofs) — how each circuit is proved, verified, and broadcast - [Commitment Consistency](./commitment-consistency) — how cross-circuit commitments are enforced - [Sortition](./sortition) — how the committee is selected before DKG begins - [Cryptography](/cryptography) — mathematical foundations (user-facing overview) diff --git a/docs/pages/internals/zk-proofs.mdx b/docs/pages/internals/zk-proofs.mdx index 433c1eeb43..ea80278539 100644 --- a/docs/pages/internals/zk-proofs.mdx +++ b/docs/pages/internals/zk-proofs.mdx @@ -18,19 +18,19 @@ The pipeline uses **Noir** circuits compiled to **UltraHonk** proofs via ## Circuit Map -| ID | Circuit | Phase | Proves | Source → Target | -| ------- | ---------------------------- | ------ | --------------------------------------------------------------------------------- | -------------------------- | -| **C0** | `PkBfv` | P1 DKG | Individual BFV keypair was generated correctly | Each node → all peers | -| **C1** | `PkGeneration` | P1 DKG | TrBFV public key share is correctly derived from secret key | Each node → aggregator | -| **C2a** | `SkShareComputation` | P1 DKG | Secret key Shamir shares satisfy parity constraints | Each node → all peers | -| **C2b** | `ESmShareComputation` | P1 DKG | Smudging noise Shamir shares satisfy parity constraints | Each node → all peers | -| **C3a** | `ShareEncryption` (sk) | P1 DKG | BFV encryption of secret key shares is valid | Per sender → per recipient | -| **C3b** | `ShareEncryption` (e_sm) | P1 DKG | BFV encryption of smudging noise shares is valid | Per sender → per recipient | -| **C4a** | `DkgShareDecryption` (sk) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local threshold key share | Each node → all peers | -| **C4b** | `DkgShareDecryption` (e_sm) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local smudging noise share | Each node → all peers | -| **C5** | `PkAggregation` | P2 Agg | Sum of all pk shares equals the published threshold public key | Aggregator → on-chain | -| **C6** | `ThresholdShareDecryption` | P4 Dec | Partial decryption share uses correct sk and e_sm from C4 | Each node → aggregator | -| **C7** | `DecryptedSharesAggregation` | P4 Dec | Lagrange reconstruction + CRT + decoding is correct | Aggregator → on-chain | +| ID | Circuit | Phase | Proves | Source → Target | +| ------- | ---------------------------- | ------ | -------------------------------------------------------------------------------------------- | -------------------------- | +| **C0** | `PkBfv` | P1 DKG | Commits to individual BFV public key (binding for C3) | Each node → all peers | +| **C1** | `PkGeneration` | P1 DKG | Public key share correctly derived from secret key contribution (Schwartz-Zippel on BFV eq.) | Each node → all peers | +| **C2a** | `SkShareComputation` | P1 DKG | Secret key Shamir shares consistent with C1 commitment and satisfy Reed-Solomon parity | Each node → all peers | +| **C2b** | `ESmShareComputation` | P1 DKG | Smudging noise Shamir shares consistent with C1 commitment and satisfy Reed-Solomon parity | Each node → all peers | +| **C3a** | `ShareEncryption` (sk) | P1 DKG | BFV encryption of secret key share uses correct share (C2) and correct recipient pk (C0) | Per sender → per recipient | +| **C3b** | `ShareEncryption` (e_sm) | P1 DKG | BFV encryption of smudging noise share uses correct share (C2) and correct recipient pk (C0) | Per sender → per recipient | +| **C4a** | `DkgShareDecryption` (sk) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local secret key share | Each node → all peers | +| **C4b** | `DkgShareDecryption` (e_sm) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local smudging noise share | Each node → all peers | +| **C5** | `PkAggregation` | P2 Agg | Sum of all pk shares (verified against C1 commitments) equals published threshold public key | Aggregator → on-chain | +| **C6** | `ThresholdShareDecryption` | P4 Dec | Partial decryption uses correct secret key share and smudging noise share from C4 | Each node → aggregator | +| **C7** | `DecryptedSharesAggregation` | P4 Dec | Lagrange interpolation + CRT reconstruction + decoding is correct | Aggregator → on-chain | --- diff --git a/examples/CRISP/packages/crisp-zk-inputs/README.md b/examples/CRISP/packages/crisp-zk-inputs/README.md index d11058bf3f..573ca8cc6d 100644 --- a/examples/CRISP/packages/crisp-zk-inputs/README.md +++ b/examples/CRISP/packages/crisp-zk-inputs/README.md @@ -51,8 +51,8 @@ await init() const inputs = generateVoteInputs(/* ... */) ``` -Call `init()` once before using any other imports from `@crisp-e3/zk-inputs`. In browser environments -`init()` fetches the WASM binary; in Node.js it is a no-op. +Call `init()` once before using any other imports from `@crisp-e3/zk-inputs`. In browser +environments `init()` fetches the WASM binary; in Node.js it is a no-op. ## Building From f17b0b62d524c82681dd98c482e82266e7f00afb Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:57:04 +0100 Subject: [PATCH 6/6] chore: pr comments --- docs/pages/cryptography.mdx | 10 +- docs/pages/internals/_meta.json | 5 +- .../internals/commitment-consistency.mdx | 33 +-- docs/pages/internals/dkg.mdx | 207 +++++++++++++++--- docs/pages/internals/sortition.mdx | 2 +- docs/pages/internals/zk-proofs.mdx | 185 ---------------- 6 files changed, 196 insertions(+), 246 deletions(-) delete mode 100644 docs/pages/internals/zk-proofs.mdx diff --git a/docs/pages/cryptography.mdx b/docs/pages/cryptography.mdx index d663ff2f90..29c43e7b5c 100644 --- a/docs/pages/cryptography.mdx +++ b/docs/pages/cryptography.mdx @@ -157,11 +157,11 @@ sequenceDiagram ## Publicly verifiable threshold BFV (PV-TBFV) -**PV-TBFV** means **publicly verifiable threshold Ring-BFV**: every sensitive step you would worry -about in a centralised system—generating key material, moving it under encryption, aggregating -public keys, taking user ciphertexts, emitting decryption shares—is backed by a statement that -verifiers can check **without** seeing the private witnesses. The DKG slice of that story is what we -call **PVDKG**; in the repository it lands in **C0** through **C4** (with +**PV-TBFV** means **publicly verifiable threshold BFV**: every sensitive step you would worry about +in a centralised system—generating key material, moving it under encryption, aggregating public +keys, taking user ciphertexts, emitting decryption shares—is backed by a statement that verifiers +can check **without** seeing the private witnesses. The DKG slice of that story is what we call +**PVDKG**; in the repository it lands in **C0** through **C4** (with [**C1**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_generation) living under [`circuits/bin/threshold`](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold) diff --git a/docs/pages/internals/_meta.json b/docs/pages/internals/_meta.json index d95183b82b..6cb8f0df9f 100644 --- a/docs/pages/internals/_meta.json +++ b/docs/pages/internals/_meta.json @@ -3,10 +3,7 @@ "title": "Sortition" }, "dkg": { - "title": "DKG & Threshold Cryptography" - }, - "zk-proofs": { - "title": "ZK Proof Pipeline" + "title": "PVDKG & ZK Proofs" }, "commitment-consistency": { "title": "Commitment Consistency" diff --git a/docs/pages/internals/commitment-consistency.mdx b/docs/pages/internals/commitment-consistency.mdx index 6e40cc45e8..5785a8ca79 100644 --- a/docs/pages/internals/commitment-consistency.mdx +++ b/docs/pages/internals/commitment-consistency.mdx @@ -2,15 +2,15 @@ title: 'Commitment Consistency' description: 'How the Interfold protocol detects dishonest ciphernodes by cross-checking commitment values - across ZK circuit proofs — link types, scoping rules, and the C2→C4 check' + across ZK circuit proofs.' --- -# Commitment Consistency — Technical Deep Dive +# Commitment Consistency -The [ZK proof pipeline](./zk-proofs) ensures each individual circuit execution is correct. But a -dishonest node could still cheat by using **different inputs** across circuits — for example, -committing to one set of shares in C2 but using different shares in C4. Each proof would verify in -isolation, but the overall protocol output would be wrong. +The [ZK proof pipeline](./dkg) ensures each individual circuit execution is correct. But a dishonest +node could still cheat by using **different inputs** across circuits — for example, committing to +one set of shares in C2 but using different shares in C4. Each proof would verify in isolation, but +the overall protocol output would be wrong. The **commitment consistency checker** closes this gap by cross-referencing public signals across circuit proofs. If a commitment value produced by one circuit doesn't match the expected value in @@ -28,7 +28,7 @@ The checker is a per-E3 actor (`CommitmentConsistencyChecker`) that: It operates in two modes: -### Pre-ZK Gating (fast path) +**Pre-ZK Gating (fast path)** Before expensive ZK verification, the `ShareVerificationActor` sends all received proofs to the checker in a batch via `CommitmentConsistencyCheckRequested`. The checker evaluates all links and @@ -37,7 +37,7 @@ returns a set of `inconsistent_parties` that are **excluded from ZK verification This is a significant optimization — a dishonest party caught by commitment checks never wastes resources on ZK verification. -### Post-ZK Cross-Check (defense in depth) +**Post-ZK Cross-Check (defense in depth)** After ZK verification passes, the checker receives `ProofVerificationPassed` events and re-evaluates links. This catches cases where a proof from one phase arrives after the initial batch, or where a @@ -147,21 +147,6 @@ Given C2 sender `X` (party_id) and C4 recipient `R` (party_id): If any modulus commitment differs, sender X is accused. -### The L Parameter - -The block size `L` (number of CRT moduli) varies by BFV preset: - -| Preset | L | -| ------------------------ | --- | -| `INSECURE_THRESHOLD_512` | 2 | -| `SECURE_THRESHOLD_8192` | 4 | - -The `L` used in the check **must match the actual preset** for the E3 — otherwise every index -calculation is wrong and the check produces false positives. This is why the checker reads `L` from -`E3Meta.params_preset` at E3 startup rather than using a hardcoded default. - ---- - ## Deduplication The same proof can arrive via two paths: @@ -204,6 +189,6 @@ response, preventing them from consuming ZK verification resources. ## Related -- [ZK Proof Pipeline](./zk-proofs) — the circuits whose commitments are being cross-checked +- [ZK Proof Pipeline](./dkg) — the circuits whose commitments are being cross-checked - [DKG & Threshold Cryptography](./dkg) — the protocol that generates the proofs - [Sortition](./sortition) — committee selection before any of this begins diff --git a/docs/pages/internals/dkg.mdx b/docs/pages/internals/dkg.mdx index aa5f015012..128629f360 100644 --- a/docs/pages/internals/dkg.mdx +++ b/docs/pages/internals/dkg.mdx @@ -1,27 +1,32 @@ --- -title: 'DKG & Threshold Cryptography' +title: 'PVDKG & ZK Proof Pipeline' description: - 'How ciphernodes collectively generate a threshold public key without any party learning the full - secret — BFV keypairs, Shamir sharing, TrBFV, and the DKG state machine' + 'How ciphernodes collectively generate a threshold public key, prove every step with + zero-knowledge circuits (C0–C7), and verify proofs across the network' --- -# DKG & Threshold Cryptography — Technical Deep Dive +# PVDKG & ZK Proof Pipeline After [sortition](./sortition) selects a committee, the selected ciphernodes run a Publicly Verifiable Distributed Key Generation (PVDKG) protocol to produce a **threshold public key** that -users encrypt to. No single party ever holds the full secret key — instead, each node holds a Shamir +users encrypt to. No single party ever holds the full secret key, instead, each node holds a Shamir share, and any `T+1` of `N` committee members can collaboratively decrypt (where `T` is the threshold parameter, configurable per E3 program). -The scheme is built on **Threshold Ring-BFV** (TrBFV), which extends the standard BFV -fully-homomorphic encryption scheme to a threshold setting. +Every step of the protocol is accompanied by a zero-knowledge proof. These proofs let nodes verify +each other's work without trusting anyone, and allow on-chain contracts to verify correctness +without seeing secret data. The pipeline uses **Noir** circuits compiled to **UltraHonk** proofs via +[Barretenberg](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg) (`bb`). + +The scheme is built on **threshold BFV**, which extends the standard BFV fully-homomorphic +encryption scheme to a threshold setting where no single party holds the full secret key. For the +full cryptographic specification, see the +[PVDKG article](https://blog.theinterfold.com/verifiability-in-the-coordination-trilemma/). --- ## Terminology -Following the conventions from the PVDKG specification: - | Term | Definition | | ------------------------------- | ------------------------------------------------------------------------------------------------------------- | | **Individual key pair** | A BFV key pair each ciphernode holds independently, used to encrypt/decrypt shares exchanged during DKG | @@ -36,14 +41,53 @@ Following the conventions from the PVDKG specification: --- +## Circuit Map + +| ID | Circuit | Phase | Proves | Source → Target | +| ------- | ---------------------------- | ------ | -------------------------------------------------------------------------------------------- | -------------------------- | +| **C0** | `PkBfv` | P1 DKG | Commits to individual BFV public key (binding for C3) | Each node → all peers | +| **C1** | `PkGeneration` | P1 DKG | Public key share correctly derived from secret key contribution (Schwartz-Zippel on BFV eq.) | Each node → all peers | +| **C2a** | `SkShareComputation` | P1 DKG | Secret key Shamir shares consistent with C1 commitment and satisfy Reed-Solomon parity | Each node → all peers | +| **C2b** | `ESmShareComputation` | P1 DKG | Smudging noise Shamir shares consistent with C1 commitment and satisfy Reed-Solomon parity | Each node → all peers | +| **C3a** | `ShareEncryption` (sk) | P1 DKG | BFV encryption of secret key share uses correct share (C2) and correct recipient pk (C0) | Per sender → per recipient | +| **C3b** | `ShareEncryption` (e_sm) | P1 DKG | BFV encryption of smudging noise share uses correct share (C2) and correct recipient pk (C0) | Per sender → per recipient | +| **C4a** | `DkgShareDecryption` (sk) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local secret key share | Each node → all peers | +| **C4b** | `DkgShareDecryption` (e_sm) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local smudging noise share | Each node → all peers | +| **C5** | `PkAggregation` | P2 Agg | Sum of all pk shares (verified against C1 commitments) equals published threshold public key | Aggregator → on-chain | +| **C6** | `ThresholdShareDecryption` | P4 Dec | Partial decryption uses correct secret key share and smudging noise share from C4 | Each node → aggregator | +| **C7** | `DecryptedSharesAggregation` | P4 Dec | Lagrange interpolation + CRT reconstruction + decoding is correct | Aggregator → on-chain | + +### Proof Count Per E3 + +For a committee of **N** nodes with **L** CRT moduli: + +| Circuit | Count per node | Total across committee | +| ------- | ------------------- | ---------------------- | +| C0 | 1 | N | +| C1 | 1 | N | +| C2a | 1 | N | +| C2b | 1 | N | +| C3a | (N-1) × L | N × (N-1) × L | +| C3b | (N-1) × L × ESI | N × (N-1) × L × ESI | +| C4a | 1 | N | +| C4b | 1 | N | +| C5 | 1 (aggregator only) | 1 | +| C6 | 1 | N | +| C7 | 1 (aggregator only) | 1 | + +C3 is by far the most proof-intensive circuit. With N=3 and L=4 (secure params), each node generates +8 C3a proofs alone. This is why proof generation runs on a multithread pool. + +--- + ## Two Key Hierarchies There are **two separate key pairs** in the protocol — a common source of confusion: -| Key type | Scheme | Purpose | Lifetime | -| ------------------ | ------------ | -------------------------------------------------------------------- | ------------------ | -| **Individual key** | Standard BFV | Encrypt Shamir shares in transit during DKG (like a secure channel) | One DKG session | -| **Threshold key** | TrBFV | The collective key users encrypt to; used for homomorphic operations | One E3 computation | +| Key type | Scheme | Purpose | Lifetime | +| ------------------ | ------------- | -------------------------------------------------------------------- | ------------------ | +| **Individual key** | Standard BFV | Encrypt Shamir shares in transit during DKG (like a secure channel) | One DKG session | +| **Threshold key** | Threshold BFV | The collective key users encrypt to; used for homomorphic operations | One E3 computation | The individual public key is committed in **C0**. The threshold key contributions are proved correct in **C1**. They live in different polynomial rings — the individual key uses a smaller "DKG" @@ -88,7 +132,9 @@ authorised set **A** is updated. --- -## Phase 1: Individual Key Commitment (C0) +## P1: Distributed Key Generation + +### Individual Key Commitment (C0) ``` CiphernodeSelected event arrives at ThresholdKeyshare @@ -113,14 +159,10 @@ moduli) and produces a cryptographic **commitment**. This commitment is consumed committed in C0. C0 itself does not prove key generation correctness — its role is binding the individual public key for later verification. ---- - -## Phase 2: Threshold Share Generation (C1, C2, C3) +### Threshold Share Generation (C1) Once all N individual public keys are collected, each node generates its threshold contribution. -### Step 1 — Generation (C1) - Each ciphernode generates its secret key contribution, the corresponding public key share, and its smudging noise contribution. **C1** proves that the public key share was correctly derived from the secret key contribution using the BFV key generation equation: @@ -144,7 +186,7 @@ circuits: 2. `commit(e_sm_contribution)` → consumed by **C2b** 3. `commit(pk_share)` → consumed by **C5** in P2 -### Step 2 — Share Computation (C2a / C2b) +### Share Computation (C2a / C2b) C2 runs separately for the secret key shares (C2a) and smudging noise shares (C2b). Each C2 circuit verifies two properties: @@ -159,7 +201,7 @@ The circuit outputs a commitment for each party's share per CRT modulus — an ` commitment values. These commitments are the bridge to C4: the recipient verifies the decrypted shares match what was committed here. -### Step 3 — Share Encryption (C3a / C3b) +### Share Encryption (C3a / C3b) Each share must be encrypted before transmission. C3a handles secret key shares and C3b handles smudging noise shares. The circuit verifies two commitment links simultaneously: @@ -186,9 +228,7 @@ complete does the node broadcast its `ThresholdShareCreated` message containing: This atomic publication prevents partial data from reaching peers. Any party whose proofs fail verification is excluded from the authorised set **A**. ---- - -## Phase 3: Share Decryption and Aggregation (C4) +### Share Decryption and Aggregation (C4) Each node decrypts the encrypted shares it received from all other authorised parties using its individual BFV secret key. C4 then: @@ -288,6 +328,101 @@ aggregator. --- +## Proof Lifecycle + +### Generation + +The `ProofRequestActor` dispatches ZK requests to the `ZkActor`: + +``` +ProofRequestActor receives event (e.g. EncryptionKeyPending) + │ + ├─ Builds witness data from event payload + ├─ Dispatches ComputeRequest::zk(ZkRequest::PkBfv { ... }) + │ + ▼ +ZkActor (IO layer): + ├─ Writes witness to temp directory + ├─ Spawns: bb prove -b circuit.json -w witness.gz -o proof/ + └─ Returns: Proof { data, public_signals } +``` + +### Signing + +Every proof is ECDSA-signed by the generating node before broadcast: + +``` +digest = keccak256(abi.encode( + PROOF_PAYLOAD_TYPEHASH, + chainId, + e3Id, + proofType, // e.g. C0, C2a, C3b + keccak256(proof.data), + keccak256(proof.public_signals) +)) + +signature = ecSign(digest, operator_private_key) // 65-byte r||s||v +``` + +This binds the proof to a specific E3, chain, and proof type — preventing replay attacks. The +signature also identifies the prover, so verifiers know which committee member produced the proof. + +### Broadcast + +Signed proofs are wrapped in protocol events and broadcast via **libp2p gossip**: + +- **C0**: Published as `EncryptionKeyCreated` — all peers receive and verify +- **C1–C3**: Bundled into `ThresholdShareCreated` — atomic publication of all DKG proofs +- **C6**: Published as `DecryptionShareCreated` — aggregator collects + +A node does **not** publish partial results. All proofs for a given phase complete before anything +is broadcast. + +### Verification + +On receiving a signed proof, the `ProofVerificationActor`: + +``` +ProofVerificationActor receives signed proof from gossip + │ + ├─ Recover ECDSA signer address from signature + ├─ Validate signer is a known committee member + │ + ├─ Dispatch to ZkActor: bb verify -k vk -p proof.data + │ (uses the inner circuit's verification key) + │ + ├─ If PASS: + │ ├─ Publish ProofVerificationPassed (cached by CommitmentConsistencyChecker) + │ └─ Continue protocol + │ + └─ If FAIL: + └─ Publish SignedProofFailed → triggers accusation pipeline +``` + +### Share Verification (Three-Phase Pipeline) + +For DKG shares (C1, C2, C3 together), there's a more sophisticated pipeline in +`ShareVerificationActor`: + +**Phase 1 — Lightweight ECDSA** (inline, fast): + +- Verify signature, recover signer, check consistency (all proofs from same address) +- Filter obviously invalid submissions + +**Phase 2 — Commitment Consistency** (dispatched to per-E3 checker): + +- Evaluate all registered [commitment links](./commitment-consistency) +- Exclude parties whose commitments are inconsistent across circuits +- This catches dishonest behavior **before** expensive ZK verification + +**Phase 3 — Heavy ZK Verification** (multithread): + +- Only consistency-passing parties reach this stage +- Each proof verified independently via `bb verify` +- Results aggregated into `ShareVerificationComplete { dishonest_parties }` + +--- + ## Commitment Chain Circuits are not isolated — they are linked through cryptographic commitments using @@ -298,8 +433,24 @@ circuit, and verify that it matches. This is how, for example, C1 connects to C5 commitment produced by C1 without the raw polynomials ever appearing on-chain — only the compact commitment is published. -See [Commitment Consistency](./commitment-consistency) for how these cross-circuit links are -enforced at runtime. +The full dependency graph: + +``` +C0 ──────────────────────────────────────→ C3 (pk_commitment verified) +C1 ──→ C2a (sk_commitment) +C1 ──→ C2b (e_sm_commitment) +C1 ──→ C5 (pk_commitment) +C2a ─→ C3a (share_commitment) +C2b ─→ C3b (share_commitment) +C2a ─→ C4a (expected_commitments for all L moduli) +C2b ─→ C4b (expected_commitments for all L moduli) +C4a ─→ C6 (sk_commitment) +C4b ─→ C6 (e_sm_commitment) +C6 ─→ C7 (d_commitment) +``` + +These dependencies are enforced by the [commitment consistency checker](./commitment-consistency), +which evaluates 12 cross-circuit links to detect any party submitting inconsistent values. --- @@ -323,7 +474,9 @@ separately for each preset, so both the prover and verifier must use matching pa ## Related -- [ZK Proof Pipeline](./zk-proofs) — how each circuit is proved, verified, and broadcast - [Commitment Consistency](./commitment-consistency) — how cross-circuit commitments are enforced - [Sortition](./sortition) — how the committee is selected before DKG begins - [Cryptography](/cryptography) — mathematical foundations (user-facing overview) +- [Noir Circuits](/noir-circuits) — toolchain setup and circuit compilation +- [Verifiability in the Coordination Trilemma](https://blog.theinterfold.com/verifiability-in-the-coordination-trilemma/) + — full cryptographic specification diff --git a/docs/pages/internals/sortition.mdx b/docs/pages/internals/sortition.mdx index c4613b0217..f11a74b35a 100644 --- a/docs/pages/internals/sortition.mdx +++ b/docs/pages/internals/sortition.mdx @@ -5,7 +5,7 @@ description: derivation, buffer strategy, and on-chain/off-chain determinism' --- -# Sortition — Technical Deep Dive +# Sortition Sortition is the mechanism that selects a subset of registered ciphernodes to form a committee for each E3 request. The design goals are: diff --git a/docs/pages/internals/zk-proofs.mdx b/docs/pages/internals/zk-proofs.mdx deleted file mode 100644 index ea80278539..0000000000 --- a/docs/pages/internals/zk-proofs.mdx +++ /dev/null @@ -1,185 +0,0 @@ ---- -title: 'ZK Proof Pipeline' -description: - 'The complete C0–C7 circuit pipeline — what each circuit proves, how proofs are generated with - Barretenberg, verified over P2P, and recursively aggregated' ---- - -# ZK Proof Pipeline — Technical Deep Dive - -Every cryptographic operation in the Interfold DKG and threshold decryption protocol is accompanied -by a zero-knowledge proof. These proofs let nodes verify each other's work without trusting anyone, -and allow on-chain contracts to verify correctness without seeing secret data. - -The pipeline uses **Noir** circuits compiled to **UltraHonk** proofs via -[Barretenberg](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg) (`bb`). - ---- - -## Circuit Map - -| ID | Circuit | Phase | Proves | Source → Target | -| ------- | ---------------------------- | ------ | -------------------------------------------------------------------------------------------- | -------------------------- | -| **C0** | `PkBfv` | P1 DKG | Commits to individual BFV public key (binding for C3) | Each node → all peers | -| **C1** | `PkGeneration` | P1 DKG | Public key share correctly derived from secret key contribution (Schwartz-Zippel on BFV eq.) | Each node → all peers | -| **C2a** | `SkShareComputation` | P1 DKG | Secret key Shamir shares consistent with C1 commitment and satisfy Reed-Solomon parity | Each node → all peers | -| **C2b** | `ESmShareComputation` | P1 DKG | Smudging noise Shamir shares consistent with C1 commitment and satisfy Reed-Solomon parity | Each node → all peers | -| **C3a** | `ShareEncryption` (sk) | P1 DKG | BFV encryption of secret key share uses correct share (C2) and correct recipient pk (C0) | Per sender → per recipient | -| **C3b** | `ShareEncryption` (e_sm) | P1 DKG | BFV encryption of smudging noise share uses correct share (C2) and correct recipient pk (C0) | Per sender → per recipient | -| **C4a** | `DkgShareDecryption` (sk) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local secret key share | Each node → all peers | -| **C4b** | `DkgShareDecryption` (e_sm) | P1 DKG | Decrypted shares match C2 commitments; aggregated into local smudging noise share | Each node → all peers | -| **C5** | `PkAggregation` | P2 Agg | Sum of all pk shares (verified against C1 commitments) equals published threshold public key | Aggregator → on-chain | -| **C6** | `ThresholdShareDecryption` | P4 Dec | Partial decryption uses correct secret key share and smudging noise share from C4 | Each node → aggregator | -| **C7** | `DecryptedSharesAggregation` | P4 Dec | Lagrange interpolation + CRT reconstruction + decoding is correct | Aggregator → on-chain | - ---- - -## Proof Count Per E3 - -For a committee of **N** nodes with **L** CRT moduli: - -| Circuit | Count per node | Total across committee | -| ------- | --------------------- | ---------------------- | -| C0 | 1 | N | -| C1 | 1 | N | -| C2a | 1 | N | -| C2b | 1 | N | -| C3a | (N-1) × L | N × (N-1) × L | -| C3b | (N-1) × L × ESI_count | N × (N-1) × L × ESI | -| C4a | 1 | N | -| C4b | 1 | N | -| C5 | 1 (aggregator only) | 1 | -| C6 | 1 | N | -| C7 | 1 (aggregator only) | 1 | - -C3 is by far the most proof-intensive circuit. With N=3 and L=4 (secure params), each node generates -8 C3a proofs alone. This is why proof generation runs on a multithread pool. - ---- - -## Proof Lifecycle - -### 1. Generation - -The `ProofRequestActor` dispatches ZK requests to the `ZkActor`: - -``` -ProofRequestActor receives event (e.g. EncryptionKeyPending) - │ - ├─ Builds witness data from event payload - ├─ Dispatches ComputeRequest::zk(ZkRequest::PkBfv { ... }) - │ - ▼ -ZkActor (IO layer): - ├─ Writes witness to temp directory - ├─ Spawns: bb prove -b circuit.json -w witness.gz -o proof/ - └─ Returns: Proof { data, public_signals } -``` - -### 2. Signing - -Every proof is ECDSA-signed by the generating node before broadcast: - -``` -digest = keccak256(abi.encode( - PROOF_PAYLOAD_TYPEHASH, - chainId, - e3Id, - proofType, // e.g. C0, C2a, C3b - keccak256(proof.data), - keccak256(proof.public_signals) -)) - -signature = ecSign(digest, operator_private_key) // 65-byte r||s||v -``` - -This binds the proof to a specific E3, chain, and proof type — preventing replay attacks. The -signature also identifies the prover, so verifiers know which committee member produced the proof. - -### 3. Broadcast - -Signed proofs are wrapped in protocol events and broadcast via **libp2p gossip**. Depending on the -proof type: - -- **C0**: Published as `EncryptionKeyCreated` — all peers receive and verify -- **C1–C3**: Bundled into `ThresholdShareCreated` — atomic publication of all DKG proofs -- **C6**: Published as `DecryptionShareCreated` — aggregator collects - -A node does **not** publish partial results. All proofs for a given phase complete before anything -is broadcast. - -### 4. Verification - -On receiving a signed proof, the `ProofVerificationActor`: - -``` -ProofVerificationActor receives signed proof from gossip - │ - ├─ Recover ECDSA signer address from signature - ├─ Validate signer is a known committee member - │ - ├─ Dispatch to ZkActor: bb verify -k vk -p proof.data - │ (uses the inner circuit's verification key) - │ - ├─ If PASS: - │ ├─ Publish ProofVerificationPassed (cached by CommitmentConsistencyChecker) - │ └─ Continue protocol - │ - └─ If FAIL: - └─ Publish SignedProofFailed → triggers accusation pipeline -``` - -### 5. Share Verification (Three-Phase Pipeline) - -For DKG shares (C1, C2, C3 together), there's a more sophisticated pipeline in -`ShareVerificationActor`: - -**Phase 1 — Lightweight ECDSA** (inline, fast): - -- Verify signature, recover signer, check consistency (all proofs from same address) -- Filter obviously invalid submissions - -**Phase 2 — Commitment Consistency** (dispatched to per-E3 checker): - -- Evaluate all registered [commitment links](./commitment-consistency) -- Exclude parties whose commitments are inconsistent across circuits -- This catches dishonest behavior **before** expensive ZK verification - -**Phase 3 — Heavy ZK Verification** (multithread): - -- Only consistency-passing parties reach this stage -- Each proof verified independently via `bb verify` -- Results aggregated into `ShareVerificationComplete { dishonest_parties }` - ---- - -## Circuit Dependencies - -The proofs form a directed acyclic graph through shared commitments: - -``` -C0 ──────────────────────────────────────→ C3 (pk used to encrypt) -C0 ──────────────────────────────────────→ C3 (pk_commitment verified) -C1 ──→ C2a (sk_commitment) -C1 ──→ C2b (e_sm_commitment) -C1 ──→ C5 (pk_commitment) -C2a ─→ C3a (share_commitment) -C2b ─→ C3b (share_commitment) -C2a ─→ C4a (expected_commitments for all L moduli) -C2b ─→ C4b (expected_commitments for all L moduli) -C4a ─→ C6 (sk_commitment) -C4b ─→ C6 (e_sm_commitment) -C6 ─→ C7 (d_commitment) -``` - -These dependencies are enforced by the [commitment consistency checker](./commitment-consistency), -which evaluates 12 cross-circuit links to detect any party submitting inconsistent values. - ---- - -## Related - -- [DKG & Threshold Cryptography](./dkg) — the protocol that generates the proofs -- [Commitment Consistency](./commitment-consistency) — how cross-circuit commitments are enforced -- [Noir Circuits](/noir-circuits) — toolchain setup and circuit compilation -- [Cryptography](/cryptography) — mathematical foundations