diff --git a/examples/CRISP/circuits/src/main.nr b/examples/CRISP/circuits/src/main.nr index 6a073fd924..08a2f8c7d1 100644 --- a/examples/CRISP/circuits/src/main.nr +++ b/examples/CRISP/circuits/src/main.nr @@ -120,7 +120,7 @@ fn main( } else { // check if vote == 0. let is_vote_zero = check_coefficient_zero(k1); - assert(is_vote_zero); + assert(is_vote_zero == true); // need to check if slot is empty (no previous ciphertext). // If so, (ct0is, ct1is) should be returned. diff --git a/examples/CRISP/circuits/src/utils.nr b/examples/CRISP/circuits/src/utils.nr index 133e11fff1..964da98e36 100644 --- a/examples/CRISP/circuits/src/utils.nr +++ b/examples/CRISP/circuits/src/utils.nr @@ -6,40 +6,44 @@ use polynomial::Polynomial; -// Check that all valid coefficients are either 0 or 1 and that the vote is <= balance +// Check that all valid coefficients are either 0 or q_mod_t and that the vote is <= balance pub fn check_coefficient_values_with_balance( k1: Polynomial, q_mod_t: Field, balance: Field, ) { - // 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) + // This value allows to fit 268,435,455 for yes and 268,435,455 for no 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; + + // After reversal, bits that were at END of first half are now at START of second half + 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 + let END_INDEX_N = HALF_LARGEST_MINIMUM_DEGREE; let mut sum_yes: u64 = 0; let mut sum_no: u64 = 0; - // Loop through all coefficients in the space where we could have a vote - // yes part - for i in START_INDEX_Y..HALF_D { - let coeff = k1.coefficients[i]; + // Yes part - process from END to START (reverse order) + for i in 0..HALF_LARGEST_MINIMUM_DEGREE { + // Access indices backwards: from (END_INDEX_Y - 1) down to START_INDEX_Y + let idx = END_INDEX_Y - 1 - i; + let coeff = k1.coefficients[idx]; assert(0 == coeff * (q_mod_t - coeff)); - // evaluate using Horner's method - sum_yes = sum_yes + sum_yes + coeff as u64; + let bit: u64 = if coeff == 0 { 0 } else { 1 }; + sum_yes = sum_yes * 2 + bit; } - // no part - for i in START_INDEX_N..D { - let coeff = k1.coefficients[i]; + // No part - process from END to START (reverse order) + for i in 0..HALF_LARGEST_MINIMUM_DEGREE { + let idx = END_INDEX_N - 1 - i; + let coeff = k1.coefficients[idx]; assert(0 == coeff * (q_mod_t - coeff)); - // evaluate using Horner's method - sum_no = sum_no + sum_no + coeff as u64; + let bit: u64 = if coeff == 0 { 0 } else { 1 }; + sum_no = sum_no * 2 + bit; } if (sum_yes != 0) { @@ -53,28 +57,25 @@ pub fn check_coefficient_values_with_balance( } } -// Check that the polynomial has 0 for all relevant coefficients 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) 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 START_INDEX_Y = HALF_D; + let END_INDEX_Y = HALF_D + HALF_LARGEST_MINIMUM_DEGREE; + + let START_INDEX_N = 0; + let END_INDEX_N = HALF_LARGEST_MINIMUM_DEGREE; 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 { + for i in START_INDEX_Y..END_INDEX_Y { if k1.coefficients[i] != 0 { res = false; } } - // no part - for i in START_INDEX_N..D { + for i in START_INDEX_N..END_INDEX_N { if k1.coefficients[i] != 0 { res = false; } @@ -88,9 +89,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, 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, 1, 1, + 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, ], }; @@ -101,10 +103,12 @@ fn test_check_vote_is_within_balance_pass() { fn test_check_vote_is_within_balance_yes_and_no_not_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, 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, 1, 1, 1, + // NO votes: index 0 + 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, + // YES votes: indices 50-52 + 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, ], }; @@ -116,9 +120,10 @@ fn test_check_vote_is_within_balance_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, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // YES votes: indices 50-52 (binary 111 = 7, exceeds balance of 1) + 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, ], }; @@ -129,9 +134,10 @@ fn test_check_vote_is_within_balance_fail() { fn test_check_coefficient_values_pass() { let pol = Polynomial { coefficients: [ + // NO vote: index 0 (binary 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], }; @@ -143,10 +149,11 @@ fn test_check_coefficient_values_pass() { fn test_check_coefficient_values_fail() { let pol = Polynomial { coefficients: [ + // Invalid coefficient value 2 (not 0 or 1) + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], }; @@ -158,9 +165,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, 0, 0, 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, + 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, ], }; diff --git a/examples/CRISP/packages/crisp-sdk/src/constants.ts b/examples/CRISP/packages/crisp-sdk/src/constants.ts index 603bb29d12..3cf49dfb41 100644 --- a/examples/CRISP/packages/crisp-sdk/src/constants.ts +++ b/examples/CRISP/packages/crisp-sdk/src/constants.ts @@ -14,13 +14,13 @@ export const CRISP_SERVER_STATE_LITE_ENDPOINT = 'state/lite' * Half the minimum degree needed to support the maxium vote value * If you change MAXIMUM_VOTE_VALUE, make sure to update this value too. */ -export const HALF_LARGEST_MINIMUM_DEGREE = 28; +export const HALF_LARGEST_MINIMUM_DEGREE = 28 /** - * This is the maximum value for a vote (Yes or No). This is 2^28 + * This is the maximum value for a vote (Yes or No). This is 2^28 - 1 * The minimum degree that BFV should use is 56 (to accommodate both Yes and No votes) */ -export const MAXIMUM_VOTE_VALUE = BigInt(Math.pow(2, HALF_LARGEST_MINIMUM_DEGREE)) +export const MAXIMUM_VOTE_VALUE = BigInt(Math.pow(2, HALF_LARGEST_MINIMUM_DEGREE) - 1) /** * Default BFV parameters for the CRISP ZK inputs generator. diff --git a/examples/CRISP/packages/crisp-sdk/src/vote.ts b/examples/CRISP/packages/crisp-sdk/src/vote.ts index 3c4ba12f51..86bc8ce47b 100644 --- a/examples/CRISP/packages/crisp-sdk/src/vote.ts +++ b/examples/CRISP/packages/crisp-sdk/src/vote.ts @@ -97,37 +97,37 @@ export const encodeVote = (vote: IVote, votingMode: VotingMode, votingPower: big export const decodeTally = (tally: string[], votingMode: VotingMode): IVote => { switch (votingMode) { case VotingMode.GOVERNANCE: - const HALF_D = tally.length / 2; - const START_INDEX_Y = HALF_D - HALF_LARGEST_MINIMUM_DEGREE; - const START_INDEX_N = tally.length - HALF_LARGEST_MINIMUM_DEGREE; + const HALF_D = tally.length / 2 + const START_INDEX_Y = HALF_D - HALF_LARGEST_MINIMUM_DEGREE + const START_INDEX_N = tally.length - HALF_LARGEST_MINIMUM_DEGREE // Extract only the relevant parts of the tally - const yesBinary = tally.slice(START_INDEX_Y, HALF_D); - const noBinary = tally.slice(START_INDEX_N, tally.length); + const yesBinary = tally.slice(START_INDEX_Y, HALF_D) + const noBinary = tally.slice(START_INDEX_N, tally.length) - let yes = 0n; - let no = 0n; + let yes = 0n + let no = 0n // Convert yes votes (from START_INDEX_Y to HALF_D) for (let i = 0; i < yesBinary.length; i += 1) { - const weight = 2n ** BigInt(yesBinary.length - 1 - i); - yes += BigInt(yesBinary[i]) * weight; + const weight = 2n ** BigInt(yesBinary.length - 1 - i) + yes += BigInt(yesBinary[i]) * weight } // Convert no votes (from START_INDEX_N to D) for (let i = 0; i < noBinary.length; i += 1) { - const weight = 2n ** BigInt(noBinary.length - 1 - i); - no += BigInt(noBinary[i]) * weight; + const weight = 2n ** BigInt(noBinary.length - 1 - i) + no += BigInt(noBinary[i]) * weight } return { yes, no, - }; + } default: - throw new Error('Unsupported voting mode'); + throw new Error('Unsupported voting mode') } -}; +} /** * Validate whether a vote is valid for a given voting mode @@ -271,7 +271,9 @@ export const generateProof = async (crispInputs: CRISPCircuitInputs): Promise => { +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) @@ -281,6 +283,14 @@ export const generateProofWithReturnValue = async (crispInputs: CRISPCircuitInpu return { returnValue, proof } } +export const getCircuitOutputValue = async (crispInputs: CRISPCircuitInputs): Promise<{ returnValue: unknown }> => { + const noir = new Noir(circuit as CompiledCircuit) + + const { returnValue } = await noir.execute(crispInputs as any) + + return { returnValue } +} + 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 index 874e8f164a..624a8dbb6b 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/utils.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/utils.ts @@ -5,31 +5,31 @@ // or FITNESS FOR A PARTICULAR PURPOSE. export function normalizeCoefficient(coeff: string): string { - if (coeff.startsWith('0x')) { - return BigInt(coeff).toString(); - } - return coeff.toString(); + 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; - } + 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; + } + + 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 daf75e271d..64243ecd25 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts @@ -14,6 +14,7 @@ import { generateMaskVote, generateProof, generateProofWithReturnValue, + getCircuitOutputValue, validateVote, verifyProof, } from '../src/vote' @@ -54,11 +55,68 @@ describe('Vote', () => { describe('decode tally', () => { it('should decode correctly', () => { - const tally = ['0', '2', '0', '1', '0', '0', '0', '0', '0', '0'] + const tally = [ + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '5', + '0', + '2', + '0', + '0', + '0', + '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', + ] const decoded = decodeTally(tally, VotingMode.GOVERNANCE) - expect(decoded.yes).toBe(18n) - expect(decoded.no).toBe(0n) + expect(decoded.yes).toBe(22n) + expect(decoded.no).toBe(1n) }) }) @@ -172,7 +230,14 @@ 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, false) + 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) @@ -262,8 +327,8 @@ describe('Vote', () => { 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); + 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 () => { @@ -280,8 +345,101 @@ describe('Vote', () => { 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); + expect(compareCoefficientsArrays(maskVote.sum_ct0is, (returnValue as any[])[0])).toBe(true) + expect(compareCoefficientsArrays(maskVote.sum_ct1is, (returnValue as any[])[1])).toBe(true) + }) + + it('should throw when the signature is invalid and it is a vote (no masking)', { timeout: 100000 }, async () => { + const encodedVote = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPowerLeaf) + + // 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, + message: MESSAGE, + merkleData: merkleProof, + balance: votingPowerLeaf, + slotAddress: account.address.toLowerCase(), + isFirstVote: false, + }) + + // invalidate signature + inputs.signature[0] = '0' + + expect(async () => { + await getCircuitOutputValue(inputs) + }).rejects.toThrow() + }) + + it('should throw when the merkle tree inclusion proof is invalid and it is a vote (no masking)', { timeout: 100000 }, async () => { + const encodedVote = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPowerLeaf) + + // 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, + message: MESSAGE, + merkleData: merkleProof, + balance: votingPowerLeaf, + slotAddress: account.address.toLowerCase(), + isFirstVote: false, + }) + + // invalidate signature + inputs.merkle_root = '0' + + expect(async () => { + await getCircuitOutputValue(inputs) + }).rejects.toThrow() + }) + + it('should throw when the vote is > balance', { timeout: 100000 }, async () => { + const encodedVote = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPowerLeaf) + + // 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, + message: MESSAGE, + merkleData: merkleProof, + balance: votingPowerLeaf, + slotAddress: account.address.toLowerCase(), + isFirstVote: false, + }) + + // set balance to 0 + inputs.balance = '0' + + expect(async () => { + await getCircuitOutputValue(inputs) + }).rejects.toThrow() }) }) })