From 55fa6f14d7ed91e0bdbece447d905ba5f6744e86 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 1 May 2024 15:57:10 +0100 Subject: [PATCH] feat(multiple-polls): allow concurrent polls use lazyIMT for signups to allow for concurrent polls --- .../circom/core/non-qv/processMessages.circom | 65 +++-- circuits/circom/core/non-qv/tallyVotes.circom | 14 +- .../circom/core/qv/processMessages.circom | 67 +++-- circuits/circom/core/qv/tallyVotes.circom | 16 +- .../circom/trees/incrementalMerkleTree.circom | 93 ++++++- .../utils/processMessagesInputHasher.circom | 6 +- circuits/circom/utils/processTopup.circom | 14 +- circuits/ts/__tests__/CeremonyParams.test.ts | 38 +-- circuits/ts/__tests__/ProcessMessages.test.ts | 139 ++-------- circuits/ts/__tests__/TallyVotes.test.ts | 28 +- circuits/ts/__tests__/utils/constants.ts | 2 +- circuits/ts/types.ts | 1 + .../ceremony-params/ceremonyParams.test.ts | 2 +- cli/tests/e2e/e2e.test.ts | 92 +++++++ cli/ts/commands/deploy.ts | 7 +- cli/ts/commands/genLocalState.ts | 2 +- cli/ts/commands/genProofs.ts | 6 +- cli/ts/commands/mergeSignups.ts | 59 +---- cli/ts/commands/proveOnChain.ts | 7 +- cli/ts/commands/setVerifyingKeys.ts | 4 +- cli/ts/utils/interfaces.ts | 1 - contracts/contracts/MACI.sol | 82 ++---- contracts/contracts/MessageProcessor.sol | 5 +- contracts/contracts/Poll.sol | 35 +-- contracts/contracts/Tally.sol | 13 +- contracts/contracts/interfaces/IMACI.sol | 16 +- contracts/contracts/interfaces/IPoll.sol | 14 +- .../contracts/trees/EmptyBallotRoots.sol | 10 +- contracts/contracts/trees/LazyIMT.sol | 241 ++++++++++++++++++ .../trees/zeros/MerkleBinaryBlankSl.sol | 43 ++++ contracts/package.json | 1 + contracts/scripts/compileSol.ts | 10 +- contracts/tasks/deploy/maci/09-maci.ts | 15 -- contracts/tasks/deploy/poll/01-poll.ts | 14 +- contracts/tasks/helpers/ProofGenerator.ts | 2 +- contracts/tasks/helpers/TreeMerger.ts | 75 +----- contracts/tasks/helpers/types.ts | 10 - contracts/tasks/runner/merge.ts | 12 +- contracts/tests/MACI.test.ts | 116 +++------ contracts/tests/MessageProcessor.test.ts | 3 +- contracts/tests/Poll.test.ts | 7 - contracts/tests/Tally.test.ts | 28 +- contracts/tests/TallyNonQv.test.ts | 5 +- contracts/tests/constants.ts | 7 +- contracts/tests/utils.ts | 3 +- contracts/ts/deploy.ts | 7 +- contracts/ts/genEmptyBallotRootsContract.ts | 7 +- contracts/ts/genMaciState.ts | 4 +- contracts/ts/types.ts | 3 - core/ts/Poll.ts | 40 ++- core/ts/__tests__/Poll.test.ts | 9 +- core/ts/__tests__/e2e.test.ts | 20 +- core/ts/index.ts | 2 +- core/ts/utils/constants.ts | 2 +- core/ts/utils/types.ts | 1 + crypto/ts/quinTree.ts | 2 +- pnpm-lock.yaml | 13 + .../solidity-docs/interfaces/IMACI.md | 94 ------- .../versioned_docs/version-v1.3_alpha/spec.md | 2 +- 59 files changed, 836 insertions(+), 800 deletions(-) create mode 100644 contracts/contracts/trees/LazyIMT.sol create mode 100644 contracts/contracts/trees/zeros/MerkleBinaryBlankSl.sol delete mode 100644 website/versioned_docs/version-v1.2/solidity-docs/interfaces/IMACI.md diff --git a/circuits/circom/core/non-qv/processMessages.circom b/circuits/circom/core/non-qv/processMessages.circom index d6b2cb2697..644126dc03 100644 --- a/circuits/circom/core/non-qv/processMessages.circom +++ b/circuits/circom/core/non-qv/processMessages.circom @@ -32,8 +32,10 @@ include "../../trees/incrementalQuinaryTree.circom"; assert(msgTreeDepth >= msgBatchDepth); // Default for IQT (quinary trees). - var TREE_ARITY = 5; - var batchSize = TREE_ARITY ** msgBatchDepth; + var MESSAGE_TREE_ARITY = 5; + // Default for Binary trees. + var STATE_TREE_ARITY = 2; + var batchSize = MESSAGE_TREE_ARITY ** msgBatchDepth; var MSG_LENGTH = 11; var PACKED_CMD_LENGTH = 4; var STATE_LEAF_LENGTH = 4; @@ -64,7 +66,7 @@ include "../../trees/incrementalQuinaryTree.circom"; // The messages. signal input msgs[batchSize][MSG_LENGTH]; // Sibling messages. - signal input msgSubrootPathElements[msgTreeDepth - msgBatchDepth][TREE_ARITY - 1]; + signal input msgSubrootPathElements[msgTreeDepth - msgBatchDepth][MESSAGE_TREE_ARITY - 1]; // The coordinator's private key. signal input coordPrivKey; // The cooordinator's public key (derived from the contract). @@ -73,6 +75,10 @@ include "../../trees/incrementalQuinaryTree.circom"; signal input encPubKeys[batchSize][2]; // The current state root (before the processing). signal input currentStateRoot; + // The actual tree depth (might be <= stateTreeDepth). + // @note it is a public input to ensure fair processing from + // the coordinator (no censoring) + signal input actualStateTreeDepth; // The state leaves upon which messages are applied. // transform(currentStateLeaf[4], message5) => newStateLeaf4 @@ -84,7 +90,7 @@ include "../../trees/incrementalQuinaryTree.circom"; signal input currentStateLeaves[batchSize][STATE_LEAF_LENGTH]; // The Merkle path to each incremental new state root. - signal input currentStateLeavesPathElements[batchSize][stateTreeDepth][TREE_ARITY - 1]; + signal input currentStateLeavesPathElements[batchSize][stateTreeDepth][STATE_TREE_ARITY - 1]; // The salted commitment to the state and ballot roots. signal input currentSbCommitment; signal input currentSbSalt; @@ -95,10 +101,10 @@ include "../../trees/incrementalQuinaryTree.circom"; signal input currentBallotRoot; // Intermediate ballots. signal input currentBallots[batchSize][BALLOT_LENGTH]; - signal input currentBallotsPathElements[batchSize][stateTreeDepth][TREE_ARITY - 1]; + signal input currentBallotsPathElements[batchSize][stateTreeDepth][STATE_TREE_ARITY - 1]; // Intermediate vote weights. signal input currentVoteWeights[batchSize]; - signal input currentVoteWeightsPathElements[batchSize][voteOptionTreeDepth][TREE_ARITY - 1]; + signal input currentVoteWeightsPathElements[batchSize][voteOptionTreeDepth][MESSAGE_TREE_ARITY - 1]; // nb. The messages are processed in REVERSE order. // Therefore, the index of the first message to process does not match the index of the @@ -138,7 +144,8 @@ include "../../trees/incrementalQuinaryTree.circom"; msgRoot, currentSbCommitment, newSbCommitment, - pollEndTimestamp + pollEndTimestamp, + actualStateTreeDepth ); // The unpacked values from packedVals. @@ -152,12 +159,12 @@ include "../../trees/incrementalQuinaryTree.circom"; // ----------------------------------------------------------------------- // 0. Ensure that the maximum vote options signal is valid and if // the maximum users signal is valid. - var maxVoValid = LessEqThan(32)([maxVoteOptions, TREE_ARITY ** voteOptionTreeDepth]); + var maxVoValid = LessEqThan(32)([maxVoteOptions, MESSAGE_TREE_ARITY ** voteOptionTreeDepth]); maxVoValid === 1; // Check numSignUps <= the max number of users (i.e., number of state leaves // that can fit the state tree). - var numSignUpsValid = LessEqThan(32)([numSignUps, TREE_ARITY ** stateTreeDepth]); + var numSignUpsValid = LessEqThan(32)([numSignUps, STATE_TREE_ARITY ** stateTreeDepth]); numSignUpsValid === 1; // Hash each Message to check their existence in the Message tree. @@ -171,7 +178,7 @@ include "../../trees/incrementalQuinaryTree.circom"; // e.g. [m, z, z, z, z] if there is only 1 real message in the batch // This makes possible to have a batch of messages which is only partially full. var computedLeaves[batchSize]; - var computedPathElements[msgTreeDepth - msgBatchDepth][TREE_ARITY - 1]; + var computedPathElements[msgTreeDepth - msgBatchDepth][MESSAGE_TREE_ARITY - 1]; var computedPathIndex[msgTreeDepth - msgBatchDepth]; for (var i = 0; i < batchSize; i++) { @@ -180,7 +187,7 @@ include "../../trees/incrementalQuinaryTree.circom"; } for (var i = 0; i < msgTreeDepth - msgBatchDepth; i++) { - for (var j = 0; j < TREE_ARITY - 1; j++) { + for (var j = 0; j < MESSAGE_TREE_ARITY - 1; j++) { computedPathElements[i][j] = msgSubrootPathElements[i][j]; } } @@ -267,22 +274,23 @@ include "../../trees/incrementalQuinaryTree.circom"; // Start from batchSize and decrement for process in reverse order. for (var i = batchSize - 1; i >= 0; i--) { // Process as vote type message. - var currentStateLeavesPathElement[stateTreeDepth][TREE_ARITY - 1]; - var currentBallotPathElement[stateTreeDepth][TREE_ARITY - 1]; - var currentVoteWeightsPathElement[voteOptionTreeDepth][TREE_ARITY - 1]; + var currentStateLeavesPathElement[stateTreeDepth][STATE_TREE_ARITY - 1]; + var currentBallotPathElement[stateTreeDepth][STATE_TREE_ARITY - 1]; + var currentVoteWeightsPathElement[voteOptionTreeDepth][MESSAGE_TREE_ARITY - 1]; for (var j = 0; j < stateTreeDepth; j++) { - for (var k = 0; k < TREE_ARITY - 1; k++) { + for (var k = 0; k < STATE_TREE_ARITY - 1; k++) { currentStateLeavesPathElement[j][k] = currentStateLeavesPathElements[i][j][k]; currentBallotPathElement[j][k] = currentBallotsPathElements[i][j][k]; } } for (var j = 0; j < voteOptionTreeDepth; j++) { - for (var k = 0; k < TREE_ARITY - 1; k++) { + for (var k = 0; k < MESSAGE_TREE_ARITY - 1; k++) { currentVoteWeightsPathElement[j][k] = currentVoteWeightsPathElements[i][j][k]; } } + (computedNewVoteStateRoot[i], computedNewVoteBallotRoot[i]) = ProcessOneNonQv(stateTreeDepth, voteOptionTreeDepth)( msgs[i][0], numSignUps, @@ -290,6 +298,7 @@ include "../../trees/incrementalQuinaryTree.circom"; pollEndTimestamp, stateRoots[i + 1], ballotRoots[i + 1], + actualStateTreeDepth, currentStateLeaves[i], currentStateLeavesPathElement, currentBallots[i], @@ -315,6 +324,7 @@ include "../../trees/incrementalQuinaryTree.circom"; msgs[i][1], msgs[i][2], numSignUps, + actualStateTreeDepth, currentStateLeaves[i], currentStateLeavesPathElement ); @@ -349,7 +359,8 @@ template ProcessOneNonQv(stateTreeDepth, voteOptionTreeDepth) { var BALLOT_LENGTH = 2; var MSG_LENGTH = 11; var PACKED_CMD_LENGTH = 4; - var TREE_ARITY = 5; + var MESSAGE_TREE_ARITY = 5; + var STATE_TREE_ARITY = 2; var BALLOT_NONCE_IDX = 0; // Ballot vote option (VO) root index. var BALLOT_VO_ROOT_IDX = 1; @@ -374,19 +385,21 @@ template ProcessOneNonQv(stateTreeDepth, voteOptionTreeDepth) { signal input currentStateRoot; // The current value of the ballot tree root. signal input currentBallotRoot; + // The actual tree depth (might be <= stateTreeDepth). + signal input actualStateTreeDepth; // The state leaf and related path elements. signal input stateLeaf[STATE_LEAF_LENGTH]; // Sibling nodes at each level of the state tree to verify the specific state leaf. - signal input stateLeafPathElements[stateTreeDepth][TREE_ARITY - 1]; + signal input stateLeafPathElements[stateTreeDepth][STATE_TREE_ARITY - 1]; // The ballot and related path elements. signal input ballot[BALLOT_LENGTH]; - signal input ballotPathElements[stateTreeDepth][TREE_ARITY - 1]; + signal input ballotPathElements[stateTreeDepth][STATE_TREE_ARITY - 1]; // The current vote weight and related path elements. signal input currentVoteWeight; - signal input currentVoteWeightsPathElements[voteOptionTreeDepth][TREE_ARITY - 1]; + signal input currentVoteWeightsPathElements[voteOptionTreeDepth][MESSAGE_TREE_ARITY - 1]; // Inputs related to the command being processed. signal input cmdStateIndex; @@ -450,12 +463,13 @@ template ProcessOneNonQv(stateTreeDepth, voteOptionTreeDepth) { var stateLeafIndexValid = SafeLessThan(N_BITS)([indexByType, numSignUps]); var stateIndexMux = Mux1()([0, indexByType], stateLeafIndexValid); - var computedStateLeafPathIndices[stateTreeDepth] = QuinGeneratePathIndices(stateTreeDepth)(stateIndexMux); + var computedStateLeafPathIndices[stateTreeDepth] = MerkleGeneratePathIndices(stateTreeDepth)(stateIndexMux); // 3. Verify that the original state leaf exists in the given state root. var stateLeafHash = PoseidonHasher(4)(stateLeaf); - var stateLeafQip = QuinTreeInclusionProof(stateTreeDepth)( + var stateLeafQip = BinaryMerkleRoot(stateTreeDepth)( stateLeafHash, + actualStateTreeDepth, computedStateLeafPathIndices, stateLeafPathElements ); @@ -468,7 +482,7 @@ template ProcessOneNonQv(stateTreeDepth, voteOptionTreeDepth) { ballot[BALLOT_VO_ROOT_IDX] ]); - var computedBallotQip = QuinTreeInclusionProof(stateTreeDepth)( + var computedBallotQip = MerkleTreeInclusionProof(stateTreeDepth)( computedBallot, computedStateLeafPathIndices, ballotPathElements @@ -538,8 +552,9 @@ template ProcessOneNonQv(stateTreeDepth, voteOptionTreeDepth) { stateLeaf[STATE_LEAF_TIMESTAMP_IDX] ]); - var computedNewStateLeafQip = QuinTreeInclusionProof(stateTreeDepth)( + var computedNewStateLeafQip = BinaryMerkleRoot(stateTreeDepth)( computedNewStateLeafhash, + actualStateTreeDepth, computedStateLeafPathIndices, stateLeafPathElements ); @@ -549,7 +564,7 @@ template ProcessOneNonQv(stateTreeDepth, voteOptionTreeDepth) { // 7. Generate a new ballot root. var newBallotNonceMux = Mux1()([ballot[BALLOT_NONCE_IDX], computedNewBallotNonce], computedIsMessageEqual); var computedNewBallot = PoseidonHasher(2)([newBallotNonceMux, newBallotVoRoot]); - var computedNewBallotQip = QuinTreeInclusionProof(stateTreeDepth)( + var computedNewBallotQip = MerkleTreeInclusionProof(stateTreeDepth)( computedNewBallot, computedStateLeafPathIndices, ballotPathElements diff --git a/circuits/circom/core/non-qv/tallyVotes.circom b/circuits/circom/core/non-qv/tallyVotes.circom index b4df81b571..2feb0232f7 100644 --- a/circuits/circom/core/non-qv/tallyVotes.circom +++ b/circuits/circom/core/non-qv/tallyVotes.circom @@ -5,6 +5,7 @@ include "./comparators.circom"; // zk-kit import include "./unpack-element.circom"; // local imports +include "../../trees/incrementalMerkleTree.circom"; include "../../trees/incrementalQuinaryTree.circom"; include "../../utils/calculateTotal.circom"; include "../../utils/hashers.circom"; @@ -28,9 +29,10 @@ template TallyVotesNonQv( // Number of children per node in the tree, defining the tree's branching factor. var TREE_ARITY = 5; + var BALLOT_TREE_ARITY = 2; // The number of ballots processed at once, determined by the depth of the intermediate state tree. - var batchSize = TREE_ARITY ** intStateTreeDepth; + var batchSize = BALLOT_TREE_ARITY ** intStateTreeDepth; // Number of voting options available, determined by the depth of the vote option tree. var numVoteOptions = TREE_ARITY ** voteOptionTreeDepth; @@ -69,7 +71,7 @@ template TallyVotesNonQv( // Ballots and their corresponding path elements for verification in the tree. signal input ballots[batchSize][BALLOT_LENGTH]; - signal input ballotPathElements[k][TREE_ARITY - 1]; + signal input ballotPathElements[k][BALLOT_TREE_ARITY - 1]; signal input votes[batchSize][numVoteOptions]; // Current results for each vote option. @@ -122,14 +124,14 @@ template TallyVotesNonQv( computedBallotHashers[i] = PoseidonHasher(2)([ballots[i][BALLOT_NONCE_IDX], ballots[i][BALLOT_VO_ROOT_IDX]]); } - var computedBallotSubroot = QuinCheckRoot(intStateTreeDepth)(computedBallotHashers); - var computedBallotPathIndices[k] = QuinGeneratePathIndices(k)(computedBatchNum); + var computedBallotSubroot = CheckRoot(intStateTreeDepth)(computedBallotHashers); + var computedBallotPathIndices[k] = MerkleGeneratePathIndices(k)(computedBatchNum); // Verifies each ballot's existence within the ballot tree. - QuinLeafExists(k)( + LeafExists(k)( computedBallotSubroot, - computedBallotPathIndices, ballotPathElements, + computedBallotPathIndices, ballotRoot ); diff --git a/circuits/circom/core/qv/processMessages.circom b/circuits/circom/core/qv/processMessages.circom index 5472f59bb2..c432e23342 100644 --- a/circuits/circom/core/qv/processMessages.circom +++ b/circuits/circom/core/qv/processMessages.circom @@ -12,6 +12,7 @@ include "../../utils/processMessagesInputHasher.circom"; include "../../utils/processTopup.circom"; include "../../utils/qv/stateLeafAndBallotTransformer.circom"; include "../../trees/incrementalQuinaryTree.circom"; +include "../../trees/incrementalMerkleTree.circom"; /** * Proves the correctness of processing a batch of MACI messages. @@ -32,8 +33,10 @@ template ProcessMessages( assert(msgTreeDepth >= msgBatchDepth); // Default for IQT (quinary trees). - var TREE_ARITY = 5; - var batchSize = TREE_ARITY ** msgBatchDepth; + var MESSAGE_TREE_ARITY = 5; + // Default for binary trees. + var STATE_TREE_ARITY = 2; + var batchSize = MESSAGE_TREE_ARITY ** msgBatchDepth; var MSG_LENGTH = 11; var PACKED_CMD_LENGTH = 4; var STATE_LEAF_LENGTH = 4; @@ -64,7 +67,7 @@ template ProcessMessages( // The messages. signal input msgs[batchSize][MSG_LENGTH]; // Sibling messages. - signal input msgSubrootPathElements[msgTreeDepth - msgBatchDepth][TREE_ARITY - 1]; + signal input msgSubrootPathElements[msgTreeDepth - msgBatchDepth][MESSAGE_TREE_ARITY - 1]; // The coordinator's private key. signal input coordPrivKey; // The cooordinator's public key (derived from the contract). @@ -73,6 +76,10 @@ template ProcessMessages( signal input encPubKeys[batchSize][2]; // The current state root (before the processing). signal input currentStateRoot; + // The actual tree depth (might be <= stateTreeDepth). + // @note it is a public input to ensure fair processing from + // the coordinator (no censoring) + signal input actualStateTreeDepth; // The state leaves upon which messages are applied. // transform(currentStateLeaf[4], message5) => newStateLeaf4 @@ -84,7 +91,7 @@ template ProcessMessages( signal input currentStateLeaves[batchSize][STATE_LEAF_LENGTH]; // The Merkle path to each incremental new state root. - signal input currentStateLeavesPathElements[batchSize][stateTreeDepth][TREE_ARITY - 1]; + signal input currentStateLeavesPathElements[batchSize][stateTreeDepth][STATE_TREE_ARITY - 1]; // The salted commitment to the state and ballot roots. signal input currentSbCommitment; signal input currentSbSalt; @@ -95,10 +102,10 @@ template ProcessMessages( signal input currentBallotRoot; // Intermediate ballots. signal input currentBallots[batchSize][BALLOT_LENGTH]; - signal input currentBallotsPathElements[batchSize][stateTreeDepth][TREE_ARITY - 1]; + signal input currentBallotsPathElements[batchSize][stateTreeDepth][STATE_TREE_ARITY - 1]; // Intermediate vote weights. signal input currentVoteWeights[batchSize]; - signal input currentVoteWeightsPathElements[batchSize][voteOptionTreeDepth][TREE_ARITY - 1]; + signal input currentVoteWeightsPathElements[batchSize][voteOptionTreeDepth][MESSAGE_TREE_ARITY - 1]; // nb. The messages are processed in REVERSE order. // Therefore, the index of the first message to process does not match the index of the @@ -138,7 +145,8 @@ template ProcessMessages( msgRoot, currentSbCommitment, newSbCommitment, - pollEndTimestamp + pollEndTimestamp, + actualStateTreeDepth ); // The unpacked values from packedVals. @@ -151,12 +159,12 @@ template ProcessMessages( // 0. Ensure that the maximum vote options signal is valid and if // the maximum users signal is valid. - var maxVoValid = LessEqThan(32)([maxVoteOptions, TREE_ARITY ** voteOptionTreeDepth]); + var maxVoValid = LessEqThan(32)([maxVoteOptions, MESSAGE_TREE_ARITY ** voteOptionTreeDepth]); maxVoValid === 1; // Check numSignUps <= the max number of users (i.e., number of state leaves // that can fit the state tree). - var numSignUpsValid = LessEqThan(32)([numSignUps, TREE_ARITY ** stateTreeDepth]); + var numSignUpsValid = LessEqThan(32)([numSignUps, STATE_TREE_ARITY ** stateTreeDepth]); numSignUpsValid === 1; // Hash each Message to check their existence in the Message tree. @@ -170,7 +178,7 @@ template ProcessMessages( // e.g. [m, z, z, z, z] if there is only 1 real message in the batch // This makes possible to have a batch of messages which is only partially full. var computedLeaves[batchSize]; - var computedPathElements[msgTreeDepth - msgBatchDepth][TREE_ARITY - 1]; + var computedPathElements[msgTreeDepth - msgBatchDepth][MESSAGE_TREE_ARITY - 1]; var computedPathIndex[msgTreeDepth - msgBatchDepth]; for (var i = 0; i < batchSize; i++) { @@ -179,7 +187,7 @@ template ProcessMessages( } for (var i = 0; i < msgTreeDepth - msgBatchDepth; i++) { - for (var j = 0; j < TREE_ARITY - 1; j++) { + for (var j = 0; j < MESSAGE_TREE_ARITY - 1; j++) { computedPathElements[i][j] = msgSubrootPathElements[i][j]; } } @@ -266,19 +274,19 @@ template ProcessMessages( // Start from batchSize and decrement for process in reverse order. for (var i = batchSize - 1; i >= 0; i--) { // Process as vote type message. - var currentStateLeavesPathElement[stateTreeDepth][TREE_ARITY - 1]; - var currentBallotPathElement[stateTreeDepth][TREE_ARITY - 1]; - var currentVoteWeightsPathElement[voteOptionTreeDepth][TREE_ARITY - 1]; + var currentStateLeavesPathElement[stateTreeDepth][STATE_TREE_ARITY - 1]; + var currentBallotPathElement[stateTreeDepth][STATE_TREE_ARITY - 1]; + var currentVoteWeightsPathElement[voteOptionTreeDepth][MESSAGE_TREE_ARITY - 1]; for (var j = 0; j < stateTreeDepth; j++) { - for (var k = 0; k < TREE_ARITY - 1; k++) { + for (var k = 0; k < STATE_TREE_ARITY - 1; k++) { currentStateLeavesPathElement[j][k] = currentStateLeavesPathElements[i][j][k]; currentBallotPathElement[j][k] = currentBallotsPathElements[i][j][k]; } } for (var j = 0; j < voteOptionTreeDepth; j++) { - for (var k = 0; k < TREE_ARITY - 1; k++) { + for (var k = 0; k < MESSAGE_TREE_ARITY - 1; k++) { currentVoteWeightsPathElement[j][k] = currentVoteWeightsPathElements[i][j][k]; } } @@ -290,6 +298,7 @@ template ProcessMessages( pollEndTimestamp, stateRoots[i + 1], ballotRoots[i + 1], + actualStateTreeDepth, currentStateLeaves[i], currentStateLeavesPathElement, currentBallots[i], @@ -315,6 +324,7 @@ template ProcessMessages( msgs[i][1], msgs[i][2], numSignUps, + actualStateTreeDepth, currentStateLeaves[i], currentStateLeavesPathElement ); @@ -349,7 +359,8 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { var BALLOT_LENGTH = 2; var MSG_LENGTH = 11; var PACKED_CMD_LENGTH = 4; - var TREE_ARITY = 5; + var MESSAGE_TREE_ARITY = 5; + var STATE_TREE_ARITY = 2; var BALLOT_NONCE_IDX = 0; // Ballot vote option (VO) root index. var BALLOT_VO_ROOT_IDX = 1; @@ -374,19 +385,21 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { signal input currentStateRoot; // The current value of the ballot tree root. signal input currentBallotRoot; + // The actual tree depth (might be <= stateTreeDepth). + signal input actualStateTreeDepth; // The state leaf and related path elements. signal input stateLeaf[STATE_LEAF_LENGTH]; // Sibling nodes at each level of the state tree to verify the specific state leaf. - signal input stateLeafPathElements[stateTreeDepth][TREE_ARITY - 1]; + signal input stateLeafPathElements[stateTreeDepth][STATE_TREE_ARITY - 1]; // The ballot and related path elements. signal input ballot[BALLOT_LENGTH]; - signal input ballotPathElements[stateTreeDepth][TREE_ARITY - 1]; + signal input ballotPathElements[stateTreeDepth][STATE_TREE_ARITY - 1]; // The current vote weight and related path elements. signal input currentVoteWeight; - signal input currentVoteWeightsPathElements[voteOptionTreeDepth][TREE_ARITY - 1]; + signal input currentVoteWeightsPathElements[voteOptionTreeDepth][MESSAGE_TREE_ARITY - 1]; // Inputs related to the command being processed. signal input cmdStateIndex; @@ -450,12 +463,13 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { var stateLeafIndexValid = SafeLessThan(N_BITS)([indexByType, numSignUps]); var stateIndexMux = Mux1()([0, indexByType], stateLeafIndexValid); - var computedStateLeafPathIndices[stateTreeDepth] = QuinGeneratePathIndices(stateTreeDepth)(stateIndexMux); + var computedStateLeafPathIndices[stateTreeDepth] = MerkleGeneratePathIndices(stateTreeDepth)(stateIndexMux); // 3. Verify that the original state leaf exists in the given state root. var stateLeafHash = PoseidonHasher(4)(stateLeaf); - var stateLeafQip = QuinTreeInclusionProof(stateTreeDepth)( + var stateLeafQip = BinaryMerkleRoot(stateTreeDepth)( stateLeafHash, + actualStateTreeDepth, computedStateLeafPathIndices, stateLeafPathElements ); @@ -468,7 +482,7 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { ballot[BALLOT_VO_ROOT_IDX] ]); - var computedBallotQip = QuinTreeInclusionProof(stateTreeDepth)( + var computedBallotQip = MerkleTreeInclusionProof(stateTreeDepth)( computedBallot, computedStateLeafPathIndices, ballotPathElements @@ -538,8 +552,9 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { stateLeaf[STATE_LEAF_TIMESTAMP_IDX] ]); - var computedNewStateLeafQip = QuinTreeInclusionProof(stateTreeDepth)( + var computedNewStateLeafQip = BinaryMerkleRoot(stateTreeDepth)( computedNewStateLeafhash, + actualStateTreeDepth, computedStateLeafPathIndices, stateLeafPathElements ); @@ -549,11 +564,11 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { // 7. Generate a new ballot root. var newBallotNonceMux = Mux1()([ballot[BALLOT_NONCE_IDX], computedNewBallotNonce], computedIsMessageEqual); var computedNewBallot = PoseidonHasher(2)([newBallotNonceMux, newBallotVoRoot]); - var computedNewBallotQip = QuinTreeInclusionProof(stateTreeDepth)( + var computedNewBallotQip = MerkleTreeInclusionProof(stateTreeDepth)( computedNewBallot, computedStateLeafPathIndices, ballotPathElements ); newBallotRoot <== computedNewBallotQip; -} \ No newline at end of file +} diff --git a/circuits/circom/core/qv/tallyVotes.circom b/circuits/circom/core/qv/tallyVotes.circom index 878304922b..24d663c503 100644 --- a/circuits/circom/core/qv/tallyVotes.circom +++ b/circuits/circom/core/qv/tallyVotes.circom @@ -5,6 +5,7 @@ include "./comparators.circom"; // zk-kit import include "./unpack-element.circom"; // local imports +include "../../trees/incrementalMerkleTree.circom"; include "../../trees/incrementalQuinaryTree.circom"; include "../../utils/calculateTotal.circom"; include "../../utils/hashers.circom"; @@ -12,7 +13,7 @@ include "../../utils/tallyVotesInputHasher.circom"; /** * Processes batches of votes and verifies their validity in a Merkle tree structure. - * This template supports the Quadratic Voting (QV). + * This template supports Quadratic Voting (QV). */ template TallyVotes( stateTreeDepth, @@ -28,9 +29,10 @@ template TallyVotes( // Number of children per node in the tree, defining the tree's branching factor. var TREE_ARITY = 5; + var BALLOT_TREE_ARITY = 2; // The number of ballots processed at once, determined by the depth of the intermediate state tree. - var batchSize = TREE_ARITY ** intStateTreeDepth; + var batchSize = BALLOT_TREE_ARITY ** intStateTreeDepth; // Number of voting options available, determined by the depth of the vote option tree. var numVoteOptions = TREE_ARITY ** voteOptionTreeDepth; @@ -69,7 +71,7 @@ template TallyVotes( // Ballots and their corresponding path elements for verification in the tree. signal input ballots[batchSize][BALLOT_LENGTH]; - signal input ballotPathElements[k][TREE_ARITY - 1]; + signal input ballotPathElements[k][BALLOT_TREE_ARITY - 1]; signal input votes[batchSize][numVoteOptions]; // Current results for each vote option. @@ -129,14 +131,14 @@ template TallyVotes( computedBallotHashers[i] = PoseidonHasher(2)([ballots[i][BALLOT_NONCE_IDX], ballots[i][BALLOT_VO_ROOT_IDX]]); } - var computedBallotSubroot = QuinCheckRoot(intStateTreeDepth)(computedBallotHashers); - var computedBallotPathIndices[k] = QuinGeneratePathIndices(k)(computedBatchNum); + var computedBallotSubroot = CheckRoot(intStateTreeDepth)(computedBallotHashers); + var computedBallotPathIndices[k] = MerkleGeneratePathIndices(k)(computedBatchNum); // Verifies each ballot's existence within the ballot tree. - QuinLeafExists(k)( + LeafExists(k)( computedBallotSubroot, - computedBallotPathIndices, ballotPathElements, + computedBallotPathIndices, ballotRoot ); diff --git a/circuits/circom/trees/incrementalMerkleTree.circom b/circuits/circom/trees/incrementalMerkleTree.circom index 92d6969076..2583211e16 100644 --- a/circuits/circom/trees/incrementalMerkleTree.circom +++ b/circuits/circom/trees/incrementalMerkleTree.circom @@ -2,8 +2,10 @@ pragma circom 2.0.0; // circomlib import include "./mux1.circom"; +include "./comparators.circom"; // local import include "../utils/hashers.circom"; +include "../utils/calculateTotal.circom"; /** * Recomputes a Merkle root from a given leaf and its path in a Merkle tree. @@ -104,10 +106,97 @@ template CheckRoot(levels) { // Initialize hashers for intermediate levels, each taking the outputs of two hashers from the previous level. var k = 0; for (var i = numLeafHashers; i < numLeafHashers + numIntermediateHashers; i++) { - computedLevelHashers[i] = PoseidonHasher(2)([hashers[k*2], hashers[k*2+1]]); + computedLevelHashers[i] = PoseidonHasher(2)([computedLevelHashers[k*2], computedLevelHashers[k*2+1]]); k++; } // Connect the output of the final hasher in the array to the root output signal. root <== computedLevelHashers[numHashers-1]; -} \ No newline at end of file +} + +/** + * Calculates the path indices required for Merkle proof verifications. + * Given a node index within an IMT and the total tree levels, it outputs the path indices leading to that node. + * The template handles the modulo and division operations to break down the tree index into its constituent path indices. + */ +template MerkleGeneratePathIndices(levels) { + var BASE = 2; + + signal input in; + signal output out[levels]; + signal n[levels + 1]; + + var m = in; + var computedResults[levels]; + + for (var i = 0; i < levels; i++) { + // circom's best practices suggests to avoid using <-- unless you + // are aware of what's going on. This is the only way to do modulo operation. + n[i] <-- m; + out[i] <-- m % BASE; + m = m \ BASE; + } + + n[levels] <-- m; + + for (var i = 0; i < levels; i++) { + // Check that each output element is less than the base. + var computedIsOutputElementLessThanBase = SafeLessThan(3)([out[i], BASE]); + computedIsOutputElementLessThanBase === 1; + + // Re-compute the total sum. + computedResults[i] = out[i] * (BASE ** i); + } + + // Check that the total sum matches the index. + var computedCalculateTotal = CalculateTotal(levels)(computedResults); + + computedCalculateTotal === in; +} + +// @note taken from @zk-kit/circuits +// if used directly in processMessages circom complains about duplicated +// templates (Ark, Poseidon, etc.) +// This circuit is designed to calculate the root of a binary Merkle +// tree given a leaf, its depth, and the necessary sibling +// information (aka proof of membership). +// A circuit is designed without the capability to iterate through +// a dynamic array. To address this, a parameter with the static maximum +// tree depth is defined (i.e. 'MAX_DEPTH'). And additionally, the circuit +// receives a dynamic depth as an input, which is utilized in calculating the +// true root of the Merkle tree. The actual depth of the Merkle tree +// may be equal to or less than the static maximum depth. +// NOTE: This circuit will successfully verify `out = 0` for `depth > MAX_DEPTH`. +// Make sure to enforce `depth <= MAX_DEPTH` outside the circuit. +template BinaryMerkleRoot(MAX_DEPTH) { + signal input leaf, depth, indices[MAX_DEPTH], siblings[MAX_DEPTH][1]; + + signal output out; + + signal nodes[MAX_DEPTH + 1]; + nodes[0] <== leaf; + + signal roots[MAX_DEPTH]; + var root = 0; + + for (var i = 0; i < MAX_DEPTH; i++) { + var isDepth = IsEqual()([depth, i]); + + roots[i] <== isDepth * nodes[i]; + + root += roots[i]; + + var c[2][2] = [ + [nodes[i], siblings[i][0]], + [siblings[i][0], nodes[i]] + ]; + + var childNodes[2] = MultiMux1(2)(c, indices[i]); + + nodes[i + 1] <== PoseidonHasher(2)(childNodes); + } + + var isDepth = IsEqual()([depth, MAX_DEPTH]); + + out <== root + isDepth * nodes[MAX_DEPTH]; +} diff --git a/circuits/circom/utils/processMessagesInputHasher.circom b/circuits/circom/utils/processMessagesInputHasher.circom index 8b7acec0aa..d9dd1999a7 100644 --- a/circuits/circom/utils/processMessagesInputHasher.circom +++ b/circuits/circom/utils/processMessagesInputHasher.circom @@ -29,6 +29,7 @@ template ProcessMessagesInputHasher() { signal input currentSbCommitment; signal input newSbCommitment; signal input pollEndTimestamp; + signal input actualStateTreeDepth; signal output maxVoteOptions; signal output numSignUps; @@ -48,12 +49,13 @@ template ProcessMessagesInputHasher() { var computedPubKey = PoseidonHasher(2)(coordPubKey); // 3. Hash the 6 inputs with SHA256. - hash <== Sha256Hasher(6)([ + hash <== Sha256Hasher(7)([ packedVals, computedPubKey, msgRoot, currentSbCommitment, newSbCommitment, - pollEndTimestamp + pollEndTimestamp, + actualStateTreeDepth ]); } \ No newline at end of file diff --git a/circuits/circom/utils/processTopup.circom b/circuits/circom/utils/processTopup.circom index 2f90787a9a..365cdcc3d7 100644 --- a/circuits/circom/utils/processTopup.circom +++ b/circuits/circom/utils/processTopup.circom @@ -6,7 +6,7 @@ include "./mux1.circom"; include "./safe-comparators.circom"; // local imports include "./hashers.circom"; -include "../trees/incrementalQuinaryTree.circom"; +include "../trees/incrementalMerkleTree.circom"; /** * Processes top-ups for a state tree, managing updates based on the transaction's message type and amount. @@ -20,7 +20,7 @@ template ProcessTopup(stateTreeDepth) { // Constants defining the structure and size of state and ballots. var STATE_LEAF_LENGTH = 4; var MSG_LENGTH = 11; - var TREE_ARITY = 5; + var STATE_TREE_ARITY = 2; // Indices for elements within a state leaf. // Public key. @@ -38,10 +38,12 @@ template ProcessTopup(stateTreeDepth) { signal input amount; signal input numSignUps; + // The actual state tree depth (can be <= stateTreeDepth) + signal input actualStateTreeDepth; // The state leaf and related path elements. signal input stateLeaf[STATE_LEAF_LENGTH]; // Sibling nodes at each level of the state tree to verify the specific state leaf. - signal input stateLeafPathElements[stateTreeDepth][TREE_ARITY - 1]; + signal input stateLeafPathElements[stateTreeDepth][STATE_TREE_ARITY - 1]; signal output newStateRoot; @@ -82,9 +84,11 @@ template ProcessTopup(stateTreeDepth) { stateLeaf[STATE_LEAF_TIMESTAMP_IDX] ]); - var computedStateLeafPathIndices[stateTreeDepth] = QuinGeneratePathIndices(stateTreeDepth)(computedIndexMux); - newStateRoot <== QuinTreeInclusionProof(stateTreeDepth)( + var computedStateLeafPathIndices[stateTreeDepth] = MerkleGeneratePathIndices(stateTreeDepth)(computedIndexMux); + + newStateRoot <== BinaryMerkleRoot(stateTreeDepth)( computedNewStateLeaf, + actualStateTreeDepth, computedStateLeafPathIndices, stateLeafPathElements ); diff --git a/circuits/ts/__tests__/CeremonyParams.test.ts b/circuits/ts/__tests__/CeremonyParams.test.ts index 81979a5595..ebc48b08f4 100644 --- a/circuits/ts/__tests__/CeremonyParams.test.ts +++ b/circuits/ts/__tests__/CeremonyParams.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { type WitnessTester } from "circomkit"; -import { MaciState, Poll, packProcessMessageSmallVals, STATE_TREE_ARITY } from "maci-core"; -import { hash5, IncrementalQuinTree, NOTHING_UP_MY_SLEEVE, AccQueue } from "maci-crypto"; +import { MaciState, Poll, packProcessMessageSmallVals, STATE_TREE_ARITY, MESSAGE_TREE_ARITY } from "maci-core"; +import { hash5, IncrementalQuinTree } from "maci-crypto"; import { PrivKey, Keypair, PCommand, Message, Ballot } from "maci-domainobjs"; import { IProcessMessagesInputs, ITallyVotesInputs } from "../types"; @@ -24,8 +24,8 @@ describe("Ceremony param tests", () => { const maxValues = { maxUsers: STATE_TREE_ARITY ** params.stateTreeDepth, - maxMessages: STATE_TREE_ARITY ** params.messageTreeDepth, - maxVoteOptions: STATE_TREE_ARITY ** params.voteOptionTreeDepth, + maxMessages: MESSAGE_TREE_ARITY ** params.messageTreeDepth, + maxVoteOptions: MESSAGE_TREE_ARITY ** params.voteOptionTreeDepth, }; const treeDepths = { @@ -35,7 +35,7 @@ describe("Ceremony param tests", () => { voteOptionTreeDepth: params.voteOptionTreeDepth, }; - const messageBatchSize = STATE_TREE_ARITY ** params.messageBatchTreeDepth; + const messageBatchSize = MESSAGE_TREE_ARITY ** params.messageBatchTreeDepth; const voiceCreditBalance = BigInt(100); const duration = 30; @@ -156,21 +156,6 @@ describe("Ceremony param tests", () => { messages.push(message2); commands.push(command2); poll.publishMessage(message2, ecdhKeypair2.pubKey); - - // Use the accumulator queue to compare the root of the message tree - const accumulatorQueue: AccQueue = new AccQueue( - params.messageTreeDepth, - STATE_TREE_ARITY, - NOTHING_UP_MY_SLEEVE, - ); - accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey)); - accumulatorQueue.enqueue(message2.hash(ecdhKeypair2.pubKey)); - accumulatorQueue.mergeSubRoots(0); - accumulatorQueue.merge(params.messageTreeDepth); - - expect(poll.messageTree.root.toString()).to.be.eq( - accumulatorQueue.getMainRoots()[params.messageTreeDepth].toString(), - ); }); it("should produce the correct state root and ballot root", async () => { @@ -216,6 +201,7 @@ describe("Ceremony param tests", () => { currentSbCommitment: inputs.currentSbCommitment, newSbCommitment: inputs.newSbCommitment, pollEndTimestamp: inputs.pollEndTimestamp, + actualStateTreeDepth: inputs.actualStateTreeDepth, }; const hasherWitness = await hasherCircuit.calculateWitness(hasherCircuitInputs); @@ -311,19 +297,7 @@ describe("Ceremony param tests", () => { commands.push(command); poll.publishMessage(message, ecdhKeypair.pubKey); - // Use the accumulator queue to compare the root of the message tree - const accumulatorQueue: AccQueue = new AccQueue( - params.messageTreeDepth, - STATE_TREE_ARITY, - NOTHING_UP_MY_SLEEVE, - ); - accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey)); - accumulatorQueue.mergeSubRoots(0); - accumulatorQueue.merge(params.messageTreeDepth); - expect(poll.messageTree.root.toString()).to.be.eq( - accumulatorQueue.getMainRoots()[params.messageTreeDepth].toString(), - ); // Process messages poll.processMessages(pollId); }); diff --git a/circuits/ts/__tests__/ProcessMessages.test.ts b/circuits/ts/__tests__/ProcessMessages.test.ts index 99ec70f66b..020a56622f 100644 --- a/circuits/ts/__tests__/ProcessMessages.test.ts +++ b/circuits/ts/__tests__/ProcessMessages.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { type WitnessTester } from "circomkit"; import { MaciState, Poll, packProcessMessageSmallVals, STATE_TREE_ARITY } from "maci-core"; -import { hash5, IncrementalQuinTree, NOTHING_UP_MY_SLEEVE, AccQueue } from "maci-crypto"; +import { IncrementalQuinTree, hash2 } from "maci-crypto"; import { PrivKey, Keypair, PCommand, Message, Ballot, PubKey } from "maci-domainobjs"; import { IProcessMessagesInputs } from "../types"; @@ -73,11 +73,10 @@ describe("ProcessMessage circuit", function test() { }); }); - describe("1 user, 2 messages", () => { + describe("5 users, 1 messages", () => { const maciState = new MaciState(STATE_TREE_DEPTH); const voteWeight = BigInt(9); const voteOptionIndex = BigInt(1); - let stateIndex: bigint; let pollId: bigint; let poll: Poll; const messages: Message[] = []; @@ -85,10 +84,11 @@ 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))), - ); + const users = new Array(5).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), @@ -123,15 +123,15 @@ describe("ProcessMessage circuit", function test() { // First command (valid) const command = new PCommand( - stateIndex, // BigInt(1), - userKeypair.pubKey, + 5n, + users[4].pubKey, voteOptionIndex, // voteOptionIndex, voteWeight, // vote weight BigInt(2), // nonce BigInt(pollId), ); - const signature = command.sign(userKeypair.privKey); + const signature = command.sign(users[4].privKey); const ecdhKeypair = new Keypair(); const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); @@ -140,91 +140,14 @@ describe("ProcessMessage circuit", function test() { commands.push(command); poll.publishMessage(message, ecdhKeypair.pubKey); - - // Second command (valid) - const command2 = new PCommand( - stateIndex, - userKeypair.pubKey, - voteOptionIndex, // voteOptionIndex, - BigInt(1), // vote weight - BigInt(1), // nonce - BigInt(pollId), - ); - const signature2 = command2.sign(userKeypair.privKey); - - const ecdhKeypair2 = new Keypair(); - const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); - const message2 = command2.encrypt(signature2, sharedKey2); - messages.push(message2); - commands.push(command2); - poll.publishMessage(message2, ecdhKeypair2.pubKey); - // Use the accumulator queue to compare the root of the message tree - const accumulatorQueue: AccQueue = new AccQueue( - treeDepths.messageTreeSubDepth, - STATE_TREE_ARITY, - NOTHING_UP_MY_SLEEVE, - ); - accumulatorQueue.enqueue(nothing.hash(encP)); - accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey)); - accumulatorQueue.enqueue(message2.hash(ecdhKeypair2.pubKey)); - accumulatorQueue.mergeSubRoots(0); - accumulatorQueue.merge(treeDepths.messageTreeDepth); - - expect(poll.messageTree.root.toString()).to.be.eq( - accumulatorQueue.getMainRoots()[treeDepths.messageTreeDepth].toString(), - ); }); - it("should produce the correct state root and ballot root", async () => { - // The current roots - const emptyBallot = new Ballot(poll.maxValues.maxVoteOptions, poll.treeDepths.voteOptionTreeDepth); - const emptyBallotHash = emptyBallot.hash(); - const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash5); - - ballotTree.insert(emptyBallot.hash()); - - poll.stateLeaves.forEach(() => { - ballotTree.insert(emptyBallotHash); - }); - - const currentStateRoot = poll.stateTree?.root; - const currentBallotRoot = ballotTree.root; - + it("should produce a proof", async () => { const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; // Calculate the witness const witness = await circuit.calculateWitness(inputs); await circuit.expectConstraintPass(witness); - - // 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 newBallotRoot = poll.ballotTree?.root; - - expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString()); - expect(newBallotRoot?.toString()).not.to.be.eq(currentBallotRoot.toString()); - - const packedVals = packProcessMessageSmallVals( - BigInt(maxValues.maxVoteOptions), - BigInt(poll.maciStateRef.numSignUps), - 0, - 3, - ); - - // Test the ProcessMessagesInputHasher circuit - const hasherCircuitInputs = { - packedVals, - coordPubKey: inputs.coordPubKey, - msgRoot: inputs.msgRoot, - currentSbCommitment: inputs.currentSbCommitment, - newSbCommitment: inputs.newSbCommitment, - pollEndTimestamp: inputs.pollEndTimestamp, - }; - - const hasherWitness = await hasherCircuit.calculateWitness(hasherCircuitInputs); - await hasherCircuit.expectConstraintPass(hasherWitness); - const hash = await getSignal(hasherCircuit, hasherWitness, "hash"); - expect(hash.toString()).to.be.eq(inputs.inputHash.toString()); }); }); @@ -299,7 +222,7 @@ describe("ProcessMessage circuit", function test() { // The current roots const emptyBallot = new Ballot(poll.maxValues.maxVoteOptions, poll.treeDepths.voteOptionTreeDepth); const emptyBallotHash = emptyBallot.hash(); - const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash5); + const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash2); ballotTree.insert(emptyBallot.hash()); @@ -339,6 +262,7 @@ describe("ProcessMessage circuit", function test() { currentSbCommitment: inputs.currentSbCommitment, newSbCommitment: inputs.newSbCommitment, pollEndTimestamp: inputs.pollEndTimestamp, + actualStateTreeDepth: inputs.actualStateTreeDepth, }; const hasherWitness = await hasherCircuit.calculateWitness(hasherCircuitInputs); @@ -401,27 +325,13 @@ describe("ProcessMessage circuit", function test() { commands.push(command); poll.publishMessage(message, ecdhKeypair.pubKey); - - // Use the accumulator queue to compare the root of the message tree - const accumulatorQueue: AccQueue = new AccQueue( - treeDepths.messageTreeSubDepth, - STATE_TREE_ARITY, - NOTHING_UP_MY_SLEEVE, - ); - accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey)); - accumulatorQueue.mergeSubRoots(0); - accumulatorQueue.merge(treeDepths.messageTreeDepth); - - expect(poll.messageTree.root.toString()).to.be.eq( - accumulatorQueue.getRoot(treeDepths.messageTreeDepth)?.toString(), - ); }); it("should produce the correct state root and ballot root", async () => { // The current roots const emptyBallot = new Ballot(poll.maxValues.maxVoteOptions, poll.treeDepths.voteOptionTreeDepth); const emptyBallotHash = emptyBallot.hash(); - const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash5); + const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash2); ballotTree.insert(emptyBallot.hash()); @@ -537,21 +447,6 @@ describe("ProcessMessage circuit", function test() { messages.push(message3); commands.push(command3); poll.publishMessage(message3, ecdhKeypair3.pubKey); - // Use the accumulator queue to compare the root of the message tree - const accumulatorQueue: AccQueue = new AccQueue( - treeDepths.messageTreeSubDepth, - STATE_TREE_ARITY, - NOTHING_UP_MY_SLEEVE, - ); - accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey)); - accumulatorQueue.enqueue(message2.hash(ecdhKeypair2.pubKey)); - accumulatorQueue.enqueue(message3.hash(ecdhKeypair3.pubKey)); - accumulatorQueue.mergeSubRoots(0); - accumulatorQueue.merge(treeDepths.messageTreeDepth); - - expect(poll.messageTree.root.toString()).to.be.eq( - accumulatorQueue.getRoot(treeDepths.messageTreeDepth)?.toString(), - ); }); describe(`1 user, ${messageBatchSize * NUM_BATCHES} messages`, () => { @@ -765,7 +660,7 @@ describe("ProcessMessage circuit", function test() { // The current roots const emptyBallot = new Ballot(poll.maxValues.maxVoteOptions, poll.treeDepths.voteOptionTreeDepth); const emptyBallotHash = emptyBallot.hash(); - const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash5); + const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash2); ballotTree.insert(emptyBallot.hash()); @@ -887,7 +782,7 @@ describe("ProcessMessage circuit", function test() { // The current roots const emptyBallot = new Ballot(poll.maxValues.maxVoteOptions, poll.treeDepths.voteOptionTreeDepth); const emptyBallotHash = emptyBallot.hash(); - const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash5); + const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash2); ballotTree.insert(emptyBallot.hash()); @@ -1031,7 +926,7 @@ describe("ProcessMessage circuit", function test() { // The current roots const emptyBallot = new Ballot(poll.maxValues.maxVoteOptions, poll.treeDepths.voteOptionTreeDepth); const emptyBallotHash = emptyBallot.hash(); - const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash5); + const ballotTree = new IncrementalQuinTree(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash2); ballotTree.insert(emptyBallot.hash()); diff --git a/circuits/ts/__tests__/TallyVotes.test.ts b/circuits/ts/__tests__/TallyVotes.test.ts index d7ebf204f3..e907652e0f 100644 --- a/circuits/ts/__tests__/TallyVotes.test.ts +++ b/circuits/ts/__tests__/TallyVotes.test.ts @@ -1,7 +1,5 @@ -import { expect } from "chai"; import { type WitnessTester } from "circomkit"; -import { MaciState, Poll, STATE_TREE_ARITY } from "maci-core"; -import { AccQueue, NOTHING_UP_MY_SLEEVE } from "maci-crypto"; +import { MaciState, Poll } from "maci-core"; import { Keypair, PCommand, Message } from "maci-domainobjs"; import { ITallyVotesInputs } from "../types"; @@ -110,19 +108,7 @@ describe("TallyVotes circuit", function test() { commands.push(command); poll.publishMessage(message, ecdhKeypair.pubKey); - // Use the accumulator queue to compare the root of the message tree - const accumulatorQueue: AccQueue = new AccQueue( - treeDepths.messageTreeSubDepth, - STATE_TREE_ARITY, - NOTHING_UP_MY_SLEEVE, - ); - accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey)); - accumulatorQueue.mergeSubRoots(0); - accumulatorQueue.merge(treeDepths.messageTreeDepth); - expect(poll.messageTree.root.toString()).to.be.eq( - accumulatorQueue.getMainRoots()[treeDepths.messageTreeDepth].toString(), - ); // Process messages poll.processMessages(pollId); }); @@ -196,19 +182,7 @@ describe("TallyVotes circuit", function test() { commands.push(command); poll.publishMessage(message, ecdhKeypair.pubKey); - // Use the accumulator queue to compare the root of the message tree - const accumulatorQueue: AccQueue = new AccQueue( - treeDepths.messageTreeSubDepth, - STATE_TREE_ARITY, - NOTHING_UP_MY_SLEEVE, - ); - accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey)); - accumulatorQueue.mergeSubRoots(0); - accumulatorQueue.merge(treeDepths.messageTreeDepth); - expect(poll.messageTree.root.toString()).to.be.eq( - accumulatorQueue.getMainRoots()[treeDepths.messageTreeDepth].toString(), - ); // Process messages poll.processMessages(pollId, false); }); diff --git a/circuits/ts/__tests__/utils/constants.ts b/circuits/ts/__tests__/utils/constants.ts index 460c85eba9..6b5aa6ff9a 100644 --- a/circuits/ts/__tests__/utils/constants.ts +++ b/circuits/ts/__tests__/utils/constants.ts @@ -7,7 +7,7 @@ export const maxValues = { maxVoteOptions: 25, }; export const treeDepths = { - intStateTreeDepth: 2, + intStateTreeDepth: 5, messageTreeDepth: 2, messageTreeSubDepth: 1, voteOptionTreeDepth: 2, diff --git a/circuits/ts/types.ts b/circuits/ts/types.ts index 49c4ea0a56..3edaa337dc 100644 --- a/circuits/ts/types.ts +++ b/circuits/ts/types.ts @@ -19,6 +19,7 @@ export interface IGenProofOptions { * Inputs for circuit ProcessMessages */ export interface IProcessMessagesInputs { + actualStateTreeDepth: bigint; inputHash: bigint; packedVals: bigint; pollEndTimestamp: bigint; diff --git a/cli/tests/ceremony-params/ceremonyParams.test.ts b/cli/tests/ceremony-params/ceremonyParams.test.ts index 17f08d10a8..9e5c0e215c 100644 --- a/cli/tests/ceremony-params/ceremonyParams.test.ts +++ b/cli/tests/ceremony-params/ceremonyParams.test.ts @@ -128,7 +128,7 @@ describe("Stress tests with ceremony params (6,9,2,3)", function test() { await setVerifyingKeys({ ...verifyingKeysArgs, signer }); }); - describe("1 user, 2 messages", () => { + describe.only("1 user, 2 messages", () => { after(() => { clean(); }); diff --git a/cli/tests/e2e/e2e.test.ts b/cli/tests/e2e/e2e.test.ts index b6253930bb..d427d12625 100644 --- a/cli/tests/e2e/e2e.test.ts +++ b/cli/tests/e2e/e2e.test.ts @@ -599,6 +599,98 @@ describe("e2e tests", function test() { }); }); + describe("multiplePolls with new signups", () => { + after(() => { + clean(); + }); + + const users = Array.from({ length: 4 }, () => new Keypair()); + + before(async () => { + // deploy the smart contracts + maciAddresses = await deploy({ ...deployArgs, signer }); + // deploy a poll contract + pollAddresses = await deployPoll({ ...deployPollArgs, signer }); + // signup + await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: users[0].pubKey.serialize(), signer }); + // publish + await publish({ + pubkey: users[0].pubKey.serialize(), + stateIndex: 1n, + voteOptionIndex: 0n, + nonce: 1n, + pollId: 0n, + newVoteWeight: 9n, + maciAddress: maciAddresses.maciAddress, + salt: genRandomSalt(), + privateKey: users[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 }); + + // time travel + await timeTravel({ ...timeTravelArgs, signer }); + // generate proofs + await mergeMessages({ ...mergeMessagesArgs, signer }); + await mergeSignups({ ...mergeSignupsArgs, signer }); + const tallyFileData = await genProofs({ ...genProofsArgs, signer }); + await proveOnChain({ ...proveOnChainArgs, signer }); + await verify({ ...verifyArgs(), tallyData: tallyFileData, signer }); + clean(); + }); + + it("should deploy a new poll", async () => { + pollAddresses = await deployPoll({ ...deployPollArgs, signer }); + }); + + it("should signup four new users", async () => { + await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: users[2].pubKey.serialize(), signer }); + await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: users[3].pubKey.serialize(), signer }); + await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: users[3].pubKey.serialize(), signer }); + await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: users[3].pubKey.serialize(), signer }); + }); + + it("should publish a new message from the first poll voter", async () => { + await publish({ + pubkey: users[0].pubKey.serialize(), + stateIndex: 1n, + voteOptionIndex: 0n, + nonce: 1n, + pollId: 1n, + newVoteWeight: 7n, + maciAddress: maciAddresses.maciAddress, + salt: genRandomSalt(), + privateKey: users[0].privKey.serialize(), + signer, + }); + }); + + it("should publish a new message by the new poll voters", async () => { + await publish({ + pubkey: users[1].pubKey.serialize(), + stateIndex: 1n, + voteOptionIndex: 0n, + nonce: 1n, + pollId: 1n, + newVoteWeight: 7n, + maciAddress: maciAddresses.maciAddress, + salt: genRandomSalt(), + privateKey: users[1].privKey.serialize(), + signer, + }); + }); + + it("should generate proofs and verify them", async () => { + await timeTravel({ ...timeTravelArgs, signer }); + await mergeMessages({ pollId: 1n, signer }); + await mergeSignups({ pollId: 1n, signer }); + await genProofs({ ...genProofsArgs, pollId: 1n, signer }); + await proveOnChain({ ...proveOnChainArgs, pollId: 1n, signer }); + await verify({ ...verifyArgs(), pollId: 1n, signer }); + }); + }); + describe("multiplePolls2", () => { const users = [ new Keypair(), diff --git a/cli/ts/commands/deploy.ts b/cli/ts/commands/deploy.ts index 7d454abe50..2f98c9d89e 100644 --- a/cli/ts/commands/deploy.ts +++ b/cli/ts/commands/deploy.ts @@ -83,7 +83,7 @@ export const deploy = async ({ ]); // deploy MACI, stateAq, PollFactory and poseidon - const { maciContract, stateAqContract, pollFactoryContract, poseidonAddrs } = await deployMaci({ + const { maciContract, pollFactoryContract, poseidonAddrs } = await deployMaci({ signUpTokenGatekeeperContractAddress: signupGatekeeperContractAddress, initialVoiceCreditBalanceAddress: initialVoiceCreditProxyContractAddress, topupCreditContractAddress: topUpCreditAddress, @@ -98,9 +98,8 @@ export const deploy = async ({ quiet: true, }); - const [maciContractAddress, stateAqContractAddress, pollFactoryContractAddress] = await Promise.all([ + const [maciContractAddress, pollFactoryContractAddress] = await Promise.all([ maciContract.getAddress(), - stateAqContract.getAddress(), pollFactoryContract.getAddress(), ]); @@ -109,7 +108,6 @@ export const deploy = async ({ storeContractAddress("SignUpGatekeeper", signupGatekeeperContractAddress, network?.name); storeContractAddress("Verifier", verifierContractAddress, network?.name); storeContractAddress("MACI", maciContractAddress, network?.name); - storeContractAddress("StateAq", stateAqContractAddress, network?.name); storeContractAddress("PollFactory", pollFactoryContractAddress, network?.name); storeContractAddress("TopupCredit", topUpCreditAddress, network?.name); storeContractAddress("PoseidonT3", poseidonAddrs.poseidonT3, network?.name); @@ -122,7 +120,6 @@ export const deploy = async ({ // return all addresses return { maciAddress: maciContractAddress, - stateAqAddress: stateAqContractAddress, pollFactoryAddress: pollFactoryContractAddress, verifierAddress: verifierContractAddress, topupCreditAddress: topUpCreditAddress, diff --git a/cli/ts/commands/genLocalState.ts b/cli/ts/commands/genLocalState.ts index 535ec5b9bf..0f8fec58b3 100644 --- a/cli/ts/commands/genLocalState.ts +++ b/cli/ts/commands/genLocalState.ts @@ -83,7 +83,7 @@ export const genLocalState = async ({ maciContract .queryFilter(maciContract.filters.DeployPoll(), startBlock) .then((events) => events[0]?.blockNumber ?? 0), - maciContract.getStateAqRoot(), + maciContract.getStateTreeRoot(), maciContract.numSignUps(), messageAqContract.getMainRoot(messageTreeDepth), ]); diff --git a/cli/ts/commands/genProofs.ts b/cli/ts/commands/genProofs.ts index 3118f7febd..2e87c2107e 100644 --- a/cli/ts/commands/genProofs.ts +++ b/cli/ts/commands/genProofs.ts @@ -155,8 +155,8 @@ export const genProofs = async ({ const messageAqContractAddr = extContracts.messageAq; const messageAqContract = AccQueueFactory.connect(messageAqContractAddr, signer); - // Check that the state and message trees have been merged for at least the first poll - if (!(await pollContract.stateAqMerged()) && pollId.toString() === "0") { + // Check that the state and message trees have been merged + if (!(await pollContract.stateAqMerged())) { logError("The state tree has not been merged yet. Please use the mergeSignups subcommmand to do so."); } @@ -189,7 +189,7 @@ export const genProofs = async ({ maciContract .queryFilter(maciContract.filters.DeployPoll(), startBlock) .then((events) => events[0]?.blockNumber ?? 0), - maciContract.getStateAqRoot(), + maciContract.getStateTreeRoot(), maciContract.numSignUps(), messageAqContract.getMainRoot(messageTreeDepth), ]); diff --git a/cli/ts/commands/mergeSignups.ts b/cli/ts/commands/mergeSignups.ts index 2ee2709ed1..282fe85fb1 100644 --- a/cli/ts/commands/mergeSignups.ts +++ b/cli/ts/commands/mergeSignups.ts @@ -1,11 +1,6 @@ -import { - AccQueue__factory as AccQueueFactory, - MACI__factory as MACIFactory, - Poll__factory as PollFactory, -} from "maci-contracts"; +import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-contracts"; import { - DEFAULT_SR_QUEUE_OPS, banner, contractExists, currentBlockTimestamp, @@ -22,13 +17,7 @@ import { * Command to merge the signups of a MACI contract * @param MergeSignupsArgs - The arguments for the mergeSignups command */ -export const mergeSignups = async ({ - pollId, - maciAddress, - numQueueOps, - signer, - quiet = true, -}: MergeSignupsArgs): Promise => { +export const mergeSignups = async ({ pollId, maciAddress, signer, quiet = true }: MergeSignupsArgs): Promise => { banner(quiet); const network = await signer.provider?.getNetwork(); @@ -55,7 +44,6 @@ export const mergeSignups = async ({ } const pollContract = PollFactory.connect(pollAddress, signer); - const accQueueContract = AccQueueFactory.connect(await maciContract.stateAq(), signer); // check if it's time to merge the message AQ const dd = await pollContract.getDeployTimeAndDuration(); @@ -66,47 +54,10 @@ export const mergeSignups = async ({ logError("Voting period is not over"); } - let subTreesMerged = false; - - // infinite loop to merge the sub trees - while (!subTreesMerged) { - // eslint-disable-next-line no-await-in-loop - subTreesMerged = await accQueueContract.subTreesMerged(); - - if (subTreesMerged) { - logGreen(quiet, success("All state subtrees have been merged.")); - } else { - // eslint-disable-next-line no-await-in-loop - await accQueueContract - .getSrIndices() - .then((data) => data.map((x) => Number(x))) - .then((indices) => { - logYellow(quiet, info(`Merging state subroots ${indices[0] + 1} / ${indices[1] + 1}`)); - }); - - // first merge the subroots - // eslint-disable-next-line no-await-in-loop - const tx = await pollContract.mergeMaciStateAqSubRoots(numQueueOps || DEFAULT_SR_QUEUE_OPS, pollId.toString()); - // eslint-disable-next-line no-await-in-loop - const receipt = await tx.wait(); - - if (receipt?.status !== 1) { - logError("Error merging state subroots"); - } - - logYellow(quiet, info(`Transaction hash: ${receipt!.hash}`)); - logGreen(quiet, success(`Executed mergeMaciStateAqSubRoots(); gas used: ${receipt!.gasUsed.toString()}`)); - } - } - - // check if the state AQ has been fully merged - const stateTreeDepth = Number(await maciContract.stateTreeDepth()); - const mainRoot = (await accQueueContract.getMainRoot(stateTreeDepth.toString())).toString(); - - if (mainRoot === "0" || pollId > 0) { + if (!(await pollContract.stateAqMerged())) { // go and merge the state tree logYellow(quiet, info("Merging subroots to a main state root...")); - const tx = await pollContract.mergeMaciStateAq(pollId.toString()); + const tx = await pollContract.mergeMaciStateAq(); const receipt = await tx.wait(); if (receipt?.status !== 1) { @@ -116,6 +67,6 @@ export const mergeSignups = async ({ logYellow(quiet, info(`Transaction hash: ${receipt!.hash}`)); logGreen(quiet, success(`Executed mergeStateAq(); gas used: ${receipt!.gasUsed.toString()}`)); } else { - logYellow(quiet, info("The state tree has already been merged.")); + logError("The state tree has already been merged."); } }; diff --git a/cli/ts/commands/proveOnChain.ts b/cli/ts/commands/proveOnChain.ts index 9976a0a072..61f2f527ce 100644 --- a/cli/ts/commands/proveOnChain.ts +++ b/cli/ts/commands/proveOnChain.ts @@ -11,7 +11,7 @@ import { formatProofForVerifierContract, type IVerifyingKeyStruct, } from "maci-contracts"; -import { STATE_TREE_ARITY } from "maci-core"; +import { MESSAGE_TREE_ARITY, STATE_TREE_ARITY } from "maci-core"; import { G1Point, G2Point, hashLeftRight } from "maci-crypto"; import { VerifyingKey } from "maci-domainobjs"; @@ -145,7 +145,7 @@ export const proveOnChain = async ({ const numSignUpsAndMessages = await pollContract.numSignUpsAndMessages(); const numSignUps = Number(numSignUpsAndMessages[0]); const numMessages = Number(numSignUpsAndMessages[1]); - const messageBatchSize = STATE_TREE_ARITY ** Number(treeDepths.messageTreeSubDepth); + const messageBatchSize = MESSAGE_TREE_ARITY ** Number(treeDepths.messageTreeSubDepth); const tallyBatchSize = STATE_TREE_ARITY ** Number(treeDepths.intStateTreeDepth); let totalMessageBatches = numMessages <= messageBatchSize ? 1 : Math.floor(numMessages / messageBatchSize); @@ -299,7 +299,6 @@ export const proveOnChain = async ({ try { // validate process messaging proof and store the new state and ballot root commitment - const tx = await mpContract.processMessages(asHex(circuitInputs.newSbCommitment as BigNumberish), formattedProof); const receipt = await tx.wait(); @@ -309,8 +308,6 @@ export const proveOnChain = async ({ logYellow(quiet, info(`Transaction hash: ${receipt!.hash}`)); - // Wait for the node to catch up - numberBatchesProcessed = Number(await mpContract.numBatchesProcessed()); logYellow(quiet, info(`Progress: ${numberBatchesProcessed} / ${totalMessageBatches}`)); diff --git a/cli/ts/commands/setVerifyingKeys.ts b/cli/ts/commands/setVerifyingKeys.ts index 57ec9b89b7..981bc9142d 100644 --- a/cli/ts/commands/setVerifyingKeys.ts +++ b/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 { genProcessVkSig, genTallyVkSig, MESSAGE_TREE_ARITY } from "maci-core"; import { VerifyingKey } from "maci-domainobjs"; import fs from "fs"; @@ -116,7 +116,7 @@ export const setVerifyingKeys = async ({ // connect to VkRegistry contract const vkRegistryContract = VkRegistryFactory.connect(vkRegistryAddress, signer); - const messageBatchSize = 5 ** messageBatchDepth; + const messageBatchSize = MESSAGE_TREE_ARITY ** messageBatchDepth; // check if the process messages vk was already set const processVkSig = genProcessVkSig(stateTreeDepth, messageTreeDepth, voteOptionTreeDepth, messageBatchSize); diff --git a/cli/ts/utils/interfaces.ts b/cli/ts/utils/interfaces.ts index 04ecc1036d..231c88c69a 100644 --- a/cli/ts/utils/interfaces.ts +++ b/cli/ts/utils/interfaces.ts @@ -6,7 +6,6 @@ import type { Groth16Proof, PublicSignals } from "snarkjs"; export interface DeployedContracts { maciAddress: string; - stateAqAddress: string; pollFactoryAddress: string; topupCreditAddress: string; poseidonT3Address: string; diff --git a/contracts/contracts/MACI.sol b/contracts/contracts/MACI.sol index 0dadabaaba..a967557f04 100644 --- a/contracts/contracts/MACI.sol +++ b/contracts/contracts/MACI.sol @@ -6,8 +6,6 @@ import { IMessageProcessorFactory } from "./interfaces/IMPFactory.sol"; import { ITallyFactory } from "./interfaces/ITallyFactory.sol"; import { InitialVoiceCreditProxy } from "./initialVoiceCreditProxy/InitialVoiceCreditProxy.sol"; import { SignUpGatekeeper } from "./gatekeepers/SignUpGatekeeper.sol"; -import { AccQueue } from "./trees/AccQueue.sol"; -import { AccQueueQuinaryBlankSl } from "./trees/AccQueueQuinaryBlankSl.sol"; import { IMACI } from "./interfaces/IMACI.sol"; import { Params } from "./utilities/Params.sol"; import { TopupCredit } from "./TopupCredit.sol"; @@ -15,6 +13,7 @@ import { Utilities } from "./utilities/Utilities.sol"; import { DomainObjs } from "./utilities/DomainObjs.sol"; import { CurveBabyJubJub } from "./crypto/BabyJubJub.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { InternalLazyIMT, LazyIMTData } from "./trees/LazyIMT.sol"; /// @title MACI - Minimum Anti-Collusion Infrastructure Version 1 /// @notice A contract which allows users to sign up, and deploy new polls @@ -28,7 +27,8 @@ contract MACI is IMACI, DomainObjs, Params, Utilities, Ownable(msg.sender) { /// in contracts/ts/genEmptyBallotRootsContract.ts file /// if we change the state tree depth! uint8 internal constant STATE_TREE_SUBDEPTH = 2; - uint8 internal constant TREE_ARITY = 5; + uint8 internal constant TREE_ARITY = 2; + uint8 internal constant MESSAGE_TREE_ARITY = 5; /// @notice The hash of a blank state leaf uint256 internal constant BLANK_STATE_LEAF_HASH = @@ -40,12 +40,6 @@ contract MACI is IMACI, DomainObjs, Params, Utilities, Ownable(msg.sender) { /// @notice A mapping of poll IDs to Poll contracts. mapping(uint256 => address) public polls; - /// @notice Whether the subtrees have been merged (can merge root before new signup) - bool public subtreesMerged; - - /// @notice The number of signups - uint256 public numSignUps; - /// @notice ERC20 contract that hold topup credits TopupCredit public immutable topupCredit; @@ -58,9 +52,9 @@ contract MACI is IMACI, DomainObjs, Params, Utilities, Ownable(msg.sender) { /// @notice Factory contract that deploy a Tally contract ITallyFactory public immutable tallyFactory; - /// @notice The state AccQueue. Represents a mapping between each user's public key + /// @notice The state tree. Represents a mapping between each user's public key /// and their voice credit balance. - AccQueue public immutable stateAq; + LazyIMTData public lazyIMTData; /// @notice Address of the SignUpGatekeeper, a contract which determines whether a /// user may sign up to vote @@ -124,15 +118,9 @@ contract MACI is IMACI, DomainObjs, Params, Utilities, Ownable(msg.sender) { TopupCredit _topupCredit, uint8 _stateTreeDepth ) payable { - // Deploy the state AccQueue - stateAq = new AccQueueQuinaryBlankSl(STATE_TREE_SUBDEPTH); - stateAq.enqueue(BLANK_STATE_LEAF_HASH); - - // because we add a blank leaf we need to count one signup - // so we don't allow max + 1 - unchecked { - numSignUps++; - } + // initialize and insert the blank leaf + InternalLazyIMT._init(lazyIMTData, _stateTreeDepth); + InternalLazyIMT._insert(lazyIMTData, BLANK_STATE_LEAF_HASH); pollFactory = _pollFactory; messageProcessorFactory = _messageProcessorFactory; @@ -163,24 +151,14 @@ contract MACI is IMACI, DomainObjs, Params, Utilities, Ownable(msg.sender) { bytes memory _signUpGatekeeperData, bytes memory _initialVoiceCreditProxyData ) public virtual { - // prevent new signups until we merge the roots (possible DoS) - if (subtreesMerged) revert SignupTemporaryBlocked(); - // ensure we do not have more signups than what the circuits support - if (numSignUps >= uint256(TREE_ARITY) ** uint256(stateTreeDepth)) revert TooManySignups(); + if (lazyIMTData.numberOfLeaves >= uint256(TREE_ARITY) ** uint256(stateTreeDepth)) revert TooManySignups(); // ensure that the public key is on the baby jubjub curve if (!CurveBabyJubJub.isOnCurve(_pubKey.x, _pubKey.y)) { revert InvalidPubKey(); } - // Increment the number of signups - // cannot overflow with realistic STATE_TREE_DEPTH - // values as numSignUps < 5 ** STATE_TREE_DEPTH -1 - unchecked { - numSignUps++; - } - // Register the user via the sign-up gatekeeper. This function should // throw if the user has already registered or if ineligible to do so. signUpGatekeeper.register(msg.sender, _signUpGatekeeperData); @@ -189,11 +167,12 @@ contract MACI is IMACI, DomainObjs, Params, Utilities, Ownable(msg.sender) { uint256 voiceCreditBalance = initialVoiceCreditProxy.getVoiceCredits(msg.sender, _initialVoiceCreditProxyData); uint256 timestamp = block.timestamp; - // Create a state leaf and enqueue it. + + // Create a state leaf and insert it into the tree. uint256 stateLeaf = hashStateLeaf(StateLeaf(_pubKey, voiceCreditBalance, timestamp)); - uint256 stateIndex = stateAq.enqueue(stateLeaf); + InternalLazyIMT._insert(lazyIMTData, stateLeaf); - emit SignUp(stateIndex, _pubKey.x, _pubKey.y, voiceCreditBalance, timestamp); + emit SignUp(lazyIMTData.numberOfLeaves - 1, _pubKey.x, _pubKey.y, voiceCreditBalance, timestamp); } /// @notice Deploy a new Poll contract. @@ -226,13 +205,9 @@ contract MACI is IMACI, DomainObjs, Params, Utilities, Ownable(msg.sender) { revert InvalidPubKey(); } - if (pollId > 0) { - if (!stateAq.treeMerged()) revert PreviousPollNotCompleted(pollId); - } - MaxValues memory maxValues = MaxValues({ - maxMessages: uint256(TREE_ARITY) ** _treeDepths.messageTreeDepth, - maxVoteOptions: uint256(TREE_ARITY) ** _treeDepths.voteOptionTreeDepth + maxMessages: uint256(MESSAGE_TREE_ARITY) ** _treeDepths.messageTreeDepth, + maxVoteOptions: uint256(MESSAGE_TREE_ARITY) ** _treeDepths.voteOptionTreeDepth }); address _owner = owner(); @@ -259,26 +234,8 @@ contract MACI is IMACI, DomainObjs, Params, Utilities, Ownable(msg.sender) { } /// @inheritdoc IMACI - function mergeStateAqSubRoots(uint256 _numSrQueueOps, uint256 _pollId) public onlyPoll(_pollId) { - stateAq.mergeSubRoots(_numSrQueueOps); - - // if we have merged all subtrees then put a block - if (stateAq.subTreesMerged()) { - subtreesMerged = true; - } - } - - /// @inheritdoc IMACI - function mergeStateAq(uint256 _pollId) public onlyPoll(_pollId) returns (uint256 root) { - // remove block - subtreesMerged = false; - - root = stateAq.merge(stateTreeDepth); - } - - /// @inheritdoc IMACI - function getStateAqRoot() public view returns (uint256 root) { - root = stateAq.getMainRoot(stateTreeDepth); + function getStateTreeRoot() public view returns (uint256 root) { + root = InternalLazyIMT._root(lazyIMTData); } /// @notice Get the Poll details @@ -288,4 +245,9 @@ contract MACI is IMACI, DomainObjs, Params, Utilities, Ownable(msg.sender) { if (_pollId >= nextPollId) revert PollDoesNotExist(_pollId); poll = polls[_pollId]; } + + /// @inheritdoc IMACI + function numSignUps() public view returns (uint256 signUps) { + signUps = lazyIMTData.numberOfLeaves; + } } diff --git a/contracts/contracts/MessageProcessor.sol b/contracts/contracts/MessageProcessor.sol index 866ef9f634..6a187cd001 100644 --- a/contracts/contracts/MessageProcessor.sol +++ b/contracts/contracts/MessageProcessor.sol @@ -221,6 +221,8 @@ contract MessageProcessor is Ownable(msg.sender), SnarkCommon, Hasher, CommonUti ) public view returns (uint256 inputHash) { uint256 coordinatorPubKeyHash = poll.coordinatorPubKeyHash(); + uint8 actualStateTreeDepth = poll.actualStateTreeDepth(); + // pack the values uint256 packedVals = genProcessMessagesPackedVals( _currentMessageBatchIndex, @@ -233,13 +235,14 @@ contract MessageProcessor is Ownable(msg.sender), SnarkCommon, Hasher, CommonUti (uint256 deployTime, uint256 duration) = poll.getDeployTimeAndDuration(); // generate the circuit only public input - uint256[] memory input = new uint256[](6); + uint256[] memory input = new uint256[](7); input[0] = packedVals; input[1] = coordinatorPubKeyHash; input[2] = _messageRoot; input[3] = _currentSbCommitment; input[4] = _newSbCommitment; input[5] = deployTime + duration; + input[6] = actualStateTreeDepth; inputHash = sha256Hash(input); } diff --git a/contracts/contracts/Poll.sol b/contracts/contracts/Poll.sol index da7c07b08f..49393558bf 100644 --- a/contracts/contracts/Poll.sol +++ b/contracts/contracts/Poll.sol @@ -56,6 +56,10 @@ contract Poll is Params, Utilities, SnarkCommon, Ownable(msg.sender), EmptyBallo /// before the Poll ended (stateAq merged) uint256 public numSignups; + /// @notice The actual depth of the state tree + /// to be used as public input for the circuit + uint8 public actualStateTreeDepth; + /// @notice Max values for the poll MaxValues public maxValues; @@ -225,18 +229,7 @@ contract Poll is Params, Utilities, SnarkCommon, Ownable(msg.sender), EmptyBallo } /// @inheritdoc IPoll - function mergeMaciStateAqSubRoots(uint256 _numSrQueueOps, uint256 _pollId) public onlyOwner isAfterVotingDeadline { - // This function cannot be called after the stateAq was merged - if (stateAqMerged) revert StateAqAlreadyMerged(); - - // merge subroots - extContracts.maci.mergeStateAqSubRoots(_numSrQueueOps, _pollId); - - emit MergeMaciStateAqSubRoots(_numSrQueueOps); - } - - /// @inheritdoc IPoll - function mergeMaciStateAq(uint256 _pollId) public onlyOwner isAfterVotingDeadline { + function mergeMaciStateAq() public onlyOwner isAfterVotingDeadline { // This function can only be called once per Poll after the voting // deadline if (stateAqMerged) revert StateAqAlreadyMerged(); @@ -244,10 +237,7 @@ contract Poll is Params, Utilities, SnarkCommon, Ownable(msg.sender), EmptyBallo // set merged to true so it cannot be called again stateAqMerged = true; - // the subtrees must have been merged first - if (!extContracts.maci.stateAq().subTreesMerged()) revert StateAqSubtreesNeedMerge(); - - mergedStateRoot = extContracts.maci.mergeStateAq(_pollId); + mergedStateRoot = extContracts.maci.getStateTreeRoot(); // Set currentSbCommitment uint256[3] memory sb; @@ -257,7 +247,18 @@ contract Poll is Params, Utilities, SnarkCommon, Ownable(msg.sender), EmptyBallo currentSbCommitment = hash3(sb); - numSignups = extContracts.maci.numSignUps(); + // get number of signups and cache in a var for later use + uint256 _numSignups = extContracts.maci.numSignUps(); + numSignups = _numSignups; + + // dynamically determine the actual depth of the state tree + uint8 depth = 1; + while (uint40(2) ** uint40(depth) < _numSignups) { + depth++; + } + + actualStateTreeDepth = depth; + emit MergeMaciStateAq(mergedStateRoot, numSignups); } diff --git a/contracts/contracts/Tally.sol b/contracts/contracts/Tally.sol index 6929436400..e3ca1d658f 100644 --- a/contracts/contracts/Tally.sol +++ b/contracts/contracts/Tally.sol @@ -16,7 +16,8 @@ import { DomainObjs } from "./utilities/DomainObjs.sol"; /// @notice The Tally contract is used during votes tallying /// and by users to verify the tally results. contract Tally is Ownable(msg.sender), SnarkCommon, CommonUtilities, Hasher, DomainObjs { - uint256 internal constant TREE_ARITY = 5; + uint256 internal constant TREE_ARITY = 2; + uint256 internal constant VOTE_OPTION_TREE_ARITY = 5; /// @notice The commitment to the tally results. Its initial value is 0, but after /// the tally of each batch is proven on-chain via a zk-SNARK, it should be @@ -212,14 +213,14 @@ contract Tally is Ownable(msg.sender), SnarkCommon, CommonUtilities, Hasher, Dom uint256 _leaf, uint256[][] calldata _pathElements ) internal pure returns (uint256 current) { - uint256 pos = _index % TREE_ARITY; + uint256 pos = _index % VOTE_OPTION_TREE_ARITY; current = _leaf; uint8 k; - uint256[TREE_ARITY] memory level; + uint256[VOTE_OPTION_TREE_ARITY] memory level; for (uint8 i = 0; i < _depth; ++i) { - for (uint8 j = 0; j < TREE_ARITY; ++j) { + for (uint8 j = 0; j < VOTE_OPTION_TREE_ARITY; ++j) { if (j == pos) { level[j] = current; } else { @@ -232,8 +233,8 @@ contract Tally is Ownable(msg.sender), SnarkCommon, CommonUtilities, Hasher, Dom } } - _index /= TREE_ARITY; - pos = _index % TREE_ARITY; + _index /= VOTE_OPTION_TREE_ARITY; + pos = _index % VOTE_OPTION_TREE_ARITY; current = hash5(level); } } diff --git a/contracts/contracts/interfaces/IMACI.sol b/contracts/contracts/interfaces/IMACI.sol index 2323c4fcde..bfd289040b 100644 --- a/contracts/contracts/interfaces/IMACI.sol +++ b/contracts/contracts/interfaces/IMACI.sol @@ -12,23 +12,9 @@ interface IMACI { /// @notice Return the main root of the StateAq contract /// @return The Merkle root - function getStateAqRoot() external view returns (uint256); - - /// @notice Allow Poll contracts to merge the state subroots - /// @param _numSrQueueOps Number of operations - /// @param _pollId The ID of the active Poll - function mergeStateAqSubRoots(uint256 _numSrQueueOps, uint256 _pollId) external; - - /// @notice Allow Poll contracts to merge the state root - /// @param _pollId The active Poll ID - /// @return The calculated Merkle root - function mergeStateAq(uint256 _pollId) external returns (uint256); + function getStateTreeRoot() external view returns (uint256); /// @notice Get the number of signups /// @return numsignUps The number of signups function numSignUps() external view returns (uint256); - - /// @notice Get the state AccQueue - /// @return The state AccQueue - function stateAq() external view returns (AccQueue); } diff --git a/contracts/contracts/interfaces/IPoll.sol b/contracts/contracts/interfaces/IPoll.sol index 4a3b1ac94b..919eea19e5 100644 --- a/contracts/contracts/interfaces/IPoll.sol +++ b/contracts/contracts/interfaces/IPoll.sol @@ -27,18 +27,10 @@ interface IPoll { /// to encrypt the message. function publishMessage(DomainObjs.Message memory _message, DomainObjs.PubKey calldata _encPubKey) external; - /// @notice The first step of merging the MACI state AccQueue. This allows the - /// ProcessMessages circuit to access the latest state tree and ballots via - /// currentSbCommitment. - /// @param _numSrQueueOps Number of operations - /// @param _pollId The ID of the active Poll - function mergeMaciStateAqSubRoots(uint256 _numSrQueueOps, uint256 _pollId) external; - /// @notice The second step of merging the MACI state AccQueue. This allows the /// ProcessMessages circuit to access the latest state tree and ballots via /// currentSbCommitment. - /// @param _pollId The ID of the active Poll - function mergeMaciStateAq(uint256 _pollId) external; + function mergeMaciStateAq() external; /// @notice The first step in merging the message AccQueue so that the /// ProcessMessages circuit can access the message root. @@ -92,4 +84,8 @@ interface IPoll { /// the case that none of the messages are valid. /// @return The commitment to the state leaves and the ballots function currentSbCommitment() external view returns (uint256); + + /// @notice Get the dynamic depth of the state tree at the time of poll + /// finalization (based on the number of leaves inserted) + function actualStateTreeDepth() external view returns (uint8); } diff --git a/contracts/contracts/trees/EmptyBallotRoots.sol b/contracts/contracts/trees/EmptyBallotRoots.sol index 4dc8cd6862..89a4e9d910 100644 --- a/contracts/contracts/trees/EmptyBallotRoots.sol +++ b/contracts/contracts/trees/EmptyBallotRoots.sol @@ -21,10 +21,10 @@ abstract contract EmptyBallotRoots { uint256[5] internal emptyBallotRoots; constructor() { - emptyBallotRoots[0] = uint256(6579820437991406069687396372962263845395426835385368878767605633903648955255); - emptyBallotRoots[1] = uint256(9105453741665960449792281626882014222103501499246287334255160659262747058842); - emptyBallotRoots[2] = uint256(14830222164980158319423900821611648302565544940504586015002280367515043751869); - emptyBallotRoots[3] = uint256(12031563002271722465187541954825013132282571927669361737331626664787916495335); - emptyBallotRoots[4] = uint256(5204612805325639173251450278876337947880680931527922506745154187077640790699); + emptyBallotRoots[0] = uint256(16015576667038038422103932363190100635991292382181099511410843174865570503661); + emptyBallotRoots[1] = uint256(166510078825589460025300915201657086611944528317298994959376081297530246971); + emptyBallotRoots[2] = uint256(10057734083972610459557695472359628128485394923403014377687504571662791937025); + emptyBallotRoots[3] = uint256(4904828619307091008204672239231377290495002626534171783829482835985709082773); + emptyBallotRoots[4] = uint256(18694062287284245784028624966421731916526814537891066525886866373016385890569); } } diff --git a/contracts/contracts/trees/LazyIMT.sol b/contracts/contracts/trees/LazyIMT.sol new file mode 100644 index 0000000000..cd1e60be63 --- /dev/null +++ b/contracts/contracts/trees/LazyIMT.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { PoseidonT3 } from "../crypto/PoseidonT3.sol"; + +/// @notice A struct that holds a LazyIMT data +struct LazyIMTData { + uint40 maxIndex; + uint40 numberOfLeaves; + mapping(uint256 => uint256) elements; +} + +/// @notice Custom errors +error DefaultZeroBadIndex(); +error DepthTooLarge(); +error DepthCannotBeZero(); +error NumberOfLeavesCannotBeZero(); + +/// @title InternalLazyIMT +/// @dev A LazyIMT with Zeroes value set to the hash of +/// a MACI Blank State Leaf +/// @notice This implementation is taken from zk-kit +/// https://github.com/privacy-scaling-explorations/zk-kit/blob/main/packages/imt.sol/contracts/internal/InternalLazyIMT.sol +/// and modified to work with MACI. +library InternalLazyIMT { + uint256 internal constant MAX_DEPTH = 32; + + uint40 internal constant MAX_INDEX = (1 << 32) - 1; + + uint256 internal constant Z_0 = 6769006970205099520508948723718471724660867171122235270773600567925038008762; + uint256 internal constant Z_1 = 2972820301952105722688860985556183033855705951263221082702981787813754939537; + uint256 internal constant Z_2 = 19009473369953096352828532459942637819279786575057870804186038131433538383332; + uint256 internal constant Z_3 = 1877001762518233819645599208989578372605193385355680834239714249281096297174; + uint256 internal constant Z_4 = 4022598852800694816938652741439614774645858989706174527409714109784047480520; + uint256 internal constant Z_5 = 8078617093048295855521451309865989496051030103472138252021705658681696298712; + uint256 internal constant Z_6 = 21861637049723057871988413507302821095913894718242489848472531680353400271584; + uint256 internal constant Z_7 = 2969626195902860050407584814596940245443093107470116547781577350415736914038; + uint256 internal constant Z_8 = 13863086449569754493134198846069090996475357615094865751949144794620598051673; + uint256 internal constant Z_9 = 13774233155966252113965527228795435224641075024674384267997743867571011718458; + uint256 internal constant Z_10 = 7674682532432601125535053858292577379388329393276537570517515727197672122157; + uint256 internal constant Z_11 = 2657471847139856346360223652201172662911313292042510535836997980857168085414; + uint256 internal constant Z_12 = 14112562742724116016492623819773686970029672095023612838615540190985426106768; + uint256 internal constant Z_13 = 16966520284141749853106006448832965932249937855809150844697400390499975107456; + uint256 internal constant Z_14 = 21146121663662200258116396149536742745305242191891337170899444969488030502620; + uint256 internal constant Z_15 = 8395571901509192935479743034608666551563743095742598750914087478677907730358; + uint256 internal constant Z_16 = 11584898446168752024843587018551921614604785083342073076015560385003528300499; + uint256 internal constant Z_17 = 19681365563800708744156562671961079617734353445922751560400662591064339349816; + uint256 internal constant Z_18 = 11060693795061987995391612467169498625108376769265861980249917517984263067473; + uint256 internal constant Z_19 = 20136055137568042031318427040358591430196153124171666293804511634641041409480; + uint256 internal constant Z_20 = 10438448879123510479428288344427042332522761183009746406441238260861529360499; + uint256 internal constant Z_21 = 20302411580043873005239406811066876697276902025885155920151067303221158887923; + uint256 internal constant Z_22 = 16905699456770804689394621400052823445587122726651394178036372609288266146575; + uint256 internal constant Z_23 = 13317924909658910751179983108234689413063120680580702936091220805509299490708; + uint256 internal constant Z_24 = 11624463897690689883938167321830091369950933831231839575225419984927228390345; + uint256 internal constant Z_25 = 12388077003631746290497429926117583834311703848735670874049584990731919769551; + uint256 internal constant Z_26 = 16641943593086083573943184041147806885253724243247212515325749241831788827569; + uint256 internal constant Z_27 = 8675770901378242337954792996483564563211065498082968464791979179678744114204; + uint256 internal constant Z_28 = 3741944068643598116715410464277276913339851849923986024648161859457213369743; + uint256 internal constant Z_29 = 9365051374992868354747065577256691008852056444829383197903446097138255771103; + uint256 internal constant Z_30 = 19608043542461863702809013760105552654336523908709289008189330402608282498922; + uint256 internal constant Z_31 = 15116478429455923389320892447700153271977917184085737305957533984219061034768; + uint256 internal constant Z_32 = 13372161856163346716845871420623647679532631520878788090782842562075678687737; + + /// @notice Returns the default zero value for a given index + /// @param index The index of the zero value + /// @return The zero value + function _defaultZero(uint8 index) internal pure returns (uint256) { + if (index == 0) return Z_0; + if (index == 1) return Z_1; + if (index == 2) return Z_2; + if (index == 3) return Z_3; + if (index == 4) return Z_4; + if (index == 5) return Z_5; + if (index == 6) return Z_6; + if (index == 7) return Z_7; + if (index == 8) return Z_8; + if (index == 9) return Z_9; + if (index == 10) return Z_10; + if (index == 11) return Z_11; + if (index == 12) return Z_12; + if (index == 13) return Z_13; + if (index == 14) return Z_14; + if (index == 15) return Z_15; + if (index == 16) return Z_16; + if (index == 17) return Z_17; + if (index == 18) return Z_18; + if (index == 19) return Z_19; + if (index == 20) return Z_20; + if (index == 21) return Z_21; + if (index == 22) return Z_22; + if (index == 23) return Z_23; + if (index == 24) return Z_24; + if (index == 25) return Z_25; + if (index == 26) return Z_26; + if (index == 27) return Z_27; + if (index == 28) return Z_28; + if (index == 29) return Z_29; + if (index == 30) return Z_30; + if (index == 31) return Z_31; + if (index == 32) return Z_32; + revert DefaultZeroBadIndex(); + } + + /// @notice Initializes the LazyIMT + /// @param self The LazyIMTData + /// @param depth The depth of the tree + function _init(LazyIMTData storage self, uint8 depth) internal { + if (depth > MAX_DEPTH) { + revert DepthTooLarge(); + } + self.maxIndex = uint40((1 << depth) - 1); + self.numberOfLeaves = 0; + } + + /// @notice Returns the index for a given level and index + /// @param level The level + /// @param index The index + /// @return The index for the element + function _indexForElement(uint8 level, uint40 index) internal pure returns (uint40) { + // store the elements sparsely + return MAX_INDEX * level + index; + } + + /// @notice Inserts a leaf into the LazyIMT + /// @param self The LazyIMTData + /// @param leaf The leaf to insert + function _insert(LazyIMTData storage self, uint256 leaf) internal { + uint40 index = self.numberOfLeaves; + + self.numberOfLeaves = index + 1; + + uint256 hash = leaf; + + for (uint8 i = 0; ; ) { + self.elements[_indexForElement(i, index)] = hash; + // it's a left element so we don't hash until there's a right element + if (index & 1 == 0) break; + uint40 elementIndex = _indexForElement(i, index - 1); + hash = PoseidonT3.poseidon([self.elements[elementIndex], hash]); + unchecked { + index >>= 1; + i++; + } + } + } + + /// @notice Returns the root of the LazyIMT + /// @param self The LazyIMTData + /// @return The root of the LazyIMT + function _root(LazyIMTData storage self) internal view returns (uint256) { + // this will always short circuit if self.numberOfLeaves == 0 + uint40 numberOfLeaves = self.numberOfLeaves; + // dynamically determine a depth + uint8 depth = 1; + while (uint40(2) ** uint40(depth) < numberOfLeaves) { + depth++; + } + return _root(self, numberOfLeaves, depth); + } + + /// @notice Returns the root of the LazyIMT + /// @param self The LazyIMTData + /// @param depth The depth of the tree + /// @return The root of the LazyIMT + function _root(LazyIMTData storage self, uint8 depth) internal view returns (uint256) { + if (depth == 0) { + revert DepthCannotBeZero(); + } + if (depth > MAX_DEPTH) { + revert DepthTooLarge(); + } + uint40 numberOfLeaves = self.numberOfLeaves; + require(uint40(2) ** uint40(depth) >= numberOfLeaves, "LazyIMT: ambiguous depth"); + return _root(self, numberOfLeaves, depth); + } + + /// @notice Returns the root of the LazyIMT + /// @dev Here it's assumed that the depth value is valid. + /// If it is either 0 or > 2^8-1 this function will panic. + /// @param self The LazyIMTData + /// @param numberOfLeaves The number of leaves + /// @param depth The depth of the tree + /// @return The root of the LazyIMT + function _root(LazyIMTData storage self, uint40 numberOfLeaves, uint8 depth) internal view returns (uint256) { + if (depth > MAX_DEPTH) { + revert DepthTooLarge(); + } + // this should always short circuit if self.numberOfLeaves == 0 + if (numberOfLeaves == 0) return _defaultZero(depth); + uint256[] memory levels = new uint256[](depth + 1); + _levels(self, numberOfLeaves, depth, levels); + return levels[depth]; + } + + /// @notice Updates the levels of the LazyIMT + /// @param self The LazyIMTData + /// @param numberOfLeaves The number of leaves + /// @param depth The depth of the tree + /// @param levels The levels of the tree + function _levels( + LazyIMTData storage self, + uint40 numberOfLeaves, + uint8 depth, + uint256[] memory levels + ) internal view { + if (depth > MAX_DEPTH) { + revert DepthTooLarge(); + } + if (numberOfLeaves == 0) { + revert NumberOfLeavesCannotBeZero(); + } + + // this should always short circuit if self.numberOfLeaves == 0 + uint40 index = numberOfLeaves - 1; + + if (index & 1 == 0) { + levels[0] = self.elements[_indexForElement(0, index)]; + } else { + levels[0] = _defaultZero(0); + } + + for (uint8 i = 0; i < depth; ) { + if (index & 1 == 0) { + levels[i + 1] = PoseidonT3.poseidon([levels[i], _defaultZero(i)]); + } else { + uint256 levelCount = (numberOfLeaves) >> (i + 1); + if (levelCount > index >> 1) { + uint256 parent = self.elements[_indexForElement(i + 1, index >> 1)]; + levels[i + 1] = parent; + } else { + uint256 sibling = self.elements[_indexForElement(i, index - 1)]; + levels[i + 1] = PoseidonT3.poseidon([sibling, levels[i]]); + } + } + unchecked { + index >>= 1; + i++; + } + } + } +} diff --git a/contracts/contracts/trees/zeros/MerkleBinaryBlankSl.sol b/contracts/contracts/trees/zeros/MerkleBinaryBlankSl.sol new file mode 100644 index 0000000000..dde438be02 --- /dev/null +++ b/contracts/contracts/trees/zeros/MerkleBinaryBlankSl.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +abstract contract MerkleZeros { + uint256[33] internal zeros; + + // Binary tree zeros (hash of a blank state leaf) + constructor() { + zeros[0] = uint256(6769006970205099520508948723718471724660867171122235270773600567925038008762); + zeros[1] = uint256(2972820301952105722688860985556183033855705951263221082702981787813754939537); + zeros[2] = uint256(19009473369953096352828532459942637819279786575057870804186038131433538383332); + zeros[3] = uint256(1877001762518233819645599208989578372605193385355680834239714249281096297174); + zeros[4] = uint256(4022598852800694816938652741439614774645858989706174527409714109784047480520); + zeros[5] = uint256(8078617093048295855521451309865989496051030103472138252021705658681696298712); + zeros[6] = uint256(21861637049723057871988413507302821095913894718242489848472531680353400271584); + zeros[7] = uint256(2969626195902860050407584814596940245443093107470116547781577350415736914038); + zeros[8] = uint256(13863086449569754493134198846069090996475357615094865751949144794620598051673); + zeros[9] = uint256(13774233155966252113965527228795435224641075024674384267997743867571011718458); + zeros[10] = uint256(7674682532432601125535053858292577379388329393276537570517515727197672122157); + zeros[11] = uint256(2657471847139856346360223652201172662911313292042510535836997980857168085414); + zeros[12] = uint256(14112562742724116016492623819773686970029672095023612838615540190985426106768); + zeros[13] = uint256(16966520284141749853106006448832965932249937855809150844697400390499975107456); + zeros[14] = uint256(21146121663662200258116396149536742745305242191891337170899444969488030502620); + zeros[15] = uint256(8395571901509192935479743034608666551563743095742598750914087478677907730358); + zeros[16] = uint256(11584898446168752024843587018551921614604785083342073076015560385003528300499); + zeros[17] = uint256(19681365563800708744156562671961079617734353445922751560400662591064339349816); + zeros[18] = uint256(11060693795061987995391612467169498625108376769265861980249917517984263067473); + zeros[19] = uint256(20136055137568042031318427040358591430196153124171666293804511634641041409480); + zeros[20] = uint256(10438448879123510479428288344427042332522761183009746406441238260861529360499); + zeros[21] = uint256(20302411580043873005239406811066876697276902025885155920151067303221158887923); + zeros[22] = uint256(16905699456770804689394621400052823445587122726651394178036372609288266146575); + zeros[23] = uint256(13317924909658910751179983108234689413063120680580702936091220805509299490708); + zeros[24] = uint256(11624463897690689883938167321830091369950933831231839575225419984927228390345); + zeros[25] = uint256(12388077003631746290497429926117583834311703848735670874049584990731919769551); + zeros[26] = uint256(16641943593086083573943184041147806885253724243247212515325749241831788827569); + zeros[27] = uint256(8675770901378242337954792996483564563211065498082968464791979179678744114204); + zeros[28] = uint256(3741944068643598116715410464277276913339851849923986024648161859457213369743); + zeros[29] = uint256(9365051374992868354747065577256691008852056444829383197903446097138255771103); + zeros[30] = uint256(19608043542461863702809013760105552654336523908709289008189330402608282498922); + zeros[31] = uint256(15116478429455923389320892447700153271977917184085737305957533984219061034768); + zeros[32] = uint256(13372161856163346716845871420623647679532631520878788090782842562075678687737); + } +} diff --git a/contracts/package.json b/contracts/package.json index dbb68351b6..edbd496c0f 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -73,6 +73,7 @@ "@nomicfoundation/hardhat-ethers": "^3.0.5", "@nomicfoundation/hardhat-toolbox": "^5.0.0", "@openzeppelin/contracts": "^5.0.2", + "@zk-kit/imt.sol": "2.0.0-beta.12", "circomlibjs": "^0.1.7", "ethers": "^6.12.0", "hardhat": "^2.22.3", diff --git a/contracts/scripts/compileSol.ts b/contracts/scripts/compileSol.ts index b4024cb8ee..f3c8e0838f 100644 --- a/contracts/scripts/compileSol.ts +++ b/contracts/scripts/compileSol.ts @@ -31,6 +31,12 @@ const ZERO_TREES = [ hashLength: 2, comment: "Binary tree zeros (Keccak hash of 'Maci')", }, + { + name: "MerkleBinaryBlankSl", + zero: BLANK_STATE_LEAF, + hashLength: 2, + comment: "Binary tree zeros (hash of a blank state leaf)", + }, { name: "MerkleQuinary0", zero: 0n, @@ -70,7 +76,9 @@ async function main(): Promise { ), ); - await genEmptyBallotRootsContract(); + await genEmptyBallotRootsContract().then((text) => + fs.promises.writeFile(path.resolve(__dirname, "..", "contracts/trees/EmptyBallotRoots.sol"), `${text}\n`), + ); await hre.run("compile"); diff --git a/contracts/tasks/deploy/maci/09-maci.ts b/contracts/tasks/deploy/maci/09-maci.ts index 746f7cd64f..5afc3ce2b3 100644 --- a/contracts/tasks/deploy/maci/09-maci.ts +++ b/contracts/tasks/deploy/maci/09-maci.ts @@ -8,7 +8,6 @@ const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); const DEFAULT_STATE_TREE_DEPTH = 10; -const STATE_TREE_SUBDEPTH = 2; /** * Deploy step registration and task itself @@ -93,19 +92,5 @@ deployment.deployTask("full:deploy-maci", "Deploy MACI contract").then((task) => ], network: hre.network.name, }); - - const accQueueAddress = await maciContract.stateAq(); - const accQueue = await deployment.getContract({ - name: EContracts.AccQueueQuinaryBlankSl, - address: accQueueAddress, - }); - - await storage.register({ - id: EContracts.AccQueueQuinaryBlankSl, - name: "contracts/trees/AccQueueQuinaryBlankSl.sol:AccQueueQuinaryBlankSl", - contract: accQueue, - args: [STATE_TREE_SUBDEPTH], - network: hre.network.name, - }); }), ); diff --git a/contracts/tasks/deploy/poll/01-poll.ts b/contracts/tasks/deploy/poll/01-poll.ts index b492010199..f830b95388 100644 --- a/contracts/tasks/deploy/poll/01-poll.ts +++ b/contracts/tasks/deploy/poll/01-poll.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { PubKey } from "maci-domainobjs"; -import type { AccQueueBinary, MACI, Poll } from "../../../typechain-types"; +import type { MACI, Poll } from "../../../typechain-types"; import { EMode } from "../../../ts/constants"; import { ContractStorage } from "../../helpers/ContractStorage"; @@ -36,18 +36,6 @@ deployment.deployTask("poll:deploy-poll", "Deploy poll").then((task) => const maciContract = await deployment.getContract({ name: EContracts.MACI }); const pollId = await maciContract.nextPollId(); - const stateAqContractAddress = await maciContract.stateAq(); - - const stateAq = await deployment.getContract({ - name: EContracts.AccQueue, - address: stateAqContractAddress, - }); - const isTreeMerged = await stateAq.treeMerged(); - - if (pollId > 0n && !isTreeMerged) { - console.log("Previous poll is not completed"); - return; - } const coordinatorPubkey = deployment.getDeployConfigField(EContracts.Poll, "coordinatorPubkey"); const pollDuration = deployment.getDeployConfigField(EContracts.Poll, "pollDuration"); diff --git a/contracts/tasks/helpers/ProofGenerator.ts b/contracts/tasks/helpers/ProofGenerator.ts index ee4f6e17a3..1e903c7c82 100644 --- a/contracts/tasks/helpers/ProofGenerator.ts +++ b/contracts/tasks/helpers/ProofGenerator.ts @@ -101,7 +101,7 @@ export class ProofGenerator { .queryFilter(maciContract.filters.DeployPoll(), startBlock) .then((events) => events[0]?.blockNumber ?? 0), pollContract.treeDepths(), - maciContract.getStateAqRoot(), + maciContract.getStateTreeRoot(), maciContract.numSignUps(), ]); const defaultStartBlock = Math.min(defaultStartBlockPoll, defaultStartBlockSignup); diff --git a/contracts/tasks/helpers/TreeMerger.ts b/contracts/tasks/helpers/TreeMerger.ts index 4d04a52345..57943abb43 100644 --- a/contracts/tasks/helpers/TreeMerger.ts +++ b/contracts/tasks/helpers/TreeMerger.ts @@ -1,8 +1,6 @@ /* eslint-disable no-console */ -import { type BigNumberish } from "ethers"; - import type { ITreeMergeParams } from "./types"; -import type { AccQueue, MACI, Poll } from "../../typechain-types"; +import type { AccQueue, Poll } from "../../typechain-types"; import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; /** @@ -10,16 +8,6 @@ import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signer * This class is using for merging signups and messages. */ export class TreeMerger { - /** - * MACI contract - */ - private maciContract: MACI; - - /** - * User signups AccQueue contract - */ - private signupAccQueueContract: AccQueue; - /** * User messages AccQueue contract */ @@ -40,16 +28,8 @@ export class TreeMerger { * * @param {ITreeMergeParams} params - contracts and signer */ - constructor({ - deployer, - signupAccQueueContract, - messageAccQueueContract, - pollContract, - maciContract, - }: ITreeMergeParams) { - this.maciContract = maciContract; + constructor({ deployer, messageAccQueueContract, pollContract }: ITreeMergeParams) { this.pollContract = pollContract; - this.signupAccQueueContract = signupAccQueueContract; this.messageAccQueueContract = messageAccQueueContract; this.deployer = deployer; } @@ -83,58 +63,17 @@ export class TreeMerger { } } - /** - * Merge user signup subtrees - * - * @param pollId - poll id - * @param queueOps - the number of queue operations to perform - */ - async mergeSignupSubtrees(pollId: BigNumberish, queueOps: number): Promise { - let subTreesMerged = false; - - // infinite loop to merge the sub trees - while (!subTreesMerged) { - // eslint-disable-next-line no-await-in-loop - subTreesMerged = await this.signupAccQueueContract.subTreesMerged(); - - if (subTreesMerged) { - console.log("All state subtrees have been merged."); - } else { - // eslint-disable-next-line no-await-in-loop - await this.signupAccQueueContract.getSrIndices().then((indices) => { - console.log(`Merging state subroots ${indices[0] + 1n} / ${indices[1] + 1n}`); - }); - - // first merge the subroots - // eslint-disable-next-line no-await-in-loop - const receipt = await this.pollContract - .mergeMaciStateAqSubRoots(queueOps, pollId.toString()) - .then((tx) => tx.wait()); - - if (receipt?.status !== 1) { - throw new Error("Error merging state subroots"); - } - - console.log(`Transaction hash: ${receipt.hash}`); - console.log(`Executed mergeMaciStateAqSubRoots(); gas used: ${receipt.gasUsed.toString()}`); - } - } - } - /** * Merge user signup MACI state * * @param pollId - poll id */ - async mergeSignups(pollId: BigNumberish): Promise { - // check if the state AQ has been fully merged - const stateTreeDepth = Number(await this.maciContract.stateTreeDepth()); - const mainRoot = await this.signupAccQueueContract.getMainRoot(stateTreeDepth.toString()); - - if (mainRoot.toString() === "0" || BigInt(pollId) > 0n) { + async mergeSignups(): Promise { + // check if the state tree has been fully merged + if (!(await this.pollContract.stateAqMerged())) { // go and merge the state tree console.log("Merging subroots to a main state root..."); - const receipt = await this.pollContract.mergeMaciStateAq(pollId.toString()).then((tx) => tx.wait()); + const receipt = await this.pollContract.mergeMaciStateAq().then((tx) => tx.wait()); if (receipt?.status !== 1) { throw new Error("Error merging signup state subroots"); @@ -143,7 +82,7 @@ export class TreeMerger { console.log(`Transaction hash: ${receipt.hash}`); console.log(`Executed mergeStateAq(); gas used: ${receipt.gasUsed.toString()}`); } else { - console.log("The signup state tree has already been merged."); + console.log("The state tree has already been merged."); } } diff --git a/contracts/tasks/helpers/types.ts b/contracts/tasks/helpers/types.ts index 452201e3ce..bdcdbcadb6 100644 --- a/contracts/tasks/helpers/types.ts +++ b/contracts/tasks/helpers/types.ts @@ -532,21 +532,11 @@ export interface ITreeMergeParams { */ deployer: HardhatEthersSigner; - /** - * AccQueue contract - */ - signupAccQueueContract: AccQueue; - /** * Poll contract */ pollContract: Poll; - /** - * MACI contract - */ - maciContract: MACI; - /** * Message AccQueue contract */ diff --git a/contracts/tasks/runner/merge.ts b/contracts/tasks/runner/merge.ts index 1b680a0c30..5d0188a2d4 100644 --- a/contracts/tasks/runner/merge.ts +++ b/contracts/tasks/runner/merge.ts @@ -25,17 +25,11 @@ task("merge", "Merge signups and messages") const deployer = await deployment.getDeployer(); const maciContract = await deployment.getContract({ name: EContracts.MACI }); - const signupAccQueueContractAddress = await maciContract.stateAq(); const pollContractAddress = await maciContract.polls(poll); const pollContract = await deployment.getContract({ name: EContracts.Poll, address: pollContractAddress }); const [, messageAccQueueContractAddress] = await pollContract.extContracts(); - const signupAccQueueContract = await deployment.getContract({ - name: EContracts.AccQueue, - address: signupAccQueueContractAddress, - }); - const messageAccQueueContract = await deployment.getContract({ name: EContracts.AccQueue, address: messageAccQueueContractAddress, @@ -48,8 +42,6 @@ task("merge", "Merge signups and messages") const treeMerger = new TreeMerger({ deployer, pollContract, - signupAccQueueContract, - maciContract, messageAccQueueContract, }); @@ -60,9 +52,7 @@ task("merge", "Merge signups and messages") await treeMerger.checkPollOwner(); await treeMerger.checkPollDuration(); - await treeMerger.mergeSignupSubtrees(poll, queueOps); - await treeMerger.mergeSignups(poll); - + await treeMerger.mergeSignups(); await treeMerger.mergeMessageSubtrees(queueOps); await treeMerger.mergeMessages(); diff --git a/contracts/tests/MACI.test.ts b/contracts/tests/MACI.test.ts index aad4710af4..a575af2f09 100644 --- a/contracts/tests/MACI.test.ts +++ b/contracts/tests/MACI.test.ts @@ -7,16 +7,8 @@ import { NOTHING_UP_MY_SLEEVE } from "maci-crypto"; import { Keypair, PubKey, Message } from "maci-domainobjs"; import { EMode } from "../ts/constants"; -import { getDefaultSigner, getSigners } from "../ts/utils"; -import { - AccQueueQuinaryBlankSl__factory as AccQueueQuinaryBlankSlFactory, - AccQueueQuinaryMaci, - MACI, - Poll as PollContract, - Poll__factory as PollFactory, - Verifier, - VkRegistry, -} from "../typechain-types"; +import { getDefaultSigner } from "../ts/utils"; +import { MACI, Poll as PollContract, Poll__factory as PollFactory, Verifier, VkRegistry } from "../typechain-types"; import { STATE_TREE_DEPTH, @@ -28,9 +20,10 @@ import { } from "./constants"; import { timeTravel, deployTestContracts } from "./utils"; -describe("MACI", () => { +describe("MACI", function test() { + this.timeout(90000); + let maciContract: MACI; - let stateAqContract: AccQueueQuinaryMaci; let vkRegistryContract: VkRegistry; let verifierContract: Verifier; let pollId: bigint; @@ -49,7 +42,6 @@ describe("MACI", () => { const r = await deployTestContracts(initialVoiceCreditBalance, STATE_TREE_DEPTH, signer, true); maciContract = r.maciContract; - stateAqContract = r.stateAqContract; vkRegistryContract = r.vkRegistryContract; verifierContract = r.mockVerifierContract as Verifier; }); @@ -59,13 +51,6 @@ describe("MACI", () => { expect(std.toString()).to.eq(STATE_TREE_DEPTH.toString()); }); - it("should be the owner of the stateAqContract", async () => { - const stateAqAddr = await maciContract.stateAq(); - const stateAq = AccQueueQuinaryBlankSlFactory.connect(stateAqAddr, signer); - - expect(await stateAq.owner()).to.eq(await maciContract.getAddress()); - }); - it("should be possible to deploy Maci contracts with different state tree depth values", async () => { const checkStateTreeDepth = async (stateTreeDepthTest: number): Promise => { const { maciContract: testMaci } = await deployTestContracts( @@ -179,7 +164,7 @@ describe("MACI", () => { it("should not allow to sign up more than the supported amount of users (5 ** stateTreeDepth)", async () => { const stateTreeDepthTest = 1; - const maxUsers = 5 ** stateTreeDepthTest; + const maxUsers = 2 ** stateTreeDepthTest; const maci = (await deployTestContracts(initialVoiceCreditBalance, stateTreeDepthTest, signer, true)) .maciContract; const keypair = new Keypair(); @@ -200,7 +185,29 @@ describe("MACI", () => { AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), ), - ).to.be.revertedWithCustomError(maci, "TooManySignups"); + ).to.be.revertedWithCustomError(maciContract, "TooManySignups"); + }); + + it("should signup 2 ** 10 users", async () => { + const stateTreeDepthTest = 10; + const maxUsers = 2 ** stateTreeDepthTest; + const { maciContract: maci } = await deployTestContracts( + initialVoiceCreditBalance, + stateTreeDepthTest, + signer, + true, + ); + + const keypair = new Keypair(); + // start from one as we already have one signup (blank state leaf) + for (let i = 1; i < maxUsers; i += 1) { + // eslint-disable-next-line no-await-in-loop + await maci.signUp( + keypair.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + ); + } }); }); @@ -252,24 +259,6 @@ describe("MACI", () => { ]); maciState.polls.get(pollId)?.publishMessage(message, padKey); }); - - it("should prevent deploying a second poll before the first has finished", async () => { - await expect( - maciContract.deployPoll( - duration, - treeDepths, - coordinator.pubKey.asContractParam(), - verifierContract, - vkRegistryContract, - EMode.QV, - { - gasLimit: 10000000, - }, - ), - ) - .to.be.revertedWithCustomError(maciContract, "PreviousPollNotCompleted") - .withArgs(1); - }); }); describe("Merge sign-ups", () => { @@ -280,46 +269,10 @@ describe("MACI", () => { pollContract = PollFactory.connect(pollContractAddress, signer); }); - it("should not allow the coordinator to merge the signUp AccQueue", async () => { - await expect(maciContract.mergeStateAqSubRoots(0, 0, { gasLimit: 3000000 })).to.be.revertedWithCustomError( - maciContract, - "CallerMustBePoll", - ); - - await expect(maciContract.mergeStateAq(0, { gasLimit: 3000000 })).to.be.revertedWithCustomError( - maciContract, - "CallerMustBePoll", - ); - }); - - it("should not allow a user to merge the signUp AccQueue", async () => { - const nonAdminUser = (await getSigners())[1]; - await expect( - maciContract.connect(nonAdminUser).mergeStateAqSubRoots(0, 0, { gasLimit: 3000000 }), - ).to.be.revertedWithCustomError(maciContract, "CallerMustBePoll"); - }); - - it("should prevent a new user from signin up after the accQueue subtrees have been merged", async () => { + it("should allow a Poll contract to merge the signUp AccQueue", async () => { await timeTravel(signer.provider as unknown as EthereumProvider, Number(duration) + 1); - const tx = await pollContract.mergeMaciStateAqSubRoots(0, pollId, { - gasLimit: 3000000, - }); - const receipt = await tx.wait(); - expect(receipt?.status).to.eq(1); - - await expect( - maciContract.signUp( - users[0].pubKey.asContractParam(), - AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), - AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), - signUpTxOpts, - ), - ).to.be.revertedWithCustomError(maciContract, "SignupTemporaryBlocked"); - }); - - it("should allow a Poll contract to merge the signUp AccQueue", async () => { - const tx = await pollContract.mergeMaciStateAq(pollId, { + const tx = await pollContract.mergeMaciStateAq({ gasLimit: 3000000, }); const receipt = await tx.wait(); @@ -327,13 +280,12 @@ describe("MACI", () => { }); it("should have the correct state root on chain after merging the acc queue", async () => { - const onChainStateRoot = await stateAqContract.getMainRoot(STATE_TREE_DEPTH); maciState.polls.get(pollId)?.updatePoll(await pollContract.numSignups()); - expect(onChainStateRoot.toString()).to.eq(maciState.polls.get(pollId)?.stateTree?.root.toString()); + expect(await maciContract.getStateTreeRoot()).to.eq(maciState.polls.get(pollId)?.stateTree?.root.toString()); }); - it("should get the correct state root with getStateAqRoot", async () => { - const onChainStateRoot = await maciContract.getStateAqRoot(); + 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()); }); diff --git a/contracts/tests/MessageProcessor.test.ts b/contracts/tests/MessageProcessor.test.ts index 87ad19f590..502ec550ae 100644 --- a/contracts/tests/MessageProcessor.test.ts +++ b/contracts/tests/MessageProcessor.test.ts @@ -153,8 +153,7 @@ describe("MessageProcessor", () => { describe("after merging acc queues", () => { before(async () => { - await pollContract.mergeMaciStateAqSubRoots(0, pollId); - await pollContract.mergeMaciStateAq(0); + await pollContract.mergeMaciStateAq(); await pollContract.mergeMessageAqSubRoots(0); await pollContract.mergeMessageAq(); diff --git a/contracts/tests/Poll.test.ts b/contracts/tests/Poll.test.ts index ce2cbb04f4..df96a69103 100644 --- a/contracts/tests/Poll.test.ts +++ b/contracts/tests/Poll.test.ts @@ -323,13 +323,6 @@ describe("Poll", () => { messageAqContract = AccQueueQuinaryMaciFactory.connect(messageAqAddress, signer); }); - it("should revert if the subtrees are not merged for StateAq", async () => { - await expect(pollContract.mergeMaciStateAq(0, { gasLimit: 4000000 })).to.be.revertedWithCustomError( - pollContract, - "StateAqSubtreesNeedMerge", - ); - }); - it("should allow the coordinator to merge the message AccQueue", async () => { let tx = await pollContract.mergeMessageAqSubRoots(0, { gasLimit: 3000000, diff --git a/contracts/tests/Tally.test.ts b/contracts/tests/Tally.test.ts index b47b3fc5e9..bb821c0322 100644 --- a/contracts/tests/Tally.test.ts +++ b/contracts/tests/Tally.test.ts @@ -183,8 +183,7 @@ describe("TallyVotes", () => { describe("after merging acc queues", () => { let tallyGeneratedInputs: ITallyCircuitInputs; before(async () => { - await pollContract.mergeMaciStateAqSubRoots(0, pollId); - await pollContract.mergeMaciStateAq(0); + await pollContract.mergeMaciStateAq(); await pollContract.mergeMessageAqSubRoots(0); await pollContract.mergeMessageAq(); @@ -338,8 +337,7 @@ describe("TallyVotes", () => { ); await timeTravel(signer.provider! as unknown as EthereumProvider, updatedDuration); - await pollContract.mergeMaciStateAqSubRoots(0, pollId); - await pollContract.mergeMaciStateAq(0); + await pollContract.mergeMaciStateAq(); await pollContract.mergeMessageAqSubRoots(0); await pollContract.mergeMessageAq(); @@ -349,13 +347,17 @@ describe("TallyVotes", () => { }); it("should tally votes correctly", async () => { - const tallyGeneratedInputs = poll.tallyVotes(); - await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + let tallyGeneratedInputs: ITallyCircuitInputs; + while (poll.hasUntalliedBallots()) { + tallyGeneratedInputs = poll.tallyVotes(); + // eslint-disable-next-line no-await-in-loop + await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + } const onChainNewTallyCommitment = await tallyContract.tallyCommitment(); - expect(tallyGeneratedInputs.newTallyCommitment).to.eq(onChainNewTallyCommitment.toString()); + expect(tallyGeneratedInputs!.newTallyCommitment).to.eq(onChainNewTallyCommitment.toString()); await expect( - tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), + tallyContract.tallyVotes(tallyGeneratedInputs!.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), ).to.be.revertedWithCustomError(tallyContract, "AllBallotsTallied"); }); }); @@ -480,8 +482,7 @@ describe("TallyVotes", () => { ); await timeTravel(signer.provider! as unknown as EthereumProvider, updatedDuration); - await pollContract.mergeMaciStateAqSubRoots(0, pollId); - await pollContract.mergeMaciStateAq(0); + await pollContract.mergeMaciStateAq(); await pollContract.mergeMessageAqSubRoots(0); await pollContract.mergeMessageAq(); @@ -505,6 +506,13 @@ describe("TallyVotes", () => { await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + // tally everything else + while (poll.hasUntalliedBallots()) { + tallyGeneratedInputs = poll.tallyVotes(); + // eslint-disable-next-line no-await-in-loop + await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + } + // check that it fails to tally again await expect( tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), diff --git a/contracts/tests/TallyNonQv.test.ts b/contracts/tests/TallyNonQv.test.ts index a07e911279..7c45850c30 100644 --- a/contracts/tests/TallyNonQv.test.ts +++ b/contracts/tests/TallyNonQv.test.ts @@ -64,7 +64,7 @@ describe("TallyVotesNonQv", () => { signer = await getDefaultSigner(); - const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, false); + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true); maciContract = r.maciContract; verifierContract = r.mockVerifierContract as Verifier; vkRegistryContract = r.vkRegistryContract; @@ -182,8 +182,7 @@ describe("TallyVotesNonQv", () => { describe("after merging acc queues", () => { let tallyGeneratedInputs: ITallyCircuitInputs; before(async () => { - await pollContract.mergeMaciStateAqSubRoots(0, pollId); - await pollContract.mergeMaciStateAq(0); + await pollContract.mergeMaciStateAq(); await pollContract.mergeMessageAqSubRoots(0); await pollContract.mergeMessageAq(); diff --git a/contracts/tests/constants.ts b/contracts/tests/constants.ts index a7c6957e9b..76960a201e 100644 --- a/contracts/tests/constants.ts +++ b/contracts/tests/constants.ts @@ -1,14 +1,13 @@ -import { MaxValues, TreeDepths } from "maci-core"; +import { MaxValues, TreeDepths, STATE_TREE_ARITY, MESSAGE_TREE_ARITY } from "maci-core"; import { G1Point, G2Point } from "maci-crypto"; import { VerifyingKey } from "maci-domainobjs"; export const duration = 2_000; export const STATE_TREE_DEPTH = 10; -export const STATE_TREE_ARITY = 5; export const MESSAGE_TREE_DEPTH = 2; export const MESSAGE_TREE_SUBDEPTH = 1; -export const messageBatchSize = STATE_TREE_ARITY ** MESSAGE_TREE_SUBDEPTH; +export const messageBatchSize = MESSAGE_TREE_ARITY ** MESSAGE_TREE_SUBDEPTH; export const testProcessVk = new VerifyingKey( new G1Point(BigInt(0), BigInt(1)), @@ -44,7 +43,7 @@ export const testTallyVkNonQv = new VerifyingKey( export const initialVoiceCreditBalance = 100; export const maxValues: MaxValues = { - maxMessages: STATE_TREE_ARITY ** MESSAGE_TREE_DEPTH, + maxMessages: MESSAGE_TREE_ARITY ** MESSAGE_TREE_DEPTH, maxVoteOptions: 25, }; diff --git a/contracts/tests/utils.ts b/contracts/tests/utils.ts index 12b0baf132..dbe9f7f4b4 100644 --- a/contracts/tests/utils.ts +++ b/contracts/tests/utils.ts @@ -529,7 +529,7 @@ export const deployTestContracts = async ( topupCreditContract.getAddress(), ]); - const { maciContract, stateAqContract } = await deployMaci({ + const { maciContract } = await deployMaci({ signUpTokenGatekeeperContractAddress: gatekeeperContractAddress, initialVoiceCreditBalanceAddress: constantIntialVoiceCreditProxyContractAddress, topupCreditContractAddress, @@ -543,7 +543,6 @@ export const deployTestContracts = async ( gatekeeperContract, constantIntialVoiceCreditProxyContract, maciContract, - stateAqContract, vkRegistryContract, topupCreditContract, }; diff --git a/contracts/ts/deploy.ts b/contracts/ts/deploy.ts index 2478b3edcc..9c6c49d20d 100644 --- a/contracts/ts/deploy.ts +++ b/contracts/ts/deploy.ts @@ -26,14 +26,13 @@ import { TopupCredit, Verifier, VkRegistry, - AccQueueQuinaryMaci__factory as AccQueueQuinaryMaciFactory, PollFactory__factory as PollFactoryFactory, MACI__factory as MACIFactory, MessageProcessorFactory__factory as MessageProcessorFactoryFactory, TallyFactory__factory as TallyFactoryFactory, } from "../typechain-types"; -import { getDefaultSigner, log } from "./utils"; +import { log } from "./utils"; /** * Creates contract factory from abi and bytecode @@ -303,12 +302,8 @@ export const deployMaci = async ({ stateTreeDepth, ); - const [stateAqContractAddress, deployer] = await Promise.all([maciContract.stateAq(), getDefaultSigner()]); - const stateAqContract = AccQueueQuinaryMaciFactory.connect(stateAqContractAddress, signer || deployer); - return { maciContract, - stateAqContract, pollFactoryContract, poseidonAddrs, }; diff --git a/contracts/ts/genEmptyBallotRootsContract.ts b/contracts/ts/genEmptyBallotRootsContract.ts index 9c801f54ef..acdaae169d 100644 --- a/contracts/ts/genEmptyBallotRootsContract.ts +++ b/contracts/ts/genEmptyBallotRootsContract.ts @@ -1,4 +1,5 @@ -import { IncrementalQuinTree, hash5 } from "maci-crypto"; +import { STATE_TREE_ARITY } from "maci-core"; +import { IncrementalQuinTree, hash2 } from "maci-crypto"; import { Ballot } from "maci-domainobjs"; import fs from "fs"; @@ -17,9 +18,9 @@ export const genEmptyBallotRootsContract = async (): Promise => { for (let i = 0; i < 5; i += 1) { const ballot = new Ballot(0, i + 1); // The empty Ballot tree root - const ballotTree = new IncrementalQuinTree(stateTreeDepth, ballot.hash(), 5, hash5); + const ballotTree = new IncrementalQuinTree(stateTreeDepth, ballot.hash(), STATE_TREE_ARITY, hash2); - roots.push(`emptyBallotRoots[${i}] = uint256(${ballotTree.root});`.padStart(4)); + roots.push(`${"".padStart(4)}emptyBallotRoots[${i}] = uint256(${ballotTree.root});`.padStart(4)); } return template.replace("<% ROOTS %>", roots.join("\n")).trim(); diff --git a/contracts/ts/genMaciState.ts b/contracts/ts/genMaciState.ts index 685c84c32e..b95f666e85 100644 --- a/contracts/ts/genMaciState.ts +++ b/contracts/ts/genMaciState.ts @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle */ import { type Provider } from "ethers"; -import { MaciState, STATE_TREE_ARITY } from "maci-core"; +import { MaciState, MESSAGE_TREE_ARITY, STATE_TREE_ARITY } from "maci-core"; import { type Keypair, PubKey, Message } from "maci-domainobjs"; import assert from "assert"; @@ -137,7 +137,7 @@ export const genMaciStateFromContract = async ( const batchSizes = { tallyBatchSize: STATE_TREE_ARITY ** Number(onChainTreeDepths.intStateTreeDepth), - messageBatchSize: STATE_TREE_ARITY ** Number(onChainTreeDepths.messageTreeSubDepth), + messageBatchSize: MESSAGE_TREE_ARITY ** Number(onChainTreeDepths.messageTreeSubDepth), }; // fetch poll contract logs diff --git a/contracts/ts/types.ts b/contracts/ts/types.ts index 22f566b586..8c4d9ac27f 100644 --- a/contracts/ts/types.ts +++ b/contracts/ts/types.ts @@ -1,5 +1,4 @@ import type { - AccQueueQuinaryMaci, ConstantInitialVoiceCreditProxy, FreeForAllGatekeeper, MACI, @@ -80,7 +79,6 @@ export interface IDeployedTestContracts { gatekeeperContract: FreeForAllGatekeeper; constantIntialVoiceCreditProxyContract: ConstantInitialVoiceCreditProxy; maciContract: MACI; - stateAqContract: AccQueueQuinaryMaci; vkRegistryContract: VkRegistry; topupCreditContract: TopupCredit; } @@ -169,7 +167,6 @@ export interface IDeployMaciArgs { */ export interface IDeployedMaci { maciContract: MACI; - stateAqContract: AccQueueQuinaryMaci; pollFactoryContract: PollFactory; poseidonAddrs: { poseidonT3: string; diff --git a/core/ts/Poll.ts b/core/ts/Poll.ts index 56004967d6..dd732884ff 100644 --- a/core/ts/Poll.ts +++ b/core/ts/Poll.ts @@ -9,6 +9,7 @@ import { sha256Hash, stringifyBigInts, genTreeCommitment, + hash2, } from "maci-crypto"; import { PCommand, @@ -65,6 +66,9 @@ export class Poll implements IPoll { // the depth of the state tree stateTreeDepth: number; + // the actual depth of the state tree (can be <= stateTreeDepth) + actualStateTreeDepth: number; + pollEndTimestamp: bigint; ballots: Ballot[] = []; @@ -145,6 +149,7 @@ export class Poll implements IPoll { this.maciStateRef = maciStateRef; this.pollId = BigInt(maciStateRef.polls.size); this.stateTreeDepth = maciStateRef.stateTreeDepth; + this.actualStateTreeDepth = maciStateRef.stateTreeDepth; this.messageTree = new IncrementalQuinTree( this.treeDepths.messageTreeDepth, @@ -179,9 +184,11 @@ export class Poll implements IPoll { this.setNumSignups(numSignups); // copy up to numSignups state leaves this.stateLeaves = this.maciStateRef.stateLeaves.slice(0, Number(this.numSignups)).map((x) => x.copy()); + // 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.stateTreeDepth, blankStateLeafHash, STATE_TREE_ARITY, hash5); + this.stateTree = new IncrementalQuinTree(this.actualStateTreeDepth, blankStateLeafHash, STATE_TREE_ARITY, hash2); // add all leaves this.stateLeaves.forEach((stateLeaf) => { this.stateTree?.insert(stateLeaf.hash()); @@ -189,8 +196,8 @@ export class Poll implements IPoll { // Create as many ballots as state leaves this.emptyBallotHash = this.emptyBallot.hash(); - this.ballotTree = new IncrementalQuinTree(this.stateTreeDepth, this.emptyBallotHash, STATE_TREE_ARITY, hash5); - this.ballotTree.insert(this.emptyBallot.hash()); + this.ballotTree = new IncrementalQuinTree(this.stateTreeDepth, this.emptyBallotHash, STATE_TREE_ARITY, hash2); + this.ballotTree.insert(this.emptyBallotHash); // we fill the ballotTree with empty ballots hashes to match the number of signups in the tree while (this.ballots.length < this.stateLeaves.length) { @@ -291,7 +298,7 @@ export class Poll implements IPoll { const originalBallotPathElements = this.ballotTree?.genProof(Number(stateLeafIndex)).pathElements; // create a new quinary tree where we insert the votes of the origin (up until this message is processed) ballot - const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, STATE_TREE_ARITY, hash5); + const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, MESSAGE_TREE_ARITY, hash5); for (let i = 0; i < this.ballots[0].votes.length; i += 1) { vt.insert(ballot.votes[i]); } @@ -577,7 +584,7 @@ export class Poll implements IPoll { const vt = new IncrementalQuinTree( this.treeDepths.voteOptionTreeDepth, 0n, - STATE_TREE_ARITY, + MESSAGE_TREE_ARITY, hash5, ); @@ -618,7 +625,12 @@ export class Poll implements IPoll { currentVoteWeights.unshift(this.ballots[0].votes[0]); // create a new quinary tree and add an empty vote - const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, STATE_TREE_ARITY, hash5); + const vt = new IncrementalQuinTree( + this.treeDepths.voteOptionTreeDepth, + 0n, + MESSAGE_TREE_ARITY, + hash5, + ); vt.insert(this.ballots[0].votes[0]); // get the path elements for this empty vote weight leaf currentVoteWeightsPathElements.unshift(vt.genProof(0).pathElements); @@ -662,7 +674,7 @@ export class Poll implements IPoll { currentVoteWeights.unshift(currentBallot.votes[0]); // create a quinary tree to fill with the votes of the current ballot - const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, STATE_TREE_ARITY, hash5); + const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, MESSAGE_TREE_ARITY, hash5); for (let j = 0; j < this.ballots[0].votes.length; j += 1) { vt.insert(currentBallot.votes[j]); @@ -693,7 +705,7 @@ export class Poll implements IPoll { currentVoteWeights.unshift(this.ballots[0].votes[0]); // create a new quinary tree and add an empty vote - const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, STATE_TREE_ARITY, hash5); + const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, MESSAGE_TREE_ARITY, hash5); vt.insert(this.ballots[0].votes[0]); // get the path elements for this empty vote weight leaf @@ -703,6 +715,14 @@ export class Poll implements IPoll { // store the data in the circuit inputs object circuitInputs.currentStateLeaves = currentStateLeaves.map((x) => x.asCircuitInputs()); + // we need to fill the array with 0s to match the length of the state leaves + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < currentStateLeavesPathElements.length; i += 1) { + while (currentStateLeavesPathElements[i].length < this.stateTreeDepth) { + currentStateLeavesPathElements[i].push([0n]); + } + } + circuitInputs.currentStateLeavesPathElements = currentStateLeavesPathElements; circuitInputs.currentBallots = currentBallots.map((x) => x.asCircuitInputs()); circuitInputs.currentBallotsPathElements = currentBallotsPathElements; @@ -743,6 +763,7 @@ export class Poll implements IPoll { circuitInputs.currentSbCommitment as bigint, circuitInputs.newSbCommitment, this.pollEndTimestamp, + BigInt(this.actualStateTreeDepth), ]); // If this is the last batch, release the lock @@ -750,6 +771,9 @@ export class Poll implements IPoll { this.maciStateRef.pollBeingProcessed = false; } + // ensure we pass the dynamic tree depth + circuitInputs.actualStateTreeDepth = this.actualStateTreeDepth.toString(); + return stringifyBigInts(circuitInputs) as unknown as IProcessMessagesCircuitInputs; }; diff --git a/core/ts/__tests__/Poll.test.ts b/core/ts/__tests__/Poll.test.ts index dd0aad5a95..4989ebdcdd 100644 --- a/core/ts/__tests__/Poll.test.ts +++ b/core/ts/__tests__/Poll.test.ts @@ -483,8 +483,13 @@ describe("Poll", function test() { }); it("should generate the correct results", () => { - poll.processAllMessages(); - poll.tallyVotes(); + while (poll.hasUnprocessedMessages()) { + poll.processAllMessages(); + } + + while (poll.hasUntalliedBallots()) { + poll.tallyVotes(); + } const spentVoiceCredits = poll.totalSpentVoiceCredits; const results = poll.tallyResult; diff --git a/core/ts/__tests__/e2e.test.ts b/core/ts/__tests__/e2e.test.ts index a1de8a4bd6..2de79fa5a3 100644 --- a/core/ts/__tests__/e2e.test.ts +++ b/core/ts/__tests__/e2e.test.ts @@ -1,10 +1,10 @@ import { expect } from "chai"; -import { hash5, NOTHING_UP_MY_SLEEVE, IncrementalQuinTree, AccQueue } from "maci-crypto"; +import { hash5, NOTHING_UP_MY_SLEEVE, IncrementalQuinTree, AccQueue, hash2 } from "maci-crypto"; import { PCommand, Keypair, StateLeaf, blankStateLeafHash } from "maci-domainobjs"; import { MaciState } from "../MaciState"; import { Poll } from "../Poll"; -import { STATE_TREE_DEPTH, STATE_TREE_ARITY } from "../utils/constants"; +import { STATE_TREE_DEPTH, STATE_TREE_ARITY, MESSAGE_TREE_ARITY } from "../utils/constants"; import { packProcessMessageSmallVals, unpackProcessMessageSmallVals } from "../utils/utils"; import { @@ -357,7 +357,6 @@ describe("MaciState/Poll e2e", function test() { let pollId: bigint; let poll: Poll; let msgTree: IncrementalQuinTree; - let stateTree: IncrementalQuinTree; const voteWeight = 9n; const voteOptionIndex = 0n; let stateIndex: number; @@ -365,8 +364,7 @@ describe("MaciState/Poll e2e", function test() { before(() => { maciState = new MaciState(STATE_TREE_DEPTH); - msgTree = new IncrementalQuinTree(treeDepths.messageTreeDepth, NOTHING_UP_MY_SLEEVE, 5, hash5); - stateTree = new IncrementalQuinTree(STATE_TREE_DEPTH, blankStateLeafHash, STATE_TREE_ARITY, hash5); + msgTree = new IncrementalQuinTree(treeDepths.messageTreeDepth, NOTHING_UP_MY_SLEEVE, MESSAGE_TREE_ARITY, hash5); pollId = maciState.deployPoll( BigInt(Math.floor(Date.now() / 1000) + duration), @@ -386,11 +384,13 @@ describe("MaciState/Poll e2e", function test() { const stateLeaf = new StateLeaf(userKeypair.pubKey, voiceCreditBalance, timestamp); stateIndex = maciState.signUp(userKeypair.pubKey, voiceCreditBalance, timestamp); + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + const stateTree = new IncrementalQuinTree(poll.actualStateTreeDepth, blankStateLeafHash, STATE_TREE_ARITY, hash2); + stateTree.insert(blankStateLeafHash); stateTree.insert(stateLeaf.hash()); - poll.updatePoll(BigInt(maciState.stateLeaves.length)); - expect(stateIndex.toString()).to.eq("1"); expect(stateTree.root.toString()).to.eq(poll.stateTree?.root.toString()); }); @@ -417,7 +417,7 @@ describe("MaciState/Poll e2e", function test() { // Use the accumulator queue to compare the root of the message tree const accumulatorQueue: AccQueue = new AccQueue( treeDepths.messageTreeSubDepth, - STATE_TREE_ARITY, + MESSAGE_TREE_ARITY, NOTHING_UP_MY_SLEEVE, ); accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey)); @@ -588,7 +588,9 @@ describe("MaciState/Poll e2e", function test() { expect(poll.hasUntalliedBallots()).to.eq(true); // First batch tally - poll.tallyVotes(); + while (poll.hasUntalliedBallots()) { + poll.tallyVotes(); + } // Recall that each user `i` cast the same number of votes for // their option `i` diff --git a/core/ts/index.ts b/core/ts/index.ts index 0df07c8154..54fbe47163 100644 --- a/core/ts/index.ts +++ b/core/ts/index.ts @@ -21,4 +21,4 @@ export type { IJsonMaciState, } from "./utils/types"; -export { STATE_TREE_ARITY } from "./utils/constants"; +export { STATE_TREE_ARITY, MESSAGE_TREE_ARITY } from "./utils/constants"; diff --git a/core/ts/utils/constants.ts b/core/ts/utils/constants.ts index 6e06744ff4..a77a2b5375 100644 --- a/core/ts/utils/constants.ts +++ b/core/ts/utils/constants.ts @@ -1,5 +1,5 @@ export const STATE_TREE_DEPTH = 10; -export const STATE_TREE_ARITY = 5; +export const STATE_TREE_ARITY = 2; export const STATE_TREE_SUBDEPTH = 2; export const MESSAGE_TREE_ARITY = 5; export const VOTE_OPTION_TREE_ARITY = 5; diff --git a/core/ts/utils/types.ts b/core/ts/utils/types.ts index df483ead42..4ca716b1d8 100644 --- a/core/ts/utils/types.ts +++ b/core/ts/utils/types.ts @@ -147,6 +147,7 @@ export interface IProcessMessagesOutput { * An interface describing the circuit inputs to the ProcessMessage circuit */ export interface IProcessMessagesCircuitInputs { + actualStateTreeDepth: string; pollEndTimestamp: string; packedVals: string; msgRoot: string; diff --git a/crypto/ts/quinTree.ts b/crypto/ts/quinTree.ts index ee474c49cc..4c46ebd06e 100644 --- a/crypto/ts/quinTree.ts +++ b/crypto/ts/quinTree.ts @@ -125,7 +125,7 @@ export class IncrementalQuinTree { } if (index >= this.capacity) { - throw new Error("+The leaf index must be less than the tree capacity"); + throw new Error("The leaf index must be less than the tree capacity"); } const pathElements: bigint[][] = []; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7ab6df239..0ead7c22cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,6 +232,9 @@ importers: '@openzeppelin/contracts': specifier: ^5.0.2 version: 5.0.2 + '@zk-kit/imt.sol': + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 circomlibjs: specifier: ^0.1.7 version: 0.1.7 @@ -6359,6 +6362,12 @@ packages: buffer: 6.0.3 dev: false + /@zk-kit/imt.sol@2.0.0-beta.12: + resolution: {integrity: sha512-kKgopVO6zlfSiQgv3X9WykaCeyb8jGtthWGqdo1ZD7fY1bH8A7BWhhWxtoCuU5mPEgRbamw1cAoUynuLoEULsg==} + dependencies: + poseidon-solidity: 0.0.5 + dev: false + /@zk-kit/poseidon-cipher@0.3.0: resolution: {integrity: sha512-Byszt7dxssgsR7hog2nf9AMaBKYr8LrgtlU/PPHPEe2OkJwIeQSshoxqquLlZsyfOn2c1ZmTJb4Mo4aHY11pCA==} dependencies: @@ -15744,6 +15753,10 @@ packages: engines: {node: '>=4'} dev: true + /poseidon-solidity@0.0.5: + resolution: {integrity: sha512-NzrvSwHzvZgT4hvg2GyGqeR+UOU/eLSEt4wAoXEua+VaR7NTKKwx1X9bPlh1VMBEVEno+IWvkRBbidFGzTeAqQ==} + dev: false + /possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} diff --git a/website/versioned_docs/version-v1.2/solidity-docs/interfaces/IMACI.md b/website/versioned_docs/version-v1.2/solidity-docs/interfaces/IMACI.md deleted file mode 100644 index 948e8ce88d..0000000000 --- a/website/versioned_docs/version-v1.2/solidity-docs/interfaces/IMACI.md +++ /dev/null @@ -1,94 +0,0 @@ -# IMACI - -MACI interface - -### stateTreeDepth - -```solidity -function stateTreeDepth() external view returns (uint8) -``` - -Get the depth of the state tree - -#### Return Values - -| Name | Type | Description | -| ---- | ----- | --------------------------- | -| [0] | uint8 | The depth of the state tree | - -### getStateAqRoot - -```solidity -function getStateAqRoot() external view returns (uint256) -``` - -Return the main root of the StateAq contract - -#### Return Values - -| Name | Type | Description | -| ---- | ------- | --------------- | -| [0] | uint256 | The Merkle root | - -### mergeStateAqSubRoots - -```solidity -function mergeStateAqSubRoots(uint256 _numSrQueueOps, uint256 _pollId) external -``` - -Allow Poll contracts to merge the state subroots - -#### Parameters - -| Name | Type | Description | -| --------------- | ------- | ------------------------- | -| \_numSrQueueOps | uint256 | Number of operations | -| \_pollId | uint256 | The ID of the active Poll | - -### mergeStateAq - -```solidity -function mergeStateAq(uint256 _pollId) external returns (uint256) -``` - -Allow Poll contracts to merge the state root - -#### Parameters - -| Name | Type | Description | -| -------- | ------- | ------------------ | -| \_pollId | uint256 | The active Poll ID | - -#### Return Values - -| Name | Type | Description | -| ---- | ------- | -------------------------- | -| [0] | uint256 | The calculated Merkle root | - -### numSignUps - -```solidity -function numSignUps() external view returns (uint256) -``` - -Get the number of signups - -#### Return Values - -| Name | Type | Description | -| ---- | ------- | -------------------------------- | -| [0] | uint256 | numsignUps The number of signups | - -### stateAq - -```solidity -function stateAq() external view returns (contract AccQueue) -``` - -Get the state AccQueue - -#### Return Values - -| Name | Type | Description | -| ---- | ----------------- | ------------------ | -| [0] | contract AccQueue | The state AccQueue | diff --git a/website/versioned_docs/version-v1.3_alpha/spec.md b/website/versioned_docs/version-v1.3_alpha/spec.md index 5844b8269a..5dc8d1eae8 100644 --- a/website/versioned_docs/version-v1.3_alpha/spec.md +++ b/website/versioned_docs/version-v1.3_alpha/spec.md @@ -575,7 +575,7 @@ The integration tests and shell scripts in the `cli` directory provide examples | `signUp(PubKey memory _pubKey, bytes memory _signUpGatekeeperData, bytes memory _initialVoiceCreditProxyData)` | Executable only during the sign-up period and after initialisation | Participant registration and voice credit assignment | | `mergeStateAqSubRoots(uint256 _numSrQueueOps, uint256 _pollId)` | Executable only by poll contract `_pollId` and after initialisation | Merge queued state leaves to form the state tree subroots | | `mergeStateAq(uint256 _pollId)` | Executable only by poll contract `_pollId` and after initialisation | Merge the state subroots to form the state root | -| `getStateAqRoot()` | Non-applicable | Query the state root | +| `getStateTreeRoot()` | Non-applicable | Query the state root | | `deployPoll(uint256 _duration, MaxValues memory _maxValues, TreeDepths memory _treeDepths, PubKey memory _coordinatorPubKey)` | Executable only after initialisation | Create a new poll | | `getPoll(uint256 _pollId)` | Non-applicable | Query a poll address |