diff --git a/circuits/circom/circuits.json b/circuits/circom/circuits.json index d3dd5ce2c8..e397c16669 100644 --- a/circuits/circom/circuits.json +++ b/circuits/circom/circuits.json @@ -5,12 +5,24 @@ "params": [10, 2, 1, 2], "pubs": ["inputHash"] }, + "ProcessMessagesNonQv_10-2-1-2_test": { + "file": "processMessagesNonQv", + "template": "ProcessMessagesNonQv", + "params": [10, 2, 1, 2], + "pubs": ["inputHash"] + }, "TallyVotes_10-1-2_test": { "file": "tallyVotes", "template": "TallyVotes", "params": [10, 1, 2], "pubs": ["inputHash"] }, + "TallyVotesNonQv_10-1-2_test": { + "file": "tallyVotesNonQv", + "template": "TallyVotesNonQv", + "params": [10, 1, 2], + "pubs": ["inputHash"] + }, "SubsidyPerBatch_10-1-2_test": { "file": "subsidy", "template": "SubsidyPerBatch", diff --git a/circuits/circom/hasherSha256.circom b/circuits/circom/hasherSha256.circom index 644559ea2d..d5804098d6 100644 --- a/circuits/circom/hasherSha256.circom +++ b/circuits/circom/hasherSha256.circom @@ -23,7 +23,7 @@ template Sha256Hasher3() { signal output hash; component hasher = Sha256Hasher(length); - for (var i = 0; i < length; i ++) { + for (var i = 0; i < length; i++) { hasher.in[i] <== in[i]; } hash <== hasher.hash; @@ -37,7 +37,7 @@ template Sha256Hasher4() { signal output hash; component hasher = Sha256Hasher(length); - for (var i = 0; i < length; i ++) { + for (var i = 0; i < length; i++) { hasher.in[i] <== in[i]; } hash <== hasher.hash; @@ -51,7 +51,7 @@ template Sha256Hasher5() { signal output hash; component hasher = Sha256Hasher(length); - for (var i = 0; i < length; i ++) { + for (var i = 0; i < length; i++) { hasher.in[i] <== in[i]; } hash <== hasher.hash; @@ -65,7 +65,7 @@ template Sha256Hasher6() { signal output hash; component hasher = Sha256Hasher(length); - for (var i = 0; i < length; i ++) { + for (var i = 0; i < length; i++) { hasher.in[i] <== in[i]; } hash <== hasher.hash; @@ -79,7 +79,7 @@ template Sha256Hasher10() { signal output hash; component hasher = Sha256Hasher(length); - for (var i = 0; i < length; i ++) { + for (var i = 0; i < length; i++) { hasher.in[i] <== in[i]; } hash <== hasher.hash; @@ -98,8 +98,8 @@ template Sha256Hasher(length) { } component sha = Sha256(inBits); - for (var i = 0; i < length; i ++) { - for (var j = 0; j < 256; j ++) { + for (var i = 0; i < length; i++) { + for (var j = 0; j < 256; j++) { sha.in[(i * 256) + 255 - j] <== n2b[i].out[j]; } } diff --git a/circuits/circom/messageHasher.circom b/circuits/circom/messageHasher.circom index b0459b128e..4eda988c03 100644 --- a/circuits/circom/messageHasher.circom +++ b/circuits/circom/messageHasher.circom @@ -16,7 +16,7 @@ template MessageHasher() { component hasher = Hasher13(); // we add all parts of the msg - for (var i = 0; i < 11; i ++) { + for (var i = 0; i < 11; i++) { hasher.in[i] <== in[i]; } hasher.in[11] <== encPubKey[0]; diff --git a/circuits/circom/messageToCommand.circom b/circuits/circom/messageToCommand.circom index 25ff4c916d..80cefd9269 100644 --- a/circuits/circom/messageToCommand.circom +++ b/circuits/circom/messageToCommand.circom @@ -55,7 +55,7 @@ template MessageToCommand() { // save the decrypted message into a packed command signal signal packedCommand[PACKED_CMD_LENGTH]; - for (var i = 0; i < PACKED_CMD_LENGTH; i ++) { + for (var i = 0; i < PACKED_CMD_LENGTH; i++) { packedCommand[i] <== decryptor.decrypted[i]; } @@ -80,7 +80,7 @@ template MessageToCommand() { // this could be removed and instead // use packedCommand as output - for (var i = 0; i < PACKED_CMD_LENGTH; i ++) { + for (var i = 0; i < PACKED_CMD_LENGTH; i++) { packedCommandOut[i] <== packedCommand[i]; } } diff --git a/circuits/circom/messageValidator.circom b/circuits/circom/messageValidator.circom index 7de6251832..fae47b6a87 100644 --- a/circuits/circom/messageValidator.circom +++ b/circuits/circom/messageValidator.circom @@ -48,7 +48,7 @@ template MessageValidator() { validSignature.R8[0] <== sigR8[0]; validSignature.R8[1] <== sigR8[1]; validSignature.S <== sigS; - for (var i = 0; i < PACKED_CMD_LENGTH; i ++) { + for (var i = 0; i < PACKED_CMD_LENGTH; i++) { validSignature.preimage[i] <== cmd[i]; } diff --git a/circuits/circom/messageValidatorNonQv.circom b/circuits/circom/messageValidatorNonQv.circom new file mode 100644 index 0000000000..eedd317a74 --- /dev/null +++ b/circuits/circom/messageValidatorNonQv.circom @@ -0,0 +1,84 @@ +pragma circom 2.0.0; + +// local imports +include "./verifySignature.circom"; +include "./utils.circom"; + +// template that validates whether a message +// is valid or not +// @note it does not do quadratic voting +template MessageValidatorNonQv() { + // a) Whether the state leaf index is valid + signal input stateTreeIndex; + // how many signups we have in the state tree + signal input numSignUps; + // we check that the state tree index is <= than the number of signups + // as first validation + // it is <= because the state tree index is 1-based + // 0 is for blank state leaf then 1 for the first actual user + // which is where the numSignUps starts + component validStateLeafIndex = SafeLessEqThan(252); + validStateLeafIndex.in[0] <== stateTreeIndex; + validStateLeafIndex.in[1] <== numSignUps; + + // b) Whether the max vote option tree index is correct + signal input voteOptionIndex; + signal input maxVoteOptions; + component validVoteOptionIndex = SafeLessThan(252); + validVoteOptionIndex.in[0] <== voteOptionIndex; + validVoteOptionIndex.in[1] <== maxVoteOptions; + + // c) Whether the nonce is correct + signal input originalNonce; + signal input nonce; + component validNonce = IsEqual(); + // the nonce should be previous nonce + 1 + validNonce.in[0] <== originalNonce + 1; + validNonce.in[1] <== nonce; + + var PACKED_CMD_LENGTH = 4; + // d) Whether the signature is correct + signal input cmd[PACKED_CMD_LENGTH]; + signal input pubKey[2]; + signal input sigR8[2]; + signal input sigS; + + component validSignature = VerifySignature(); + validSignature.pubKey[0] <== pubKey[0]; + validSignature.pubKey[1] <== pubKey[1]; + validSignature.R8[0] <== sigR8[0]; + validSignature.R8[1] <== sigR8[1]; + validSignature.S <== sigS; + for (var i = 0; i < PACKED_CMD_LENGTH; i++) { + validSignature.preimage[i] <== cmd[i]; + } + + // e) Whether the state leaf was inserted before the Poll period ended + signal input slTimestamp; + signal input pollEndTimestamp; + component validTimestamp = SafeLessEqThan(252); + validTimestamp.in[0] <== slTimestamp; + validTimestamp.in[1] <== pollEndTimestamp; + + // f) Whether there are sufficient voice credits + signal input currentVoiceCreditBalance; + signal input currentVotesForOption; + signal input voteWeight; + + // Check that currentVoiceCreditBalance + (currentVotesForOption) >= (voteWeight) + component sufficientVoiceCredits = SafeGreaterEqThan(252); + sufficientVoiceCredits.in[0] <== currentVotesForOption + currentVoiceCreditBalance; + sufficientVoiceCredits.in[1] <== voteWeight; + + // if all 6 checks are correct then is IsValid = 1 + component validUpdate = IsEqual(); + validUpdate.in[0] <== 6; + validUpdate.in[1] <== validSignature.valid + + sufficientVoiceCredits.out + + validNonce.out + + validStateLeafIndex.out + + validTimestamp.out + + validVoteOptionIndex.out; + signal output isValid; + isValid <== validUpdate.out; +} diff --git a/circuits/circom/poseidon/poseidonHashT3.circom b/circuits/circom/poseidon/poseidonHashT3.circom index 4d87444989..9c270eea14 100644 --- a/circuits/circom/poseidon/poseidonHashT3.circom +++ b/circuits/circom/poseidon/poseidonHashT3.circom @@ -8,7 +8,7 @@ template PoseidonHashT3() { signal output out; component hasher = Poseidon_OLD(nInputs); - for (var i = 0; i < nInputs; i ++) { + for (var i = 0; i < nInputs; i++) { hasher.inputs[i] <== inputs[i]; } out <== hasher.out; diff --git a/circuits/circom/poseidon/poseidonHashT4.circom b/circuits/circom/poseidon/poseidonHashT4.circom index 3697aca816..98b1945d93 100644 --- a/circuits/circom/poseidon/poseidonHashT4.circom +++ b/circuits/circom/poseidon/poseidonHashT4.circom @@ -8,7 +8,7 @@ template PoseidonHashT4() { signal output out; component hasher = Poseidon_OLD(nInputs); - for (var i = 0; i < nInputs; i ++) { + for (var i = 0; i < nInputs; i++) { hasher.inputs[i] <== inputs[i]; } out <== hasher.out; diff --git a/circuits/circom/poseidon/poseidonHashT5.circom b/circuits/circom/poseidon/poseidonHashT5.circom index 7100fd9b3f..964aecf4f6 100644 --- a/circuits/circom/poseidon/poseidonHashT5.circom +++ b/circuits/circom/poseidon/poseidonHashT5.circom @@ -8,7 +8,7 @@ template PoseidonHashT5() { signal output out; component hasher = Poseidon_OLD(nInputs); - for (var i = 0; i < nInputs; i ++) { + for (var i = 0; i < nInputs; i++) { hasher.inputs[i] <== inputs[i]; } out <== hasher.out; diff --git a/circuits/circom/poseidon/poseidonHashT6.circom b/circuits/circom/poseidon/poseidonHashT6.circom index a74dcdedbb..9d944057c1 100644 --- a/circuits/circom/poseidon/poseidonHashT6.circom +++ b/circuits/circom/poseidon/poseidonHashT6.circom @@ -8,7 +8,7 @@ template PoseidonHashT6() { signal output out; component hasher = Poseidon_OLD(nInputs); - for (var i = 0; i < nInputs; i ++) { + for (var i = 0; i < nInputs; i++) { hasher.inputs[i] <== inputs[i]; } out <== hasher.out; diff --git a/circuits/circom/processMessages.circom b/circuits/circom/processMessages.circom index 2be3bd4564..9022a59121 100644 --- a/circuits/circom/processMessages.circom +++ b/circuits/circom/processMessages.circom @@ -174,9 +174,9 @@ template ProcessMessages( // Hash each Message (along with the encPubKey) so we can check their // existence in the Message tree component messageHashers[batchSize]; - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { messageHashers[i] = MessageHasher(); - for (var j = 0; j < MSG_LENGTH; j ++) { + for (var j = 0; j < MSG_LENGTH; j++) { messageHashers[i].in[j] <== msgs[i][j]; } messageHashers[i].encPubKey[0] <== encPubKeys[i][0]; @@ -203,7 +203,7 @@ template ProcessMessages( component lt[batchSize]; component muxes[batchSize]; - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { lt[i] = SafeLessThan(32); lt[i].in[0] <== batchStartIndex + i; lt[i].in[1] <== batchEndIndex; @@ -215,8 +215,8 @@ template ProcessMessages( msgBatchLeavesExists.leaves[i] <== muxes[i].out; } - for (var i = 0; i < msgTreeDepth - msgBatchDepth; i ++) { - for (var j = 0; j < TREE_ARITY - 1; j ++) { + for (var i = 0; i < msgTreeDepth - msgBatchDepth; i++) { + for (var j = 0; j < TREE_ARITY - 1; j++) { msgBatchLeavesExists.path_elements[i][j] <== msgSubrootPathElements[i][j]; } } @@ -230,7 +230,7 @@ template ProcessMessages( // [1, 0] component msgBatchPathIndices = QuinGeneratePathIndices(msgTreeDepth); msgBatchPathIndices.in <== batchStartIndex; - for (var i = msgBatchDepth; i < msgTreeDepth; i ++) { + for (var i = msgBatchDepth; i < msgTreeDepth; i++) { msgBatchLeavesExists.path_index[i - msgBatchDepth] <== msgBatchPathIndices.out[i]; } @@ -251,12 +251,12 @@ template ProcessMessages( // Decrypt each Message into a Command component commands[batchSize]; - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { commands[i] = MessageToCommand(); commands[i].encPrivKey <== coordPrivKey; commands[i].encPubKey[0] <== encPubKeys[i][0]; commands[i].encPubKey[1] <== encPubKeys[i][1]; - for (var j = 0; j < MSG_LENGTH; j ++) { + for (var j = 0; j < MSG_LENGTH; j++) { commands[i].message[j] <== msgs[i][j]; } } @@ -289,16 +289,16 @@ template ProcessMessages( processors[i].currentStateRoot <== stateRoots[i + 1]; processors[i].currentBallotRoot <== ballotRoots[i + 1]; - for (var j = 0; j < STATE_LEAF_LENGTH; j ++) { + for (var j = 0; j < STATE_LEAF_LENGTH; j++) { processors[i].stateLeaf[j] <== currentStateLeaves[i][j]; } - for (var j = 0; j < BALLOT_LENGTH; j ++) { + for (var j = 0; j < BALLOT_LENGTH; j++) { processors[i].ballot[j] <== currentBallots[i][j]; } - for (var j = 0; j < stateTreeDepth; j ++) { - for (var k = 0; k < TREE_ARITY - 1; k ++) { + for (var j = 0; j < stateTreeDepth; j++) { + for (var k = 0; k < TREE_ARITY - 1; k++) { processors[i].stateLeafPathElements[j][k] <== currentStateLeavesPathElements[i][j][k]; @@ -310,8 +310,8 @@ template ProcessMessages( processors[i].currentVoteWeight <== currentVoteWeights[i]; - for (var j = 0; j < voteOptionTreeDepth; j ++) { - for (var k = 0; k < TREE_ARITY - 1; k ++) { + for (var j = 0; j < voteOptionTreeDepth; j++) { + for (var k = 0; k < TREE_ARITY - 1; k++) { processors[i].currentVoteWeightsPathElements[j][k] <== currentVoteWeightsPathElements[i][j][k]; } @@ -329,7 +329,7 @@ template ProcessMessages( processors[i].cmdSigR8[0] <== commands[i].sigR8[0]; processors[i].cmdSigR8[1] <== commands[i].sigR8[1]; processors[i].cmdSigS <== commands[i].sigS; - for (var j = 0; j < PACKED_CMD_LENGTH; j ++) { + for (var j = 0; j < PACKED_CMD_LENGTH; j++) { processors[i].packedCmd[j] <== commands[i].packedCommandOut[j]; } @@ -340,20 +340,20 @@ template ProcessMessages( processors2[i].stateTreeIndex <== msgs[i][1]; processors2[i].amount <== msgs[i][2]; processors2[i].numSignUps <== numSignUps; - for (var j = 0; j < STATE_LEAF_LENGTH; j ++) { + for (var j = 0; j < STATE_LEAF_LENGTH; j++) { processors2[i].stateLeaf[j] <== currentStateLeaves[i][j]; } - for (var j = 0; j < stateTreeDepth; j ++) { - for (var k = 0; k < TREE_ARITY - 1; k ++) { + for (var j = 0; j < stateTreeDepth; j++) { + for (var k = 0; k < TREE_ARITY - 1; k++) { processors2[i].stateLeafPathElements[j][k] <== currentStateLeavesPathElements[i][j][k]; } } // pick the correct result by msg type - tmpStateRoot1[i] <== processors[i].newStateRoot * (2-msgs[i][0]); - tmpStateRoot2[i] <== processors2[i].newStateRoot * (msgs[i][0]-1); - tmpBallotRoot1[i] <== processors[i].newBallotRoot * (2-msgs[i][0]); - tmpBallotRoot2[i] <== ballotRoots[i+1] * (msgs[i][0]-1); + tmpStateRoot1[i] <== processors[i].newStateRoot * (2 - msgs[i][0]); + tmpStateRoot2[i] <== processors2[i].newStateRoot * (msgs[i][0] - 1); + tmpBallotRoot1[i] <== processors[i].newBallotRoot * (2 - msgs[i][0]); + tmpBallotRoot2[i] <== ballotRoots[i + 1] * (msgs[i][0] - 1); stateRoots[i] <== tmpStateRoot1[i] + tmpStateRoot2[i]; ballotRoots[i] <== tmpBallotRoot1[i] + tmpBallotRoot2[i]; } @@ -431,7 +431,7 @@ template ProcessTopup(stateTreeDepth) { component newStateLeafQip = QuinTreeInclusionProof(stateTreeDepth); newStateLeafQip.leaf <== newStateLeafHasher.hash; - for (var i = 0; i < stateTreeDepth; i ++) { + for (var i = 0; i < stateTreeDepth; i++) { newStateLeafQip.path_index[i] <== stateLeafPathIndices.out[i]; for (var j = 0; j < TREE_ARITY - 1; j++) { newStateLeafQip.path_elements[i][j] <== stateLeafPathElements[i][j]; @@ -520,7 +520,7 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { transformer.cmdSigR8[0] <== cmdSigR8[0]; transformer.cmdSigR8[1] <== cmdSigR8[1]; transformer.cmdSigS <== cmdSigS; - for (var i = 0; i < PACKED_CMD_LENGTH; i ++) { + for (var i = 0; i < PACKED_CMD_LENGTH; i++) { transformer.packedCommand[i] <== packedCmd[i]; } @@ -550,7 +550,7 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { stateLeafHasher.in[i] <== stateLeaf[i]; } stateLeafQip.leaf <== stateLeafHasher.hash; - for (var i = 0; i < stateTreeDepth; i ++) { + for (var i = 0; i < stateTreeDepth; i++) { stateLeafQip.path_index[i] <== stateLeafPathIndices.out[i]; for (var j = 0; j < TREE_ARITY - 1; j++) { stateLeafQip.path_elements[i][j] <== stateLeafPathElements[i][j]; @@ -566,7 +566,7 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { component ballotQip = QuinTreeInclusionProof(stateTreeDepth); ballotQip.leaf <== ballotHasher.hash; - for (var i = 0; i < stateTreeDepth; i ++) { + for (var i = 0; i < stateTreeDepth; i++) { ballotQip.path_index[i] <== stateLeafPathIndices.out[i]; for (var j = 0; j < TREE_ARITY - 1; j++) { ballotQip.path_elements[i][j] <== ballotPathElements[i][j]; @@ -602,7 +602,7 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { component currentVoteWeightQip = QuinTreeInclusionProof(voteOptionTreeDepth); currentVoteWeightQip.leaf <== currentVoteWeight; - for (var i = 0; i < voteOptionTreeDepth; i ++) { + for (var i = 0; i < voteOptionTreeDepth; i++) { currentVoteWeightQip.path_index[i] <== currentVoteWeightPathIndices.out[i]; for (var j = 0; j < TREE_ARITY - 1; j++) { currentVoteWeightQip.path_elements[i][j] <== currentVoteWeightsPathElements[i][j]; @@ -630,7 +630,7 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { // 5.1. Update the ballot's vote option root with the new vote weight component newVoteOptionTreeQip = QuinTreeInclusionProof(voteOptionTreeDepth); newVoteOptionTreeQip.leaf <== voteWeightMux.out; - for (var i = 0; i < voteOptionTreeDepth; i ++) { + for (var i = 0; i < voteOptionTreeDepth; i++) { newVoteOptionTreeQip.path_index[i] <== currentVoteWeightPathIndices.out[i]; for (var j = 0; j < TREE_ARITY - 1; j++) { newVoteOptionTreeQip.path_elements[i][j] <== currentVoteWeightsPathElements[i][j]; @@ -655,7 +655,7 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { component newStateLeafQip = QuinTreeInclusionProof(stateTreeDepth); newStateLeafQip.leaf <== newStateLeafHasher.hash; - for (var i = 0; i < stateTreeDepth; i ++) { + for (var i = 0; i < stateTreeDepth; i++) { newStateLeafQip.path_index[i] <== stateLeafPathIndices.out[i]; for (var j = 0; j < TREE_ARITY - 1; j++) { newStateLeafQip.path_elements[i][j] <== stateLeafPathElements[i][j]; @@ -677,7 +677,7 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { component newBallotQip = QuinTreeInclusionProof(stateTreeDepth); newBallotQip.leaf <== newBallotHasher.hash; - for (var i = 0; i < stateTreeDepth; i ++) { + for (var i = 0; i < stateTreeDepth; i++) { newBallotQip.path_index[i] <== stateLeafPathIndices.out[i]; for (var j = 0; j < TREE_ARITY - 1; j++) { newBallotQip.path_elements[i][j] <== ballotPathElements[i][j]; diff --git a/circuits/circom/processMessagesNonQv.circom b/circuits/circom/processMessagesNonQv.circom new file mode 100644 index 0000000000..82d114c43b --- /dev/null +++ b/circuits/circom/processMessagesNonQv.circom @@ -0,0 +1,614 @@ +pragma circom 2.0.0; + +// circomlib import +include "./mux1.circom"; + +// local imports +include "./hasherSha256.circom"; +include "./messageHasher.circom"; +include "./messageToCommand.circom"; +include "./privToPubKey.circom"; +include "./stateLeafAndBallotTransformerNonQv.circom"; +include "./trees/incrementalQuinTree.circom"; +include "./utils.circom"; +include "./processMessages.circom"; + +// Proves the correctness of processing a batch of messages. +template ProcessMessagesNonQv( + stateTreeDepth, + msgTreeDepth, + msgBatchDepth, // aka msgSubtreeDepth + voteOptionTreeDepth +) { + // stateTreeDepth: the depth of the state tree + // msgTreeDepth: the depth of the message tree + // msgBatchDepth: the depth of the shortest tree that can fit all the + // messages in a batch + // voteOptionTreeDepth: depth of the vote option tree + + // we want to ensure that the trees have a valid structure + assert(stateTreeDepth > 0); + assert(msgBatchDepth > 0); + assert(voteOptionTreeDepth > 0); + assert(msgTreeDepth >= msgBatchDepth); + + // default to quinary merkle tree + var TREE_ARITY = 5; + var batchSize = TREE_ARITY ** msgBatchDepth; + + var MSG_LENGTH = 11; + var PACKED_CMD_LENGTH = 4; + + var STATE_LEAF_LENGTH = 4; + var BALLOT_LENGTH = 2; + + var BALLOT_NONCE_IDX = 0; + var BALLOT_VO_ROOT_IDX = 1; + + var STATE_LEAF_PUB_X_IDX = 0; + var STATE_LEAF_PUB_Y_IDX = 1; + var STATE_LEAF_VOICE_CREDIT_BALANCE_IDX = 2; + var STATE_LEAF_TIMESTAMP_IDX = 3; + + // Note that we sha256 hash some values from the contract, pass in the hash + // as a public input, and pass in said values as private inputs. This saves + // a lot of gas for the verifier at the cost of constraints for the prover. + + // ----------------------------------------------------------------------- + // The only public input, which is the SHA256 hash of values provided + // by the contract + signal input inputHash; + signal input packedVals; + + // how many users signed up + signal numSignUps; + // how many options are there for this poll + signal maxVoteOptions; + + // when the poll ends + signal input pollEndTimestamp; + // The existing message root + signal input msgRoot; + + // The messages + signal input msgs[batchSize][MSG_LENGTH]; + + // The message leaf Merkle proofs + signal input msgSubrootPathElements[msgTreeDepth - msgBatchDepth][TREE_ARITY - 1]; + + // The index of the first message leaf in the batch, inclusive. Note that + // messages are processed in reverse order, so this is not be the index of + // the first message to process (unless there is only 1 message) + signal batchStartIndex; + + // The index of the last message leaf in the batch to process, exclusive. + // This value may be less than batchStartIndex + batchSize if this batch is + // the last batch and the total number of mesages is not a multiple of the + // batch size. + signal batchEndIndex; + + // The coordinator's private key + signal input coordPrivKey; + + // The cooordinator's public key from the contract. + signal input coordPubKey[2]; + + // The ECDH public key per message + signal input encPubKeys[batchSize][2]; + + // The state root before it is processed + signal input currentStateRoot; + + // The state leaves upon which messages are applied. + // transform(currentStateLeaf[4], message5) => newStateLeaf4 + // transform(currentStateLeaf[3], message4) => newStateLeaf3 + // transform(currentStateLeaf[2], message3) => newStateLeaf2 + // transform(currentStateLeaf[1], message1) => newStateLeaf1 + // transform(currentStateLeaf[0], message0) => newStateLeaf0 + // ... + // Likewise, currentStateLeavesPathElements contains the Merkle path to + // each incremental new state root. + signal input currentStateLeaves[batchSize][STATE_LEAF_LENGTH]; + signal input currentStateLeavesPathElements[batchSize][stateTreeDepth][TREE_ARITY - 1]; + + // The salted commitment to the state root and ballot root + signal input currentSbCommitment; + signal input currentSbSalt; + + // The salted commitment to the new state root and ballot root + signal input newSbCommitment; + signal input newSbSalt; + + // The ballots before any messages are processed + signal input currentBallotRoot; + + // Intermediate ballots, like currentStateLeaves + signal input currentBallots[batchSize][BALLOT_LENGTH]; + signal input currentBallotsPathElements[batchSize][stateTreeDepth][TREE_ARITY - 1]; + + signal input currentVoteWeights[batchSize]; + signal input currentVoteWeightsPathElements[batchSize][voteOptionTreeDepth][TREE_ARITY - 1]; + + var msgTreeZeroValue = 8370432830353022751713833565135785980866757267633941821328460903436894336785; + + // Verify currentSbCommitment + // currentSbCommitment === hash3(currentStateRoot, currentBallotRoot, currentSbSalt) + component currentSbCommitmentHasher = Hasher3(); + currentSbCommitmentHasher.in[0] <== currentStateRoot; + currentSbCommitmentHasher.in[1] <== currentBallotRoot; + currentSbCommitmentHasher.in[2] <== currentSbSalt; + currentSbCommitmentHasher.hash === currentSbCommitment; + + // Verify "public" inputs and assign unpacked values + component inputHasher = ProcessMessagesInputHasher(); + inputHasher.packedVals <== packedVals; + inputHasher.coordPubKey[0] <== coordPubKey[0]; + inputHasher.coordPubKey[1] <== coordPubKey[1]; + inputHasher.msgRoot <== msgRoot; + inputHasher.currentSbCommitment <== currentSbCommitment; + inputHasher.newSbCommitment <== newSbCommitment; + inputHasher.pollEndTimestamp <== pollEndTimestamp; + + // The unpacked values from packedVals + inputHasher.maxVoteOptions ==> maxVoteOptions; + inputHasher.numSignUps ==> numSignUps; + inputHasher.batchStartIndex ==> batchStartIndex; + inputHasher.batchEndIndex ==> batchEndIndex; + + // constraints that they match + inputHasher.hash === inputHash; + + // ----------------------------------------------------------------------- + // 0. Ensure that the maximum vote options signal is valid and whether + // the maximum users signal is valid + component maxVoValid = LessEqThan(32); + maxVoValid.in[0] <== maxVoteOptions; + maxVoValid.in[1] <== TREE_ARITY ** voteOptionTreeDepth; + maxVoValid.out === 1; + // we check that the number of signups is less than the max number of users + // which is the n of state leaves we can fit in the state tree + component numSignUpsValid = LessEqThan(32); + numSignUpsValid.in[0] <== numSignUps; + numSignUpsValid.in[1] <== TREE_ARITY ** stateTreeDepth; + numSignUpsValid.out === 1; + + // Hash each Message (along with the encPubKey) so we can check their + // existence in the Message tree + component messageHashers[batchSize]; + for (var i = 0; i < batchSize; i++) { + messageHashers[i] = MessageHasher(); + for (var j = 0; j < MSG_LENGTH; j++) { + messageHashers[i].in[j] <== msgs[i][j]; + } + messageHashers[i].encPubKey[0] <== encPubKeys[i][0]; + messageHashers[i].encPubKey[1] <== encPubKeys[i][1]; + } + + // ----------------------------------------------------------------------- + // Check whether each message exists in the message tree. Throw if + // otherwise (aka create a constraint that prevents such a proof). + + // To save constraints, compute the subroot of the messages and check + // whether the subroot is a member of the message tree. This means that + // batchSize must be the message tree arity raised to some power + // (e.g. 5 ^ n) + + component msgBatchLeavesExists = QuinBatchLeavesExists(msgTreeDepth, msgBatchDepth); + msgBatchLeavesExists.root <== msgRoot; + + // If batchEndIndex - batchStartIndex < batchSize, the remaining + // message hashes should be the zero value. + // e.g. [m, z, z, z, z] if there is only 1 real message in the batch + // This allows us to have a batch of messages which is only partially + // full. + component lt[batchSize]; + component muxes[batchSize]; + + for (var i = 0; i < batchSize; i++) { + lt[i] = SafeLessThan(32); + lt[i].in[0] <== batchStartIndex + i; + lt[i].in[1] <== batchEndIndex; + + muxes[i] = Mux1(); + muxes[i].s <== lt[i].out; + muxes[i].c[0] <== msgTreeZeroValue; + muxes[i].c[1] <== messageHashers[i].hash; + msgBatchLeavesExists.leaves[i] <== muxes[i].out; + } + + for (var i = 0; i < msgTreeDepth - msgBatchDepth; i++) { + for (var j = 0; j < TREE_ARITY - 1; j++) { + msgBatchLeavesExists.path_elements[i][j] <== msgSubrootPathElements[i][j]; + } + } + + // Assign values to msgBatchLeavesExists.path_index. Since + // msgBatchLeavesExists tests for the existence of a subroot, the length of + // the proof is the last n elements of a proof from the root to a leaf + // where n = msgTreeDepth - msgBatchDepth + // e.g. if batchStartIndex = 25, msgTreeDepth = 4, msgBatchDepth = 2 + // msgBatchLeavesExists.path_index should be: + // [1, 0] + component msgBatchPathIndices = QuinGeneratePathIndices(msgTreeDepth); + msgBatchPathIndices.in <== batchStartIndex; + for (var i = msgBatchDepth; i < msgTreeDepth; i++) { + msgBatchLeavesExists.path_index[i - msgBatchDepth] <== msgBatchPathIndices.out[i]; + } + + // ----------------------------------------------------------------------- + // Decrypt each Message to a Command + + // MessageToCommand derives the ECDH shared key from the coordinator's + // private key and the message's ephemeral public key. Next, it uses this + // shared key to decrypt a Message to a Command. + + // Ensure that the coordinator's public key from the contract is correct + // based on the given private key - that is, the prover knows the + // coordinator's private key. + component derivedPubKey = PrivToPubKey(); + derivedPubKey.privKey <== coordPrivKey; + derivedPubKey.pubKey[0] === coordPubKey[0]; + derivedPubKey.pubKey[1] === coordPubKey[1]; + + // Decrypt each Message into a Command + component commands[batchSize]; + for (var i = 0; i < batchSize; i++) { + commands[i] = MessageToCommand(); + commands[i].encPrivKey <== coordPrivKey; + commands[i].encPubKey[0] <== encPubKeys[i][0]; + commands[i].encPubKey[1] <== encPubKeys[i][1]; + for (var j = 0; j < MSG_LENGTH; j++) { + commands[i].message[j] <== msgs[i][j]; + } + } + + signal stateRoots[batchSize + 1]; + signal ballotRoots[batchSize + 1]; + + stateRoots[batchSize] <== currentStateRoot; + ballotRoots[batchSize] <== currentBallotRoot; + + // ----------------------------------------------------------------------- + // Process messages in reverse order + signal tmpStateRoot1[batchSize]; + signal tmpStateRoot2[batchSize]; + signal tmpBallotRoot1[batchSize]; + signal tmpBallotRoot2[batchSize]; + // vote type processor + component processors[batchSize]; + // topup type processor + component processors2[batchSize]; + for (var i = batchSize - 1; i >= 0; i--) { + // process it as vote type message + processors[i] = ProcessOneNonQv(stateTreeDepth, voteOptionTreeDepth); + + processors[i].msgType <== msgs[i][0]; + processors[i].numSignUps <== numSignUps; + processors[i].maxVoteOptions <== maxVoteOptions; + processors[i].pollEndTimestamp <== pollEndTimestamp; + + processors[i].currentStateRoot <== stateRoots[i + 1]; + processors[i].currentBallotRoot <== ballotRoots[i + 1]; + + for (var j = 0; j < STATE_LEAF_LENGTH; j++) { + processors[i].stateLeaf[j] <== currentStateLeaves[i][j]; + } + + for (var j = 0; j < BALLOT_LENGTH; j++) { + processors[i].ballot[j] <== currentBallots[i][j]; + } + + for (var j = 0; j < stateTreeDepth; j++) { + for (var k = 0; k < TREE_ARITY - 1; k++) { + + processors[i].stateLeafPathElements[j][k] + <== currentStateLeavesPathElements[i][j][k]; + + processors[i].ballotPathElements[j][k] + <== currentBallotsPathElements[i][j][k]; + } + } + + processors[i].currentVoteWeight <== currentVoteWeights[i]; + + for (var j = 0; j < voteOptionTreeDepth; j++) { + for (var k = 0; k < TREE_ARITY - 1; k++) { + processors[i].currentVoteWeightsPathElements[j][k] + <== currentVoteWeightsPathElements[i][j][k]; + } + } + + processors[i].cmdStateIndex <== commands[i].stateIndex; + processors[i].topupStateIndex <== msgs[i][1]; + processors[i].cmdNewPubKey[0] <== commands[i].newPubKey[0]; + processors[i].cmdNewPubKey[1] <== commands[i].newPubKey[1]; + processors[i].cmdVoteOptionIndex <== commands[i].voteOptionIndex; + processors[i].cmdNewVoteWeight <== commands[i].newVoteWeight; + processors[i].cmdNonce <== commands[i].nonce; + processors[i].cmdPollId <== commands[i].pollId; + processors[i].cmdSalt <== commands[i].salt; + processors[i].cmdSigR8[0] <== commands[i].sigR8[0]; + processors[i].cmdSigR8[1] <== commands[i].sigR8[1]; + processors[i].cmdSigS <== commands[i].sigS; + for (var j = 0; j < PACKED_CMD_LENGTH; j++) { + processors[i].packedCmd[j] <== commands[i].packedCommandOut[j]; + } + + // -------------------------------------------- + // process it as topup type message, + processors2[i] = ProcessTopup(stateTreeDepth); + processors2[i].msgType <== msgs[i][0]; + processors2[i].stateTreeIndex <== msgs[i][1]; + processors2[i].amount <== msgs[i][2]; + processors2[i].numSignUps <== numSignUps; + for (var j = 0; j < STATE_LEAF_LENGTH; j++) { + processors2[i].stateLeaf[j] <== currentStateLeaves[i][j]; + } + for (var j = 0; j < stateTreeDepth; j++) { + for (var k = 0; k < TREE_ARITY - 1; k++) { + processors2[i].stateLeafPathElements[j][k] + <== currentStateLeavesPathElements[i][j][k]; + } + } + // pick the correct result by msg type + tmpStateRoot1[i] <== processors[i].newStateRoot * (2 - msgs[i][0]); + tmpStateRoot2[i] <== processors2[i].newStateRoot * (msgs[i][0] - 1); + tmpBallotRoot1[i] <== processors[i].newBallotRoot * (2 - msgs[i][0]); + tmpBallotRoot2[i] <== ballotRoots[i + 1] * (msgs[i][0] -1); + stateRoots[i] <== tmpStateRoot1[i] + tmpStateRoot2[i]; + ballotRoots[i] <== tmpBallotRoot1[i] + tmpBallotRoot2[i]; + } + + component sbCommitmentHasher = Hasher3(); + sbCommitmentHasher.in[0] <== stateRoots[0]; + sbCommitmentHasher.in[1] <== ballotRoots[0]; + sbCommitmentHasher.in[2] <== newSbSalt; + + sbCommitmentHasher.hash === newSbCommitment; +} + +// process one message +template ProcessOneNonQv(stateTreeDepth, voteOptionTreeDepth) { + /* + transform(currentStateLeaves0, cmd0) -> newStateLeaves0, isValid0 + genIndices(isValid0, cmd0) -> pathIndices0 + verify(currentStateRoot, pathElements0, pathIndices0, currentStateLeaves0) + qip(newStateLeaves0, pathElements0) -> newStateRoot0 + */ + var STATE_LEAF_LENGTH = 4; + var BALLOT_LENGTH = 2; + var MSG_LENGTH = 11; + var PACKED_CMD_LENGTH = 4; + var TREE_ARITY = 5; + + var BALLOT_NONCE_IDX = 0; + var BALLOT_VO_ROOT_IDX = 1; + + var STATE_LEAF_PUB_X_IDX = 0; + var STATE_LEAF_PUB_Y_IDX = 1; + var STATE_LEAF_VOICE_CREDIT_BALANCE_IDX = 2; + var STATE_LEAF_TIMESTAMP_IDX = 3; + + signal input msgType; + signal input numSignUps; + signal input maxVoteOptions; + + signal input pollEndTimestamp; + + signal input currentStateRoot; + signal input currentBallotRoot; + + signal input stateLeaf[STATE_LEAF_LENGTH]; + signal input stateLeafPathElements[stateTreeDepth][TREE_ARITY - 1]; + + signal input ballot[BALLOT_LENGTH]; + signal input ballotPathElements[stateTreeDepth][TREE_ARITY - 1]; + + signal input currentVoteWeight; + signal input currentVoteWeightsPathElements[voteOptionTreeDepth][TREE_ARITY - 1]; + + signal input cmdStateIndex; + signal input topupStateIndex; + signal input cmdNewPubKey[2]; + signal input cmdVoteOptionIndex; + signal input cmdNewVoteWeight; + signal input cmdNonce; + signal input cmdPollId; + signal input cmdSalt; + signal input cmdSigR8[2]; + signal input cmdSigS; + signal input packedCmd[PACKED_CMD_LENGTH]; + + signal output newStateRoot; + signal output newBallotRoot; + + // ----------------------------------------------------------------------- + // 1. Transform a state leaf and a ballot with a command. + // The result is a new state leaf, a new ballot, and an isValid signal (0 + // or 1) + component transformer = StateLeafAndBallotTransformerNonQv(); + transformer.numSignUps <== numSignUps; + transformer.maxVoteOptions <== maxVoteOptions; + transformer.slPubKey[STATE_LEAF_PUB_X_IDX] <== stateLeaf[STATE_LEAF_PUB_X_IDX]; + transformer.slPubKey[STATE_LEAF_PUB_Y_IDX] <== stateLeaf[STATE_LEAF_PUB_Y_IDX]; + transformer.slVoiceCreditBalance <== stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX]; + transformer.slTimestamp <== stateLeaf[STATE_LEAF_TIMESTAMP_IDX]; + transformer.pollEndTimestamp <== pollEndTimestamp; + transformer.ballotNonce <== ballot[BALLOT_NONCE_IDX]; + transformer.ballotCurrentVotesForOption <== currentVoteWeight; + transformer.cmdStateIndex <== cmdStateIndex; + transformer.cmdNewPubKey[0] <== cmdNewPubKey[0]; + transformer.cmdNewPubKey[1] <== cmdNewPubKey[1]; + transformer.cmdVoteOptionIndex <== cmdVoteOptionIndex; + transformer.cmdNewVoteWeight <== cmdNewVoteWeight; + transformer.cmdNonce <== cmdNonce; + transformer.cmdPollId <== cmdPollId; + transformer.cmdSalt <== cmdSalt; + transformer.cmdSigR8[0] <== cmdSigR8[0]; + transformer.cmdSigR8[1] <== cmdSigR8[1]; + transformer.cmdSigS <== cmdSigS; + for (var i = 0; i < PACKED_CMD_LENGTH; i++) { + transformer.packedCommand[i] <== packedCmd[i]; + } + + // ----------------------------------------------------------------------- + // 2. If msgType = 0 and isValid is 0, generate indices for leaf 0 + // Otherwise, generate indices for commmand.stateIndex or topupStateIndex depending on msgType + signal indexByType; + signal tmpIndex1; + signal tmpIndex2; + tmpIndex1 <== cmdStateIndex * (2 - msgType); + tmpIndex2 <== topupStateIndex * (msgType - 1); + indexByType <== tmpIndex1 + tmpIndex2; + + component stateIndexMux = Mux1(); + stateIndexMux.s <== transformer.isValid + msgType - 1; + stateIndexMux.c[0] <== 0; + stateIndexMux.c[1] <== indexByType; + + component stateLeafPathIndices = QuinGeneratePathIndices(stateTreeDepth); + stateLeafPathIndices.in <== stateIndexMux.out; + + // ----------------------------------------------------------------------- + // 3. Verify that the original state leaf exists in the given state root + component stateLeafQip = QuinTreeInclusionProof(stateTreeDepth); + component stateLeafHasher = Hasher4(); + for (var i = 0; i < STATE_LEAF_LENGTH; i++) { + stateLeafHasher.in[i] <== stateLeaf[i]; + } + stateLeafQip.leaf <== stateLeafHasher.hash; + for (var i = 0; i < stateTreeDepth; i++) { + stateLeafQip.path_index[i] <== stateLeafPathIndices.out[i]; + for (var j = 0; j < TREE_ARITY - 1; j++) { + stateLeafQip.path_elements[i][j] <== stateLeafPathElements[i][j]; + } + } + stateLeafQip.root === currentStateRoot; + + // ----------------------------------------------------------------------- + // 4. Verify that the original ballot exists in the given ballot root + component ballotHasher = HashLeftRight(); + ballotHasher.left <== ballot[BALLOT_NONCE_IDX]; + ballotHasher.right <== ballot[BALLOT_VO_ROOT_IDX]; + + component ballotQip = QuinTreeInclusionProof(stateTreeDepth); + ballotQip.leaf <== ballotHasher.hash; + for (var i = 0; i < stateTreeDepth; i++) { + ballotQip.path_index[i] <== stateLeafPathIndices.out[i]; + for (var j = 0; j < TREE_ARITY - 1; j++) { + ballotQip.path_elements[i][j] <== ballotPathElements[i][j]; + } + } + ballotQip.root === currentBallotRoot; + + // ----------------------------------------------------------------------- + // 5. Verify that currentVoteWeight exists in the ballot's vote option root + // at cmdVoteOptionIndex + + signal b; + signal c; + b <== currentVoteWeight; + c <== cmdNewVoteWeight; + + component enoughVoiceCredits = SafeGreaterEqThan(252); + enoughVoiceCredits.in[0] <== stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX] + b; + enoughVoiceCredits.in[1] <== c; + + component isMessageValid = IsEqual(); + var bothValid = 2; + isMessageValid.in[0] <== bothValid; + isMessageValid.in[1] <== transformer.isValid + enoughVoiceCredits.out; + + component cmdVoteOptionIndexMux = Mux1(); + cmdVoteOptionIndexMux.s <== isMessageValid.out; + cmdVoteOptionIndexMux.c[0] <== 0; + cmdVoteOptionIndexMux.c[1] <== cmdVoteOptionIndex; + + component currentVoteWeightPathIndices = QuinGeneratePathIndices(voteOptionTreeDepth); + currentVoteWeightPathIndices.in <== cmdVoteOptionIndexMux.out; + + component currentVoteWeightQip = QuinTreeInclusionProof(voteOptionTreeDepth); + currentVoteWeightQip.leaf <== currentVoteWeight; + for (var i = 0; i < voteOptionTreeDepth; i++) { + currentVoteWeightQip.path_index[i] <== currentVoteWeightPathIndices.out[i]; + for (var j = 0; j < TREE_ARITY - 1; j++) { + currentVoteWeightQip.path_elements[i][j] <== currentVoteWeightsPathElements[i][j]; + } + } + + currentVoteWeightQip.root === ballot[BALLOT_VO_ROOT_IDX]; + + component voteWeightMux = Mux1(); + voteWeightMux.s <== isMessageValid.out; + voteWeightMux.c[0] <== currentVoteWeight; + voteWeightMux.c[1] <== cmdNewVoteWeight; + + component newSlVoiceCreditBalance = Mux1(); + newSlVoiceCreditBalance.c[0] <== stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX]; + newSlVoiceCreditBalance.c[1] <== stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX] + b - c; + newSlVoiceCreditBalance.s <== enoughVoiceCredits.out; + + component voiceCreditBalanceMux = Mux1(); + voiceCreditBalanceMux.s <== isMessageValid.out; + voiceCreditBalanceMux.c[0] <== stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX]; + voiceCreditBalanceMux.c[1] <== newSlVoiceCreditBalance.out; + + // ----------------------------------------------------------------------- + // 5.1. Update the ballot's vote option root with the new vote weight + component newVoteOptionTreeQip = QuinTreeInclusionProof(voteOptionTreeDepth); + newVoteOptionTreeQip.leaf <== voteWeightMux.out; + for (var i = 0; i < voteOptionTreeDepth; i++) { + newVoteOptionTreeQip.path_index[i] <== currentVoteWeightPathIndices.out[i]; + for (var j = 0; j < TREE_ARITY - 1; j++) { + newVoteOptionTreeQip.path_elements[i][j] <== currentVoteWeightsPathElements[i][j]; + } + } + + // The new vote option root in the ballot + signal newBallotVoRoot; + component newBallotVoRootMux = Mux1(); + newBallotVoRootMux.s <== isMessageValid.out; + newBallotVoRootMux.c[0] <== ballot[BALLOT_VO_ROOT_IDX]; + newBallotVoRootMux.c[1] <== newVoteOptionTreeQip.root; + newBallotVoRoot <== newBallotVoRootMux.out; + + // ----------------------------------------------------------------------- + // 6. Generate a new state root + component newStateLeafHasher = Hasher4(); + newStateLeafHasher.in[STATE_LEAF_PUB_X_IDX] <== transformer.newSlPubKey[STATE_LEAF_PUB_X_IDX]; + newStateLeafHasher.in[STATE_LEAF_PUB_Y_IDX] <== transformer.newSlPubKey[STATE_LEAF_PUB_Y_IDX]; + newStateLeafHasher.in[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX] <== voiceCreditBalanceMux.out; + newStateLeafHasher.in[STATE_LEAF_TIMESTAMP_IDX] <== stateLeaf[STATE_LEAF_TIMESTAMP_IDX]; + + component newStateLeafQip = QuinTreeInclusionProof(stateTreeDepth); + newStateLeafQip.leaf <== newStateLeafHasher.hash; + for (var i = 0; i < stateTreeDepth; i++) { + newStateLeafQip.path_index[i] <== stateLeafPathIndices.out[i]; + for (var j = 0; j < TREE_ARITY - 1; j++) { + newStateLeafQip.path_elements[i][j] <== stateLeafPathElements[i][j]; + } + } + newStateRoot <== newStateLeafQip.root; + + // ----------------------------------------------------------------------- + // 7. Generate a new ballot root + + component newBallotNonceMux = Mux1(); + newBallotNonceMux.s <== isMessageValid.out; + newBallotNonceMux.c[0] <== ballot[BALLOT_NONCE_IDX]; + newBallotNonceMux.c[1] <== transformer.newBallotNonce; + + component newBallotHasher = HashLeftRight(); + newBallotHasher.left <== newBallotNonceMux.out; + newBallotHasher.right <== newBallotVoRoot; + + component newBallotQip = QuinTreeInclusionProof(stateTreeDepth); + newBallotQip.leaf <== newBallotHasher.hash; + for (var i = 0; i < stateTreeDepth; i++) { + newBallotQip.path_index[i] <== stateLeafPathIndices.out[i]; + for (var j = 0; j < TREE_ARITY - 1; j++) { + newBallotQip.path_elements[i][j] <== ballotPathElements[i][j]; + } + } + newBallotRoot <== newBallotQip.root; +} diff --git a/circuits/circom/stateLeafAndBallotTransformer.circom b/circuits/circom/stateLeafAndBallotTransformer.circom index f2106db6bb..609c688f1f 100644 --- a/circuits/circom/stateLeafAndBallotTransformer.circom +++ b/circuits/circom/stateLeafAndBallotTransformer.circom @@ -70,7 +70,7 @@ template StateLeafAndBallotTransformer() { messageValidator.maxVoteOptions <== maxVoteOptions; messageValidator.originalNonce <== ballotNonce; messageValidator.nonce <== cmdNonce; - for (var i = 0; i < PACKED_CMD_LENGTH; i ++) { + for (var i = 0; i < PACKED_CMD_LENGTH; i++) { messageValidator.cmd[i] <== packedCommand[i]; } messageValidator.pubKey[0] <== slPubKey[0]; diff --git a/circuits/circom/stateLeafAndBallotTransformerNonQv.circom b/circuits/circom/stateLeafAndBallotTransformerNonQv.circom new file mode 100644 index 0000000000..3a3a8b3e6f --- /dev/null +++ b/circuits/circom/stateLeafAndBallotTransformerNonQv.circom @@ -0,0 +1,111 @@ +pragma circom 2.0.0; + +// circomlib import +include "./mux1.circom"; + +// local import +include "./messageValidatorNonQv.circom"; + +// Apply a command to a state leaf and ballot. +template StateLeafAndBallotTransformerNonQv() { + var PACKED_CMD_LENGTH = 4; + + // For the MessageValidator + // how many users signed up + signal input numSignUps; + // what is the maximum number of vote options + signal input maxVoteOptions; + + // State leaf + // the public key of the signed up user + signal input slPubKey[2]; + // the current voice credit balance of the signed up user + signal input slVoiceCreditBalance; + // the signup timestmap + signal input slTimestamp; + // when the poll ends + signal input pollEndTimestamp; + + // Ballot + // the nonce of the ballot + signal input ballotNonce; + // the vote for this option + signal input ballotCurrentVotesForOption; + + // Command + // state index of the user + signal input cmdStateIndex; + // the pub key + signal input cmdNewPubKey[2]; + // the vote option index + signal input cmdVoteOptionIndex; + // the vote weight + signal input cmdNewVoteWeight; + // the nonce of the command + signal input cmdNonce; + // the id of the poll for which this command is for + signal input cmdPollId; + // the salt of the command + signal input cmdSalt; + // the ECDSA signature of the command + // r part + signal input cmdSigR8[2]; + // s part + signal input cmdSigS; + // @note we assume that packedCommand is valid! + signal input packedCommand[PACKED_CMD_LENGTH]; + + // New state leaf (if the command is valid) + signal output newSlPubKey[2]; + + // New ballot (if the command is valid) + signal output newBallotNonce; + signal output isValid; + + // Check if the command / message is valid + component messageValidator = MessageValidatorNonQv(); + messageValidator.stateTreeIndex <== cmdStateIndex; + messageValidator.numSignUps <== numSignUps; + messageValidator.voteOptionIndex <== cmdVoteOptionIndex; + messageValidator.maxVoteOptions <== maxVoteOptions; + messageValidator.originalNonce <== ballotNonce; + messageValidator.nonce <== cmdNonce; + for (var i = 0; i < PACKED_CMD_LENGTH; i++) { + messageValidator.cmd[i] <== packedCommand[i]; + } + messageValidator.pubKey[0] <== slPubKey[0]; + messageValidator.pubKey[1] <== slPubKey[1]; + messageValidator.sigR8[0] <== cmdSigR8[0]; + messageValidator.sigR8[1] <== cmdSigR8[1]; + messageValidator.sigS <== cmdSigS; + + messageValidator.currentVoiceCreditBalance <== slVoiceCreditBalance; + messageValidator.slTimestamp <== slTimestamp; + messageValidator.pollEndTimestamp <== pollEndTimestamp; + messageValidator.currentVotesForOption <== ballotCurrentVotesForOption; + messageValidator.voteWeight <== cmdNewVoteWeight; + + // if the message is valid then we swap out the public key + // we have to do this in two Mux one for pucKey[0] + // and one for pubKey[1] + component newSlPubKey0Mux = Mux1(); + newSlPubKey0Mux.s <== messageValidator.isValid; + newSlPubKey0Mux.c[0] <== slPubKey[0]; + newSlPubKey0Mux.c[1] <== cmdNewPubKey[0]; + newSlPubKey[0] <== newSlPubKey0Mux.out; + + component newSlPubKey1Mux = Mux1(); + newSlPubKey1Mux.s <== messageValidator.isValid; + newSlPubKey1Mux.c[0] <== slPubKey[1]; + newSlPubKey1Mux.c[1] <== cmdNewPubKey[1]; + newSlPubKey[1] <== newSlPubKey1Mux.out; + + // if the message is valid then we swap out the ballot nonce + component newBallotNonceMux = Mux1(); + newBallotNonceMux.s <== messageValidator.isValid; + newBallotNonceMux.c[0] <== ballotNonce; + newBallotNonceMux.c[1] <== cmdNonce; + newBallotNonce <== newBallotNonceMux.out; + + isValid <== messageValidator.isValid; +} diff --git a/circuits/circom/subsidy.circom b/circuits/circom/subsidy.circom index 2edc1d8eef..93caa66796 100644 --- a/circuits/circom/subsidy.circom +++ b/circuits/circom/subsidy.circom @@ -105,7 +105,7 @@ template SubsidyPerBatch ( // Verify both batches belong to the ballot tree component ballotTreeVerifier1 = BatchMerkleTreeVerifier(stateTreeDepth, intStateTreeDepth, TREE_ARITY); component ballotHashers1[batchSize]; - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { ballotHashers1[i] = HashLeftRight(); ballotHashers1[i].left <== ballots1[i][BALLOT_NONCE_IDX]; ballotHashers1[i].right <== ballots1[i][BALLOT_VO_ROOT_IDX]; @@ -113,15 +113,15 @@ template SubsidyPerBatch ( } ballotTreeVerifier1.index <== inputHasher.rbi; ballotTreeVerifier1.root <== ballotRoot; - for (var i = 0; i < k; i ++) { - for (var j = 0; j < TREE_ARITY - 1; j ++) { + for (var i = 0; i < k; i++) { + for (var j = 0; j < TREE_ARITY - 1; j++) { ballotTreeVerifier1.pathElements[i][j] <== ballotPathElements1[i][j]; } } component ballotTreeVerifier2 = BatchMerkleTreeVerifier(stateTreeDepth, intStateTreeDepth, TREE_ARITY); component ballotHashers2[batchSize]; - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { ballotHashers2[i] = HashLeftRight(); ballotHashers2[i].left <== ballots2[i][BALLOT_NONCE_IDX]; ballotHashers2[i].right <== ballots2[i][BALLOT_VO_ROOT_IDX]; @@ -129,8 +129,8 @@ template SubsidyPerBatch ( } ballotTreeVerifier2.index <== inputHasher.cbi; ballotTreeVerifier2.root <== ballotRoot; - for (var i = 0; i < k; i ++) { - for (var j = 0; j < TREE_ARITY - 1; j ++) { + for (var i = 0; i < k; i++) { + for (var j = 0; j < TREE_ARITY - 1; j++) { ballotTreeVerifier2.pathElements[i][j] <== ballotPathElements2[i][j]; } } @@ -139,17 +139,17 @@ template SubsidyPerBatch ( // ----------------------------------------------------------------------- // Verify the vote option roots component voteTree1[batchSize]; - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { voteTree1[i] = QuinCheckRoot(voteOptionTreeDepth); - for (var j = 0; j < TREE_ARITY ** voteOptionTreeDepth; j ++) { + for (var j = 0; j < TREE_ARITY ** voteOptionTreeDepth; j++) { voteTree1[i].leaves[j] <== votes1[i][j]; } voteTree1[i].root === ballots1[i][BALLOT_VO_ROOT_IDX]; } component voteTree2[batchSize]; - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { voteTree2[i] = QuinCheckRoot(voteOptionTreeDepth); - for (var j = 0; j < TREE_ARITY ** voteOptionTreeDepth; j ++) { + for (var j = 0; j < TREE_ARITY ** voteOptionTreeDepth; j++) { voteTree2[i].leaves[j] <== votes2[i][j]; } voteTree2[i].root === ballots2[i][BALLOT_VO_ROOT_IDX]; @@ -162,8 +162,8 @@ template SubsidyPerBatch ( component tmp[batchSize*batchSize]; component kij[batchSize*batchSize]; var idx = 0; - for (var i = 0; i < batchSize; i ++) { - for (var j = 0; j < batchSize; j ++) { + for (var i = 0; i < batchSize; i++) { + for (var j = 0; j < batchSize; j++) { tmp[idx] = CalculateTotal(numVoteOptions); kij[idx] = DivisionFromNormal(WW,NN); for (var p = 0; p < numVoteOptions; p++) { @@ -189,7 +189,7 @@ template SubsidyPerBatch ( component subsidy1[numVoteOptions]; component subsidy2[numVoteOptions]; - for (var p = 0; p < numVoteOptions; p ++) { + for (var p = 0; p < numVoteOptions; p++) { subsidy1[p] = CalculateTotal(batchSize * (batchSize-1)/2 + 1); subsidy2[p] = CalculateTotal(batchSize * (batchSize+1)/2); @@ -197,7 +197,7 @@ template SubsidyPerBatch ( var cnt1 = 0; subsidy1[p].nums[batchSize * (batchSize-1)/2] <== currentSubsidy[p] * iz.out; for (var i = 0; i < batchSize; i++) { - for (var j = i+1; j < batchSize; j ++) { + for (var j = i+1; j < batchSize; j++) { var idx = i * batchSize + j; subsidy1[p].nums[cnt1] <== 2 * kij[idx].c * vsquares[idx][p]; cnt1++; @@ -224,7 +224,7 @@ template SubsidyPerBatch ( rcv.currentSubsidySalt <== currentSubsidySalt; rcv.newSubsidySalt <== newSubsidySalt; - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { rcv.currentSubsidy[i] <== currentSubsidy[i]; rcv.newSubsidy[i] <== subsidy1[i].sum + subsidy2[i].sum; } @@ -259,7 +259,7 @@ template SubsidyCommitmentVerifier(voteOptionTreeDepth) { // Compute the commitment to the current results component currentResultsRoot = QuinCheckRoot(voteOptionTreeDepth); - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { currentResultsRoot.leaves[i] <== currentSubsidy[i]; } @@ -284,7 +284,7 @@ template SubsidyCommitmentVerifier(voteOptionTreeDepth) { // Compute the root of the new results component newResultsRoot = QuinCheckRoot(voteOptionTreeDepth); - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { newResultsRoot.leaves[i] <== newSubsidy[i]; } @@ -331,7 +331,7 @@ template BatchMerkleTreeVerifier(coeffTreeDepth, intCoeffTreeDepth, TREE_ARITY) signal input root; signal input index; component ballotSubroot = QuinCheckRoot(intCoeffTreeDepth); - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { ballotSubroot.leaves[i] <== leaves[i]; } @@ -340,9 +340,9 @@ template BatchMerkleTreeVerifier(coeffTreeDepth, intCoeffTreeDepth, TREE_ARITY) upperTreePathIndices.in <== index; ballotQle.leaf <== ballotSubroot.root; ballotQle.root <== root; - for (var i = 0; i < k; i ++) { + for (var i = 0; i < k; i++) { ballotQle.path_index[i] <== upperTreePathIndices.out[i]; - for (var j = 0; j < TREE_ARITY - 1; j ++) { + for (var j = 0; j < TREE_ARITY - 1; j++) { ballotQle.path_elements[i][j] <== pathElements[i][j]; } } diff --git a/circuits/circom/tallyVotes.circom b/circuits/circom/tallyVotes.circom index 48b657c908..3e0b8d570d 100644 --- a/circuits/circom/tallyVotes.circom +++ b/circuits/circom/tallyVotes.circom @@ -104,7 +104,7 @@ template TallyVotes( // Hash each ballot and generate the subroot of the ballots component ballotSubroot = QuinCheckRoot(intStateTreeDepth); component ballotHashers[batchSize]; - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { ballotHashers[i] = HashLeftRight(); ballotHashers[i].left <== ballots[i][BALLOT_NONCE_IDX]; ballotHashers[i].right <== ballots[i][BALLOT_VO_ROOT_IDX]; @@ -117,9 +117,9 @@ template TallyVotes( ballotPathIndices.in <== inputHasher.batchNum; ballotQle.leaf <== ballotSubroot.root; ballotQle.root <== ballotRoot; - for (var i = 0; i < k; i ++) { + for (var i = 0; i < k; i++) { ballotQle.path_index[i] <== ballotPathIndices.out[i]; - for (var j = 0; j < TREE_ARITY - 1; j ++) { + for (var j = 0; j < TREE_ARITY - 1; j++) { ballotQle.path_elements[i][j] <== ballotPathElements[i][j]; } } @@ -127,9 +127,9 @@ template TallyVotes( // ----------------------------------------------------------------------- // Verify the vote option roots component voteTree[batchSize]; - for (var i = 0; i < batchSize; i ++) { + for (var i = 0; i < batchSize; i++) { voteTree[i] = QuinCheckRoot(voteOptionTreeDepth); - for (var j = 0; j < TREE_ARITY ** voteOptionTreeDepth; j ++) { + for (var j = 0; j < TREE_ARITY ** voteOptionTreeDepth; j++) { voteTree[i].leaves[j] <== votes[i][j]; } voteTree[i].root === ballots[i][BALLOT_VO_ROOT_IDX]; @@ -144,10 +144,10 @@ template TallyVotes( // ----------------------------------------------------------------------- // Tally the new results component resultCalc[numVoteOptions]; - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { resultCalc[i] = CalculateTotal(batchSize + 1); resultCalc[i].nums[batchSize] <== currentResults[i] * iz.out; - for (var j = 0; j < batchSize; j ++) { + for (var j = 0; j < batchSize; j++) { resultCalc[i].nums[j] <== votes[j][i]; } } @@ -155,8 +155,8 @@ template TallyVotes( // Tally the new total of spent voice credits component newSpentVoiceCreditSubtotal = CalculateTotal(batchSize * numVoteOptions + 1); newSpentVoiceCreditSubtotal.nums[batchSize * numVoteOptions] <== currentSpentVoiceCreditSubtotal * iz.out; - for (var i = 0; i < batchSize; i ++) { - for (var j = 0; j < numVoteOptions; j ++) { + for (var i = 0; i < batchSize; i++) { + for (var j = 0; j < numVoteOptions; j++) { newSpentVoiceCreditSubtotal.nums[i * numVoteOptions + j] <== votes[i][j] * votes[i][j]; } @@ -164,10 +164,10 @@ template TallyVotes( // Tally the spent voice credits per vote option component newPerVOSpentVoiceCredits[numVoteOptions]; - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { newPerVOSpentVoiceCredits[i] = CalculateTotal(batchSize + 1); newPerVOSpentVoiceCredits[i].nums[batchSize] <== currentPerVOSpentVoiceCredits[i] * iz.out; - for (var j = 0; j < batchSize; j ++) { + for (var j = 0; j < batchSize; j++) { newPerVOSpentVoiceCredits[i].nums[j] <== votes[j][i] * votes[j][i]; } } @@ -186,7 +186,7 @@ template TallyVotes( rcv.currentPerVOSpentVoiceCreditsRootSalt <== currentPerVOSpentVoiceCreditsRootSalt; rcv.newPerVOSpentVoiceCreditsRootSalt <== newPerVOSpentVoiceCreditsRootSalt; - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { rcv.currentResults[i] <== currentResults[i]; rcv.newResults[i] <== resultCalc[i].sum; rcv.currentPerVOSpentVoiceCredits[i] <== currentPerVOSpentVoiceCredits[i]; @@ -228,7 +228,7 @@ template ResultCommitmentVerifier(voteOptionTreeDepth) { // Compute the commitment to the current results component currentResultsRoot = QuinCheckRoot(voteOptionTreeDepth); - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { currentResultsRoot.leaves[i] <== currentResults[i]; } @@ -243,7 +243,7 @@ template ResultCommitmentVerifier(voteOptionTreeDepth) { // Compute the root of the spent voice credits per vote option component currentPerVOSpentVoiceCreditsRoot = QuinCheckRoot(voteOptionTreeDepth); - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { currentPerVOSpentVoiceCreditsRoot.leaves[i] <== currentPerVOSpentVoiceCredits[i]; } @@ -276,7 +276,7 @@ template ResultCommitmentVerifier(voteOptionTreeDepth) { // Compute the root of the new results component newResultsRoot = QuinCheckRoot(voteOptionTreeDepth); - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { newResultsRoot.leaves[i] <== newResults[i]; } @@ -291,7 +291,7 @@ template ResultCommitmentVerifier(voteOptionTreeDepth) { // Compute the root of the spent voice credits per vote option component newPerVOSpentVoiceCreditsRoot = QuinCheckRoot(voteOptionTreeDepth); - for (var i = 0; i < numVoteOptions; i ++) { + for (var i = 0; i < numVoteOptions; i++) { newPerVOSpentVoiceCreditsRoot.leaves[i] <== newPerVOSpentVoiceCredits[i]; } diff --git a/circuits/circom/tallyVotesNonQv.circom b/circuits/circom/tallyVotesNonQv.circom new file mode 100644 index 0000000000..58a4fff882 --- /dev/null +++ b/circuits/circom/tallyVotesNonQv.circom @@ -0,0 +1,196 @@ +pragma circom 2.0.0; + +// circomlib import +include "./comparators.circom"; + +// local imports +include "./trees/incrementalQuinTree.circom"; +include "./trees/calculateTotal.circom"; +include "./trees/checkRoot.circom"; +include "./hasherSha256.circom"; +include "./hasherPoseidon.circom"; +include "./unpackElement.circom"; +include "./tallyVotes.circom"; + +// Tally votes in the ballots, batch by batch. +template TallyVotesNonQv( + stateTreeDepth, + intStateTreeDepth, + voteOptionTreeDepth +) { + assert(voteOptionTreeDepth > 0); + assert(intStateTreeDepth > 0); + assert(intStateTreeDepth < stateTreeDepth); + + var TREE_ARITY = 5; + + // The number of ballots in this batch + var batchSize = TREE_ARITY ** intStateTreeDepth; + var numVoteOptions = TREE_ARITY ** voteOptionTreeDepth; + + var BALLOT_LENGTH = 2; + var BALLOT_NONCE_IDX = 0; + var BALLOT_VO_ROOT_IDX = 1; + + signal input stateRoot; + signal input ballotRoot; + signal input sbSalt; + + // The only public input (inputHash) is the hash of the following: + signal input packedVals; + signal input sbCommitment; + signal input currentTallyCommitment; + signal input newTallyCommitment; + + // A tally commitment is the hash of the following salted values: + // - the vote results + // - the number of voice credits spent per vote option + // - the total number of spent voice credits + + signal input inputHash; + + var k = stateTreeDepth - intStateTreeDepth; + // The ballots + signal input ballots[batchSize][BALLOT_LENGTH]; + signal input ballotPathElements[k][TREE_ARITY - 1]; + signal input votes[batchSize][numVoteOptions]; + + signal input currentResults[numVoteOptions]; + signal input currentResultsRootSalt; + + signal input currentSpentVoiceCreditSubtotal; + signal input currentSpentVoiceCreditSubtotalSalt; + + signal input currentPerVOSpentVoiceCredits[numVoteOptions]; + signal input currentPerVOSpentVoiceCreditsRootSalt; + + signal input newResultsRootSalt; + signal input newPerVOSpentVoiceCreditsRootSalt; + signal input newSpentVoiceCreditSubtotalSalt; + + // ----------------------------------------------------------------------- + // Verify sbCommitment + component sbCommitmentHasher = Hasher3(); + sbCommitmentHasher.in[0] <== stateRoot; + sbCommitmentHasher.in[1] <== ballotRoot; + sbCommitmentHasher.in[2] <== sbSalt; + sbCommitmentHasher.hash === sbCommitment; + + // ----------------------------------------------------------------------- + // Verify inputHash + component inputHasher = TallyVotesInputHasher(); + inputHasher.sbCommitment <== sbCommitment; + inputHasher.currentTallyCommitment <== currentTallyCommitment; + inputHasher.newTallyCommitment <== newTallyCommitment; + inputHasher.packedVals <== packedVals; + inputHasher.hash === inputHash; + + signal numSignUps; + signal batchStartIndex; + + numSignUps <== inputHasher.numSignUps; + batchStartIndex <== inputHasher.batchNum * batchSize; + + // ----------------------------------------------------------------------- + // Validate batchStartIndex and numSignUps + // batchStartIndex should be less than numSignUps + component validNumSignups = LessEqThan(50); + validNumSignups.in[0] <== batchStartIndex; + validNumSignups.in[1] <== numSignUps; + validNumSignups.out === 1; + + // ----------------------------------------------------------------------- + // Verify the ballots + + // Hash each ballot and generate the subroot of the ballots + component ballotSubroot = QuinCheckRoot(intStateTreeDepth); + component ballotHashers[batchSize]; + for (var i = 0; i < batchSize; i++) { + ballotHashers[i] = HashLeftRight(); + ballotHashers[i].left <== ballots[i][BALLOT_NONCE_IDX]; + ballotHashers[i].right <== ballots[i][BALLOT_VO_ROOT_IDX]; + + ballotSubroot.leaves[i] <== ballotHashers[i].hash; + } + + component ballotQle = QuinLeafExists(k); + component ballotPathIndices = QuinGeneratePathIndices(k); + ballotPathIndices.in <== inputHasher.batchNum; + ballotQle.leaf <== ballotSubroot.root; + ballotQle.root <== ballotRoot; + for (var i = 0; i < k; i++) { + ballotQle.path_index[i] <== ballotPathIndices.out[i]; + for (var j = 0; j < TREE_ARITY - 1; j++) { + ballotQle.path_elements[i][j] <== ballotPathElements[i][j]; + } + } + + // ----------------------------------------------------------------------- + // Verify the vote option roots + component voteTree[batchSize]; + for (var i = 0; i < batchSize; i++) { + voteTree[i] = QuinCheckRoot(voteOptionTreeDepth); + for (var j = 0; j < TREE_ARITY ** voteOptionTreeDepth; j++) { + voteTree[i].leaves[j] <== votes[i][j]; + } + voteTree[i].root === ballots[i][BALLOT_VO_ROOT_IDX]; + } + + component isFirstBatch = IsZero(); + isFirstBatch.in <== batchStartIndex; + + component iz = IsZero(); + iz.in <== isFirstBatch.out; + + // ----------------------------------------------------------------------- + // Tally the new results + component resultCalc[numVoteOptions]; + for (var i = 0; i < numVoteOptions; i++) { + resultCalc[i] = CalculateTotal(batchSize + 1); + resultCalc[i].nums[batchSize] <== currentResults[i] * iz.out; + for (var j = 0; j < batchSize; j++) { + resultCalc[i].nums[j] <== votes[j][i]; + } + } + + // Tally the new total of spent voice credits + component newSpentVoiceCreditSubtotal = CalculateTotal(batchSize * numVoteOptions + 1); + newSpentVoiceCreditSubtotal.nums[batchSize * numVoteOptions] <== currentSpentVoiceCreditSubtotal * iz.out; + for (var i = 0; i < batchSize; i++) { + for (var j = 0; j < numVoteOptions; j++) { + newSpentVoiceCreditSubtotal.nums[i * numVoteOptions + j] <== + votes[i][j]; + } + } + + // Tally the spent voice credits per vote option + component newPerVOSpentVoiceCredits[numVoteOptions]; + for (var i = 0; i < numVoteOptions; i++) { + newPerVOSpentVoiceCredits[i] = CalculateTotal(batchSize + 1); + newPerVOSpentVoiceCredits[i].nums[batchSize] <== currentPerVOSpentVoiceCredits[i] * iz.out; + for (var j = 0; j < batchSize; j++) { + newPerVOSpentVoiceCredits[i].nums[j] <== votes[j][i]; + } + } + + // Verify the current and new tally + component rcv = ResultCommitmentVerifier(voteOptionTreeDepth); + rcv.isFirstBatch <== isFirstBatch.out; + rcv.currentTallyCommitment <== currentTallyCommitment; + rcv.newTallyCommitment <== newTallyCommitment; + rcv.currentResultsRootSalt <== currentResultsRootSalt; + rcv.newResultsRootSalt <== newResultsRootSalt; + rcv.currentSpentVoiceCreditSubtotal <== currentSpentVoiceCreditSubtotal; + rcv.currentSpentVoiceCreditSubtotalSalt <== currentSpentVoiceCreditSubtotalSalt; + rcv.newSpentVoiceCreditSubtotal <== newSpentVoiceCreditSubtotal.sum; + rcv.newSpentVoiceCreditSubtotalSalt <== newSpentVoiceCreditSubtotalSalt; + rcv.currentPerVOSpentVoiceCreditsRootSalt <== currentPerVOSpentVoiceCreditsRootSalt; + rcv.newPerVOSpentVoiceCreditsRootSalt <== newPerVOSpentVoiceCreditsRootSalt; + + for (var i = 0; i < numVoteOptions; i++) { + rcv.currentResults[i] <== currentResults[i]; + rcv.newResults[i] <== resultCalc[i].sum; + rcv.currentPerVOSpentVoiceCredits[i] <== currentPerVOSpentVoiceCredits[i]; + rcv.newPerVOSpentVoiceCredits[i] <== newPerVOSpentVoiceCredits[i].sum; + } +} diff --git a/circuits/circom/trees/checkRoot.circom b/circuits/circom/trees/checkRoot.circom index d11d640b89..d203851ce5 100644 --- a/circuits/circom/trees/checkRoot.circom +++ b/circuits/circom/trees/checkRoot.circom @@ -27,31 +27,31 @@ template QuinCheckRoot(levels) { // The total number of hashers var numHashers = 0; - for (i = 0; i < levels; i ++) { + for (i = 0; i < levels; i++) { numHashers += LEAVES_PER_NODE ** i; } component hashers[numHashers]; // Instantiate all hashers - for (i = 0; i < numHashers; i ++) { + for (i = 0; i < numHashers; i++) { hashers[i] = Hasher5(); } // Wire the leaf values into the leaf hashers - for (i = 0; i < numLeafHashers; i ++){ - for (j = 0; j < LEAVES_PER_NODE; j ++){ + for (i = 0; i < numLeafHashers; i++){ + for (j = 0; j < LEAVES_PER_NODE; j++){ hashers[i].in[j] <== leaves[i * LEAVES_PER_NODE + j]; } } // Wire the outputs of the leaf hashers to the intermediate hasher inputs var k = 0; - for (i = numLeafHashers; i < numHashers; i ++) { - for (j = 0; j < LEAVES_PER_NODE; j ++){ + for (i = numLeafHashers; i < numHashers; i++) { + for (j = 0; j < LEAVES_PER_NODE; j++){ hashers[i].in[j] <== hashers[k * LEAVES_PER_NODE + j].hash; } - k ++; + k++; } // Wire the output of the final hash to this circuit's output diff --git a/circuits/circom/trees/incrementalQuinTree.circom b/circuits/circom/trees/incrementalQuinTree.circom index 606048efa0..434cf9f7ad 100644 --- a/circuits/circom/trees/incrementalQuinTree.circom +++ b/circuits/circom/trees/incrementalQuinTree.circom @@ -45,7 +45,7 @@ template QuinSelector(choices) { component eqs[choices]; // For each item, check whether its index equals the input index. - for (var i = 0; i < choices; i ++) { + for (var i = 0; i < choices; i++) { eqs[i] = IsEqual(); eqs[i].in[0] <== i; eqs[i].in[1] <== index; @@ -110,7 +110,7 @@ template Splicer(numItems) { The output signal from the QuinSelector is and gets wired to Mux1 (as above). */ - for (i = 0; i < numItems + 1; i ++) { + for (i = 0; i < numItems + 1; i++) { // greaterThen[i].out will be 1 if the i is greater than the index greaterThan[i] = SafeGreaterThan(3); greaterThan[i].in[0] <== i; @@ -123,7 +123,7 @@ template Splicer(numItems) { // but if index = 2 and i = 3, greaterThan[i].out = 1, so 3 - 1 = 2 quinSelectors[i].index <== i - greaterThan[i].out; - for (j = 0; j < numItems; j ++) { + for (j = 0; j < numItems; j++) { quinSelectors[i].in[j] <== in[j]; } quinSelectors[i].in[numItems] <== 0; @@ -178,12 +178,12 @@ template QuinTreeInclusionProof(levels) { splicers[i] = Splicer(LEAVES_PER_PATH_LEVEL); splicers[i].index <== path_index[i]; splicers[i].leaf <== hashers[i - 1].hash; - for (j = 0; j < LEAVES_PER_PATH_LEVEL; j ++) { + for (j = 0; j < LEAVES_PER_PATH_LEVEL; j++) { splicers[i].in[j] <== path_elements[i][j]; } hashers[i] = Hasher5(); - for (j = 0; j < LEAVES_PER_NODE; j ++) { + for (j = 0; j < LEAVES_PER_NODE; j++) { hashers[i].in[j] <== splicers[i].out[j]; } } @@ -207,9 +207,9 @@ template QuinLeafExists(levels){ // Verify the Merkle path component verifier = QuinTreeInclusionProof(levels); verifier.leaf <== leaf; - for (i = 0; i < levels; i ++) { + for (i = 0; i < levels; i++) { verifier.path_index[i] <== path_index[i]; - for (j = 0; j < LEAVES_PER_PATH_LEVEL; j ++) { + for (j = 0; j < LEAVES_PER_PATH_LEVEL; j++) { verifier.path_elements[i][j] <== path_elements[i][j]; } } @@ -239,7 +239,7 @@ template QuinBatchLeavesExists(levels, batchLevels) { // Compute the subroot component qcr = QuinCheckRoot(batchLevels); - for (var i = 0; i < LEAVES_PER_BATCH; i ++) { + for (var i = 0; i < LEAVES_PER_BATCH; i++) { qcr.leaves[i] <== leaves[i]; } @@ -249,9 +249,9 @@ template QuinBatchLeavesExists(levels, batchLevels) { // The subroot is the leaf qle.leaf <== qcr.root; qle.root <== root; - for (var i = 0; i < levels - batchLevels; i ++) { + for (var i = 0; i < levels - batchLevels; i++) { qle.path_index[i] <== path_index[i]; - for (var j = 0; j < LEAVES_PER_PATH_LEVEL; j ++) { + for (var j = 0; j < LEAVES_PER_PATH_LEVEL; j++) { qle.path_elements[i][j] <== path_elements[i][j]; } } @@ -268,7 +268,7 @@ template QuinGeneratePathIndices(levels) { var m = in; signal n[levels + 1]; - for (var i = 0; i < levels; i ++) { + for (var i = 0; i < levels; i++) { // circom's best practices state that we should avoid using <-- unless // we know what we are doing. But this is the only way to perform the // modulo operation. @@ -284,7 +284,7 @@ template QuinGeneratePathIndices(levels) { component leq[levels]; component sum = CalculateTotal(levels); - for (var i = 0; i < levels; i ++) { + for (var i = 0; i < levels; i++) { // Check that each output element is less than the base leq[i] = SafeLessThan(3); leq[i].in[0] <== out[i]; diff --git a/circuits/circom/unpackElement.circom b/circuits/circom/unpackElement.circom index a7482ce82a..9f3d41bd57 100644 --- a/circuits/circom/unpackElement.circom +++ b/circuits/circom/unpackElement.circom @@ -16,9 +16,9 @@ template UnpackElement(n) { inputBits.in <== in; component outputElements[n]; - for (var i = 0; i < n; i ++) { + for (var i = 0; i < n; i++) { outputElements[i] = Bits2Num(50); - for (var j = 0; j < 50; j ++) { + for (var j = 0; j < 50; j++) { outputElements[i].in[j] <== inputBits.out[((n - i - 1) * 50) + j]; } out[i] <== outputElements[i].out; diff --git a/circuits/ts/__tests__/ProcessMessages.test.ts b/circuits/ts/__tests__/ProcessMessages.test.ts index 06e18a604a..6959fe5a0b 100644 --- a/circuits/ts/__tests__/ProcessMessages.test.ts +++ b/circuits/ts/__tests__/ProcessMessages.test.ts @@ -21,31 +21,33 @@ describe("ProcessMessage circuit", function test() { const coordinatorKeypair = new Keypair(); - let circuit: WitnessTester< - [ - "inputHash", - "packedVals", - "pollEndTimestamp", - "msgRoot", - "msgs", - "msgSubrootPathElements", - "coordPrivKey", - "coordPubKey", - "encPubKeys", - "currentStateRoot", - "currentStateLeaves", - "currentStateLeavesPathElements", - "currentSbCommitment", - "currentSbSalt", - "newSbCommitment", - "newSbSalt", - "currentBallotRoot", - "currentBallots", - "currentBallotsPathElements", - "currentVoteWeights", - "currentVoteWeightsPathElements", - ] - >; + type ProcessMessageCircuitInputs = [ + "inputHash", + "packedVals", + "pollEndTimestamp", + "msgRoot", + "msgs", + "msgSubrootPathElements", + "coordPrivKey", + "coordPubKey", + "encPubKeys", + "currentStateRoot", + "currentStateLeaves", + "currentStateLeavesPathElements", + "currentSbCommitment", + "currentSbSalt", + "newSbCommitment", + "newSbSalt", + "currentBallotRoot", + "currentBallots", + "currentBallotsPathElements", + "currentVoteWeights", + "currentVoteWeightsPathElements", + ]; + + let circuit: WitnessTester; + + let circuitNonQv: WitnessTester; let hasherCircuit: WitnessTester< ["packedVals", "coordPubKey", "msgRoot", "currentSbCommitment", "newSbCommitment", "pollEndTimestamp"], @@ -59,6 +61,12 @@ describe("ProcessMessage circuit", function test() { params: [10, 2, 1, 2], }); + circuitNonQv = await circomkitInstance.WitnessTester("processMessagesNonQv", { + file: "processMessagesNonQv", + template: "ProcessMessagesNonQv", + params: [10, 2, 1, 2], + }); + hasherCircuit = await circomkitInstance.WitnessTester("processMessageInputHasher", { file: "processMessages", template: "ProcessMessagesInputHasher", @@ -199,6 +207,126 @@ describe("ProcessMessage circuit", function test() { }); }); + describe("1 user, 2 messages (non-quadratic voting)", () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + const voteWeight = BigInt(9); + const voteOptionIndex = BigInt(0); + let stateIndex: bigint; + let pollId: bigint; + let poll: Poll; + const messages: Message[] = []; + const commands: PCommand[] = []; + + 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))), + ); + + pollId = maciState.deployPoll( + BigInt(Math.floor(Date.now() / 1000) + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinatorKeypair, + ); + + poll = maciState.polls.get(pollId)!; + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // First command (valid) + const command = new PCommand( + stateIndex, // BigInt(1), + userKeypair.pubKey, + voteOptionIndex, // voteOptionIndex, + voteWeight, // vote weight + BigInt(2), // nonce + BigInt(pollId), + ); + + const signature = command.sign(userKeypair.privKey); + + const ecdhKeypair = new Keypair(); + const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); + const message = command.encrypt(signature, sharedKey); + messages.push(message); + commands.push(command); + + poll.publishMessage(message, ecdhKeypair.pubKey); + + // 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); + }); + + 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; + + const inputs = poll.processMessages(pollId, false) as unknown as IProcessMessagesInputs; + + // Calculate the witness + const witness = await circuitNonQv.calculateWitness(inputs); + await circuitNonQv.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, + 2, + ); + + // 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()); + }); + }); + describe("2 users, 1 message", () => { const maciState = new MaciState(STATE_TREE_DEPTH); let pollId: bigint; diff --git a/circuits/ts/__tests__/TallyVotes.test.ts b/circuits/ts/__tests__/TallyVotes.test.ts index e210e200bb..a4608bcbd9 100644 --- a/circuits/ts/__tests__/TallyVotes.test.ts +++ b/circuits/ts/__tests__/TallyVotes.test.ts @@ -21,30 +21,32 @@ describe("TallyVotes circuit", function test() { const coordinatorKeypair = new Keypair(); - let circuit: WitnessTester< - [ - "stateRoot", - "ballotRoot", - "sbSalt", - "packedVals", - "sbCommitment", - "currentTallyCommitment", - "newTallyCommitment", - "inputHash", - "ballots", - "ballotPathElements", - "votes", - "currentResults", - "currentResultsRootSalt", - "currentSpentVoiceCreditSubtotal", - "currentSpentVoiceCreditSubtotalSalt", - "currentPerVOSpentVoiceCredits", - "currentPerVOSpentVoiceCreditsRootSalt", - "newResultsRootSalt", - "newPerVOSpentVoiceCreditsRootSalt", - "newSpentVoiceCreditSubtotalSalt", - ] - >; + type TallyVotesCircuitInputs = [ + "stateRoot", + "ballotRoot", + "sbSalt", + "packedVals", + "sbCommitment", + "currentTallyCommitment", + "newTallyCommitment", + "inputHash", + "ballots", + "ballotPathElements", + "votes", + "currentResults", + "currentResultsRootSalt", + "currentSpentVoiceCreditSubtotal", + "currentSpentVoiceCreditSubtotalSalt", + "currentPerVOSpentVoiceCredits", + "currentPerVOSpentVoiceCreditsRootSalt", + "newResultsRootSalt", + "newPerVOSpentVoiceCreditsRootSalt", + "newSpentVoiceCreditSubtotalSalt", + ]; + + let circuit: WitnessTester; + + let circuitNonQv: WitnessTester; before(async () => { circuit = await circomkitInstance.WitnessTester("tallyVotes", { @@ -52,6 +54,12 @@ describe("TallyVotes circuit", function test() { template: "TallyVotes", params: [10, 1, 2], }); + + circuitNonQv = await circomkitInstance.WitnessTester("tallyVotesNonQv", { + file: "tallyVotesNonQv", + template: "TallyVotesNonQv", + params: [10, 1, 2], + }); }); describe("1 user, 2 messages", () => { @@ -140,6 +148,92 @@ describe("TallyVotes circuit", function test() { }); }); + describe("1 user, 2 messages (non qv)", () => { + let stateIndex: bigint; + let pollId: bigint; + let poll: Poll; + let maciState: MaciState; + const voteWeight = BigInt(9); + const voteOptionIndex = BigInt(0); + + beforeEach(() => { + maciState = new MaciState(STATE_TREE_DEPTH); + const messages: Message[] = []; + const commands: PCommand[] = []; + // Sign up and publish + const userKeypair = new Keypair(); + stateIndex = BigInt( + maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))), + ); + + pollId = maciState.deployPoll( + BigInt(Math.floor(Date.now() / 1000) + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinatorKeypair, + ); + + poll = maciState.polls.get(pollId)!; + poll.updatePoll(stateIndex); + + // First command (valid) + const command = new PCommand( + stateIndex, + userKeypair.pubKey, + voteOptionIndex, // voteOptionIndex, + voteWeight, // vote weight + BigInt(1), // nonce + BigInt(pollId), + ); + + const signature = command.sign(userKeypair.privKey); + + const ecdhKeypair = new Keypair(); + const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); + const message = command.encrypt(signature, sharedKey); + messages.push(message); + commands.push(command); + + poll.publishMessage(message, ecdhKeypair.pubKey); + // 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); + }); + + it("should produce the correct result commitments", async () => { + const generatedInputs = poll.tallyVotes() as unknown as ITallyVotesInputs; + const witness = await circuit.calculateWitness(generatedInputs); + await circuit.expectConstraintPass(witness); + }); + + it("should produce the correct result if the inital tally is not zero", async () => { + const generatedInputs = poll.tallyVotes(false) as unknown as ITallyVotesInputs; + + // Start the tally from non-zero value + let randIdx = generateRandomIndex(Object.keys(generatedInputs).length); + while (randIdx === 0) { + randIdx = generateRandomIndex(Object.keys(generatedInputs).length); + } + + generatedInputs.currentResults[randIdx] = 1n; + const witness = await circuitNonQv.calculateWitness(generatedInputs); + await circuitNonQv.expectConstraintPass(witness); + }); + }); + const NUM_BATCHES = 2; const x = messageBatchSize * NUM_BATCHES; diff --git a/cli/package.json b/cli/package.json index 6b3d2ecb35..5f0395a693 100644 --- a/cli/package.json +++ b/cli/package.json @@ -19,6 +19,7 @@ "test": "nyc ts-mocha --exit tests/e2e/*.test.ts tests/unit/*.test.ts", "test:ceremony": "ts-mocha --exit tests/ceremony-params/ceremonyParams.test.ts", "test:e2e": "ts-mocha --exit tests/e2e/e2e.test.ts", + "test:e2e-non-qv": "ts-mocha --exit tests/e2e/e2e.nonQv.test.ts", "test:e2e-subsidy": "ts-mocha --exit tests/e2e/e2e.subsidy.test.ts", "test:keyChange": "ts-mocha --exit tests/e2e/keyChange.test.ts", "test:unit": "nyc ts-mocha --exit tests/unit/*.test.ts", diff --git a/cli/tests/constants.ts b/cli/tests/constants.ts index f09d872062..65ab04eff0 100644 --- a/cli/tests/constants.ts +++ b/cli/tests/constants.ts @@ -24,6 +24,9 @@ export const coordinatorPubKey = coordinatorKeypair.pubKey.serialize(); export const coordinatorPrivKey = coordinatorKeypair.privKey.serialize(); export const processMessageTestZkeyPath = "./zkeys/ProcessMessages_10-2-1-2_test/ProcessMessages_10-2-1-2_test.0.zkey"; export const tallyVotesTestZkeyPath = "./zkeys/TallyVotes_10-1-2_test/TallyVotes_10-1-2_test.0.zkey"; +export const processMessageTestNonQvZkeyPath = + "./zkeys/ProcessMessagesNonQv_10-2-1-2_test/ProcessMessagesNonQv_10-2-1-2_test.0.zkey"; +export const tallyVotesTestNonQvZkeyPath = "./zkeys/TallyVotesNonQv_10-1-2_test/TallyVotesNonQv_10-1-2_test.0.zkey"; export const subsidyTestZkeyPath = "./zkeys/SubsidyPerBatch_10-1-2_test/SubsidyPerBatch_10-1-2_test.0.zkey"; export const testTallyFilePath = "./tally.json"; export const testSubsidyFilePath = "./subsidy.json"; @@ -58,6 +61,18 @@ export const ceremonyTallyVotesDatPath = "./zkeys/tallyVotes_6-2-3/tallyVotes_6- export const ceremonyProcessMessagesWasmPath = "./zkeys/processMessages_6-8-2-3/processMessages_6-8-2-3_js/processMessages_6-8-2-3.wasm"; export const ceremonyTallyVotesWasmPath = "./zkeys/tallyVotes_6-2-3/tallyVotes_6-2-3_js/tallyVotes_6-2-3.wasm"; +export const testProcessMessagesNonQvWitnessPath = + "./zkeys/ProcessMessagesNonQv_10-2-1-2_test/ProcessMessagesNonQv_10-2-1-2_test_cpp/ProcessMessagesNonQv_10-2-1-2_test"; +export const testProcessMessagesNonQvWitnessDatPath = + "./zkeys/ProcessMessagesNonQv_10-2-1-2_test/ProcessMessagesNonQv_10-2-1-2_test_cpp/ProcessMessagesNonQv_10-2-1-2_test.dat"; +export const testTallyVotesNonQvWitnessPath = + "./zkeys/TallyVotesNonQv_10-1-2_test/TallyVotesNonQv_10-1-2_test_cpp/TallyVotesNonQv_10-1-2_test"; +export const testTallyVotesNonQvWitnessDatPath = + "./zkeys/TallyVotesNonQv_10-1-2_test/TallyVotesNonQv_10-1-2_test_cpp/TallyVotesNonQv_10-1-2_test.dat"; +export const testProcessMessagesNonQvWasmPath = + "./zkeys/ProcessMessagesNonQv_10-2-1-2_test/ProcessMessagesNonQv_10-2-1-2_test_js/ProcessMessagesNonQv_10-2-1-2_test.wasm"; +export const testTallyVotesNonQvWasmPath = + "./zkeys/TallyVotesNonQv_10-1-2_test/TallyVotesNonQv_10-1-2_test_js/TallyVotesNonQv_10-1-2_test.wasm"; export const pollDuration = 90; export const maxMessages = 25; @@ -74,6 +89,17 @@ export const setVerifyingKeysArgs: SetVerifyingKeysArgs = { tallyVotesZkeyPath: tallyVotesTestZkeyPath, }; +export const setVerifyingKeysNonQvArgs: SetVerifyingKeysArgs = { + quiet: true, + stateTreeDepth: STATE_TREE_DEPTH, + intStateTreeDepth: INT_STATE_TREE_DEPTH, + messageTreeDepth: MSG_TREE_DEPTH, + voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH, + messageBatchDepth: MSG_BATCH_DEPTH, + processMessagesZkeyPath: processMessageTestNonQvZkeyPath, + tallyVotesZkeyPath: tallyVotesTestNonQvZkeyPath, +}; + export const checkVerifyingKeysArgs: CheckVerifyingKeysArgs = { stateTreeDepth: STATE_TREE_DEPTH, intStateTreeDepth: INT_STATE_TREE_DEPTH, diff --git a/cli/tests/e2e/e2e.nonQv.test.ts b/cli/tests/e2e/e2e.nonQv.test.ts new file mode 100644 index 0000000000..f2cbc986d7 --- /dev/null +++ b/cli/tests/e2e/e2e.nonQv.test.ts @@ -0,0 +1,129 @@ +import { Signer } from "ethers"; +import { getDefaultSigner } from "maci-contracts"; +import { genRandomSalt } from "maci-crypto"; +import { Keypair } from "maci-domainobjs"; + +import { + deploy, + deployPoll, + deployVkRegistryContract, + genProofs, + mergeMessages, + mergeSignups, + proveOnChain, + publish, + setVerifyingKeys, + signup, + timeTravel, + verify, +} from "../../ts/commands"; +import { DeployedContracts, GenProofsArgs } from "../../ts/utils"; +import { + deployPollArgs, + coordinatorPrivKey, + pollDuration, + proveOnChainArgs, + verifyArgs, + mergeMessagesArgs, + mergeSignupsArgs, + testProofsDirPath, + testRapidsnarkPath, + testTallyFilePath, + deployArgs, + setVerifyingKeysNonQvArgs, + testProcessMessagesNonQvWitnessPath, + testProcessMessagesNonQvWitnessDatPath, + testTallyVotesNonQvWitnessPath, + testTallyVotesNonQvWitnessDatPath, + testProcessMessagesNonQvWasmPath, + testTallyVotesNonQvWasmPath, + processMessageTestNonQvZkeyPath, + tallyVotesTestNonQvZkeyPath, +} from "../constants"; +import { cleanVanilla, isArm } from "../utils"; + +/** + Test scenarios: + 1 signup, 1 message with quadratic voting disabled + */ +describe("e2e tests", function test() { + const useWasm = isArm(); + this.timeout(900000); + + let maciAddresses: DeployedContracts; + + const genProofsArgs: GenProofsArgs = { + outputDir: testProofsDirPath, + tallyFile: testTallyFilePath, + tallyZkey: tallyVotesTestNonQvZkeyPath, + processZkey: processMessageTestNonQvZkeyPath, + pollId: 0n, + rapidsnark: testRapidsnarkPath, + processWitgen: testProcessMessagesNonQvWitnessPath, + processDatFile: testProcessMessagesNonQvWitnessDatPath, + tallyWitgen: testTallyVotesNonQvWitnessPath, + tallyDatFile: testTallyVotesNonQvWitnessDatPath, + coordinatorPrivKey, + processWasm: testProcessMessagesNonQvWasmPath, + tallyWasm: testTallyVotesNonQvWasmPath, + useWasm, + }; + + // before all tests we deploy the vk registry contract and set the verifying keys + before(async () => { + // we deploy the vk registry contract + await deployVkRegistryContract({}); + // we set the verifying keys + await setVerifyingKeys(setVerifyingKeysNonQvArgs); + }); + + describe("1 signup, 1 message (with signer as argument)", () => { + let signer: Signer; + + after(() => { + cleanVanilla(); + }); + + const user = new Keypair(); + + before(async () => { + signer = await getDefaultSigner(); + // deploy the smart contracts + maciAddresses = await deploy({ ...deployArgs, signer }); + // deploy a poll contract + await deployPoll({ ...deployPollArgs, signer }); + }); + + it("should signup one user", async () => { + await signup({ maciPubKey: user.pubKey.serialize(), signer }); + }); + + it("should publish one message", async () => { + await publish({ + pubkey: user.pubKey.serialize(), + stateIndex: 1n, + voteOptionIndex: 0n, + nonce: 1n, + pollId: 0n, + newVoteWeight: 9n, + maciContractAddress: maciAddresses.maciAddress, + salt: genRandomSalt(), + privateKey: user.privKey.serialize(), + signer, + }); + }); + + it("should generate zk-SNARK proofs and verify them", async () => { + await timeTravel({ seconds: pollDuration, signer }); + await mergeMessages({ ...mergeMessagesArgs, signer }); + await mergeSignups({ ...mergeSignupsArgs, signer }); + const tallyFileData = await genProofs({ ...genProofsArgs, signer, useQuadraticVoting: false }); + await proveOnChain({ ...proveOnChainArgs, signer }); + await verify({ + ...verifyArgs, + tallyData: tallyFileData, + signer, + }); + }); + }); +}); diff --git a/cli/ts/commands/genProofs.ts b/cli/ts/commands/genProofs.ts index 875394a0fd..fbd08fec4f 100644 --- a/cli/ts/commands/genProofs.ts +++ b/cli/ts/commands/genProofs.ts @@ -68,6 +68,7 @@ export const genProofs = async ({ blocksPerBatch, endBlock, signer, + useQuadraticVoting = true, quiet = true, }: GenProofsArgs): Promise => { banner(quiet); @@ -283,7 +284,7 @@ export const genProofs = async ({ // while we have unprocessed messages, process them while (poll.hasUnprocessedMessages()) { // process messages in batches - const circuitInputs = poll.processMessages(pollId, quiet) as unknown as CircuitInputs; + const circuitInputs = poll.processMessages(pollId, useQuadraticVoting, quiet) as unknown as CircuitInputs; try { // generate the proof for this batch // eslint-disable-next-line no-await-in-loop @@ -417,7 +418,7 @@ export const genProofs = async ({ // tally all ballots for this poll while (poll.hasUntalliedBallots()) { // tally votes in batches - tallyCircuitInputs = poll.tallyVotes() as unknown as CircuitInputs; + tallyCircuitInputs = poll.tallyVotes(useQuadraticVoting) as unknown as CircuitInputs; try { // generate the proof diff --git a/cli/ts/index.ts b/cli/ts/index.ts index 64af54b244..cbefab4588 100644 --- a/cli/ts/index.ts +++ b/cli/ts/index.ts @@ -466,6 +466,7 @@ program .option("-sb, --start-block ", "the block number to start looking for events from", parseInt) .option("-eb, --end-block ", "the block number to end looking for events from", parseInt) .option("-bb, --blocks-per-batch ", "the number of blocks to process per batch", parseInt) + .option("-uq, --use-quadratic-voting", "whether to use quadratic voting", (value) => value === "true", true) .action(async (cmdObj) => { try { await genProofs({ @@ -494,6 +495,7 @@ program startBlock: cmdObj.startBlock, endBlock: cmdObj.endBlock, blocksPerBatch: cmdObj.blocksPerBatch, + useQuadraticVoting: cmdObj.useQuadraticVoting, quiet: cmdObj.quiet, }); } catch (error) { diff --git a/cli/ts/utils/interfaces.ts b/cli/ts/utils/interfaces.ts index 3ad922cd36..bc05ac070a 100644 --- a/cli/ts/utils/interfaces.ts +++ b/cli/ts/utils/interfaces.ts @@ -557,6 +557,11 @@ export interface GenProofsArgs { * A signer object */ signer?: Signer; + + /** + * Whether to use quadratic voting or not + */ + useQuadraticVoting?: boolean; } /** diff --git a/core/ts/Poll.ts b/core/ts/Poll.ts index afc8d9fe48..412fcfc912 100644 --- a/core/ts/Poll.ts +++ b/core/ts/Poll.ts @@ -222,14 +222,14 @@ export class Poll implements IPoll { * @param encPubKey - The public key associated with the encryption private key. * @returns A number of variables which will be used in the zk-SNARK circuit. */ - processMessage = (message: Message, encPubKey: PubKey): IProcessMessagesOutput => { + processMessage = (message: Message, encPubKey: PubKey, qv = true): IProcessMessagesOutput => { try { // Decrypt the message const sharedKey = Keypair.genEcdhSharedKey(this.coordinatorKeypair.privKey, encPubKey); const { command, signature } = PCommand.decrypt(message, sharedKey); - const stateLeafIndex = BigInt(`${command.stateIndex}`); + const stateLeafIndex = command.stateIndex; // If the state tree index in the command is invalid, do nothing if ( @@ -252,7 +252,7 @@ export class Poll implements IPoll { } // If the nonce is invalid, do nothing - if (command.nonce !== BigInt(`${ballot.nonce}`) + 1n) { + if (command.nonce !== ballot.nonce + 1n) { throw new ProcessMessageError(ProcessMessageErrors.InvalidNonce); } @@ -271,10 +271,13 @@ export class Poll implements IPoll { // basically we are replacing the previous vote weight for this // particular vote option with the new one // but we need to ensure that we are not going >= balance - const voiceCreditsLeft = - BigInt(stateLeaf.voiceCreditBalance) + - BigInt(originalVoteWeight) * BigInt(originalVoteWeight) - - BigInt(command.newVoteWeight) * BigInt(command.newVoteWeight); + // @note that above comment is valid for quadratic voting + // for non quadratic voting, we simply remove the exponentiation + const voiceCreditsLeft = qv + ? stateLeaf.voiceCreditBalance + + originalVoteWeight * originalVoteWeight - + command.newVoteWeight * command.newVoteWeight + : stateLeaf.voiceCreditBalance + originalVoteWeight - command.newVoteWeight; // If the remaining voice credits is insufficient, do nothing if (voiceCreditsLeft < 0n) { @@ -290,7 +293,7 @@ export class Poll implements IPoll { // Deep-copy the ballot and update its attributes const newBallot = ballot.copy(); // increase the nonce - newBallot.nonce = BigInt(`${newBallot.nonce}`) + 1n; + newBallot.nonce += 1n; // we change the vote for this exact vote option newBallot.votes[voteOptionIndex] = command.newVoteWeight; @@ -428,9 +431,10 @@ export class Poll implements IPoll { * poll has concluded. * @param pollId The ID of the poll associated with the messages to * process + * @param quiet - Whether to log errors or not * @returns stringified circuit inputs */ - processMessages = (pollId: bigint, quiet = true): IProcessMessagesCircuitInputs => { + processMessages = (pollId: bigint, qv = true, quiet = true): IProcessMessagesCircuitInputs => { assert(this.hasUnprocessedMessages(), "No more messages to process"); const batchSize = this.batchSizes.messageBatchSize; @@ -518,7 +522,7 @@ export class Poll implements IPoll { case 1n: try { // check if the command is valid - const r = this.processMessage(message, encPubKey); + const r = this.processMessage(message, encPubKey, qv); const index = r.stateLeafIndex!; // we add at position 0 the original data @@ -978,9 +982,10 @@ export class Poll implements IPoll { /** * This method tallies a ballots and updates the tally results. + * @param useQuadraticVoting - Whether to use quadratic voting or not. Default is true. * @returns the circuit inputs for the TallyVotes circuit. */ - tallyVotes = (): ITallyCircuitInputs => { + tallyVotes = (useQuadraticVoting = true): ITallyCircuitInputs => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.sbSalts[this.currentMessageBatchIndex!] === undefined) { throw new Error("You must process the messages first"); @@ -1013,12 +1018,14 @@ export class Poll implements IPoll { const currentPerVOSpentVoiceCreditsCommitment = this.genPerVOSpentVoiceCreditsCommitment( currentPerVOSpentVoiceCreditsRootSalt, batchStartIndex, + useQuadraticVoting, ); // generate a commitment to the current spent voice credits const currentSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( currentSpentVoiceCreditSubtotalSalt, batchStartIndex, + useQuadraticVoting, ); // the current commitment for the first batch will be 0 @@ -1060,10 +1067,10 @@ export class Poll implements IPoll { this.tallyResult[j] += v; // the per vote option spent voice credits will be the sum of the squares of the votes - this.perVOSpentVoiceCredits[j] += v * v; + this.perVOSpentVoiceCredits[j] += useQuadraticVoting ? v * v : v; // the total spent voice credits will be the sum of the squares of the votes - this.totalSpentVoiceCredits += v * v; + this.totalSpentVoiceCredits += useQuadraticVoting ? v * v : v; } } @@ -1095,12 +1102,14 @@ export class Poll implements IPoll { const newSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( newSpentVoiceCreditSubtotalSalt, batchStartIndex + batchSize, + useQuadraticVoting, ); // generate the new per VO spent voice credits commitment with the new salts and data const newPerVOSpentVoiceCreditsCommitment = this.genPerVOSpentVoiceCreditsCommitment( newPerVOSpentVoiceCreditsRootSalt, batchStartIndex + batchSize, + useQuadraticVoting, ); // generate the new tally commitment @@ -1157,9 +1166,14 @@ export class Poll implements IPoll { * This is the hash of the total spent voice credits and a salt, computed as Poseidon([totalCredits, _salt]). * @param salt - The salt used in the hash function. * @param numBallotsToCount - The number of ballots to count for the calculation. + * @param useQuadraticVoting - Whether to use quadratic voting or not. Default is true. * @returns Returns the hash of the total spent voice credits and a salt, computed as Poseidon([totalCredits, _salt]). */ - private genSpentVoiceCreditSubtotalCommitment = (salt: bigint, numBallotsToCount: number): bigint => { + private genSpentVoiceCreditSubtotalCommitment = ( + salt: bigint, + numBallotsToCount: number, + useQuadraticVoting = true, + ): bigint => { let subtotal = 0n; for (let i = 0; i < numBallotsToCount; i += 1) { if (this.ballots.length <= i) { @@ -1168,7 +1182,7 @@ export class Poll implements IPoll { for (let j = 0; j < this.tallyResult.length; j += 1) { const v = BigInt(`${this.ballots[i].votes[j]}`); - subtotal = BigInt(subtotal) + v * v; + subtotal += useQuadraticVoting ? v * v : v; } } return hashLeftRight(subtotal, salt); @@ -1180,10 +1194,14 @@ export class Poll implements IPoll { * This is the hash of the Merkle root of the spent voice credits per vote option and a salt, computed as Poseidon([root, _salt]). * @param salt - The salt used in the hash function. * @param numBallotsToCount - The number of ballots to count for the calculation. - * + * @param useQuadraticVoting - Whether to use quadratic voting or not. Default is true. * @returns Returns the hash of the Merkle root of the spent voice credits per vote option and a salt, computed as Poseidon([root, _salt]). */ - private genPerVOSpentVoiceCreditsCommitment = (salt: bigint, numBallotsToCount: number): bigint => { + private genPerVOSpentVoiceCreditsCommitment = ( + salt: bigint, + numBallotsToCount: number, + useQuadraticVoting = true, + ): bigint => { const leaves: bigint[] = Array(this.tallyResult.length).fill(0n); for (let i = 0; i < numBallotsToCount; i += 1) { @@ -1194,7 +1212,7 @@ export class Poll implements IPoll { for (let j = 0; j < this.tallyResult.length; j += 1) { const v = this.ballots[i].votes[j]; - leaves[j] += v * v; + leaves[j] += useQuadraticVoting ? v * v : v; } } diff --git a/core/ts/__tests__/Poll.test.ts b/core/ts/__tests__/Poll.test.ts index 0835640ff6..83aa6d6978 100644 --- a/core/ts/__tests__/Poll.test.ts +++ b/core/ts/__tests__/Poll.test.ts @@ -191,6 +191,50 @@ describe("Poll", function test() { poll.processMessage(message, user1Keypair.pubKey); }).to.throw("failed decryption due to either wrong encryption public key or corrupted ciphertext"); }); + + it("should throw when going over the voice credit limit (non qv)", () => { + const command = new PCommand( + // invalid state index as it is one more than the number of state leaves + BigInt(user1StateIndex), + user1Keypair.pubKey, + 0n, + voiceCreditBalance + 1n, + 1n, + BigInt(pollId), + ); + + const signature = command.sign(user1Keypair.privKey); + + const ecdhKeypair = new Keypair(); + const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); + + const message = command.encrypt(signature, sharedKey); + poll.publishMessage(message, ecdhKeypair.pubKey); + expect(() => { + poll.processMessage(message, ecdhKeypair.pubKey, false); + }).to.throw("insufficient voice credits"); + }); + + it("should work when submitting a valid message (voteWeight === voiceCreditBalance and non qv)", () => { + const command = new PCommand( + // invalid state index as it is one more than the number of state leaves + BigInt(user1StateIndex), + user1Keypair.pubKey, + 0n, + voiceCreditBalance, + 1n, + BigInt(pollId), + ); + + const signature = command.sign(user1Keypair.privKey); + + const ecdhKeypair = new Keypair(); + const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); + + const message = command.encrypt(signature, sharedKey); + poll.publishMessage(message, ecdhKeypair.pubKey); + poll.processMessage(message, ecdhKeypair.pubKey, false); + }); }); describe("processMessages", () => { @@ -247,7 +291,7 @@ describe("Poll", function test() { ); }); - it("should succeed even if send an invalid message", () => { + it("should succeed even if we send an invalid message", () => { const command = new PCommand( // we only signed up one user so the state index is invalid BigInt(user1StateIndex + 1), @@ -397,6 +441,8 @@ describe("Poll", function test() { const poll = maciState.polls.get(pollId)!; const user1Keypair = new Keypair(); + const user2Keypair = new Keypair(); + // signup the user const user1StateIndex = maciState.signUp( user1Keypair.pubKey, @@ -404,6 +450,12 @@ describe("Poll", function test() { BigInt(Math.floor(Date.now() / 1000)), ); + const user2StateIndex = maciState.signUp( + user2Keypair.pubKey, + voiceCreditBalance, + BigInt(Math.floor(Date.now() / 1000)), + ); + poll.updatePoll(BigInt(maciState.stateLeaves.length)); const voteWeight = 5n; @@ -438,6 +490,54 @@ describe("Poll", function test() { const results = poll.tallyResult; expect(spentVoiceCredits).to.eq(voteWeight * voteWeight); expect(results[Number.parseInt(voteOption.toString(), 10)]).to.eq(voteWeight); + expect(poll.perVOSpentVoiceCredits[Number.parseInt(voteOption.toString(), 10)]).to.eq(voteWeight * voteWeight); + }); + + it("should generate the correct results (non-qv)", () => { + // deploy a second poll + const secondPollId = maciState.deployPoll( + BigInt(Math.floor(Date.now() / 1000) + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinatorKeypair, + ); + + const secondPoll = maciState.polls.get(secondPollId)!; + secondPoll.updatePoll(BigInt(maciState.stateLeaves.length)); + + const secondVoteWeight = 10n; + const secondVoteOption = 1n; + + const secondCommand = new PCommand( + BigInt(user2StateIndex), + user2Keypair.pubKey, + secondVoteOption, + secondVoteWeight, + 1n, + secondPollId, + ); + + const secondSignature = secondCommand.sign(user2Keypair.privKey); + + const secondEcdhKeypair = new Keypair(); + const secondSharedKey = Keypair.genEcdhSharedKey(secondEcdhKeypair.privKey, coordinatorKeypair.pubKey); + + const secondMessage = secondCommand.encrypt(secondSignature, secondSharedKey); + secondPoll.publishMessage(secondMessage, secondEcdhKeypair.pubKey); + + secondPoll.processAllMessages(); + secondPoll.tallyVotes(false); + + const spentVoiceCredits = secondPoll.totalSpentVoiceCredits; + const results = secondPoll.tallyResult; + // spent voice credit is not vote weight * vote weight + expect(spentVoiceCredits).to.eq(secondVoteWeight); + expect(results[Number.parseInt(secondVoteOption.toString(), 10)]).to.eq(secondVoteWeight); + // per VO spent voice credit is not vote weight * vote weight + expect(secondPoll.perVOSpentVoiceCredits[Number.parseInt(secondVoteOption.toString(), 10)]).to.eq( + secondVoteWeight, + ); }); it("should throw when there are no more ballots to tally", () => { diff --git a/core/ts/__tests__/e2e.test.ts b/core/ts/__tests__/e2e.test.ts index 683ef64651..4733064cbc 100644 --- a/core/ts/__tests__/e2e.test.ts +++ b/core/ts/__tests__/e2e.test.ts @@ -604,6 +604,89 @@ describe("MaciState/Poll e2e", function test() { }); }); + describe("Process and tally with non quadratic voting", () => { + let maciState: MaciState; + let pollId: bigint; + let poll: Poll; + let msgTree: IncrementalQuinTree; + let stateTree: IncrementalQuinTree; + const voteWeight = 9n; + const voteOptionIndex = 0n; + let stateIndex: number; + const userKeypair = new Keypair(); + const useQv = false; + + 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); + + pollId = maciState.deployPoll( + BigInt(Math.floor(Date.now() / 1000) + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinatorKeypair, + ); + + poll = maciState.polls.get(pollId)!; + + const timestamp = BigInt(Math.floor(Date.now() / 1000)); + const stateLeaf = new StateLeaf(userKeypair.pubKey, voiceCreditBalance, timestamp); + + stateIndex = maciState.signUp(userKeypair.pubKey, voiceCreditBalance, timestamp); + stateTree.insert(blankStateLeafHash); + stateTree.insert(stateLeaf.hash()); + + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + const command = new PCommand( + BigInt(stateIndex), + userKeypair.pubKey, + voteOptionIndex, + voteWeight, + 1n, + BigInt(pollId), + ); + + const signature = command.sign(userKeypair.privKey); + + const ecdhKeypair = new Keypair(); + const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); + const message = command.encrypt(signature, sharedKey); + + poll.publishMessage(message, ecdhKeypair.pubKey); + msgTree.insert(message.hash(ecdhKeypair.pubKey)); + }); + + it("Process a batch of messages (though only 1 message is in the batch)", () => { + poll.processMessages(pollId, useQv); + + // Check the ballot + expect(poll.ballots[1].votes[Number(voteOptionIndex)].toString()).to.eq(voteWeight.toString()); + // Check the state leaf in the poll + expect(poll.stateLeaves[1].voiceCreditBalance.toString()).to.eq((voiceCreditBalance - voteWeight).toString()); + }); + + it("Tally ballots", () => { + const initialTotal = calculateTotal(poll.tallyResult); + expect(initialTotal.toString()).to.eq("0"); + + expect(poll.hasUntalliedBallots()).to.eq(true); + + poll.tallyVotes(useQv); + + const finalTotal = calculateTotal(poll.tallyResult); + expect(finalTotal.toString()).to.eq(voteWeight.toString()); + + // check that the perVOSpentVoiceCredits is correct + expect(poll.perVOSpentVoiceCredits[0].toString()).to.eq(voteWeight.toString()); + + // check that the totalSpentVoiceCredits is correct + expect(poll.totalSpentVoiceCredits.toString()).to.eq(voteWeight.toString()); + }); + }); + describe("Sanity checks", () => { let testHarness: TestHarness; let poll: Poll;