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()); };