Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions examples/CRISP/circuits/src/ecdsa.nr
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,30 @@

use keccak256::keccak256;

/// Verifies an ECDSA signature over a hashed message.
/// Validates an ECDSA signature over a hashed message.
///
/// This function verifies that a signature was created by the holder of the private key
/// This function validates 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.
/// The function asserts internally, ensuring the signature is valid.
///
/// # 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(
pub fn validate_signature(
hashed_message: [u8; 32],
pub_key_x: [u8; 32],
pub_key_y: [u8; 32],
signature: [u8; 64],
) -> bool {
std::ecdsa_secp256k1::verify_signature(pub_key_x, pub_key_y, signature, hashed_message)
) {
let is_valid =
std::ecdsa_secp256k1::verify_signature(pub_key_x, pub_key_y, signature, hashed_message);
assert(is_valid == true);
}

/// Derives an Ethereum address from a secp256k1 public key.
Expand Down Expand Up @@ -105,7 +105,7 @@ fn test_derive_address() {
}

#[test]
fn test_verify_signature() {
fn test_validate_signature() {
let hashed_message = [
67, 126, 157, 164, 162, 165, 56, 242, 155, 214, 113, 196, 83, 198, 228, 36, 174, 104, 152,
87, 167, 108, 64, 34, 234, 161, 122, 55, 44, 62, 151, 55,
Expand All @@ -124,11 +124,11 @@ fn test_verify_signature() {
124, 118, 143, 228, 126, 216, 173, 160, 231, 62, 52, 188, 154, 110, 230, 183, 71, 36, 161,
171, 163, 213, 62, 223, 152,
];
assert(verify_signature(hashed_message, pub_key_x, pub_key_y, signature) == true);
validate_signature(hashed_message, pub_key_x, pub_key_y, signature);
}

#[test]
fn test_verify_signature_sdk_input() {
fn test_validate_signature_sdk_input() {
let hashed_message = [
200, 232, 98, 162, 80, 131, 242, 57, 252, 76, 226, 45, 127, 206, 207, 39, 206, 44, 211, 171,
113, 67, 121, 68, 78, 253, 202, 79, 29, 128, 130, 76,
Expand All @@ -149,11 +149,14 @@ fn test_verify_signature_sdk_input() {
236, 18,
];

assert(verify_signature(hashed_message, pub_key_x, pub_key_y, signature) == true);
validate_signature(hashed_message, pub_key_x, pub_key_y, signature);
}

#[test]
fn test_fail_verify_signature() {
#[test(should_fail)]
fn test_fail_validate_signature() {
// This test verifies that invalid signatures cause validation to fail.
// The test itself will fail (assertion failure) when validate_signature is called
// with an invalid signature, which demonstrates that invalid signatures are properly rejected.
let hashed_message = [
67, 126, 157, 164, 162, 165, 56, 242, 155, 214, 113, 196, 83, 198, 228, 36, 174, 104, 152,
87, 167, 108, 64, 34, 234, 161, 122, 55, 44, 62, 151, 55,
Expand All @@ -173,7 +176,8 @@ fn test_fail_verify_signature() {
171, 163, 213, 62, 223, 151,
];

assert(verify_signature(hashed_message, pub_key_x, pub_key_y, signature) == false);
// This call should fail (assertion failure) because the signature is invalid
validate_signature(hashed_message, pub_key_x, pub_key_y, signature);
}

#[test]
Expand Down
139 changes: 59 additions & 80 deletions examples/CRISP/circuits/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mod constants;
mod ciphertext_addition;
use ciphertext_addition::CiphertextAddition;
mod ecdsa;
use ecdsa::{address_to_field, derive_address, verify_signature};
use ecdsa::{address_to_field, derive_address, validate_signature};
mod merkle_tree;
use merkle_tree::get_merkle_root;
mod utils;
Expand All @@ -43,11 +43,11 @@ use utils::{check_coefficient_values_with_balance, check_coefficient_zero};
/// 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
/// The circuit verifies all inputs and returns the appropriate ciphertext commitment.
/// Specifically, it returns a Field commitment depending on the case:
/// - **Actual vote**: `ct_commitment` - commitment to the new vote ciphertext
/// - **Mask vote (first vote)**: `ct_commitment` - commitment to the zero ciphertext
/// - **Mask vote (updating slot)**: `sum_ct_commitment` - commitment to the sum of previous votes in slot
///
fn main(
// Ciphertext Addition Section
Expand Down Expand Up @@ -88,23 +88,10 @@ fn main(
slot_address: pub Field,
balance: Field,
is_first_vote: pub bool,
is_mask_vote: bool,
) -> pub Field {
// ============================================================================
// 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);
let address = address_to_field(derive_address(public_key_x, public_key_y));

// ============================================================================
// STEP 2: Eligibility - Merkle Tree Proof
// STEP 1: 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.
Expand All @@ -126,10 +113,10 @@ fn main(
assert(merkle_root_calculated == merkle_root);

// ============================================================================
// STEP 3: BFV Encryption Verification (GRECO)
// STEP 2: 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
// plaintext k1 under the public key (pk0is, pk1is). This ensures the ciphertext
// is correctly formed.
//
// This check applies to BOTH cases:
Expand Down Expand Up @@ -157,78 +144,70 @@ fn main(
assert(greco.verify());

// ============================================================================
// STEP 4: Ciphertext Addition Verification
// STEP 3: Vote Type Detection and Return Logic
// ============================================================================
// 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.
//
// Commitments are cryptographic hashes of the polynomial coefficients that bind the
// prover to specific ciphertext values. Three commitments are generated:
// - prev_ct_commitment: commitment to the previous ciphertext (prev_ct0is, prev_ct1is)
// - ct_commitment: commitment to the new vote ciphertext (ct0is, ct1is)
// - sum_ct_commitment: commitment to the sum ciphertext (sum_ct0is, sum_ct1is)
// The circuit branches into two cases based on the is_mask_vote flag:
//
// The prev_ct_commitment is verified against the actual polynomials to ensure the
// prover hasn't tampered with the previous ciphertext. This check is only performed
// for mask votes when it's not the first vote (i.e., when there's a previous ciphertext
// to verify). The commitments are then used in the Fiat-Shamir transform to generate
// random challenges for the Schwartz-Zippel lemma verification, ensuring the addition
// equations hold without revealing the full polynomial coefficients.
let _prev_ct_commitment = generate_commitment::<N, L, GRECO_BIT_CT>(prev_ct0is, prev_ct1is);
let ct_commitment = generate_commitment::<N, L, GRECO_BIT_CT>(ct0is, ct1is);
let sum_ct_commitment = generate_commitment::<N, L, GRECO_BIT_CT>(sum_ct0is, sum_ct1is);

let ct_add: CiphertextAddition<N, L, GRECO_BIT_CT, GRECO_BIT_CT, GRECO_BIT_CT> = CiphertextAddition::new(
GRECO_CONFIGS,
ct0is,
ct1is,
ct_commitment,
prev_ct0is,
prev_ct1is,
_prev_ct_commitment,
sum_ct0is,
sum_ct1is,
sum_ct_commitment,
sum_r0is,
sum_r1is,
);

let is_ct_add_valid = ct_add.verify();

// ============================================================================
// 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
// CASE 1: ACTUAL VOTE (is_mask_vote == false)
// - This is an eligible voter casting a vote
// - Verify vote amount <= balance
// - Return new vote ciphertext (ct0is, ct1is)
// - Validate signature over the hashed message (voter authenticates themselves)
// - Verify address matches slot address
// - Return new vote ciphertext commitment (ct_commitment)
//
// CASE 2: MASK VOTE
// Condition: Signature invalid OR address mismatch
// CASE 2: MASK VOTE (is_mask_vote == true)
// - 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 vote is zero (k1 must be zero)
// - If first vote in slot: return zero ciphertext commitment (ct_commitment)
// - If updating slot:
// * Verify that the sum ciphertext correctly represents the homomorphic addition
// of a zero vote to the previous ciphertext without decrypting
// * Mask votes add zero to the previous ciphertext, creating a different ciphertext
// with the same plaintext as the previous ciphertext
// * Generate commitments for prev_ct and sum_ct (ct_commitment is generated before)
// * Verify prev_ct_commitment matches the actual polynomials to ensure the prover
// hasn't tampered with the previous ciphertext
// * Verify ciphertext addition using commitments and Fiat-Shamir transform with
// Schwartz-Zippel lemma verification
// * Return sum ciphertext commitment (sum_ct_commitment)

// Generate the vote ciphertext commitment.
let ct_commitment = generate_commitment::<512, 2, 36>(ct0is, ct1is);

if is_mask_vote == false {
check_coefficient_values_with_balance(k1, Q_MOD_T_MOD_P, balance);
validate_signature(hashed_message, public_key_x, public_key_y, signature);

let voter_address = address_to_field(derive_address(public_key_x, public_key_y));
assert(slot_address == voter_address);

ct_commitment
} else {
let is_vote_zero = check_coefficient_zero(k1);
assert(is_vote_zero == true);
check_coefficient_zero(k1);

if is_first_vote {
ct_commitment
} else {
let _prev_ct_commitment = generate_commitment::<512, 2, 36>(prev_ct0is, prev_ct1is);
let sum_ct_commitment = generate_commitment::<512, 2, 36>(sum_ct0is, sum_ct1is);

let ct_add: CiphertextAddition<512, 2, 36, 36, 36> = CiphertextAddition::new(
GRECO_CONFIGS,
ct0is,
ct1is,
ct_commitment,
prev_ct0is,
prev_ct1is,
_prev_ct_commitment,
sum_ct0is,
sum_ct1is,
sum_ct_commitment,
sum_r0is,
sum_r1is,
);

assert(prev_ct_commitment == _prev_ct_commitment);
assert(is_ct_add_valid);
assert(ct_add.verify());

sum_ct_commitment
}
Expand Down
24 changes: 6 additions & 18 deletions examples/CRISP/circuits/src/utils.nr
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,10 @@ pub fn check_coefficient_values_with_balance<let D: u32>(
/// # 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<let D: u32>(k1: Polynomial<D>) -> bool {
pub fn check_coefficient_zero<let D: u32>(k1: Polynomial<D>) {
let HALF_D = D / 2;

// Define vote region boundaries
Expand All @@ -109,24 +106,15 @@ pub fn check_coefficient_zero<let D: u32>(k1: Polynomial<D>) -> bool {
let START_INDEX_N = 0;
let END_INDEX_N = HALF_LARGEST_MINIMUM_DEGREE;

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;
}
assert(k1.coefficients[i] == 0);
}

// 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;
}
assert(k1.coefficients[i] == 0);
}

// Return true only if both regions are completely zero
res
}

#[test]
Expand Down Expand Up @@ -205,7 +193,7 @@ fn test_check_coefficient_values_fail() {
check_coefficient_values_with_balance(pol, 1, 100);
}

#[test]
#[test(should_fail)]
fn test_check_coefficient_zero_fail() {
let pol = Polynomial {
coefficients: [
Expand All @@ -217,7 +205,7 @@ fn test_check_coefficient_zero_fail() {
],
};

assert(check_coefficient_zero(pol) == false);
check_coefficient_zero(pol);
}

#[test]
Expand All @@ -231,5 +219,5 @@ fn test_check_coefficient_zero() {
],
};

assert(check_coefficient_zero(pol) == true);
check_coefficient_zero(pol);
}
Loading
Loading