From d4831b8cdca0c08edbf4cdc6edbfcd50d23fb3a8 Mon Sep 17 00:00:00 2001 From: Cvitak Boris <57068961+djanluka@users.noreply.github.com> Date: Wed, 11 Sep 2024 20:50:55 +0200 Subject: [PATCH] feat: anonymous poll joining milestone 2 and 3 (#1750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(poll): add chain hash features BREAKING CHANGE: message processing is changed * fix(ipoll): add missing parameter * fix(poll-tests): add missing parameter maxMessagebatchSize * feat(poll.ts): add chain hash updating * test(poll tests): add test for checking chain hash computation * feat(poll.ts): add batch hashes array computation * feat(poll.sol): pad zeroes to the maximum size of batch * feat(messageprocessor): update process messages to use chain hash * refactor(vkregistry): refactor function call * feat(processmessages.circom): add chainHash feature in circuits and test for that * test(processmessages): rearrange test for key-change * refactor(mergemessages): refactor functions calls which include mergemessages * refactor(mergemessages): add some more changes about functions call which include mergemessages * test(all tests): fixing tests after refactoring code * refactor(accqueue): remove all calls for accqueue * fix(currentmessagebatchindex): fix message batch indexing * refactor(circuit tests): refactor code for circuit testing * test(ceremonyparams.test): correct constants for CeremonyParams test * perf(processmessages.circom + contracts): optimize last batch padding, remove unused inputs * docs(padlastbatch method): update doc comment * docs(poll.ts): remove stale comments * docs(test comments): fix typos * ci(treedepths mock): modify interface for mocked function * fix(ceremony params test): fix circuit inputs * test(messagevalidator): fix function calls for messagevalidator circuit in tests * chore(comments): fix unusefull comments * refactor(poll.sol): replace external contracts with maci only * perf(messageprocessor.sol): hardcode initialization for batchHashes array * docs(comments): fix some more comments * test(test for pr checks): correct some of tests for PR checks * ci: 🎡 renamed old ProcessMessages_10-2-1-2_test * ci: 🎡 correct rapidsnark/build/prover path * style(reviews): solve some reviews for merging * refactor(messageaqq): remove more message merging and message aqq * style(messageaqq): remove more message merging and message aqq * refactor(messageaqq): remove message aqq from subgraph * test(coordinator): hide NOT_MERGED_MESSAGE_TREE error * test(coordinator): fix test about message merging * test(proveonchain): change chainHash calculation * test(proveonchain): fix chainHashes declaration * test(proveonchain): fix chainHash calculation * test(proveonchain): fix chainHashes calculations * test(proveonchain): fix chainHashes calculation * test(proveonchain): fix loop limit * style(review comments): resolve some of review comments * style(review comments): resolve some of review comments * test(lint:ts): fix e2e test because of lint:ts check * docs(wrong changes): fix wrong changes about documentation that is not in our scope * refactor(batchsizes): change batchSizes struct with messageBatchSize variable * refactor(contracts): rollback to provide external contract references * docs(messageprocessor.sol): fix typo * refactor(messagebatchsize): chenge messageBatchSize location from Params.sol to Poll.sol * refactor(maxmessages): remove maxMessages from maxValues * refactor(sltimestemp): remove slTimestamp from circuits * refactor(review comments): resolve more review comments * fix(subgraph): fix bug about maxVoteOptions dunction call * fix(sltimestamp): fix test for removing slTimestap signal * refactor(promise.all): refactor promise.all for only one async call * fix(subgraph): try to fix subgraph build * revert(.nx folder): remove .nx folder from cli folder * fix(merge): tmp-anon-poll-joining merge * fix(merge): tmp-anon-poll-joining merge * test(ceremonyparams): add poll joining in the test * test(processmessages): add poll joining for the test without key-change test * test(polljoining): add poll joining in the test * test(tallyvotes): add poll joining in the test * test(core): add joinPoll function in tests * style(typo): inclusion proof * style(todo): remove finished todo * style(merge): after merge style * style(return): inline return * style(eslint): remove unnecessary eslint-disable * refactor(joiningcircuitargs): add interface IJoiningCircuitArgs * refactor(joinpoll): async read state file * style(genmacisignup): add function description * refactor(gensignuptree): add IGenSignUpTreeArgs interface * style(polljoining): remove extra inlcudes and comments * feat(pollvkkeys): init * feat(vkregistry): separate set functions (process/tally/poll) * test(pollvkkey): adjust test to setPollVkKey * refactor(vkregistry): use setVerifyingKeys in setVerifyingKeysBatch * refactor(poll): add verifier and vkRegystry in constructor * refactor(poll): put verifier and vkRegistry into extContracts * test(core e2e): fix sanity checks test for incorrect signature * refactor(test): removing only from tests * refactor(macistatetree): use LeanIMT instead of QuinTree * refactor(crypto): export hashLeanIMT from index * feat(joinpoll): use genSignUpTree instead of genMaciStateFromContract * feat(joinpoll cli): add optional parameters * test(coordinator): add pollJoiningZkeyPath in app.test * refactor(joinpoll): prettier * test(coordinator): add joinPoll * fix(poll): joiningCircuitInputs with correct siblings, indices and actualStateTreeDepth * test(integration): add joinPoll * build(coordinator): add COORDINATOR_POLL_ZKEY_NAME * refactor(mergestate): remove Maci from MergeState * test(e2e): test:e2e add joinPoll * test(e2e): test:keyChange add joinPoll * docs(complete documentation): complete documentation of the new workflow * docs(documentation): add v3 docs, revert v2 docs * style(docs): prettier * refactor(joinpoll): add generateAndVerifyProof and getStateIndexAndCreditBalance * docs(blogpost): blogpost cuvering the latest updates * docs(blogpost): kudos to our team members! * style(prettier): blog * fix(joinpoll): index value of the user in the state tree leaves * docs(blog): remove poll-joining --------- Co-authored-by: radojevicMihailo Co-authored-by: Aleksandar Veljković --- apps/subgraph/src/poll.ts | 7 +- .../subgraph/templates/subgraph.template.yaml | 6 +- apps/subgraph/tests/poll/poll.test.ts | 10 +- apps/subgraph/tests/poll/utils.ts | 6 +- packages/circuits/circom/circuits.json | 6 + .../circom/core/qv/pollJoining.circom | 81 +++ packages/circuits/package.json | 3 +- .../ts/__tests__/CeremonyParams.test.ts | 44 +- .../circuits/ts/__tests__/PollJoining.test.ts | 153 ++++++ .../ts/__tests__/ProcessMessages.test.ts | 201 +++++--- .../circuits/ts/__tests__/TallyVotes.test.ts | 59 ++- packages/circuits/ts/types.ts | 17 + packages/cli/package.json | 1 + .../ceremony-params/ceremonyParams.test.ts | 2 + packages/cli/tests/constants.ts | 21 +- packages/cli/tests/e2e/e2e.test.ts | 463 +++++++++++++++--- packages/cli/tests/e2e/keyChange.test.ts | 124 +++-- packages/cli/tests/unit/joinPoll.test.ts | 138 ++++++ packages/cli/tests/unit/poll.test.ts | 4 +- packages/cli/tests/unit/publish.test.ts | 4 +- packages/cli/tests/unit/signup.test.ts | 4 +- .../cli/ts/commands/checkVerifyingKeys.ts | 8 + packages/cli/ts/commands/extractVkToFile.ts | 9 +- packages/cli/ts/commands/genLocalState.ts | 2 +- packages/cli/ts/commands/genProofs.ts | 5 +- packages/cli/ts/commands/index.ts | 1 + packages/cli/ts/commands/mergeSignups.ts | 2 +- packages/cli/ts/commands/publish.ts | 10 +- packages/cli/ts/commands/setVerifyingKeys.ts | 47 +- packages/cli/ts/index.ts | 109 +++++ packages/cli/ts/utils/constants.ts | 2 + packages/cli/ts/utils/index.ts | 5 + packages/cli/ts/utils/interfaces.ts | 190 ++++++- packages/contracts/contracts/MACI.sol | 38 +- packages/contracts/contracts/Poll.sol | 108 +++- packages/contracts/contracts/VkRegistry.sol | 117 ++++- .../contracts/contracts/interfaces/IMACI.sol | 9 + .../contracts/contracts/interfaces/IPoll.sol | 13 +- .../contracts/interfaces/IPollFactory.sol | 2 +- .../contracts/interfaces/IVkRegistry.sol | 9 + .../contracts/contracts/trees/LeanIMT.sol | 348 +++++++++++++ packages/contracts/package.json | 3 +- .../tasks/deploy/maci/09-vkRegistry.ts | 6 +- .../contracts/tasks/helpers/TreeMerger.ts | 2 +- packages/contracts/tests/MACI.test.ts | 10 - .../contracts/tests/MessageProcessor.test.ts | 2 +- packages/contracts/tests/Poll.test.ts | 112 ++++- packages/contracts/tests/PollFactory.test.ts | 15 +- packages/contracts/tests/Tally.test.ts | 62 ++- packages/contracts/tests/TallyNonQv.test.ts | 2 +- packages/contracts/tests/VkRegistry.test.ts | 39 +- packages/contracts/tests/constants.ts | 15 + packages/contracts/ts/genMaciState.ts | 41 +- packages/contracts/ts/genSignUpTree.ts | 69 +++ packages/contracts/ts/index.ts | 3 +- packages/contracts/ts/types.ts | 60 ++- packages/core/package.json | 3 +- packages/core/ts/MaciState.ts | 15 +- packages/core/ts/Poll.ts | 179 ++++++- packages/core/ts/__tests__/Poll.test.ts | 166 ++++--- packages/core/ts/__tests__/e2e.test.ts | 287 ++++++----- packages/core/ts/__tests__/utils/utils.ts | 3 + packages/core/ts/index.ts | 7 +- packages/core/ts/utils/types.ts | 32 +- packages/core/ts/utils/utils.ts | 11 + packages/crypto/ts/hashing.ts | 1 + packages/crypto/ts/index.ts | 14 +- .../ts/__tests__/integration.test.ts | 46 +- .../ts/__tests__/utils/constants.ts | 2 +- pnpm-lock.yaml | 85 +++- 70 files changed, 3154 insertions(+), 526 deletions(-) create mode 100644 packages/circuits/circom/core/qv/pollJoining.circom create mode 100644 packages/circuits/ts/__tests__/PollJoining.test.ts create mode 100644 packages/cli/tests/unit/joinPoll.test.ts create mode 100644 packages/contracts/contracts/trees/LeanIMT.sol create mode 100644 packages/contracts/ts/genSignUpTree.ts diff --git a/apps/subgraph/src/poll.ts b/apps/subgraph/src/poll.ts index 82117110c6..d57a1dd010 100644 --- a/apps/subgraph/src/poll.ts +++ b/apps/subgraph/src/poll.ts @@ -1,14 +1,11 @@ /* eslint-disable no-underscore-dangle */ import { Poll, Vote, MACI } from "../generated/schema"; -import { - MergeMaciState as MergeMaciStateEvent, - PublishMessage as PublishMessageEvent, -} from "../generated/templates/Poll/Poll"; +import { MergeState as MergeStateEvent, PublishMessage as PublishMessageEvent } from "../generated/templates/Poll/Poll"; import { ONE_BIG_INT } from "./utils/constants"; -export function handleMergeMaciState(event: MergeMaciStateEvent): void { +export function handleMergeState(event: MergeStateEvent): void { const poll = Poll.load(event.address); if (poll) { diff --git a/apps/subgraph/templates/subgraph.template.yaml b/apps/subgraph/templates/subgraph.template.yaml index b62ff6afee..76b6ac291c 100644 --- a/apps/subgraph/templates/subgraph.template.yaml +++ b/apps/subgraph/templates/subgraph.template.yaml @@ -31,7 +31,7 @@ dataSources: eventHandlers: - event: DeployPoll(uint256,indexed uint256,indexed uint256,uint8) handler: handleDeployPoll - - event: SignUp(uint256,indexed uint256,indexed uint256,uint256,uint256) + - event: SignUp(uint256,indexed uint256,indexed uint256,uint256,uint256,uint256) handler: handleSignUp file: ./src/maci.ts templates: @@ -54,8 +54,8 @@ templates: - name: Poll file: ./node_modules/maci-contracts/build/artifacts/contracts/Poll.sol/Poll.json eventHandlers: - - event: MergeMaciState(indexed uint256,indexed uint256) - handler: handleMergeMaciState + - event: MergeState(indexed uint256,indexed uint256) + handler: handleMergeState - event: PublishMessage((uint256[10]),(uint256,uint256)) handler: handlePublishMessage file: ./src/poll.ts diff --git a/apps/subgraph/tests/poll/poll.test.ts b/apps/subgraph/tests/poll/poll.test.ts index 5caffbb228..20c1f7b9a5 100644 --- a/apps/subgraph/tests/poll/poll.test.ts +++ b/apps/subgraph/tests/poll/poll.test.ts @@ -4,13 +4,13 @@ import { test, describe, afterEach, clearStore, assert, beforeEach } from "match import { MACI, Poll } from "../../generated/schema"; import { handleDeployPoll } from "../../src/maci"; -import { handleMergeMaciState, handlePublishMessage } from "../../src/poll"; +import { handleMergeState, handlePublishMessage } from "../../src/poll"; import { DEFAULT_POLL_ADDRESS, mockPollContract } from "../common"; import { createDeployPollEvent } from "../maci/utils"; -import { createMergeMaciStateEvent, createPublishMessageEvent } from "./utils"; +import { createMergeStateEvent, createPublishMessageEvent } from "./utils"; -export { handleMergeMaciState, handlePublishMessage }; +export { handleMergeState, handlePublishMessage }; describe("Poll", () => { beforeEach(() => { @@ -27,9 +27,9 @@ describe("Poll", () => { }); test("should handle merge maci state properly", () => { - const event = createMergeMaciStateEvent(DEFAULT_POLL_ADDRESS, BigInt.fromI32(1), BigInt.fromI32(3)); + const event = createMergeStateEvent(DEFAULT_POLL_ADDRESS, BigInt.fromI32(1), BigInt.fromI32(3)); - handleMergeMaciState(event); + handleMergeState(event); const poll = Poll.load(event.address)!; const maci = MACI.load(poll.maci)!; diff --git a/apps/subgraph/tests/poll/utils.ts b/apps/subgraph/tests/poll/utils.ts index 118618c0d3..ce3ec3b136 100644 --- a/apps/subgraph/tests/poll/utils.ts +++ b/apps/subgraph/tests/poll/utils.ts @@ -2,10 +2,10 @@ import { Address, BigInt as GraphBN, ethereum } from "@graphprotocol/graph-ts"; // eslint-disable-next-line import/no-extraneous-dependencies import { newMockEvent } from "matchstick-as"; -import { MergeMaciState, PublishMessage } from "../../generated/templates/Poll/Poll"; +import { MergeState, PublishMessage } from "../../generated/templates/Poll/Poll"; -export function createMergeMaciStateEvent(address: Address, stateRoot: GraphBN, numSignups: GraphBN): MergeMaciState { - const event = changetype(newMockEvent()); +export function createMergeStateEvent(address: Address, stateRoot: GraphBN, numSignups: GraphBN): MergeState { + const event = changetype(newMockEvent()); event.parameters.push(new ethereum.EventParam("_stateRoot", ethereum.Value.fromUnsignedBigInt(stateRoot))); event.parameters.push(new ethereum.EventParam("_numSignups", ethereum.Value.fromUnsignedBigInt(numSignups))); diff --git a/packages/circuits/circom/circuits.json b/packages/circuits/circom/circuits.json index 01e0d7b731..459f73cd8d 100644 --- a/packages/circuits/circom/circuits.json +++ b/packages/circuits/circom/circuits.json @@ -1,4 +1,10 @@ { + "PollJoining_10_test": { + "file": "./core/qv/pollJoining", + "template": "PollJoining", + "params": [10], + "pubs": ["inputHash"] + }, "ProcessMessages_10-20-2_test": { "file": "./core/qv/processMessages", "template": "ProcessMessages", diff --git a/packages/circuits/circom/core/qv/pollJoining.circom b/packages/circuits/circom/core/qv/pollJoining.circom new file mode 100644 index 0000000000..9de81f7951 --- /dev/null +++ b/packages/circuits/circom/core/qv/pollJoining.circom @@ -0,0 +1,81 @@ +pragma circom 2.0.0; + + +// circomlib import +include "./mux1.circom"; +// zk-kit imports +include "./safe-comparators.circom"; +// local imports +include "../../utils/hashers.circom"; +include "../../utils/privToPubKey.circom"; +include "../../trees/incrementalMerkleTree.circom"; + +template PollJoining(stateTreeDepth) { + + // Constants defining the structure and size of state. + var STATE_LEAF_LENGTH = 4; + var STATE_TREE_ARITY = 2; + + // Public key IDs. + var STATE_LEAF_PUB_X_IDX = 0; + var STATE_LEAF_PUB_Y_IDX = 1; + // Voice Credit balance id + var STATE_LEAF_VOICE_CREDIT_BALANCE_IDX = 2; + var N_BITS = 252; + + // User's private key + signal input privKey; + // Poll's private key + signal input pollPrivKey; + // Poll's public key + signal input pollPubKey[2]; + // The state leaf and related path elements. + signal input stateLeaf[STATE_LEAF_LENGTH]; + // Siblings + signal input siblings[stateTreeDepth][STATE_TREE_ARITY - 1]; + // Indices + signal input indices[stateTreeDepth]; + // User's hashed private key + signal input nullifier; + // User's credits for poll joining (might be <= oldCredits) + signal input credits; + // MACI State tree root which proves the user is signed up + signal input stateRoot; + // The actual tree depth (might be <= stateTreeDepth) Used in BinaryMerkleRoot + signal input actualStateTreeDepth; + // Public input hash (nullifier, credits, stateRoot) + signal input inputHash; + + // Check public input hash + var computedInputHash = Sha256Hasher(5)([nullifier, credits, stateRoot, pollPubKey[0], pollPubKey[1]]); + inputHash === computedInputHash; + + // User private to public key + var derivedPubKey[2] = PrivToPubKey()(privKey); + derivedPubKey[0] === stateLeaf[STATE_LEAF_PUB_X_IDX]; + derivedPubKey[1] === stateLeaf[STATE_LEAF_PUB_Y_IDX]; + + // Poll private to public key + var derivedPollPubKey[2] = PrivToPubKey()(pollPrivKey); + derivedPollPubKey[0] === pollPubKey[0]; + derivedPollPubKey[1] === pollPubKey[1]; + + // Inclusion proof + var stateLeafHash = PoseidonHasher(4)(stateLeaf); + var stateLeafQip = BinaryMerkleRoot(stateTreeDepth)( + stateLeafHash, + actualStateTreeDepth, + indices, + siblings + ); + + stateLeafQip === stateRoot; + + // Check credits + var isCreditsValid = SafeLessEqThan(N_BITS)([credits, stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX]]); + isCreditsValid === 1; + + // Check nullifier + var hashedPrivKey = PoseidonHasher(1)([privKey]); + hashedPrivKey === nullifier; +} diff --git a/packages/circuits/package.json b/packages/circuits/package.json index 79974158f7..38ddfa55f8 100644 --- a/packages/circuits/package.json +++ b/packages/circuits/package.json @@ -34,7 +34,8 @@ "test:processMessages": "pnpm run mocha-test ts/__tests__/ProcessMessages.test.ts", "test:tallyVotes": "pnpm run mocha-test ts/__tests__/TallyVotes.test.ts", "test:ceremonyParams": "pnpm run mocha-test ts/__tests__/CeremonyParams.test.ts", - "test:incrementalQuinaryTree": "pnpm run mocha-test ts/__tests__/IncrementalQuinaryTree.test.ts" + "test:incrementalQuinaryTree": "pnpm run mocha-test ts/__tests__/IncrementalQuinaryTree.test.ts", + "test:pollJoining": "pnpm run mocha-test ts/__tests__/PollJoining.test.ts" }, "dependencies": { "@zk-kit/circuits": "^0.4.0", diff --git a/packages/circuits/ts/__tests__/CeremonyParams.test.ts b/packages/circuits/ts/__tests__/CeremonyParams.test.ts index a85b942352..7d215b6702 100644 --- a/packages/circuits/ts/__tests__/CeremonyParams.test.ts +++ b/packages/circuits/ts/__tests__/CeremonyParams.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { type WitnessTester } from "circomkit"; import { MaciState, Poll, STATE_TREE_ARITY, VOTE_OPTION_TREE_ARITY, MESSAGE_BATCH_SIZE } from "maci-core"; -import { hash5, IncrementalQuinTree } from "maci-crypto"; +import { hash5, IncrementalQuinTree, poseidon } from "maci-crypto"; import { PrivKey, Keypair, PCommand, Message, Ballot } from "maci-domainobjs"; import { IProcessMessagesInputs, ITallyVotesInputs } from "../types"; @@ -85,9 +85,7 @@ describe("Ceremony param tests", () => { before(() => { // Sign up and publish const userKeypair = new Keypair(new PrivKey(BigInt(1))); - stateIndex = BigInt( - maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))), - ); + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), @@ -102,17 +100,26 @@ describe("Ceremony param tests", () => { // update the state poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); + // First command (valid) const command = new PCommand( stateIndex, // BigInt(1), - userKeypair.pubKey, + pollPubKey, voteOptionIndex, // voteOptionIndex, voteWeight, // vote weight BigInt(2), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -131,7 +138,7 @@ describe("Ceremony param tests", () => { BigInt(1), // nonce BigInt(pollId), ); - const signature2 = command2.sign(userKeypair.privKey); + const signature2 = command2.sign(pollPrivKey); const ecdhKeypair2 = new Keypair(); const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); @@ -148,11 +155,11 @@ describe("Ceremony param tests", () => { const ballotTree = new IncrementalQuinTree(params.stateTreeDepth, emptyBallot.hash(), STATE_TREE_ARITY, hash5); ballotTree.insert(emptyBallot.hash()); - poll.stateLeaves.forEach(() => { + poll.pollStateLeaves.forEach(() => { ballotTree.insert(emptyBallotHash); }); - const currentStateRoot = poll.stateTree?.root; + const currentStateRoot = poll.pollStateLeaves?.root; const currentBallotRoot = ballotTree.root; const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; @@ -163,7 +170,7 @@ describe("Ceremony param tests", () => { // The new roots, which should differ, since at least one of the // messages modified a Ballot or State Leaf - const newStateRoot = poll.stateTree?.root; + const newStateRoot = poll.pollStateLeaves?.root; const newBallotRoot = poll.ballotTree?.root; expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString()); @@ -222,9 +229,7 @@ describe("Ceremony param tests", () => { const commands: PCommand[] = []; // Sign up and publish const userKeypair = new Keypair(); - stateIndex = BigInt( - maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))), - ); + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), @@ -239,17 +244,26 @@ describe("Ceremony param tests", () => { // update the state poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); + // First command (valid) const command = new PCommand( stateIndex, - userKeypair.pubKey, + pollPubKey, voteOptionIndex, // voteOptionIndex, voteWeight, // vote weight BigInt(1), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); diff --git a/packages/circuits/ts/__tests__/PollJoining.test.ts b/packages/circuits/ts/__tests__/PollJoining.test.ts new file mode 100644 index 0000000000..677f9c7932 --- /dev/null +++ b/packages/circuits/ts/__tests__/PollJoining.test.ts @@ -0,0 +1,153 @@ +import { expect } from "chai"; +import { type WitnessTester } from "circomkit"; +import { MaciState, Poll } from "maci-core"; +import { poseidon } from "maci-crypto"; +import { Keypair, Message, PCommand } from "maci-domainobjs"; + +import { IPollJoiningInputs } from "../types"; + +import { + STATE_TREE_DEPTH, + duration, + maxValues, + messageBatchSize, + treeDepths, + voiceCreditBalance, +} from "./utils/constants"; +import { circomkitInstance } from "./utils/utils"; + +describe("Poll Joining circuit", function test() { + this.timeout(900000); + const NUM_USERS = 50; + + const coordinatorKeypair = new Keypair(); + + type PollJoiningCircuitInputs = [ + "privKey", + "pollPrivKey", + "pollPubKey", + "stateLeaf", + "siblings", + "indices", + "nullifier", + "credits", + "stateRoot", + "actualStateTreeDepth", + "inputHash", + ]; + + let circuit: WitnessTester; + + before(async () => { + circuit = await circomkitInstance.WitnessTester("pollJoining", { + file: "./core/qv/pollJoining", + template: "PollJoining", + params: [STATE_TREE_DEPTH], + }); + }); + + describe(`${NUM_USERS} users, 1 join`, () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + let pollId: bigint; + let poll: Poll; + let users: Keypair[]; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + const messages: Message[] = []; + const commands: PCommand[] = []; + + before(() => { + // Sign up + users = new Array(NUM_USERS).fill(0).map(() => new Keypair()); + + users.forEach((userKeypair) => { + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); + }); + + pollId = maciState.deployPoll( + BigInt(Math.floor(Date.now() / 1000) + duration), + maxValues.maxVoteOptions, + treeDepths, + messageBatchSize, + coordinatorKeypair, + ); + + poll = maciState.polls.get(pollId)!; + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // Join the poll + const { privKey } = users[0]; + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + const stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); + + // First command (valid) + const command = new PCommand( + stateIndex, + pollPubKey, + BigInt(0), // voteOptionIndex, + BigInt(9), // vote weight + BigInt(1), // nonce + BigInt(pollId), + ); + + const signature = command.sign(pollPrivKey); + + const ecdhKeypair = new Keypair(); + const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); + const message = command.encrypt(signature, sharedKey); + messages.push(message); + commands.push(command); + + poll.publishMessage(message, ecdhKeypair.pubKey); + + // Process messages + poll.processMessages(pollId); + }); + + it("should produce a proof", async () => { + const privateKey = users[0].privKey; + const stateLeafIndex = BigInt(1); + const credits = BigInt(10); + + const inputs = poll.joiningCircuitInputs({ + maciPrivKey: privateKey, + stateLeafIndex, + credits, + pollPrivKey, + pollPubKey, + }) as unknown as IPollJoiningInputs; + const witness = await circuit.calculateWitness(inputs); + await circuit.expectConstraintPass(witness); + }); + + it("should fail for fake witness", async () => { + const privateKey = users[0].privKey; + const stateLeafIndex = BigInt(1); + const credits = BigInt(10); + + const inputs = poll.joiningCircuitInputs({ + maciPrivKey: privateKey, + stateLeafIndex, + credits, + pollPrivKey, + pollPubKey, + }) as unknown as IPollJoiningInputs; + const witness = await circuit.calculateWitness(inputs); + + const fakeWitness = Array(witness.length).fill(1n) as bigint[]; + await circuit.expectConstraintFail(fakeWitness); + }); + + it("should fail for improper credits", () => { + const privateKey = users[0].privKey; + const stateLeafIndex = BigInt(1); + const credits = BigInt(105); + + expect(() => + poll.joiningCircuitInputs({ maciPrivKey: privateKey, stateLeafIndex, credits, pollPrivKey, pollPubKey }), + ).to.throw("Credits must be lower than signed up credits"); + }); + }); +}); diff --git a/packages/circuits/ts/__tests__/ProcessMessages.test.ts b/packages/circuits/ts/__tests__/ProcessMessages.test.ts index d95ad85799..89b7ef316c 100644 --- a/packages/circuits/ts/__tests__/ProcessMessages.test.ts +++ b/packages/circuits/ts/__tests__/ProcessMessages.test.ts @@ -1,12 +1,19 @@ import { expect } from "chai"; import { type WitnessTester } from "circomkit"; -import { MaciState, MESSAGE_TREE_ARITY, Poll, STATE_TREE_ARITY } from "maci-core"; -import { IncrementalQuinTree, hash2 } from "maci-crypto"; +import { MaciState, Poll, STATE_TREE_ARITY } from "maci-core"; +import { IncrementalQuinTree, hash2, poseidon } from "maci-crypto"; import { PrivKey, Keypair, PCommand, Message, Ballot, PubKey } from "maci-domainobjs"; import { IProcessMessagesInputs } from "../types"; -import { STATE_TREE_DEPTH, duration, maxValues, messageBatchSize, treeDepths, voiceCreditBalance } from "./utils/constants"; +import { + STATE_TREE_DEPTH, + duration, + maxValues, + messageBatchSize, + treeDepths, + voiceCreditBalance, +} from "./utils/constants"; import { circomkitInstance } from "./utils/utils"; describe("ProcessMessage circuit", function test() { @@ -69,9 +76,11 @@ describe("ProcessMessage circuit", function test() { before(() => { // Sign up and publish const users = new Array(5).fill(0).map(() => new Keypair()); + const pollKeys: Keypair[] = []; users.forEach((userKeypair) => { maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); + pollKeys.push(new Keypair()); }); pollId = maciState.deployPoll( @@ -85,6 +94,17 @@ describe("ProcessMessage circuit", function test() { poll = maciState.polls.get(pollId)!; poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + for (let i = 0; i < users.length; i += 1) { + const { privKey } = users[i]; + const { pubKey: pollPubKey } = pollKeys[i]; + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp); + } + const nothing = new Message([ 8370432830353022751713833565135785980866757267633941821328460903436894336785n, 0n, @@ -108,14 +128,14 @@ describe("ProcessMessage circuit", function test() { // First command (valid) const command = new PCommand( 5n, - users[4].pubKey, + pollKeys[4].pubKey, voteOptionIndex, // voteOptionIndex, voteWeight, // vote weight BigInt(2), // nonce BigInt(pollId), ); - const signature = command.sign(users[4].privKey); + const signature = command.sign(pollKeys[4].privKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -148,9 +168,7 @@ describe("ProcessMessage circuit", function test() { before(() => { // Sign up and publish const userKeypair = new Keypair(new PrivKey(BigInt(1))); - stateIndex = BigInt( - maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))), - ); + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), @@ -163,17 +181,26 @@ describe("ProcessMessage circuit", function test() { poll = maciState.polls.get(pollId)!; poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); + // First command (valid) const command = new PCommand( stateIndex, // BigInt(1), - userKeypair.pubKey, + pollPubKey, voteOptionIndex, // voteOptionIndex, voteWeight, // vote weight BigInt(2), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -186,13 +213,13 @@ describe("ProcessMessage circuit", function test() { // Second command (valid) const command2 = new PCommand( stateIndex, - userKeypair.pubKey, + pollPubKey, voteOptionIndex, // voteOptionIndex, BigInt(1), // vote weight BigInt(1), // nonce BigInt(pollId), ); - const signature2 = command2.sign(userKeypair.privKey); + const signature2 = command2.sign(pollPrivKey); const ecdhKeypair2 = new Keypair(); const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); @@ -214,7 +241,7 @@ describe("ProcessMessage circuit", function test() { ballotTree.insert(emptyBallotHash); }); - const currentStateRoot = poll.stateTree?.root; + const currentStateRoot = poll.pollStateTree?.root; const currentBallotRoot = ballotTree.root; const inputs = poll.processMessages(pollId, false) as unknown as IProcessMessagesInputs; @@ -225,7 +252,7 @@ describe("ProcessMessage circuit", function test() { // The new roots, which should differ, since at least one of the // messages modified a Ballot or State Leaf - const newStateRoot = poll.stateTree?.root; + const newStateRoot = poll.pollStateTree?.root; const newBallotRoot = poll.ballotTree?.root; expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString()); @@ -268,16 +295,25 @@ describe("ProcessMessage circuit", function test() { poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); + const command = new PCommand( - BigInt(1), - userKeypair.pubKey, + stateIndex, + pollPubKey, BigInt(0), // voteOptionIndex, BigInt(1), // vote weight BigInt(1), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -300,7 +336,7 @@ describe("ProcessMessage circuit", function test() { ballotTree.insert(emptyBallotHash); }); - const currentStateRoot = poll.stateTree?.root; + const currentStateRoot = poll.pollStateTree?.root; const currentBallotRoot = ballotTree.root; const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; @@ -310,7 +346,7 @@ describe("ProcessMessage circuit", function test() { // The new roots, which should differ, since at least one of the // messages modified a Ballot or State Leaf - const newStateRoot = poll.stateTree?.root; + const newStateRoot = poll.pollStateTree?.root; const newBallotRoot = poll.ballotTree?.root; expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString()); @@ -321,7 +357,6 @@ describe("ProcessMessage circuit", function test() { describe("4) 1 user, key-change", () => { const maciState = new MaciState(STATE_TREE_DEPTH); const voteWeight = BigInt(9); - let stateIndex: number; let pollId: bigint; let poll: Poll; const messages: Message[] = []; @@ -330,9 +365,8 @@ describe("ProcessMessage circuit", function test() { before(() => { // Sign up and publish const userKeypair = new Keypair(new PrivKey(BigInt(123))); - const userKeypair2 = new Keypair(new PrivKey(BigInt(456))); - stateIndex = maciState.signUp( + maciState.signUp( userKeypair.pubKey, voiceCreditBalance, BigInt(1), // BigInt(Math.floor(Date.now() / 1000)), @@ -350,17 +384,27 @@ describe("ProcessMessage circuit", function test() { poll.updatePoll(BigInt(maciState.stateLeaves.length)); + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const stateIndex = poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp); + + const { privKey: pollPrivKey2, pubKey: pollPubKey2 } = new Keypair(); + // Vote for option 0 const command = new PCommand( BigInt(stateIndex), // BigInt(1), - userKeypair.pubKey, + pollPubKey, BigInt(0), // voteOptionIndex, voteWeight, // vote weight BigInt(1), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -373,13 +417,13 @@ describe("ProcessMessage circuit", function test() { // Vote for option 1 const command2 = new PCommand( BigInt(stateIndex), - userKeypair2.pubKey, + pollPubKey, BigInt(1), // voteOptionIndex, voteWeight, // vote weight BigInt(2), // nonce BigInt(pollId), ); - const signature2 = command2.sign(userKeypair2.privKey); + const signature2 = command2.sign(pollPrivKey); const ecdhKeypair2 = new Keypair(); const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); @@ -391,14 +435,14 @@ describe("ProcessMessage circuit", function test() { // Change key const command3 = new PCommand( BigInt(stateIndex), // BigInt(1), - userKeypair2.pubKey, + pollPubKey2, BigInt(1), // voteOptionIndex, BigInt(0), // vote weight BigInt(1), // nonce BigInt(pollId), ); - const signature3 = command3.sign(userKeypair.privKey); + const signature3 = command3.sign(pollPrivKey2); const ecdhKeypair3 = new Keypair(); const sharedKey3 = Keypair.genEcdhSharedKey(ecdhKeypair3.privKey, coordinatorKeypair.pubKey); @@ -420,7 +464,7 @@ describe("ProcessMessage circuit", function test() { ballotTree.insert(emptyBallotHash); }); - const currentStateRoot = poll.stateTree?.root; + const currentStateRoot = poll.pollStateTree?.root; const currentBallotRoot = ballotTree.root; const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; @@ -430,7 +474,8 @@ describe("ProcessMessage circuit", function test() { // The new roots, which should differ, since at least one of the // messages modified a Ballot or State Leaf - const newStateRoot = poll.stateTree?.root; + + const newStateRoot = poll.pollStateTree?.root; const newBallotRoot = poll.ballotTree?.root; expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString()); @@ -447,7 +492,7 @@ describe("ProcessMessage circuit", function test() { before(() => { const userKeypair = new Keypair(new PrivKey(BigInt(1))); - stateIndex = maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); // Sign up and publish pollId = maciState.deployPoll( @@ -462,19 +507,28 @@ describe("ProcessMessage circuit", function test() { poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + stateIndex = poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp); + // Second batch is not a full batch const numMessages = messageBatchSize * NUM_BATCHES - 1; for (let i = 0; i < numMessages; i += 1) { const command = new PCommand( BigInt(stateIndex), - userKeypair.pubKey, + pollPubKey, BigInt(i), // vote option index BigInt(1), // vote weight BigInt(numMessages - i), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -506,10 +560,7 @@ describe("ProcessMessage circuit", function test() { before(() => { // Sign up and publish const userKeypair = new Keypair(new PrivKey(BigInt(1))); - stateIndex = BigInt( - maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))), - ); - + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), maxValues.maxVoteOptions, @@ -521,6 +572,15 @@ describe("ProcessMessage circuit", function test() { poll = maciState.polls.get(pollId)!; poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); + const nothing = new Message([ 8370432830353022751713833565135785980866757267633941821328460903436894336785n, 0n, @@ -544,14 +604,14 @@ describe("ProcessMessage circuit", function test() { // First command (valid) const command = new PCommand( stateIndex, // BigInt(1), - userKeypair.pubKey, + pollPubKey, 1n, // voteOptionIndex, 2n, // vote weight 2n, // nonce pollId, ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -564,13 +624,13 @@ describe("ProcessMessage circuit", function test() { // Second command (valid) const command2 = new PCommand( stateIndex, - userKeypair.pubKey, + pollPubKey, voteOptionIndex, // voteOptionIndex, 9n, // vote weight 9 ** 2 = 81 1n, // nonce pollId, ); - const signature2 = command2.sign(userKeypair.privKey); + const signature2 = command2.sign(pollPrivKey); const ecdhKeypair2 = new Keypair(); const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); @@ -592,7 +652,7 @@ describe("ProcessMessage circuit", function test() { ballotTree.insert(emptyBallotHash); }); - const currentStateRoot = poll.stateTree?.root; + const currentStateRoot = poll.pollStateTree?.root; const currentBallotRoot = ballotTree.root; const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; @@ -603,7 +663,7 @@ describe("ProcessMessage circuit", function test() { // The new roots, which should differ, since at least one of the // messages modified a Ballot or State Leaf - const newStateRoot = poll.stateTree?.root; + const newStateRoot = poll.pollStateTree?.root; const newBallotRoot = poll.ballotTree?.root; expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString()); @@ -623,9 +683,7 @@ describe("ProcessMessage circuit", function test() { before(() => { // Sign up and publish const userKeypair = new Keypair(new PrivKey(BigInt(1))); - stateIndex = BigInt( - maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))), - ); + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), @@ -638,6 +696,15 @@ describe("ProcessMessage circuit", function test() { poll = maciState.polls.get(pollId)!; poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); + const nothing = new Message([ 8370432830353022751713833565135785980866757267633941821328460903436894336785n, 0n, @@ -661,14 +728,14 @@ describe("ProcessMessage circuit", function test() { // First command (valid) const command = new PCommand( stateIndex, // BigInt(1), - userKeypair.pubKey, + pollPubKey, 1n, // voteOptionIndex, 2n, // vote weight 2n, // nonce pollId, ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -686,13 +753,13 @@ describe("ProcessMessage circuit", function test() { // Second command (valid) in second batch (which is first due to reverse processing) const command2 = new PCommand( stateIndex, - userKeypair.pubKey, + pollPubKey, voteOptionIndex, // voteOptionIndex, 9n, // vote weight 9 ** 2 = 81 1n, // nonce pollId, ); - const signature2 = command2.sign(userKeypair.privKey); + const signature2 = command2.sign(pollPrivKey); const ecdhKeypair2 = new Keypair(); const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); @@ -715,7 +782,7 @@ describe("ProcessMessage circuit", function test() { }); while (poll.hasUnprocessedMessages()) { - const currentStateRoot = poll.stateTree?.root; + const currentStateRoot = poll.pollStateTree?.root; const currentBallotRoot = ballotTree.root; const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; @@ -727,7 +794,7 @@ describe("ProcessMessage circuit", function test() { // The new roots, which should differ, since at least one of the // messages modified a Ballot or State Leaf - const newStateRoot = poll.stateTree?.root; + const newStateRoot = poll.pollStateTree?.root; const newBallotRoot = poll.ballotTree?.root; expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString()); @@ -748,10 +815,7 @@ describe("ProcessMessage circuit", function test() { before(() => { // Sign up and publish const userKeypair = new Keypair(new PrivKey(BigInt(1))); - stateIndex = BigInt( - maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))), - ); - + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), maxValues.maxVoteOptions, @@ -763,6 +827,15 @@ describe("ProcessMessage circuit", function test() { poll = maciState.polls.get(pollId)!; poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); + const nothing = new Message([ 8370432830353022751713833565135785980866757267633941821328460903436894336785n, 0n, @@ -785,14 +858,14 @@ describe("ProcessMessage circuit", function test() { const commandFinal = new PCommand( stateIndex, // BigInt(1), - userKeypair.pubKey, + pollPubKey, 1n, // voteOptionIndex, 1n, // vote weight 3n, // nonce pollId, ); - const signatureFinal = commandFinal.sign(userKeypair.privKey); + const signatureFinal = commandFinal.sign(pollPrivKey); const ecdhKeypairFinal = new Keypair(); const sharedKeyFinal = Keypair.genEcdhSharedKey(ecdhKeypairFinal.privKey, coordinatorKeypair.pubKey); @@ -805,14 +878,14 @@ describe("ProcessMessage circuit", function test() { // First command (valid) const command = new PCommand( stateIndex, // BigInt(1), - userKeypair.pubKey, + pollPubKey, 1n, // voteOptionIndex, 2n, // vote weight 2n, // nonce pollId, ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -830,13 +903,13 @@ describe("ProcessMessage circuit", function test() { // Second command (valid) in second batch (which is first due to reverse processing) const command2 = new PCommand( stateIndex, - userKeypair.pubKey, + pollPubKey, voteOptionIndex, // voteOptionIndex, 9n, // vote weight 9 ** 2 = 81 1n, // nonce pollId, ); - const signature2 = command2.sign(userKeypair.privKey); + const signature2 = command2.sign(pollPrivKey); const ecdhKeypair2 = new Keypair(); const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); @@ -859,7 +932,7 @@ describe("ProcessMessage circuit", function test() { }); while (poll.hasUnprocessedMessages()) { - const currentStateRoot = poll.stateTree?.root; + const currentStateRoot = poll.pollStateTree?.root; const currentBallotRoot = ballotTree.root; const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; @@ -871,7 +944,7 @@ describe("ProcessMessage circuit", function test() { // The new roots, which should differ, since at least one of the // messages modified a Ballot or State Leaf - const newStateRoot = poll.stateTree?.root; + const newStateRoot = poll.pollStateTree?.root; const newBallotRoot = poll.ballotTree?.root; expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString()); diff --git a/packages/circuits/ts/__tests__/TallyVotes.test.ts b/packages/circuits/ts/__tests__/TallyVotes.test.ts index 1ee38363ac..7003dc56de 100644 --- a/packages/circuits/ts/__tests__/TallyVotes.test.ts +++ b/packages/circuits/ts/__tests__/TallyVotes.test.ts @@ -1,5 +1,6 @@ import { type WitnessTester } from "circomkit"; import { MaciState, Poll } from "maci-core"; +import { poseidon } from "maci-crypto"; import { Keypair, PCommand, Message } from "maci-domainobjs"; import { ITallyVotesInputs } from "../types"; @@ -72,9 +73,7 @@ describe("TallyVotes circuit", function test() { const commands: PCommand[] = []; // Sign up and publish const userKeypair = new Keypair(); - stateIndex = BigInt( - maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))), - ); + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), @@ -85,19 +84,28 @@ describe("TallyVotes circuit", function test() { ); poll = maciState.polls.get(pollId)!; - poll.updatePoll(stateIndex); + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); // First command (valid) const command = new PCommand( stateIndex, - userKeypair.pubKey, + pollPubKey, voteOptionIndex, // voteOptionIndex, voteWeight, // vote weight BigInt(1), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -146,9 +154,7 @@ describe("TallyVotes circuit", function test() { const commands: PCommand[] = []; // Sign up and publish const userKeypair = new Keypair(); - stateIndex = BigInt( - maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))), - ); + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), @@ -159,19 +165,28 @@ describe("TallyVotes circuit", function test() { ); poll = maciState.polls.get(pollId)!; - poll.updatePoll(stateIndex); + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // Join the poll + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp)); // First command (valid) const command = new PCommand( stateIndex, - userKeypair.pubKey, + pollPubKey, voteOptionIndex, // voteOptionIndex, voteWeight, // vote weight BigInt(1), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -214,12 +229,17 @@ describe("TallyVotes circuit", function test() { it("should produce the correct state root and ballot root", async () => { const maciState = new MaciState(STATE_TREE_DEPTH); const userKeypairs: Keypair[] = []; + const pollKeypairs: Keypair[] = []; + + // Sign up for (let i = 0; i < x; i += 1) { const k = new Keypair(); userKeypairs.push(k); + pollKeypairs.push(new Keypair()); maciState.signUp(k.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000) + duration)); } + // Deploy poll const pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), maxValues.maxVoteOptions, @@ -231,18 +251,29 @@ describe("TallyVotes circuit", function test() { const poll = maciState.polls.get(pollId)!; poll.updatePoll(BigInt(maciState.stateLeaves.length)); + // Join the poll + for (let i = 0; i < x; i += 1) { + const { privKey } = userKeypairs[i]; + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + + poll.joinPoll(nullifier, pollKeypairs[i].pubKey, voiceCreditBalance, timestamp); + } + + // Commands const numMessages = messageBatchSize * NUM_BATCHES; for (let i = 0; i < numMessages; i += 1) { const command = new PCommand( BigInt(i), - userKeypairs[i].pubKey, + pollKeypairs[i].pubKey, BigInt(i), // vote option index BigInt(1), // vote weight BigInt(1), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypairs[i].privKey); + const signature = command.sign(pollKeypairs[i].privKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); diff --git a/packages/circuits/ts/types.ts b/packages/circuits/ts/types.ts index de265390eb..87f2fb24f0 100644 --- a/packages/circuits/ts/types.ts +++ b/packages/circuits/ts/types.ts @@ -40,6 +40,23 @@ export interface IGenProofOptions { silent?: boolean; } +/** + * Inputs for circuit PollJoining + */ +export interface IPollJoiningInputs { + privKey: bigint; + pollPrivKey: bigint; + pollPubKey: bigint[][]; + stateLeaf: bigint[]; + siblings: bigint[][]; + indices: bigint[]; + nullifier: bigint; + credits: bigint; + stateRoot: bigint; + actualStateTreeDepth: bigint; + inputHash: bigint; +} + /** * Inputs for circuit ProcessMessages */ diff --git a/packages/cli/package.json b/packages/cli/package.json index c7bd838da5..7df295c8d4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -35,6 +35,7 @@ "test:airdrop": "nyc ts-mocha --exit tests/unit/airdrop.test.ts", "test:genPubKey": "ts-mocha --exit tests/unit/genPubKey.test.ts", "test:genKeypair": "ts-mocha --exit tests/unit/genKeyPair.test.ts", + "test:joinPoll": "ts-mocha --exit tests/unit/joinPoll.test.ts", "test:timeTravel": "ts-mocha --exit tests/unit/timeTravel.test.ts", "test:fundWallet": "ts-mocha --exit tests/unit/fundWallet.test.ts", "test:signup": "ts-mocha --exit tests/unit/signup.test.ts", diff --git a/packages/cli/tests/ceremony-params/ceremonyParams.test.ts b/packages/cli/tests/ceremony-params/ceremonyParams.test.ts index c5398cdd87..5de069dc31 100644 --- a/packages/cli/tests/ceremony-params/ceremonyParams.test.ts +++ b/packages/cli/tests/ceremony-params/ceremonyParams.test.ts @@ -43,6 +43,7 @@ import { ceremonyTallyVotesNonQvZkeyPath, ceremonyProcessMessagesNonQvWasmPath, ceremonyTallyVotesNonQvWasmPath, + ceremonyPollJoiningZkeyPath, } from "../constants"; import { clean, isArm } from "../utils"; @@ -66,6 +67,7 @@ describe("Stress tests with ceremony params (6,3,2,20)", function test() { intStateTreeDepth, voteOptionTreeDepth, messageBatchSize, + pollJoiningZkeyPath: ceremonyPollJoiningZkeyPath, processMessagesZkeyPathQv: ceremonyProcessMessagesZkeyPath, tallyVotesZkeyPathQv: ceremonyTallyVotesZkeyPath, processMessagesZkeyPathNonQv: ceremonyProcessMessagesNonQvZkeyPath, diff --git a/packages/cli/tests/constants.ts b/packages/cli/tests/constants.ts index acd1a29b1e..14397f0067 100644 --- a/packages/cli/tests/constants.ts +++ b/packages/cli/tests/constants.ts @@ -22,6 +22,8 @@ export const MESSAGE_BATCH_SIZE = 20; const coordinatorKeypair = new Keypair(); export const coordinatorPubKey = coordinatorKeypair.pubKey.serialize(); export const coordinatorPrivKey = coordinatorKeypair.privKey.serialize(); + +export const pollJoiningTestZkeyPath = "./zkeys/PollJoining_10_test/PollJoining_10_test.0.zkey"; export const processMessageTestZkeyPath = "./zkeys/ProcessMessages_10-20-2_test/ProcessMessages_10-20-2_test.0.zkey"; export const tallyVotesTestZkeyPath = "./zkeys/TallyVotes_10-1-2_test/TallyVotes_10-1-2_test.0.zkey"; export const processMessageTestNonQvZkeyPath = @@ -29,6 +31,7 @@ export const processMessageTestNonQvZkeyPath = export const tallyVotesTestNonQvZkeyPath = "./zkeys/TallyVotesNonQv_10-1-2_test/TallyVotesNonQv_10-1-2_test.0.zkey"; export const testTallyFilePath = "./tally.json"; export const testProofsDirPath = "./proofs"; +export const testPollJoiningWitnessPath = "./zkeys/PollJoining_10_test/PollJoining_10_test_cpp/PollJoining_10_test"; export const testProcessMessagesWitnessPath = "./zkeys/ProcessMessages_10-20-2_test/ProcessMessages_10-20-2_test_cpp/ProcessMessages_10-20-2_test"; export const testProcessMessagesWitnessDatPath = @@ -37,16 +40,19 @@ export const testTallyVotesWitnessPath = "./zkeys/TallyVotes_10-1-2_test/TallyVotes_10-1-2_test_cpp/TallyVotes_10-1-2_test"; export const testTallyVotesWitnessDatPath = "./zkeys/TallyVotes_10-1-2_test/TallyVotes_10-1-2_test_cpp/TallyVotes_10-1-2_test.dat"; +export const testPollJoiningWasmPath = "./zkeys/PollJoining_10_test/PollJoining_10_test_js/PollJoining_10_test.wasm"; export const testProcessMessagesWasmPath = "./zkeys/ProcessMessages_10-20-2_test/ProcessMessages_10-20-2_test_js/ProcessMessages_10-20-2_test.wasm"; export const testTallyVotesWasmPath = "./zkeys/TallyVotes_10-1-2_test/TallyVotes_10-1-2_test_js/TallyVotes_10-1-2_test.wasm"; export const testRapidsnarkPath = `${homedir()}/rapidsnark/build/prover`; -export const ceremonyProcessMessagesZkeyPath = "./zkeys/ProcessMessages_14-9-2-3/processmessages_14-9-2-3.zkey"; +export const ceremonyPollJoiningZkeyPath = "./zkeys/PollJoining_10/pollJoining_10.zkey"; +export const ceremonyProcessMessagesZkeyPath = "./zkeys/ProcessMessages_6-9-2-3/processMessages_6-9-2-3.zkey"; export const ceremonyProcessMessagesNonQvZkeyPath = - "./zkeys/ProcessMessagesNonQv_14-9-2-3/processmessagesnonqv_14-9-2-3.zkey"; -export const ceremonyTallyVotesZkeyPath = "./zkeys/TallyVotes_14-5-3/tallyvotes_14-5-3.zkey"; -export const ceremonyTallyVotesNonQvZkeyPath = "./zkeys/TallyVotesNonQv_14-5-3/tallyvotesnonqv_14-5-3.zkey"; + "./zkeys/ProcessMessagesNonQv_6-9-2-3/processMessagesNonQv_6-9-2-3.zkey"; +export const ceremonyTallyVotesZkeyPath = "./zkeys/TallyVotes_6-2-3/tallyVotes_6-2-3.zkey"; +export const ceremonyTallyVotesNonQvZkeyPath = "./zkeys/TallyVotesNonQv_6-2-3/tallyVotesNonQv_6-2-3.zkey"; +export const ceremonyPollJoiningWitnessPath = "./zkeys/PollJoining/PollJoining_10_cpp/PollJoining_10"; export const ceremonyProcessMessagesWitnessPath = "./zkeys/ProcessMessages_14-9-2-3/ProcessMessages_14-9-2-3_cpp/ProcessMessages_14-9-2-3"; export const ceremonyProcessMessagesNonQvWitnessPath = @@ -81,7 +87,9 @@ export const testProcessMessagesNonQvWasmPath = export const testTallyVotesNonQvWasmPath = "./zkeys/TallyVotesNonQv_10-1-2_test/TallyVotesNonQv_10-1-2_test_js/TallyVotesNonQv_10-1-2_test.wasm"; -export const pollDuration = 90; +export const pollDuration = 2000; +export const maxMessages = 25; +export const maxVoteOptions = 25; export const setVerifyingKeysArgs: Omit = { quiet: true, @@ -89,6 +97,7 @@ export const setVerifyingKeysArgs: Omit = { intStateTreeDepth: INT_STATE_TREE_DEPTH, voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH, messageBatchSize: MESSAGE_BATCH_SIZE, + pollJoiningZkeyPath: pollJoiningTestZkeyPath, processMessagesZkeyPathQv: processMessageTestZkeyPath, tallyVotesZkeyPathQv: tallyVotesTestZkeyPath, }; @@ -99,6 +108,7 @@ export const setVerifyingKeysNonQvArgs: Omit = { intStateTreeDepth: INT_STATE_TREE_DEPTH, voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH, messageBatchSize: MESSAGE_BATCH_SIZE, + pollJoiningZkeyPath: pollJoiningTestZkeyPath, processMessagesZkeyPathNonQv: processMessageTestNonQvZkeyPath, tallyVotesZkeyPathNonQv: tallyVotesTestNonQvZkeyPath, }; @@ -108,6 +118,7 @@ export const checkVerifyingKeysArgs: Omit = { intStateTreeDepth: INT_STATE_TREE_DEPTH, voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH, messageBatchSize: MESSAGE_BATCH_SIZE, + pollJoiningZkeyPath: pollJoiningTestZkeyPath, processMessagesZkeyPath: processMessageTestZkeyPath, tallyVotesZkeyPath: tallyVotesTestZkeyPath, }; diff --git a/packages/cli/tests/e2e/e2e.test.ts b/packages/cli/tests/e2e/e2e.test.ts index 47a91099a4..b17ffa4374 100644 --- a/packages/cli/tests/e2e/e2e.test.ts +++ b/packages/cli/tests/e2e/e2e.test.ts @@ -23,6 +23,8 @@ import { verify, isRegisteredUser, getGatekeeperTrait, + joinPoll, + isJoinedUser, } from "../../ts/commands"; import { DeployedContracts, GatekeeperTrait, GenProofsArgs } from "../../ts/utils"; import { @@ -33,10 +35,13 @@ import { proveOnChainArgs, verifyArgs, mergeSignupsArgs, + pollJoiningTestZkeyPath, processMessageTestZkeyPath, setVerifyingKeysArgs, tallyVotesTestZkeyPath, + testPollJoiningWasmPath, testProcessMessagesWasmPath, + testPollJoiningWitnessPath, testProcessMessagesWitnessDatPath, testProcessMessagesWitnessPath, testProofsDirPath, @@ -102,6 +107,7 @@ describe("e2e tests", function test() { }); const user = new Keypair(); + const pollKeys = new Keypair(); before(async () => { // deploy the smart contracts @@ -119,9 +125,27 @@ describe("e2e tests", function test() { await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: user.pubKey.serialize(), signer }); }); + it("should join one user", async () => { + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: user.privKey.serialize(), + pollPrivKey: pollKeys.privKey.serialize(), + stateIndex: 1n, + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 10n, + quiet: true, + }); + }); + it("should publish one message", async () => { await publish({ - pubkey: user.pubKey.serialize(), + pubkey: pollKeys.pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -129,7 +153,7 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: user.privKey.serialize(), + privateKey: pollKeys.privKey.serialize(), signer, }); }); @@ -153,6 +177,7 @@ describe("e2e tests", function test() { }); const user = new Keypair(); + const pollKeys = new Keypair(); before(async () => { // deploy the smart contracts @@ -165,9 +190,27 @@ describe("e2e tests", function test() { await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: user.pubKey.serialize(), signer }); }); + it("should join one user", async () => { + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: user.privKey.serialize(), + pollPrivKey: pollKeys.privKey.serialize(), + stateIndex: 1n, + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 10n, + quiet: true, + }); + }); + it("should publish one message", async () => { await publish({ - pubkey: user.pubKey.serialize(), + pubkey: pollKeys.pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -175,7 +218,7 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: user.privKey.serialize(), + privateKey: pollKeys.privKey.serialize(), signer, }); }); @@ -201,6 +244,7 @@ describe("e2e tests", function test() { }); const users = [new Keypair(), new Keypair(), new Keypair(), new Keypair()]; + const pollKeys = [new Keypair(), new Keypair(), new Keypair(), new Keypair()]; before(async () => { // deploy the smart contracts @@ -217,9 +261,31 @@ describe("e2e tests", function test() { } }); + it("should join four users", async () => { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < users.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: users[i].privKey.serialize(), + pollPrivKey: pollKeys[i].privKey.serialize(), + stateIndex: BigInt(i + 1), + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + } + }); + it("should publish eight messages", async () => { await publish({ - pubkey: users[0].pubKey.serialize(), + pubkey: pollKeys[0].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 2n, @@ -227,11 +293,11 @@ describe("e2e tests", function test() { newVoteWeight: 4n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[0].privKey.serialize(), + privateKey: pollKeys[0].privKey.serialize(), signer, }); await publish({ - pubkey: users[0].pubKey.serialize(), + pubkey: pollKeys[0].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 2n, @@ -239,11 +305,11 @@ describe("e2e tests", function test() { newVoteWeight: 3n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[0].privKey.serialize(), + privateKey: pollKeys[0].privKey.serialize(), signer, }); await publish({ - pubkey: users[0].pubKey.serialize(), + pubkey: pollKeys[0].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -251,11 +317,11 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[0].privKey.serialize(), + privateKey: pollKeys[0].privKey.serialize(), signer, }); await publish({ - pubkey: users[1].pubKey.serialize(), + pubkey: pollKeys[1].pubKey.serialize(), stateIndex: 2n, voteOptionIndex: 2n, nonce: 1n, @@ -263,11 +329,11 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[1].privKey.serialize(), + privateKey: pollKeys[1].privKey.serialize(), signer, }); await publish({ - pubkey: users[2].pubKey.serialize(), + pubkey: pollKeys[2].pubKey.serialize(), stateIndex: 3n, voteOptionIndex: 2n, nonce: 1n, @@ -275,11 +341,11 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[2].privKey.serialize(), + privateKey: pollKeys[2].privKey.serialize(), signer, }); await publish({ - pubkey: users[3].pubKey.serialize(), + pubkey: pollKeys[3].pubKey.serialize(), stateIndex: 4n, voteOptionIndex: 2n, nonce: 3n, @@ -287,11 +353,11 @@ describe("e2e tests", function test() { newVoteWeight: 3n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[3].privKey.serialize(), + privateKey: pollKeys[3].privKey.serialize(), signer, }); await publish({ - pubkey: users[3].pubKey.serialize(), + pubkey: pollKeys[3].pubKey.serialize(), stateIndex: 4n, voteOptionIndex: 2n, nonce: 2n, @@ -299,11 +365,11 @@ describe("e2e tests", function test() { newVoteWeight: 2n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[3].privKey.serialize(), + privateKey: pollKeys[3].privKey.serialize(), signer, }); await publish({ - pubkey: users[3].pubKey.serialize(), + pubkey: pollKeys[3].pubKey.serialize(), stateIndex: 4n, voteOptionIndex: 1n, nonce: 1n, @@ -311,7 +377,7 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[3].privKey.serialize(), + privateKey: pollKeys[3].privKey.serialize(), signer, }); }); @@ -325,7 +391,7 @@ describe("e2e tests", function test() { }); }); - describe("5 signups, 1 message", () => { + describe("9 signups, 1 message", () => { after(async () => { await clean(); }); @@ -342,6 +408,18 @@ describe("e2e tests", function test() { new Keypair(), ]; + const pollKeys = [ + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + ]; + before(async () => { // deploy the smart contracts maciAddresses = await deploy({ ...deployArgs, signer }); @@ -357,9 +435,31 @@ describe("e2e tests", function test() { } }); + it("should join nine users", async () => { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < users.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: users[i].privKey.serialize(), + pollPrivKey: pollKeys[i].privKey.serialize(), + stateIndex: BigInt(i + 1), + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + } + }); + it("should publish one message", async () => { await publish({ - pubkey: users[0].pubKey.serialize(), + pubkey: pollKeys[0].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -367,7 +467,7 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[0].privKey.serialize(), + privateKey: pollKeys[0].privKey.serialize(), signer, }); }); @@ -387,6 +487,7 @@ describe("e2e tests", function test() { }); const user = new Keypair(); + const pollKeys = new Keypair(); before(async () => { // deploy the smart contracts @@ -402,11 +503,30 @@ describe("e2e tests", function test() { } }); + it("should join user", async () => { + // eslint-disable-next-line no-await-in-loop + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: user.privKey.serialize(), + pollPrivKey: pollKeys.privKey.serialize(), + stateIndex: 1n, + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + }); + it("should publish 12 messages with the same nonce", async () => { for (let i = 0; i < 12; i += 1) { // eslint-disable-next-line no-await-in-loop await publish({ - pubkey: user.pubKey.serialize(), + pubkey: pollKeys.pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -414,7 +534,7 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: user.privKey.serialize(), + privateKey: pollKeys.privKey.serialize(), signer, }); } @@ -435,6 +555,7 @@ describe("e2e tests", function test() { }); const users = Array.from({ length: 30 }, () => new Keypair()); + const pollKeys = Array.from({ length: 30 }, () => new Keypair()); before(async () => { // deploy the smart contracts @@ -451,57 +572,79 @@ describe("e2e tests", function test() { } }); + it("should join thirty users", async () => { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < users.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: users[i].privKey.serialize(), + pollPrivKey: pollKeys[i].privKey.serialize(), + stateIndex: BigInt(i + 1), + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + } + }); + it("should publish 4 messages", async () => { // publish four different messages await publish({ maciAddress: maciAddresses.maciAddress, - pubkey: users[0].pubKey.serialize(), + pubkey: pollKeys[0].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, pollId: 0n, newVoteWeight: 9n, salt: genRandomSalt(), - privateKey: users[0].privKey.serialize(), + privateKey: pollKeys[0].privKey.serialize(), signer, }); await publish({ maciAddress: maciAddresses.maciAddress, - pubkey: users[1].pubKey.serialize(), + pubkey: pollKeys[1].pubKey.serialize(), stateIndex: 2n, voteOptionIndex: 1n, nonce: 1n, pollId: 0n, newVoteWeight: 9n, salt: genRandomSalt(), - privateKey: users[1].privKey.serialize(), + privateKey: pollKeys[1].privKey.serialize(), signer, }); await publish({ maciAddress: maciAddresses.maciAddress, - pubkey: users[2].pubKey.serialize(), + pubkey: pollKeys[2].pubKey.serialize(), stateIndex: 3n, voteOptionIndex: 2n, nonce: 1n, pollId: 0n, newVoteWeight: 9n, salt: genRandomSalt(), - privateKey: users[2].privKey.serialize(), + privateKey: pollKeys[2].privKey.serialize(), signer, }); await publish({ maciAddress: maciAddresses.maciAddress, - pubkey: users[3].pubKey.serialize(), + pubkey: pollKeys[3].pubKey.serialize(), stateIndex: 4n, voteOptionIndex: 3n, nonce: 1n, pollId: 0n, newVoteWeight: 9n, salt: genRandomSalt(), - privateKey: users[3].privKey.serialize(), + privateKey: pollKeys[3].privKey.serialize(), signer, }); }); @@ -531,6 +674,7 @@ describe("e2e tests", function test() { }); const user = new Keypair(); + const pollKeys = new Keypair(); before(async () => { // deploy the smart contracts @@ -539,6 +683,22 @@ describe("e2e tests", function test() { await deployPoll({ ...deployPollArgs, signer }); // signup await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: user.pubKey.serialize(), signer }); + // joinPoll + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: user.privKey.serialize(), + pollPrivKey: pollKeys.privKey.serialize(), + stateIndex: 1n, + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); // publish await publish({ pubkey: user.pubKey.serialize(), @@ -566,9 +726,28 @@ describe("e2e tests", function test() { await deployPoll({ ...deployPollArgs, signer }); }); + it("should join to new poll", async () => { + // joinPoll + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: user.privKey.serialize(), + pollPrivKey: pollKeys.privKey.serialize(), + stateIndex: 1n, + pollId: 1n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + }); + it("should publish a new message", async () => { await publish({ - pubkey: user.pubKey.serialize(), + pubkey: pollKeys.pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -576,7 +755,7 @@ describe("e2e tests", function test() { newVoteWeight: 7n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: user.privKey.serialize(), + privateKey: pollKeys.privKey.serialize(), signer, }); }); @@ -596,6 +775,7 @@ describe("e2e tests", function test() { }); const users = Array.from({ length: 4 }, () => new Keypair()); + const pollKeys = Array.from({ length: 4 }, () => new Keypair()); before(async () => { // deploy the smart contracts @@ -604,9 +784,26 @@ describe("e2e tests", function test() { await deployPoll({ ...deployPollArgs, signer }); // signup await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: users[0].pubKey.serialize(), signer }); + // joinPoll + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: users[0].privKey.serialize(), + pollPrivKey: pollKeys[0].privKey.serialize(), + stateIndex: 1n, + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + // publish await publish({ - pubkey: users[0].pubKey.serialize(), + pubkey: pollKeys[0].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -614,12 +811,31 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[0].privKey.serialize(), + privateKey: pollKeys[0].privKey.serialize(), signer, }); + await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: users[1].pubKey.serialize(), signer }); + await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: users[1].pubKey.serialize(), signer }); + // joinPoll + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: users[1].privKey.serialize(), + pollPrivKey: pollKeys[1].privKey.serialize(), + stateIndex: 2n, + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + // time travel await timeTravel({ ...timeTravelArgs, signer }); // generate proofs @@ -641,9 +857,44 @@ describe("e2e tests", function test() { await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: users[3].pubKey.serialize(), signer }); }); + it("should join users", async () => { + // joinPoll + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: users[2].privKey.serialize(), + pollPrivKey: pollKeys[2].privKey.serialize(), + stateIndex: 4n, + pollId: 1n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + // joinPoll + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: users[3].privKey.serialize(), + pollPrivKey: pollKeys[3].privKey.serialize(), + stateIndex: 5n, + pollId: 1n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + }); + it("should publish a new message from the first poll voter", async () => { await publish({ - pubkey: users[0].pubKey.serialize(), + pubkey: pollKeys[0].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -651,14 +902,14 @@ describe("e2e tests", function test() { newVoteWeight: 7n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[0].privKey.serialize(), + privateKey: pollKeys[0].privKey.serialize(), signer, }); }); it("should publish a new message by the new poll voters", async () => { await publish({ - pubkey: users[1].pubKey.serialize(), + pubkey: pollKeys[1].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -666,7 +917,7 @@ describe("e2e tests", function test() { newVoteWeight: 7n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[1].privKey.serialize(), + privateKey: pollKeys[1].privKey.serialize(), signer, }); }); @@ -690,6 +941,15 @@ describe("e2e tests", function test() { new Keypair(), new Keypair(), ]; + const pollKeys = [ + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + new Keypair(), + ]; after(async () => { await clean(); @@ -721,9 +981,42 @@ describe("e2e tests", function test() { expect(stateIndex).to.not.eq(undefined); } + // join the first poll + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < users.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: users[i].privKey.serialize(), + pollPrivKey: pollKeys[i].privKey.serialize(), + stateIndex: BigInt(i + 1), + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + // eslint-disable-next-line no-await-in-loop + const { isJoined, pollStateIndex } = await isJoinedUser({ + maciAddress: maciAddresses.maciAddress, + pollId: 0n, + pollPubKey: pollKeys[i].pubKey.serialize(), + signer, + startBlock: 0, + quiet: true, + }); + + expect(isJoined).to.eq(true); + expect(pollStateIndex).to.not.eq(undefined); + } + // publish await publish({ - pubkey: users[0].pubKey.serialize(), + pubkey: pollKeys[0].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -731,7 +1024,7 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[0].privKey.serialize(), + privateKey: pollKeys[0].privKey.serialize(), signer, }); @@ -751,9 +1044,45 @@ describe("e2e tests", function test() { await deployPoll({ ...deployPollArgs, signer }); }); + it("join the second and third polls", async () => { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let p = 1; p <= 2; p += 1) { + for (let i = 0; i < users.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: users[i].privKey.serialize(), + pollPrivKey: pollKeys[i].privKey.serialize(), + stateIndex: BigInt(i + 1), + pollId: BigInt(p), + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + // eslint-disable-next-line no-await-in-loop + const { isJoined, pollStateIndex } = await isJoinedUser({ + maciAddress: maciAddresses.maciAddress, + pollId: BigInt(p), + pollPubKey: pollKeys[i].pubKey.serialize(), + signer, + startBlock: 0, + quiet: true, + }); + + expect(isJoined).to.eq(true); + expect(pollStateIndex).to.not.eq(undefined); + } + } + }); + it("should publish messages to the second poll", async () => { await publish({ - pubkey: users[0].pubKey.serialize(), + pubkey: pollKeys[0].pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 0n, nonce: 1n, @@ -761,12 +1090,12 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[0].privKey.serialize(), + privateKey: pollKeys[0].privKey.serialize(), signer, }); await publish({ - pubkey: users[1].pubKey.serialize(), + pubkey: pollKeys[1].pubKey.serialize(), stateIndex: 2n, voteOptionIndex: 3n, nonce: 1n, @@ -774,12 +1103,12 @@ describe("e2e tests", function test() { newVoteWeight: 1n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[1].privKey.serialize(), + privateKey: pollKeys[1].privKey.serialize(), signer, }); await publish({ - pubkey: users[2].pubKey.serialize(), + pubkey: pollKeys[2].pubKey.serialize(), stateIndex: 3n, voteOptionIndex: 5n, nonce: 1n, @@ -787,14 +1116,14 @@ describe("e2e tests", function test() { newVoteWeight: 3n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[2].privKey.serialize(), + privateKey: pollKeys[2].privKey.serialize(), signer, }); }); it("should publish messages to the third poll", async () => { await publish({ - pubkey: users[3].pubKey.serialize(), + pubkey: pollKeys[3].pubKey.serialize(), stateIndex: 3n, voteOptionIndex: 5n, nonce: 1n, @@ -802,12 +1131,12 @@ describe("e2e tests", function test() { newVoteWeight: 3n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[3].privKey.serialize(), + privateKey: pollKeys[3].privKey.serialize(), signer, }); await publish({ - pubkey: users[4].pubKey.serialize(), + pubkey: pollKeys[4].pubKey.serialize(), stateIndex: 4n, voteOptionIndex: 7n, nonce: 1n, @@ -815,12 +1144,12 @@ describe("e2e tests", function test() { newVoteWeight: 2n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[4].privKey.serialize(), + privateKey: pollKeys[4].privKey.serialize(), signer, }); await publish({ - pubkey: users[5].pubKey.serialize(), + pubkey: pollKeys[5].pubKey.serialize(), stateIndex: 5n, voteOptionIndex: 5n, nonce: 1n, @@ -828,7 +1157,7 @@ describe("e2e tests", function test() { newVoteWeight: 9n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: users[5].privKey.serialize(), + privateKey: pollKeys[5].privKey.serialize(), signer, }); }); @@ -876,6 +1205,7 @@ describe("e2e tests", function test() { const stateOutPath = "./state.json"; const user = new Keypair(); + const pollKeys = new Keypair(); after(async () => { await clean(); @@ -896,9 +1226,28 @@ describe("e2e tests", function test() { await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: user.pubKey.serialize(), signer }); }); + it("should join one user", async () => { + // joinPoll + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: user.privKey.serialize(), + pollPrivKey: pollKeys.privKey.serialize(), + stateIndex: 1n, + pollId: 0n, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 1n, + quiet: true, + }); + }); + it("should publish one message", async () => { await publish({ - pubkey: user.pubKey.serialize(), + pubkey: pollKeys.pubKey.serialize(), stateIndex: 1n, voteOptionIndex: 5n, nonce: 1n, @@ -906,7 +1255,7 @@ describe("e2e tests", function test() { newVoteWeight: 3n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: user.privKey.serialize(), + privateKey: pollKeys.privKey.serialize(), signer, }); }); diff --git a/packages/cli/tests/e2e/keyChange.test.ts b/packages/cli/tests/e2e/keyChange.test.ts index 729e3dc6fd..212bcd5c6a 100644 --- a/packages/cli/tests/e2e/keyChange.test.ts +++ b/packages/cli/tests/e2e/keyChange.test.ts @@ -13,6 +13,7 @@ import { deployPoll, deployVkRegistryContract, genProofs, + joinPoll, mergeSignups, proveOnChain, publish, @@ -42,6 +43,9 @@ import { proveOnChainArgs, verifyArgs, timeTravelArgs, + pollJoiningTestZkeyPath, + testPollJoiningWasmPath, + testPollJoiningWitnessPath, } from "../constants"; import { clean, isArm } from "../utils"; @@ -52,8 +56,6 @@ describe("keyChange tests", function test() { let maciAddresses: DeployedContracts; let signer: Signer; - deployPollArgs.pollDuration = 90; - const genProofsArgs: Omit = { outputDir: testProofsDirPath, tallyFile: testTallyFilePath, @@ -86,8 +88,10 @@ describe("keyChange tests", function test() { await clean(); }); - const keypair1 = new Keypair(); - const keypair2 = new Keypair(); + const user1Keypair = new Keypair(); + const { privKey: pollPrivKey1, pubKey: pollPubKey1 } = new Keypair(); + const { pubKey: pollPubKey2 } = new Keypair(); + const initialNonce = 1n; const initialVoteOption = 0n; const initialVoteAmount = 9n; @@ -102,12 +106,29 @@ describe("keyChange tests", function test() { // deploy a poll contract await deployPoll({ ...deployPollArgs, signer }); stateIndex = BigInt( - await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: keypair1.pubKey.serialize(), signer }).then( - (result) => result.stateIndex, - ), + await signup({ + maciAddress: maciAddresses.maciAddress, + maciPubKey: user1Keypair.pubKey.serialize(), + signer, + }).then((result) => result.stateIndex), ); + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: user1Keypair.privKey.serialize(), + pollPrivKey: pollPrivKey1.serialize(), + stateIndex, + pollId, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 99n, + quiet: true, + }); await publish({ - pubkey: keypair1.pubKey.serialize(), + pubkey: pollPubKey1.serialize(), stateIndex, voteOptionIndex: initialVoteOption, nonce: initialNonce, @@ -115,14 +136,14 @@ describe("keyChange tests", function test() { newVoteWeight: initialVoteAmount, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: keypair1.privKey.serialize(), + privateKey: pollPrivKey1.serialize(), signer, }); }); - it("should publish a message to change the user maci key and cast a new vote", async () => { + it("should publish a message to change the poll key and cast a new vote", async () => { await publish({ - pubkey: keypair2.pubKey.serialize(), + pubkey: pollPubKey2.serialize(), stateIndex, voteOptionIndex: initialVoteOption, nonce: initialNonce, @@ -130,7 +151,7 @@ describe("keyChange tests", function test() { newVoteWeight: initialVoteAmount - 1n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: keypair1.privKey.serialize(), + privateKey: pollPrivKey1.serialize(), signer, }); }); @@ -157,8 +178,10 @@ describe("keyChange tests", function test() { await clean(); }); - const keypair1 = new Keypair(); - const keypair2 = new Keypair(); + const user1Keypair = new Keypair(); + const { privKey: pollPrivKey1, pubKey: pollPubKey1 } = new Keypair(); + const { pubKey: pollPubKey2 } = new Keypair(); + const initialNonce = 1n; const initialVoteOption = 0n; const initialVoteAmount = 9n; @@ -173,12 +196,29 @@ describe("keyChange tests", function test() { // deploy a poll contract await deployPoll({ ...deployPollArgs, signer }); stateIndex = BigInt( - await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: keypair1.pubKey.serialize(), signer }).then( - (result) => result.stateIndex, - ), + await signup({ + maciAddress: maciAddresses.maciAddress, + maciPubKey: user1Keypair.pubKey.serialize(), + signer, + }).then((result) => result.stateIndex), ); + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: user1Keypair.privKey.serialize(), + pollPrivKey: pollPrivKey1.serialize(), + stateIndex, + pollId, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 99n, + quiet: true, + }); await publish({ - pubkey: keypair1.pubKey.serialize(), + pubkey: pollPubKey1.serialize(), stateIndex, voteOptionIndex: initialVoteOption, nonce: initialNonce, @@ -186,14 +226,14 @@ describe("keyChange tests", function test() { newVoteWeight: initialVoteAmount, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: keypair1.privKey.serialize(), + privateKey: pollPrivKey1.serialize(), signer, }); }); - it("should publish a message to change the user maci key and cast a new vote", async () => { + it("should publish a message to change the poll and cast a new vote", async () => { await publish({ - pubkey: keypair2.pubKey.serialize(), + pubkey: pollPubKey2.serialize(), stateIndex, voteOptionIndex: initialVoteOption + 1n, nonce: initialNonce + 1n, @@ -201,7 +241,7 @@ describe("keyChange tests", function test() { newVoteWeight: initialVoteAmount - 1n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: keypair1.privKey.serialize(), + privateKey: pollPrivKey1.serialize(), signer, }); }); @@ -228,8 +268,10 @@ describe("keyChange tests", function test() { await clean(); }); - const keypair1 = new Keypair(); - const keypair2 = new Keypair(); + const user1Keypair = new Keypair(); + const { privKey: pollPrivKey1, pubKey: pollPubKey1 } = new Keypair(); + const { pubKey: pollPubKey2 } = new Keypair(); + const initialNonce = 1n; const initialVoteOption = 0n; const initialVoteAmount = 9n; @@ -244,12 +286,30 @@ describe("keyChange tests", function test() { // deploy a poll contract await deployPoll({ ...deployPollArgs, signer }); stateIndex = BigInt( - await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: keypair1.pubKey.serialize(), signer }).then( - (result) => result.stateIndex, - ), + await signup({ + maciAddress: maciAddresses.maciAddress, + maciPubKey: user1Keypair.pubKey.serialize(), + signer, + }).then((result) => result.stateIndex), ); + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: user1Keypair.privKey.serialize(), + pollPrivKey: pollPrivKey1.serialize(), + stateIndex, + pollId, + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + signer, + newVoiceCreditBalance: 99n, + quiet: true, + }); + await publish({ - pubkey: keypair1.pubKey.serialize(), + pubkey: pollPubKey1.serialize(), stateIndex, voteOptionIndex: initialVoteOption, nonce: initialNonce, @@ -257,14 +317,14 @@ describe("keyChange tests", function test() { newVoteWeight: initialVoteAmount, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: keypair1.privKey.serialize(), + privateKey: pollPrivKey1.serialize(), signer, }); }); - it("should publish a message to change the user maci key, and a new vote", async () => { + it("should publish a message to change the poll key, and a new vote", async () => { await publish({ - pubkey: keypair2.pubKey.serialize(), + pubkey: pollPubKey2.serialize(), stateIndex, voteOptionIndex: initialVoteOption + 2n, nonce: initialNonce, @@ -272,7 +332,7 @@ describe("keyChange tests", function test() { newVoteWeight: initialVoteAmount - 3n, maciAddress: maciAddresses.maciAddress, salt: genRandomSalt(), - privateKey: keypair1.privKey.serialize(), + privateKey: pollPrivKey1.serialize(), signer, }); }); diff --git a/packages/cli/tests/unit/joinPoll.test.ts b/packages/cli/tests/unit/joinPoll.test.ts new file mode 100644 index 0000000000..6e70e4cff7 --- /dev/null +++ b/packages/cli/tests/unit/joinPoll.test.ts @@ -0,0 +1,138 @@ +import { expect } from "chai"; +import { Signer } from "ethers"; +import { getDefaultSigner } from "maci-contracts"; +import { Keypair } from "maci-domainobjs"; + +import { + deploy, + DeployedContracts, + deployVkRegistryContract, + setVerifyingKeys, + joinPoll, + signup, + deployPoll, + isJoinedUser, +} from "../../ts"; +import { + deployArgs, + deployPollArgs, + setVerifyingKeysArgs, + pollJoiningTestZkeyPath, + testPollJoiningWasmPath, + testRapidsnarkPath, + testPollJoiningWitnessPath, +} from "../constants"; + +describe("joinPoll", function test() { + let signer: Signer; + let maciAddresses: DeployedContracts; + const user = new Keypair(); + const userPrivateKey = user.privKey.serialize(); + const userPublicKey = user.pubKey.serialize(); + + const { privKey: pollPrivateKey, pubKey: pollPublicKey } = new Keypair(); + const mockNewVoiceCreditBalance = 10n; + const mockStateIndex = 1n; + const mockPollId = 9000n; + + this.timeout(900000); + // before all tests we deploy the vk registry contract and set the verifying keys + before(async () => { + signer = await getDefaultSigner(); + + // we deploy the vk registry contract + await deployVkRegistryContract({ signer }); + // we set the verifying keys + await setVerifyingKeys({ ...setVerifyingKeysArgs, signer }); + // deploy the smart contracts + maciAddresses = await deploy({ ...deployArgs, signer }); + // signup the user + await signup({ + maciAddress: maciAddresses.maciAddress, + maciPubKey: userPublicKey, + signer, + }); + + await deployPoll({ ...deployPollArgs, signer }); + }); + + it("should allow to join the poll and return the user data", async () => { + const startBlock = await signer.provider?.getBlockNumber(); + + await joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: userPrivateKey, + stateIndex: 1n, + signer, + pollId: 0n, + pollPrivKey: pollPrivateKey.serialize(), + pollJoiningZkey: pollJoiningTestZkeyPath, + useWasm: true, + pollWasm: testPollJoiningWasmPath, + pollWitgen: testPollJoiningWitnessPath, + rapidsnark: testRapidsnarkPath, + newVoiceCreditBalance: mockNewVoiceCreditBalance, + quiet: true, + }); + + const registeredUserData = await isJoinedUser({ + maciAddress: maciAddresses.maciAddress, + pollId: 0n, + pollPubKey: pollPublicKey.serialize(), + signer, + startBlock: startBlock || 0, + quiet: true, + }); + + expect(registeredUserData.isJoined).to.eq(true); + expect(BigInt(registeredUserData.pollStateIndex!)).to.eq(1); + }); + + it("should throw error if poll does not exist", async () => { + await expect( + joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: userPrivateKey, + stateIndex: mockStateIndex, + signer, + pollId: mockPollId, + pollPrivKey: pollPrivateKey.serialize(), + pollJoiningZkey: pollJoiningTestZkeyPath, + newVoiceCreditBalance: mockNewVoiceCreditBalance, + quiet: true, + }), + ).eventually.rejectedWith("PollDoesNotExist(9000)"); + }); + + it("should throw error if state index is invalid", async () => { + await expect( + joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: userPrivateKey, + stateIndex: -1n, + signer, + pollId: 0n, + pollPrivKey: pollPrivateKey.serialize(), + pollJoiningZkey: pollJoiningTestZkeyPath, + newVoiceCreditBalance: mockNewVoiceCreditBalance, + quiet: true, + }), + ).eventually.rejectedWith("Invalid state index"); + }); + + it("should throw error if current poll id is invalid", async () => { + await expect( + joinPoll({ + maciAddress: maciAddresses.maciAddress, + privateKey: userPrivateKey, + stateIndex: mockStateIndex, + signer, + pollId: -1n, + pollPrivKey: pollPrivateKey.serialize(), + pollJoiningZkey: pollJoiningTestZkeyPath, + newVoiceCreditBalance: mockNewVoiceCreditBalance, + quiet: true, + }), + ).eventually.rejectedWith("Invalid poll id"); + }); +}); diff --git a/packages/cli/tests/unit/poll.test.ts b/packages/cli/tests/unit/poll.test.ts index bf5fc70d91..be0bc2e980 100644 --- a/packages/cli/tests/unit/poll.test.ts +++ b/packages/cli/tests/unit/poll.test.ts @@ -16,7 +16,9 @@ import { DeployedContracts, PollContracts } from "../../ts/utils"; import { deployPollArgs, setVerifyingKeysArgs, deployArgs } from "../constants"; import { clean } from "../utils"; -describe("poll", () => { +describe("poll", function test() { + this.timeout(900000); + let maciAddresses: DeployedContracts; let pollAddresses: PollContracts; let signer: Signer; diff --git a/packages/cli/tests/unit/publish.test.ts b/packages/cli/tests/unit/publish.test.ts index 1ba9a6601c..02a04ad376 100644 --- a/packages/cli/tests/unit/publish.test.ts +++ b/packages/cli/tests/unit/publish.test.ts @@ -17,7 +17,9 @@ import { import { DeployedContracts, IPublishBatchArgs, IPublishMessage, PollContracts } from "../../ts/utils"; import { deployPollArgs, setVerifyingKeysArgs, deployArgs } from "../constants"; -describe("publish", () => { +describe("publish", function test() { + this.timeout(900000); + let maciAddresses: DeployedContracts; let pollAddresses: PollContracts; let signer: Signer; diff --git a/packages/cli/tests/unit/signup.test.ts b/packages/cli/tests/unit/signup.test.ts index 14d4ec65a0..017a2d8b8a 100644 --- a/packages/cli/tests/unit/signup.test.ts +++ b/packages/cli/tests/unit/signup.test.ts @@ -13,7 +13,9 @@ import { } from "../../ts"; import { deployArgs, setVerifyingKeysArgs } from "../constants"; -describe("signup", () => { +describe("signup", function test() { + this.timeout(900000); + let signer: Signer; let maciAddresses: DeployedContracts; const user = new Keypair(); diff --git a/packages/cli/ts/commands/checkVerifyingKeys.ts b/packages/cli/ts/commands/checkVerifyingKeys.ts index bb42ccca8a..805dbd8a9a 100644 --- a/packages/cli/ts/commands/checkVerifyingKeys.ts +++ b/packages/cli/ts/commands/checkVerifyingKeys.ts @@ -31,6 +31,7 @@ export const checkVerifyingKeys = async ({ messageBatchSize, processMessagesZkeyPath, tallyVotesZkeyPath, + pollJoiningZkeyPath, vkRegistry, signer, useQuadraticVoting = true, @@ -68,11 +69,14 @@ export const checkVerifyingKeys = async ({ // extract the verification keys from the zkey files const processVk = VerifyingKey.fromObj(await extractVk(processMessagesZkeyPath)); const tallyVk = VerifyingKey.fromObj(await extractVk(tallyVotesZkeyPath)); + const pollVk = VerifyingKey.fromObj(await extractVk(pollJoiningZkeyPath)); try { logYellow(quiet, info("Retrieving verifying keys from the contract...")); // retrieve the verifying keys from the contract + const pollVkOnChain = await vkRegistryContractInstance.getPollVk(stateTreeDepth, voteOptionTreeDepth); + const processVkOnChain = await vkRegistryContractInstance.getProcessVk( stateTreeDepth, voteOptionTreeDepth, @@ -88,6 +92,10 @@ export const checkVerifyingKeys = async ({ ); // do the actual validation + if (!compareVks(pollVk, pollVkOnChain)) { + logError("Poll verifying keys do not match"); + } + if (!compareVks(processVk, processVkOnChain)) { logError("Process verifying keys do not match"); } diff --git a/packages/cli/ts/commands/extractVkToFile.ts b/packages/cli/ts/commands/extractVkToFile.ts index 54af523e53..64d7a67fc6 100644 --- a/packages/cli/ts/commands/extractVkToFile.ts +++ b/packages/cli/ts/commands/extractVkToFile.ts @@ -16,14 +16,19 @@ export const extractVkToFile = async ({ tallyVotesZkeyPathQv, processMessagesZkeyPathNonQv, tallyVotesZkeyPathNonQv, + pollJoiningZkeyPath, outputFilePath, }: ExtractVkToFileArgs): Promise => { - const [processVkQv, tallyVkQv, processVkNonQv, tallyVkNonQv] = await Promise.all([ + const [processVkQv, tallyVkQv, processVkNonQv, tallyVkNonQv, pollVk] = await Promise.all([ extractVk(processMessagesZkeyPathQv), extractVk(tallyVotesZkeyPathQv), extractVk(processMessagesZkeyPathNonQv), extractVk(tallyVotesZkeyPathNonQv), + extractVk(pollJoiningZkeyPath), ]); - await fs.promises.writeFile(outputFilePath, JSON.stringify({ processVkQv, tallyVkQv, processVkNonQv, tallyVkNonQv })); + await fs.promises.writeFile( + outputFilePath, + JSON.stringify({ processVkQv, tallyVkQv, processVkNonQv, tallyVkNonQv, pollVk }), + ); }; diff --git a/packages/cli/ts/commands/genLocalState.ts b/packages/cli/ts/commands/genLocalState.ts index 219f234087..fda6905183 100644 --- a/packages/cli/ts/commands/genLocalState.ts +++ b/packages/cli/ts/commands/genLocalState.ts @@ -80,7 +80,7 @@ export const genLocalState = async ({ const defaultEndBlock = await Promise.all([ pollContract - .queryFilter(pollContract.filters.MergeMaciState(stateRoot, numSignups), fromBlock) + .queryFilter(pollContract.filters.MergeState(stateRoot, numSignups), fromBlock) .then((events) => events[events.length - 1]?.blockNumber), ]).then((blocks) => Math.max(...blocks)); diff --git a/packages/cli/ts/commands/genProofs.ts b/packages/cli/ts/commands/genProofs.ts index 400a66f43d..e302a10c63 100644 --- a/packages/cli/ts/commands/genProofs.ts +++ b/packages/cli/ts/commands/genProofs.ts @@ -1,3 +1,4 @@ +import { type BigNumberish } from "ethers"; import { extractVk, genProof, verifyProof } from "maci-circuits"; import { MACI__factory as MACIFactory, Poll__factory as PollFactory, genMaciStateFromContract } from "maci-contracts"; import { type CircuitInputs, type IJsonMaciState, MaciState } from "maci-core"; @@ -7,8 +8,6 @@ import { Keypair, PrivKey } from "maci-domainobjs"; import fs from "fs"; import path from "path"; -import type { BigNumberish } from "ethers"; - import { asHex, banner, @@ -184,7 +183,7 @@ export const genProofs = async ({ const defaultEndBlock = await Promise.all([ pollContract - .queryFilter(pollContract.filters.MergeMaciState(stateRoot, numSignups), fromBlock) + .queryFilter(pollContract.filters.MergeState(stateRoot, numSignups), fromBlock) .then((events) => events[events.length - 1]?.blockNumber), ]).then((blocks) => Math.max(...blocks)); diff --git a/packages/cli/ts/commands/index.ts b/packages/cli/ts/commands/index.ts index 7099e6c610..d1ac5d93d1 100644 --- a/packages/cli/ts/commands/index.ts +++ b/packages/cli/ts/commands/index.ts @@ -1,6 +1,7 @@ export { deploy } from "./deploy"; export { deployPoll } from "./deployPoll"; export { getPoll } from "./poll"; +export { joinPoll, isJoinedUser } from "./joinPoll"; export { deployVkRegistryContract } from "./deployVkRegistry"; export { genKeyPair } from "./genKeyPair"; export { genMaciPubKey } from "./genPubKey"; diff --git a/packages/cli/ts/commands/mergeSignups.ts b/packages/cli/ts/commands/mergeSignups.ts index c836c186de..a73d6e5790 100644 --- a/packages/cli/ts/commands/mergeSignups.ts +++ b/packages/cli/ts/commands/mergeSignups.ts @@ -51,7 +51,7 @@ export const mergeSignups = async ({ pollId, maciAddress, signer, quiet = true } if (!(await pollContract.stateMerged())) { // go and merge the state tree logYellow(quiet, info("Calculating root and storing on Poll...")); - const tx = await pollContract.mergeMaciState(); + const tx = await pollContract.mergeState(); const receipt = await tx.wait(); if (receipt?.status !== 1) { diff --git a/packages/cli/ts/commands/publish.ts b/packages/cli/ts/commands/publish.ts index 197624ba55..5c6d205c99 100644 --- a/packages/cli/ts/commands/publish.ts +++ b/packages/cli/ts/commands/publish.ts @@ -41,7 +41,7 @@ export const publish = async ({ logError("invalid MACI public key"); } // deserialize - const userMaciPubKey = PubKey.deserialize(pubkey); + const pollPubKey = PubKey.deserialize(pubkey); if (!(await contractExists(signer.provider!, maciAddress))) { logError("MACI contract does not exist"); @@ -51,7 +51,7 @@ export const publish = async ({ logError("Invalid MACI private key"); } - const userMaciPrivKey = PrivKey.deserialize(privateKey); + const pollPrivKey = PrivKey.deserialize(privateKey); // validate args if (voteOptionIndex < 0) { @@ -104,7 +104,7 @@ export const publish = async ({ // create the command object const command: PCommand = new PCommand( stateIndex, - userMaciPubKey, + pollPubKey, voteOptionIndex, newVoteWeight, nonce, @@ -112,8 +112,8 @@ export const publish = async ({ userSalt, ); - // sign the command with the user private key - const signature = command.sign(userMaciPrivKey); + // sign the command with the poll private key + const signature = command.sign(pollPrivKey); // encrypt the command using a shared key between the user and the coordinator const message = command.encrypt(signature, Keypair.genEcdhSharedKey(encKeypair.privKey, coordinatorPubKey)); diff --git a/packages/cli/ts/commands/setVerifyingKeys.ts b/packages/cli/ts/commands/setVerifyingKeys.ts index 7decc3a622..12d06f8d0e 100644 --- a/packages/cli/ts/commands/setVerifyingKeys.ts +++ b/packages/cli/ts/commands/setVerifyingKeys.ts @@ -1,6 +1,6 @@ import { extractVk } from "maci-circuits"; import { type IVerifyingKeyStruct, VkRegistry__factory as VkRegistryFactory, EMode } from "maci-contracts"; -import { genProcessVkSig, genTallyVkSig } from "maci-core"; +import { genPollVkSig, genProcessVkSig, genTallyVkSig } from "maci-core"; import { VerifyingKey } from "maci-domainobjs"; import fs from "fs"; @@ -28,6 +28,7 @@ export const setVerifyingKeys = async ({ intStateTreeDepth, voteOptionTreeDepth, messageBatchSize, + pollJoiningZkeyPath, processMessagesZkeyPathQv, tallyVotesZkeyPathQv, processMessagesZkeyPathNonQv, @@ -49,11 +50,15 @@ export const setVerifyingKeys = async ({ } // check if zKey files exist + if (pollJoiningZkeyPath && !fs.existsSync(pollJoiningZkeyPath)) { + logError(`${pollJoiningZkeyPath} does not exist.`); + } + const isProcessMessagesZkeyPathQvExists = processMessagesZkeyPathQv ? fs.existsSync(processMessagesZkeyPathQv) : false; - if (useQuadraticVoting && processMessagesZkeyPathQv && !isProcessMessagesZkeyPathQvExists) { + if (useQuadraticVoting && !isProcessMessagesZkeyPathQvExists) { logError(`${processMessagesZkeyPathQv} does not exist.`); } @@ -78,6 +83,7 @@ export const setVerifyingKeys = async ({ } // extract the vks + const pollVk = pollJoiningZkeyPath && VerifyingKey.fromObj(await extractVk(pollJoiningZkeyPath)); const processVkQv = processMessagesZkeyPathQv && VerifyingKey.fromObj(await extractVk(processMessagesZkeyPathQv)); const tallyVkQv = tallyVotesZkeyPathQv && VerifyingKey.fromObj(await extractVk(tallyVotesZkeyPathQv)); const processVkNonQv = @@ -94,6 +100,7 @@ export const setVerifyingKeys = async ({ } checkZkeyFilepaths({ + pollJoiningZkeyPath: pollJoiningZkeyPath!, processMessagesZkeyPath: processMessagesZkeyPathQv!, tallyVotesZkeyPath: tallyVotesZkeyPathQv!, stateTreeDepth, @@ -103,7 +110,7 @@ export const setVerifyingKeys = async ({ }); checkZkeyFilepaths({ - processMessagesZkeyPath: processMessagesZkeyPathNonQv!, + pollJoiningZkeyPath: pollJoiningZkeyPath!, tallyVotesZkeyPath: tallyVotesZkeyPathNonQv!, stateTreeDepth, messageBatchSize, @@ -119,6 +126,12 @@ export const setVerifyingKeys = async ({ // connect to VkRegistry contract const vkRegistryContract = VkRegistryFactory.connect(vkRegistryAddress, signer); + // check if the poll vk was already set + const pollVkSig = genPollVkSig(stateTreeDepth, voteOptionTreeDepth); + if (await vkRegistryContract.isPollVkSet(pollVkSig)) { + logError("This poll verifying key is already set in the contract"); + } + // check if the process messages vk was already set const processVkSig = genProcessVkSig(stateTreeDepth, voteOptionTreeDepth, messageBatchSize); @@ -144,6 +157,7 @@ export const setVerifyingKeys = async ({ // actually set those values try { logYellow(quiet, info("Setting verifying keys...")); + const pollZkeys = (pollVk as VerifyingKey).asContractParam() as IVerifyingKeyStruct; const processZkeys = [processVkQv, processVkNonQv] .filter(Boolean) @@ -168,6 +182,7 @@ export const setVerifyingKeys = async ({ voteOptionTreeDepth, messageBatchSize, modes, + pollZkeys, processZkeys, tallyZkeys, ); @@ -182,6 +197,8 @@ export const setVerifyingKeys = async ({ // confirm that they were actually set correctly if (useQuadraticVoting) { + const pollVkOnChain = await vkRegistryContract.getPollVk(stateTreeDepth, voteOptionTreeDepth); + const processVkOnChain = await vkRegistryContract.getProcessVk( stateTreeDepth, voteOptionTreeDepth, @@ -196,6 +213,10 @@ export const setVerifyingKeys = async ({ EMode.QV, ); + if (!compareVks(pollVk as VerifyingKey, pollVkOnChain)) { + logError("pollVk mismatch"); + } + if (!compareVks(processVkQv as VerifyingKey, processVkOnChain)) { logError("processVk mismatch"); } @@ -204,6 +225,8 @@ export const setVerifyingKeys = async ({ logError("tallyVk mismatch"); } } else { + const pollVkOnChain = await vkRegistryContract.getPollVk(stateTreeDepth, voteOptionTreeDepth); + const processVkOnChain = await vkRegistryContract.getProcessVk( stateTreeDepth, voteOptionTreeDepth, @@ -218,6 +241,10 @@ export const setVerifyingKeys = async ({ EMode.NON_QV, ); + if (!compareVks(pollVk as VerifyingKey, pollVkOnChain)) { + logError("pollVk mismatch"); + } + if (!compareVks(processVkNonQv as VerifyingKey, processVkOnChain)) { logError("processVk mismatch"); } @@ -238,11 +265,13 @@ interface ICheckZkeyFilepathsArgs { messageBatchSize: number; voteOptionTreeDepth: number; intStateTreeDepth: number; + pollJoiningZkeyPath?: string; processMessagesZkeyPath?: string; tallyVotesZkeyPath?: string; } function checkZkeyFilepaths({ + pollJoiningZkeyPath, processMessagesZkeyPath, tallyVotesZkeyPath, stateTreeDepth, @@ -250,11 +279,20 @@ function checkZkeyFilepaths({ voteOptionTreeDepth, intStateTreeDepth, }: ICheckZkeyFilepathsArgs): void { - if (!processMessagesZkeyPath || !tallyVotesZkeyPath) { + if (!pollJoiningZkeyPath || !processMessagesZkeyPath || !tallyVotesZkeyPath) { return; } // Check the pm zkey filename against specified params + const pjMatch = pollJoiningZkeyPath.match(/.+_(\d+)/); + + if (!pjMatch) { + logError(`${pollJoiningZkeyPath} has an invalid filename`); + return; + } + + const pjStateTreeDepth = Number(pjMatch[1]); + const pmMatch = processMessagesZkeyPath.match(/.+_(\d+)-(\d+)-(\d+)/); if (!pmMatch) { @@ -278,6 +316,7 @@ function checkZkeyFilepaths({ const tvVoteOptionTreeDepth = Number(tvMatch[3]); if ( + stateTreeDepth !== pjStateTreeDepth || stateTreeDepth !== pmStateTreeDepth || messageBatchSize !== pmMsgBatchSize || voteOptionTreeDepth !== pmVoteOptionTreeDepth || diff --git a/packages/cli/ts/index.ts b/packages/cli/ts/index.ts index 388830eeb7..056a0d2e80 100644 --- a/packages/cli/ts/index.ts +++ b/packages/cli/ts/index.ts @@ -29,6 +29,8 @@ import { checkVerifyingKeys, genLocalState, extractVkToFile, + joinPoll, + isJoinedUser, } from "./commands"; import { TallyData, logError, promptSensitiveValue, readContractAddress } from "./utils"; @@ -102,6 +104,10 @@ program "-t, --tally-votes-zkey ", "the tally votes zkey path (see different options for zkey files to use specific circuits https://maci.pse.dev/docs/trusted-setup, https://maci.pse.dev/docs/testing/#pre-compiled-artifacts-for-testing)", ) + .requiredOption( + "-pj, --poll-joining-zkey ", + "the poll join zkey path (see different options for zkey files to use specific circuits https://maci.pse.dev/docs/trusted-setup, https://maci.pse.dev/docs/testing/#pre-compiled-artifacts-for-testing)", + ) .action(async (cmdOptions) => { try { const signer = await getSigner(); @@ -113,6 +119,7 @@ program messageBatchSize: cmdOptions.msgBatchSize, processMessagesZkeyPath: cmdOptions.processMessagesZkey, tallyVotesZkeyPath: cmdOptions.tallyVotesZkey, + pollJoiningZkeyPath: cmdOptions.pollJoiningZkey, vkRegistry: cmdOptions.vkContract, quiet: cmdOptions.quiet, useQuadraticVoting: cmdOptions.useQuadraticVoting, @@ -204,6 +211,65 @@ program program.error((error as Error).message, { exitCode: 1 }); } }); +program + .command("joinPoll") + .description("join the poll") + .requiredOption("-sk, --priv-key ", "the private key") + .option("-i, --state-index ", "the user's state index", BigInt) + .requiredOption("-esk, --poll-priv-key ", "the user ephemeral private key for the poll") + .option( + "-nv, --new-voice-credit-balance ", + "the voice credit balance of the user for the poll", + BigInt, + ) + .requiredOption("-pid, --poll-id ", "the id of the poll", BigInt) + .option("-x, --maci-address ", "the MACI contract address") + .option("-q, --quiet ", "whether to print values to the console", (value) => value === "true", false) + .option("-st, --state-file ", "the path to the state file containing the serialized maci state") + .option("-sb, --start-block ", "the block number to start looking for events from", parseInt) + .option("-eb, --end-block ", "the block number to end looking for events from", parseInt) + .option("-bb, --blocks-per-batch ", "the number of blocks to process per batch", parseInt) + .option("-tx, --transaction-hash ", "transaction hash of MACI contract creation") + .option("-pw, --poll-wasm ", "the path to the poll witness generation wasm binary") + .requiredOption( + "-pj, --poll-joining-zkey ", + "the poll join zkey path (see different options for zkey files to use specific circuits https://maci.pse.dev/docs/trusted-setup, https://maci.pse.dev/docs/testing/#pre-compiled-artifacts-for-testing)", + ) + .option("-w, --wasm", "whether to use the wasm binaries") + .option("-r, --rapidsnark ", "the path to the rapidsnark binary") + .option("-wp, --poll-witnessgen ", "the path to the poll witness generation binary") + .action(async (cmdObj) => { + try { + const signer = await getSigner(); + const network = await signer.provider?.getNetwork(); + + const maciAddress = cmdObj.maciAddress || readContractAddress("MACI", network?.name); + const privateKey = cmdObj.privKey || (await promptSensitiveValue("Insert your MACI private key")); + + await joinPoll({ + maciAddress, + privateKey, + pollPrivKey: cmdObj.pollPrivKey, + stateIndex: cmdObj.stateIndex || null, + newVoiceCreditBalance: cmdObj.newVoiceCreditBalance || null, + stateFile: cmdObj.stateFile, + pollId: cmdObj.pollId, + signer, + startBlock: cmdObj.startBlock, + endBlock: cmdObj.endBlock, + blocksPerBatch: cmdObj.blocksPerBatch, + transactionHash: cmdObj.transactionHash, + pollJoiningZkey: cmdObj.pollJoiningZkey, + pollWasm: cmdObj.pollWasm, + quiet: cmdObj.quiet, + useWasm: cmdObj.wasm, + rapidsnark: cmdObj.rapidsnark, + pollWitgen: cmdObj.pollWitnessgen, + }); + } catch (error) { + program.error((error as Error).message, { exitCode: 1 }); + } + }); program .command("setVerifyingKeys") .description("set the verifying keys") @@ -211,6 +277,10 @@ program .requiredOption("-i, --int-state-tree-depth ", "the intermediate state tree depth", parseInt) .requiredOption("-v, --vote-option-tree-depth ", "the vote option tree depth", parseInt) .requiredOption("-b, --msg-batch-size ", "the message batch size", parseInt) + .option( + "-pj, --poll-joining-zkey ", + "the poll join zkey path (see different options for zkey files to use specific circuits https://maci.pse.dev/docs/trusted-setup, https://maci.pse.dev/docs/testing/#pre-compiled-artifacts-for-testing)", + ) .option( "-pqv, --process-messages-zkey-qv ", "the process messages qv zkey path (see different options for zkey files to use specific circuits https://maci.pse.dev/docs/trusted-setup, https://maci.pse.dev/docs/testing/#pre-compiled-artifacts-for-testing)", @@ -245,6 +315,7 @@ program intStateTreeDepth: cmdObj.intStateTreeDepth, voteOptionTreeDepth: cmdObj.voteOptionTreeDepth, messageBatchSize: cmdObj.msgBatchSize, + pollJoiningZkeyPath: cmdObj.pollJoiningZkey, processMessagesZkeyPathQv: cmdObj.processMessagesZkeyQv, tallyVotesZkeyPathQv: cmdObj.tallyVotesZkeyQv, processMessagesZkeyPathNonQv: cmdObj.processMessagesZkeyNonQv, @@ -342,6 +413,10 @@ program program .command("extractVkToFile") .description("extract vkey to json file") + .requiredOption( + "-pj, --poll-joining-zkey ", + "the poll join zkey path (see different options for zkey files to use specific circuits https://maci.pse.dev/docs/trusted-setup, https://maci.pse.dev/docs/testing/#pre-compiled-artifacts-for-testing)", + ) .requiredOption( "-pqv, --process-messages-zkey-qv ", "the process messages qv zkey path (see different options for zkey files to use specific circuits https://maci.pse.dev/docs/trusted-setup, https://maci.pse.dev/docs/testing/#pre-compiled-artifacts-for-testing)", @@ -366,6 +441,7 @@ program tallyVotesZkeyPathQv: cmdObj.tallyVotesZkeyQv, processMessagesZkeyPathNonQv: cmdObj.processMessagesZkeyNonQv, tallyVotesZkeyPathNonQv: cmdObj.tallyVotesZkeyNonQv, + pollJoiningZkeyPath: cmdObj.pollJoiningZkey, outputFilePath: cmdObj.outputFile, }); } catch (error) { @@ -423,6 +499,36 @@ program program.error((error as Error).message, { exitCode: 1 }); } }); +program + .command("isJoinedUser") + .description("Checks if user is joined to the poll with public key") + .requiredOption("-p, --pubkey ", "the MACI public key") + .option("-x, --maci-address ", "the MACI contract address") + .requiredOption("-o, --poll-id ", "the poll id", BigInt) + .option("-q, --quiet ", "whether to print values to the console", (value) => value === "true", false) + .option("-sb, --start-block ", "the block number to start looking for events from", parseInt) + .option("-eb, --end-block ", "the block number to end looking for events from", parseInt) + .option("-bb, --blocks-per-batch ", "the number of blocks to process per batch", parseInt) + .action(async (cmdObj) => { + try { + const signer = await getSigner(); + const network = await signer.provider?.getNetwork(); + + const maciAddress = cmdObj.maciAddress || readContractAddress("MACI", network?.name); + + await isJoinedUser({ + pollPubKey: cmdObj.pubkey, + startBlock: cmdObj.startBlock!, + maciAddress, + pollId: cmdObj.pollId, + signer, + quiet: cmdObj.quiet, + }); + } catch (error) { + program.error((error as Error).message, { exitCode: 1 }); + } + }); + program .command("getPoll") .description("Get deployed poll from MACI contract") @@ -514,6 +620,7 @@ program .option("-pd, --process-witnessdat ", "the path to the process witness dat file") .option("-wt, --tally-witnessgen ", "the path to the tally witness generation binary") .option("-td, --tally-witnessdat ", "the path to the tally witness dat file") + .requiredOption("-zpj, --poll-joining-zkey ", "the path to the poll join zkey") .requiredOption("-zp, --process-zkey ", "the path to the process zkey") .requiredOption("-zt, --tally-zkey ", "the path to the tally zkey") .option("-q, --quiet ", "whether to print values to the console", (value) => value === "true", false) @@ -656,6 +763,8 @@ export { isRegisteredUser, timeTravel, verify, + joinPoll, + isJoinedUser, } from "./commands"; export type { diff --git a/packages/cli/ts/utils/constants.ts b/packages/cli/ts/utils/constants.ts index 07522cc7a0..b3d36b4992 100644 --- a/packages/cli/ts/utils/constants.ts +++ b/packages/cli/ts/utils/constants.ts @@ -8,3 +8,5 @@ export const oldContractAddressStoreName = "contractAddresses.old.json"; export const contractAddressesStore = path.resolve(__dirname, "..", "..", contractAddressStoreName); // local file path where we are storing a previous deployment's contract addresses export const oldContractAddressesStore = path.resolve(__dirname, "..", "..", oldContractAddressStoreName); + +export const BLOCKS_STEP = 1000; diff --git a/packages/cli/ts/utils/index.ts b/packages/cli/ts/utils/index.ts index 7f854dbfbc..1b72094616 100644 --- a/packages/cli/ts/utils/index.ts +++ b/packages/cli/ts/utils/index.ts @@ -4,6 +4,7 @@ export { oldContractAddressStoreName, contractAddressesStore, oldContractAddressesStore, + BLOCKS_STEP, } from "./constants"; export { contractExists, currentBlockTimestamp } from "./contracts"; export { @@ -27,6 +28,10 @@ export type { TimeTravelArgs, SignupArgs, ISignupData, + IJoinPollArgs, + IJoinPollData, + IJoinedUserArgs, + IParsePollJoinEventsArgs, SetVerifyingKeysArgs, MergeSignupsArgs, ProveOnChainArgs, diff --git a/packages/cli/ts/utils/interfaces.ts b/packages/cli/ts/utils/interfaces.ts index eaf9d883b1..f1054c0b83 100644 --- a/packages/cli/ts/utils/interfaces.ts +++ b/packages/cli/ts/utils/interfaces.ts @@ -2,7 +2,7 @@ import { MACI } from "maci-contracts/typechain-types"; import { PubKey } from "maci-domainobjs"; import type { Provider, Signer } from "ethers"; -import type { SnarkProof } from "maci-contracts"; +import type { Poll, SnarkProof } from "maci-contracts"; import type { CircuitInputs } from "maci-core"; import type { IMessageContractParams } from "maci-domainobjs"; import type { Groth16Proof, PublicSignals } from "snarkjs"; @@ -174,6 +174,11 @@ export interface CheckVerifyingKeysArgs { */ messageBatchSize: number; + /** + * The path to the poll zkey + */ + pollJoiningZkeyPath: string; + /** * The path to the process messages zkey */ @@ -315,6 +320,150 @@ export interface DeployPollArgs { useQuadraticVoting?: boolean; } +/** + * Interface for the arguments to the isJoinedUser command + */ +export interface IJoinedUserArgs { + /** + * The address of the MACI contract + */ + maciAddress: string; + + /** + * The id of the poll + */ + pollId: bigint; + + /** + * Poll public key for the poll + */ + pollPubKey: string; + + /** + * A signer object + */ + signer: Signer; + + /** + * The start block number + */ + startBlock: number; + + /** + * Whether to log the output + */ + quiet: boolean; +} +/** + * Interface for the arguments to the joinPoll command + */ +export interface IJoinPollArgs { + /** + * A signer object + */ + signer: Signer; + + /** + * The private key of the user + */ + privateKey: string; + + /** + * User's credit balance for voting within this poll + */ + newVoiceCreditBalance: bigint | null; + + /** + * The id of the poll + */ + pollId: bigint; + + /** + * The index of the state leaf + */ + stateIndex: bigint | null; + + /** + * Whether to log the output + */ + quiet: boolean; + + /** + * Path to the state file with MACI state + */ + stateFile?: string; + + /** + * The address of the MACI contract + */ + maciAddress: string; + + /** + * The end block number + */ + endBlock?: number; + + /** + * The start block number + */ + startBlock?: number; + + /** + * The number of blocks to fetch per batch + */ + blocksPerBatch?: number; + + /** + * The transaction hash of the first transaction + */ + transactionHash?: string; + + /** + * The path to the poll zkey file + */ + pollJoiningZkey: string; + + /** + * Whether to use wasm or rapidsnark + */ + useWasm?: boolean; + + /** + * The path to the rapidsnark binary + */ + rapidsnark?: string; + + /** + * The path to the poll witnessgen binary + */ + pollWitgen?: string; + + /** + * The path to the poll wasm file + */ + pollWasm?: string; + + /** + * Poll private key for the poll + */ + pollPrivKey: string; +} + +/** + * Interface for the return data to the joinPoll command + */ +export interface IJoinPollData { + /** + * The poll state index of the joined user + */ + pollStateIndex: string; + + /** + * The join poll transaction hash + */ + hash: string; +} + /** * Interface for the arguments to the genLocalState command * Generate a local MACI state from the smart contracts events @@ -571,12 +720,12 @@ export interface ProveOnChainArgs { */ export interface PublishArgs extends IPublishMessage { /** - * The public key of the user + * The poll public key */ pubkey: string; /** - * The private key of the user + * The poll private key */ privateKey: string; @@ -715,6 +864,11 @@ export interface SetVerifyingKeysArgs { */ messageBatchSize: number; + /** + * The path to the poll zkey + */ + pollJoiningZkeyPath?: string; + /** * The path to the process messages qv zkey */ @@ -1017,6 +1171,11 @@ export interface DeployVkRegistryArgs { } export interface ExtractVkToFileArgs { + /** + * File path for poll zkey + */ + pollJoiningZkeyPath: string; + /** * File path for processMessagesQv zkey */ @@ -1043,6 +1202,31 @@ export interface ExtractVkToFileArgs { outputFilePath: string; } +/** + * Interface for the arguments to the parsePollJoinEvents function + */ +export interface IParsePollJoinEventsArgs { + /** + * The MACI contract + */ + pollContract: Poll; + + /** + * The start block + */ + startBlock: number; + + /** + * The current block + */ + currentBlock: number; + + /** + * The public key + */ + pollPublicKey: PubKey; +} + /** * Interface for the arguments to the parseSignupEvents function */ diff --git a/packages/contracts/contracts/MACI.sol b/packages/contracts/contracts/MACI.sol index ce295847d2..f05ecbd0f5 100644 --- a/packages/contracts/contracts/MACI.sol +++ b/packages/contracts/contracts/MACI.sol @@ -13,7 +13,7 @@ import { Params } from "./utilities/Params.sol"; import { Utilities } from "./utilities/Utilities.sol"; import { DomainObjs } from "./utilities/DomainObjs.sol"; import { CurveBabyJubJub } from "./crypto/BabyJubJub.sol"; -import { InternalLazyIMT, LazyIMTData } from "./trees/LazyIMT.sol"; +import { InternalLeanIMT, LeanIMTData } from "./trees/LeanIMT.sol"; /// @title MACI - Minimum Anti-Collusion Infrastructure Version 1 /// @notice A contract which allows users to sign up, and deploy new polls @@ -56,7 +56,7 @@ contract MACI is IMACI, DomainObjs, Params, Utilities { /// @notice The state tree. Represents a mapping between each user's public key /// and their voice credit balance. - LazyIMTData public lazyIMTData; + LeanIMTData public leanIMTData; /// @notice Address of the SignUpGatekeeper, a contract which determines whether a /// user may sign up to vote @@ -66,6 +66,10 @@ contract MACI is IMACI, DomainObjs, Params, Utilities { /// balance per user InitialVoiceCreditProxy public immutable initialVoiceCreditProxy; + /// @notice The array of the state tree roots for each sign up + /// For the N'th sign up, the state tree root will be stored at the index N + uint256[] public stateRootsOnSignUp; + /// @notice A struct holding the addresses of poll, mp and tally struct PollContracts { address poll; @@ -79,7 +83,8 @@ contract MACI is IMACI, DomainObjs, Params, Utilities { uint256 indexed _userPubKeyX, uint256 indexed _userPubKeyY, uint256 _voiceCreditBalance, - uint256 _timestamp + uint256 _timestamp, + uint256 _stateLeaf ); event DeployPoll( uint256 _pollId, @@ -113,8 +118,8 @@ contract MACI is IMACI, DomainObjs, Params, Utilities { uint256[5] memory _emptyBallotRoots ) payable { // initialize and insert the blank leaf - InternalLazyIMT._init(lazyIMTData, _stateTreeDepth); - InternalLazyIMT._insert(lazyIMTData, BLANK_STATE_LEAF_HASH); + InternalLeanIMT._insert(leanIMTData, BLANK_STATE_LEAF_HASH); + stateRootsOnSignUp.push(BLANK_STATE_LEAF_HASH); pollFactory = _pollFactory; messageProcessorFactory = _messageProcessorFactory; @@ -164,9 +169,13 @@ contract MACI is IMACI, DomainObjs, Params, Utilities { // Create a state leaf and insert it into the tree. uint256 stateLeaf = hashStateLeaf(StateLeaf(_pubKey, voiceCreditBalance, timestamp)); - InternalLazyIMT._insert(lazyIMTData, stateLeaf); + InternalLeanIMT._insert(leanIMTData, stateLeaf); + + // Store the current state tree root in the array + uint256 stateRoot = InternalLeanIMT._root(leanIMTData); + stateRootsOnSignUp.push(stateRoot); - emit SignUp(lazyIMTData.numberOfLeaves - 1, _pubKey.x, _pubKey.y, voiceCreditBalance, timestamp); + emit SignUp(leanIMTData.size - 1, _pubKey.x, _pubKey.y, voiceCreditBalance, timestamp, stateLeaf); } /// @notice Deploy a new Poll contract. @@ -209,6 +218,12 @@ contract MACI is IMACI, DomainObjs, Params, Utilities { vkRegistry: IVkRegistry(_vkRegistry) }); + ExtContracts memory extContracts = ExtContracts({ + maci: IMACI(address(this)), + verifier: IVerifier(_verifier), + vkRegistry: IVkRegistry(_vkRegistry) + }); + address p = pollFactory.deploy( _duration, maxVoteOptions, @@ -231,7 +246,7 @@ contract MACI is IMACI, DomainObjs, Params, Utilities { /// @inheritdoc IMACI function getStateTreeRoot() public view returns (uint256 root) { - root = InternalLazyIMT._root(lazyIMTData); + root = InternalLeanIMT._root(leanIMTData); } /// @notice Get the Poll details @@ -244,6 +259,11 @@ contract MACI is IMACI, DomainObjs, Params, Utilities { /// @inheritdoc IMACI function numSignUps() public view returns (uint256 signUps) { - signUps = lazyIMTData.numberOfLeaves; + signUps = leanIMTData.size; + } + + /// @inheritdoc IMACI + function getStateRootOnIndexedSignUp(uint256 _index) external view returns (uint256) { + return stateRootsOnSignUp[_index]; } } diff --git a/packages/contracts/contracts/Poll.sol b/packages/contracts/contracts/Poll.sol index d3ef94cede..6bf6d5377c 100644 --- a/packages/contracts/contracts/Poll.sol +++ b/packages/contracts/contracts/Poll.sol @@ -5,8 +5,11 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Params } from "./utilities/Params.sol"; import { EmptyBallotRoots } from "./trees/EmptyBallotRoots.sol"; import { SnarkCommon } from "./crypto/SnarkCommon.sol"; -import { IPoll } from "./interfaces/IPoll.sol"; +import { LazyIMTData, InternalLazyIMT } from "./trees/LazyIMT.sol"; import { IMACI } from "./interfaces/IMACI.sol"; +import { IPoll } from "./interfaces/IPoll.sol"; +import { IVerifier } from "./interfaces/IVerifier.sol"; +import { IVkRegistry } from "./interfaces/IVkRegistry.sol"; import { Utilities } from "./utilities/Utilities.sol"; import { CurveBabyJubJub } from "./crypto/BabyJubJub.sol"; @@ -78,6 +81,16 @@ contract Poll is Params, Utilities, SnarkCommon, EmptyBallotRoots, IPoll { /// @notice flag for batch padding bool public isBatchHashesPadded; + /// @notice Poll state tree for anonymous joining + LazyIMTData public pollStateTree; + + /// @notice The hash of a blank state leaf + uint256 internal constant BLANK_STATE_LEAF_HASH = + uint256(6769006970205099520508948723718471724660867171122235270773600567925038008762); + + /// @notice Poll voting nullifier + mapping(uint256 => bool) private pollNullifier; + error VotingPeriodOver(); error VotingPeriodNotOver(); error PollAlreadyInit(); @@ -86,9 +99,19 @@ contract Poll is Params, Utilities, SnarkCommon, EmptyBallotRoots, IPoll { error StateAlreadyMerged(); error InvalidBatchLength(); error BatchHashesAlreadyPadded(); + error UserAlreadyJoined(); + error InvalidPollProof(); event PublishMessage(Message _message, PubKey _encPubKey); - event MergeMaciState(uint256 indexed _stateRoot, uint256 indexed _numSignups); + event MergeState(uint256 indexed _stateRoot, uint256 indexed _numSignups); + event PollJoined( + uint256 indexed _pollPubKeyX, + uint256 indexed _pollPubKeyY, + uint256 _newVoiceCreditBalance, + uint256 _timestamp, + uint256 _nullifier, + uint256 _pollStateIndex + ); /// @notice Each MACI instance can have multiple Polls. /// When a Poll is deployed, its voting period starts immediately. @@ -172,6 +195,9 @@ contract Poll is Params, Utilities, SnarkCommon, EmptyBallotRoots, IPoll { batchHashes.push(NOTHING_UP_MY_SLEEVE); updateChainHash(placeholderLeaf); + InternalLazyIMT._init(pollStateTree, extContracts.maci.stateTreeDepth()); + InternalLazyIMT._insert(pollStateTree, BLANK_STATE_LEAF_HASH); + emit PublishMessage(_message, _padKey); } @@ -242,8 +268,75 @@ contract Poll is Params, Utilities, SnarkCommon, EmptyBallotRoots, IPoll { } } + /// @notice Join the poll for voting + /// @param _nullifier Hashed user's private key to check whether user has already voted + /// @param _pubKey Poll user's public key + /// @param _newVoiceCreditBalance User's credit balance for voting within this poll + /// @param _stateRootIndex Index of the MACI's stateRootOnSignUp when the user signed up + /// @param _proof The zk-SNARK proof + function joinPoll( + uint256 _nullifier, + PubKey memory _pubKey, + uint256 _newVoiceCreditBalance, + uint256 _stateRootIndex, + uint256[8] memory _proof + ) external { + // Whether the user has already joined + if (pollNullifier[_nullifier]) { + revert UserAlreadyJoined(); + } + + // Verify user's proof + if (!verifyPollProof(_nullifier, _newVoiceCreditBalance, _stateRootIndex, _pubKey, _proof)) { + revert InvalidPollProof(); + } + + // Store user in the pollStateTree + uint256 timestamp = block.timestamp; + uint256 stateLeaf = hashStateLeaf(StateLeaf(_pubKey, _newVoiceCreditBalance, timestamp)); + InternalLazyIMT._insert(pollStateTree, stateLeaf); + + // Set nullifier for user's private key + pollNullifier[_nullifier] = true; + + uint256 pollStateIndex = pollStateTree.numberOfLeaves - 1; + emit PollJoined(_pubKey.x, _pubKey.y, _newVoiceCreditBalance, timestamp, _nullifier, pollStateIndex); + } + + /// @notice Verify the proof for Poll + /// @param _nullifier Hashed user's private key to check whether user has already voted + /// @param _voiceCreditBalance User's credit balance for voting + /// @param _index Index of the MACI's stateRootOnSignUp when the user signed up + /// @param _pubKey Poll user's public key + /// @param _proof The zk-SNARK proof + /// @return isValid Whether the proof is valid + function verifyPollProof( + uint256 _nullifier, + uint256 _voiceCreditBalance, + uint256 _index, + PubKey memory _pubKey, + uint256[8] memory _proof + ) internal returns (bool isValid) { + // Get the verifying key from the VkRegistry + VerifyingKey memory vk = extContracts.vkRegistry.getPollVk( + extContracts.maci.stateTreeDepth(), + treeDepths.voteOptionTreeDepth + ); + + // Generate the circuit public input + uint256[] memory input = new uint256[](5); + input[0] = _nullifier; + input[1] = _voiceCreditBalance; + input[2] = extContracts.maci.getStateRootOnIndexedSignUp(_index); + input[3] = _pubKey.x; + input[4] = _pubKey.y; + uint256 publicInputHash = sha256Hash(input); + + isValid = extContracts.verifier.verify(_proof, vk, publicInputHash); + } + /// @inheritdoc IPoll - function mergeMaciState() public isAfterVotingDeadline { + function mergeState() public isAfterVotingDeadline { // This function can only be called once per Poll after the voting // deadline if (stateMerged) revert StateAlreadyMerged(); @@ -251,8 +344,7 @@ contract Poll is Params, Utilities, SnarkCommon, EmptyBallotRoots, IPoll { // set merged to true so it cannot be called again stateMerged = true; - uint256 _mergedStateRoot = extContracts.maci.getStateTreeRoot(); - mergedStateRoot = _mergedStateRoot; + mergedStateRoot = InternalLazyIMT._root(pollStateTree); // Set currentSbCommitment uint256[3] memory sb; @@ -262,8 +354,8 @@ contract Poll is Params, Utilities, SnarkCommon, EmptyBallotRoots, IPoll { currentSbCommitment = hash3(sb); - // get number of signups and cache in a var for later use - uint256 _numSignups = extContracts.maci.numSignUps(); + // get number of joined users and cache in a var for later use + uint256 _numSignups = pollStateTree.numberOfLeaves; numSignups = _numSignups; // dynamically determine the actual depth of the state tree @@ -274,7 +366,7 @@ contract Poll is Params, Utilities, SnarkCommon, EmptyBallotRoots, IPoll { actualStateTreeDepth = depth; - emit MergeMaciState(_mergedStateRoot, _numSignups); + emit MergeState(mergedStateRoot, numSignups); } /// @inheritdoc IPoll diff --git a/packages/contracts/contracts/VkRegistry.sol b/packages/contracts/contracts/VkRegistry.sol index d69c17eb6c..e65fc0d942 100644 --- a/packages/contracts/contracts/VkRegistry.sol +++ b/packages/contracts/contracts/VkRegistry.sol @@ -17,11 +17,17 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry mapping(Mode => mapping(uint256 => VerifyingKey)) internal tallyVks; mapping(Mode => mapping(uint256 => bool)) internal tallyVkSet; + mapping(uint256 => VerifyingKey) internal pollVks; + mapping(uint256 => bool) internal pollVkSet; + + event PollVkSet(uint256 _sig); event ProcessVkSet(uint256 _sig, Mode _mode); event TallyVkSet(uint256 _sig, Mode _mode); + error PollVkAlreadySet(); error ProcessVkAlreadySet(); error TallyVkAlreadySet(); + error PollVkNotSet(); error ProcessVkNotSet(); error TallyVkNotSet(); error SubsidyVkNotSet(); @@ -31,6 +37,12 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry // solhint-disable-next-line no-empty-blocks constructor() payable {} + /// @notice Check if the poll verifying key is set + /// @param _sig The signature + /// @return isSet whether the verifying key is set + function isPollVkSet(uint256 _sig) public view returns (bool isSet) { + isSet = pollVkSet[_sig]; + } /// @notice Check if the process verifying key is set /// @param _sig The signature /// @param _mode QV or Non-QV @@ -47,6 +59,12 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry isSet = tallyVkSet[_mode][_sig]; } + /// @notice generate the signature for the poll verifying key + /// @param _stateTreeDepth The state tree depth + /// @param _voteOptionTreeDepth The vote option tree depth + function genPollVkSig(uint256 _stateTreeDepth, uint256 _voteOptionTreeDepth) public pure returns (uint256 sig) { + sig = (_stateTreeDepth << 64) + _voteOptionTreeDepth; + } /// @notice generate the signature for the process verifying key /// @param _stateTreeDepth The state tree depth /// @param _voteOptionTreeDepth The vote option tree depth @@ -79,6 +97,7 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry /// @param _voteOptionTreeDepth The vote option tree depth /// @param _messageBatchSize The message batch size /// @param _modes Array of QV or Non-QV modes (must have the same length as process and tally keys) + /// @param _pollVk The poll verifying key /// @param _processVks The process verifying keys (must have the same length as modes) /// @param _tallyVks The tally verifying keys (must have the same length as modes) function setVerifyingKeysBatch( @@ -87,6 +106,7 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry uint256 _voteOptionTreeDepth, uint8 _messageBatchSize, Mode[] calldata _modes, + VerifyingKey calldata _pollVk, VerifyingKey[] calldata _processVks, VerifyingKey[] calldata _tallyVks ) public onlyOwner { @@ -96,6 +116,8 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry uint256 length = _modes.length; + setPollVkKey(_stateTreeDepth, _voteOptionTreeDepth, _pollVk); + for (uint256 index = 0; index < length; ) { setVerifyingKeys( _stateTreeDepth, @@ -114,7 +136,6 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry } /// @notice Set the process and tally verifying keys for a certain combination - /// of parameters /// @param _stateTreeDepth The state tree depth /// @param _intStateTreeDepth The intermediate state tree depth /// @param _voteOptionTreeDepth The vote option tree depth @@ -130,15 +151,28 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry Mode _mode, VerifyingKey calldata _processVk, VerifyingKey calldata _tallyVk + ) public onlyOwner { + setProcessVkKey(_stateTreeDepth, _voteOptionTreeDepth, _messageBatchSize, _mode, _processVk); + setTallyVkKey(_stateTreeDepth, _intStateTreeDepth, _voteOptionTreeDepth, _mode, _tallyVk); + } + + /// @notice Set the process verifying key for a certain combination of parameters + /// @param _stateTreeDepth The state tree depth + /// @param _voteOptionTreeDepth The vote option tree depth + /// @param _messageBatchSize The message batch size + /// @param _mode QV or Non-QV + /// @param _processVk The process verifying key + function setProcessVkKey( + uint256 _stateTreeDepth, + uint256 _voteOptionTreeDepth, + uint8 _messageBatchSize, + Mode _mode, + VerifyingKey calldata _processVk ) public onlyOwner { uint256 processVkSig = genProcessVkSig(_stateTreeDepth, _voteOptionTreeDepth, _messageBatchSize); if (processVkSet[_mode][processVkSig]) revert ProcessVkAlreadySet(); - uint256 tallyVkSig = genTallyVkSig(_stateTreeDepth, _intStateTreeDepth, _voteOptionTreeDepth); - - if (tallyVkSet[_mode][tallyVkSig]) revert TallyVkAlreadySet(); - VerifyingKey storage processVk = processVks[_mode][processVkSig]; processVk.alpha1 = _processVk.alpha1; processVk.beta2 = _processVk.beta2; @@ -156,6 +190,26 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry processVkSet[_mode][processVkSig] = true; + emit ProcessVkSet(processVkSig, _mode); + } + + /// @notice Set the tally verifying key for a certain combination of parameters + /// @param _stateTreeDepth The state tree depth + /// @param _intStateTreeDepth The intermediate state tree depth + /// @param _voteOptionTreeDepth The vote option tree depth + /// @param _mode QV or Non-QV + /// @param _tallyVk The tally verifying key + function setTallyVkKey( + uint256 _stateTreeDepth, + uint256 _intStateTreeDepth, + uint256 _voteOptionTreeDepth, + Mode _mode, + VerifyingKey calldata _tallyVk + ) public onlyOwner { + uint256 tallyVkSig = genTallyVkSig(_stateTreeDepth, _intStateTreeDepth, _voteOptionTreeDepth); + + if (tallyVkSet[_mode][tallyVkSig]) revert TallyVkAlreadySet(); + VerifyingKey storage tallyVk = tallyVks[_mode][tallyVkSig]; tallyVk.alpha1 = _tallyVk.alpha1; tallyVk.beta2 = _tallyVk.beta2; @@ -174,7 +228,39 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry tallyVkSet[_mode][tallyVkSig] = true; emit TallyVkSet(tallyVkSig, _mode); - emit ProcessVkSet(processVkSig, _mode); + } + + /// @notice Set the poll verifying key for a certain combination of parameters + /// @param _stateTreeDepth The state tree depth + /// @param _voteOptionTreeDepth The vote option tree depth + /// @param _pollVk The poll verifying key + function setPollVkKey( + uint256 _stateTreeDepth, + uint256 _voteOptionTreeDepth, + VerifyingKey calldata _pollVk + ) public onlyOwner { + uint256 pollVkSig = genPollVkSig(_stateTreeDepth, _voteOptionTreeDepth); + + if (pollVkSet[pollVkSig]) revert PollVkAlreadySet(); + + VerifyingKey storage pollVk = pollVks[pollVkSig]; + pollVk.alpha1 = _pollVk.alpha1; + pollVk.beta2 = _pollVk.beta2; + pollVk.gamma2 = _pollVk.gamma2; + pollVk.delta2 = _pollVk.delta2; + + uint256 pollIcLength = _pollVk.ic.length; + for (uint256 i = 0; i < pollIcLength; ) { + pollVk.ic.push(_pollVk.ic[i]); + + unchecked { + i++; + } + } + + pollVkSet[pollVkSig] = true; + + emit PollVkSet(pollVkSig); } /// @notice Check if the process verifying key is set @@ -253,4 +339,23 @@ contract VkRegistry is Ownable(msg.sender), DomainObjs, SnarkCommon, IVkRegistry vk = getTallyVkBySig(sig, _mode); } + + /// @notice Get the poll verifying key by signature + /// @param _sig The signature + /// @return vk The verifying key + function getPollVkBySig(uint256 _sig) public view returns (VerifyingKey memory vk) { + if (!pollVkSet[_sig]) revert PollVkNotSet(); + + vk = pollVks[_sig]; + } + + /// @inheritdoc IVkRegistry + function getPollVk( + uint256 _stateTreeDepth, + uint256 _voteOptionTreeDepth + ) public view returns (VerifyingKey memory vk) { + uint256 sig = genPollVkSig(_stateTreeDepth, _voteOptionTreeDepth); + + vk = getPollVkBySig(sig); + } } diff --git a/packages/contracts/contracts/interfaces/IMACI.sol b/packages/contracts/contracts/interfaces/IMACI.sol index 050c780503..eb4fc09edb 100644 --- a/packages/contracts/contracts/interfaces/IMACI.sol +++ b/packages/contracts/contracts/interfaces/IMACI.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import { IVerifier } from "./IVerifier.sol"; +import { IVkRegistry } from "./IVkRegistry.sol"; +import { DomainObjs } from "../utilities/DomainObjs.sol"; + /// @title IMACI /// @notice MACI interface interface IMACI { @@ -12,6 +16,11 @@ interface IMACI { /// @return The Merkle root function getStateTreeRoot() external view returns (uint256); + /// @notice Return the state root when the '_index' user signed up + /// @param _index The serial number when the user signed up + /// @return The Merkle root + function getStateRootOnIndexedSignUp(uint256 _index) external view returns (uint256); + /// @notice Get the number of signups /// @return numsignUps The number of signups function numSignUps() external view returns (uint256); diff --git a/packages/contracts/contracts/interfaces/IPoll.sol b/packages/contracts/contracts/interfaces/IPoll.sol index 00a7942ed3..c918791ed0 100644 --- a/packages/contracts/contracts/interfaces/IPoll.sol +++ b/packages/contracts/contracts/interfaces/IPoll.sol @@ -7,6 +7,15 @@ import { IMACI } from "./IMACI.sol"; /// @title IPoll /// @notice Poll interface interface IPoll { + /// @notice Join the poll + function joinPoll( + uint256 _nullifier, + DomainObjs.PubKey memory _pubKey, + uint256 _newVoiceCreditBalance, + uint256 _stateRootIndex, + uint256[8] memory _proof + ) external; + /// @notice The number of messages which have been processed and the number of signups /// @return numSignups The number of signups /// @return numMsgs The number of messages sent by voters @@ -27,10 +36,10 @@ interface IPoll { /// to encrypt the message. function publishMessage(DomainObjs.Message memory _message, DomainObjs.PubKey calldata _encPubKey) external; - /// @notice The second step of merging the MACI state. This allows the + /// @notice The second step of merging the poll state. This allows the /// ProcessMessages circuit to access the latest state tree and ballots via /// currentSbCommitment. - function mergeMaciState() external; + function mergeState() external; /// @notice Returns the Poll's deploy time and duration /// @return _deployTime The deployment timestamp diff --git a/packages/contracts/contracts/interfaces/IPollFactory.sol b/packages/contracts/contracts/interfaces/IPollFactory.sol index 8d8351af23..a1e318ba3b 100644 --- a/packages/contracts/contracts/interfaces/IPollFactory.sol +++ b/packages/contracts/contracts/interfaces/IPollFactory.sol @@ -13,7 +13,7 @@ interface IPollFactory { /// @param _treeDepths The depths of the merkle trees /// @param _messageBatchSize The size of message batch /// @param _coordinatorPubKey The coordinator's public key - /// @param _extContracts The external contract interface references + /// @param _extContracts The external contracts interface references /// @return The deployed Poll contract function deploy( uint256 _duration, diff --git a/packages/contracts/contracts/interfaces/IVkRegistry.sol b/packages/contracts/contracts/interfaces/IVkRegistry.sol index 5d0437d972..9b8855a160 100644 --- a/packages/contracts/contracts/interfaces/IVkRegistry.sol +++ b/packages/contracts/contracts/interfaces/IVkRegistry.sol @@ -32,4 +32,13 @@ interface IVkRegistry { uint8 _messageBatchSize, DomainObjs.Mode _mode ) external view returns (SnarkCommon.VerifyingKey memory); + + /// @notice Get the poll verifying key + /// @param _stateTreeDepth The state tree depth + /// @param _voteOptionTreeDepth The vote option tree depth + /// @return The verifying key + function getPollVk( + uint256 _stateTreeDepth, + uint256 _voteOptionTreeDepth + ) external view returns (SnarkCommon.VerifyingKey memory); } diff --git a/packages/contracts/contracts/trees/LeanIMT.sol b/packages/contracts/contracts/trees/LeanIMT.sol new file mode 100644 index 0000000000..c82f8d7a0b --- /dev/null +++ b/packages/contracts/contracts/trees/LeanIMT.sol @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import { PoseidonT3 } from "../crypto/PoseidonT3.sol"; + +struct LeanIMTData { + // Tracks the current number of leaves in the tree. + uint256 size; + // Represents the current depth of the tree, which can increase as new leaves are inserted. + uint256 depth; + // A mapping from each level of the tree to the node value of the last even position at that level. + // Used for efficient inserts, updates and root calculations. + mapping(uint256 => uint256) sideNodes; + // A mapping from leaf values to their respective indices in the tree. + // This facilitates checks for leaf existence and retrieval of leaf positions. + mapping(uint256 => uint256) leaves; +} + +error WrongSiblingNodes(); +error LeafGreaterThanSnarkScalarField(); +error LeafCannotBeZero(); +error LeafAlreadyExists(); +error LeafDoesNotExist(); + +/// @title Lean Incremental binary Merkle tree. +/// @dev The LeanIMT is an optimized version of the BinaryIMT. +/// This implementation eliminates the use of zeroes, and make the tree depth dynamic. +/// When a node doesn't have the right child, instead of using a zero hash as in the BinaryIMT, +/// the node's value becomes that of its left child. Furthermore, rather than utilizing a static tree depth, +/// it is updated based on the number of leaves in the tree. This approach +/// results in the calculation of significantly fewer hashes, making the tree more efficient. +library InternalLeanIMT { + /// @notice The scalar field + uint256 internal constant SNARK_SCALAR_FIELD = + 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + /// @dev Inserts a new leaf into the incremental merkle tree. + /// The function ensures that the leaf is valid according to the + /// constraints of the tree and then updates the tree's structure accordingly. + /// @param self: A storage reference to the 'LeanIMTData' struct. + /// @param leaf: The value of the new leaf to be inserted into the tree. + /// @return The new hash of the node after the leaf has been inserted. + function _insert(LeanIMTData storage self, uint256 leaf) internal returns (uint256) { + if (leaf >= SNARK_SCALAR_FIELD) { + revert LeafGreaterThanSnarkScalarField(); + } else if (leaf == 0) { + revert LeafCannotBeZero(); + } else if (_has(self, leaf)) { + revert LeafAlreadyExists(); + } + + uint256 index = self.size; + + // Cache tree depth to optimize gas + uint256 treeDepth = self.depth; + + // A new insertion can increase a tree's depth by at most 1, + // and only if the number of leaves supported by the current + // depth is less than the number of leaves to be supported after insertion. + if (2 ** treeDepth < index + 1) { + ++treeDepth; + } + + self.depth = treeDepth; + + uint256 node = leaf; + + for (uint256 level = 0; level < treeDepth; ) { + if ((index >> level) & 1 == 1) { + node = PoseidonT3.poseidon([self.sideNodes[level], node]); + } else { + self.sideNodes[level] = node; + } + + unchecked { + ++level; + } + } + + self.size = ++index; + + self.sideNodes[treeDepth] = node; + self.leaves[leaf] = index; + + return node; + } + + /// @dev Inserts many leaves into the incremental merkle tree. + /// The function ensures that the leaves are valid according to the + /// constraints of the tree and then updates the tree's structure accordingly. + /// @param self: A storage reference to the 'LeanIMTData' struct. + /// @param leaves: The values of the new leaves to be inserted into the tree. + /// @return The root after the leaves have been inserted. + function _insertMany(LeanIMTData storage self, uint256[] calldata leaves) internal returns (uint256) { + // Cache tree size to optimize gas + uint256 treeSize = self.size; + + // Check that all the new values are correct to be added. + for (uint256 i = 0; i < leaves.length; ) { + if (leaves[i] >= SNARK_SCALAR_FIELD) { + revert LeafGreaterThanSnarkScalarField(); + } else if (leaves[i] == 0) { + revert LeafCannotBeZero(); + } else if (_has(self, leaves[i])) { + revert LeafAlreadyExists(); + } + + self.leaves[leaves[i]] = treeSize + 1 + i; + + unchecked { + ++i; + } + } + + // Array to save the nodes that will be used to create the next level of the tree. + uint256[] memory currentLevelNewNodes; + + currentLevelNewNodes = leaves; + + // Cache tree depth to optimize gas + uint256 treeDepth = self.depth; + + // Calculate the depth of the tree after adding the new values. + // Unlike the 'insert' function, we need a while here as + // N insertions can increase the tree's depth more than once. + while (2 ** treeDepth < treeSize + leaves.length) { + ++treeDepth; + } + + self.depth = treeDepth; + + // First index to change in every level. + uint256 currentLevelStartIndex = treeSize; + + // Size of the level used to create the next level. + uint256 currentLevelSize = treeSize + leaves.length; + + // The index where changes begin at the next level. + uint256 nextLevelStartIndex = currentLevelStartIndex >> 1; + + // The size of the next level. + uint256 nextLevelSize = ((currentLevelSize - 1) >> 1) + 1; + + for (uint256 level = 0; level < treeDepth; ) { + // The number of nodes for the new level that will be created, + // only the new values, not the entire level. + uint256 numberOfNewNodes = nextLevelSize - nextLevelStartIndex; + uint256[] memory nextLevelNewNodes = new uint256[](numberOfNewNodes); + for (uint256 i = 0; i < numberOfNewNodes; ) { + uint256 leftNode; + + // Assign the left node using the saved path or the position in the array. + if ((i + nextLevelStartIndex) * 2 < currentLevelStartIndex) { + leftNode = self.sideNodes[level]; + } else { + leftNode = currentLevelNewNodes[(i + nextLevelStartIndex) * 2 - currentLevelStartIndex]; + } + + uint256 rightNode; + + // Assign the right node if the value exists. + if ((i + nextLevelStartIndex) * 2 + 1 < currentLevelSize) { + rightNode = currentLevelNewNodes[(i + nextLevelStartIndex) * 2 + 1 - currentLevelStartIndex]; + } + + uint256 parentNode; + + // Assign the parent node. + // If it has a right child the result will be the hash(leftNode, rightNode) if not, + // it will be the leftNode. + if (rightNode != 0) { + parentNode = PoseidonT3.poseidon([leftNode, rightNode]); + } else { + parentNode = leftNode; + } + + nextLevelNewNodes[i] = parentNode; + + unchecked { + ++i; + } + } + + // Update the `sideNodes` variable. + // If `currentLevelSize` is odd, the saved value will be the last value of the array + // if it is even and there are more than 1 element in `currentLevelNewNodes`, the saved value + // will be the value before the last one. + // If it is even and there is only one element, there is no need to save anything because + // the correct value for this level was already saved before. + if (currentLevelSize & 1 == 1) { + self.sideNodes[level] = currentLevelNewNodes[currentLevelNewNodes.length - 1]; + } else if (currentLevelNewNodes.length > 1) { + self.sideNodes[level] = currentLevelNewNodes[currentLevelNewNodes.length - 2]; + } + + currentLevelStartIndex = nextLevelStartIndex; + + // Calculate the next level startIndex value. + // It is the position of the parent node which is pos/2. + nextLevelStartIndex >>= 1; + + // Update the next array that will be used to calculate the next level. + currentLevelNewNodes = nextLevelNewNodes; + + currentLevelSize = nextLevelSize; + + // Calculate the size of the next level. + // The size of the next level is (currentLevelSize - 1) / 2 + 1. + nextLevelSize = ((nextLevelSize - 1) >> 1) + 1; + + unchecked { + ++level; + } + } + + // Update tree size + self.size = treeSize + leaves.length; + + // Update tree root + self.sideNodes[treeDepth] = currentLevelNewNodes[0]; + + return currentLevelNewNodes[0]; + } + + /// @dev Updates the value of an existing leaf and recalculates hashes + /// to maintain tree integrity. + /// @param self: A storage reference to the 'LeanIMTData' struct. + /// @param oldLeaf: The value of the leaf that is to be updated. + /// @param newLeaf: The new value that will replace the oldLeaf in the tree. + /// @param siblingNodes: An array of sibling nodes that are necessary to recalculate the path to the root. + /// @return The new hash of the updated node after the leaf has been updated. + function _update( + LeanIMTData storage self, + uint256 oldLeaf, + uint256 newLeaf, + uint256[] calldata siblingNodes + ) internal returns (uint256) { + if (newLeaf >= SNARK_SCALAR_FIELD) { + revert LeafGreaterThanSnarkScalarField(); + } else if (!_has(self, oldLeaf)) { + revert LeafDoesNotExist(); + } else if (_has(self, newLeaf)) { + revert LeafAlreadyExists(); + } + + uint256 index = _indexOf(self, oldLeaf); + uint256 node = newLeaf; + uint256 oldRoot = oldLeaf; + + uint256 lastIndex = self.size - 1; + uint256 i = 0; + + // Cache tree depth to optimize gas + uint256 treeDepth = self.depth; + + for (uint256 level = 0; level < treeDepth; ) { + if ((index >> level) & 1 == 1) { + if (siblingNodes[i] >= SNARK_SCALAR_FIELD) { + revert LeafGreaterThanSnarkScalarField(); + } + + node = PoseidonT3.poseidon([siblingNodes[i], node]); + oldRoot = PoseidonT3.poseidon([siblingNodes[i], oldRoot]); + + unchecked { + ++i; + } + } else { + if (index >> level != lastIndex >> level) { + if (siblingNodes[i] >= SNARK_SCALAR_FIELD) { + revert LeafGreaterThanSnarkScalarField(); + } + + node = PoseidonT3.poseidon([node, siblingNodes[i]]); + oldRoot = PoseidonT3.poseidon([oldRoot, siblingNodes[i]]); + + unchecked { + ++i; + } + } else { + self.sideNodes[i] = node; + } + } + + unchecked { + ++level; + } + } + + if (oldRoot != _root(self)) { + revert WrongSiblingNodes(); + } + + self.sideNodes[treeDepth] = node; + + if (newLeaf != 0) { + self.leaves[newLeaf] = self.leaves[oldLeaf]; + } + + self.leaves[oldLeaf] = 0; + + return node; + } + + /// @dev Removes a leaf from the tree by setting its value to zero. + /// This function utilizes the update function to set the leaf's value + /// to zero and update the tree's state accordingly. + /// @param self: A storage reference to the 'LeanIMTData' struct. + /// @param oldLeaf: The value of the leaf to be removed. + /// @param siblingNodes: An array of sibling nodes required for updating the path to the root after removal. + /// @return The new root hash of the tree after the leaf has been removed. + function _remove( + LeanIMTData storage self, + uint256 oldLeaf, + uint256[] calldata siblingNodes + ) internal returns (uint256) { + return _update(self, oldLeaf, 0, siblingNodes); + } + + /// @dev Checks if a leaf exists in the tree. + /// @param self: A storage reference to the 'LeanIMTData' struct. + /// @param leaf: The value of the leaf to check for existence. + /// @return A boolean value indicating whether the leaf exists in the tree. + function _has(LeanIMTData storage self, uint256 leaf) internal view returns (bool) { + return self.leaves[leaf] != 0; + } + + /// @dev Retrieves the index of a given leaf in the tree. + /// @param self: A storage reference to the 'LeanIMTData' struct. + /// @param leaf: The value of the leaf whose index is to be found. + /// @return The index of the specified leaf within the tree. If the leaf is not present, the function + /// reverts with a custom error. + function _indexOf(LeanIMTData storage self, uint256 leaf) internal view returns (uint256) { + if (self.leaves[leaf] == 0) { + revert LeafDoesNotExist(); + } + + return self.leaves[leaf] - 1; + } + + /// @dev Retrieves the root of the tree from the 'sideNodes' mapping using the + /// current tree depth. + /// @param self: A storage reference to the 'LeanIMTData' struct. + /// @return The root hash of the tree. + function _root(LeanIMTData storage self) internal view returns (uint256) { + return self.sideNodes[self.depth]; + } +} diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 32f0d222d4..812b74a0d2 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -167,7 +167,8 @@ "@nomicfoundation/hardhat-ethers": "^3.0.8", "@nomicfoundation/hardhat-toolbox": "^5.0.0", "@openzeppelin/contracts": "^5.1.0", - "@openzeppelin/merkle-tree": "^1.0.7", + "@zk-kit/imt.sol": "2.0.0-beta.12", + "@zk-kit/lean-imt": "^2.1.0", "circomlibjs": "^0.1.7", "ethers": "^6.13.4", "hardhat": "^2.22.15", diff --git a/packages/contracts/tasks/deploy/maci/09-vkRegistry.ts b/packages/contracts/tasks/deploy/maci/09-vkRegistry.ts index 4826526c57..1a45c11a0c 100644 --- a/packages/contracts/tasks/deploy/maci/09-vkRegistry.ts +++ b/packages/contracts/tasks/deploy/maci/09-vkRegistry.ts @@ -33,6 +33,7 @@ deployment.deployTask(EDeploySteps.VkRegistry, "Deploy Vk Registry and set keys" const intStateTreeDepth = deployment.getDeployConfigField(EContracts.VkRegistry, "intStateTreeDepth"); const messageBatchDepth = deployment.getDeployConfigField(EContracts.VkRegistry, "messageBatchDepth"); const voteOptionTreeDepth = deployment.getDeployConfigField(EContracts.VkRegistry, "voteOptionTreeDepth"); + const pollJoiningTestZkeyPath = deployment.getDeployConfigField(EContracts.VkRegistry, "zkeys.pollZkey"); const processMessagesZkeyPathQv = deployment.getDeployConfigField( EContracts.VkRegistry, "zkeys.qv.processMessagesZkey", @@ -60,11 +61,12 @@ deployment.deployTask(EDeploySteps.VkRegistry, "Deploy Vk Registry and set keys" throw new Error("Non-QV zkeys are not set"); } - const [qvProcessVk, qvTallyVk, nonQvProcessVk, nonQvTallyQv] = await Promise.all([ + const [qvProcessVk, qvTallyVk, nonQvProcessVk, nonQvTallyQv, pollVk] = await Promise.all([ processMessagesZkeyPathQv && extractVk(processMessagesZkeyPathQv), tallyVotesZkeyPathQv && extractVk(tallyVotesZkeyPathQv), processMessagesZkeyPathNonQv && extractVk(processMessagesZkeyPathNonQv), tallyVotesZkeyPathNonQv && extractVk(tallyVotesZkeyPathNonQv), + pollJoiningTestZkeyPath && extractVk(pollJoiningTestZkeyPath), ]).then((vks) => vks.map( (vk: IVkObjectParams | "" | undefined) => @@ -77,6 +79,7 @@ deployment.deployTask(EDeploySteps.VkRegistry, "Deploy Vk Registry and set keys" signer: deployer, }); + const pollZkeys = pollVk as IVerifyingKeyStruct; const processZkeys = [qvProcessVk, nonQvProcessVk].filter(Boolean) as IVerifyingKeyStruct[]; const tallyZkeys = [qvTallyVk, nonQvTallyQv].filter(Boolean) as IVerifyingKeyStruct[]; const modes: EMode[] = []; @@ -96,6 +99,7 @@ deployment.deployTask(EDeploySteps.VkRegistry, "Deploy Vk Registry and set keys" voteOptionTreeDepth, 5 ** messageBatchDepth, modes, + pollZkeys, processZkeys, tallyZkeys, ) diff --git a/packages/contracts/tasks/helpers/TreeMerger.ts b/packages/contracts/tasks/helpers/TreeMerger.ts index 729ca6c81d..3d0646ff0c 100644 --- a/packages/contracts/tasks/helpers/TreeMerger.ts +++ b/packages/contracts/tasks/helpers/TreeMerger.ts @@ -55,7 +55,7 @@ export class TreeMerger { if (!(await this.pollContract.stateMerged())) { // go and merge the state tree console.log("Merging subroots to a main state root..."); - const receipt = await this.pollContract.mergeMaciState().then((tx) => tx.wait()); + const receipt = await this.pollContract.mergeState().then((tx) => tx.wait()); if (receipt?.status !== 1) { throw new Error("Error merging signup state subroots"); diff --git a/packages/contracts/tests/MACI.test.ts b/packages/contracts/tests/MACI.test.ts index bbcc411b41..3fa14ac8b0 100644 --- a/packages/contracts/tests/MACI.test.ts +++ b/packages/contracts/tests/MACI.test.ts @@ -306,16 +306,6 @@ describe("MACI", function test() { expect(receipt?.status).to.eq(1); }); - it("should have the correct state root on chain after calculating the root on chain", async () => { - maciState.polls.get(pollId)?.updatePoll(await pollContract.numSignups()); - expect(await maciContract.getStateTreeRoot()).to.eq(maciState.polls.get(pollId)?.stateTree?.root.toString()); - }); - - it("should get the correct state root with getStateTreeRoot", async () => { - const onChainStateRoot = await maciContract.getStateTreeRoot(); - expect(onChainStateRoot.toString()).to.eq(maciState.polls.get(pollId)?.stateTree?.root.toString()); - }); - it("should allow a user to signup after the state tree root was calculated", async () => { const tx = await maciContract.signUp( users[0].pubKey.asContractParam(), diff --git a/packages/contracts/tests/MessageProcessor.test.ts b/packages/contracts/tests/MessageProcessor.test.ts index 4cfe01175a..3c1d499dc4 100644 --- a/packages/contracts/tests/MessageProcessor.test.ts +++ b/packages/contracts/tests/MessageProcessor.test.ts @@ -129,7 +129,7 @@ describe("MessageProcessor", () => { generatedInputs = poll.processMessages(pollId); // set the verification keys on the vk smart contract - vkRegistryContract.setVerifyingKeys( + await vkRegistryContract.setVerifyingKeys( STATE_TREE_DEPTH, treeDepths.intStateTreeDepth, treeDepths.voteOptionTreeDepth, diff --git a/packages/contracts/tests/Poll.test.ts b/packages/contracts/tests/Poll.test.ts index dfb246749b..c9678c2a10 100644 --- a/packages/contracts/tests/Poll.test.ts +++ b/packages/contracts/tests/Poll.test.ts @@ -1,12 +1,14 @@ +/* eslint-disable no-await-in-loop */ /* eslint-disable no-underscore-dangle */ import { expect } from "chai"; -import { Signer } from "ethers"; +import { AbiCoder, Signer } from "ethers"; import { EthereumProvider } from "hardhat/types"; import { MaciState } from "maci-core"; import { NOTHING_UP_MY_SLEEVE } from "maci-crypto"; import { Keypair, Message, PCommand, PubKey } from "maci-domainobjs"; import { EMode } from "../ts/constants"; +import { IVerifyingKeyStruct } from "../ts/types"; import { getDefaultSigner } from "../ts/utils"; import { Poll__factory as PollFactory, MACI, Poll as PollContract, Verifier, VkRegistry } from "../typechain-types"; @@ -16,6 +18,9 @@ import { initialVoiceCreditBalance, maxVoteOptions, messageBatchSize, + testPollVk, + testProcessVk, + testTallyVk, treeDepths, } from "./constants"; import { timeTravel, deployTestContracts } from "./utils"; @@ -34,6 +39,8 @@ describe("Poll", () => { const keypair = new Keypair(); + const NUM_USERS = 2; + describe("deployment", () => { before(async () => { signer = await getDefaultSigner(); @@ -46,6 +53,19 @@ describe("Poll", () => { verifierContract = r.mockVerifierContract as Verifier; vkRegistryContract = r.vkRegistryContract; + for (let i = 0; i < NUM_USERS; i += 1) { + const timestamp = Math.floor(Date.now() / 1000); + const user = new Keypair(); + maciState.signUp(user.pubKey, BigInt(initialVoiceCreditBalance), BigInt(timestamp), BigInt(0)); + + // eslint-disable-next-line no-await-in-loop + await maciContract.signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + ); + } + // deploy on chain poll const tx = await maciContract.deployPoll( duration, @@ -88,6 +108,25 @@ describe("Poll", () => { BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), ]); maciState.polls.get(pollId)?.publishMessage(message, padKey); + + // set the verification keys on the vk smart contract + await vkRegistryContract.setPollVkKey( + STATE_TREE_DEPTH, + treeDepths.voteOptionTreeDepth, + testPollVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 10000000 }, + ); + + await vkRegistryContract.setVerifyingKeys( + STATE_TREE_DEPTH, + treeDepths.intStateTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + EMode.QV, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 10000000 }, + ); }); it("should not be possible to init the Poll contract twice", async () => { @@ -269,4 +308,75 @@ describe("Poll", () => { expect(await pollContract.getBatchHashes()).to.deep.eq(maciState.polls.get(pollId)?.batchHashes); }); }); + + describe("Poll join", () => { + it("The users have joined the poll", async () => { + const iface = pollContract.interface; + const pubkey = keypair.pubKey.asContractParam(); + const mockProof = [0, 0, 0, 0, 0, 0, 0, 0]; + + for (let i = 0; i < NUM_USERS; i += 1) { + const mockNullifier = AbiCoder.defaultAbiCoder().encode(["uint256"], [i]); + const voiceCreditBalance = AbiCoder.defaultAbiCoder().encode(["uint256"], [i]); + + const response = await pollContract.joinPoll(mockNullifier, pubkey, voiceCreditBalance, i, mockProof); + const receipt = await response.wait(); + const logs = receipt!.logs[0]; + const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as { + args: { _pollStateIndex: bigint }; + }; + const index = event.args._pollStateIndex; + + expect(receipt!.status).to.eq(1); + + const block = await signer.provider!.getBlock(receipt!.blockHash); + const { timestamp } = block!; + + const expectedIndex = maciState.polls + .get(pollId) + ?.joinPoll(BigInt(mockNullifier), keypair.pubKey, BigInt(voiceCreditBalance), BigInt(timestamp)); + + expect(index).to.eq(expectedIndex); + } + }); + + it("Poll state tree size after user's joining", async () => { + const pollStateTree = await pollContract.pollStateTree(); + const size = Number(pollStateTree.numberOfLeaves); + expect(size).to.eq(maciState.polls.get(pollId)?.pollStateLeaves.length); + }); + + it("The first user has been rejected for the second join", async () => { + const mockNullifier = AbiCoder.defaultAbiCoder().encode(["uint256"], [0]); + const pubkey = keypair.pubKey.asContractParam(); + const voiceCreditBalance = AbiCoder.defaultAbiCoder().encode(["uint256"], [0]); + const mockProof = [0, 0, 0, 0, 0, 0, 0, 0]; + + await expect( + pollContract.joinPoll(mockNullifier, pubkey, voiceCreditBalance, 0, mockProof), + ).to.be.revertedWithCustomError(pollContract, "UserAlreadyJoined"); + }); + + it("should allow a Poll contract to merge the state tree (calculate the state root)", async () => { + await timeTravel(signer.provider as unknown as EthereumProvider, Number(duration) + 1); + + const tx = await pollContract.mergeState({ + gasLimit: 3000000, + }); + + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + + it("should get the correct numSignUps", async () => { + const numSignUps = await pollContract.numSignups(); + expect(numSignUps).to.be.eq(maciState.polls.get(pollId)?.pollStateLeaves.length); + maciState.polls.get(pollId)?.updatePoll(numSignUps); + }); + + it("should get the correct mergedStateRoot", async () => { + const mergedStateRoot = await pollContract.mergedStateRoot(); + expect(mergedStateRoot.toString()).to.eq(maciState.polls.get(pollId)?.pollStateTree?.root.toString()); + }); + }); }); diff --git a/packages/contracts/tests/PollFactory.test.ts b/packages/contracts/tests/PollFactory.test.ts index 3cd8e4f3b7..af65c4b9e7 100644 --- a/packages/contracts/tests/PollFactory.test.ts +++ b/packages/contracts/tests/PollFactory.test.ts @@ -5,13 +5,21 @@ import { Keypair } from "maci-domainobjs"; import { deployPollFactory, getDefaultSigner } from "../ts"; import { MACI, PollFactory, Verifier, VkRegistry } from "../typechain-types"; -import { messageBatchSize, initialVoiceCreditBalance, maxVoteOptions, STATE_TREE_DEPTH, treeDepths } from "./constants"; +import { + messageBatchSize, + initialVoiceCreditBalance, + maxVoteOptions, + STATE_TREE_DEPTH, + treeDepths, + ExtContractsStruct, +} from "./constants"; import { deployTestContracts } from "./utils"; describe("pollFactory", () => { let maciContract: MACI; let verifierContract: Verifier; let vkRegistryContract: VkRegistry; + let extContracts: ExtContractsStruct; let pollFactory: PollFactory; let signer: Signer; @@ -23,6 +31,7 @@ describe("pollFactory", () => { maciContract = r.maciContract; verifierContract = r.mockVerifierContract as Verifier; vkRegistryContract = r.vkRegistryContract; + extContracts = { maci: maciContract, verifier: verifierContract, vkRegistry: vkRegistryContract }; pollFactory = (await deployPollFactory(signer, undefined, true)) as BaseContract as PollFactory; }); @@ -35,7 +44,7 @@ describe("pollFactory", () => { treeDepths, messageBatchSize, coordinatorPubKey.asContractParam(), - { maci: maciContract, verifier: verifierContract, vkRegistry: vkRegistryContract }, + extContracts, ); const receipt = await tx.wait(); expect(receipt?.status).to.eq(1); @@ -50,7 +59,7 @@ describe("pollFactory", () => { treeDepths, messageBatchSize, coordinatorPubKey.asContractParam(), - { maci: maciContract, verifier: verifierContract, vkRegistry: vkRegistryContract }, + extContracts, ), ).to.be.revertedWithCustomError(pollFactory, "InvalidMaxVoteOptions"); }); diff --git a/packages/contracts/tests/Tally.test.ts b/packages/contracts/tests/Tally.test.ts index 32666a77df..5083a6a3ab 100644 --- a/packages/contracts/tests/Tally.test.ts +++ b/packages/contracts/tests/Tally.test.ts @@ -27,6 +27,7 @@ import { initialVoiceCreditBalance, maxVoteOptions, messageBatchSize, + testPollVk, testProcessVk, testTallyVk, treeDepths, @@ -44,6 +45,7 @@ describe("TallyVotes", () => { const coordinator = new Keypair(); let users: Keypair[]; + let pollKeys: Keypair[]; let maciState: MaciState; let pollId: bigint; @@ -174,7 +176,7 @@ describe("TallyVotes", () => { let tallyGeneratedInputs: ITallyCircuitInputs; before(async () => { - await pollContract.mergeMaciState(); + await pollContract.mergeState(); tallyGeneratedInputs = poll.tallyVotes(); }); @@ -210,6 +212,7 @@ describe("TallyVotes", () => { before(async () => { // create 24 users (total 25 - 24 + 1 nothing up my sleeve) users = Array.from({ length: 24 }, () => new Keypair()); + pollKeys = Array.from({ length: 24 }, () => new Keypair()); maciState = new MaciState(STATE_TREE_DEPTH); const updatedDuration = 5000000; @@ -311,7 +314,13 @@ describe("TallyVotes", () => { // update the poll state poll.updatePoll(BigInt(maciState.stateLeaves.length)); - // set the verification keys on the vk smart contract + await vkRegistryContract.setPollVkKey( + STATE_TREE_DEPTH, + treeDepths.voteOptionTreeDepth, + testPollVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 10000000 }, + ); + await vkRegistryContract.setVerifyingKeys( STATE_TREE_DEPTH, intStateTreeDepth, @@ -322,8 +331,26 @@ describe("TallyVotes", () => { testTallyVk.asContractParam() as IVerifyingKeyStruct, ); + // join all user to the Poll + for (let i = 0; i < users.length; i += 1) { + const timestamp = Math.floor(Date.now() / 1000); + // join locally + const nullifier = poseidon([BigInt(users[i].privKey.rawPrivKey)]); + poll.joinPoll(nullifier, pollKeys[i].pubKey, BigInt(initialVoiceCreditBalance), BigInt(timestamp)); + + // join on chain + // eslint-disable-next-line no-await-in-loop + await pollContract.joinPoll( + nullifier, + pollKeys[i].pubKey.asContractParam(), + BigInt(initialVoiceCreditBalance), + i, + [0, 0, 0, 0, 0, 0, 0, 0], + ); + } + await timeTravel(signer.provider! as unknown as EthereumProvider, updatedDuration); - await pollContract.mergeMaciState(); + await pollContract.mergeState(); const processMessagesInputs = poll.processMessages(pollId); @@ -507,6 +534,8 @@ describe("TallyVotes", () => { before(async () => { // create 25 users (and thus 26 ballots) (total 26 - 25 + 1 nothing up my sleeve) users = Array.from({ length: 25 }, () => new Keypair()); + pollKeys = Array.from({ length: 25 }, () => new Keypair()); + maciState = new MaciState(STATE_TREE_DEPTH); const updatedDuration = 5000000; @@ -609,6 +638,13 @@ describe("TallyVotes", () => { poll.updatePoll(BigInt(maciState.stateLeaves.length)); // set the verification keys on the vk smart contract + await vkRegistryContract.setPollVkKey( + STATE_TREE_DEPTH, + treeDepths.voteOptionTreeDepth, + testPollVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 10000000 }, + ); + await vkRegistryContract.setVerifyingKeys( STATE_TREE_DEPTH, intStateTreeDepth, @@ -619,8 +655,26 @@ describe("TallyVotes", () => { testTallyVk.asContractParam() as IVerifyingKeyStruct, ); + // join all user to the Poll + for (let i = 0; i < users.length; i += 1) { + const timestamp = Math.floor(Date.now() / 1000); + // join locally + const nullifier = poseidon([BigInt(users[i].privKey.rawPrivKey)]); + poll.joinPoll(nullifier, pollKeys[i].pubKey, BigInt(initialVoiceCreditBalance), BigInt(timestamp)); + + // join on chain + // eslint-disable-next-line no-await-in-loop + await pollContract.joinPoll( + nullifier, + pollKeys[i].pubKey.asContractParam(), + BigInt(initialVoiceCreditBalance), + i, + [0, 0, 0, 0, 0, 0, 0, 0], + ); + } + await timeTravel(signer.provider! as unknown as EthereumProvider, updatedDuration); - await pollContract.mergeMaciState(); + await pollContract.mergeState(); const processMessagesInputs = poll.processMessages(pollId); diff --git a/packages/contracts/tests/TallyNonQv.test.ts b/packages/contracts/tests/TallyNonQv.test.ts index 3125a76ec8..bcb464b85c 100644 --- a/packages/contracts/tests/TallyNonQv.test.ts +++ b/packages/contracts/tests/TallyNonQv.test.ts @@ -169,7 +169,7 @@ describe("TallyVotesNonQv", () => { describe("after messages processing", () => { let tallyGeneratedInputs: ITallyCircuitInputs; before(async () => { - await pollContract.mergeMaciState(); + await pollContract.mergeState(); tallyGeneratedInputs = poll.tallyVotes(); }); diff --git a/packages/contracts/tests/VkRegistry.test.ts b/packages/contracts/tests/VkRegistry.test.ts index e587f5e096..63e2098e32 100644 --- a/packages/contracts/tests/VkRegistry.test.ts +++ b/packages/contracts/tests/VkRegistry.test.ts @@ -6,6 +6,7 @@ import { EMode } from "../ts/constants"; import { messageBatchSize, + testPollVk, testProcessVk, testProcessVkNonQv, testTallyVk, @@ -31,8 +32,32 @@ describe("VkRegistry", () => { }); }); + describe("setPollVkKey", () => { + it("should set the poll vk", async () => { + const tx = await vkRegistryContract.setPollVkKey( + stateTreeDepth + 1, + treeDepths.voteOptionTreeDepth, + testPollVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + + it("should throw when trying to set another vk for the same params", async () => { + await expect( + vkRegistryContract.setPollVkKey( + stateTreeDepth + 1, + treeDepths.voteOptionTreeDepth, + testPollVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ), + ).to.be.revertedWithCustomError(vkRegistryContract, "PollVkAlreadySet"); + }); + }); + describe("setVerifyingKeys", () => { - it("should set the process and tally vks", async () => { + it("should set the process, tally vks", async () => { const tx = await vkRegistryContract.setVerifyingKeys( stateTreeDepth, treeDepths.intStateTreeDepth, @@ -90,13 +115,14 @@ describe("VkRegistry", () => { }); describe("setVerifyingKeysBatch", () => { - it("should set the process and tally vks", async () => { + it("should set the process, tally, poll vks", async () => { const tx = await vkRegistryContract.setVerifyingKeysBatch( stateTreeDepth, treeDepths.intStateTreeDepth, treeDepths.voteOptionTreeDepth, messageBatchSize, [EMode.NON_QV], + testPollVk.asContractParam() as IVerifyingKeyStruct, [testProcessVkNonQv.asContractParam() as IVerifyingKeyStruct], [testTallyVkNonQv.asContractParam() as IVerifyingKeyStruct], ); @@ -113,6 +139,7 @@ describe("VkRegistry", () => { treeDepths.voteOptionTreeDepth, messageBatchSize, [EMode.QV], + testPollVk.asContractParam() as IVerifyingKeyStruct, [ testProcessVk.asContractParam() as IVerifyingKeyStruct, testProcessVkNonQv.asContractParam() as IVerifyingKeyStruct, @@ -174,6 +201,14 @@ describe("VkRegistry", () => { }); describe("genSignatures", () => { + describe("genPollVkSig", () => { + it("should generate a valid signature", async () => { + const sig = await vkRegistryContract.genPollVkSig(stateTreeDepth, treeDepths.voteOptionTreeDepth); + const vk = await vkRegistryContract.getPollVkBySig(sig); + compareVks(testPollVk, vk); + }); + }); + describe("genProcessVkSig", () => { it("should generate a valid signature", async () => { const sig = await vkRegistryContract.genProcessVkSig( diff --git a/packages/contracts/tests/constants.ts b/packages/contracts/tests/constants.ts index 3758375f44..855b0e102e 100644 --- a/packages/contracts/tests/constants.ts +++ b/packages/contracts/tests/constants.ts @@ -1,7 +1,14 @@ +import { AddressLike } from "ethers"; import { TreeDepths, STATE_TREE_ARITY } from "maci-core"; import { G1Point, G2Point } from "maci-crypto"; import { VerifyingKey } from "maci-domainobjs"; +export interface ExtContractsStruct { + maci: AddressLike; + verifier: AddressLike; + vkRegistry: AddressLike; +} + export const duration = 2_000; export const STATE_TREE_DEPTH = 10; @@ -9,6 +16,14 @@ export const MESSAGE_TREE_DEPTH = 2; export const MESSAGE_TREE_SUBDEPTH = 1; export const messageBatchSize = 20; +export const testPollVk = new VerifyingKey( + new G1Point(BigInt(0), BigInt(1)), + new G2Point([BigInt(2), BigInt(3)], [BigInt(4), BigInt(5)]), + new G2Point([BigInt(6), BigInt(7)], [BigInt(8), BigInt(9)]), + new G2Point([BigInt(10), BigInt(11)], [BigInt(12), BigInt(13)]), + [new G1Point(BigInt(14), BigInt(15)), new G1Point(BigInt(16), BigInt(17))], +); + export const testProcessVk = new VerifyingKey( new G1Point(BigInt(0), BigInt(1)), new G2Point([BigInt(2), BigInt(3)], [BigInt(4), BigInt(5)]), diff --git a/packages/contracts/ts/genMaciState.ts b/packages/contracts/ts/genMaciState.ts index f6f90136c0..a23b80f882 100644 --- a/packages/contracts/ts/genMaciState.ts +++ b/packages/contracts/ts/genMaciState.ts @@ -78,6 +78,7 @@ export const genMaciStateFromContract = async ( pubKey: new PubKey([BigInt(event.args._userPubKeyX), BigInt(event.args._userPubKeyY)]), voiceCreditBalance: Number(event.args._voiceCreditBalance), timestamp: Number(event.args._timestamp), + stateLeaf: BigInt(event.args._stateLeaf), }, }); }); @@ -144,6 +145,33 @@ export const genMaciStateFromContract = async ( // eslint-disable-next-line no-await-in-loop const publishMessageLogs = await pollContract.queryFilter(pollContract.filters.PublishMessage(), i, toBlock); + // eslint-disable-next-line no-await-in-loop + const joinPollLogs = await pollContract.queryFilter(pollContract.filters.PollJoined(), i, toBlock); + + joinPollLogs.forEach((event) => { + assert(!!event); + + const nullifier = BigInt(event.args._nullifier); + + const pubKeyX = BigInt(event.args._pollPubKeyX); + const pubKeyY = BigInt(event.args._pollPubKeyY); + const timestamp = Number(event.args._timestamp); + + const newVoiceCreditBalance = BigInt(event.args._newVoiceCreditBalance); + + actions.push({ + type: "PollJoined", + blockNumber: event.blockNumber, + transactionIndex: event.transactionIndex, + data: { + pubKey: new PubKey([pubKeyX, pubKeyY]), + newVoiceCreditBalance, + timestamp, + nullifier, + }, + }); + }); + publishMessageLogs.forEach((event) => { assert(!!event); @@ -172,9 +200,9 @@ export const genMaciStateFromContract = async ( sortActions(actions).forEach((action) => { switch (true) { case action.type === "SignUp": { - const { pubKey, voiceCreditBalance, timestamp } = action.data; + const { pubKey, voiceCreditBalance, timestamp, stateLeaf } = action.data; - maciState.signUp(pubKey!, BigInt(voiceCreditBalance!), BigInt(timestamp!)); + maciState.signUp(pubKey!, BigInt(voiceCreditBalance!), BigInt(timestamp!), stateLeaf); break; } @@ -200,6 +228,12 @@ export const genMaciStateFromContract = async ( break; } + case action.type === "PollJoined": { + const { pubKey, newVoiceCreditBalance, timestamp, nullifier } = action.data; + maciState.polls.get(pollId)?.joinPoll(nullifier!, pubKey!, newVoiceCreditBalance!, BigInt(timestamp!)); + break; + } + default: break; } @@ -215,9 +249,6 @@ export const genMaciStateFromContract = async ( // set the number of signups poll.updatePoll(numSignUpsAndMessages[0]); - // we need to ensure that the stateRoot is correct - assert(poll.stateTree?.root.toString() === (await pollContract.mergedStateRoot()).toString()); - maciState.polls.set(pollId, poll); return maciState; diff --git a/packages/contracts/ts/genSignUpTree.ts b/packages/contracts/ts/genSignUpTree.ts new file mode 100644 index 0000000000..598c47b795 --- /dev/null +++ b/packages/contracts/ts/genSignUpTree.ts @@ -0,0 +1,69 @@ +/* eslint-disable no-underscore-dangle */ +import { LeanIMT, LeanIMTHashFunction } from "@zk-kit/lean-imt"; +import { hashLeanIMT } from "maci-crypto"; +import { PubKey, StateLeaf, blankStateLeaf, blankStateLeafHash } from "maci-domainobjs"; + +import { assert } from "console"; + +import { MACI__factory as MACIFactory } from "../typechain-types"; + +import { IGenSignUpTreeArgs, IGenSignUpTree } from "./types"; +import { sleep } from "./utils"; + +/** + * Generate a State tree object from the events of a MACI smart contracts + * @param provider - the ethereum provider + * @param address - the address of the MACI contract + * @param fromBlock - the block number from which to start fetching events + * @param blocksPerRequest - the number of blocks to fetch in each request + * @param endBlock - the block number at which to stop fetching events + * @param sleepAmount - the amount of time to sleep between each request + * @returns State tree + */ +export const genSignUpTree = async ({ + provider, + address, + fromBlock = 0, + blocksPerRequest = 50, + endBlock, + sleepAmount, +}: IGenSignUpTreeArgs): Promise => { + const lastBlock = endBlock || (await provider.getBlockNumber()); + + const maciContract = MACIFactory.connect(address, provider); + const signUpTree = new LeanIMT(hashLeanIMT as LeanIMTHashFunction); + signUpTree.insert(blankStateLeafHash); + const stateLeaves: StateLeaf[] = [blankStateLeaf]; + + // Fetch event logs in batches (lastBlock inclusive) + for (let i = fromBlock; i <= lastBlock; i += blocksPerRequest + 1) { + // the last block batch will be either current iteration block + blockPerRequest + // or the end block if it is set + const toBlock = i + blocksPerRequest >= lastBlock ? lastBlock : i + blocksPerRequest; + + // eslint-disable-next-line no-await-in-loop + const signUpLogs = await maciContract.queryFilter(maciContract.filters.SignUp(), i, toBlock); + signUpLogs.forEach((event) => { + assert(!!event); + const pubKeyX = event.args._userPubKeyX; + const pubKeyY = event.args._userPubKeyY; + const voiceCreditBalance = event.args._voiceCreditBalance; + const timestamp = event.args._timestamp; + + const pubKey = new PubKey([pubKeyX, pubKeyY]); + const stateLeaf = new StateLeaf(pubKey, voiceCreditBalance, timestamp); + + stateLeaves.push(stateLeaf); + signUpTree.insert(event.args._stateLeaf); + }); + + if (sleepAmount) { + // eslint-disable-next-line no-await-in-loop + await sleep(sleepAmount); + } + } + return { + signUpTree, + stateLeaves, + }; +}; diff --git a/packages/contracts/ts/index.ts b/packages/contracts/ts/index.ts index abf906236b..1de6bcd054 100644 --- a/packages/contracts/ts/index.ts +++ b/packages/contracts/ts/index.ts @@ -17,6 +17,7 @@ export { } from "./deploy"; export { genMaciStateFromContract } from "./genMaciState"; export { genEmptyBallotRoots } from "./genEmptyBallotRoots"; +export { genSignUpTree } from "./genSignUpTree"; export { formatProofForVerifierContract, getDefaultSigner, getDefaultNetwork, getSigners } from "./utils"; export { EMode } from "./constants"; export { EDeploySteps } from "../tasks/helpers/constants"; @@ -38,5 +39,5 @@ export { } from "../tasks/helpers/types"; export { linkPoseidonLibraries } from "../tasks/helpers/abi"; -export type { IVerifyingKeyStruct, SnarkProof, Groth16Proof, Proof } from "./types"; +export type { IVerifyingKeyStruct, SnarkProof, Groth16Proof, Proof, IGenSignUpTree } from "./types"; export * from "../typechain-types"; diff --git a/packages/contracts/ts/types.ts b/packages/contracts/ts/types.ts index 81c0ecd4f2..61435de2f3 100644 --- a/packages/contracts/ts/types.ts +++ b/packages/contracts/ts/types.ts @@ -1,3 +1,5 @@ +import { LeanIMT } from "@zk-kit/lean-imt"; + import type { ConstantInitialVoiceCreditProxy, FreeForAllGatekeeper, @@ -10,9 +12,9 @@ import type { PoseidonT6, VkRegistry, } from "../typechain-types"; -import type { BigNumberish, ContractFactory, Signer } from "ethers"; +import type { BigNumberish, Provider, Signer, ContractFactory } from "ethers"; import type { CircuitInputs } from "maci-core"; -import type { Message, PubKey } from "maci-domainobjs"; +import type { Message, PubKey, StateLeaf } from "maci-domainobjs"; import type { PublicSignals } from "snarkjs"; /** @@ -106,11 +108,13 @@ export interface Action { message: Message; voiceCreditBalance: number; timestamp: number; + nullifier: bigint; + newVoiceCreditBalance: bigint; stateIndex: number; numSrQueueOps: number; pollId: bigint; pollAddr: string; - stateRoot: bigint; + stateLeaf: bigint; messageRoot: bigint; }>; blockNumber: number; @@ -185,3 +189,53 @@ export interface IDeployedMaci { poseidonT6: string; }; } + +/** + * An interface that represents arguments of generation sign up tree and state leaves + */ +export interface IGenSignUpTreeArgs { + /** + * The etherum provider + */ + provider: Provider; + + /** + * The address of MACI contract + */ + address: string; + + /** + * The block number from which to start fetching events + */ + fromBlock?: number; + + /** + * The number of blocks to fetch in each request + */ + blocksPerRequest?: number; + + /** + * The block number at which to stop fetching events + */ + endBlock?: number; + + /** + * The amount of time to sleep between each request + */ + sleepAmount?: number; +} + +/** + * An interface that represents sign up tree and state leaves + */ +export interface IGenSignUpTree { + /** + * Sign up tree + */ + signUpTree: LeanIMT; + + /** + * State leaves + */ + stateLeaves: StateLeaf[]; +} diff --git a/packages/core/package.json b/packages/core/package.json index 489983b42e..a10cf36a4a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,7 +23,8 @@ }, "dependencies": { "maci-crypto": "^2.5.0", - "maci-domainobjs": "^2.5.0" + "maci-domainobjs": "^2.5.0", + "@zk-kit/lean-imt": "^2.2.1" }, "devDependencies": { "@types/chai": "^4.3.11", diff --git a/packages/core/ts/MaciState.ts b/packages/core/ts/MaciState.ts index e62e840656..82df27cda7 100644 --- a/packages/core/ts/MaciState.ts +++ b/packages/core/ts/MaciState.ts @@ -1,3 +1,4 @@ +import { hash4, IncrementalQuinTree } from "maci-crypto"; import { type PubKey, type Keypair, StateLeaf, blankStateLeaf } from "maci-domainobjs"; import type { IJsonMaciState, IJsonPoll, IMaciState, TreeDepths } from "./utils/types"; @@ -18,6 +19,9 @@ export class MaciState implements IMaciState { // how deep the state tree is stateTreeDepth: number; + // state tree + stateTree?: IncrementalQuinTree; + numSignUps = 0; // to keep track if a poll is currently being processed @@ -44,13 +48,18 @@ export class MaciState implements IMaciState { * @param pubKey - The public key of the user. * @param initialVoiceCreditBalance - The initial voice credit balance of the user. * @param timestamp - The timestamp of the sign-up. + * @param stateLeaf - The hash state leaf. * @returns The index of the newly signed-up user in the state tree. */ - signUp(pubKey: PubKey, initialVoiceCreditBalance: bigint, timestamp: bigint): number { + signUp(pubKey: PubKey, initialVoiceCreditBalance: bigint, timestamp: bigint, stateLeaf?: bigint): number { this.numSignUps += 1; - const stateLeaf = new StateLeaf(pubKey, initialVoiceCreditBalance, timestamp); + const stateLeafObj = new StateLeaf(pubKey, initialVoiceCreditBalance, timestamp); - return this.stateLeaves.push(stateLeaf.copy()) - 1; + const pubKeyAsArray = pubKey.asArray(); + const stateLeafHash = + stateLeaf || hash4([pubKeyAsArray[0], pubKeyAsArray[1], initialVoiceCreditBalance, timestamp]); + this.stateTree?.insert(stateLeafHash); + return this.stateLeaves.push(stateLeafObj.copy()) - 1; } /** diff --git a/packages/core/ts/Poll.ts b/packages/core/ts/Poll.ts index 91f5800be2..c647274cf6 100644 --- a/packages/core/ts/Poll.ts +++ b/packages/core/ts/Poll.ts @@ -1,3 +1,4 @@ +import { LeanIMT, LeanIMTHashFunction } from "@zk-kit/lean-imt"; import { IncrementalQuinTree, genRandomSalt, @@ -9,6 +10,8 @@ import { stringifyBigInts, genTreeCommitment, hash2, + poseidon, + hashLeanIMT, } from "maci-crypto"; import { PCommand, @@ -17,8 +20,8 @@ import { PubKey, PrivKey, Message, + StateLeaf, blankStateLeaf, - type StateLeaf, type IMessageContractParams, type IJsonPCommand, blankStateLeafHash, @@ -26,7 +29,6 @@ import { import assert from "assert"; -import type { MaciState } from "./MaciState"; import type { CircuitInputs, TreeDepths, @@ -36,7 +38,10 @@ import type { IProcessMessagesOutput, ITallyCircuitInputs, IProcessMessagesCircuitInputs, -} from "./utils/types"; + IPollJoiningCircuitInputs, + IJoiningCircuitArgs, +} from "./index"; +import type { MaciState } from "./MaciState"; import type { PathElements } from "maci-crypto"; import { STATE_TREE_ARITY, VOTE_OPTION_TREE_ARITY } from "./utils/constants"; @@ -78,7 +83,7 @@ export class Poll implements IPoll { stateLeaves: StateLeaf[] = [blankStateLeaf]; - stateTree?: IncrementalQuinTree; + stateTree?: LeanIMT; // For message processing numBatchesProcessed = 0; @@ -118,6 +123,15 @@ export class Poll implements IPoll { // batch chain hashes batchHashes = [NOTHING_UP_MY_SLEEVE]; + // Poll state tree leaves + pollStateLeaves: StateLeaf[] = [blankStateLeaf]; + + // Poll state tree + pollStateTree?: IncrementalQuinTree; + + // Poll voting nullifier + pollNullifiers: Map; + // how many users signed up private numSignups = 0n; @@ -149,6 +163,8 @@ export class Poll implements IPoll { this.actualStateTreeDepth = maciStateRef.stateTreeDepth; this.currentMessageBatchIndex = 0; + this.pollNullifiers = new Map(); + this.tallyResult = new Array(this.maxVoteOptions).fill(0n) as bigint[]; this.perVOSpentVoiceCredits = new Array(this.maxVoteOptions).fill(0n) as bigint[]; @@ -157,6 +173,33 @@ export class Poll implements IPoll { this.ballots.push(this.emptyBallot); } + /** + * Check if user has already joined the poll by checking if the nullifier is registered + */ + hasJoined = (nullifier: bigint): boolean => this.pollNullifiers.get(nullifier) != null; + + /** + * Join the anonymous user to the Poll (to the tree) + * @param nullifier - Hashed private key used as nullifier + * @param pubKey - The poll public key. + * @param newVoiceCreditBalance - New voice credit balance of the user. + * @param timestamp - The timestamp of the sign-up. + * @returns The index of added state leaf + */ + joinPoll = (nullifier: bigint, pubKey: PubKey, newVoiceCreditBalance: bigint, timestamp: bigint): number => { + const stateLeaf = new StateLeaf(pubKey, newVoiceCreditBalance, timestamp); + + if (this.hasJoined(nullifier)) { + throw new Error("UserAlreadyJoined"); + } + + this.pollNullifiers.set(nullifier, true); + this.pollStateLeaves.push(stateLeaf.copy()); + this.pollStateTree?.insert(stateLeaf.hash()); + + return this.pollStateLeaves.length - 1; + }; + /** * Update a Poll with data from MaciState. * This is the step where we copy the state from the MaciState instance, @@ -178,13 +221,24 @@ export class Poll implements IPoll { // ensure we have the correct actual state tree depth value this.actualStateTreeDepth = Math.max(1, Math.ceil(Math.log2(Number(this.numSignups)))); - // create a new state tree - this.stateTree = new IncrementalQuinTree(this.actualStateTreeDepth, blankStateLeafHash, STATE_TREE_ARITY, hash2); + this.stateTree = new LeanIMT(hashLeanIMT as LeanIMTHashFunction); // add all leaves this.stateLeaves.forEach((stateLeaf) => { this.stateTree?.insert(stateLeaf.hash()); }); + // create a poll state tree + this.pollStateTree = new IncrementalQuinTree( + this.actualStateTreeDepth, + blankStateLeafHash, + STATE_TREE_ARITY, + hash2, + ); + + this.pollStateLeaves.forEach((stateLeaf) => { + this.pollStateTree?.insert(stateLeaf.hash()); + }); + // Create as many ballots as state leaves this.emptyBallotHash = this.emptyBallot.hash(); this.ballotTree = new IncrementalQuinTree(this.stateTreeDepth, this.emptyBallotHash, STATE_TREE_ARITY, hash2); @@ -218,13 +272,13 @@ export class Poll implements IPoll { if ( stateLeafIndex >= BigInt(this.ballots.length) || stateLeafIndex < 1n || - stateLeafIndex >= BigInt(this.stateTree?.nextIndex || -1) + stateLeafIndex >= BigInt(this.pollStateTree?.nextIndex || -1) ) { throw new ProcessMessageError(ProcessMessageErrors.InvalidStateLeafIndex); } // The user to update (or not) - const stateLeaf = this.stateLeaves[Number(stateLeafIndex)]; + const stateLeaf = this.pollStateLeaves[Number(stateLeafIndex)]; // The ballot to update (or not) const ballot = this.ballots[Number(stateLeafIndex)]; @@ -283,7 +337,7 @@ export class Poll implements IPoll { // calculate the path elements for the state tree given the original state tree (before any changes) // changes could effectively be made by this new vote - either a key change or vote change // would result in a different state leaf - const originalStateLeafPathElements = this.stateTree?.genProof(Number(stateLeafIndex)).pathElements; + const originalStateLeafPathElements = this.pollStateTree?.genProof(Number(stateLeafIndex)).pathElements; // calculate the path elements for the ballot tree given the original ballot tree (before any changes) // changes could effectively be made by this new ballot const originalBallotPathElements = this.ballotTree?.genProof(Number(stateLeafIndex)).pathElements; @@ -372,6 +426,82 @@ export class Poll implements IPoll { } }; + /** + * Create circuit input for pollJoining + * @param maciPrivKey User's private key for signing up + * @param stateLeafIndex Index where the user is stored in the state leaves + * @param credits Credits for voting + * @param pollPrivKey Poll's private key for the poll joining + * @param pollPubKey Poll's public key for the poll joining + * @returns stringified circuit inputs + */ + joiningCircuitInputs = ({ + maciPrivKey, + stateLeafIndex, + credits, + pollPrivKey, + pollPubKey, + }: IJoiningCircuitArgs): IPollJoiningCircuitInputs => { + // Get the state leaf on the index position + const stateLeaf = this.stateLeaves[Number(stateLeafIndex)]; + const { pubKey, voiceCreditBalance, timestamp } = stateLeaf; + const pubKeyX = pubKey.asArray()[0]; + const pubKeyY = pubKey.asArray()[1]; + const stateLeafArray = [pubKeyX, pubKeyY, voiceCreditBalance, timestamp]; + const pollPubKeyArray = pollPubKey.asArray(); + + assert(credits <= voiceCreditBalance, "Credits must be lower than signed up credits"); + + // calculate the path elements for the state tree given the original state tree + const { siblings, index } = this.stateTree!.generateProof(Number(stateLeafIndex)); + const siblingsLength = siblings.length; + + // The index must be converted to a list of indices, 1 for each tree level. + // The circuit tree depth is this.stateTreeDepth, so the number of siblings must be this.stateTreeDepth, + // even if the tree depth is actually 3. The missing siblings can be set to 0, as they + // won't be used to calculate the root in the circuit. + const indices: bigint[] = []; + + for (let i = 0; i < this.stateTreeDepth; i += 1) { + // eslint-disable-next-line no-bitwise + indices.push(BigInt((index >> i) & 1)); + + if (i >= siblingsLength) { + siblings[i] = BigInt(0); + } + } + const siblingsArray = siblings.map((sibling) => [sibling]); + + // Create nullifier from private key + const inputNullifier = BigInt(maciPrivKey.asCircuitInputs()); + const nullifier = poseidon([inputNullifier]); + + // Get pll state tree's root + const stateRoot = this.stateTree!.root; + + // Set actualStateTreeDepth as number of initial siblings length + const actualStateTreeDepth = BigInt(siblingsLength); + + // Calculate public input hash from nullifier, credits and current root + const inputHash = sha256Hash([nullifier, credits, stateRoot, pollPubKeyArray[0], pollPubKeyArray[1]]); + + const circuitInputs = { + privKey: maciPrivKey.asCircuitInputs(), + pollPrivKey: pollPrivKey.asCircuitInputs(), + pollPubKey: pollPubKey.asCircuitInputs(), + stateLeaf: stateLeafArray, + siblings: siblingsArray, + indices, + nullifier, + credits, + stateRoot, + actualStateTreeDepth, + inputHash, + }; + + return stringifyBigInts(circuitInputs) as unknown as IPollJoiningCircuitInputs; + }; + /** * Pad last unclosed batch */ @@ -494,10 +624,10 @@ export class Poll implements IPoll { currentBallotsPathElements.unshift(r.originalBallotPathElements!); // update the state leaves with the new state leaf (result of processing the message) - this.stateLeaves[index] = r.newStateLeaf!.copy(); + this.pollStateLeaves[index] = r.newStateLeaf!.copy(); // we also update the state tree with the hash of the new state leaf - this.stateTree?.update(index, r.newStateLeaf!.hash()); + this.pollStateTree?.update(index, r.newStateLeaf!.hash()); // store the new ballot this.ballots[index] = r.newBallot!; @@ -532,9 +662,9 @@ export class Poll implements IPoll { const stateLeafIndex = command.stateIndex; // if the state leaf index is valid then use it - if (stateLeafIndex < this.stateLeaves.length) { - currentStateLeaves.unshift(this.stateLeaves[Number(stateLeafIndex)].copy()); - currentStateLeavesPathElements.unshift(this.stateTree!.genProof(Number(stateLeafIndex)).pathElements); + if (stateLeafIndex < this.pollStateLeaves.length) { + currentStateLeaves.unshift(this.pollStateLeaves[Number(stateLeafIndex)].copy()); + currentStateLeavesPathElements.unshift(this.pollStateTree!.genProof(Number(stateLeafIndex)).pathElements); // copy the ballot const ballot = this.ballots[Number(stateLeafIndex)].copy(); @@ -584,8 +714,8 @@ export class Poll implements IPoll { } } else { // just use state leaf index 0 - currentStateLeaves.unshift(this.stateLeaves[0].copy()); - currentStateLeavesPathElements.unshift(this.stateTree!.genProof(0).pathElements); + currentStateLeaves.unshift(this.pollStateLeaves[0].copy()); + currentStateLeavesPathElements.unshift(this.pollStateTree!.genProof(0).pathElements); currentBallots.unshift(this.ballots[0].copy()); currentBallotsPathElements.unshift(this.ballotTree!.genProof(0).pathElements); @@ -609,8 +739,8 @@ export class Poll implements IPoll { } } else { // Since we don't have a command at that position, use a blank state leaf - currentStateLeaves.unshift(this.stateLeaves[0].copy()); - currentStateLeavesPathElements.unshift(this.stateTree!.genProof(0).pathElements); + currentStateLeaves.unshift(this.pollStateLeaves[0].copy()); + currentStateLeavesPathElements.unshift(this.pollStateTree!.genProof(0).pathElements); // since the command is invliad we use the blank ballot currentBallots.unshift(this.ballots[0].copy()); currentBallotsPathElements.unshift(this.ballotTree!.genProof(0).pathElements); @@ -659,7 +789,7 @@ export class Poll implements IPoll { // store the salt in the circuit inputs circuitInputs.newSbSalt = newSbSalt; - const newStateRoot = this.stateTree!.root; + const newStateRoot = this.pollStateTree!.root; const newBallotRoot = this.ballotTree!.root; // create a commitment to the state and ballot tree roots // this will be the hash of the roots with a salt @@ -734,7 +864,7 @@ export class Poll implements IPoll { encPubKeys = encPubKeys.slice((index - 1) * messageBatchSize, index * messageBatchSize); // cache tree roots - const currentStateRoot = this.stateTree!.root; + const currentStateRoot = this.pollStateTree!.root; const currentBallotRoot = this.ballotTree!.root; // calculate the current state and ballot root // commitment which is the hash of the state tree @@ -771,7 +901,7 @@ export class Poll implements IPoll { * @returns The state leaves and ballots of the poll */ processAllMessages = (): { stateLeaves: StateLeaf[]; ballots: Ballot[] } => { - const stateLeaves = this.stateLeaves.map((x) => x.copy()); + const stateLeaves = this.pollStateLeaves.map((x) => x.copy()); const ballots = this.ballots.map((x) => x.copy()); // process all messages in one go (batch by batch but without manual intervention) @@ -927,7 +1057,7 @@ export class Poll implements IPoll { ]); // cache vars - const stateRoot = this.stateTree!.root; + const stateRoot = this.pollStateTree!.root; const ballotRoot = this.ballotTree!.root; const sbSalt = this.sbSalts[this.currentMessageBatchIndex]; const sbCommitment = hash3([stateRoot, ballotRoot, sbSalt]); @@ -1064,7 +1194,7 @@ export class Poll implements IPoll { const newTallyCommitment = hashLeftRight(newResultsCommitment, newSpentVoiceCreditsCommitment); // cache vars - const stateRoot = this.stateTree!.root; + const stateRoot = this.pollStateTree!.root; const ballotRoot = this.ballotTree!.root; const sbSalt = this.sbSalts[this.currentMessageBatchIndex]; const sbCommitment = hash3([stateRoot, ballotRoot, sbSalt]); @@ -1178,6 +1308,7 @@ export class Poll implements IPoll { ); copied.stateLeaves = this.stateLeaves.map((x) => x.copy()); + copied.pollStateLeaves = this.pollStateLeaves.map((x) => x.copy()); copied.messages = this.messages.map((x) => x.copy()); copied.commands = this.commands.map((x) => x.copy()); copied.ballots = this.ballots.map((x) => x.copy()); @@ -1274,6 +1405,7 @@ export class Poll implements IPoll { encPubKeys: this.encPubKeys.map((encPubKey) => encPubKey.serialize()), currentMessageBatchIndex: this.currentMessageBatchIndex, stateLeaves: this.stateLeaves.map((leaf) => leaf.toJSON()), + pollStateLeaves: this.pollStateLeaves.map((leaf) => leaf.toJSON()), results: this.tallyResult.map((result) => result.toString()), numBatchesProcessed: this.numBatchesProcessed, numSignups: this.numSignups.toString(), @@ -1299,6 +1431,7 @@ export class Poll implements IPoll { ); // set all properties + poll.pollStateLeaves = json.pollStateLeaves.map((leaf) => StateLeaf.fromJSON(leaf)); poll.ballots = json.ballots.map((ballot) => Ballot.fromJSON(ballot)); poll.encPubKeys = json.encPubKeys.map((key: string) => PubKey.deserialize(key)); poll.messages = json.messages.map((message) => Message.fromJSON(message as IMessageContractParams)); diff --git a/packages/core/ts/__tests__/Poll.test.ts b/packages/core/ts/__tests__/Poll.test.ts index af7de7739c..a8378c453d 100644 --- a/packages/core/ts/__tests__/Poll.test.ts +++ b/packages/core/ts/__tests__/Poll.test.ts @@ -1,4 +1,5 @@ import { expect } from "chai"; +import { poseidon } from "maci-crypto"; import { PCommand, Keypair, StateLeaf, PrivKey, Ballot } from "maci-domainobjs"; import { MaciState } from "../MaciState"; @@ -31,27 +32,31 @@ describe("Poll", function test() { const user1Keypair = new Keypair(); // signup the user - const user1StateIndex = maciState.signUp( - user1Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); + maciState.signUp(user1Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); // copy the state from the MaciState ref poll.updatePoll(BigInt(maciState.stateLeaves.length)); + const { privKey } = user1Keypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const stateIndex = poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp); + it("should throw if a message has an invalid state index", () => { const command = new PCommand( // invalid state index as it is one more than the number of state leaves - BigInt(user1StateIndex + 1), - user1Keypair.pubKey, + BigInt(stateIndex + 1), + pollPubKey, 0n, 1n, 0n, BigInt(pollId), ); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -64,9 +69,9 @@ describe("Poll", function test() { }); it("should throw if a message has an invalid nonce", () => { - const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, 0n, 0n, 0n, BigInt(pollId)); + const command = new PCommand(BigInt(stateIndex), pollPubKey, 0n, 0n, 0n, BigInt(pollId)); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -80,7 +85,7 @@ describe("Poll", function test() { }); it("should throw if a message has an invalid signature", () => { - const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, 0n, 0n, 0n, BigInt(pollId)); + const command = new PCommand(BigInt(stateIndex), pollPubKey, 0n, 0n, 0n, BigInt(pollId)); const signature = command.sign(new PrivKey(0n)); const ecdhKeypair = new Keypair(); @@ -96,8 +101,8 @@ describe("Poll", function test() { it("should throw if a message consumes more than the available voice credits for a user", () => { const command = new PCommand( - BigInt(user1StateIndex), - user1Keypair.pubKey, + BigInt(stateIndex), + pollPubKey, 0n, // voice credits spent would be this value ** this value BigInt(Math.sqrt(Number.parseInt(voiceCreditBalance.toString(), 10)) + 1), @@ -105,7 +110,7 @@ describe("Poll", function test() { BigInt(pollId), ); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -120,8 +125,8 @@ describe("Poll", function test() { it("should throw if a message has an invalid vote option index (>= max vote options)", () => { const command = new PCommand( - BigInt(user1StateIndex), - user1Keypair.pubKey, + BigInt(stateIndex), + pollPubKey, BigInt(maxValues.maxVoteOptions), // voice credits spent would be this value ** this value 1n, @@ -129,7 +134,7 @@ describe("Poll", function test() { BigInt(pollId), ); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -143,9 +148,9 @@ describe("Poll", function test() { }); it("should throw if a message has an invalid vote option index (< 0)", () => { - const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, -1n, 1n, 1n, BigInt(pollId)); + const command = new PCommand(BigInt(stateIndex), pollPubKey, -1n, 1n, 1n, BigInt(pollId)); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -159,9 +164,9 @@ describe("Poll", function test() { }); it("should throw when passed a message that cannot be decrypted (wrong encPubKey)", () => { - const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, 0n, 1n, 1n, BigInt(pollId)); + const command = new PCommand(BigInt(stateIndex), pollPubKey, 0n, 1n, 1n, BigInt(pollId)); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(new Keypair().privKey, coordinatorKeypair.pubKey); @@ -170,17 +175,17 @@ describe("Poll", function test() { poll.publishMessage(message, ecdhKeypair.pubKey); expect(() => { - poll.processMessage(message, user1Keypair.pubKey); + poll.processMessage(message, pollPubKey); }).to.throw("failed decryption due to either wrong encryption public key or corrupted ciphertext"); }); it("should throw when passed a corrupted message", () => { - const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, 0n, 1n, 1n, BigInt(pollId)); + const command = new PCommand(BigInt(stateIndex), pollPubKey, 0n, 1n, 1n, BigInt(pollId)); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); - const sharedKey = Keypair.genEcdhSharedKey(user1Keypair.privKey, coordinatorKeypair.pubKey); + const sharedKey = Keypair.genEcdhSharedKey(pollPrivKey, coordinatorKeypair.pubKey); const message = command.encrypt(signature, sharedKey); poll.publishMessage(message, ecdhKeypair.pubKey); @@ -188,22 +193,22 @@ describe("Poll", function test() { message.data[0] = 0n; expect(() => { - poll.processMessage(message, user1Keypair.pubKey); + poll.processMessage(message, pollPubKey); }).to.throw("failed decryption due to either wrong encryption public key or corrupted ciphertext"); }); it("should throw when going over the voice credit limit (non qv)", () => { const command = new PCommand( // invalid state index as it is one more than the number of state leaves - BigInt(user1StateIndex), - user1Keypair.pubKey, + BigInt(stateIndex), + pollPubKey, 0n, voiceCreditBalance + 1n, 1n, BigInt(pollId), ); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -218,15 +223,15 @@ describe("Poll", function test() { it("should work when submitting a valid message (voteWeight === voiceCreditBalance and non qv)", () => { const command = new PCommand( // invalid state index as it is one more than the number of state leaves - BigInt(user1StateIndex), - user1Keypair.pubKey, + BigInt(stateIndex), + pollPubKey, 0n, voiceCreditBalance, 1n, BigInt(pollId), ); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -252,11 +257,15 @@ describe("Poll", function test() { const user1Keypair = new Keypair(); // signup the user - const user1StateIndex = maciState.signUp( - user1Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); + maciState.signUp(user1Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); + + const { privKey } = user1Keypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const stateIndex = poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp); it("should throw if the state has not been copied prior to calling processMessages", () => { const tmpPoll = maciState.deployPoll( @@ -275,15 +284,15 @@ describe("Poll", function test() { it("should succeed even if we send an invalid message", () => { const command = new PCommand( // we only signed up one user so the state index is invalid - BigInt(user1StateIndex + 1), - user1Keypair.pubKey, + BigInt(stateIndex + 1), + pollPubKey, 0n, 1n, 0n, BigInt(pollId), ); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -325,26 +334,30 @@ describe("Poll", function test() { const user1Keypair = new Keypair(); // signup the user - const user1StateIndex = maciState.signUp( - user1Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); + maciState.signUp(user1Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); poll.updatePoll(BigInt(maciState.stateLeaves.length)); + const { privKey } = user1Keypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const stateIndex = poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp); + it("it should succeed even if send an invalid message", () => { const command = new PCommand( // we only signed up one user so the state index is invalid - BigInt(user1StateIndex + 1), - user1Keypair.pubKey, + BigInt(stateIndex + 1), + pollPubKey, 0n, 1n, 0n, BigInt(pollId), ); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -359,9 +372,9 @@ describe("Poll", function test() { }); it("should return the correct state leaves and ballots", () => { - const command = new PCommand(BigInt(user1StateIndex + 1), user1Keypair.pubKey, 0n, 1n, 0n, BigInt(pollId)); + const command = new PCommand(BigInt(stateIndex + 1), pollPubKey, 0n, 1n, 0n, BigInt(pollId)); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -374,14 +387,16 @@ describe("Poll", function test() { const { stateLeaves, ballots } = poll.processAllMessages(); - stateLeaves.forEach((leaf: StateLeaf, index: number) => expect(leaf.equals(poll.stateLeaves[index])).to.eq(true)); + stateLeaves.forEach((leaf: StateLeaf, index: number) => + expect(leaf.equals(poll.pollStateLeaves[index])).to.eq(true), + ); ballots.forEach((ballot: Ballot, index: number) => expect(ballot.equals(poll.ballots[index])).to.eq(true)); }); it("should have processed all messages", () => { - const command = new PCommand(BigInt(user1StateIndex + 1), user1Keypair.pubKey, 0n, 1n, 0n, BigInt(pollId)); + const command = new PCommand(BigInt(stateIndex + 1), pollPubKey, 0n, 1n, 0n, BigInt(pollId)); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -415,33 +430,32 @@ describe("Poll", function test() { const user2Keypair = new Keypair(); // signup the user - const user1StateIndex = maciState.signUp( - user1Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); + maciState.signUp(user1Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); - const user2StateIndex = maciState.signUp( - user2Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); + maciState.signUp(user2Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); poll.updatePoll(BigInt(maciState.stateLeaves.length)); + const { privKey: privKey1 } = user1Keypair; + const { privKey: pollPrivKey1, pubKey: pollPubKey1 } = new Keypair(); + + const nullifier1 = poseidon([BigInt(privKey1.rawPrivKey.toString())]); + const timestamp1 = BigInt(1); + + const stateIndex1 = poll.joinPoll(nullifier1, pollPubKey1, voiceCreditBalance, timestamp1); + + const { privKey: privKey2 } = user2Keypair; + const { privKey: pollPrivKey2, pubKey: pollPubKey2 } = new Keypair(); + + const nullifier2 = poseidon([BigInt(privKey2.rawPrivKey.toString())]); + const timestamp2 = BigInt(1); + const voteWeight = 5n; const voteOption = 0n; - const command = new PCommand( - BigInt(user1StateIndex), - user1Keypair.pubKey, - voteOption, - voteWeight, - 1n, - BigInt(pollId), - ); + const command = new PCommand(BigInt(stateIndex1), pollPubKey1, voteOption, voteWeight, 1n, BigInt(pollId)); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey1); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -482,19 +496,21 @@ describe("Poll", function test() { const secondPoll = maciState.polls.get(secondPollId)!; secondPoll.updatePoll(BigInt(maciState.stateLeaves.length)); + const stateIndex2 = secondPoll.joinPoll(nullifier2, pollPubKey2, voiceCreditBalance, timestamp2); + const secondVoteWeight = 10n; const secondVoteOption = 1n; const secondCommand = new PCommand( - BigInt(user2StateIndex), - user2Keypair.pubKey, + BigInt(stateIndex2), + pollPubKey2, secondVoteOption, secondVoteWeight, 1n, secondPollId, ); - const secondSignature = secondCommand.sign(user2Keypair.privKey); + const secondSignature = secondCommand.sign(pollPrivKey2); const secondEcdhKeypair = new Keypair(); const secondSharedKey = Keypair.genEcdhSharedKey(secondEcdhKeypair.privKey, coordinatorKeypair.pubKey); diff --git a/packages/core/ts/__tests__/e2e.test.ts b/packages/core/ts/__tests__/e2e.test.ts index 6822225f8f..84fd0e3e6e 100644 --- a/packages/core/ts/__tests__/e2e.test.ts +++ b/packages/core/ts/__tests__/e2e.test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { hash5, IncrementalQuinTree, hash2 } from "maci-crypto"; +import { hash5, IncrementalQuinTree, hash2, poseidon } from "maci-crypto"; import { PCommand, Keypair, StateLeaf, blankStateLeafHash } from "maci-domainobjs"; import { MaciState } from "../MaciState"; @@ -22,8 +22,6 @@ describe("MaciState/Poll e2e", function test() { describe("key changes", () => { const user1Keypair = new Keypair(); const user2Keypair = new Keypair(); - const user1SecondKeypair = new Keypair(); - const user2SecondKeypair = new Keypair(); let pollId: bigint; let user1StateIndex: number; let user2StateIndex: number; @@ -34,21 +32,29 @@ describe("MaciState/Poll e2e", function test() { const user1NewVoteWeight = 5n; const user2NewVoteWeight = 7n; + const { privKey: privKey1 } = user1Keypair; + const { privKey: pollPrivKey1, pubKey: pollPubKey1 } = new Keypair(); + + const nullifier1 = poseidon([BigInt(privKey1.rawPrivKey.toString())]); + const timestamp1 = BigInt(1); + + const { privKey: privKey2 } = user2Keypair; + const { privKey: pollPrivKey2, pubKey: pollPubKey2 } = new Keypair(); + + const nullifier2 = poseidon([BigInt(privKey2.rawPrivKey.toString())]); + const timestamp2 = BigInt(1); + + const { pubKey: pollPubKey1Second } = new Keypair(); + + const { pubKey: pollPubKey2Second } = new Keypair(); + describe("only user 1 changes key", () => { const maciState: MaciState = new MaciState(STATE_TREE_DEPTH); before(() => { // Sign up - user1StateIndex = maciState.signUp( - user1Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); - user2StateIndex = maciState.signUp( - user2Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); + maciState.signUp(user1Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); + maciState.signUp(user2Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); // deploy a poll pollId = maciState.deployPoll( @@ -61,19 +67,19 @@ describe("MaciState/Poll e2e", function test() { maciState.polls.get(pollId)?.updatePoll(BigInt(maciState.stateLeaves.length)); }); - it("should submit a vote for each user", () => { const poll = maciState.polls.get(pollId)!; + user1StateIndex = poll.joinPoll(nullifier1, pollPubKey1, voiceCreditBalance, timestamp1); const command1 = new PCommand( BigInt(user1StateIndex), - user1Keypair.pubKey, + pollPubKey1, user1VoteOptionIndex, user1VoteWeight, 1n, BigInt(pollId), ); - const signature1 = command1.sign(user1Keypair.privKey); + const signature1 = command1.sign(pollPrivKey1); const ecdhKeypair1 = new Keypair(); const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey); @@ -81,16 +87,17 @@ describe("MaciState/Poll e2e", function test() { const message1 = command1.encrypt(signature1, sharedKey1); poll.publishMessage(message1, ecdhKeypair1.pubKey); + user2StateIndex = poll.joinPoll(nullifier2, pollPubKey2, voiceCreditBalance, timestamp2); const command2 = new PCommand( BigInt(user2StateIndex), - user2Keypair.pubKey, + pollPubKey2, user2VoteOptionIndex, user2VoteWeight, 1n, BigInt(pollId), ); - const signature2 = command2.sign(user2Keypair.privKey); + const signature2 = command2.sign(pollPrivKey2); const ecdhKeypair2 = new Keypair(); const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); @@ -103,14 +110,14 @@ describe("MaciState/Poll e2e", function test() { const poll = maciState.polls.get(pollId)!; const command = new PCommand( BigInt(user1StateIndex), - user1SecondKeypair.pubKey, + pollPubKey1Second, user1VoteOptionIndex, user1NewVoteWeight, 1n, BigInt(pollId), ); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey1); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -129,10 +136,10 @@ describe("MaciState/Poll e2e", function test() { it("should confirm that the user key pair was changed (user's 2 one has not)", () => { const poll = maciState.polls.get(pollId)!; - const stateLeaf1 = poll.stateLeaves[user1StateIndex]; - const stateLeaf2 = poll.stateLeaves[user2StateIndex]; - expect(stateLeaf1.pubKey.equals(user1SecondKeypair.pubKey)).to.eq(true); - expect(stateLeaf2.pubKey.equals(user2Keypair.pubKey)).to.eq(true); + const stateLeaf1 = poll.pollStateLeaves[user1StateIndex]; + const stateLeaf2 = poll.pollStateLeaves[user2StateIndex]; + expect(stateLeaf1.pubKey.equals(pollPubKey1Second)).to.eq(true); + expect(stateLeaf2.pubKey.equals(pollPubKey2)).to.eq(true); }); }); @@ -142,16 +149,8 @@ describe("MaciState/Poll e2e", function test() { before(() => { // Sign up - user1StateIndex = maciState.signUp( - user1Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); - user2StateIndex = maciState.signUp( - user2Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); + maciState.signUp(user1Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); + maciState.signUp(user2Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); // deploy a poll pollId = maciState.deployPoll( @@ -166,16 +165,17 @@ describe("MaciState/Poll e2e", function test() { poll.updatePoll(BigInt(maciState.stateLeaves.length)); }); it("should submit a vote for each user", () => { + user1StateIndex = poll.joinPoll(nullifier1, pollPubKey1, voiceCreditBalance, timestamp1); const command1 = new PCommand( BigInt(user1StateIndex), - user1Keypair.pubKey, + pollPubKey1, user1VoteOptionIndex, user1VoteWeight, 1n, BigInt(pollId), ); - const signature1 = command1.sign(user1Keypair.privKey); + const signature1 = command1.sign(pollPrivKey1); const ecdhKeypair1 = new Keypair(); const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey); @@ -183,16 +183,17 @@ describe("MaciState/Poll e2e", function test() { const message1 = command1.encrypt(signature1, sharedKey1); poll.publishMessage(message1, ecdhKeypair1.pubKey); + user2StateIndex = poll.joinPoll(nullifier2, pollPubKey2, voiceCreditBalance, timestamp2); const command2 = new PCommand( BigInt(user2StateIndex), - user2Keypair.pubKey, + pollPubKey2, user2VoteOptionIndex, user2VoteWeight, 1n, BigInt(pollId), ); - const signature2 = command2.sign(user2Keypair.privKey); + const signature2 = command2.sign(pollPrivKey2); const ecdhKeypair2 = new Keypair(); const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); @@ -204,14 +205,14 @@ describe("MaciState/Poll e2e", function test() { it("user1 sends a keychange message with a new vote", () => { const command = new PCommand( BigInt(user1StateIndex), - user1SecondKeypair.pubKey, + pollPubKey1Second, user1VoteOptionIndex, user1NewVoteWeight, 1n, BigInt(pollId), ); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey1); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -223,14 +224,14 @@ describe("MaciState/Poll e2e", function test() { it("user2 sends a keychange message with a new vote", () => { const command = new PCommand( BigInt(user2StateIndex), - user2SecondKeypair.pubKey, + pollPubKey2Second, user2VoteOptionIndex, user2NewVoteWeight, 1n, BigInt(pollId), ); - const signature = command.sign(user2Keypair.privKey); + const signature = command.sign(pollPrivKey2); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -247,10 +248,10 @@ describe("MaciState/Poll e2e", function test() { }); it("should confirm that the users key pairs were changed", () => { - const stateLeaf1 = poll.stateLeaves[user1StateIndex]; - const stateLeaf2 = poll.stateLeaves[user2StateIndex]; - expect(stateLeaf1.pubKey.equals(user1SecondKeypair.pubKey)).to.eq(true); - expect(stateLeaf2.pubKey.equals(user2SecondKeypair.pubKey)).to.eq(true); + const pollStateLeaf1 = poll.pollStateLeaves[user1StateIndex]; + const pollStateLeaf2 = poll.pollStateLeaves[user2StateIndex]; + expect(pollStateLeaf1.pubKey.equals(pollPubKey1Second)).to.eq(true); + expect(pollStateLeaf2.pubKey.equals(pollPubKey2Second)).to.eq(true); }); }); @@ -260,11 +261,7 @@ describe("MaciState/Poll e2e", function test() { before(() => { // Sign up - user1StateIndex = maciState.signUp( - user1Keypair.pubKey, - voiceCreditBalance, - BigInt(Math.floor(Date.now() / 1000)), - ); + maciState.signUp(user1Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); // deploy a poll pollId = maciState.deployPoll( @@ -280,16 +277,17 @@ describe("MaciState/Poll e2e", function test() { }); it("should submit a vote for one user in one batch", () => { + user1StateIndex = poll.joinPoll(nullifier1, pollPubKey1, voiceCreditBalance, timestamp1); const command1 = new PCommand( BigInt(user1StateIndex), - user1Keypair.pubKey, + pollPubKey1, user1VoteOptionIndex, user1VoteWeight, 1n, BigInt(pollId), ); - const signature1 = command1.sign(user1Keypair.privKey); + const signature1 = command1.sign(pollPrivKey1); const ecdhKeypair1 = new Keypair(); const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey); @@ -300,16 +298,9 @@ describe("MaciState/Poll e2e", function test() { it("should fill the batch with random messages", () => { for (let i = 0; i < messageBatchSize - 1; i += 1) { - const command = new PCommand( - 1n, - user1Keypair.pubKey, - user1VoteOptionIndex, - user1VoteWeight, - 2n, - BigInt(pollId), - ); + const command = new PCommand(1n, pollPubKey1, user1VoteOptionIndex, user1VoteWeight, 2n, BigInt(pollId)); - const signature = command.sign(user1Keypair.privKey); + const signature = command.sign(pollPrivKey1); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -322,14 +313,14 @@ describe("MaciState/Poll e2e", function test() { it("should submit a new message in a new batch", () => { const command1 = new PCommand( BigInt(user1StateIndex), - user1SecondKeypair.pubKey, + pollPubKey1Second, user1VoteOptionIndex, user1NewVoteWeight, 1n, BigInt(pollId), ); - const signature1 = command1.sign(user1Keypair.privKey); + const signature1 = command1.sign(pollPrivKey1); const ecdhKeypair1 = new Keypair(); const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey); @@ -345,8 +336,8 @@ describe("MaciState/Poll e2e", function test() { }); it("should confirm that the user key pair was changed", () => { - const stateLeaf1 = poll.stateLeaves[user1StateIndex]; - expect(stateLeaf1.pubKey.equals(user1SecondKeypair.pubKey)).to.eq(true); + const pollStateLeaf1 = poll.pollStateLeaves[user1StateIndex]; + expect(pollStateLeaf1.pubKey.equals(pollPubKey1Second)).to.eq(true); }); }); }); @@ -393,16 +384,17 @@ describe("MaciState/Poll e2e", function test() { }); it("Process a batch of messages (though only 1 message is in the batch)", () => { - const command = new PCommand( - BigInt(stateIndex), - userKeypair.pubKey, - voteOptionIndex, - voteWeight, - 1n, - BigInt(pollId), - ); + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + stateIndex = poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp); + + const command = new PCommand(BigInt(stateIndex), pollPubKey, voteOptionIndex, voteWeight, 1n, BigInt(pollId)); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -415,7 +407,7 @@ describe("MaciState/Poll e2e", function test() { // Check the ballot expect(poll.ballots[1].votes[Number(voteOptionIndex)].toString()).to.eq(voteWeight.toString()); // Check the state leaf in the poll - expect(poll.stateLeaves[1].voiceCreditBalance.toString()).to.eq( + expect(poll.pollStateLeaves[1].voiceCreditBalance.toString()).to.eq( (voiceCreditBalance - voteWeight * voteWeight).toString(), ); }); @@ -440,6 +432,8 @@ describe("MaciState/Poll e2e", function test() { const voteWeight = 9n; const users: Keypair[] = []; + const pollKeys: Keypair[] = []; + const stateIndices: number[] = []; before(() => { maciState = new MaciState(STATE_TREE_DEPTH); @@ -448,6 +442,9 @@ describe("MaciState/Poll e2e", function test() { const userKeypair = new Keypair(); users.push(userKeypair); + const pollKeypair = new Keypair(); + pollKeys.push(pollKeypair); + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); } @@ -460,23 +457,30 @@ describe("MaciState/Poll e2e", function test() { ); poll = maciState.polls.get(pollId)!; poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + for (let i = 0; i < messageBatchSize - 1; i += 1) { + const nullifier = poseidon([BigInt(pollKeys[i].privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + const stateIndex = poll.joinPoll(nullifier, pollKeys[i].pubKey, voiceCreditBalance, timestamp); + stateIndices.push(stateIndex); + } }); it("should process votes correctly", () => { // 19 valid votes for (let i = 0; i < messageBatchSize - 1; i += 1) { - const userKeypair = users[i]; + const pollKeypair = pollKeys[i]; const command = new PCommand( - BigInt(i + 1), - userKeypair.pubKey, + BigInt(stateIndices[i]), + pollKeypair.pubKey, BigInt(i), // vote option index voteWeight, 1n, BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollKeypair.privKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -488,18 +492,18 @@ describe("MaciState/Poll e2e", function test() { // 19 invalid votes for (let i = 0; i < messageBatchSize - 1; i += 1) { - const userKeypair = users[i]; + const pollKeypair = pollKeys[i]; const command = new PCommand( - BigInt(i + 1), - userKeypair.pubKey, + BigInt(stateIndices[i]), + pollKeypair.pubKey, BigInt(i), // vote option index voiceCreditBalance * 2n, // invalid vote weight 1n, BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(pollKeypair.privKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -532,14 +536,14 @@ describe("MaciState/Poll e2e", function test() { // Test processAllMessages const r = poll.processAllMessages(); - expect(r.stateLeaves.length).to.eq(poll.stateLeaves.length); + expect(r.stateLeaves.length).to.eq(poll.pollStateLeaves.length); expect(r.ballots.length).to.eq(poll.ballots.length); expect(r.ballots.length).to.eq(r.stateLeaves.length); for (let i = 0; i < r.stateLeaves.length; i += 1) { - expect(r.stateLeaves[i].equals(poll.stateLeaves[i])).to.eq(true); + expect(r.stateLeaves[i].equals(poll.pollStateLeaves[i])).to.eq(true); expect(r.ballots[i].equals(poll.ballots[i])).to.eq(true); } @@ -604,22 +608,23 @@ describe("MaciState/Poll e2e", function test() { const timestamp = BigInt(Math.floor(Date.now() / 1000)); const stateLeaf = new StateLeaf(userKeypair.pubKey, voiceCreditBalance, timestamp); - stateIndex = maciState.signUp(userKeypair.pubKey, voiceCreditBalance, timestamp); + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, timestamp); stateTree.insert(blankStateLeafHash); stateTree.insert(stateLeaf.hash()); poll.updatePoll(BigInt(maciState.stateLeaves.length)); - const command = new PCommand( - BigInt(stateIndex), - userKeypair.pubKey, - voteOptionIndex, - voteWeight, - 1n, - BigInt(pollId), - ); + const { privKey } = userKeypair; + const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair(); + + const pollNullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const pollTimestamp = BigInt(1); + + stateIndex = poll.joinPoll(pollNullifier, pollPubKey, voiceCreditBalance, pollTimestamp); - const signature = command.sign(userKeypair.privKey); + const command = new PCommand(BigInt(stateIndex), pollPubKey, voteOptionIndex, voteWeight, 1n, BigInt(pollId)); + + const signature = command.sign(pollPrivKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -634,7 +639,7 @@ describe("MaciState/Poll e2e", function test() { // Check the ballot expect(poll.ballots[1].votes[Number(voteOptionIndex)].toString()).to.eq(voteWeight.toString()); // Check the state leaf in the poll - expect(poll.stateLeaves[1].voiceCreditBalance.toString()).to.eq((voiceCreditBalance - voteWeight).toString()); + expect(poll.pollStateLeaves[1].voiceCreditBalance.toString()).to.eq((voiceCreditBalance - voteWeight).toString()); }); it("Tally ballots", () => { @@ -669,7 +674,15 @@ describe("MaciState/Poll e2e", function test() { const nonce = 1n; const users = testHarness.createUsers(1); - testHarness.vote(users[0], testHarness.getStateIndex(users[0]), voteOptionIndex, voteWeight, nonce); + + const { privKey } = users[0]; + const pollKeypair = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const pollStateIndex = testHarness.joinPoll(nullifier, pollKeypair.pubKey, voiceCreditBalance, timestamp); + testHarness.vote(pollKeypair, pollStateIndex, voteOptionIndex, voteWeight, nonce); testHarness.finalizePoll(); const messageLengthResult = poll.messages.length; @@ -687,7 +700,15 @@ describe("MaciState/Poll e2e", function test() { const nonce = 1n; const users = testHarness.createUsers(1); - testHarness.vote(users[0], testHarness.getStateIndex(users[0]), voteOptionIndex, voteWeight, nonce); + + const { privKey } = users[0]; + const pollKeypair = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const pollStateIndex = testHarness.joinPoll(nullifier, pollKeypair.pubKey, voiceCreditBalance, timestamp); + testHarness.vote(pollKeypair, pollStateIndex, voteOptionIndex, voteWeight, nonce); poll.updatePoll(BigInt(testHarness.maciState.stateLeaves.length)); poll.processMessages(testHarness.pollId); @@ -719,7 +740,14 @@ describe("MaciState/Poll e2e", function test() { nonce = BigInt(Math.floor(Math.random() * 100) - 50); } while (nonce === 1n); - testHarness.vote(user, testHarness.getStateIndex(user), voteOptionIndex, voteWeight, nonce); + const { privKey } = user; + const pollKeypair = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const pollStateIndex = testHarness.joinPoll(nullifier, pollKeypair.pubKey, voiceCreditBalance, timestamp); + testHarness.vote(pollKeypair, pollStateIndex, voteOptionIndex, voteWeight, nonce); }); testHarness.finalizePoll(); @@ -750,7 +778,15 @@ describe("MaciState/Poll e2e", function test() { voteWeight = BigInt(Math.floor(Math.random() * 100) - 50); } while (voteWeight >= 1n && voteWeight <= 10n); - testHarness.vote(user, testHarness.getStateIndex(user), voteOptionIndex, voteWeight, nonce); + const { privKey } = user; + const pollKeypair = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const pollStateIndex = testHarness.joinPoll(nullifier, pollKeypair.pubKey, voiceCreditBalance, timestamp); + + testHarness.vote(pollKeypair, pollStateIndex, voteOptionIndex, voteWeight, nonce); }); testHarness.finalizePoll(); @@ -774,7 +810,14 @@ describe("MaciState/Poll e2e", function test() { users.forEach((user) => { // generate a bunch of invalid votes with incorrect state tree index - testHarness.vote(user, testHarness.getStateIndex(user) + 1, voteOptionIndex, voteWeight, nonce); + const { privKey } = user; + const pollKeypair = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const pollStateIndex = testHarness.joinPoll(nullifier, pollKeypair.pubKey, voiceCreditBalance, timestamp); + testHarness.vote(pollKeypair, pollStateIndex + 1, voteOptionIndex, voteWeight, nonce); }); testHarness.finalizePoll(); @@ -795,18 +838,28 @@ describe("MaciState/Poll e2e", function test() { const users = testHarness.createUsers(2); - const { command } = testHarness.createCommand( - users[0], - testHarness.getStateIndex(users[0]), - voteOptionIndex, - voteWeight, - nonce, - ); + const { privKey: privKey1 } = users[0]; + const pollKeypair1 = new Keypair(); + + const nullifier1 = poseidon([BigInt(privKey1.rawPrivKey.toString())]); + const timestamp1 = BigInt(1); + + const pollStateIndex1 = testHarness.joinPoll(nullifier1, pollKeypair1.pubKey, voiceCreditBalance, timestamp1); + + const { command } = testHarness.createCommand(pollKeypair1, pollStateIndex1, voteOptionIndex, voteWeight, nonce); + + const { privKey: privKey2 } = users[1]; + const pollKeypair2 = new Keypair(); + + const nullifier2 = poseidon([BigInt(privKey2.rawPrivKey.toString())]); + const timestamp2 = BigInt(1); + + testHarness.joinPoll(nullifier2, pollKeypair2.pubKey, voiceCreditBalance, timestamp2); // create an invalid signature const { signature: invalidSignature } = testHarness.createCommand( - users[1], - testHarness.getStateIndex(users[0]), + pollKeypair2, + pollStateIndex1, voteOptionIndex, voteWeight, nonce, @@ -838,9 +891,17 @@ describe("MaciState/Poll e2e", function test() { const users = testHarness.createUsers(1); + const { privKey } = users[0]; + const pollKeypair = new Keypair(); + + const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]); + const timestamp = BigInt(1); + + const pollStateIndex = testHarness.joinPoll(nullifier, pollKeypair.pubKey, voiceCreditBalance, timestamp); + const { command, signature } = testHarness.createCommand( - users[0], - testHarness.getStateIndex(users[0]), + pollKeypair, + pollStateIndex, voteOptionIndex, voteWeight, nonce, diff --git a/packages/core/ts/__tests__/utils/utils.ts b/packages/core/ts/__tests__/utils/utils.ts index cb3cec6ae8..16d2d5f493 100644 --- a/packages/core/ts/__tests__/utils/utils.ts +++ b/packages/core/ts/__tests__/utils/utils.ts @@ -70,6 +70,9 @@ export class TestHarness { return stateIndex; }; + joinPoll = (nullifier: bigint, pubKey: PubKey, newVoiceCreditBalance: bigint, timestamp: bigint): number => + this.poll.joinPoll(nullifier, pubKey, newVoiceCreditBalance, timestamp); + /** * Publishes a message to the MACI poll instance. * @param user - The keypair of the user. diff --git a/packages/core/ts/index.ts b/packages/core/ts/index.ts index e0d71333d5..199ae1cefd 100644 --- a/packages/core/ts/index.ts +++ b/packages/core/ts/index.ts @@ -2,15 +2,20 @@ export { MaciState } from "./MaciState"; export { Poll } from "./Poll"; -export { genProcessVkSig, genTallyVkSig } from "./utils/utils"; +export { genPollVkSig, genProcessVkSig, genTallyVkSig } from "./utils/utils"; export type { + IJoiningCircuitArgs, + IPollJoiningCircuitInputs, ITallyCircuitInputs, IProcessMessagesCircuitInputs, CircuitInputs, TreeDepths, BatchSizes, IJsonMaciState, + IPoll, + IJsonPoll, + IProcessMessagesOutput, } from "./utils/types"; export { STATE_TREE_ARITY, MESSAGE_BATCH_SIZE, VOTE_OPTION_TREE_ARITY } from "./utils/constants"; diff --git a/packages/core/ts/utils/types.ts b/packages/core/ts/utils/types.ts index 2cfbf14101..5496b3d51c 100644 --- a/packages/core/ts/utils/types.ts +++ b/packages/core/ts/utils/types.ts @@ -9,6 +9,7 @@ import type { Keypair, Message, PCommand, + PrivKey, PubKey, StateLeaf, } from "maci-domainobjs"; @@ -43,7 +44,7 @@ export interface BatchSizes { */ export interface IMaciState { // This method is used for signing up users to the state tree. - signUp(pubKey: PubKey, initialVoiceCreditBalance: bigint, timestamp: bigint): number; + signUp(pubKey: PubKey, initialVoiceCreditBalance: bigint, timestamp: bigint, stateRoot: bigint): number; // This method is used for deploying poll. deployPoll( pollEndTimestamp: bigint, @@ -63,6 +64,8 @@ export interface IMaciState { * An interface which represents the public API of the Poll class. */ export interface IPoll { + // Check if nullifier was already used for joining + hasJoined(nullifier: bigint): boolean; // These methods are used for sending a message to the poll from user publishMessage(message: Message, encPubKey: PubKey): void; // These methods are used to generate circuit inputs @@ -94,6 +97,7 @@ export interface IJsonPoll { encPubKeys: string[]; currentMessageBatchIndex: number; stateLeaves: IJsonStateLeaf[]; + pollStateLeaves: IJsonStateLeaf[]; results: string[]; numBatchesProcessed: number; numSignups: string; @@ -129,6 +133,32 @@ export interface IProcessMessagesOutput { command?: PCommand; } +/** + * An interface describing the joiningCircuitInputs function arguments + */ +export interface IJoiningCircuitArgs { + maciPrivKey: PrivKey; + stateLeafIndex: bigint; + credits: bigint; + pollPrivKey: PrivKey; + pollPubKey: PubKey; +} +/** + * An interface describing the circuit inputs to the PollJoining circuit + */ +export interface IPollJoiningCircuitInputs { + privKey: string; + pollPrivKey: string; + pollPubKey: string[]; + stateLeaf: string[]; + siblings: string[][]; + indices: string[]; + nullifier: string; + credits: string; + stateRoot: string; + actualStateTreeDepth: string; + inputHash: string; +} /** * An interface describing the circuit inputs to the ProcessMessage circuit */ diff --git a/packages/core/ts/utils/utils.ts b/packages/core/ts/utils/utils.ts index bab492f212..e0fcaacdb0 100644 --- a/packages/core/ts/utils/utils.ts +++ b/packages/core/ts/utils/utils.ts @@ -1,5 +1,16 @@ /* eslint-disable no-bitwise */ +/** + * This function generates the signature of a ProcessMessage Verifying Key(VK). + * This can be used to check if a ProcessMessages' circuit VK is registered + * in a smart contract that holds several VKs. + * @param stateTreeDepth - The depth of the state tree. + * @param voteOptionTreeDepth - The depth of the vote option tree. + * @returns Returns a signature for querying if a verifying key with the given parameters is already registered in the contract. + */ +export const genPollVkSig = (stateTreeDepth: number, voteOptionTreeDepth: number): bigint => + (BigInt(stateTreeDepth) << 64n) + BigInt(voteOptionTreeDepth); + /** * This function generates the signature of a ProcessMessage Verifying Key(VK). * This can be used to check if a ProcessMessages' circuit VK is registered diff --git a/packages/crypto/ts/hashing.ts b/packages/crypto/ts/hashing.ts index 4f3b5da1b6..c59a62d23c 100644 --- a/packages/crypto/ts/hashing.ts +++ b/packages/crypto/ts/hashing.ts @@ -116,6 +116,7 @@ export const hashN = (numElements: number, elements: Plaintext): bigint => { }; // hash functions +export const hashLeanIMT = (a: bigint, b: bigint): bigint => hashN(2, [a, b]); export const hash2 = (elements: Plaintext): bigint => hashN(2, elements); export const hash3 = (elements: Plaintext): bigint => hashN(3, elements); export const hash4 = (elements: Plaintext): bigint => hashN(4, elements); diff --git a/packages/crypto/ts/index.ts b/packages/crypto/ts/index.ts index e2b4b19403..bfaf8e003c 100644 --- a/packages/crypto/ts/index.ts +++ b/packages/crypto/ts/index.ts @@ -19,7 +19,19 @@ export { export { G1Point, G2Point, genRandomBabyJubValue } from "./babyjub"; -export { sha256Hash, hashLeftRight, hashN, hash2, hash3, hash4, hash5, hash12, hashOne } from "./hashing"; +export { + sha256Hash, + hashLeftRight, + hashN, + hash2, + hash3, + hash4, + hash5, + hash12, + hashOne, + poseidon, + hashLeanIMT, +} from "./hashing"; export { inCurve } from "@zk-kit/baby-jubjub"; diff --git a/packages/integrationTests/ts/__tests__/integration.test.ts b/packages/integrationTests/ts/__tests__/integration.test.ts index a424a0b86b..3f21f4e771 100644 --- a/packages/integrationTests/ts/__tests__/integration.test.ts +++ b/packages/integrationTests/ts/__tests__/integration.test.ts @@ -15,10 +15,12 @@ import { timeTravel, verify, DeployedContracts, + PollContracts, + joinPoll, } from "maci-cli"; import { getDefaultSigner } from "maci-contracts"; import { MaciState, TreeDepths } from "maci-core"; -import { genPubKey, genRandomSalt } from "maci-crypto"; +import { genPubKey, genRandomSalt, poseidon } from "maci-crypto"; import { Keypair, PCommand, PrivKey, PubKey } from "maci-domainobjs"; import fs from "fs"; @@ -72,6 +74,7 @@ describe("Integration tests", function test() { intStateTreeDepth: INT_STATE_TREE_DEPTH, voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH, messageBatchSize: MESSAGE_BATCH_SIZE, + pollJoiningZkeyPath: path.resolve(__dirname, "../../../cli/zkeys/PollJoining_10_test/PollJoining_10_test.0.zkey"), processMessagesZkeyPathQv: path.resolve( __dirname, "../../../cli/zkeys/ProcessMessages_10-20-2_test/ProcessMessages_10-20-2_test.0.zkey", @@ -124,10 +127,7 @@ describe("Integration tests", function test() { pollId = maciState.deployPoll( BigInt(Date.now() + duration * 60000), -<<<<<<< HEAD:packages/integrationTests/ts/__tests__/integration.test.ts -======= maxVoteOptions, ->>>>>>> e84f61047 (feat: anonymous poll joining milestone 1 (#1625)):integrationTests/ts/__tests__/integration.test.ts treeDepths, messageBatchSize, coordinatorKeypair, @@ -159,10 +159,12 @@ describe("Integration tests", function test() { data.suites.forEach((testCase) => { it(testCase.description, async () => { const users = genTestUserCommands(testCase.numUsers, testCase.numVotesPerUser, testCase.bribers, testCase.votes); + const pollKeys: Keypair[] = Array.from({ length: testCase.numUsers }, () => new Keypair()); // loop through all users and generate keypair + signup for (let i = 0; i < users.length; i += 1) { const user = users[i]; + const pollKey = pollKeys[i]; const timestamp = Date.now(); // signup const stateIndex = BigInt( @@ -175,9 +177,37 @@ describe("Integration tests", function test() { }).then((result) => result.stateIndex), ); + await joinPoll({ + maciAddress: contracts.maciAddress, + privateKey: user.keypair.privKey.serialize(), + pollPrivKey: pollKey.privKey.serialize(), + stateIndex, + pollId, + pollJoiningZkey: path.resolve(__dirname, "../../../cli/zkeys/PollJoining_10_test/PollJoining_10_test.0.zkey"), + useWasm: true, + pollWasm: path.resolve( + __dirname, + "../../../cli/zkeys/PollJoining_10_test/PollJoining_10_test_js/PollJoining_10_test.wasm", + ), + pollWitgen: path.resolve( + __dirname, + "../../../cli/zkeys/PollJoining_10_test/PollJoining_10_test_cpp/PollJoining_10_test", + ), + rapidsnark: `${homedir()}/rapidsnark/build/prover`, + signer, + newVoiceCreditBalance: BigInt(initialVoiceCredits), + quiet: true, + }); + // signup on local maci state maciState.signUp(user.keypair.pubKey, BigInt(initialVoiceCredits), BigInt(timestamp)); + // join the poll on local + const inputNullifier = BigInt(user.keypair.privKey.asCircuitInputs()); + const nullifier = poseidon([inputNullifier]); + const poll = maciState.polls.get(pollId); + poll?.joinPoll(nullifier, pollKey.pubKey, BigInt(initialVoiceCredits), BigInt(timestamp)); + // publish messages for (let j = 0; j < user.votes.length; j += 1) { const isKeyChange = testCase.changeUsersKeys && j in testCase.changeUsersKeys[i]; @@ -197,7 +227,7 @@ describe("Integration tests", function test() { // actually publish it const encryptionKey = await publish({ - pubkey: user.keypair.pubKey.serialize(), + pubkey: pollKey.pubKey.serialize(), stateIndex, voteOptionIndex: voteOptionIndex!, nonce, @@ -206,7 +236,7 @@ describe("Integration tests", function test() { maciAddress: contracts.maciAddress, salt, // if it's a key change command, then we pass the old private key otherwise just pass the current - privateKey: isKeyChange ? oldKeypair.privKey.serialize() : user.keypair.privKey.serialize(), + privateKey: isKeyChange ? oldKeypair.privKey.serialize() : pollKey.privKey.serialize(), signer, }); @@ -216,14 +246,14 @@ describe("Integration tests", function test() { // create the command to add to the local state const command = new PCommand( stateIndex, - user.keypair.pubKey, + pollKey.pubKey, voteOptionIndex!, newVoteWeight!, nonce, pollId, salt, ); - const signature = command.sign(isKeyChange ? oldKeypair.privKey : user.keypair.privKey); + const signature = command.sign(isKeyChange ? oldKeypair.privKey : pollKey.privKey); const message = command.encrypt(signature, Keypair.genEcdhSharedKey(encPrivKey, coordinatorKeypair.pubKey)); maciState.polls.get(pollId)?.publishMessage(message, encPubKey); } diff --git a/packages/integrationTests/ts/__tests__/utils/constants.ts b/packages/integrationTests/ts/__tests__/utils/constants.ts index 2583b36f16..746d580358 100644 --- a/packages/integrationTests/ts/__tests__/utils/constants.ts +++ b/packages/integrationTests/ts/__tests__/utils/constants.ts @@ -31,7 +31,7 @@ export const votingDurationInSeconds = 3600; export const tallyBatchSize = 4; export const quadVoteTallyBatchSize = 4; export const voteOptionsMaxLeafIndex = 3; -export const duration = 300; +export const duration = 2000; export const intStateTreeDepth = 1; export const STATE_TREE_DEPTH = 10; export const INT_STATE_TREE_DEPTH = 1; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b715a047fc..ad2dcc7b0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -346,11 +346,14 @@ importers: specifier: ^5.0.0 version: 5.0.0(tbfc45wgryrmkpgagik5qs5cue) '@openzeppelin/contracts': - specifier: ^5.1.0 - version: 5.1.0 - '@openzeppelin/merkle-tree': - specifier: ^1.0.7 - version: 1.0.7 + specifier: ^5.0.2 + version: 5.0.2 + '@zk-kit/imt.sol': + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 + '@zk-kit/lean-imt': + specifier: ^2.1.0 + version: 2.1.0 circomlibjs: specifier: ^0.1.7 version: 0.1.7 @@ -424,6 +427,9 @@ importers: packages/core: dependencies: + '@zk-kit/lean-imt': + specifier: ^2.1.0 + version: 2.1.0 maci-crypto: specifier: ^2.5.0 version: link:../crypto @@ -11362,7 +11368,15 @@ snapshots: '@colors/colors@1.5.0': {} - '@commander-js/extra-typings@12.1.0(commander@12.1.0)': + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + requiresBuild: true + + /@commander-js/extra-typings@12.1.0(commander@12.1.0): + resolution: {integrity: sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg==} + peerDependencies: + commander: ~12.1.0 dependencies: commander: 12.1.0 @@ -14078,7 +14092,10 @@ snapshots: '@types/json5@0.0.29': {} - '@types/katex@0.16.7': {} + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + requiresBuild: true + dev: true '@types/lodash@4.17.4': {} @@ -14435,11 +14452,33 @@ snapshots: '@zk-kit/baby-jubjub': 1.0.3 '@zk-kit/utils': 1.2.1 - '@zk-kit/utils@1.2.1': + /@zk-kit/lean-imt@2.1.0: + resolution: {integrity: sha512-RbG6QmTrurken7HzrJQouKiXKyGTpcoD+czQ1jvExRIA83k9w+SEsRdB7anPE8WoMKWAandDe09BzDCk6AirSw==} + dependencies: + '@zk-kit/utils': 1.2.0 + dev: false + + /@zk-kit/poseidon-cipher@0.3.1: + resolution: {integrity: sha512-3plpr4Dk0EADSRPJ0NLNt7x+QG8zlJhT264zVGRxgl4yhraE2C/wAxrclUx1mcw8I04hYoXf1BTd0noAIwd5/A==} + dependencies: + '@zk-kit/baby-jubjub': 1.0.1 + '@zk-kit/utils': 1.0.0 + dev: false + + /@zk-kit/utils@1.0.0: + resolution: {integrity: sha512-v5UjrZiaRNAN2UJmTFHvlMktaA2Efc2qN1Mwd4060ExX12yRhY8ZhzdlDODhnuHkvW5zPukuBHgQhHMScNP3Pg==} dependencies: buffer: 6.0.3 - '@zkochan/js-yaml@0.0.7': + /@zk-kit/utils@1.2.0: + resolution: {integrity: sha512-Ut9zfnlBVpopZG/s600Ds/FPSWXiPhO4q8949kmXTzwDXytjnvFbDZIFdWqE/lA7/NZjvykiTnnVwmanMxv2+w==} + dependencies: + buffer: 6.0.3 + dev: false + + /@zkochan/js-yaml@0.0.7: + resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} + hasBin: true dependencies: argparse: 2.0.1 @@ -16734,7 +16773,9 @@ snapshots: - debug - utf-8-validate - ethereum-bloom-filters@1.1.0: + /ethereum-bloom-filters@1.1.0: + resolution: {integrity: sha512-J1gDRkLpuGNvWYzWslBQR9cDV4nd4kfvVTE/Wy4Kkm4yb3EYRSlyi0eB/inTsSTTVyA0+HyzHgbr95Fn/Z1fSw==} + deprecated: do not use this package use package versions above as this can miss some topics dependencies: '@noble/hashes': 1.4.0 @@ -18755,7 +18796,10 @@ snapshots: jsonparse: 1.3.1 lodash.get: 4.4.2 - json5@1.0.2: + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + requiresBuild: true dependencies: minimist: 1.2.8 @@ -22226,7 +22270,11 @@ snapshots: strip-bom-string@1.0.0: {} - strip-bom@3.0.0: {} + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + requiresBuild: true + dev: true strip-bom@4.0.0: {} @@ -22532,7 +22580,18 @@ snapshots: source-map-support: 0.5.21 yn: 2.0.0 - tsconfig-paths@3.15.0: + /tsconfig-paths-webpack-plugin@4.1.0: + resolution: {integrity: sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==} + engines: {node: '>=10.13.0'} + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.17.0 + tsconfig-paths: 4.2.0 + dev: true + + /tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + requiresBuild: true dependencies: '@types/json5': 0.0.29 json5: 1.0.2