From 331c2f8aada17799549095cc73df5e193a4cb105 Mon Sep 17 00:00:00 2001 From: Marc Velmer Date: Wed, 28 Feb 2024 19:18:28 +0100 Subject: [PATCH] Fixed anonymous `voteHash` by using the `votePackage` to generate zk-inputs --- src/client.ts | 28 ++++++++------ src/core/vote.ts | 46 +++++------------------ src/services/anonymous.ts | 7 +++- test/integration/election.test.ts | 2 + test/integration/zk.test.ts | 62 +++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 50 deletions(-) diff --git a/src/client.ts b/src/client.ts index cdff80ff..31461130 100644 --- a/src/client.ts +++ b/src/client.ts @@ -275,6 +275,7 @@ export class VocdoniSDKClient { * @param election * @param wallet * @param signature + * @param votePackage * @param password * @returns {Promise} */ @@ -282,6 +283,7 @@ export class VocdoniSDKClient { election: PublishedElection, wallet: Wallet | Signer, signature: string, + votePackage: Buffer, password: string = '0' ): Promise { const [address, censusProof] = await Promise.all([ @@ -304,7 +306,8 @@ export class VocdoniSDKClient { zkProof.censusRoot, zkProof.censusSiblings, censusProof.root, - censusProof.siblings + censusProof.siblings, + votePackage ) ) .then((circuits) => this.anonymousService.generateZkProof(circuits)); @@ -835,6 +838,14 @@ export class VocdoniSDKClient { electionId: election.id, }; + let processKeys = null; + if (election?.electionType.secretUntilTheEnd) { + processKeys = await this.electionService.keys(election.id).then((encryptionKeys) => ({ + encryptionPubKeys: encryptionKeys.publicKeys, + })); + } + const { votePackage } = VoteCore.packageVoteContent(vote.votes, processKeys); + let censusProof: CspCensusProof | CensusProof | ZkProof; if (election.census.type == CensusType.WEIGHTED) { censusProof = await this.fetchProofForWallet(election.census.censusId, this.wallet); @@ -853,9 +864,9 @@ export class VocdoniSDKClient { signature, }; if (vote instanceof AnonymousVote) { - censusProof = await this.calcZKProofForWallet(election, this.wallet, signature, vote.password); + censusProof = await this.calcZKProofForWallet(election, this.wallet, signature, votePackage, vote.password); } else { - censusProof = await this.calcZKProofForWallet(election, this.wallet, signature); + censusProof = await this.calcZKProofForWallet(election, this.wallet, signature, votePackage); } yield { key: VoteSteps.CALC_ZK_PROOF, @@ -874,15 +885,8 @@ export class VocdoniSDKClient { } let voteTx: { tx: Uint8Array; message: string }; - if (election?.electionType.secretUntilTheEnd) { - voteTx = await this.electionService.keys(election.id).then((encryptionKeys) => - VoteCore.generateVoteTransaction(election, censusProof, vote, { - encryptionPubKeys: encryptionKeys.publicKeys, - }) - ); - } else { - voteTx = VoteCore.generateVoteTransaction(election, censusProof, vote); - } + + voteTx = VoteCore.generateVoteTransaction(election, censusProof, vote, processKeys, votePackage); yield { key: VoteSteps.GENERATE_TX, }; diff --git a/src/core/vote.ts b/src/core/vote.ts index 53436a73..890ef92e 100644 --- a/src/core/vote.ts +++ b/src/core/vote.ts @@ -42,14 +42,15 @@ export abstract class VoteCore extends TransactionCore { public static generateVoteTransaction( election: PublishedElection, censusProof: CensusProof | CspCensusProof | ZkProof, - votePackage: Vote, - processKeys?: ProcessKeys + vote: Vote, + processKeys?: ProcessKeys, + votePackage?: Buffer ): { tx: Uint8Array; message: string } { const message = TxMessage.VOTE.replace('{processId}', strip0x(election.id)); - const txData = this.prepareVoteData(election, censusProof, votePackage, processKeys); - const vote = VoteEnvelope.fromPartial(txData); + const txData = this.prepareVoteData(election, censusProof, vote, processKeys, votePackage); + const voteEnvelope = VoteEnvelope.fromPartial(txData); const tx = Tx.encode({ - payload: { $case: 'vote', vote }, + payload: { $case: 'vote', vote: voteEnvelope }, }).finish(); return { tx, message }; @@ -59,18 +60,9 @@ export abstract class VoteCore extends TransactionCore { election: PublishedElection, censusProof: CensusProof | CspCensusProof | ZkProof, vote: Vote, - processKeys?: ProcessKeys + processKeys: ProcessKeys, + generatedVotePackage: Buffer ): object { - // if (!params) throw new Error("Invalid parameters") - // else if (!Array.isArray(params.votes)) throw new Error("Invalid votes array") - // else if (typeof params.processId != "string" || !params.processId.match(/^(0x)?[0-9a-zA-Z]+$/)) throw new Error("Invalid processId") - // else if (params.processKeys) { - // if (!Array.isArray(params.processKeys.encryptionPubKeys) || !params.processKeys.encryptionPubKeys.every( - // item => item && typeof item.idx == "number" && typeof item.key == "string" && item.key.match(/^(0x)?[0-9a-zA-Z]+$/))) { - // throw new Error("Some encryption public keys are not valid") - // } - // } - try { const proof = this.packageSignedProof(election.id, election.census.type, censusProof); // const nonce = hexStringToBuffer(Random.getHex()); @@ -81,7 +73,7 @@ export abstract class VoteCore extends TransactionCore { proof, processId: new Uint8Array(Buffer.from(strip0x(election.id), 'hex')), nonce: new Uint8Array(nonce), - votePackage: new Uint8Array(votePackage), + votePackage: new Uint8Array(generatedVotePackage ?? votePackage), encryptionKeyIndexes: keyIndexes || [], }; } catch (error) { @@ -180,25 +172,7 @@ export abstract class VoteCore extends TransactionCore { return CAbundle.encode(bundle).finish(); } - private static packageVoteContent(votes: VoteValues, processKeys?: ProcessKeys) { - // if (!Array.isArray(votes)) throw new Error('Invalid votes'); - // else if (votes.some(vote => typeof vote !== 'number')) - // throw new Error('Votes needs to be an array of numbers'); - // else if (processKeys) { - // if ( - // !Array.isArray(processKeys.encryptionPubKeys) || - // !processKeys.encryptionPubKeys.every( - // item => - // item && - // typeof item.idx === 'number' && - // typeof item.key === 'string' && - // item.key.match(/^(0x)?[0-9a-zA-Z]+$/) - // ) - // ) { - // throw new Error('Some encryption public keys are not valid'); - // } - // } - + public static packageVoteContent(votes: VoteValues, processKeys?: ProcessKeys) { // produce a 8 byte nonce const nonce = getHex().substring(2, 18); diff --git a/src/services/anonymous.ts b/src/services/anonymous.ts index c232ccec..8ea509cf 100644 --- a/src/services/anonymous.ts +++ b/src/services/anonymous.ts @@ -207,11 +207,14 @@ export class AnonymousService extends Service implements AnonymousServicePropert sikRoot: string, sikSiblings: string[], censusRoot: string, - censusSiblings: string[] + censusSiblings: string[], + votePackage: Buffer ): Promise { return Promise.all([ AnonymousService.calcCircuitInputs(signature, password, electionId), - AnonymousService.arbo_utils.toHash(AnonymousService.hex_utils.fromBigInt(BigInt(ensure0x(availableWeight)))), + AnonymousService.arbo_utils.toHash( + AnonymousService.hex_utils.fromBigInt(BigInt(ensure0x(votePackage.toString('hex')))) + ), ]).then(([circuitInputs, voteHash]) => ({ electionId: circuitInputs.arboElectionId, nullifier: circuitInputs.nullifier.toString(), diff --git a/test/integration/election.test.ts b/test/integration/election.test.ts index 59b517ce..a8d8dd89 100644 --- a/test/integration/election.test.ts +++ b/test/integration/election.test.ts @@ -3,6 +3,7 @@ import { ApprovalElection, BudgetElection, CensusType, + delay, Election, ElectionCreationSteps, ElectionResultsTypeNames, @@ -359,6 +360,7 @@ describe('Election integration tests', () => { electionIdentifier = electionId; return waitForElectionReady(client, electionId); }) + .then(() => delay(15000)) .then(() => Promise.all( participants diff --git a/test/integration/zk.test.ts b/test/integration/zk.test.ts index c6457b69..e18aff10 100644 --- a/test/integration/zk.test.ts +++ b/test/integration/zk.test.ts @@ -3,6 +3,7 @@ import { clientParams, setFaucetURL } from './util/client.params'; import { AnonymousService, AnonymousVote, + delay, Election, ElectionStatus, PlainCensus, @@ -114,6 +115,67 @@ describe('zkSNARK test', () => { expect(election.census.weight).toEqual(BigInt(3)); }); }, 285000); + it('should create an encrypted anonymous election and vote successfully', async () => { + const census = new PlainCensus(); + const voter1 = Wallet.createRandom(); + const voter2 = Wallet.createRandom(); + // User that votes with account with SIK + census.add((client.wallet as Wallet).address); + // User that votes and has no account + census.add(voter1.address); + // User that votes with account without SIK + census.add(voter2.address); + + const election = createElection(census, { + anonymous: true, + secretUntilTheEnd: true, + }); + + await client.createAccount({ + sik: true, + password: 'password123', + }); + + await client + .createElection(election) + .then((electionId) => { + expect(electionId).toMatch(/^[0-9a-fA-F]{64}$/); + client.setElectionId(electionId); + return client.fetchElection(); + }) + .then((publishedElection) => { + expect(publishedElection.electionType.anonymous).toBeTruthy(); + expect(election.electionType.secretUntilTheEnd).toBeTruthy(); + return waitForElectionReady(client, publishedElection.id); + }) + .then(async () => { + await delay(15000); // wait for process keys to be ready + await expect(async () => { + await client.submitVote(new Vote([0])); + }).rejects.toThrow(); + const vote = new AnonymousVote([0], null, 'password123'); + return client.submitVote(vote); + }) + .then(() => { + client.wallet = voter1; + const vote = new AnonymousVote([0], null, 'password456'); + return client.submitVote(vote); + }) + .then(() => { + client.wallet = voter2; + return client.createAccount({ sik: false }); + }) + .then(() => { + const vote = new Vote([1]); + return client.submitVote(vote); + }) + .then(() => client.fetchElection()) + .then((election) => { + expect(election.voteCount).toEqual(3); + expect(election.census.size).toEqual(3); + expect(election.census.weight).toEqual(BigInt(3)); + }); + }, 285000); it('should create an anonymous election, vote and check if the user has voted successfully', async () => { const census = new PlainCensus(); census.add((client.wallet as Wallet).address);