From cc468de9f0f6d3ac8c413277b4b17e270f0ae5e2 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 19 Dec 2025 13:55:18 +0100 Subject: [PATCH 1/5] docs: update crisp circuit documentation --- .../CRISP/circuits/src/ciphertext_addition.nr | 150 +++++++++++++++--- examples/CRISP/circuits/src/constants.nr | 15 ++ examples/CRISP/circuits/src/ecdsa.nr | 43 ++++- examples/CRISP/circuits/src/main.nr | 117 ++++++++++---- examples/CRISP/circuits/src/merkle_tree.nr | 18 ++- examples/CRISP/circuits/src/utils.nr | 73 +++++++-- 6 files changed, 344 insertions(+), 72 deletions(-) create mode 100644 examples/CRISP/circuits/src/constants.nr diff --git a/examples/CRISP/circuits/src/ciphertext_addition.nr b/examples/CRISP/circuits/src/ciphertext_addition.nr index 4beb864fa4..40ec8220db 100644 --- a/examples/CRISP/circuits/src/ciphertext_addition.nr +++ b/examples/CRISP/circuits/src/ciphertext_addition.nr @@ -4,7 +4,40 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -// This file contains ciphertext addition verification for BFV ciphertexts. +//! # Ciphertext Addition Verification +//! +//! This module implements zero-knowledge proof verification for homomorphic addition of BFV +//! (Brakerski-Fan-Vercauteren) ciphertexts. It verifies that a sum ciphertext correctly +//! represents the addition of two input ciphertexts without revealing the plaintexts. +//! +//! ## Purpose +//! +//! In CRISP, voters can update their votes. When a vote is updated, the new vote ciphertext +//! must be homomorphically added to the previous vote ciphertext. This module verifies that +//! this addition was performed correctly. +//! +//! ## Mathematical Background +//! +//! BFV ciphertexts are represented as pairs (c0, c1) where: +//! - c0 and c1 are polynomials in the ring R_q = Z_q[X]/(X^N + 1) +//! - For CRT (Chinese Remainder Theorem) bases, we have arrays [c0_i, c1_i] for each modulus q_i +//! +//! Homomorphic addition is performed component-wise: +//! - sum_c0_i = zero_c0_i + prev_c0_i + r0_i * q_i +//! - sum_c1_i = zero_c1_i + prev_c1_i + r1_i * q_i +//! +//! Where r0_i and r1_i are correction terms (quotient polynomials) that account for modular +//! reduction when coefficients exceed the modulus q_i. +//! +//! ## Verification Strategy +//! +//! The verification uses the Schwartz-Zippel lemma: if polynomial equations hold when evaluated +//! at random points, then the polynomials are identical with high probability. The circuit: +//! +//! 1. Generates random challenge values using Fiat-Shamir transform +//! 2. Evaluates all polynomials at these challenge points +//! 3. Verifies the addition equations hold at these points +//! 4. Checks range constraints on all coefficients use greco::CryptographicParams; use polynomial::{flatten, Polynomial}; @@ -15,9 +48,9 @@ use safe::SafeSponge; /// # Arguments /// * `N` - Polynomial degree. /// * `L` - Number of CRT bases. -/// * `BIT_ZERO_CT` - Bit length of the zero ciphertext polynomials. -/// * `BIT_PREV_CT` - Bit length of the previous ciphertext polynomials. -/// * `BIT_SUM_CT` - Bit length of the sum ciphertext polynomials. +/// * `BIT_ZERO_CT` - Bit-width bound per coefficient for zero ciphertext polynomials `zero_ct0is`/`zero_ct1is`. +/// * `BIT_PREV_CT` - Bit-width bound per coefficient for previous ciphertext polynomials `prev_ct0is`/`prev_ct1is`. +/// * `BIT_SUM_CT` - Bit-width bound per coefficient for sum ciphertext polynomials `sum_ct0is`/`sum_ct1is`. pub struct CiphertextAddition { crypto_params: CryptographicParams, zero_ct0is: [Polynomial; L], @@ -38,7 +71,7 @@ impl, zero_ct0is: [Polynomial; L], @@ -87,7 +120,7 @@ impl(inputs, self.sum_ct0is); inputs = flatten::<_, _, BIT_SUM_CT>(inputs, self.sum_ct1is); - // Flatten randomness polynomials + // Flatten quotient/correction polynomials inputs = flatten::<_, _, 1>(inputs, self.r0is); inputs = flatten::<_, _, 1>(inputs, self.r1is); @@ -96,25 +129,47 @@ impl bool { // Step 1: Perform range checks on all polynomial coefficients self.check_range_bounds(); @@ -126,10 +181,28 @@ impl(1, 1); self.r1is[i].range_check_2bounds::<1>(1, 1); @@ -168,44 +241,73 @@ impl) -> bool { + // Use the first challenge as the evaluation point for all polynomials let gamma = gammas.get(0); let mut accumulator: Field = 0; - // First L equalities: p == sum_ct0i - (zero_ct0i + prev_ct0i + r0i * q_i) + // First L equalities: verify sum_ct0i = zero_ct0i + prev_ct0i + r0i * q_i + // For i=0, use weight 1; for i>0, use weight gamma_i for i in 0..L { + // Build the expected sum: zero_ct0i + prev_ct0i + r0i * q_i let mut p = self.zero_ct0is[i]; let r0i_scaled = self.r0is[i].mul_scalar(self.crypto_params.qis[i]); p = p.add(self.prev_ct0is[i]); p = p.add(r0i_scaled); + // Compute difference: sum_ct0i - expected_sum p = self.sum_ct0is[i].sub(p); + // Weight and accumulate: use gamma_i as the weight (gamma_0 = 1 for i=0) let gamma_i = if i == 0 { 1 } else { gammas.get(i) }; accumulator += gamma_i * p.eval(gamma); } - // Next L equalities: p == sum_ct1i - (zero_ct1i + prev_ct1i + r1i * q_i) + // Next L equalities: verify sum_ct1i = zero_ct1i + prev_ct1i + r1i * q_i + // Use weights gamma_{L+i} for these equations for i in 0..L { + // Build the expected sum: zero_ct1i + prev_ct1i + r1i * q_i let mut p = self.zero_ct1is[i]; let r1i_scaled = self.r1is[i].mul_scalar(self.crypto_params.qis[i]); p = p.add(self.prev_ct1is[i]); p = p.add(r1i_scaled); + // Compute difference: sum_ct1i - expected_sum p = self.sum_ct1is[i].sub(p); + // Weight and accumulate: use gamma_{L+i} as the weight let gamma_i = gammas.get(L + i); accumulator += gamma_i * p.eval(gamma); } + // All equations hold if and only if the accumulator is zero accumulator == 0 } } diff --git a/examples/CRISP/circuits/src/constants.nr b/examples/CRISP/circuits/src/constants.nr new file mode 100644 index 0000000000..5f70f87562 --- /dev/null +++ b/examples/CRISP/circuits/src/constants.nr @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/// Half of the largest minimum degree used for vote encoding regions. +/// +/// This constant defines the size of the coefficient region allocated for encoding vote amounts +/// in binary format within each half of the polynomial. Votes are encoded as binary numbers: +/// - NO votes: coefficients [0, HALF_LARGEST_MINIMUM_DEGREE) +/// - YES votes: coefficients [HALF_D, HALF_D + HALF_LARGEST_MINIMUM_DEGREE) +/// +/// With a value of 50, each vote type can represent values up to 2^50 - 1. +pub global HALF_LARGEST_MINIMUM_DEGREE: u32 = 50; diff --git a/examples/CRISP/circuits/src/ecdsa.nr b/examples/CRISP/circuits/src/ecdsa.nr index c7e5523670..1ef2a4ae01 100644 --- a/examples/CRISP/circuits/src/ecdsa.nr +++ b/examples/CRISP/circuits/src/ecdsa.nr @@ -6,7 +6,23 @@ use keccak256::keccak256; -/// Given a public key, signature, and hashed message, verify the signature +/// Verifies an ECDSA signature over a hashed message. +/// +/// This function verifies that a signature was created by the holder of the private key +/// corresponding to the provided public key, and that it signs the given hashed message. +/// +/// # Arguments +/// * `hashed_message` - The Keccak256 hash of the message being signed (32 bytes) +/// * `pub_key_x` - The x-coordinate of the public key (32 bytes, big-endian) +/// * `pub_key_y` - The y-coordinate of the public key (32 bytes, big-endian) +/// * `signature` - The ECDSA signature (64 bytes: r || s, both 32 bytes big-endian) +/// +/// # Returns +/// `true` if the signature is valid, `false` otherwise. +/// +/// # Note +/// The signature is verified using secp256k1 curve (same as Ethereum). The message +/// should already be hashed before calling this function. pub fn verify_signature( hashed_message: [u8; 32], pub_key_x: [u8; 32], @@ -16,7 +32,19 @@ pub fn verify_signature( std::ecdsa_secp256k1::verify_signature(pub_key_x, pub_key_y, signature, hashed_message) } -/// Given a public key, derive the Ethereum address +/// Derives an Ethereum address from a secp256k1 public key. +/// +/// This function implements the standard Ethereum address derivation algorithm: +/// 1. Concatenate the public key coordinates (64 bytes total) +/// 2. Compute Keccak256 hash of the concatenated bytes +/// 3. Extract the last 20 bytes as the address +/// +/// # Arguments +/// * `pub_key_x` - The x-coordinate of the public key (32 bytes, big-endian) +/// * `pub_key_y` - The y-coordinate of the public key (32 bytes, big-endian) +/// +/// # Returns +/// A 20-byte array representing the Ethereum address. pub fn derive_address(pub_key_x: [u8; 32], pub_key_y: [u8; 32]) -> [u8; 20] { let mut pub_key_bytes = [0; 64]; for i in 0..32 { @@ -36,7 +64,16 @@ pub fn derive_address(pub_key_x: [u8; 32], pub_key_y: [u8; 32]) -> [u8; 20] { derived_address } -// Convert a 20-byte address to a field element. +/// Converts a 20-byte Ethereum address to a field element. +/// +/// This function interprets the address bytes as a big-endian integer and converts it +/// to a field element. +/// +/// # Arguments +/// * `addr` - A 20-byte Ethereum address +/// +/// # Returns +/// A field element representing the address as an integer. pub fn address_to_field(addr: [u8; 20]) -> Field { let mut acc: Field = 0; diff --git a/examples/CRISP/circuits/src/main.nr b/examples/CRISP/circuits/src/main.nr index 24bc83c0f6..c230952f13 100644 --- a/examples/CRISP/circuits/src/main.nr +++ b/examples/CRISP/circuits/src/main.nr @@ -7,6 +7,7 @@ use greco::{Greco, Params}; use polynomial::Polynomial; +mod constants; mod ciphertext_addition; use ciphertext_addition::CiphertextAddition; mod ecdsa; @@ -16,8 +17,34 @@ use merkle_tree::get_merkle_root; mod utils; use utils::{check_coefficient_values_with_balance, check_coefficient_zero}; +/// Main circuit function for CRISP vote verification. +/// +/// This circuit implements the core verification logic for the CRISP (Coercion-Resistant Impartial +/// Selection Protocol) voting system. It verifies that votes are properly encrypted using BFV +/// (Brakerski-Fan-Vercauteren) fully homomorphic encryption and handles two distinct use cases: +/// 1. **Actual Voting**: An eligible voter casts a vote +/// 2. **Mask Vote**: Anyone submits a zero vote to mask slot activity +/// +/// Votes are stored in an on-chain key-value mapping, where the key is the slot address (Ethereum address) +/// of the voter, and the value is the last ciphertext added to that slot. +/// - For actual votes: the slot address must match the address derived from the signature +/// - For mask votes: the slot address can be any address (no address match required) +/// In both cases, the address of the slot must be in the eligibility Merkle tree, a tree used +/// to represent all token holders eligible to vote. +/// +/// Mask votes prevent receipt sharing attacks by creating ambiguity: anyone can submit a zero vote +/// to any slot, creating a different ciphertext that decrypts to the same value. This makes it +/// impossible for a voter to prove which ciphertext was their actual vote, preventing them from +/// providing a verifiable receipt to a coercer or briber. +/// +/// The circuit verifies all inputs and returns the appropriate ciphertext. +/// Specifically, it returns a tuple of ciphertext components depending on the case: +/// - **Actual vote**: `(ct0is, ct1is)` - the new vote ciphertext +/// - **Mask vote (first vote)**: `(ct0is, ct1is)` - zero ciphertext +/// - **Mask vote (updating slot)**: `(sum_ct0is, sum_ct1is)` - sum of previous votes in slot +/// fn main( - // Ciphertext Addition Section. + // Ciphertext Addition Section prev_ct0is: [Polynomial<512>; 2], // todo: make this pub prev_ct1is: [Polynomial<512>; 2], // todo: make this pub sum_ct0is: [Polynomial<512>; 2], @@ -51,21 +78,37 @@ fn main( merkle_proof_length: u32, merkle_proof_indices: [u1; 20], merkle_proof_siblings: [Field; 20], - // Slot Address Section. + // Other inputs. slot_address: pub Field, - // Balance Section. balance: Field, - // Whether this is the first vote for this slot. is_first_vote: pub bool, ) -> pub ([Polynomial<512>; 2], [Polynomial<512>; 2]) { - // Verify the ECDSA signature. + // ============================================================================ + // STEP 1: Authentication - Derive voter's address from signature + // ============================================================================ + // Verify that a constant message was signed by the holder of the private key + // corresponding to the provided public key. Then the address is derived from + // that public key and compared later to the slot address if the vote is an actual + // vote. + // + // For ACTUAL VOTES: Signature must be valid (voter authenticates themselves) + // For MASK VOTES: Signature may be invalid (anyone can submit mask votes) let is_signature_valid = verify_signature(hashed_message, public_key_x, public_key_y, signature); - - // Derive the Ethereum address. let address = address_to_field(derive_address(public_key_x, public_key_y)); - // Calculate the Merkle root. + // ============================================================================ + // STEP 2: Eligibility - Merkle Tree Proof + // ============================================================================ + // Verify that the (slot_address, balance) pair exists in the eligibility + // Merkle tree. The proof demonstrates membership without revealing the entire tree. + // The leaf is computed as poseidon_hash(address, balance). The root is compared to + // the expected root, which is stored in the contract and passed as a public input + // when verifying the proof. + // + // This check applies to BOTH cases: + // - For actual votes: verifies the voter is eligible + // - For mask votes: verifies the slot address is in the eligibility tree let merkle_root_calculated = get_merkle_root( slot_address, balance, @@ -73,7 +116,18 @@ fn main( merkle_proof_indices, merkle_proof_siblings, ); + assert(merkle_root_calculated == merkle_root); + // ============================================================================ + // STEP 3: BFV Encryption Verification (GRECO) + // ============================================================================ + // Verify that the ciphertext (ct0is, ct1is) is a valid encryption of the + // plaintext k1 under the public key (pk0is, pk1is). This enusres the ciphertext + // is correctly formed. + // + // This check applies to BOTH cases: + // - For actual votes: verifies the vote ciphertext is correctly formed + // - For mask votes: verifies the zero vote ciphertext is correctly formed let greco: Greco<512, 2, 36, 36, 2, 6, 6, 4, 10, 36, 10, 36> = Greco::new( params, pk_commitment, @@ -92,7 +146,17 @@ fn main( p1is, p2is, ); + assert(greco.verify()); + // ============================================================================ + // STEP 4: Ciphertext Addition Verification + // ============================================================================ + // Verify that the sum ciphertext correctly represents the homomorphic addition + // of a zero vote to the previous ciphertext without decrypting. This is only checked + // for mask votes, and only when the slot is not empty. + // + // Mask votes add zero to the previous ciphertext, creating a different ciphertext + // with the same plaintext as the previous ciphertext. let ct_add: CiphertextAddition<512, 2, 36, 36, 36> = CiphertextAddition::new( params.crypto_params(), ct0is, @@ -104,39 +168,38 @@ fn main( sum_r0is, sum_r1is, ); - - // Verify the correct ciphertext encryption. - let is_greco_valid = greco.verify(); - - // Verify the correct ciphertext addition. let is_ct_add_valid = ct_add.verify(); - // Ensure that the ciphertext is valid - assert(is_greco_valid); - - // Verify the correct Merkle root. - assert(merkle_root_calculated == merkle_root); - - // If the voter is eligible to vote, output the ciphertext. - // Otherwise, output the sum of the ciphertexts. + // ============================================================================ + // STEP 5: Vote Type Detection and Return Logic + // ============================================================================ + // The circuit branches into two cases based on signature validity and address match: + // + // CASE 1: ACTUAL VOTE + // Condition: Signature valid AND address matches slot + // - This is an eligible voter casting a vote + // - Verify vote amount <= balance + // - Return new vote ciphertext (ct0is, ct1is) + // + // CASE 2: MASK VOTE + // Condition: Signature invalid OR address mismatch + // - This is anyone submitting a zero vote to mask slot activity + // - Verify vote is actually zero (k1 must be zero) + // - If first vote in slot: return zero ciphertext (ct0is, ct1is) + // - If updating slot: verify addition and return sum of previous votes if (is_signature_valid == true) & (slot_address == address) { - // Verify the correct coefficient values and that the vote is <= balance check_coefficient_values_with_balance(k1, params.crypto_params().q_mod_t, balance); (ct0is, ct1is) } else { - // check if vote == 0. let is_vote_zero = check_coefficient_zero(k1); assert(is_vote_zero == true); - // need to check if slot is empty (no previous ciphertext). - // If so, (ct0is, ct1is) should be returned. - if is_first_vote { (ct0is, ct1is) } else { - // check if the sum is valid assert(is_ct_add_valid); + (sum_ct0is, sum_ct1is) } } diff --git a/examples/CRISP/circuits/src/merkle_tree.nr b/examples/CRISP/circuits/src/merkle_tree.nr index bad53ad9af..fea696d232 100644 --- a/examples/CRISP/circuits/src/merkle_tree.nr +++ b/examples/CRISP/circuits/src/merkle_tree.nr @@ -7,7 +7,20 @@ use binary_merkle_root::binary_merkle_root; use poseidon::poseidon::bn254::hash_2 as poseidon; -// Get the Merkle root of a Merkle tree. +/// Computes the Merkle root from a leaf value and Merkle proof. +/// +/// This function verifies that a given (address, balance) pair exists in the eligibility +/// Merkle tree by reconstructing the root from the leaf and proof path. +/// +/// # Arguments +/// * `address` - The voter's Ethereum address (as a field element) +/// * `balance` - The voter's balance/voting power (as a field element) +/// * `depth` - The depth of the Merkle tree (number of levels from root to leaf) +/// * `indices` - Path direction indicators (0 = left, 1 = right) for each level +/// * `siblings` - Sibling node values along the path from leaf to root +/// +/// # Returns +/// The computed Merkle root as a field element. pub fn get_merkle_root( address: Field, balance: Field, @@ -15,10 +28,7 @@ pub fn get_merkle_root( indices: [u1; 20], siblings: [Field; 20], ) -> Field { - // Calculate poseidon hash of address and balance. let leaf = poseidon([address, balance]); - - // Calculate Merkle root. let merkle_root = binary_merkle_root(poseidon, leaf, depth, indices, siblings); merkle_root diff --git a/examples/CRISP/circuits/src/utils.nr b/examples/CRISP/circuits/src/utils.nr index 4b0ed38068..0688a019f4 100644 --- a/examples/CRISP/circuits/src/utils.nr +++ b/examples/CRISP/circuits/src/utils.nr @@ -6,61 +6,103 @@ use polynomial::Polynomial; -// Check that all valid coefficients are either 0 or q_mod_t and that the vote is <= balance +use crate::constants::HALF_LARGEST_MINIMUM_DEGREE; + +/// Validates vote coefficients and ensures the vote amount doesn't exceed balance. +/// +/// This function checks that: +/// 1. All vote coefficients are binary (either 0 or `q_mod_t`) +/// 2. At most one vote type is non-zero (YES and NO votes are mutually exclusive; both may be zero for no vote) +/// 3. The decoded vote amount (integer value reconstructed from binary coefficients) doesn't exceed the voter's balance +/// +/// # Arguments +/// * `k1` - The plaintext polynomial encoding the vote +/// * `q_mod_t` - The plaintext modulus (typically 1 for binary encoding) +/// * `balance` - The voter's balance/voting power pub fn check_coefficient_values_with_balance( k1: Polynomial, q_mod_t: Field, balance: Field, ) { - // This value allows to fit 1125899906842624 for yes and 1125899906842624 for no - let HALF_LARGEST_MINIMUM_DEGREE = 50; let HALF_D = D / 2; - // After reversal, bits that were at END of first half are now at START of second half + // Define the boundaries for YES and NO vote regions in the polynomial + // YES votes are stored in the second half: [HALF_D, HALF_D + HALF_LARGEST_MINIMUM_DEGREE) let END_INDEX_Y = HALF_D + HALF_LARGEST_MINIMUM_DEGREE; - - // Bits that were at END of second half are now at START of first half + // NO votes are stored in the first half: [0, HALF_LARGEST_MINIMUM_DEGREE) let END_INDEX_N = HALF_LARGEST_MINIMUM_DEGREE; let mut sum_yes: u64 = 0; let mut sum_no: u64 = 0; - // Yes part - process from END to START (reverse order) + // YES votes are encoded in reverse order (LSB at highest index, MSB at lowest index) + // We read from END_INDEX_Y-1 down to HALF_D to reconstruct the binary value for i in 0..HALF_LARGEST_MINIMUM_DEGREE { - // Access indices backwards: from (END_INDEX_Y - 1) down to START_INDEX_Y + // Access coefficients in reverse order: from (END_INDEX_Y - 1) down to HALF_D let idx = END_INDEX_Y - 1 - i; let coeff = k1.coefficients[idx]; + assert(0 == coeff * (q_mod_t - coeff)); + // Convert coefficient to bit: 0 -> 0, q_mod_t -> 1 let bit: u64 = if coeff == 0 { 0 } else { 1 }; + + // Reconstruct binary value: shift left and add new bit + // Reading backwards means we're building: bit_0 * 2^0 + bit_1 * 2^1 + ... sum_yes = sum_yes * 2 + bit; } - // No part - process from END to START (reverse order) + // NO votes are encoded in reverse order: [END_INDEX_N-1, ..., 0] + // We read from END_INDEX_N-1 down to 0 to reconstruct the binary value for i in 0..HALF_LARGEST_MINIMUM_DEGREE { + // Access coefficients in reverse order: from (END_INDEX_N - 1) down to 0 let idx = END_INDEX_N - 1 - i; let coeff = k1.coefficients[idx]; + + // Binary constraint check (same as YES votes) assert(0 == coeff * (q_mod_t - coeff)); + // Convert coefficient to bit let bit: u64 = if coeff == 0 { 0 } else { 1 }; + + // Reconstruct binary value sum_no = sum_no * 2 + bit; } if (sum_yes != 0) { + // If YES vote is cast, NO vote must be zero assert(sum_no == 0); + // Vote amount must not exceed balance assert(sum_yes <= (balance as u64)); } if (sum_no != 0) { + // If NO vote is cast, YES vote must be zero assert(sum_yes == 0); + // Vote amount must not exceed balance assert(sum_no <= (balance as u64)); } } +/// Checks if all vote coefficients are zero. +/// +/// This function verifies that both the YES and NO vote regions contain only zeros. +/// Used in masking operations to ensure that invalid or unauthorized votes are properly +/// masked (set to zero) before being added to the tally. +/// +/// # Arguments +/// * `k1` - The plaintext polynomial encoding the vote +/// +/// # Returns +/// `true` if all vote coefficients are zero, `false` otherwise. +/// +/// # Vote Regions Checked +/// - NO votes: coefficients [0, HALF_LARGEST_MINIMUM_DEGREE) +/// - YES votes: coefficients [HALF_D, HALF_D + HALF_LARGEST_MINIMUM_DEGREE) pub fn check_coefficient_zero(k1: Polynomial) -> bool { - let HALF_LARGEST_MINIMUM_DEGREE = 50; let HALF_D = D / 2; + // Define vote region boundaries let START_INDEX_Y = HALF_D; let END_INDEX_Y = HALF_D + HALF_LARGEST_MINIMUM_DEGREE; @@ -69,18 +111,21 @@ pub fn check_coefficient_zero(k1: Polynomial) -> bool { let mut res = true; + // Check YES vote region: all coefficients must be zero for i in START_INDEX_Y..END_INDEX_Y { if k1.coefficients[i] != 0 { res = false; } } + // Check NO vote region: all coefficients must be zero for i in START_INDEX_N..END_INDEX_N { if k1.coefficients[i] != 0 { res = false; } } + // Return true only if both regions are completely zero res } @@ -89,10 +134,10 @@ fn test_check_vote_is_within_balance_pass() { let pol = Polynomial { coefficients: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // YES votes: bits at indices 50-52 (START of YES window) 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], }; @@ -165,10 +210,10 @@ fn test_check_coefficient_zero_fail() { let pol = Polynomial { coefficients: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Non-zero at index 50 (YES section) 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], }; From 21317d2df269592d35e6d56f94c9ae1e2d8dfcad Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 19 Dec 2025 14:22:58 +0100 Subject: [PATCH 2/5] chore: update crisp verifier contract --- .../contracts/CRISPVerifier.sol | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol b/examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol index 9941e98654..61523ff59c 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol @@ -8,7 +8,7 @@ pragma solidity >=0.8.21; uint256 constant N = 262144; uint256 constant LOG_N = 18; uint256 constant NUMBER_OF_PUBLIC_INPUTS = 2066; -uint256 constant VK_HASH = 0x20e20e306d8e4ab1bb38da9301dcff68986340a39ec3e45f3d680911b91aac9e; +uint256 constant VK_HASH = 0x127b37c45dfb3a6a153382a3cf931c718810df54513b449af9ade62385eeefac; library HonkVerificationKey { function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { Honk.VerificationKey memory vk = Honk.VerificationKey({ @@ -16,36 +16,36 @@ library HonkVerificationKey { logCircuitSize: uint256(18), publicInputsSize: uint256(2066), ql: Honk.G1Point({ - x: uint256(0x1dddb42c5a7db46ec92d83144cb1fe8f8c1c862826388c25290ac8635ebc7dd7), - y: uint256(0x09549d2a558917eb071c0aaf4285d4827c566df2f1cd2d1e1b6ad5dab4afa499) + x: uint256(0x1a14a677dcf497132668b43dd6c2eba38ac0be2d8a55042e72ab0e431c1fca04), + y: uint256(0x0e30a76f6ca16f79faa7f36c8541bac98823fce567367d5b05f6d1e1889d2dce) }), qr: Honk.G1Point({ - x: uint256(0x0ed974b450a51723240b5e11b13e961951b95bf4c686c5be6c61b0e294b990ac), - y: uint256(0x10183cfef2d2ae78b9a83e62ed895bcfd086c7824be57c748914cf79b47627b1) + x: uint256(0x069b1a425a882deab8b2ab366567379d09dd9cc2430c896796c11e2e8be34fe4), + y: uint256(0x2f4a270bd180719c0120936a6ca66c6097f378bb489db75e29e644716f6f9387) }), qo: Honk.G1Point({ - x: uint256(0x0376597dbc0a258dbe8691c07264e83d208bbab2795c9efba9f12f0cac99e773), - y: uint256(0x2704bee070b407f6d0956b82b1953f3697f7b0ebdd4cba286ba8a8aa876460be) + x: uint256(0x24638b4ee6c5e15b721bbb29c486e913deb01588c13d0cb85da47c50c16a81e5), + y: uint256(0x1bd194bdfc905dc9465a3fe4b697de6fd7d67ae421b7404b84e472c68e691e7b) }), q4: Honk.G1Point({ - x: uint256(0x26543108597623c48ef32ca11c95af0d3ea922ea43b41944ebc10940a76c67c3), - y: uint256(0x09c28d40fa91aeff40db0ce3e6b8b86f162ed9d65c3aaa1b74d25cf53999f847) + x: uint256(0x019d3ad6130f74a80db6a4f6480521fb87ea52247dd96de060ffa7bb04b32885), + y: uint256(0x2fed009cd6974244ed643b1214943f8f48d9033e6236457ec9a601e0005ba444) }), qm: Honk.G1Point({ - x: uint256(0x0b63b4e86f99c000048a814e8579bbfb107bf06c473d3edc163e754ca41678d0), - y: uint256(0x1f3c3a429312accbf47a69871e0afe1c4902ec16747ee48914767cc3bb280d92) + x: uint256(0x1aa0f6fd5c3d53ae9bc8c76ad7e015a7b3a131aee52255966082dd0584a2d867), + y: uint256(0x292ea51ab60890143445fd667e86ce788e24d625225d24fe6eef31d2cb8763b4) }), qc: Honk.G1Point({ - x: uint256(0x27213ed6f59cc8fbc7380d04b4791900114e5f85f7c728fc98ae03fb4b7093da), - y: uint256(0x021c6a47a6d42a4f6c7a01c7b043c2bfc15b3a3ff270126510d60f1b86e01dac) + x: uint256(0x24cf8ec004d7a8a319a93ae7a62193e11e33cda3b072fee4cc92fbcf04e26c53), + y: uint256(0x0acca02ae37ea0155238f82833cbc3727c9c10d74ae862a02b70edc2bbf53fec) }), qLookup: Honk.G1Point({ x: uint256(0x056cab9e0cc90d6187f1504470e987376fb9d964f5e69f79d3dc50a3aba8b070), y: uint256(0x2a0690805846bbbba0fe533d4ec11edc41678b77983bcba8f10a71ece5298fee) }), qArith: Honk.G1Point({ - x: uint256(0x02a42dcfc958fc3919cf2198202d7a6822c5ac22b5a20246ab7d35c549aeb1e0), - y: uint256(0x26cc1db7e7db68551027e647657bbbb1a4e5c6cc8d2778fb8d8833eaf0ec6c22) + x: uint256(0x18850029d66fb7379caabfcb3d3b694c550a6413b84644f0d13b9e05554329b7), + y: uint256(0x216fb14305fc176ea326c69925161a0024e25d53909f3fd0c09871be5f94f8a0) }), qDeltaRange: Honk.G1Point({ x: uint256(0x1faf3b8674c17daf3e0f63e7c9356cacb82450ef207412dcf21922e73850f746), @@ -72,20 +72,20 @@ library HonkVerificationKey { y: uint256(0x0dfb3edabbbd97745fea4c569bc99dd450bad935f50610a36639cae94acddf56) }), s1: Honk.G1Point({ - x: uint256(0x21d3dd322812fd84e7ba8137cdc4e181de182d713192f4a039cd825e5a7fc33d), - y: uint256(0x11d9b3e7533a65c73578f09d5950d207d615dbcbcba9ca10198af6994ea38d29) + x: uint256(0x22d479c6eca52747dcca3a59269018e74cb9dbd7a7bf4c0f3cd380d022896fea), + y: uint256(0x0c3961f035d12e248dfa9f22f8ab9c6343c6c636d56ac5180334381e03f96c11) }), s2: Honk.G1Point({ - x: uint256(0x245a672b0d35bdf197675411082a8ee1024aab2f95727039206a8403380fdaf8), - y: uint256(0x12abd51740375d49166bde266697381ca5260279174752c9e03ed6c466b86744) + x: uint256(0x0c5ca053933191ff3aad8b265619f6a4fe97650125d1c33d3af5513c18e3f48e), + y: uint256(0x095932332027f209dc52666412aedb31121ee12d0f6ee50870d1cf8c7662b2a0) }), s3: Honk.G1Point({ - x: uint256(0x05d92f2af6e654e025fe6c6bab1a4e12b19f7a2cc149c33402472a0cc40cbaf7), - y: uint256(0x061370e345b4062d14f056256bbfa2c10eba7e585fb46ccead341f6811bd757a) + x: uint256(0x25b689112c3e1dd78e43260f40c2c6e6316b16f58fdf665ac727f9b7d423cdb7), + y: uint256(0x2c9974d76ea25589eef8f777bceca914c3d7d145c5fba75413cf30120621fb86) }), s4: Honk.G1Point({ - x: uint256(0x0860e07d6e2e6d913e58cd1eefa86a1d9a4cf4e29133f46c3d400b32dc9594af), - y: uint256(0x14c0357e31df310162c613184178967efe7a79c13f114a07ece96e80d301f082) + x: uint256(0x22ee739f9fb4fdcb29a7ed18db916919795d688ab94175ce5acb46348b0d8809), + y: uint256(0x021d5b6eb780aa2015158d6039437c956079b61f8fc2c97352303458ce6f91ba) }), t1: Honk.G1Point({ x: uint256(0x1f16b037f0b4c96ea2a30a118a44e139881c0db8a4d6c9fde7db5c1c1738e61f), @@ -104,20 +104,20 @@ library HonkVerificationKey { y: uint256(0x2d7e8c1ecb92e2490049b50efc811df63f1ca97e58d5e82852dbec0c29715d71) }), id1: Honk.G1Point({ - x: uint256(0x2f22768b7d6cb9255a78d3045f4f91993c2b85d416d7368644777bb6a6aed6a4), - y: uint256(0x23a7f91dfce36352c68d9814946618220ada657d112dccac1d30f4b0e800cb78) + x: uint256(0x01b858b61eaf5d54df5361d88c410ea379acb66cff9b2280dfa9e2ec57609ed1), + y: uint256(0x2f764a892dc8a7efc8f9e58b4778aa8b0b94a979a7d6231ee26311a20623d2d2) }), id2: Honk.G1Point({ - x: uint256(0x0676ce7aa26b2303b49ab8eb603cd4d4d6cdde30dd8767a32e5a57cbad40d485), - y: uint256(0x0030ffd41d9c36d601af702ae6705909e05bdeae7a3668eda8a24b7f270e67a3) + x: uint256(0x0ee58f929bb58de7d3e780126fcfac0943adab83776166c10b7626d2a0d04695), + y: uint256(0x1ead787a8fbc072a941301774d04a4a590036e9db96de8432732592156a44f1a) }), id3: Honk.G1Point({ - x: uint256(0x0d4104567de0b2e61083dfc969d70b67294cadb2bcae5e2fd5974fbfe3cdd025), - y: uint256(0x19beae607e8892176b30d2494fce1a7a9fcf9b367469413261bcb1db9d0c504b) + x: uint256(0x28628c4f0e01831a2406331bc0901948f2f2bc22200b2ce26d92558502cd3b96), + y: uint256(0x16d113fc0d0188deb7bcc2148bf2066003d5d2f6b7490201570d2ba5aa9b38ea) }), id4: Honk.G1Point({ - x: uint256(0x0927f450005fd2df96b3439570e88147f600d043616c8fb9a145ab6d25bc1b72), - y: uint256(0x12de11d953c0ba1f015f7363a5bfae565d2b5edefb6854eb60f71fe3e9950c7e) + x: uint256(0x282448418450184e943367153db43c02bd17640d2a40ed1861833b63f0cef798), + y: uint256(0x2dc5b559853ab1d7bf820f393e913fe435ccf95f84f9d8740d5f99bfcc3e3cea) }), lagrangeFirst: Honk.G1Point({ x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), From 22af740d5a377c135985936aee6f6a1f2823537a Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 19 Dec 2025 17:37:12 +0100 Subject: [PATCH 3/5] docs: update crisp documentation --- docs/pages/CRISP/introduction.mdx | 185 +++++++++++++++++------------- docs/pages/CRISP/running-e3.mdx | 77 +------------ docs/pages/CRISP/setup.mdx | 70 ++--------- 3 files changed, 117 insertions(+), 215 deletions(-) diff --git a/docs/pages/CRISP/introduction.mdx b/docs/pages/CRISP/introduction.mdx index 3523c12720..be7fb63b0f 100644 --- a/docs/pages/CRISP/introduction.mdx +++ b/docs/pages/CRISP/introduction.mdx @@ -13,7 +13,8 @@ vulnerabilities. This project serves as a comprehensive example of an E3 Program, demonstrating secure and impartial decision-making processes with a modern Hardhat-based architecture. It showcases a complete -full-stack implementation including frontend, backend, smart contracts, and zkVM components. +full-stack implementation including frontend, backend, smart contracts, and zero-knowledge +components. ## Why CRISP? @@ -27,97 +28,117 @@ coercion. CRISP mitigates these risks through: ## Project Structure -CRISP follows a modern Hardhat-based structure with clear separation of concerns: +CRISP, whose implementation is available in the +[Enclave repository](https://github.com/gnosisguild/enclave/tree/main/examples/CRISP), follows a +modern structure with clear separation of concerns: ``` CRISP/ ├── client/ # React frontend application (Vite + @crisp-e3/sdk) ├── server/ # Rust coordination server & CLI -├── program/ # RISC Zero guest + prover control plane +├── program/ # FHE program for encrypted computation + RISC Zero verification ├── packages/ -│ ├── crisp-contracts/ # Hardhat deployment + helpers -│ └── crisp-sdk/ # CRISP-specific TypeScript helpers to generate a ZK proof -├── crates/ # Rust libraries for the CLI + services -├── circuits/ # Noir circuits + verifiers (see [Noir Circuits](/noir-circuits)) -├── scripts/ # dev.sh, setup.sh, compile_circuits.sh, etc. +│ ├── crisp-contracts/ # CRISP program contract + Hardhat deployment scripts +│ └── crisp-sdk/ # TypeScript helpers to generate a ZK proof +├── crates/ # Rust libraries used by the server +├── circuits/ # Noir zero-knowledge circuits +├── scripts/ # Development scripts for running, testing, and deployment ├── enclave.config.yaml # Ciphernodes + aggregator config └── docker-compose.yaml # Optional multi-node deployment ``` ---- - -### **Client Application** (`/client`) - -The client is a React application built with TypeScript that provides a voting interface and reuses -the shared [CRISP SDK](/sdk): - -- Wallet connection with MetaMask and other wallets -- Vote encryption using WebAssembly-based FHE encryption before submission -- Noir zero-knowledge proof generation for vote validation -- Real-time updates on voting status and results - ---- - -### **Coordination Server** (`/server`) - -The server is a Rust-based coordination service that manages the E3 lifecycle and drives the same -SDK from a privileged wallet: - -- Listens to blockchain events and coordinates protocol progression -- Collects encrypted votes from the Smart Contract -- Triggers FHE computations after the voting round is closed. -- Publishes results back to the blockchain -- RESTful API for client interactions - ---- - -### **ZK Program + Noir Circuits** (`/program`, `/circuits`) - -- `program/`: Rust guest code compiled to the RISC Zero zkVM image that runs the CRISP tally logic -- `circuits/`: Noir circuits + SAFE/GRECO libraries used for membership proofs and encryption checks -- `scripts/compile_circuits.sh`: compiles Noir circuits, writes verification keys, and emits the - `CRISPVerifier.sol` contract used by `packages/crisp-contracts` - ---- - -### **Smart Contracts** (`/packages/crisp-contracts`) - -Solidity contracts implementing the E3 program interface: - -#### **CRISPProgram.sol** - -Main E3 program contract implementing the `IE3Program` interface: - -- `validate()`: Validates voting parameters and setup -- `verify()`: Verifies zkVM proofs of computation -- `validateInput()`: Handles vote validation and zero-knowledge proof verification: - - Validates encrypted vote format - - Verifies voter eligibility proofs - - Ensures vote uniqueness - -#### **CRISPVerifier.sol** - -Noir verifier for proof of correct encryption: - -- Verifies if the vote was encrypted correctly -- Verifies if the vote is valid - -#### **CRISPPolicy.sol** - -Manages voting policies and governance: - -- Defines voting rules and constraints -- Manages voter registration - -#### **CRISPChecker.sol** - -Additional verification and checking logic: - -- Vote format validation -- Eligibility checking -- Result verification - ---- +### Client Application + +The [client](https://github.com/gnosisguild/enclave/tree/main/examples/CRISP/client) is a React +application built with TypeScript that provides a voting interface. It uses the CRISP +[SDK](/CRISP/introduction#javascript-sdk) and [server](/CRISP/introduction#coordination-server) +endpoints to deliver the following capabilities: + +- Connecting wallets via MetaMask +- Encrypting votes using FHE encryption before submission +- Generating Noir zero-knowledge proofs for vote validation +- Displaying real-time updates on voting status and results + +### Coordination Server + +The [server](https://github.com/gnosisguild/enclave/tree/main/examples/CRISP/server) is a Rust-based +coordination service that manages the E3 lifecycle and drives the same SDK from a privileged wallet. +It acts as a relayer and facilitator, handling blockchain interactions on behalf of clients. +However, clients can bypass the server entirely and interact directly with the blockchain if they +prefer. + +The server's key responsibilities include: + +- Monitoring blockchain events and coordinating protocol progression +- Collecting encrypted votes from smart contracts +- Triggering FHE computations after voting rounds are closed +- Publishing computation results back to the blockchain +- Providing a RESTful API for client interactions + +### FHE Program + +The [FHE program](https://github.com/gnosisguild/enclave/tree/main/examples/CRISP/program) is a Rust +guest program that runs inside the RISC Zero zkVM to perform homomorphic addition on encrypted votes +(BFV ciphertexts). It computes the encrypted tally without decrypting individual votes, ensuring +voter privacy throughout the process. + +The program decodes BFV parameters and ciphertexts, then sums all encrypted votes homomorphically to +produce a single encrypted ciphertext containing the total. RISC Zero generates zero-knowledge +proofs that verify the computation was executed correctly, enabling trustless verification of the +tally results. + +### Zero-knowledge Circuits + +[Noir circuits](https://github.com/gnosisguild/enclave/tree/main/examples/CRISP/circuits) provide +zero-knowledge proofs for vote validation before votes are accepted into the system. These circuits +verify that votes are correctly encrypted using BFV fully homomorphic encryption under a valid +public key, check that the voter's address is included in the eligibility Merkle tree, and ensure +votes conform to the expected structure and values. The circuits also support mask votes, allowing +anyone to submit zero votes to mask slot activity and prevent receipt-sharing attacks. + +For detailed documentation on how the circuit works, see the +[main circuit implementation](https://github.com/gnosisguild/enclave/blob/main/examples/CRISP/circuits/src/main.nr). + +### Smart Contracts + +The CRISP smart contracts implement the E3 program interface and handle the on-chain logic for +voting rounds. The main contract is +[CRISPProgram.sol](https://github.com/gnosisguild/enclave/tree/main/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol), +which orchestrates the entire voting process. + +When a new voting round is initialized, the `validate()` function sets up round parameters and +initializes a Merkle tree to store votes efficiently. As votes are submitted, `validateInput()` +processes each encrypted vote by verifying its Noir zero-knowledge proof through the CRISPVerifier +contract, ensuring votes are correctly encrypted and voters are eligible. Votes are stored in the +Merkle tree with slot addresses mapping to vote indices, allowing updates while maintaining +uniqueness. + +After the voting period ends and FHE computation completes, the `verify()` function validates the +RISC Zero proof attesting to the correctness of the homomorphic tally computation, verifying it +corresponds to the correct input Merkle root, parameters hash, and ciphertext output. Once verified, +`decodeTally()` decodes the encrypted results into readable yes and no vote counts. + +The +[CRISPVerifier.sol](https://github.com/gnosisguild/enclave/tree/main/examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol) +contract is a Honk verifier generated from compiled Noir circuits that verifies zero-knowledge +proofs demonstrating votes are correctly encrypted using BFV fully homomorphic encryption under the +valid public key. + +### JavaScript SDK + +The [CRISP SDK](https://github.com/gnosisguild/enclave/tree/main/examples/CRISP/packages/crisp-sdk) +is a TypeScript library that abstracts away the complex cryptographic operations required for vote +submission and proof generation. It fetches round details from the coordination server, queries +token balances at snapshot blocks to verify eligibility, and generates Merkle proofs demonstrating +voter inclusion in the eligibility tree. + +When casting a vote, the SDK encrypts votes using BFV fully homomorphic encryption under the +committee's public key and generates zero-knowledge proofs using compiled Noir circuits. These +proofs demonstrate that votes are correctly encrypted and that voters are eligible to participate. +The SDK also supports mask votes, generating proofs for zero-value votes that help prevent +receipt-sharing attacks. Before submission, proofs are verified locally to prevent failed +transactions, and after voting concludes, the SDK decodes encrypted tally results into readable vote +counts. ## Next Steps diff --git a/docs/pages/CRISP/running-e3.mdx b/docs/pages/CRISP/running-e3.mdx index 7b72f836a7..2319fb2611 100644 --- a/docs/pages/CRISP/running-e3.mdx +++ b/docs/pages/CRISP/running-e3.mdx @@ -10,51 +10,19 @@ import { Steps } from 'nextra/components' In this section, we will go through all the steps to run an E3 Program using CRISP. We will run a complete voting round of CRISP and do the following: -- Start the infrastructure (nodes and contracts) -- Start the CRISP applications (client, server, program) - Request an E3 Voting Round - Submit votes through the web interface - Compute and verify results -Please make sure you have followed the [CRISP Setup](/CRISP/setup) guide before proceeding. +Please make sure you have followed the [CRISP Setup](/CRISP/setup) guide and +[started the application](/CRISP/setup#using-the-application) before proceeding. -### Prep Once per Checkout - -From the repo root run the bundled setup script (it installs dependencies, builds the CLI, prepares -env files, and compiles contracts): - -```sh -cd examples/CRISP -pnpm dev:setup -``` - -You only need to re-run this when dependencies change. - -### Start Everything with One Command - -The CRISP workspace ships with a supervisor that launches Hardhat, deploys contracts, boots -Ciphernodes, runs the RISC Zero program server, the Rust backend, and the React client. Start it in -the example root: - -```sh -pnpm dev:up -``` - -Behind the scenes `scripts/dev.sh` calls `scripts/dev_services.sh`, which: - -- Spawns a Hardhat chain on `http://localhost:8545` -- Deploys contracts and registers five Ciphernodes against the local registry -- Runs `enclave program start --dev true` so proving happens instantly while developing -- Launches the Rust server with `cargo run --bin server` -- Starts the React client via `pnpm dev-static` - -Keep this terminal open; logs from every service are multiplexed with `pnpm concurrently`. - ### Initialize a New Voting Round -Open a new terminal and launch the CLI from the example root: +As explained in the previous section, you can launch the CLI from the example root with the +following command: ```sh pnpm cli @@ -90,15 +58,14 @@ You should see output similar to: ### Set Up MetaMask -Whether you used `pnpm dev:up` or the manual flow below, you will interact with the Hardhat chain -running at `http://localhost:8545` (chain ID `31337`). Configure MetaMask once: +Whether you used `pnpm dev:up` or the manual flow, you will interact with the Hardhat chain running +at `http://localhost:8545` (chain ID `31337`). Configure MetaMask once: 1. Import the Hardhat deployer key so you have funds available: ``` 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ``` 2. Add a custom network pointing to `http://localhost:8545` with symbol `ETH` and chain ID `31337`. -3. Connect your wallet to `http://localhost:3000` when the client asks. ### Submit Votes via Web Interface @@ -107,38 +74,6 @@ running at `http://localhost:8545` (chain ID `31337`). Configure MetaMask once: 3. You should see the active voting round seeded by the CLI request 4. Cast a vote, approve the transaction, and wait for the confirmation toast -### Manual Control (Optional) - -If you prefer to run each process in its own terminal (or need to customize flags), replicate what -`pnpm dev:up` does under the hood: - -1. **Hardhat chain + deploy contracts** - ```sh - pnpm -C packages/crisp-contracts hardhat node - # in another terminal - ./scripts/crisp_deploy.sh - ``` -2. **Ciphernodes & wallets** - ```sh - ./scripts/dev_cipher.sh ./.enclave/ready - ``` - This script wipes previous `.enclave` state, installs dev wallets, starts `enclave nodes up -v`, - and waits until all nodes are registered. -3. **Program server** - ```sh - ./scripts/dev_program.sh # add --dev true inside to skip proofs in dev - ``` -4. **Rust server** - ```sh - wait-on tcp:13151 && ./scripts/dev_server.sh - ``` -5. **React client** - ```sh - wait-on tcp:4000 && wait-on file:./.enclave/ready && ./scripts/dev_client.sh - ``` - -You can also let `./scripts/dev_services.sh` orchestrate steps 2–5 once Hardhat is up. - ### Monitor the Process You can monitor the entire process through the various terminal outputs: diff --git a/docs/pages/CRISP/setup.mdx b/docs/pages/CRISP/setup.mdx index b41fe828e3..7a04c533cc 100644 --- a/docs/pages/CRISP/setup.mdx +++ b/docs/pages/CRISP/setup.mdx @@ -8,17 +8,8 @@ import { Steps } from 'nextra/components' # Getting Started with CRISP This guide will walk you through the steps to set up and run CRISP locally. CRISP is a complete -example of an E3 Program, built with a modern Hardhat-based architecture that includes smart -contracts, frontend applications, and secure computation components. - -The setup includes the following: - -- **CRISP contracts**: Smart contracts located in the `packages/crisp-contracts/` directory -- **Client**: React frontend application in the `client/` directory -- **Server**: Rust coordination server in the `server/` directory -- **Program**: RISC Zero computation program in the `program/` directory -- **Ciphernodes**: Distributed nodes managed through the Enclave CLI via `enclave.config.yaml` -- **Development environment**: Hardhat for contracts, Cargo for Rust components +example of an E3 Program, built with a modern architecture that includes smart contracts, +zero-knowledge circuits, frontend applications, and secure computation components. ## Prerequisites @@ -30,53 +21,8 @@ Before getting started, ensure you have installed: - [NodeJS](https://nodejs.org/en/download) - [pnpm](https://pnpm.io) - [MetaMask](https://metamask.io) -- [Noir toolchain (`nargo`, `bb`)](https://noir-lang.org/docs/getting_started/installation) — - required when you recompile the circuits - -### Install Node - -You can install Node following the official [documentation](https://nodejs.org/en) or using a Node -Version Manager (e.g., [nvm](https://github.com/nvm-sh/nvm)). - -### Install Pnpm - -You can install Pnpm following the official [documentation](https://pnpm.io/installation). - -### Install Metamask - -You can add Metamask as extension to your browser following the official -[documentation](https://metamask.io). - -### Install Rust and Foundry - -You need to install Rust and Foundry first. After installation, restart your terminal. - -```sh -# Install Rust -curl https://sh.rustup.rs -sSf | sh - -# Install Foundry -curl -L https://foundry.paradigm.xyz | bash -foundryup -``` - -### Install RISC Zero Toolchain - -Install `rzup` for the `cargo-risczero` toolchain: - -```sh -# Install rzup -curl -L https://risczero.com/install | bash - -# Install RISC Zero toolchain -rzup install cargo-risczero -``` - -Verify the installation: - -```sh -cargo risczero --version -``` +- Noir toolchain ([`nargo`](https://noir-lang.org/docs/getting_started/quick_start), + [`bb`](https://barretenberg.aztec.network/docs/getting_started)) ## Quick Start @@ -96,7 +42,7 @@ pnpm dev:up 2. Deploys all contracts (Enclave, CRISPProgram, verifiers, registries) via `scripts/crisp_deploy.sh` 3. Starts ciphernodes using `enclave.config.yaml` via `scripts/dev_cipher.sh` -4. Launches the program server (RISC Zero) via `scripts/dev_program.sh` +4. Launches the program server via `scripts/dev_program.sh` 5. Starts the coordination server (Rust) via `scripts/dev_server.sh` on port `4000` 6. Starts the React client via `scripts/dev_client.sh` on port `3000` @@ -106,7 +52,7 @@ All services run concurrently and will automatically restart if needed. ```bash # Recompile Noir circuits and generate verifiers -pnpm compile:circuit +pnpm compile:circuits # Open the interactive CLI to start voting rounds pnpm cli @@ -144,10 +90,10 @@ default, it runs in development mode with fake proofs for fast local development ```yaml program: - dev: true # Uses fake proofs (fast for development) + dev: true # Uses fake zkVM proofs (fast for development) ``` -### Boundless Configuration (Production Proving) +### Boundless Configuration For production-grade zero-knowledge proofs with [Boundless](https://docs.beboundless.xyz/), update `enclave.config.yaml`: From 60a9136dcffa9d938bbdc88d1746531af547b1ef Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 19 Dec 2025 17:47:11 +0100 Subject: [PATCH 4/5] docs: align readme file to docs --- examples/CRISP/Readme.md | 306 ++++++++++----------------------------- 1 file changed, 74 insertions(+), 232 deletions(-) diff --git a/examples/CRISP/Readme.md b/examples/CRISP/Readme.md index 44e0f6c3ae..09527612c8 100644 --- a/examples/CRISP/Readme.md +++ b/examples/CRISP/Readme.md @@ -15,14 +15,17 @@ structure. ```bash CRISP/ -|── client/ # React frontend application -|── server/ # Rust coordination server -|── program/ # RISC Zero computation program -|__ packages/ # JavaScript packages. -|__ crates/ # Rust crates. -├── circuits/ # Noir circuits for ZK proofs -├── scripts/ # Development and utility scripts -├── enclave.config.yaml # Ciphernode configuration +├── client/ # React frontend application (Vite + @crisp-e3/sdk) +├── server/ # Rust coordination server & CLI +├── program/ # FHE program for encrypted computation + RISC Zero verification +├── packages/ +│ ├── crisp-contracts/ # CRISP program contract + Hardhat deployment scripts +│ └── crisp-sdk/ # TypeScript helpers to generate a ZK proof +├── crates/ # Rust libraries used by the server +├── circuits/ # Noir zero-knowledge circuits +├── scripts/ # Development scripts for running, testing, and deployment +├── enclave.config.yaml # Ciphernodes + aggregator config +└── docker-compose.yaml # Optional multi-node deployment ``` You can have an extended explanation of the single folders in the dedicated @@ -37,180 +40,84 @@ Before getting started, ensure you have installed: - [RiscZero](https://dev.risczero.com/api/zkvm/install) - [NodeJS](https://nodejs.org/en/download) - [pnpm](https://pnpm.io) -- [Metamask](https://metamask.io) - -### Install Node - -You can install Node following the official [documentation](https://nodejs.org/en) or using a Node -Version Manager (e.g., [nvm](https://github.com/nvm-sh/nvm)). - -### Install Pnpm - -You can install Pnpm following the official [documentation](https://pnpm.io/installation). - -### Install Metamask - -You can add Metamask as extension to your browser following the official -[documentation](https://metamask.io). - -### Install Rust - -You need to install Rust. After installation, restart your terminal. - -```sh -# Install Rust -curl https://sh.rustup.rs -sSf | sh - -``` - -### Install RISC Zero Toolchain - -Next, install `rzup` for the `cargo-risczero` toolchain. - -```sh -# Install rzup -curl -L https://risczero.com/install | bash - -# Install RISC Zero toolchain -rzup install cargo-risczero -``` - -Verify the installation was successful by running: - -```sh -cargo risczero --version -``` - -At this point, you should have all the tools required to develop and deploy an application with -[RISC Zero](https://www.risczero.com). - -## Environment - -You need to setup your environment variables for `client/` and `server/`. Just copy and paste the -`.env.default` as `.env` and overwrite with your values the following variables (you can leave the -others initialized with the default values). - -`pnpm dev:setup` already makes a copy of the env files for you. - -The addresses will be displayed after successfully running the `pnpm dev:up` command in a log that -will look like the following: - -```bash -Deployments: ----------------------------------------------------------------------- -Enclave: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 -Verifier: 0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0 -HonkVerifier: 0x9A676e781A523b5d0C0e43731313A708CB607508 -CRISPProgram: 0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1 -``` - -If you find any inconsistency with the addresses on the environment, you must update them and run -the script again (they must match). +- [MetaMask](https://metamask.io) +- Noir toolchain ([`nargo`](https://noir-lang.org/docs/getting_started/quick_start), + [`bb`](https://barretenberg.aztec.network/docs/getting_started)) ## Quick Start -The fastest way to get CRISP running is using the scripts provided in the `scripts/` directory: +The simplest way to run CRISP is: ```bash -# Setup and build the development environment +# Install dependencies and build everything pnpm dev:setup -# Start all services (Anvil, Ciphernodes, Applications) +# Start all services (Hardhat, contracts, ciphernodes, program server, coordination server, and UI) pnpm dev:up ``` -This will start all CRISP components: - -- Hardhat node (local blockchain) -- Deploy all contracts -- Compile all ZK circuits -- Ciphernodes network -- CRISP applications (server, client) - -Once everything is running, you can: +`dev:up` runs `scripts/dev.sh`, which: -1. Navigate `http://localhost:3000` for the client interface -2. Add the Hardhat private key to your wallet: - `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` -3. Press `Connect Wallet` button and complete the association with your MetaMask account -4. Switch to `Hardhat` local network (this will be handled automatically by the app. You just need - to press on the connected account on the frontend and select the network. Then, complete the - configuration on MetaMask pop-up). -5. Open a new terminal, run `pnpm cli` and start a new E3 Round. -6. Refresh and interact with the round following the Client interface. +1. Starts the Hardhat node in `packages/crisp-contracts` +2. Deploys all contracts (Enclave, CRISPProgram, verifiers, registries) via + `scripts/crisp_deploy.sh` +3. Starts ciphernodes using `enclave.config.yaml` via `scripts/dev_cipher.sh` +4. Launches the program server via `scripts/dev_program.sh` +5. Starts the coordination server (Rust) via `scripts/dev_server.sh` on port `4000` +6. Starts the React client via `scripts/dev_client.sh` on port `3000` -## Manual Start +All services run concurrently and will automatically restart if needed. -### Setting Up the Web App +### Running Individual Components -To set up the CRISP dApp in your local environment, follow these steps: +While `pnpm dev:up` runs everything together, you can also run components separately: -1. Navigate to the `client` directory: - - ```sh - cd examples/CRISP/client - ``` - -2. Start the development server: - - ```sh - pnpm dev - ``` +```bash +# Start only the Hardhat node +cd packages/crisp-contracts && pnpm hardhat node -### Setting Up the CRISP Server +# Start only the ciphernodes (requires Hardhat running) +./scripts/dev_cipher.sh -Setting up the CRISP server involves several components, but this guide will walk you through each -step. +# Start only the program server (requires ciphernodes) +./scripts/dev_program.sh -#### Step 1: Start a Local Testnet with Anvil +# Start only the coordination server (requires program server) +./scripts/dev_server.sh -```sh -anvil +# Start only the client (requires coordination server) +./scripts/dev_client.sh ``` -Keep Anvil running in the terminal, and open a new terminal for the next steps. - -#### Step 2: Setting Up the Ciphernodes - -1. Clone the [Enclave Repo](https://github.com/gnosisguild/enclave): - - ```sh - git clone https://github.com/gnosisguild/enclave.git - ``` +### Additional Commands -2. Navigate to the `examples/CRISP` directory inside the cloned repository: +```bash +# Recompile Noir circuits and generate verifiers +pnpm compile:circuits - ```sh - cd enclave/examples/CRISP - ``` +# Open the interactive CLI to start voting rounds +pnpm cli -3. Deploy the contracts: +# Run end-to-end tests +pnpm test:e2e +``` - ```sh - pnpm deploy:contracts:full - ``` +## Configuration -After deployment, you will see the addresses for the following contracts: +### Ciphernode Configuration -- Enclave -- Ciphernode Registry -- Bonding Registry Filter -- Mock Input Validator -- Mock E3 Program -- Mock Decryption Verifier -- Mock Compute Provider -- RISC Zero Verifier -- Honk Verifier -- CRISP Input Validator Factory -- CRISP Program +The `enclave.config.yaml` file in the CRISP root directory configures the ciphernode network. By +default, it runs in development mode with fake proofs for fast local development: -#### Step 3: Configure Boundless (Optional) +```yaml +program: + dev: true # Uses fake zkVM proofs (fast for development) +``` -> Please note that this step is optional for development only. By default, the program server runs -> in dev mode which uses fake proofs for fast local development. +### Boundless Configuration For production-grade zero-knowledge proofs with [Boundless](https://docs.beboundless.xyz/), update -the `enclave.config.yaml` file in the CRISP root directory: +`enclave.config.yaml`: ```yaml program: @@ -232,101 +139,36 @@ program: > - A Pinata JWT for uploading programs to IPFS (get one at [pinata.cloud](https://pinata.cloud)) > - Pre-uploaded program URL to avoid uploading the ~40MB program at runtime -### Uploading Your Program to IPFS +#### Uploading Your Program to IPFS -When you make changes to the guest program in `program/`, you need to upload it to IPFS: +When you make changes to the guest program in `program/`, you need to upload it to IPFS to get a +program URL: -1. Build and upload your program: +1. First, configure your Pinata JWT in `enclave.config.yaml` (as shown above) + +2. Build and upload your program: ```bash + # This compiles the guest program and uploads it to IPFS via Pinata enclave program upload ``` -2. The command outputs an IPFS hash. Update `enclave.config.yaml` with the full URL: +3. The command will output an IPFS hash like `QmXxx...`. Update your `enclave.config.yaml` with the + full URL: ```yaml program_url: 'https://gateway.pinata.cloud/ipfs/QmXxx...' ``` -> **_Important:_** Every time you modify the guest program, rebuild and re-upload it to IPFS, then -> update the `program_url` in your configuration. - -#### Step 4: Set up Environment Variables - -Create a `.env` file in the `server` directory with the following: - -```sh -# Private key for the enclave server -PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -ENCLAVE_SERVER_URL=http://0.0.0.0:4000 -HTTP_RPC_URL=http://127.0.0.1:8545 -PROGRAM_SERVER_URL=http://127.0.0.1:13151 -WS_RPC_URL=ws://127.0.0.1:8545 -CHAIN_ID=31337 - -# Etherscan API key -ETHERSCAN_API_KEY="" - -# Cron-job API key to trigger new rounds -CRON_API_KEY=1234567890 - -# Based on Default Anvil Deployments (Only for testing) -ENCLAVE_ADDRESS="0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" -CIPHERNODE_REGISTRY_ADDRESS="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" -E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address -FEE_TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3" # Mock ERC20 Token Address - -# E3 Config -E3_WINDOW_SIZE=40 -E3_THRESHOLD_MIN=1 -E3_THRESHOLD_MAX=2 -E3_DURATION=160 - -# E3 Compute Provider Config -E3_COMPUTE_PROVIDER_NAME="RISC0" -E3_COMPUTE_PROVIDER_PARALLEL=false -E3_COMPUTE_PROVIDER_BATCH_SIZE=4 # Must be a power of 2 - -# ETHERSCAN API Key (optional, leave empty if not using) -ETHERSCAN_API_KEY="" -``` - -## Running Ciphernodes - -To run three ciphernodes, use the following command inside the CRISP directory: - -```sh -./scripts/dev_cipher.sh -``` - -This script will start the ciphernodes, add the ciphernodes to the registry on chain. - -## Running the CRISP Server - -To run the CRISP Server, open a new terminal and navigate to the `server` directory. Then, execute -the following command: - -```sh -cargo run --bin server -``` - -## Interacting with CRISP via CLI - -Open a new terminal and navigate to the `server` directory. Then, execute the following command: - -```sh -cargo run --bin cli -``` - -Once the CLI client is running, you can interact with the CRISP voting protocol by following these -steps: - -1. Select `CRISP: Voting Protocol (ETH)` from the menu. +> **_Important:_** Every time you modify the guest program code in `program/`, you must rebuild and +> re-upload it to IPFS, then update the `program_url` in your configuration. This ensures Boundless +> uses your latest program version. -2. To initiate a new CRISP round, choose the option `Initialize new CRISP round`. +### Environment Variables -Ensure all services are running correctly and that components are communicating as expected before -starting a new CRISP round. +The `pnpm dev:setup` command automatically creates `.env` files for the server and client from the +`.env.example` templates. The server's `.env` file is automatically populated with contract +addresses after deployment. ## Publishing packages to npm From f70946c663b5c8598ba61717054177ced1bdb76a Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 19 Dec 2025 17:59:22 +0100 Subject: [PATCH 5/5] docs: solved github comments --- docs/pages/CRISP/introduction.mdx | 6 +++--- examples/CRISP/Readme.md | 5 +++-- examples/CRISP/circuits/src/ciphertext_addition.nr | 2 +- examples/CRISP/circuits/src/main.nr | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/pages/CRISP/introduction.mdx b/docs/pages/CRISP/introduction.mdx index be7fb63b0f..0f1d1c4470 100644 --- a/docs/pages/CRISP/introduction.mdx +++ b/docs/pages/CRISP/introduction.mdx @@ -94,7 +94,7 @@ zero-knowledge proofs for vote validation before votes are accepted into the sys verify that votes are correctly encrypted using BFV fully homomorphic encryption under a valid public key, check that the voter's address is included in the eligibility Merkle tree, and ensure votes conform to the expected structure and values. The circuits also support mask votes, allowing -anyone to submit zero votes to mask slot activity and prevent receipt-sharing attacks. +anyone to submit zero votes to mask slot activity and reduce the risk of collusion and coercion. For detailed documentation on how the circuit works, see the [main circuit implementation](https://github.com/gnosisguild/enclave/blob/main/examples/CRISP/circuits/src/main.nr). @@ -135,8 +135,8 @@ voter inclusion in the eligibility tree. When casting a vote, the SDK encrypts votes using BFV fully homomorphic encryption under the committee's public key and generates zero-knowledge proofs using compiled Noir circuits. These proofs demonstrate that votes are correctly encrypted and that voters are eligible to participate. -The SDK also supports mask votes, generating proofs for zero-value votes that help prevent -receipt-sharing attacks. Before submission, proofs are verified locally to prevent failed +The SDK also supports mask votes, generating proofs for zero-value votes that help reduce the risk +of collusion and coercion. Before submission, proofs are verified locally to prevent failed transactions, and after voting concludes, the SDK decodes encrypted tally results into readable vote counts. diff --git a/examples/CRISP/Readme.md b/examples/CRISP/Readme.md index 09527612c8..efa8abb953 100644 --- a/examples/CRISP/Readme.md +++ b/examples/CRISP/Readme.md @@ -167,8 +167,9 @@ program URL: ### Environment Variables The `pnpm dev:setup` command automatically creates `.env` files for the server and client from the -`.env.example` templates. The server's `.env` file is automatically populated with contract -addresses after deployment. +`.env.example` templates (if they don't already exist). + +The `enclave.config.yaml` file is automatically populated with contract addresses after deployment. ## Publishing packages to npm diff --git a/examples/CRISP/circuits/src/ciphertext_addition.nr b/examples/CRISP/circuits/src/ciphertext_addition.nr index 40ec8220db..82a4b2a56b 100644 --- a/examples/CRISP/circuits/src/ciphertext_addition.nr +++ b/examples/CRISP/circuits/src/ciphertext_addition.nr @@ -12,7 +12,7 @@ //! //! ## Purpose //! -//! In CRISP, voters can update their votes. When a vote is updated, the new vote ciphertext +//! In CRISP, mask votes can update slots. When a mask vote updates a slot, the new vote ciphertext //! must be homomorphically added to the previous vote ciphertext. This module verifies that //! this addition was performed correctly. //! diff --git a/examples/CRISP/circuits/src/main.nr b/examples/CRISP/circuits/src/main.nr index c230952f13..327ec54bfd 100644 --- a/examples/CRISP/circuits/src/main.nr +++ b/examples/CRISP/circuits/src/main.nr @@ -25,8 +25,8 @@ use utils::{check_coefficient_values_with_balance, check_coefficient_zero}; /// 1. **Actual Voting**: An eligible voter casts a vote /// 2. **Mask Vote**: Anyone submits a zero vote to mask slot activity /// -/// Votes are stored in an on-chain key-value mapping, where the key is the slot address (Ethereum address) -/// of the voter, and the value is the last ciphertext added to that slot. +/// Votes are stored in an on-chain Merkle tree, where each vote ciphertext is stored at a specific +/// index. A mapping stores the slot address (Ethereum address) to the index in the Merkle tree. /// - For actual votes: the slot address must match the address derived from the signature /// - For mask votes: the slot address can be any address (no address match required) /// In both cases, the address of the slot must be in the eligibility Merkle tree, a tree used