Skip to content
Merged
26 changes: 17 additions & 9 deletions examples/CRISP/circuits/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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.
Expand All @@ -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)
}
Comment thread
ctrlc03 marked this conversation as resolved.
}
}
20 changes: 12 additions & 8 deletions examples/CRISP/circuits/src/utils.nr
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub fn check_coefficient_values<let D: u32>(k1: Polynomial<D>, q_mod_t: Field) {
}

// Check that the polynomial has 0 for all relevant coefficients
pub fn check_coefficient_zero<let D: u32>(k1: Polynomial<D>) {
pub fn check_coefficient_zero<let D: u32>(k1: Polynomial<D>) -> 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)
Expand All @@ -42,20 +42,24 @@ pub fn check_coefficient_zero<let D: u32>(k1: Polynomial<D>) {
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]
Expand Down Expand Up @@ -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: [
Expand All @@ -97,7 +101,7 @@ fn test_check_coefficient_zero_fail() {
],
};

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

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

check_coefficient_zero(pol);
assert(check_coefficient_zero(pol) == true);
}
Comment thread
ctrlc03 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions examples/CRISP/packages/crisp-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -214,4 +216,5 @@ export interface EncryptVoteAndGenerateCRISPInputsParams {
balance: bigint
bfvParams?: BFVParams
slotAddress: string
isFirstVote: boolean
}
16 changes: 16 additions & 0 deletions examples/CRISP/packages/crisp-sdk/src/vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
Expand All @@ -170,6 +171,7 @@ export const encryptVoteAndGenerateCRISPInputs = async ({
signature,
balance,
slotAddress,
isFirstVote,
}: EncryptVoteAndGenerateCRISPInputsParams): Promise<CRISPCircuitInputs> => {
if (encodedVote.length !== bfvParams.degree) {
throw new RangeError(`encodedVote length ${encodedVote.length} does not match BFV degree ${bfvParams.degree}`)
Expand All @@ -195,6 +197,7 @@ export const encryptVoteAndGenerateCRISPInputs = async ({
merkle_root: merkleData.proof.root.toString(),
slot_address: slotAddress,
balance: balance.toString(),
is_first_vote: isFirstVote,
}
}

Expand All @@ -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 (
Expand All @@ -214,6 +218,7 @@ export const generateMaskVote = async (
bfvParams = DEFAULT_BFV_PARAMS,
merkleRoot: bigint,
slotAddress: string,
isFirstVote: boolean,
): Promise<CRISPCircuitInputs> => {
const plaintextVote: IVote = {
yes: 0n,
Expand Down Expand Up @@ -246,6 +251,7 @@ export const generateMaskVote = async (
merkle_root: merkleRoot.toString(),
slot_address: slotAddress,
balance: '0',
is_first_vote: isFirstVote,
}
}

Expand All @@ -259,6 +265,16 @@ export const generateProof = async (crispInputs: CRISPCircuitInputs): Promise<Pr
return proof
}

export const generateProofWithReturnValue = async (crispInputs: CRISPCircuitInputs): Promise<{ returnValue: unknown, proof:ProofData }> => {
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 }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export const verifyProof = async (proof: ProofData): Promise<boolean> => {
const backend = new UltraHonkBackend((circuit as CompiledCircuit).bytecode)

Expand Down
35 changes: 35 additions & 0 deletions examples/CRISP/packages/crisp-sdk/tests/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
46 changes: 43 additions & 3 deletions examples/CRISP/packages/crisp-sdk/tests/vote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
encryptVoteAndGenerateCRISPInputs,
generateMaskVote,
generateProof,
generateProofWithReturnValue,
validateVote,
verifyProof,
} from '../src/vote'
Expand All @@ -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
Expand Down Expand Up @@ -139,6 +141,7 @@ describe('Vote', () => {
merkleData: merkleProof,
balance: votingPowerLeaf,
slotAddress: testAddress,
isFirstVote: false,
})

expect(crispInputs.prev_ct0is).toBeInstanceOf(Array)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -218,6 +221,7 @@ describe('Vote', () => {
merkleData: merkleProof,
balance: votingPowerLeaf,
slotAddress: account.address.toLowerCase(),
isFirstVote: false,
})

const proof = await generateProof(inputs)
Expand All @@ -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);
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
})
Loading