diff --git a/packages/cli/tests/constants.ts b/packages/cli/tests/constants.ts index e78c02043e..a0100fbe0a 100644 --- a/packages/cli/tests/constants.ts +++ b/packages/cli/tests/constants.ts @@ -131,6 +131,7 @@ export const mergeSignupsArgs: Omit = { export const proveOnChainArgs: Omit = { pollId: 0n, + tallyFile: testTallyFilePath, proofDir: testProofsDirPath, }; diff --git a/packages/cli/ts/commands/proveOnChain.ts b/packages/cli/ts/commands/proveOnChain.ts index c8cf6b5a87..52259aaa8e 100644 --- a/packages/cli/ts/commands/proveOnChain.ts +++ b/packages/cli/ts/commands/proveOnChain.ts @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ import { type BigNumberish } from "ethers"; -import { type IVerifyingKeyStruct, formatProofForVerifierContract } from "maci-contracts"; +import { type IVerifyingKeyStruct, TallyData, formatProofForVerifierContract } from "maci-contracts"; import { MACI__factory as MACIFactory, AccQueue__factory as AccQueueFactory, @@ -11,7 +11,7 @@ import { Verifier__factory as VerifierFactory, } from "maci-contracts/typechain-types"; import { MESSAGE_TREE_ARITY, STATE_TREE_ARITY } from "maci-core"; -import { G1Point, G2Point } from "maci-crypto"; +import { G1Point, G2Point, genTreeProof } from "maci-crypto"; import { VerifyingKey } from "maci-domainobjs"; import fs from "fs"; @@ -42,6 +42,7 @@ export const proveOnChain = async ({ proofDir, maciAddress, signer, + tallyFile, quiet = true, }: ProveOnChainArgs): Promise => { banner(quiet); @@ -368,4 +369,24 @@ export const proveOnChain = async ({ if (tallyBatchNum === totalTallyBatches) { logGreen(quiet, success("All vote tallying proofs have been submitted.")); } + + if (tallyFile) { + const tallyData = await fs.promises.readFile(tallyFile).then((res) => JSON.parse(res.toString()) as TallyData); + + const tallyResults = tallyData.results.tally.map((t) => BigInt(t)); + const tallyResultProofs = tallyData.results.tally.map((_, index) => + genTreeProof(index, tallyResults, Number(treeDepths.voteOptionTreeDepth)), + ); + + await tallyContract + .addTallyResults( + tallyData.results.tally.map((_, index) => index), + tallyResults, + tallyResultProofs, + tallyData.results.salt, + tallyData.totalSpentVoiceCredits.commitment, + tallyData.perVOSpentVoiceCredits?.commitment ?? 0n, + ) + .then((tx) => tx.wait()); + } }; diff --git a/packages/cli/ts/index.ts b/packages/cli/ts/index.ts index 1f33753f5b..d9aba453b2 100644 --- a/packages/cli/ts/index.ts +++ b/packages/cli/ts/index.ts @@ -635,6 +635,10 @@ program .command("proveOnChain") .description("prove the results of a poll on chain") .requiredOption("-o, --poll-id ", "the poll id", BigInt) + .option( + "-t, --tally-file ", + "the tally file with results, per vote option spent credits, spent voice credits total", + ) .option("-q, --quiet ", "whether to print values to the console", (value) => value === "true", false) .option("-r, --rpc-provider ", "the rpc provider URL") .option("-x, --maci-address ", "the MACI contract address") @@ -645,6 +649,7 @@ program await proveOnChain({ pollId: cmdObj.pollId, + tallyFile: cmdObj.tallyFile, proofDir: cmdObj.proofDir, maciAddress: cmdObj.maciAddress, quiet: cmdObj.quiet, diff --git a/packages/cli/ts/utils/interfaces.ts b/packages/cli/ts/utils/interfaces.ts index bb570d6cf0..57ca72524a 100644 --- a/packages/cli/ts/utils/interfaces.ts +++ b/packages/cli/ts/utils/interfaces.ts @@ -590,6 +590,11 @@ export interface ProveOnChainArgs { */ signer: Signer; + /** + * The tally file with results, per vote option spent credits, spent voice credits total + */ + tallyFile?: string; + /** * The address of the MACI contract */ diff --git a/packages/contracts/contracts/Tally.sol b/packages/contracts/contracts/Tally.sol index 4c2a086014..0f8e744cea 100644 --- a/packages/contracts/contracts/Tally.sol +++ b/packages/contracts/contracts/Tally.sol @@ -52,6 +52,12 @@ contract Tally is Ownable, SnarkCommon, CommonUtilities, Hasher, DomainObjs, ITa IMessageProcessor public immutable messageProcessor; Mode public immutable mode; + // The tally results + mapping(uint256 => uint256) public tallyResults; + + // The total tally results number + uint256 public totalTallyResults; + /// @notice custom errors error ProcessingNotComplete(); error InvalidTallyVotesProof(); @@ -60,6 +66,7 @@ contract Tally is Ownable, SnarkCommon, CommonUtilities, Hasher, DomainObjs, ITa error BatchStartIndexTooLarge(); error TallyBatchSizeTooLarge(); error NotSupported(); + error VotesNotTallied(); /// @notice Create a new Tally contract /// @param _verifier The Verifier contract @@ -344,4 +351,83 @@ contract Tally is Ownable, SnarkCommon, CommonUtilities, Hasher, DomainObjs, ITa isValid = hash2(tally) == tallyCommitment; } } + + /** + * @notice Add and verify tally results by batch. + * @param _voteOptionIndices Vote option index. + * @param _tallyResults The results of vote tally for the recipients. + * @param _tallyResultProofs Proofs of correctness of the vote tally results. + * @param _tallyResultSalt the respective salt in the results object in the tally.json + * @param _spentVoiceCreditsHashes hashLeftRight(number of spent voice credits, spent salt) + * @param _perVOSpentVoiceCreditsHashes hashLeftRight(merkle root of the no spent voice credits per vote option, perVOSpentVoiceCredits salt) + */ + function addTallyResults( + uint256[] calldata _voteOptionIndices, + uint256[] calldata _tallyResults, + uint256[][][] calldata _tallyResultProofs, + uint256 _tallyResultSalt, + uint256 _spentVoiceCreditsHashes, + uint256 _perVOSpentVoiceCreditsHashes + ) public virtual onlyOwner { + if (!isTallied()) { + revert VotesNotTallied(); + } + + (, , , uint8 voteOptionTreeDepth) = poll.treeDepths(); + uint256 voteOptionsLength = _voteOptionIndices.length; + + for (uint256 i = 0; i < voteOptionsLength; ) { + addTallyResult( + _voteOptionIndices[i], + _tallyResults[i], + _tallyResultProofs[i], + _tallyResultSalt, + _spentVoiceCreditsHashes, + _perVOSpentVoiceCreditsHashes, + voteOptionTreeDepth + ); + + unchecked { + i++; + } + } + + totalTallyResults += voteOptionsLength; + } + + /** + * @dev Add and verify tally votes and calculate sum of tally squares for alpha calculation. + * @param _voteOptionIndex Vote option index. + * @param _tallyResult The results of vote tally for the recipients. + * @param _tallyResultProof Proofs of correctness of the vote tally results. + * @param _tallyResultSalt the respective salt in the results object in the tally.json + * @param _spentVoiceCreditsHash hashLeftRight(number of spent voice credits, spent salt) + * @param _perVOSpentVoiceCreditsHash hashLeftRight(merkle root of the no spent voice credits per vote option, perVOSpentVoiceCredits salt) + * @param _voteOptionTreeDepth vote option tree depth + */ + function addTallyResult( + uint256 _voteOptionIndex, + uint256 _tallyResult, + uint256[][] calldata _tallyResultProof, + uint256 _tallyResultSalt, + uint256 _spentVoiceCreditsHash, + uint256 _perVOSpentVoiceCreditsHash, + uint8 _voteOptionTreeDepth + ) internal virtual { + bool isValid = verifyTallyResult( + _voteOptionIndex, + _tallyResult, + _tallyResultProof, + _tallyResultSalt, + _voteOptionTreeDepth, + _spentVoiceCreditsHash, + _perVOSpentVoiceCreditsHash + ); + + if (!isValid) { + revert InvalidTallyVotesProof(); + } + + tallyResults[_voteOptionIndex] = _tallyResult; + } } diff --git a/packages/contracts/tasks/helpers/Prover.ts b/packages/contracts/tasks/helpers/Prover.ts index 0d634a469e..f5ce9578f6 100644 --- a/packages/contracts/tasks/helpers/Prover.ts +++ b/packages/contracts/tasks/helpers/Prover.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console, no-await-in-loop */ import { STATE_TREE_ARITY, MESSAGE_TREE_ARITY } from "maci-core"; -import { G1Point, G2Point } from "maci-crypto"; +import { G1Point, G2Point, genTreeProof } from "maci-crypto"; import { VerifyingKey } from "maci-domainobjs"; import type { IVerifyingKeyStruct, Proof } from "../../ts/types"; @@ -9,7 +9,7 @@ import type { BigNumberish } from "ethers"; import { asHex, formatProofForVerifierContract } from "../../ts/utils"; -import { IProverParams } from "./types"; +import { IProverParams, TallyData } from "./types"; /** * Prover class is designed to prove message processing and tally proofs on-chain. @@ -219,7 +219,7 @@ export class Prover { * * @param proofs tally proofs */ - async proveTally(proofs: Proof[]): Promise { + async proveTally(proofs: Proof[], tallyData: TallyData): Promise { const [treeDepths, numSignUpsAndMessages, tallyBatchNumber, mode, stateTreeDepth] = await Promise.all([ this.pollContract.treeDepths(), this.pollContract.numSignUpsAndMessages(), @@ -310,6 +310,22 @@ export class Prover { if (tallyBatchNum === totalTallyBatches) { console.log("All vote tallying proofs have been submitted."); } + + const tallyResults = tallyData.results.tally.map((t) => BigInt(t)); + const tallyResultProofs = tallyData.results.tally.map((_, index) => + genTreeProof(index, tallyResults, Number(treeDepths.voteOptionTreeDepth)), + ); + + await this.tallyContract + .addTallyResults( + tallyData.results.tally.map((_, index) => index), + tallyResults, + tallyResultProofs, + tallyData.results.salt, + tallyData.totalSpentVoiceCredits.commitment, + tallyData.perVOSpentVoiceCredits?.commitment ?? 0n, + ) + .then((tx) => tx.wait()); } /** diff --git a/packages/contracts/tasks/runner/prove.ts b/packages/contracts/tasks/runner/prove.ts index afaa141dd3..f093e86454 100644 --- a/packages/contracts/tasks/runner/prove.ts +++ b/packages/contracts/tasks/runner/prove.ts @@ -194,8 +194,9 @@ task("prove", "Command to generate proof and prove the result of a poll on-chain data.processProofs = await proofGenerator.generateMpProofs(); await prover.proveMessageProcessing(data.processProofs); - data.tallyProofs = await proofGenerator.generateTallyProofs(network).then(({ proofs }) => proofs); - await prover.proveTally(data.tallyProofs); + const { proofs: tallyProofs, tallyData } = await proofGenerator.generateTallyProofs(network); + data.tallyProofs = tallyProofs; + await prover.proveTally(data.tallyProofs, tallyData); const endBalance = await signer.provider.getBalance(signer); diff --git a/packages/contracts/tests/Tally.test.ts b/packages/contracts/tests/Tally.test.ts index c6261370f1..34882f8cee 100644 --- a/packages/contracts/tests/Tally.test.ts +++ b/packages/contracts/tests/Tally.test.ts @@ -1,14 +1,14 @@ /* eslint-disable no-underscore-dangle */ import { expect } from "chai"; -import { AbiCoder, Signer } from "ethers"; +import { AbiCoder, BigNumberish, Signer } from "ethers"; import { EthereumProvider } from "hardhat/types"; import { MaciState, Poll, IProcessMessagesCircuitInputs, ITallyCircuitInputs } from "maci-core"; -import { NOTHING_UP_MY_SLEEVE } from "maci-crypto"; +import { genTreeCommitment, genTreeProof, hashLeftRight, NOTHING_UP_MY_SLEEVE } from "maci-crypto"; import { Keypair, Message, PubKey } from "maci-domainobjs"; import { EMode } from "../ts/constants"; import { IVerifyingKeyStruct } from "../ts/types"; -import { getDefaultSigner } from "../ts/utils"; +import { asHex, getDefaultSigner } from "../ts/utils"; import { Tally, MACI, @@ -297,6 +297,36 @@ describe("TallyVotes", () => { await mpContract.processMessages(BigInt(processMessagesInputs.newSbCommitment), [0, 0, 0, 0, 0, 0, 0, 0]); }); + it("should not add tally results if there are no results", async () => { + const tallyData = { + results: { + tally: poll.tallyResult.map((x) => BigInt(x)), + salt: 0n, + commitment: 0n, + }, + totalSpentVoiceCredits: { + spent: poll.totalSpentVoiceCredits.toString(), + salt: 0n, + commitment: 0n, + }, + }; + + const tallyResultProofs = tallyData.results.tally.map((_, index) => + genTreeProof(index, tallyData.results.tally, Number(treeDepths.voteOptionTreeDepth)), + ); + + await expect( + tallyContract.addTallyResults( + tallyData.results.tally.map((_, index) => index), + tallyData.results.tally, + tallyResultProofs, + tallyData.results.salt, + tallyData.totalSpentVoiceCredits.commitment, + 0n, + ), + ).to.be.revertedWithCustomError(tallyContract, "VotesNotTallied"); + }); + it("should tally votes correctly", async () => { let tallyGeneratedInputs: ITallyCircuitInputs; @@ -306,12 +336,95 @@ describe("TallyVotes", () => { await tallyContract.tallyVotes(BigInt(tallyGeneratedInputs.newTallyCommitment), [0, 0, 0, 0, 0, 0, 0, 0]); } + const newResultsCommitment = genTreeCommitment( + poll.tallyResult, + BigInt(asHex(tallyGeneratedInputs!.newResultsRootSalt as BigNumberish)), + treeDepths.voteOptionTreeDepth, + ); + + const newSpentVoiceCreditsCommitment = hashLeftRight( + poll.totalSpentVoiceCredits, + BigInt(asHex(tallyGeneratedInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish)), + ); + + const newPerVOSpentVoiceCreditsCommitment = genTreeCommitment( + poll.perVOSpentVoiceCredits, + BigInt(asHex(tallyGeneratedInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish)), + treeDepths.voteOptionTreeDepth, + ); + + const tallyData = { + results: { + tally: poll.tallyResult.map((x) => BigInt(x)), + salt: asHex(tallyGeneratedInputs!.newResultsRootSalt as BigNumberish), + commitment: asHex(newResultsCommitment), + }, + totalSpentVoiceCredits: { + spent: poll.totalSpentVoiceCredits.toString(), + salt: asHex(tallyGeneratedInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish), + commitment: asHex(newSpentVoiceCreditsCommitment), + }, + }; + + const tallyResultProofs = tallyData.results.tally.map((_, index) => + genTreeProof(index, tallyData.results.tally, Number(treeDepths.voteOptionTreeDepth)), + ); + + const indices = tallyData.results.tally.map((_, index) => index); + + await tallyContract + .addTallyResults( + indices, + tallyData.results.tally, + tallyResultProofs, + tallyData.results.salt, + tallyData.totalSpentVoiceCredits.commitment, + newPerVOSpentVoiceCreditsCommitment, + ) + .then((tx) => tx.wait()); + + const results = await Promise.all(indices.map((index) => tallyContract.tallyResults(index))); + const totalResults = await tallyContract.totalTallyResults(); + + expect(totalResults).to.equal(tallyData.results.tally.length); + expect(results).to.deep.equal(tallyData.results.tally); + const onChainNewTallyCommitment = await tallyContract.tallyCommitment(); expect(tallyGeneratedInputs!.newTallyCommitment).to.eq(onChainNewTallyCommitment.toString()); await expect( tallyContract.tallyVotes(tallyGeneratedInputs!.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), ).to.be.revertedWithCustomError(tallyContract, "AllBallotsTallied"); }); + + it("should not add tally results if there are some invalid proofs", async () => { + const tallyData = { + results: { + tally: poll.tallyResult.map((x) => BigInt(x)), + salt: 0n, + commitment: 0n, + }, + totalSpentVoiceCredits: { + spent: poll.totalSpentVoiceCredits.toString(), + salt: 0n, + commitment: 0n, + }, + }; + + const tallyResultProofs = tallyData.results.tally.map((_, index) => + genTreeProof(index, tallyData.results.tally, Number(treeDepths.voteOptionTreeDepth)), + ); + + await expect( + tallyContract.addTallyResults( + tallyData.results.tally.map((_, index) => index), + tallyData.results.tally, + tallyResultProofs, + tallyData.results.salt, + tallyData.totalSpentVoiceCredits.commitment, + 0n, + ), + ).to.be.revertedWithCustomError(tallyContract, "InvalidTallyVotesProof"); + }); }); describe("ballots > tallyBatchSize", () => { diff --git a/packages/integrationTests/ts/__tests__/integration.test.ts b/packages/integrationTests/ts/__tests__/integration.test.ts index 0739ab581e..6f6e84a1da 100644 --- a/packages/integrationTests/ts/__tests__/integration.test.ts +++ b/packages/integrationTests/ts/__tests__/integration.test.ts @@ -298,6 +298,7 @@ describe("Integration tests", function test() { await expect( proveOnChain({ pollId, + tallyFile: path.resolve(__dirname, "../../../cli/tally.json"), proofDir: path.resolve(__dirname, "../../../cli/proofs"), maciAddress: contracts.maciAddress, signer,