diff --git a/examples/CRISP/circuits/src/main.nr b/examples/CRISP/circuits/src/main.nr index 2f3a3490a2..5f35542770 100644 --- a/examples/CRISP/circuits/src/main.nr +++ b/examples/CRISP/circuits/src/main.nr @@ -14,7 +14,7 @@ use ecdsa::{address_to_field, derive_address, verify_signature}; mod merkle_tree; use merkle_tree::get_merkle_root; mod utils; -use utils::check_coefficient_values; +use utils::{check_coefficient_values, check_coefficient_zero}; fn main( // Ciphertext Addition Section. @@ -84,6 +84,7 @@ fn main( p1is, p2is, ); + let ct_add: CiphertextAddition<2048, 1, 54, 54, 54> = CiphertextAddition::new( params.crypto_params(), ct0is, @@ -99,18 +100,17 @@ fn main( // Verify the correct ciphertext encryption. let is_greco_valid = greco.verify(); - assert(is_greco_valid == true); - // Verify the correct ciphertext addition. let is_ct_add_valid = ct_add.verify(); + // Ensure that the ciphertext is valid + assert(is_greco_valid); + // If the voter is eligible to vote, output the ciphertext. // Otherwise, output the sum of the ciphertexts. - if ( - (is_signature_valid == true) - & (merkle_root_calculated == merkle_root) - & (slot_address == address) - ) { + if (is_signature_valid == true) + & (merkle_root_calculated == merkle_root) + & (slot_address == address) { // @todo: need to check if vote <= balance. // Verify the correct coefficient values. @@ -118,12 +118,14 @@ fn main( (ct0is, ct1is) } else { - // @todo: need to check if vote == 0. + // check if vote == 0. + check_coefficient_zero(k1); // @todo: need to check if slot is empty (no previous ciphertext). // If so, (ct0is, ct1is) should be returned. - assert(is_ct_add_valid == true); + // as well as the sum + assert(is_ct_add_valid); (sum_ct0is, sum_ct1is) } diff --git a/examples/CRISP/circuits/src/utils.nr b/examples/CRISP/circuits/src/utils.nr index d9b33940fd..1300072f87 100644 --- a/examples/CRISP/circuits/src/utils.nr +++ b/examples/CRISP/circuits/src/utils.nr @@ -31,6 +31,33 @@ pub fn check_coefficient_values(k1: Polynomial, q_mod_t: Field) { } } +// Check that the polynomial has 0 for all relevant coefficients +pub fn check_coefficient_zero(k1: Polynomial) { + // This value would allow to fit + // 268435456 for yes and 268435456 for no + // which would fit the supply of most tokens really (imagining one user just holds all tokens) + let HALF_LARGEST_MINIMUM_DEGREE = 28; + + let HALF_D = D / 2; + let START_INDEX_Y = HALF_D - HALF_LARGEST_MINIMUM_DEGREE; + let START_INDEX_N = D - HALF_LARGEST_MINIMUM_DEGREE; + + let mut sum = 0; + + // Loop through all coefficients in the space where we could have a vote + // yes part + for i in START_INDEX_Y..HALF_D { + sum += k1.coefficients[i]; + } + + // no part + for i in START_INDEX_N..D { + sum += k1.coefficients[i]; + } + + assert(sum == 0); +} + #[test] fn test_check_coefficient_values_pass() { let pol = Polynomial { @@ -58,3 +85,31 @@ fn test_check_coefficient_values_fail() { check_coefficient_values(pol, 1); } + +#[test(should_fail)] +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, 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, 1, + ], + }; + + check_coefficient_zero(pol); +} + +#[test] +fn test_check_coefficient_zero() { + 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, 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, + ], + }; + + check_coefficient_zero(pol); +} diff --git a/examples/CRISP/packages/crisp-sdk/src/constants.ts b/examples/CRISP/packages/crisp-sdk/src/constants.ts index 2b701e0251..c757a0daaa 100644 --- a/examples/CRISP/packages/crisp-sdk/src/constants.ts +++ b/examples/CRISP/packages/crisp-sdk/src/constants.ts @@ -22,3 +22,8 @@ export const MAXIMUM_VOTE_VALUE = 268435456n * These are the parameters used for the default testing purposes only. */ export const DEFAULT_BFV_PARAMS = ZKInputsGenerator.withDefaults().getBFVParams() as BFVParams + +/** + * Mock message for masking signature + */ +export const MESSAGE = 'Vote for round 0' diff --git a/examples/CRISP/packages/crisp-sdk/src/types.ts b/examples/CRISP/packages/crisp-sdk/src/types.ts index 0dd7228a2f..05330732b9 100644 --- a/examples/CRISP/packages/crisp-sdk/src/types.ts +++ b/examples/CRISP/packages/crisp-sdk/src/types.ts @@ -213,4 +213,5 @@ export interface EncryptVoteAndGenerateCRISPInputsParams { merkleData: IMerkleProof balance: bigint bfvParams?: BFVParams + slotAddress: string } diff --git a/examples/CRISP/packages/crisp-sdk/src/utils.ts b/examples/CRISP/packages/crisp-sdk/src/utils.ts index d3928eca4b..804013b60f 100644 --- a/examples/CRISP/packages/crisp-sdk/src/utils.ts +++ b/examples/CRISP/packages/crisp-sdk/src/utils.ts @@ -61,9 +61,8 @@ export const generateMerkleProof = ( // Pad siblings with zeros const paddedSiblings = [...proof.siblings, ...Array(maxDepth - proof.siblings.length).fill(0n)] - // Pad indices with zeros - const indices = proof.siblings.map((_, i) => Number((BigInt(index) >> BigInt(i)) & 1n)) + const indices = proof.siblings.map((_, i) => Number((BigInt(proof.index) >> BigInt(i)) & 1n)) const paddedIndices = [...indices, ...Array(maxDepth - indices.length).fill(0)] return { diff --git a/examples/CRISP/packages/crisp-sdk/src/vote.ts b/examples/CRISP/packages/crisp-sdk/src/vote.ts index 3e00020de1..ae2f35b2ab 100644 --- a/examples/CRISP/packages/crisp-sdk/src/vote.ts +++ b/examples/CRISP/packages/crisp-sdk/src/vote.ts @@ -7,11 +7,12 @@ import { ZKInputsGenerator } from '@enclave/crisp-zk-inputs' import { BFVParams, type CRISPCircuitInputs, type EncryptVoteAndGenerateCRISPInputsParams, type IVote, VotingMode } from './types' import { toBinary } from './utils' -import { MAXIMUM_VOTE_VALUE, DEFAULT_BFV_PARAMS } from './constants' +import { MAXIMUM_VOTE_VALUE, DEFAULT_BFV_PARAMS, MESSAGE } from './constants' import { extractSignature } from './signature' import { Noir, type CompiledCircuit } from '@noir-lang/noir_js' import { UltraHonkBackend, type ProofData } from '@aztec/bb.js' import circuit from '../../../circuits/target/crisp_circuit.json' +import { privateKeyToAccount } from 'viem/accounts' /** * This utility function calculates the first valid index for vote options @@ -152,6 +153,11 @@ export const validateVote = (votingMode: VotingMode, vote: IVote, votingPower: b * @param publicKey The public key to use for encryption * @param previousCiphertext The previous ciphertext to use for addition operation * @param bfvParams The BFV parameters to use for encryption + * @param merkleData The merkle proof data + * @param message The message that was signed + * @param signature The signature of the message + * @param balance The voter's balance + * @param slotAddress The voter's slot address * @returns The CRISP circuit inputs */ export const encryptVoteAndGenerateCRISPInputs = async ({ @@ -163,6 +169,7 @@ export const encryptVoteAndGenerateCRISPInputs = async ({ message, signature, balance, + slotAddress, }: EncryptVoteAndGenerateCRISPInputsParams): Promise => { if (encodedVote.length !== bfvParams.degree) { throw new RangeError(`encodedVote length ${encodedVote.length} does not match BFV degree ${bfvParams.degree}`) @@ -186,7 +193,7 @@ export const encryptVoteAndGenerateCRISPInputs = async ({ merkle_proof_indices: merkleData.indices.map((i) => i.toString()), merkle_proof_siblings: merkleData.proof.siblings.map((s) => s.toString()), merkle_root: merkleData.proof.root.toString(), - slot_address: merkleData.proof.root.toString(), // temporary, will be replaced with the actual slot address. + slot_address: slotAddress, balance: balance.toString(), } } @@ -196,6 +203,9 @@ export const encryptVoteAndGenerateCRISPInputs = async ({ * @param voter The voter's address * @param publicKey The voter's public key * @param previousCiphertext The previous ciphertext + * @param bfvParams The BFV parameters + * @param merkleRoot The merkle root of the census tree + * @param slotAddress The voter's slot address * @returns The CRISP circuit inputs for a mask vote */ export const generateMaskVote = async ( @@ -203,6 +213,7 @@ export const generateMaskVote = async ( previousCiphertext: Uint8Array, bfvParams = DEFAULT_BFV_PARAMS, merkleRoot: bigint, + slotAddress: string, ): Promise => { const plaintextVote: IVote = { yes: 0n, @@ -217,17 +228,23 @@ export const generateMaskVote = async ( const crispInputs = (await zkInputsGenerator.generateInputs(previousCiphertext, publicKey, vote)) as CRISPCircuitInputs + // hardhat default private key + const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + const account = privateKeyToAccount(privateKey) + const signature = await account.signMessage({ message: MESSAGE }) + const { hashed_message, pub_key_x, pub_key_y, signature: extractedSignature } = await extractSignature(MESSAGE, signature) + return { ...crispInputs, - public_key_x: Array.from({ length: 32 }, () => '0'), - public_key_y: Array.from({ length: 32 }, () => '0'), - signature: Array.from({ length: 64 }, () => '0'), - hashed_message: Array.from({ length: 32 }, () => '0'), - merkle_proof_indices: Array.from({ length: 4 }, () => '0'), - merkle_proof_siblings: Array.from({ length: 4 }, () => '0'), + hashed_message: Array.from(hashed_message).map((b) => b.toString()), + public_key_x: Array.from(pub_key_x).map((b) => b.toString()), + public_key_y: Array.from(pub_key_y).map((b) => b.toString()), + signature: Array.from(extractedSignature).map((b) => b.toString()), + merkle_proof_indices: Array.from({ length: 20 }, () => '0'), + merkle_proof_siblings: Array.from({ length: 20 }, () => '0'), merkle_proof_length: '1', merkle_root: merkleRoot.toString(), - slot_address: merkleRoot.toString(), // temporary, will be replaced with the actual slot address. + slot_address: slotAddress, balance: '0', } } diff --git a/examples/CRISP/packages/crisp-sdk/tests/constants.ts b/examples/CRISP/packages/crisp-sdk/tests/constants.ts index 40c03e35fd..26d797d734 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/constants.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/constants.ts @@ -29,4 +29,5 @@ export const LEAVES = [ export const MAX_DEPTH = 20 export const votingPowerLeaf = 1000n -export const merkleProof = generateMerkleProof(0n, votingPowerLeaf, '0x1234567890123456789012345678901234567890', LEAVES, MAX_DEPTH) +export const testAddress = '0x1234567890123456789012345678901234567890' +export const merkleProof = generateMerkleProof(0n, votingPowerLeaf, testAddress, LEAVES, MAX_DEPTH) diff --git a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts index 61c385d9f3..5b06c80d18 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts @@ -6,7 +6,6 @@ import { describe, it, expect } from 'vitest' import { ZKInputsGenerator } from '@enclave/crisp-zk-inputs' - import { calculateValidIndicesForPlaintext, decodeTally, @@ -18,12 +17,10 @@ import { verifyProof, } from '../src/vote' import { BFVParams, VotingMode } from '../src/types' -import { DEFAULT_BFV_PARAMS, MAXIMUM_VOTE_VALUE } from '../src' -import { UltraHonkBackend, type ProofData } from '@aztec/bb.js' -import { Noir, type CompiledCircuit } from '@noir-lang/noir_js' -import circuit from '../../../circuits/target/crisp_circuit.json' +import { DEFAULT_BFV_PARAMS, generateMerkleProof, hashLeaf, MAXIMUM_VOTE_VALUE } from '../src' -import { merkleProof, MESSAGE, SIGNATURE, VOTE, votingPowerLeaf } from './constants' +import { LEAVES, merkleProof, MESSAGE, SIGNATURE, testAddress, VOTE, votingPowerLeaf } from './constants' +import { privateKeyToAccount } from 'viem/accounts' describe('Vote', () => { const votingPower = 10n @@ -141,6 +138,7 @@ describe('Vote', () => { message: MESSAGE, merkleData: merkleProof, balance: votingPowerLeaf, + slotAddress: testAddress, }) expect(crispInputs.prev_ct0is).toBeInstanceOf(Array) @@ -171,7 +169,7 @@ describe('Vote', () => { describe('generateMaskVote', () => { it('should generate a mask vote and the right circuit inputs', async () => { - const crispInputs = await generateMaskVote(publicKey, previousCiphertext, DEFAULT_BFV_PARAMS, merkleProof.proof.root) + const crispInputs = await generateMaskVote(publicKey, previousCiphertext, DEFAULT_BFV_PARAMS, merkleProof.proof.root, testAddress) expect(crispInputs.prev_ct0is).toBeInstanceOf(Array) expect(crispInputs.prev_ct1is).toBeInstanceOf(Array) @@ -200,16 +198,26 @@ describe('Vote', () => { }) describe('generateProof/verifyProof', () => { - it('should generate a proof and verify it', { timeout: 60000 }, async () => { + it('should generate a proof for a voter and verify it', { timeout: 180000 }, async () => { const encodedVote = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPower) + + // hardhat default private key + const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + const account = privateKeyToAccount(privateKey) + const signature = await account.signMessage({ message: MESSAGE }) + const leaf = hashLeaf(account.address.toLowerCase(), votingPowerLeaf.toString()) + const leaves = [...LEAVES, leaf] + const merkleProof = generateMerkleProof(0n, votingPowerLeaf, account.address.toLowerCase(), leaves, 20) + const inputs = await encryptVoteAndGenerateCRISPInputs({ encodedVote, publicKey, previousCiphertext, - signature: SIGNATURE, + signature, message: MESSAGE, merkleData: merkleProof, balance: votingPowerLeaf, + slotAddress: account.address.toLowerCase(), }) const proof = await generateProof(inputs) @@ -217,5 +225,23 @@ describe('Vote', () => { expect(isValid).toBe(true) }) + + it('should generate a proof for a masking user and verify it', { timeout: 180000 }, async () => { + const encodedVote = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPower) + const zkInputsGenerator: ZKInputsGenerator = new ZKInputsGenerator( + DEFAULT_BFV_PARAMS.degree, + DEFAULT_BFV_PARAMS.plaintextModulus, + DEFAULT_BFV_PARAMS.moduli, + ) + const vote = BigInt64Array.from(encodedVote.map(BigInt)) + const encryptedVote = zkInputsGenerator.encryptVote(publicKey, vote) + + let maskVote = await generateMaskVote(publicKey, encryptedVote, DEFAULT_BFV_PARAMS, merkleProof.proof.root, testAddress) + + const proof = await generateProof(maskVote) + const isValid = await verifyProof(proof) + + expect(isValid).toBe(true) + }) }) })