diff --git a/examples/CRISP/circuits/src/main.nr b/examples/CRISP/circuits/src/main.nr index 5f35542770..f6c6b9ccff 100644 --- a/examples/CRISP/circuits/src/main.nr +++ b/examples/CRISP/circuits/src/main.nr @@ -52,6 +52,8 @@ fn main( slot_address: pub Field, // Balance Section. balance: Field, + // Whether this is the first vote for this slot. + is_first_vote: pub bool, ) -> pub ([Polynomial<2048>; 1], [Polynomial<2048>; 1]) { // Verify the ECDSA signature. let is_signature_valid = @@ -108,9 +110,11 @@ fn main( // 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. @@ -119,14 +123,18 @@ fn main( (ct0is, ct1is) } else { // check if vote == 0. - check_coefficient_zero(k1); + let is_vote_zero = check_coefficient_zero(k1); + assert(is_vote_zero); - // @todo: need to check if slot is empty (no previous ciphertext). + // need to check if slot is empty (no previous ciphertext). // If so, (ct0is, ct1is) should be returned. - // as well as the sum - assert(is_ct_add_valid); - - (sum_ct0is, sum_ct1is) + 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/utils.nr b/examples/CRISP/circuits/src/utils.nr index 1300072f87..30ae1f6b93 100644 --- a/examples/CRISP/circuits/src/utils.nr +++ b/examples/CRISP/circuits/src/utils.nr @@ -32,7 +32,7 @@ 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) { +pub fn check_coefficient_zero(k1: Polynomial) -> bool { // 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) @@ -42,20 +42,24 @@ pub fn check_coefficient_zero(k1: Polynomial) { let START_INDEX_Y = HALF_D - HALF_LARGEST_MINIMUM_DEGREE; let START_INDEX_N = D - HALF_LARGEST_MINIMUM_DEGREE; - let mut sum = 0; + let mut res = true; // 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]; + if k1.coefficients[i] != 0 { + res = false; + } } // no part for i in START_INDEX_N..D { - sum += k1.coefficients[i]; + if k1.coefficients[i] != 0 { + res = false; + } } - assert(sum == 0); + res } #[test] @@ -86,7 +90,7 @@ fn test_check_coefficient_values_fail() { check_coefficient_values(pol, 1); } -#[test(should_fail)] +#[test] fn test_check_coefficient_zero_fail() { let pol = Polynomial { coefficients: [ @@ -97,7 +101,7 @@ fn test_check_coefficient_zero_fail() { ], }; - check_coefficient_zero(pol); + assert(check_coefficient_zero(pol) == false); } #[test] @@ -111,5 +115,5 @@ fn test_check_coefficient_zero() { ], }; - check_coefficient_zero(pol); + assert(check_coefficient_zero(pol) == true); } diff --git a/examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol b/examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol index b23029bc89..917879e62d 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol @@ -89,6 +89,11 @@ contract CRISPInputValidator is IInputValidator, Clone, Ownable(msg.sender) { address slot ) = abi.decode(data, (bytes, bytes32[], bytes, address)); + /// @notice we need to check whether the slot is empty. + /// if the slot is empty + /// @todo pass it to the verifier + // bool isFirstVote = voteSlots[slot].length == 0; + // Check if the ciphertext was encrypted correctly if (!noirVerifier.verify(noirProof, noirPublicInputs)) revert InvalidNoirProof(); diff --git a/examples/CRISP/packages/crisp-sdk/src/types.ts b/examples/CRISP/packages/crisp-sdk/src/types.ts index 05330732b9..39f04e0bbd 100644 --- a/examples/CRISP/packages/crisp-sdk/src/types.ts +++ b/examples/CRISP/packages/crisp-sdk/src/types.ts @@ -168,6 +168,8 @@ export interface CRISPCircuitInputs { slot_address: string // Balance Section. balance: string + // Whether this is the first vote for this slot. + is_first_vote: boolean } /** @@ -214,4 +216,5 @@ export interface EncryptVoteAndGenerateCRISPInputsParams { balance: bigint bfvParams?: BFVParams slotAddress: string + isFirstVote: boolean } diff --git a/examples/CRISP/packages/crisp-sdk/src/vote.ts b/examples/CRISP/packages/crisp-sdk/src/vote.ts index ae2f35b2ab..6bea3fd9f1 100644 --- a/examples/CRISP/packages/crisp-sdk/src/vote.ts +++ b/examples/CRISP/packages/crisp-sdk/src/vote.ts @@ -158,6 +158,7 @@ export const validateVote = (votingMode: VotingMode, vote: IVote, votingPower: b * @param signature The signature of the message * @param balance The voter's balance * @param slotAddress The voter's slot address + * @param isFirstVote Whether this is the first vote for this slot * @returns The CRISP circuit inputs */ export const encryptVoteAndGenerateCRISPInputs = async ({ @@ -170,6 +171,7 @@ export const encryptVoteAndGenerateCRISPInputs = async ({ signature, balance, slotAddress, + isFirstVote, }: EncryptVoteAndGenerateCRISPInputsParams): Promise => { if (encodedVote.length !== bfvParams.degree) { throw new RangeError(`encodedVote length ${encodedVote.length} does not match BFV degree ${bfvParams.degree}`) @@ -195,6 +197,7 @@ export const encryptVoteAndGenerateCRISPInputs = async ({ merkle_root: merkleData.proof.root.toString(), slot_address: slotAddress, balance: balance.toString(), + is_first_vote: isFirstVote, } } @@ -206,6 +209,7 @@ export const encryptVoteAndGenerateCRISPInputs = async ({ * @param bfvParams The BFV parameters * @param merkleRoot The merkle root of the census tree * @param slotAddress The voter's slot address + * @param isFirstVote Whether this is the first vote for this slot * @returns The CRISP circuit inputs for a mask vote */ export const generateMaskVote = async ( @@ -214,6 +218,7 @@ export const generateMaskVote = async ( bfvParams = DEFAULT_BFV_PARAMS, merkleRoot: bigint, slotAddress: string, + isFirstVote: boolean, ): Promise => { const plaintextVote: IVote = { yes: 0n, @@ -246,6 +251,7 @@ export const generateMaskVote = async ( merkle_root: merkleRoot.toString(), slot_address: slotAddress, balance: '0', + is_first_vote: isFirstVote, } } @@ -259,6 +265,16 @@ export const generateProof = async (crispInputs: CRISPCircuitInputs): Promise => { + const noir = new Noir(circuit as CompiledCircuit) + const backend = new UltraHonkBackend((circuit as CompiledCircuit).bytecode) + + const { witness, returnValue } = await noir.execute(crispInputs as any) + const proof = await backend.generateProof(witness) + + return { returnValue, proof } +} + export const verifyProof = async (proof: ProofData): Promise => { const backend = new UltraHonkBackend((circuit as CompiledCircuit).bytecode) diff --git a/examples/CRISP/packages/crisp-sdk/tests/utils.ts b/examples/CRISP/packages/crisp-sdk/tests/utils.ts new file mode 100644 index 0000000000..874e8f164a --- /dev/null +++ b/examples/CRISP/packages/crisp-sdk/tests/utils.ts @@ -0,0 +1,35 @@ +// 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. + +export function normalizeCoefficient(coeff: string): string { + if (coeff.startsWith('0x')) { + return BigInt(coeff).toString(); + } + return coeff.toString(); +} + +export function compareCoefficientsArrays(arr1: any[], arr2: any[]): boolean { + if (arr1.length !== arr2.length) return false; + + for (let i = 0; i < arr1.length; i++) { + if (!arr1[i] || !arr2[i]) return false; + + const coeff1 = arr1[i].coefficients; + const coeff2 = arr2[i].coefficients; + + if (!coeff1 || !coeff2) return false; + if (coeff1.length !== coeff2.length) return false; + + for (let k = 0; k < coeff1.length; k++) { + const normalized1 = normalizeCoefficient(coeff1[k]); + const normalized2 = normalizeCoefficient(coeff2[k]); + + if (normalized1 !== normalized2) return false; + } + } + + return true; +} diff --git a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts index 5b06c80d18..c4ee3328ad 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts @@ -13,6 +13,7 @@ import { encryptVoteAndGenerateCRISPInputs, generateMaskVote, generateProof, + generateProofWithReturnValue, validateVote, verifyProof, } from '../src/vote' @@ -21,6 +22,7 @@ import { DEFAULT_BFV_PARAMS, generateMerkleProof, hashLeaf, MAXIMUM_VOTE_VALUE } import { LEAVES, merkleProof, MESSAGE, SIGNATURE, testAddress, VOTE, votingPowerLeaf } from './constants' import { privateKeyToAccount } from 'viem/accounts' +import { compareCoefficientsArrays } from './utils' describe('Vote', () => { const votingPower = 10n @@ -139,6 +141,7 @@ describe('Vote', () => { merkleData: merkleProof, balance: votingPowerLeaf, slotAddress: testAddress, + isFirstVote: false, }) expect(crispInputs.prev_ct0is).toBeInstanceOf(Array) @@ -169,7 +172,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, testAddress) + const crispInputs = await generateMaskVote(publicKey, previousCiphertext, DEFAULT_BFV_PARAMS, merkleProof.proof.root, testAddress, false) expect(crispInputs.prev_ct0is).toBeInstanceOf(Array) expect(crispInputs.prev_ct1is).toBeInstanceOf(Array) @@ -198,7 +201,7 @@ describe('Vote', () => { }) describe('generateProof/verifyProof', () => { - it('should generate a proof for a voter and verify it', { timeout: 180000 }, async () => { + it('should generate a proof for a voter and verify it', { timeout: 100000 }, async () => { const encodedVote = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPower) // hardhat default private key @@ -218,6 +221,7 @@ describe('Vote', () => { merkleData: merkleProof, balance: votingPowerLeaf, slotAddress: account.address.toLowerCase(), + isFirstVote: false, }) const proof = await generateProof(inputs) @@ -236,12 +240,48 @@ describe('Vote', () => { 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) + let maskVote = await generateMaskVote(publicKey, encryptedVote, DEFAULT_BFV_PARAMS, merkleProof.proof.root, testAddress, false) const proof = await generateProof(maskVote) const isValid = await verifyProof(proof) expect(isValid).toBe(true) }) + + it('should return ciphertext if masking a vote and it is the first operation on the slot', { 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, true) + + const { returnValue } = await generateProofWithReturnValue(maskVote) + + expect(compareCoefficientsArrays(maskVote.ct0is, (returnValue as any[])[0])).toBe(true); + expect(compareCoefficientsArrays(maskVote.ct1is, (returnValue as any[])[1])).toBe(true); + }) + + it('should return the sum if masking a vote and it is not the first operation on the slot', { 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, false) + + const { returnValue } = await generateProofWithReturnValue(maskVote) + + expect(compareCoefficientsArrays(maskVote.sum_ct0is, (returnValue as any[])[0])).toBe(true); + expect(compareCoefficientsArrays(maskVote.sum_ct1is, (returnValue as any[])[1])).toBe(true); + }) }) })