From ea632a97862c4304c764f511ab480b3664224b5f Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Fri, 9 Feb 2024 23:28:57 +0000 Subject: [PATCH] refactor(nonqv): optimize tally votes non qv circuit and contracts reduce the number of constraints for the tally votes non qv circuit, and amend the repo code to support the changes. This includes a new Tally contract with reduced logic which has been removed from the circuit. --- circuits/circom/tallyVotesNonQv.circom | 106 +++++-- circuits/ts/__tests__/TallyVotes.test.ts | 9 +- cli/tests/e2e/e2e.nonQv.test.ts | 5 +- cli/tests/e2e/keyChange.test.ts | 6 +- cli/ts/commands/deploy.ts | 2 + cli/ts/commands/genProofs.ts | 48 +-- cli/ts/commands/verify.ts | 184 +++++++----- cli/ts/index.ts | 2 + cli/ts/utils/interfaces.ts | 12 +- cli/ts/utils/verifiers.ts | 48 +-- contracts/contracts/TallyNonQv.sol | 273 ++++++++++++++++++ contracts/contracts/TallyNonQvFactory.sol | 23 ++ contracts/deploy-config-example.json | 3 +- contracts/package.json | 1 + .../tasks/deploy/maci/08-tallyFactory.ts | 4 +- contracts/tasks/deploy/poll/01-poll.ts | 6 +- contracts/tasks/helpers/Deployment.ts | 2 +- contracts/tasks/helpers/ProofGenerator.ts | 50 ++-- contracts/tasks/helpers/Prover.ts | 3 +- contracts/tasks/helpers/types.ts | 106 ++++++- contracts/tasks/runner/deployFull.ts | 3 +- contracts/tasks/runner/deployPoll.ts | 3 +- contracts/tasks/runner/prove.ts | 16 +- contracts/tests/EASGatekeeper.test.ts | 9 +- contracts/tests/SignUpGatekeeper.test.ts | 1 + contracts/tests/Tally.test.ts | 6 +- contracts/tests/TallyNonQv.test.ts | 213 ++++++++++++++ contracts/tests/utils.ts | 4 +- contracts/ts/deploy.ts | 36 ++- contracts/ts/types.ts | 5 + core/ts/Poll.ts | 152 +++++++++- core/ts/__tests__/Poll.test.ts | 6 +- core/ts/__tests__/e2e.test.ts | 5 +- core/ts/utils/types.ts | 6 +- integrationTests/ts/__tests__/utils/utils.ts | 2 +- 35 files changed, 1159 insertions(+), 201 deletions(-) create mode 100644 contracts/contracts/TallyNonQv.sol create mode 100644 contracts/contracts/TallyNonQvFactory.sol create mode 100644 contracts/tests/TallyNonQv.test.ts diff --git a/circuits/circom/tallyVotesNonQv.circom b/circuits/circom/tallyVotesNonQv.circom index 58a4fff882..695a6aa9eb 100644 --- a/circuits/circom/tallyVotesNonQv.circom +++ b/circuits/circom/tallyVotesNonQv.circom @@ -61,11 +61,7 @@ template TallyVotesNonQv( signal input currentSpentVoiceCreditSubtotal; signal input currentSpentVoiceCreditSubtotalSalt; - signal input currentPerVOSpentVoiceCredits[numVoteOptions]; - signal input currentPerVOSpentVoiceCreditsRootSalt; - signal input newResultsRootSalt; - signal input newPerVOSpentVoiceCreditsRootSalt; signal input newSpentVoiceCreditSubtotalSalt; // ----------------------------------------------------------------------- @@ -163,18 +159,8 @@ template TallyVotesNonQv( } } - // Tally the spent voice credits per vote option - component newPerVOSpentVoiceCredits[numVoteOptions]; - for (var i = 0; i < numVoteOptions; i++) { - newPerVOSpentVoiceCredits[i] = CalculateTotal(batchSize + 1); - newPerVOSpentVoiceCredits[i].nums[batchSize] <== currentPerVOSpentVoiceCredits[i] * iz.out; - for (var j = 0; j < batchSize; j++) { - newPerVOSpentVoiceCredits[i].nums[j] <== votes[j][i]; - } - } - // Verify the current and new tally - component rcv = ResultCommitmentVerifier(voteOptionTreeDepth); + component rcv = ResultCommitmentNonQvVerifier(voteOptionTreeDepth); rcv.isFirstBatch <== isFirstBatch.out; rcv.currentTallyCommitment <== currentTallyCommitment; rcv.newTallyCommitment <== newTallyCommitment; @@ -184,13 +170,95 @@ template TallyVotesNonQv( rcv.currentSpentVoiceCreditSubtotalSalt <== currentSpentVoiceCreditSubtotalSalt; rcv.newSpentVoiceCreditSubtotal <== newSpentVoiceCreditSubtotal.sum; rcv.newSpentVoiceCreditSubtotalSalt <== newSpentVoiceCreditSubtotalSalt; - rcv.currentPerVOSpentVoiceCreditsRootSalt <== currentPerVOSpentVoiceCreditsRootSalt; - rcv.newPerVOSpentVoiceCreditsRootSalt <== newPerVOSpentVoiceCreditsRootSalt; for (var i = 0; i < numVoteOptions; i++) { rcv.currentResults[i] <== currentResults[i]; rcv.newResults[i] <== resultCalc[i].sum; - rcv.currentPerVOSpentVoiceCredits[i] <== currentPerVOSpentVoiceCredits[i]; - rcv.newPerVOSpentVoiceCredits[i] <== newPerVOSpentVoiceCredits[i].sum; } } + +// Verifies the commitment to the current results. Also computes and outputs a +// commitment to the new results. Works for non quadratic voting +// - so no need for perVOSpentCredits as they would just match +// the results. +template ResultCommitmentNonQvVerifier(voteOptionTreeDepth) { + var TREE_ARITY = 5; + var numVoteOptions = TREE_ARITY ** voteOptionTreeDepth; + + // 1 if this is the first batch, and 0 otherwise + signal input isFirstBatch; + signal input currentTallyCommitment; + signal input newTallyCommitment; + + // Results + signal input currentResults[numVoteOptions]; + signal input currentResultsRootSalt; + + signal input newResults[numVoteOptions]; + signal input newResultsRootSalt; + + // Spent voice credits + signal input currentSpentVoiceCreditSubtotal; + signal input currentSpentVoiceCreditSubtotalSalt; + + signal input newSpentVoiceCreditSubtotal; + signal input newSpentVoiceCreditSubtotalSalt; + + // Compute the commitment to the current results + component currentResultsRoot = QuinCheckRoot(voteOptionTreeDepth); + for (var i = 0; i < numVoteOptions; i++) { + currentResultsRoot.leaves[i] <== currentResults[i]; + } + + component currentResultsCommitment = HashLeftRight(); + currentResultsCommitment.left <== currentResultsRoot.root; + currentResultsCommitment.right <== currentResultsRootSalt; + + // Compute the commitment to the current spent voice credits + component currentSpentVoiceCreditsCommitment = HashLeftRight(); + currentSpentVoiceCreditsCommitment.left <== currentSpentVoiceCreditSubtotal; + currentSpentVoiceCreditsCommitment.right <== currentSpentVoiceCreditSubtotalSalt; + + // Commit to the current tally + component currentTallyCommitmentHasher = HashLeftRight(); + currentTallyCommitmentHasher.left <== currentResultsCommitment.hash; + currentTallyCommitmentHasher.right <== currentSpentVoiceCreditsCommitment.hash; + + // Check if the current tally commitment is correct only if this is not the first batch + component iz = IsZero(); + iz.in <== isFirstBatch; + // iz.out is 1 if this is not the first batch + // iz.out is 0 if this is the first batch + + // hz is 0 if this is the first batch + // currentTallyCommitment should be 0 if this is the first batch + + // hz is 1 if this is not the first batch + // currentTallyCommitment should not be 0 if this is the first batch + signal hz; + hz <== iz.out * currentTallyCommitmentHasher.hash; + + hz === currentTallyCommitment; + + // Compute the root of the new results + component newResultsRoot = QuinCheckRoot(voteOptionTreeDepth); + for (var i = 0; i < numVoteOptions; i++) { + newResultsRoot.leaves[i] <== newResults[i]; + } + + component newResultsCommitment = HashLeftRight(); + newResultsCommitment.left <== newResultsRoot.root; + newResultsCommitment.right <== newResultsRootSalt; + + // Compute the commitment to the new spent voice credits value + component newSpentVoiceCreditsCommitment = HashLeftRight(); + newSpentVoiceCreditsCommitment.left <== newSpentVoiceCreditSubtotal; + newSpentVoiceCreditsCommitment.right <== newSpentVoiceCreditSubtotalSalt; + + // Commit to the new tally + component newTallyCommitmentHasher = HashLeftRight(); + newTallyCommitmentHasher.left <== newResultsCommitment.hash; + newTallyCommitmentHasher.right <== newSpentVoiceCreditsCommitment.hash; + + newTallyCommitmentHasher.hash === newTallyCommitment; +} diff --git a/circuits/ts/__tests__/TallyVotes.test.ts b/circuits/ts/__tests__/TallyVotes.test.ts index a4608bcbd9..d141691ef3 100644 --- a/circuits/ts/__tests__/TallyVotes.test.ts +++ b/circuits/ts/__tests__/TallyVotes.test.ts @@ -214,13 +214,14 @@ describe("TallyVotes circuit", function test() { }); it("should produce the correct result commitments", async () => { - const generatedInputs = poll.tallyVotes() as unknown as ITallyVotesInputs; - const witness = await circuit.calculateWitness(generatedInputs); - await circuit.expectConstraintPass(witness); + const generatedInputs = poll.tallyVotesNonQv() as unknown as ITallyVotesInputs; + + const witness = await circuitNonQv.calculateWitness(generatedInputs); + await circuitNonQv.expectConstraintPass(witness); }); it("should produce the correct result if the inital tally is not zero", async () => { - const generatedInputs = poll.tallyVotes(false) as unknown as ITallyVotesInputs; + const generatedInputs = poll.tallyVotesNonQv() as unknown as ITallyVotesInputs; // Start the tally from non-zero value let randIdx = generateRandomIndex(Object.keys(generatedInputs).length); diff --git a/cli/tests/e2e/e2e.nonQv.test.ts b/cli/tests/e2e/e2e.nonQv.test.ts index 7949b7a0e4..04a34a83fd 100644 --- a/cli/tests/e2e/e2e.nonQv.test.ts +++ b/cli/tests/e2e/e2e.nonQv.test.ts @@ -47,7 +47,7 @@ import { cleanVanilla, isArm } from "../utils"; Test scenarios: 1 signup, 1 message with quadratic voting disabled */ -describe("e2e tests", function test() { +describe("e2e tests with non quadratic voting", function test() { const useWasm = isArm(); this.timeout(900000); @@ -69,6 +69,7 @@ describe("e2e tests", function test() { processWasm: testProcessMessagesNonQvWasmPath, tallyWasm: testTallyVotesNonQvWasmPath, useWasm, + useQuadraticVoting: false, }; // before all tests we deploy the vk registry contract and set the verifying keys @@ -90,7 +91,7 @@ describe("e2e tests", function test() { before(async () => { // deploy the smart contracts - maciAddresses = await deploy({ ...deployArgs, signer }); + maciAddresses = await deploy({ ...deployArgs, signer, useQv: false }); // deploy a poll contract await deployPoll({ ...deployPollArgs, signer }); }); diff --git a/cli/tests/e2e/keyChange.test.ts b/cli/tests/e2e/keyChange.test.ts index bdc428cd49..420cf06e11 100644 --- a/cli/tests/e2e/keyChange.test.ts +++ b/cli/tests/e2e/keyChange.test.ts @@ -147,7 +147,7 @@ describe("keyChange tests", function test() { it("should confirm the tally is correct", () => { const tallyData = JSON.parse(fs.readFileSync(testTallyFilePath).toString()) as TallyData; expect(tallyData.results.tally[0]).to.equal(expectedTally.toString()); - expect(tallyData.perVOSpentVoiceCredits.tally[0]).to.equal(expectedPerVoteOptionTally.toString()); + expect(tallyData.perVOSpentVoiceCredits?.tally[0]).to.equal(expectedPerVoteOptionTally.toString()); }); }); @@ -215,7 +215,7 @@ describe("keyChange tests", function test() { it("should confirm the tally is correct", () => { const tallyData = JSON.parse(fs.readFileSync(testTallyFilePath).toString()) as TallyData; expect(tallyData.results.tally[0]).to.equal(expectedTally.toString()); - expect(tallyData.perVOSpentVoiceCredits.tally[0]).to.equal(expectedPerVoteOptionTally.toString()); + expect(tallyData.perVOSpentVoiceCredits?.tally[0]).to.equal(expectedPerVoteOptionTally.toString()); }); }); @@ -283,7 +283,7 @@ describe("keyChange tests", function test() { it("should confirm the tally is correct", () => { const tallyData = JSON.parse(fs.readFileSync(testTallyFilePath).toString()) as TallyData; expect(tallyData.results.tally[2]).to.equal(expectedTally.toString()); - expect(tallyData.perVOSpentVoiceCredits.tally[2]).to.equal(expectedPerVoteOptionTally.toString()); + expect(tallyData.perVOSpentVoiceCredits?.tally[2]).to.equal(expectedPerVoteOptionTally.toString()); }); }); }); diff --git a/cli/ts/commands/deploy.ts b/cli/ts/commands/deploy.ts index b4f7bdffea..5249fbada4 100644 --- a/cli/ts/commands/deploy.ts +++ b/cli/ts/commands/deploy.ts @@ -32,6 +32,7 @@ export const deploy = async ({ poseidonT4Address, poseidonT5Address, poseidonT6Address, + useQv = true, signer, quiet = true, }: DeployArgs): Promise => { @@ -95,6 +96,7 @@ export const deploy = async ({ signer, stateTreeDepth, quiet: true, + useQv, }); const [maciContractAddress, stateAqContractAddress, pollFactoryContractAddress] = await Promise.all([ diff --git a/cli/ts/commands/genProofs.ts b/cli/ts/commands/genProofs.ts index 568dd7e733..d92ea3f982 100644 --- a/cli/ts/commands/genProofs.ts +++ b/cli/ts/commands/genProofs.ts @@ -412,7 +412,9 @@ export const genProofs = async ({ // tally all ballots for this poll while (poll.hasUntalliedBallots()) { // tally votes in batches - tallyCircuitInputs = poll.tallyVotes(useQuadraticVoting) as unknown as CircuitInputs; + tallyCircuitInputs = useQuadraticVoting + ? (poll.tallyVotes() as unknown as CircuitInputs) + : (poll.tallyVotesNonQv() as unknown as CircuitInputs); try { // generate the proof @@ -467,29 +469,19 @@ export const genProofs = async ({ BigInt(asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish)), ); - // Compute newPerVOSpentVoiceCreditsCommitment - const newPerVOSpentVoiceCreditsCommitment = genTreeCommitment( - poll.perVOSpentVoiceCredits, - BigInt(asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish)), - poll.treeDepths.voteOptionTreeDepth, - ); - - // Compute newTallyCommitment - const newTallyCommitment = hash3([ - newResultsCommitment, - newSpentVoiceCreditsCommitment, - newPerVOSpentVoiceCreditsCommitment, - ]); - // get the tally contract address const tallyContractAddress = tallyAddress || readContractAddress(`Tally-${pollId}`, network?.name); + let newPerVOSpentVoiceCreditsCommitment: bigint | undefined; + let newTallyCommitment: bigint; + // create the tally file data to store for verification later const tallyFileData: TallyData = { maci: maciContractAddress, pollId: pollId.toString(), network: network?.name, chainId: network?.chainId.toString(), + isQuadratic: useQuadraticVoting, tallyAddress: tallyContractAddress, newTallyCommitment: asHex(tallyCircuitInputs!.newTallyCommitment as BigNumberish), results: { @@ -502,12 +494,32 @@ export const genProofs = async ({ salt: asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish), commitment: asHex(newSpentVoiceCreditsCommitment), }, - perVOSpentVoiceCredits: { + }; + + if (useQuadraticVoting) { + // Compute newPerVOSpentVoiceCreditsCommitment + newPerVOSpentVoiceCreditsCommitment = genTreeCommitment( + poll.perVOSpentVoiceCredits, + BigInt(asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish)), + poll.treeDepths.voteOptionTreeDepth, + ); + + // Compute newTallyCommitment + newTallyCommitment = hash3([ + newResultsCommitment, + newSpentVoiceCreditsCommitment, + newPerVOSpentVoiceCreditsCommitment, + ]); + + // update perVOSpentVoiceCredits in the tally file data + tallyFileData.perVOSpentVoiceCredits = { tally: poll.perVOSpentVoiceCredits.map((x) => x.toString()), salt: asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish), commitment: asHex(newPerVOSpentVoiceCreditsCommitment), - }, - }; + }; + } else { + newTallyCommitment = hashLeftRight(newResultsCommitment, newSpentVoiceCreditsCommitment); + } fs.writeFileSync(tallyFile, JSON.stringify(tallyFileData, null, 4)); diff --git a/cli/ts/commands/verify.ts b/cli/ts/commands/verify.ts index b84820a07d..8251074756 100644 --- a/cli/ts/commands/verify.ts +++ b/cli/ts/commands/verify.ts @@ -1,10 +1,13 @@ import { Tally__factory as TallyFactory, + TallyNonQv__factory as TallyNonQvFactory, MACI__factory as MACIFactory, Subsidy__factory as SubsidyFactory, Poll__factory as PollFactory, + Tally, + TallyNonQv, } from "maci-contracts/typechain-types"; -import { hash2, hash3, genTreeCommitment } from "maci-crypto"; +import { hash2, hash3, genTreeCommitment, hashLeftRight } from "maci-crypto"; import type { VerifyArgs } from "../utils/interfaces"; @@ -31,6 +34,7 @@ export const verify = async ({ banner(quiet); const tallyResults = tallyData; + const useQv = tallyResults.isQuadratic; // we prioritize the tally file data const tallyContractAddress = tallyResults.tallyAddress || tallyAddress; @@ -75,9 +79,11 @@ export const verify = async ({ const pollAddr = await maciContract.polls(pollId); const pollContract = PollFactory.connect(pollAddr, signer); - const tallyContract = TallyFactory.connect(tallyContractAddress, signer); const subsidyContract = subsidyEnabled ? SubsidyFactory.connect(subsidyContractAddress, signer) : undefined; + const tallyContract = useQv + ? TallyFactory.connect(tallyContractAddress, signer) + : TallyNonQvFactory.connect(tallyContractAddress, signer); // verification const onChainTallyCommitment = BigInt(await tallyContract.tallyCommitment()); @@ -99,13 +105,6 @@ export const verify = async ({ logError("Wrong number of vote options."); } - if (tallyResults.perVOSpentVoiceCredits.tally.length !== numVoteOptions) { - logError("Wrong number of vote options."); - } - - // verify that the results commitment matches the output of genTreeCommitment() - - // verify the results // compute newResultsCommitment const newResultsCommitment = genTreeCommitment( tallyResults.results.tally.map((x) => BigInt(x)), @@ -119,75 +118,124 @@ export const verify = async ({ BigInt(tallyResults.totalSpentVoiceCredits.salt), ]); - // compute newPerVOSpentVoiceCreditsCommitment - const newPerVOSpentVoiceCreditsCommitment = genTreeCommitment( - tallyResults.perVOSpentVoiceCredits.tally.map((x) => BigInt(x)), - BigInt(tallyResults.perVOSpentVoiceCredits.salt), - voteOptionTreeDepth, - ); + if (useQv) { + if (tallyResults.perVOSpentVoiceCredits?.tally.length !== numVoteOptions) { + logError("Wrong number of vote options."); + } - // compute newTallyCommitment - const newTallyCommitment = hash3([ - newResultsCommitment, - newSpentVoiceCreditsCommitment, - newPerVOSpentVoiceCreditsCommitment, - ]); + // verify that the results commitment matches the output of genTreeCommitment() - if (onChainTallyCommitment !== newTallyCommitment) { - logError("The on-chain tally commitment does not match."); - } - logGreen(quiet, success("The on-chain tally commitment matches.")); - - // verify total spent voice credits on-chain - const isValid = await tallyContract.verifySpentVoiceCredits( - tallyResults.totalSpentVoiceCredits.spent, - tallyResults.totalSpentVoiceCredits.salt, - newResultsCommitment, - newPerVOSpentVoiceCreditsCommitment, - ); + // compute newPerVOSpentVoiceCreditsCommitment + const newPerVOSpentVoiceCreditsCommitment = genTreeCommitment( + tallyResults.perVOSpentVoiceCredits!.tally.map((x) => BigInt(x)), + BigInt(tallyResults.perVOSpentVoiceCredits!.salt), + voteOptionTreeDepth, + ); - if (isValid) { - logGreen(quiet, success("The on-chain verification of total spent voice credits passed.")); - } else { - logError("The on-chain verification of total spent voice credits failed."); - } + // compute newTallyCommitment + const newTallyCommitment = hash3([ + newResultsCommitment, + newSpentVoiceCreditsCommitment, + newPerVOSpentVoiceCreditsCommitment, + ]); - // verify per vote option voice credits on-chain - const failedSpentCredits = await verifyPerVOSpentVoiceCredits( - tallyContract, - tallyResults, - voteOptionTreeDepth, - newSpentVoiceCreditsCommitment, - newResultsCommitment, - ); + if (onChainTallyCommitment !== newTallyCommitment) { + logError("The on-chain tally commitment does not match."); + } + logGreen(quiet, success("The on-chain tally commitment matches.")); + + // verify total spent voice credits on-chain + const isValid = await (tallyContract as Tally).verifySpentVoiceCredits( + tallyResults.totalSpentVoiceCredits.spent, + tallyResults.totalSpentVoiceCredits.salt, + newResultsCommitment, + newPerVOSpentVoiceCreditsCommitment, + ); - if (failedSpentCredits.length === 0) { - logGreen(quiet, success("The on-chain verification of per vote option spent voice credits passed")); - } else { - logError( - `At least one tally result failed the on-chain verification. Please check your Tally data at these indexes: ${failedSpentCredits.join( - ", ", - )}`, + if (isValid) { + logGreen(quiet, success("The on-chain verification of total spent voice credits passed.")); + } else { + logError("The on-chain verification of total spent voice credits failed."); + } + + // verify per vote option voice credits on-chain + const failedSpentCredits = await verifyPerVOSpentVoiceCredits( + tallyContract as Tally, + tallyResults, + voteOptionTreeDepth, + newSpentVoiceCreditsCommitment, + newResultsCommitment, ); - } - // verify tally result on-chain for each vote option - const failedPerVOSpentCredits = await verifyTallyResults( - tallyContract, - tallyResults, - voteOptionTreeDepth, - newSpentVoiceCreditsCommitment, - newPerVOSpentVoiceCreditsCommitment, - ); + if (failedSpentCredits.length === 0) { + logGreen(quiet, success("The on-chain verification of per vote option spent voice credits passed")); + } else { + logError( + `At least one tally result failed the on-chain verification. Please check your Tally data at these indexes: ${failedSpentCredits.join( + ", ", + )}`, + ); + } - if (failedPerVOSpentCredits.length === 0) { - logGreen(quiet, success("The on-chain verification of tally results passed")); + // verify tally result on-chain for each vote option + const failedPerVOSpentCredits = await verifyTallyResults( + tallyContract, + tallyResults, + voteOptionTreeDepth, + newSpentVoiceCreditsCommitment, + newPerVOSpentVoiceCreditsCommitment, + ); + + if (failedPerVOSpentCredits.length === 0) { + logGreen(quiet, success("The on-chain verification of tally results passed")); + } else { + logError( + `At least one spent voice credits entry in the tally results failed the on-chain verification. Please check your tally results at these indexes: ${failedPerVOSpentCredits.join( + ", ", + )}`, + ); + } } else { - logError( - `At least one spent voice credits entry in the tally results failed the on-chain verification. Please check your tally results at these indexes: ${failedPerVOSpentCredits.join( - ", ", - )}`, + // verify that the results commitment matches the output of genTreeCommitment() + + // compute newTallyCommitment + const newTallyCommitment = hashLeftRight(newResultsCommitment, newSpentVoiceCreditsCommitment); + + if (onChainTallyCommitment !== newTallyCommitment) { + logError("The on-chain tally commitment does not match."); + } + logGreen(quiet, success("The on-chain tally commitment matches.")); + + // verify total spent voice credits on-chain + const isValid = await (tallyContract as TallyNonQv).verifySpentVoiceCredits( + tallyResults.totalSpentVoiceCredits.spent, + tallyResults.totalSpentVoiceCredits.salt, + newResultsCommitment, + ); + + if (isValid) { + logGreen(quiet, success("The on-chain verification of total spent voice credits passed.")); + } else { + logError("The on-chain verification of total spent voice credits failed."); + } + + // verify tally result on-chain for each vote option + const failedResult = await verifyTallyResults( + tallyContract, + tallyResults, + voteOptionTreeDepth, + newSpentVoiceCreditsCommitment, ); + + if (failedResult.length === 0) { + logGreen(quiet, success("The on-chain verification of tally results passed")); + } else { + logError( + `At least one result entry in the tally results failed the on-chain verification. Please check your tally results at these indexes: ${failedResult.join( + ", ", + )}`, + ); + } } // verify subsidy result if subsidy file is provided diff --git a/cli/ts/index.ts b/cli/ts/index.ts index f6cd35b143..e71d738c80 100644 --- a/cli/ts/index.ts +++ b/cli/ts/index.ts @@ -57,6 +57,7 @@ program .option("-g, --signupGatekeeperAddress ", "the signup gatekeeper contract address") .option("-q, --quiet ", "whether to print values to the console", (value) => value === "true", false) .option("-r, --rpc-provider ", "the rpc provider URL") + .option("-uq, --use-quadratic-voting", "whether to use quadratic voting", (value) => value === "true", true) .requiredOption("-s, --stateTreeDepth ", "the state tree depth", parseInt) .action(async (cmdOptions) => { try { @@ -71,6 +72,7 @@ program poseidonT4Address: cmdOptions.poseidonT4Address, poseidonT5Address: cmdOptions.poseidonT5Address, poseidonT6Address: cmdOptions.poseidonT6Address, + useQv: cmdOptions.useQuadraticVoting, quiet: cmdOptions.quiet, signer, }); diff --git a/cli/ts/utils/interfaces.ts b/cli/ts/utils/interfaces.ts index 03e9954715..bcc214deef 100644 --- a/cli/ts/utils/interfaces.ts +++ b/cli/ts/utils/interfaces.ts @@ -50,6 +50,11 @@ export interface TallyData { */ chainId?: string; + /** + * Whether the poll is using quadratic voting or not. + */ + isQuadratic: boolean; + /** * The address of the Tally contract. */ @@ -103,7 +108,7 @@ export interface TallyData { /** * The per VO spent voice credits. */ - perVOSpentVoiceCredits: { + perVOSpentVoiceCredits?: { /** * The tally of the per VO spent voice credits. */ @@ -307,6 +312,11 @@ export interface DeployArgs { * Whether to log the output */ quiet?: boolean; + + /** + * Whether to use quadratic voting or not + */ + useQv?: boolean; } /** diff --git a/cli/ts/utils/verifiers.ts b/cli/ts/utils/verifiers.ts index 906b635028..bd865783de 100644 --- a/cli/ts/utils/verifiers.ts +++ b/cli/ts/utils/verifiers.ts @@ -1,7 +1,7 @@ import { genTreeProof } from "maci-crypto"; import type { TallyData } from "./interfaces"; -import type { Tally } from "maci-contracts"; +import type { Tally, TallyNonQv } from "maci-contracts"; /** * Loop through each per vote option spent voice credits and verify it on-chain @@ -22,19 +22,19 @@ export const verifyPerVOSpentVoiceCredits = async ( ): Promise => { const failedIndices: number[] = []; - for (let i = 0; i < tallyData.perVOSpentVoiceCredits.tally.length; i += 1) { + for (let i = 0; i < tallyData.perVOSpentVoiceCredits!.tally.length; i += 1) { const proof = genTreeProof( i, - tallyData.perVOSpentVoiceCredits.tally.map((x) => BigInt(x)), + tallyData.perVOSpentVoiceCredits!.tally.map((x) => BigInt(x)), voteOptionTreeDepth, ); // eslint-disable-next-line no-await-in-loop const isValid = await tallyContract.verifyPerVOSpentVoiceCredits( i, - tallyData.perVOSpentVoiceCredits.tally[i], + tallyData.perVOSpentVoiceCredits!.tally[i], proof, - tallyData.perVOSpentVoiceCredits.salt, + tallyData.perVOSpentVoiceCredits!.salt, voteOptionTreeDepth, newSpentVoiceCreditsCommitment, newResultsCommitment, @@ -57,11 +57,11 @@ export const verifyPerVOSpentVoiceCredits = async ( * @returns list of the indexes of the tally result that failed on-chain verification */ export const verifyTallyResults = async ( - tallyContract: Tally, + tallyContract: Tally | TallyNonQv, tallyData: TallyData, voteOptionTreeDepth: number, newSpentVoiceCreditsCommitment: bigint, - newPerVOSpentVoiceCreditsCommitment: bigint, + newPerVOSpentVoiceCreditsCommitment?: bigint, ): Promise => { const failedIndices: number[] = []; @@ -72,16 +72,30 @@ export const verifyTallyResults = async ( voteOptionTreeDepth, ); - // eslint-disable-next-line no-await-in-loop - const isValid = await tallyContract.verifyTallyResult( - i, - tallyData.results.tally[i], - proof, - tallyData.results.salt, - voteOptionTreeDepth, - newSpentVoiceCreditsCommitment, - newPerVOSpentVoiceCreditsCommitment, - ); + let isValid: boolean; + + if (!newPerVOSpentVoiceCreditsCommitment) { + // eslint-disable-next-line no-await-in-loop + isValid = await (tallyContract as TallyNonQv).verifyTallyResult( + i, + tallyData.results.tally[i], + proof, + tallyData.results.salt, + voteOptionTreeDepth, + newSpentVoiceCreditsCommitment, + ); + } else { + // eslint-disable-next-line no-await-in-loop + isValid = await (tallyContract as Tally).verifyTallyResult( + i, + tallyData.results.tally[i], + proof, + tallyData.results.salt, + voteOptionTreeDepth, + newSpentVoiceCreditsCommitment, + newPerVOSpentVoiceCreditsCommitment, + ); + } if (!isValid) { failedIndices.push(i); diff --git a/contracts/contracts/TallyNonQv.sol b/contracts/contracts/TallyNonQv.sol new file mode 100644 index 0000000000..4a6bf1d197 --- /dev/null +++ b/contracts/contracts/TallyNonQv.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import { IMACI } from "./interfaces/IMACI.sol"; +import { Hasher } from "./crypto/Hasher.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IPoll } from "./interfaces/IPoll.sol"; +import { IMessageProcessor } from "./interfaces/IMessageProcessor.sol"; +import { SnarkCommon } from "./crypto/SnarkCommon.sol"; +import { IVerifier } from "./interfaces/IVerifier.sol"; +import { IVkRegistry } from "./interfaces/IVkRegistry.sol"; +import { CommonUtilities } from "./utilities/CommonUtilities.sol"; + +/// @title TallyNonQv +/// @notice The TallyNonQv contract is used during votes tallying +/// and by users to verify the tally results. +contract TallyNonQv is Ownable, SnarkCommon, CommonUtilities, Hasher { + uint256 private constant TREE_ARITY = 5; + + /// @notice The commitment to the tally results. Its initial value is 0, but after + /// the tally of each batch is proven on-chain via a zk-SNARK, it should be + /// updated to: + /// + /// hash2( + /// hashLeftRight(merkle root of current results, salt0) + /// hashLeftRight(number of spent voice credits, salt1), + /// ) + /// + /// Where each salt is unique and the merkle roots are of arrays of leaves + /// TREE_ARITY ** voteOptionTreeDepth long. + uint256 public tallyCommitment; + + uint256 public tallyBatchNum; + + // The final commitment to the state and ballot roots + uint256 public sbCommitment; + + IVerifier public immutable verifier; + IVkRegistry public immutable vkRegistry; + IPoll public immutable poll; + IMessageProcessor public immutable messageProcessor; + + /// @notice custom errors + error ProcessingNotComplete(); + error InvalidTallyVotesProof(); + error AllBallotsTallied(); + error NumSignUpsTooLarge(); + error BatchStartIndexTooLarge(); + error TallyBatchSizeTooLarge(); + + /// @notice Create a new Tally contract + /// @param _verifier The Verifier contract + /// @param _vkRegistry The VkRegistry contract + /// @param _poll The Poll contract + /// @param _mp The MessageProcessor contract + constructor(address _verifier, address _vkRegistry, address _poll, address _mp) payable { + verifier = IVerifier(_verifier); + vkRegistry = IVkRegistry(_vkRegistry); + poll = IPoll(_poll); + messageProcessor = IMessageProcessor(_mp); + } + + /// @notice Pack the batch start index and number of signups into a 100-bit value. + /// @param _numSignUps: number of signups + /// @param _batchStartIndex: the start index of given batch + /// @param _tallyBatchSize: size of batch + /// @return result an uint256 representing the 3 inputs packed together + function genTallyVotesPackedVals( + uint256 _numSignUps, + uint256 _batchStartIndex, + uint256 _tallyBatchSize + ) public pure returns (uint256 result) { + if (_numSignUps >= 2 ** 50) revert NumSignUpsTooLarge(); + if (_batchStartIndex >= 2 ** 50) revert BatchStartIndexTooLarge(); + if (_tallyBatchSize >= 2 ** 50) revert TallyBatchSizeTooLarge(); + + result = (_batchStartIndex / _tallyBatchSize) + (_numSignUps << uint256(50)); + } + + /// @notice Check if all ballots are tallied + /// @return tallied whether all ballots are tallied + function isTallied() external view returns (bool tallied) { + (uint8 intStateTreeDepth, , , ) = poll.treeDepths(); + (uint256 numSignUps, ) = poll.numSignUpsAndMessages(); + + // Require that there are untallied ballots left + tallied = tallyBatchNum * (TREE_ARITY ** intStateTreeDepth) >= numSignUps; + } + + /// @notice generate hash of public inputs for tally circuit + /// @param _numSignUps: number of signups + /// @param _batchStartIndex: the start index of given batch + /// @param _tallyBatchSize: size of batch + /// @param _newTallyCommitment: the new tally commitment to be updated + /// @return inputHash hash of public inputs + function genTallyVotesPublicInputHash( + uint256 _numSignUps, + uint256 _batchStartIndex, + uint256 _tallyBatchSize, + uint256 _newTallyCommitment + ) public view returns (uint256 inputHash) { + uint256 packedVals = genTallyVotesPackedVals(_numSignUps, _batchStartIndex, _tallyBatchSize); + uint256[] memory input = new uint256[](4); + input[0] = packedVals; + input[1] = sbCommitment; + input[2] = tallyCommitment; + input[3] = _newTallyCommitment; + inputHash = sha256Hash(input); + } + + /// @notice Update the state and ballot root commitment + function updateSbCommitment() public onlyOwner { + // Require that all messages have been processed + if (!messageProcessor.processingComplete()) { + revert ProcessingNotComplete(); + } + + if (sbCommitment == 0) { + sbCommitment = messageProcessor.sbCommitment(); + } + } + + /// @notice Verify the result of a tally batch + /// @param _newTallyCommitment the new tally commitment to be verified + /// @param _proof the proof generated after tallying this batch + function tallyVotes(uint256 _newTallyCommitment, uint256[8] calldata _proof) public onlyOwner { + _votingPeriodOver(poll); + updateSbCommitment(); + + // get the batch size and start index + (uint8 intStateTreeDepth, , , ) = poll.treeDepths(); + uint256 tallyBatchSize = TREE_ARITY ** intStateTreeDepth; + uint256 batchStartIndex = tallyBatchNum * tallyBatchSize; + + // save some gas because we won't overflow uint256 + unchecked { + tallyBatchNum++; + } + + (uint256 numSignUps, ) = poll.numSignUpsAndMessages(); + + // Require that there are untallied ballots left + if (batchStartIndex >= numSignUps) { + revert AllBallotsTallied(); + } + + bool isValid = verifyTallyProof(_proof, numSignUps, batchStartIndex, tallyBatchSize, _newTallyCommitment); + + if (!isValid) { + revert InvalidTallyVotesProof(); + } + + // Update the tally commitment and the tally batch num + tallyCommitment = _newTallyCommitment; + } + + /// @notice Verify the tally proof using the verifying key + /// @param _proof the proof generated after processing all messages + /// @param _numSignUps number of signups for a given poll + /// @param _batchStartIndex the number of batches multiplied by the size of the batch + /// @param _tallyBatchSize batch size for the tally + /// @param _newTallyCommitment the tally commitment to be verified at a given batch index + /// @return isValid whether the proof is valid + function verifyTallyProof( + uint256[8] calldata _proof, + uint256 _numSignUps, + uint256 _batchStartIndex, + uint256 _tallyBatchSize, + uint256 _newTallyCommitment + ) public view returns (bool isValid) { + (uint8 intStateTreeDepth, , , uint8 voteOptionTreeDepth) = poll.treeDepths(); + + (IMACI maci, , ) = poll.extContracts(); + + // Get the verifying key + VerifyingKey memory vk = vkRegistry.getTallyVk(maci.stateTreeDepth(), intStateTreeDepth, voteOptionTreeDepth); + + // Get the public inputs + uint256 publicInputHash = genTallyVotesPublicInputHash( + _numSignUps, + _batchStartIndex, + _tallyBatchSize, + _newTallyCommitment + ); + + // Verify the proof + isValid = verifier.verify(_proof, vk, publicInputHash); + } + + /// @notice Compute the merkle root from the path elements + /// and a leaf + /// @param _depth the depth of the merkle tree + /// @param _index the index of the leaf + /// @param _leaf the leaf + /// @param _pathElements the path elements to reconstruct the merkle root + /// @return current The merkle root + function computeMerkleRootFromPath( + uint8 _depth, + uint256 _index, + uint256 _leaf, + uint256[][] calldata _pathElements + ) internal pure returns (uint256 current) { + uint256 pos = _index % TREE_ARITY; + current = _leaf; + uint8 k; + + uint256[TREE_ARITY] memory level; + + for (uint8 i = 0; i < _depth; ++i) { + for (uint8 j = 0; j < TREE_ARITY; ++j) { + if (j == pos) { + level[j] = current; + } else { + if (j > pos) { + k = j - 1; + } else { + k = j; + } + level[j] = _pathElements[i][k]; + } + } + + _index /= TREE_ARITY; + pos = _index % TREE_ARITY; + current = hash5(level); + } + } + + /// @notice Verify the number of spent voice credits from the tally.json + /// @param _totalSpent spent field retrieved in the totalSpentVoiceCredits object + /// @param _totalSpentSalt the corresponding salt in the totalSpentVoiceCredit object + /// @param _resultCommitment hashLeftRight(merkle root of the results.tally, results.salt) in tally.json file + /// @return isValid Whether the provided values are valid + function verifySpentVoiceCredits( + uint256 _totalSpent, + uint256 _totalSpentSalt, + uint256 _resultCommitment + ) public view returns (bool isValid) { + uint256[2] memory tally; + tally[0] = _resultCommitment; + tally[1] = hashLeftRight(_totalSpent, _totalSpentSalt); + + isValid = hash2(tally) == tallyCommitment; + } + + /// @notice Verify the result generated from the tally.json + /// @param _voteOptionIndex the index of the vote option to verify the correctness of the tally + /// @param _tallyResult Flattened array of the tally + /// @param _tallyResultProof Corresponding proof of the tally result + /// @param _tallyResultSalt the respective salt in the results object in the tally.json + /// @param _voteOptionTreeDepth depth of the vote option tree + /// @param _spentVoiceCreditsHash hashLeftRight(number of spent voice credits, spent salt) + /// @return isValid Whether the provided proof is valid + function verifyTallyResult( + uint256 _voteOptionIndex, + uint256 _tallyResult, + uint256[][] calldata _tallyResultProof, + uint256 _tallyResultSalt, + uint8 _voteOptionTreeDepth, + uint256 _spentVoiceCreditsHash + ) public view returns (bool isValid) { + uint256 computedRoot = computeMerkleRootFromPath( + _voteOptionTreeDepth, + _voteOptionIndex, + _tallyResult, + _tallyResultProof + ); + + uint256[2] memory tally = [hashLeftRight(computedRoot, _tallyResultSalt), _spentVoiceCreditsHash]; + + isValid = hash2(tally) == tallyCommitment; + } +} diff --git a/contracts/contracts/TallyNonQvFactory.sol b/contracts/contracts/TallyNonQvFactory.sol new file mode 100644 index 0000000000..3d48f958f3 --- /dev/null +++ b/contracts/contracts/TallyNonQvFactory.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import { TallyNonQv } from "./TallyNonQv.sol"; +import { ITallySubsidyFactory } from "./interfaces/ITallySubsidyFactory.sol"; + +/// @title TallyNonQvFactory +/// @notice A factory contract which deploys TallyNonQv contracts. +contract TallyNonQvFactory is ITallySubsidyFactory { + /// @inheritdoc ITallySubsidyFactory + function deploy( + address _verifier, + address _vkRegistry, + address _poll, + address _messageProcessor, + address _owner + ) public returns (address tallyAddr) { + // deploy Tally for this Poll + TallyNonQv tally = new TallyNonQv(_verifier, _vkRegistry, _poll, _messageProcessor); + tally.transferOwnership(_owner); + tallyAddr = address(tally); + } +} diff --git a/contracts/deploy-config-example.json b/contracts/deploy-config-example.json index d73c5b4cbc..46b15a34a0 100644 --- a/contracts/deploy-config-example.json +++ b/contracts/deploy-config-example.json @@ -29,7 +29,8 @@ "Poll": { "pollDuration": 30, "coordinatorPubkey": "macipk.ea638a3366ed91f2e955110888573861f7c0fc0bb5fb8b8dca9cd7a08d7d6b93", - "subsidyEnabled": false + "subsidyEnabled": false, + "useQuadraticVoting": true } } } diff --git a/contracts/package.json b/contracts/package.json index 0eb7b08b44..dce749b6d1 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -37,6 +37,7 @@ "test:poll": "pnpm run test ./tests/Poll.test.ts", "test:messageProcessor": "pnpm run test ./tests/MessageProcessor.test.ts", "test:tally": "pnpm run test ./tests/Tally.test.ts", + "test:tallyNonQv": "pnpm run test ./tests/TallyNonQv.test.ts", "test:hasher": "pnpm run test ./tests/Hasher.test.ts", "test:utilities": "pnpm run test ./tests/Utilities.test.ts", "test:signupGatekeeper": "pnpm run test ./tests/SignUpGatekeeper.test.ts", diff --git a/contracts/tasks/deploy/maci/08-tallyFactory.ts b/contracts/tasks/deploy/maci/08-tallyFactory.ts index 090b78c590..ac3812a837 100644 --- a/contracts/tasks/deploy/maci/08-tallyFactory.ts +++ b/contracts/tasks/deploy/maci/08-tallyFactory.ts @@ -25,8 +25,10 @@ deployment const poseidonT5ContractAddress = storage.mustGetAddress(EContracts.PoseidonT5, hre.network.name); const poseidonT6ContractAddress = storage.mustGetAddress(EContracts.PoseidonT6, hre.network.name); + const useQuadraticVoting = deployment.getDeployConfigField(EContracts.Poll, "useQuadraticVoting"); + const linkedTallyFactoryContract = await deployment.linkPoseidonLibraries( - EContracts.TallyFactory, + useQuadraticVoting ? EContracts.TallyFactory : EContracts.TallyNonQvFactory, poseidonT3ContractAddress, poseidonT4ContractAddress, poseidonT5ContractAddress, diff --git a/contracts/tasks/deploy/poll/01-poll.ts b/contracts/tasks/deploy/poll/01-poll.ts index 71aa96465b..0038d62905 100644 --- a/contracts/tasks/deploy/poll/01-poll.ts +++ b/contracts/tasks/deploy/poll/01-poll.ts @@ -54,6 +54,8 @@ deployment.deployTask("poll:deploy-poll", "Deploy poll").setAction(async (_, hre const messageTreeDepth = deployment.getDeployConfigField(EContracts.VkRegistry, "messageTreeDepth"); const voteOptionTreeDepth = deployment.getDeployConfigField(EContracts.VkRegistry, "voteOptionTreeDepth"); const subsidyEnabled = deployment.getDeployConfigField(EContracts.Poll, "subsidyEnabled") ?? false; + const useQuadraticVoting = + deployment.getDeployConfigField(EContracts.Poll, "useQuadraticVoting") ?? false; const unserializedKey = PubKey.deserialize(coordinatorPubkey); const [pollContractAddress, messageProcessorContractAddress, tallyContractAddress, subsidyContractAddress] = @@ -100,7 +102,7 @@ deployment.deployTask("poll:deploy-poll", "Deploy poll").setAction(async (_, hre }); const tallyContract = await deployment.getContract({ - name: EContracts.Tally, + name: useQuadraticVoting ? EContracts.Tally : EContracts.TallyNonQv, address: tallyContractAddress, }); @@ -133,7 +135,7 @@ deployment.deployTask("poll:deploy-poll", "Deploy poll").setAction(async (_, hre }), storage.register({ - id: EContracts.Tally, + id: useQuadraticVoting ? EContracts.Tally : EContracts.TallyNonQv, key: `poll-${pollId}`, contract: tallyContract, args: [verifierContractAddress, vkRegistryContractAddress, pollContractAddress, messageProcessorContractAddress], diff --git a/contracts/tasks/helpers/Deployment.ts b/contracts/tasks/helpers/Deployment.ts index 27c4cada42..64ae134b41 100644 --- a/contracts/tasks/helpers/Deployment.ts +++ b/contracts/tasks/helpers/Deployment.ts @@ -7,13 +7,13 @@ import FileSync from "lowdb/adapters/FileSync"; import { exit } from "process"; -import type { EContracts, IDeployParams, IDeployStep, IDeployStepCatalog, IGetContractParams } from "./types"; import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import type { ConfigurableTaskDefinition, HardhatRuntimeEnvironment, TaskArguments } from "hardhat/types"; import { parseArtifact } from "../../ts/abi"; import { ContractStorage } from "./ContractStorage"; +import { EContracts, IDeployParams, IDeployStep, IDeployStepCatalog, IGetContractParams } from "./types"; /** * Internal deploy config structure type. diff --git a/contracts/tasks/helpers/ProofGenerator.ts b/contracts/tasks/helpers/ProofGenerator.ts index 4900cbb57e..f8ba045be6 100644 --- a/contracts/tasks/helpers/ProofGenerator.ts +++ b/contracts/tasks/helpers/ProofGenerator.ts @@ -7,7 +7,7 @@ import { genTreeCommitment, hash3, hashLeftRight } from "maci-crypto"; import fs from "fs"; import path from "path"; -import type { ICircuitFiles, IPrepareStateParams, IProofGeneratorParams } from "./types"; +import type { ICircuitFiles, IPrepareStateParams, IProofGeneratorParams, TallyData } from "./types"; import type { Proof } from "../../ts/types"; import type { BigNumberish } from "ethers"; @@ -266,7 +266,9 @@ export class ProofGenerator { let tallyCircuitInputs: CircuitInputs; while (this.poll.hasUntalliedBallots()) { - tallyCircuitInputs = this.poll.tallyVotes(this.useQuadraticVoting) as unknown as CircuitInputs; + tallyCircuitInputs = (this.useQuadraticVoting + ? this.poll.tallyVotes() + : this.poll.tallyVotesNonQv()) as unknown as CircuitInputs; // eslint-disable-next-line no-await-in-loop await this.generateProofs(tallyCircuitInputs, this.tally, `tally_${this.poll.numBatchesTallied - 1}.json`).then( @@ -290,26 +292,16 @@ export class ProofGenerator { BigInt(asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish)), ); - // Compute newPerVOSpentVoiceCreditsCommitment - const newPerVOSpentVoiceCreditsCommitment = genTreeCommitment( - this.poll.perVOSpentVoiceCredits, - BigInt(asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish)), - this.poll.treeDepths.voteOptionTreeDepth, - ); - - // Compute newTallyCommitment - const newTallyCommitment = hash3([ - newResultsCommitment, - newSpentVoiceCreditsCommitment, - newPerVOSpentVoiceCreditsCommitment, - ]); + let newPerVOSpentVoiceCreditsCommitment: bigint | undefined; + let newTallyCommitment: bigint; // create the tally file data to store for verification later - const tallyFileData = { + const tallyFileData: TallyData = { maci: this.maciContractAddress, pollId: this.poll.pollId.toString(), network: network.name, chainId: network.config.chainId?.toString(), + isQuadratic: this.useQuadraticVoting!, tallyAddress: this.tallyContractAddress, newTallyCommitment: asHex(tallyCircuitInputs!.newTallyCommitment as BigNumberish), results: { @@ -322,12 +314,32 @@ export class ProofGenerator { salt: asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish), commitment: asHex(newSpentVoiceCreditsCommitment), }, - perVOSpentVoiceCredits: { + }; + + if (this.useQuadraticVoting) { + // Compute newPerVOSpentVoiceCreditsCommitment + newPerVOSpentVoiceCreditsCommitment = genTreeCommitment( + this.poll.perVOSpentVoiceCredits, + BigInt(asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish)), + this.poll.treeDepths.voteOptionTreeDepth, + ); + + // Compute newTallyCommitment + newTallyCommitment = hash3([ + newResultsCommitment, + newSpentVoiceCreditsCommitment, + newPerVOSpentVoiceCreditsCommitment, + ]); + + // update perVOSpentVoiceCredits in the tally file data + tallyFileData.perVOSpentVoiceCredits = { tally: this.poll.perVOSpentVoiceCredits.map((x) => x.toString()), salt: asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish), commitment: asHex(newPerVOSpentVoiceCreditsCommitment), - }, - }; + }; + } else { + newTallyCommitment = hashLeftRight(newResultsCommitment, newSpentVoiceCreditsCommitment); + } fs.writeFileSync(this.tallyOutputFile, JSON.stringify(tallyFileData, null, 4)); diff --git a/contracts/tasks/helpers/Prover.ts b/contracts/tasks/helpers/Prover.ts index 65e2cfce45..b203718ab9 100644 --- a/contracts/tasks/helpers/Prover.ts +++ b/contracts/tasks/helpers/Prover.ts @@ -10,6 +10,7 @@ import type { Poll, Subsidy, Tally, + TallyNonQv, Verifier, VkRegistry, } from "../../typechain-types"; @@ -57,7 +58,7 @@ export class Prover { /** * Tally contract typechain wrapper */ - private tallyContract: Tally; + private tallyContract: Tally | TallyNonQv; /** * Subsidy contract typechain wrapper diff --git a/contracts/tasks/helpers/types.ts b/contracts/tasks/helpers/types.ts index 3166ee8dae..9931cd0efd 100644 --- a/contracts/tasks/helpers/types.ts +++ b/contracts/tasks/helpers/types.ts @@ -5,6 +5,7 @@ import type { Poll, Subsidy, Tally, + TallyNonQv, Verifier, VkRegistry, } from "../../typechain-types"; @@ -346,7 +347,7 @@ export interface IProverParams { /** * Tally contract typechain wrapper */ - tallyContract: Tally; + tallyContract: Tally | TallyNonQv; /** * Subsidy contract typechain wrapper @@ -505,6 +506,7 @@ export enum EContracts { PollFactory = "PollFactory", MessageProcessorFactory = "MessageProcessorFactory", TallyFactory = "TallyFactory", + TallyNonQvFactory = "TallyNonQvFactory", SubsidyFactory = "SubsidyFactory", PoseidonT3 = "PoseidonT3", PoseidonT4 = "PoseidonT4", @@ -513,6 +515,7 @@ export enum EContracts { VkRegistry = "VkRegistry", Poll = "Poll", Tally = "Tally", + TallyNonQv = "TallyNonQv", MessageProcessor = "MessageProcessor", Subsidy = "Subsidy", AccQueue = "AccQueue", @@ -580,3 +583,104 @@ export interface ITreeMergeParams { */ messageAccQueueContract: AccQueue; } + +/** + * Interface for the tally file data. + */ +export interface TallyData { + /** + * The MACI address. + */ + maci: string; + + /** + * The ID of the poll. + */ + pollId: string; + + /** + * The name of the network for which these proofs + * are valid for + */ + network?: string; + + /** + * The chain ID for which these proofs are valid for + */ + chainId?: string; + + /** + * Whether the poll is using quadratic voting or not. + */ + isQuadratic: boolean; + + /** + * The address of the Tally contract. + */ + tallyAddress: string; + + /** + * The new tally commitment. + */ + newTallyCommitment: string; + + /** + * The results of the poll. + */ + results: { + /** + * The tally of the results. + */ + tally: string[]; + + /** + * The salt of the results. + */ + salt: string; + + /** + * The commitment of the results. + */ + commitment: string; + }; + + /** + * The total spent voice credits. + */ + totalSpentVoiceCredits: { + /** + * The spent voice credits. + */ + spent: string; + + /** + * The salt of the spent voice credits. + */ + salt: string; + + /** + * The commitment of the spent voice credits. + */ + commitment: string; + }; + + /** + * The per VO spent voice credits. + */ + perVOSpentVoiceCredits?: { + /** + * The tally of the per VO spent voice credits. + */ + tally: string[]; + + /** + * The salt of the per VO spent voice credits. + */ + salt: string; + + /** + * The commitment of the per VO spent voice credits. + */ + commitment: string; + }; +} diff --git a/contracts/tasks/runner/deployFull.ts b/contracts/tasks/runner/deployFull.ts index 30b8d42af9..f900ae95c6 100644 --- a/contracts/tasks/runner/deployFull.ts +++ b/contracts/tasks/runner/deployFull.ts @@ -1,9 +1,8 @@ /* eslint-disable no-console */ import { task, types } from "hardhat/config"; -import type { IDeployParams } from "../helpers/types"; - import { Deployment } from "../helpers/Deployment"; +import { type IDeployParams } from "../helpers/types"; /** * Main deployment task which runs deploy steps in the same order that `Deployment#deployTask` is called. diff --git a/contracts/tasks/runner/deployPoll.ts b/contracts/tasks/runner/deployPoll.ts index 56cb7de35a..3985fcd392 100644 --- a/contracts/tasks/runner/deployPoll.ts +++ b/contracts/tasks/runner/deployPoll.ts @@ -1,9 +1,8 @@ /* eslint-disable no-console */ import { task, types } from "hardhat/config"; -import type { IDeployParams } from "../helpers/types"; - import { Deployment } from "../helpers/Deployment"; +import { type IDeployParams } from "../helpers/types"; /** * Poll deployment task which runs deploy steps in the same order that `Deployment#deployTask` is called. diff --git a/contracts/tasks/runner/prove.ts b/contracts/tasks/runner/prove.ts index 3c7c9d3dbb..06a1358367 100644 --- a/contracts/tasks/runner/prove.ts +++ b/contracts/tasks/runner/prove.ts @@ -16,6 +16,7 @@ import { type AccQueue, MessageProcessor, Tally, + TallyNonQv, } from "../../typechain-types"; import { ContractStorage } from "../helpers/ContractStorage"; import { Deployment } from "../helpers/Deployment"; @@ -150,10 +151,17 @@ task("prove", "Command to generate proof and prove the result of a poll on-chain name: EContracts.MessageProcessor, key: `poll-${poll.toString()}`, }); - const tallyContract = await deployment.getContract({ - name: EContracts.Tally, - key: `poll-${poll.toString()}`, - }); + + // get the tally contract based on the useQuadraticVoting flag + const tallyContract = useQuadraticVoting + ? await deployment.getContract({ + name: EContracts.Tally, + key: `poll-${poll.toString()}`, + }) + : await deployment.getContract({ + name: EContracts.TallyNonQv, + key: `poll-${poll.toString()}`, + }); const tallyContractAddress = await tallyContract.getAddress(); let subsidyContract: Subsidy | undefined; diff --git a/contracts/tests/EASGatekeeper.test.ts b/contracts/tests/EASGatekeeper.test.ts index c848bce645..baa7c36e65 100644 --- a/contracts/tests/EASGatekeeper.test.ts +++ b/contracts/tests/EASGatekeeper.test.ts @@ -87,7 +87,14 @@ describe("EAS Gatekeeper", () => { let maciContract: MACI; before(async () => { - const r = await deployTestContracts(initialVoiceCreditBalance, STATE_TREE_DEPTH, signer, true, easGatekeeper); + const r = await deployTestContracts( + initialVoiceCreditBalance, + STATE_TREE_DEPTH, + signer, + true, + true, + easGatekeeper, + ); maciContract = r.maciContract; }); diff --git a/contracts/tests/SignUpGatekeeper.test.ts b/contracts/tests/SignUpGatekeeper.test.ts index bc09aad040..254a0338e7 100644 --- a/contracts/tests/SignUpGatekeeper.test.ts +++ b/contracts/tests/SignUpGatekeeper.test.ts @@ -46,6 +46,7 @@ describe("SignUpGatekeeper", () => { STATE_TREE_DEPTH, signer, true, + true, signUpTokenGatekeeperContract, ); diff --git a/contracts/tests/Tally.test.ts b/contracts/tests/Tally.test.ts index 1e5aa412d2..ead2378ca8 100644 --- a/contracts/tests/Tally.test.ts +++ b/contracts/tests/Tally.test.ts @@ -59,7 +59,7 @@ describe("TallyVotes", () => { signer = await getDefaultSigner(); - const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true); + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true, true); maciContract = r.maciContract; verifierContract = r.mockVerifierContract as Verifier; vkRegistryContract = r.vkRegistryContract; @@ -221,7 +221,7 @@ describe("TallyVotes", () => { const intStateTreeDepth = 2; - const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true); + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true, true); maciContract = r.maciContract; verifierContract = r.mockVerifierContract as Verifier; vkRegistryContract = r.vkRegistryContract; @@ -362,7 +362,7 @@ describe("TallyVotes", () => { const intStateTreeDepth = 2; - const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true); + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true, true); maciContract = r.maciContract; verifierContract = r.mockVerifierContract as Verifier; vkRegistryContract = r.vkRegistryContract; diff --git a/contracts/tests/TallyNonQv.test.ts b/contracts/tests/TallyNonQv.test.ts new file mode 100644 index 0000000000..5ea9f2aa82 --- /dev/null +++ b/contracts/tests/TallyNonQv.test.ts @@ -0,0 +1,213 @@ +/* eslint-disable no-underscore-dangle */ +import { expect } from "chai"; +import { BaseContract, Signer } from "ethers"; +import { EthereumProvider } from "hardhat/types"; +import { + MaciState, + Poll, + packTallyVotesSmallVals, + IProcessMessagesCircuitInputs, + ITallyCircuitInputs, +} from "maci-core"; +import { NOTHING_UP_MY_SLEEVE } from "maci-crypto"; +import { Keypair, Message, PubKey } from "maci-domainobjs"; + +import type { Tally, MACI, Poll as PollContract, MessageProcessor, Verifier, VkRegistry } from "../typechain-types"; + +import { parseArtifact } from "../ts/abi"; +import { IVerifyingKeyStruct } from "../ts/types"; +import { getDefaultSigner } from "../ts/utils"; + +import { + STATE_TREE_DEPTH, + duration, + maxValues, + messageBatchSize, + tallyBatchSize, + testProcessVk, + testTallyVk, + treeDepths, +} from "./constants"; +import { timeTravel, deployTestContracts } from "./utils"; + +describe("TallyVotesNonQv", () => { + let signer: Signer; + let maciContract: MACI; + let pollContract: PollContract; + let tallyContract: Tally; + let mpContract: MessageProcessor; + let verifierContract: Verifier; + let vkRegistryContract: VkRegistry; + + const coordinator = new Keypair(); + let users: Keypair[]; + let maciState: MaciState; + + const [pollAbi] = parseArtifact("Poll"); + const [mpAbi] = parseArtifact("MessageProcessor"); + const [tallyAbi] = parseArtifact("TallyNonQv"); + + let pollId: bigint; + let poll: Poll; + + let generatedInputs: IProcessMessagesCircuitInputs; + + before(async () => { + maciState = new MaciState(STATE_TREE_DEPTH); + + users = [new Keypair(), new Keypair()]; + + signer = await getDefaultSigner(); + + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, false, true); + maciContract = r.maciContract; + verifierContract = r.mockVerifierContract as Verifier; + vkRegistryContract = r.vkRegistryContract; + + // deploy a poll + // deploy on chain poll + const tx = await maciContract.deployPoll( + duration, + treeDepths, + coordinator.pubKey.asContractParam(), + verifierContract, + vkRegistryContract, + false, + { + gasLimit: 10000000, + }, + ); + const receipt = await tx.wait(); + + const block = await signer.provider!.getBlock(receipt!.blockHash); + const deployTime = block!.timestamp; + + expect(receipt?.status).to.eq(1); + const iface = maciContract.interface; + const logs = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as { + args: { + _pollId: bigint; + pollAddr: { + poll: string; + messageProcessor: string; + tally: string; + }; + }; + name: string; + }; + expect(event.name).to.eq("DeployPoll"); + + pollId = event.args._pollId; + + const pollContractAddress = await maciContract.getPoll(pollId); + pollContract = new BaseContract(pollContractAddress, pollAbi, signer) as PollContract; + mpContract = new BaseContract(event.args.pollAddr.messageProcessor, mpAbi, signer) as MessageProcessor; + tallyContract = new BaseContract(event.args.pollAddr.tally, tallyAbi, signer) as Tally; + + // deploy local poll + const p = maciState.deployPoll(BigInt(deployTime + duration), maxValues, treeDepths, messageBatchSize, coordinator); + expect(p.toString()).to.eq(pollId.toString()); + // publish the NOTHING_UP_MY_SLEEVE message + const messageData = [NOTHING_UP_MY_SLEEVE]; + for (let i = 1; i < 10; i += 1) { + messageData.push(BigInt(0)); + } + const message = new Message(BigInt(1), messageData); + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + + // save the poll + poll = maciState.polls.get(pollId)!; + + poll.publishMessage(message, padKey); + + // update the poll state + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // process messages locally + generatedInputs = poll.processMessages(pollId, false); + + // set the verification keys on the vk smart contract + await vkRegistryContract.setVerifyingKeys( + STATE_TREE_DEPTH, + treeDepths.intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + }); + + it("should not be possible to tally votes before the poll has ended", async () => { + await expect(tallyContract.tallyVotes(0, [0, 0, 0, 0, 0, 0, 0, 0])).to.be.revertedWithCustomError( + tallyContract, + "VotingPeriodNotPassed", + ); + }); + + it("genTallyVotesPackedVals() should generate the correct value", async () => { + const onChainPackedVals = BigInt(await tallyContract.genTallyVotesPackedVals(users.length, 0, tallyBatchSize)); + const packedVals = packTallyVotesSmallVals(0, tallyBatchSize, users.length); + expect(onChainPackedVals.toString()).to.eq(packedVals.toString()); + }); + + it("updateSbCommitment() should revert when the messages have not been processed yet", async () => { + // go forward in time + await timeTravel(signer.provider! as unknown as EthereumProvider, duration + 1); + + await expect(tallyContract.updateSbCommitment()).to.be.revertedWithCustomError( + tallyContract, + "ProcessingNotComplete", + ); + }); + + it("tallyVotes() should fail as the messages have not been processed yet", async () => { + await expect(tallyContract.tallyVotes(0, [0, 0, 0, 0, 0, 0, 0, 0])).to.be.revertedWithCustomError( + tallyContract, + "ProcessingNotComplete", + ); + }); + + describe("after merging acc queues", () => { + let tallyGeneratedInputs: ITallyCircuitInputs; + before(async () => { + await pollContract.mergeMaciStateAqSubRoots(0, pollId); + await pollContract.mergeMaciStateAq(0); + + await pollContract.mergeMessageAqSubRoots(0); + await pollContract.mergeMessageAq(); + tallyGeneratedInputs = poll.tallyVotes(); + }); + + it("isTallied should return false", async () => { + const isTallied = await tallyContract.isTallied(); + expect(isTallied).to.eq(false); + }); + + it("tallyVotes() should update the tally commitment", async () => { + // do the processing on the message processor contract + await mpContract.processMessages(generatedInputs.newSbCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + const onChainNewTallyCommitment = await tallyContract.tallyCommitment(); + expect(tallyGeneratedInputs.newTallyCommitment).to.eq(onChainNewTallyCommitment.toString()); + }); + + it("isTallied should return true", async () => { + const isTallied = await tallyContract.isTallied(); + expect(isTallied).to.eq(true); + }); + + it("tallyVotes() should revert when votes have already been tallied", async () => { + await expect( + tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), + ).to.be.revertedWithCustomError(tallyContract, "AllBallotsTallied"); + }); + }); +}); diff --git a/contracts/tests/utils.ts b/contracts/tests/utils.ts index 0651c6cd30..4c41c9145d 100644 --- a/contracts/tests/utils.ts +++ b/contracts/tests/utils.ts @@ -488,7 +488,8 @@ export const deployTestContracts = async ( initialVoiceCreditBalance: number, stateTreeDepth: number, signer?: Signer, - quiet = false, + useQv = true, + quiet = true, gatekeeper: FreeForAllGatekeeper | undefined = undefined, ): Promise => { const mockVerifierContract = await deployMockVerifier(signer, true); @@ -520,6 +521,7 @@ export const deployTestContracts = async ( topupCreditContractAddress, signer, stateTreeDepth, + useQv, quiet, }); diff --git a/contracts/ts/deploy.ts b/contracts/ts/deploy.ts index 9fb4dc7299..dbea0ff1f6 100644 --- a/contracts/ts/deploy.ts +++ b/contracts/ts/deploy.ts @@ -25,6 +25,7 @@ import { TopupCredit, Verifier, VkRegistry, + TallyNonQvFactory, } from "../typechain-types"; import { parseArtifact } from "./abi"; @@ -264,7 +265,8 @@ export const deployMaci = async ({ signer, poseidonAddresses, stateTreeDepth = 10, - quiet = false, + useQv = true, + quiet = true, }: IDeployMaciArgs): Promise => { const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = await deployPoseidonContracts(signer, poseidonAddresses, quiet); @@ -281,7 +283,14 @@ export const deployMaci = async ({ poseidonT6, })); - const contractsToLink = ["MACI", "PollFactory", "MessageProcessorFactory", "TallyFactory", "SubsidyFactory"]; + const contractsToLink = [ + "MACI", + "PollFactory", + "MessageProcessorFactory", + "TallyFactory", + "TallyNonQvFactory", + "SubsidyFactory", + ]; // Link Poseidon contracts to MACI const linkedContractFactories = await Promise.all( @@ -298,24 +307,33 @@ export const deployMaci = async ({ ), ); - const [maciContractFactory, pollFactoryContractFactory, messageProcessorFactory, tallyFactory, subsidyFactory] = - await Promise.all(linkedContractFactories); + const [ + maciContractFactory, + pollFactoryContractFactory, + messageProcessorFactory, + tallyFactory, + tallyFactoryNonQv, + subsidyFactory, + ] = await Promise.all(linkedContractFactories); const pollFactoryContract = await deployContractWithLinkedLibraries( pollFactoryContractFactory, "PollFactory", quiet, ); + const messageProcessorFactoryContract = await deployContractWithLinkedLibraries( messageProcessorFactory, "MessageProcessorFactory", quiet, ); - const tallyFactoryContract = await deployContractWithLinkedLibraries( - tallyFactory, - "TallyFactory", - quiet, - ); + + // deploy either the qv or non qv tally factory - they both implement the same interface + // so as long as maci is concerned, they are interchangeable + const tallyFactoryContract = useQv + ? await deployContractWithLinkedLibraries(tallyFactory, "TallyFactory", quiet) + : await deployContractWithLinkedLibraries(tallyFactoryNonQv, "TallyNonQvFactory", quiet); + const subsidyFactoryContract = await deployContractWithLinkedLibraries( subsidyFactory, "SubsidyFactory", diff --git a/contracts/ts/types.ts b/contracts/ts/types.ts index 0ddb280eec..4bf861b24c 100644 --- a/contracts/ts/types.ts +++ b/contracts/ts/types.ts @@ -162,6 +162,11 @@ export interface IDeployMaciArgs { * Whether to suppress console output */ quiet?: boolean; + + /** + * Whether to support QV or not + */ + useQv?: boolean; } /** diff --git a/core/ts/Poll.ts b/core/ts/Poll.ts index 016fd6b8a1..7d8dfddf69 100644 --- a/core/ts/Poll.ts +++ b/core/ts/Poll.ts @@ -974,10 +974,9 @@ export class Poll implements IPoll { /** * This method tallies a ballots and updates the tally results. - * @param useQuadraticVoting - Whether to use quadratic voting or not. Default is true. * @returns the circuit inputs for the TallyVotes circuit. */ - tallyVotes = (useQuadraticVoting = true): ITallyCircuitInputs => { + tallyVotes = (): ITallyCircuitInputs => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.sbSalts[this.currentMessageBatchIndex!] === undefined) { throw new Error("You must process the messages first"); @@ -1010,14 +1009,14 @@ export class Poll implements IPoll { const currentPerVOSpentVoiceCreditsCommitment = this.genPerVOSpentVoiceCreditsCommitment( currentPerVOSpentVoiceCreditsRootSalt, batchStartIndex, - useQuadraticVoting, + true, ); // generate a commitment to the current spent voice credits const currentSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( currentSpentVoiceCreditSubtotalSalt, batchStartIndex, - useQuadraticVoting, + true, ); // the current commitment for the first batch will be 0 @@ -1059,10 +1058,10 @@ export class Poll implements IPoll { this.tallyResult[j] += v; // the per vote option spent voice credits will be the sum of the squares of the votes - this.perVOSpentVoiceCredits[j] += useQuadraticVoting ? v * v : v; + this.perVOSpentVoiceCredits[j] += v * v; // the total spent voice credits will be the sum of the squares of the votes - this.totalSpentVoiceCredits += useQuadraticVoting ? v * v : v; + this.totalSpentVoiceCredits += v * v; } } @@ -1094,14 +1093,14 @@ export class Poll implements IPoll { const newSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( newSpentVoiceCreditSubtotalSalt, batchStartIndex + batchSize, - useQuadraticVoting, + true, ); // generate the new per VO spent voice credits commitment with the new salts and data const newPerVOSpentVoiceCreditsCommitment = this.genPerVOSpentVoiceCreditsCommitment( newPerVOSpentVoiceCreditsRootSalt, batchStartIndex + batchSize, - useQuadraticVoting, + true, ); // generate the new tally commitment @@ -1152,6 +1151,143 @@ export class Poll implements IPoll { return circuitInputs; }; + tallyVotesNonQv = (): ITallyCircuitInputs => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.sbSalts[this.currentMessageBatchIndex!] === undefined) { + throw new Error("You must process the messages first"); + } + + const batchSize = this.batchSizes.tallyBatchSize; + + assert(this.hasUntalliedBallots(), "No more ballots to tally"); + + // calculate where we start tallying next + const batchStartIndex = this.numBatchesTallied * batchSize; + + // get the salts needed for the commitments + const currentResultsRootSalt = batchStartIndex === 0 ? 0n : this.resultRootSalts[batchStartIndex - batchSize]; + + const currentSpentVoiceCreditSubtotalSalt = + batchStartIndex === 0 ? 0n : this.spentVoiceCreditSubtotalSalts[batchStartIndex - batchSize]; + + // generate a commitment to the current results + const currentResultsCommitment = genTreeCommitment( + this.tallyResult, + currentResultsRootSalt, + this.treeDepths.voteOptionTreeDepth, + ); + + // generate a commitment to the current spent voice credits + const currentSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( + currentSpentVoiceCreditSubtotalSalt, + batchStartIndex, + false, + ); + + // the current commitment for the first batch will be 0 + // otherwise calculate as + // hash([ + // currentResultsCommitment, + // currentSpentVoiceCreditsCommitment, + // ]) + const currentTallyCommitment = + batchStartIndex === 0 ? 0n : hashLeftRight(currentResultsCommitment, currentSpentVoiceCreditsCommitment); + + const ballots: Ballot[] = []; + const currentResults = this.tallyResult.map((x) => BigInt(x.toString())); + const currentSpentVoiceCreditSubtotal = BigInt(this.totalSpentVoiceCredits.toString()); + + // loop in normal order to tally the ballots one by one + for (let i = this.numBatchesTallied * batchSize; i < this.numBatchesTallied * batchSize + batchSize; i += 1) { + // we stop if we have no more ballots to tally + if (i >= this.ballots.length) { + break; + } + + // save to the local ballot array + ballots.push(this.ballots[i]); + + // for each possible vote option we loop and calculate + for (let j = 0; j < this.maxValues.maxVoteOptions; j += 1) { + const v = this.ballots[i].votes[j]; + + this.tallyResult[j] += v; + + // the total spent voice credits will be the sum of the the votes + this.totalSpentVoiceCredits += v; + } + } + + const emptyBallot = new Ballot(this.maxValues.maxVoteOptions, this.treeDepths.voteOptionTreeDepth); + + // pad the ballots array + while (ballots.length < batchSize) { + ballots.push(emptyBallot); + } + + // generate the new salts + const newResultsRootSalt = genRandomSalt(); + const newSpentVoiceCreditSubtotalSalt = genRandomSalt(); + + // and save them to be used in the next batch + this.resultRootSalts[batchStartIndex] = newResultsRootSalt; + this.spentVoiceCreditSubtotalSalts[batchStartIndex] = newSpentVoiceCreditSubtotalSalt; + + // generate the new results commitment with the new salts and data + const newResultsCommitment = genTreeCommitment( + this.tallyResult, + newResultsRootSalt, + this.treeDepths.voteOptionTreeDepth, + ); + + // generate the new spent voice credits commitment with the new salts and data + const newSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( + newSpentVoiceCreditSubtotalSalt, + batchStartIndex + batchSize, + false, + ); + + // generate the new tally commitment + const newTallyCommitment = hashLeftRight(newResultsCommitment, newSpentVoiceCreditsCommitment); + + // cache vars + const stateRoot = this.stateTree!.root; + const ballotRoot = this.ballotTree!.root; + const sbSalt = this.sbSalts[this.currentMessageBatchIndex!]; + const sbCommitment = hash3([stateRoot, ballotRoot, sbSalt]); + + const packedVals = packTallyVotesSmallVals(batchStartIndex, batchSize, Number(this.numSignups)); + const inputHash = sha256Hash([packedVals, sbCommitment, currentTallyCommitment, newTallyCommitment]); + + const ballotSubrootProof = this.ballotTree?.genSubrootProof(batchStartIndex, batchStartIndex + batchSize); + + const votes = ballots.map((x) => x.votes); + + const circuitInputs = stringifyBigInts({ + stateRoot, + ballotRoot, + sbSalt, + sbCommitment, + currentTallyCommitment, + newTallyCommitment, + packedVals, // contains numSignUps and batchStartIndex + inputHash, + ballots: ballots.map((x) => x.asCircuitInputs()), + ballotPathElements: ballotSubrootProof!.pathElements, + votes, + currentResults, + currentResultsRootSalt, + currentSpentVoiceCreditSubtotal, + currentSpentVoiceCreditSubtotalSalt, + newResultsRootSalt, + newSpentVoiceCreditSubtotalSalt, + }) as unknown as ITallyCircuitInputs; + + this.numBatchesTallied += 1; + + return circuitInputs; + }; + /** * This method generates a commitment to the total spent voice credits. * diff --git a/core/ts/__tests__/Poll.test.ts b/core/ts/__tests__/Poll.test.ts index 83aa6d6978..02908f50c2 100644 --- a/core/ts/__tests__/Poll.test.ts +++ b/core/ts/__tests__/Poll.test.ts @@ -527,17 +527,13 @@ describe("Poll", function test() { secondPoll.publishMessage(secondMessage, secondEcdhKeypair.pubKey); secondPoll.processAllMessages(); - secondPoll.tallyVotes(false); + secondPoll.tallyVotesNonQv(); const spentVoiceCredits = secondPoll.totalSpentVoiceCredits; const results = secondPoll.tallyResult; // spent voice credit is not vote weight * vote weight expect(spentVoiceCredits).to.eq(secondVoteWeight); expect(results[Number.parseInt(secondVoteOption.toString(), 10)]).to.eq(secondVoteWeight); - // per VO spent voice credit is not vote weight * vote weight - expect(secondPoll.perVOSpentVoiceCredits[Number.parseInt(secondVoteOption.toString(), 10)]).to.eq( - secondVoteWeight, - ); }); it("should throw when there are no more ballots to tally", () => { diff --git a/core/ts/__tests__/e2e.test.ts b/core/ts/__tests__/e2e.test.ts index 4733064cbc..a1de8a4bd6 100644 --- a/core/ts/__tests__/e2e.test.ts +++ b/core/ts/__tests__/e2e.test.ts @@ -674,14 +674,11 @@ describe("MaciState/Poll e2e", function test() { expect(poll.hasUntalliedBallots()).to.eq(true); - poll.tallyVotes(useQv); + poll.tallyVotesNonQv(); const finalTotal = calculateTotal(poll.tallyResult); expect(finalTotal.toString()).to.eq(voteWeight.toString()); - // check that the perVOSpentVoiceCredits is correct - expect(poll.perVOSpentVoiceCredits[0].toString()).to.eq(voteWeight.toString()); - // check that the totalSpentVoiceCredits is correct expect(poll.totalSpentVoiceCredits.toString()).to.eq(voteWeight.toString()); }); diff --git a/core/ts/utils/types.ts b/core/ts/utils/types.ts index 6290a10936..d02abe7e3c 100644 --- a/core/ts/utils/types.ts +++ b/core/ts/utils/types.ts @@ -193,10 +193,10 @@ export interface ITallyCircuitInputs { currentResultsRootSalt: string; currentSpentVoiceCreditSubtotal: string; currentSpentVoiceCreditSubtotalSalt: string; - currentPerVOSpentVoiceCredits: string[]; - currentPerVOSpentVoiceCreditsRootSalt: string; + currentPerVOSpentVoiceCredits?: string[]; + currentPerVOSpentVoiceCreditsRootSalt?: string; newResultsRootSalt: string; - newPerVOSpentVoiceCreditsRootSalt: string; + newPerVOSpentVoiceCreditsRootSalt?: string; newSpentVoiceCreditSubtotalSalt: string; } diff --git a/integrationTests/ts/__tests__/utils/utils.ts b/integrationTests/ts/__tests__/utils/utils.ts index efcaa1028a..1623114362 100644 --- a/integrationTests/ts/__tests__/utils/utils.ts +++ b/integrationTests/ts/__tests__/utils/utils.ts @@ -136,7 +136,7 @@ export const expectTally = ( }); expect(tallyFile.results.tally).to.deep.equal(genTally); - expect(tallyFile.perVOSpentVoiceCredits.tally).to.deep.equal(genPerVOSpentVoiceCredits); + expect(tallyFile.perVOSpentVoiceCredits?.tally).to.deep.equal(genPerVOSpentVoiceCredits); expect(tallyFile.totalSpentVoiceCredits.spent).to.eq(expectedTotalSpentVoiceCredits.toString()); };