From 8ef789349369153cdee74bbb0678e63b150ef346 Mon Sep 17 00:00:00 2001 From: Chao Ma Date: Thu, 23 Jun 2022 04:37:01 +0800 Subject: [PATCH] Revert "Revert "Topup feature"" This reverts commit a42d582f3bf8309e3d9d3dce20bb507c33eb8eca. --- circuits/circom/hasherPoseidon.circom | 32 +- circuits/circom/messageHasher.circom | 10 +- circuits/circom/messageToCommand.circom | 6 +- circuits/circom/processMessages.circom | 114 ++++- ...her12_test.circom => hasher13_test.circom} | 2 +- circuits/circom/verifySignature.circom | 2 +- circuits/ts/__tests__/Hasher.test.ts | 16 +- .../ts/__tests__/MessageToCommand.test.ts | 4 +- .../ts/__tests__/MessageValidator.test.ts | 4 +- circuits/ts/__tests__/ProcessMessages.test.ts | 22 +- .../StateLeafAndBallotTransformer.test.ts | 4 +- circuits/ts/__tests__/TallyVotes.test.ts | 8 +- circuits/ts/__tests__/VerifySignature.test.ts | 8 +- circuits/ts/index.ts | 1 - cli/ts/airdrop.ts | 150 +++++++ cli/ts/create.ts | 6 + cli/ts/deployPoll.ts | 9 + cli/ts/index.ts | 20 + cli/ts/publish.ts | 4 +- cli/ts/topup.ts | 145 ++++++ contracts/contracts/DomainObjs.sol | 11 +- contracts/contracts/MACI.sol | 119 ++--- contracts/contracts/Poll.sol | 424 ++++++++++-------- contracts/contracts/TopupCredit.sol | 27 ++ contracts/contracts/crypto/SnarkConstants.sol | 10 + contracts/ts/__tests__/MACI.test.ts | 6 +- contracts/ts/deploy.ts | 10 + contracts/ts/genMaciState.ts | 35 +- contracts/ts/index.ts | 2 + contracts/ts/utils.ts | 4 + core/ts/MaciState.ts | 293 ++++++++---- core/ts/__tests__/MaciState.test.ts | 10 +- crypto/ts/__tests__/Crypto.test.ts | 12 +- crypto/ts/index.ts | 17 +- domainobjs/ts/__tests__/DomainObjs.test.ts | 8 +- domainobjs/ts/index.ts | 89 +++- integrationTests/ts/__tests__/suites.ts | 4 +- package.json | 6 + 38 files changed, 1189 insertions(+), 465 deletions(-) rename circuits/circom/test/{hasher12_test.circom => hasher13_test.circom} (66%) create mode 100644 cli/ts/airdrop.ts create mode 100644 cli/ts/topup.ts create mode 100644 contracts/contracts/TopupCredit.sol diff --git a/circuits/circom/hasherPoseidon.circom b/circuits/circom/hasherPoseidon.circom index 38697e12fa..1a158125a0 100644 --- a/circuits/circom/hasherPoseidon.circom +++ b/circuits/circom/hasherPoseidon.circom @@ -46,31 +46,33 @@ template Hasher5() { hash <== hasher.out; } -template Hasher12() { - // Hasher4( - // Hasher5_1(in[0], in[1], in[2], in[3], in[4]), - // Hasher5_2(in[5], in[6], in[7], in[8], in[9]) - // in[10], - // in[11] +template Hasher13() { + // Hasher5( + // in[0] + // Hasher5_1(in[1], in[2], in[3], in[4], in[5]), + // Hasher5_2(in[6], in[7], in[8], in[9], in[10]) + // in[11], + // in[12] // ) - signal input in[12]; + signal input in[13]; signal output hash; - component hasher4 = PoseidonHashT5(); + component hasher5 = PoseidonHashT6(); component hasher5_1 = PoseidonHashT6(); component hasher5_2 = PoseidonHashT6(); for (var i = 0; i < 5; i++) { - hasher5_1.inputs[i] <== in[i]; - hasher5_2.inputs[i] <== in[i+5]; + hasher5_1.inputs[i] <== in[i+1]; + hasher5_2.inputs[i] <== in[i+6]; } - hasher4.inputs[0] <== hasher5_1.out; - hasher4.inputs[1] <== hasher5_2.out; - hasher4.inputs[2] <== in[10]; - hasher4.inputs[3] <== in[11]; + hasher5.inputs[0] <== in[0]; + hasher5.inputs[1] <== hasher5_1.out; + hasher5.inputs[2] <== hasher5_2.out; + hasher5.inputs[3] <== in[11]; + hasher5.inputs[4] <== in[12]; - hash <== hasher4.out; + hash <== hasher5.out; } template HashLeftRight() { diff --git a/circuits/circom/messageHasher.circom b/circuits/circom/messageHasher.circom index ee23d564de..3d66414f66 100644 --- a/circuits/circom/messageHasher.circom +++ b/circuits/circom/messageHasher.circom @@ -2,17 +2,17 @@ pragma circom 2.0.0; include "./hasherPoseidon.circom"; template MessageHasher() { - signal input in[10]; + signal input in[11]; signal input encPubKey[2]; signal output hash; - component hasher = Hasher12(); + component hasher = Hasher13(); - for (var i = 0; i < 10; i ++) { + for (var i = 0; i < 11; i ++) { hasher.in[i] <== in[i]; } - hasher.in[10] <== encPubKey[0]; - hasher.in[11] <== encPubKey[1]; + hasher.in[11] <== encPubKey[0]; + hasher.in[12] <== encPubKey[1]; hash <== hasher.hash; } diff --git a/circuits/circom/messageToCommand.circom b/circuits/circom/messageToCommand.circom index 06287bdb98..30ac776b8f 100644 --- a/circuits/circom/messageToCommand.circom +++ b/circuits/circom/messageToCommand.circom @@ -9,7 +9,7 @@ template MessageToCommand() { var PACKED_CMD_LENGTH = 4; var UNPACKED_CMD_LENGTH = 8; - signal input message[10]; + signal input message[11]; signal input encPrivKey; signal input encPubKey[2]; @@ -33,8 +33,8 @@ template MessageToCommand() { decryptor.key[0] <== ecdh.sharedKey[0]; decryptor.key[1] <== ecdh.sharedKey[1]; decryptor.nonce <== 0; - for (var i = 0; i < 10; i++) { - decryptor.ciphertext[i] <== message[i]; + for (var i = 1; i < 11; i++) { // the first one is msg type, skip + decryptor.ciphertext[i-1] <== message[i]; } signal packedCommand[PACKED_CMD_LENGTH]; diff --git a/circuits/circom/processMessages.circom b/circuits/circom/processMessages.circom index 44cedeebc4..c6fbd02ced 100644 --- a/circuits/circom/processMessages.circom +++ b/circuits/circom/processMessages.circom @@ -6,6 +6,7 @@ include "./privToPubKey.circom"; include "./stateLeafAndBallotTransformer.circom"; include "./trees/incrementalQuinTree.circom"; include "../node_modules/circomlib/circuits/mux1.circom"; +include "../node_modules/circomlib/circuits/comparators.circom"; /* * Proves the correctness of processing a batch of messages. @@ -30,7 +31,7 @@ template ProcessMessages( var TREE_ARITY = 5; var batchSize = TREE_ARITY ** msgBatchDepth; - var MSG_LENGTH = 10; + var MSG_LENGTH = 11; var PACKED_CMD_LENGTH = 4; var STATE_LEAF_LENGTH = 4; @@ -90,6 +91,12 @@ template ProcessMessages( // The state root before it is processed signal input currentStateRoot; + // topup signals + signal input topupAmounts[batchSize]; + signal input topupStateIndexes[batchSize]; + signal input topupStateLeaves[batchSize][STATE_LEAF_LENGTH]; + signal input topupStateLeavesPathElements[batchSize][stateTreeDepth][TREE_ARITY - 1]; + // The state leaves upon which messages are applied. // transform(currentStateLeaf[4], message5) => newStateLeaf4 // transform(currentStateLeaf[3], message4) => newStateLeaf3 @@ -258,8 +265,14 @@ template ProcessMessages( // ----------------------------------------------------------------------- // Process messages in reverse order - component processors[batchSize]; + signal tmpStateRoot1[batchSize]; + signal tmpStateRoot2[batchSize]; + signal tmpBallotRoot1[batchSize]; + signal tmpBallotRoot2[batchSize]; + component processors[batchSize]; // vote type processor + component processors2[batchSize]; // topup type processor for (var i = batchSize - 1; i >= 0; i --) { + // process it as vote type message processors[i] = ProcessOne(stateTreeDepth, voteOptionTreeDepth); processors[i].numSignUps <== numSignUps; @@ -312,8 +325,29 @@ template ProcessMessages( processors[i].packedCmd[j] <== commands[i].packedCommandOut[j]; } - stateRoots[i] <== processors[i].newStateRoot; - ballotRoots[i] <== processors[i].newBallotRoot; + // -------------------------------------------- + // process it as topup type message, + processors2[i] = ProcessTopup(stateTreeDepth); + processors2[i].msgType <== msgs[i][0]; + processors2[i].stateTreeIndex <== topupStateIndexes[i]; + processors2[i].amount <== topupAmounts[i]; + processors2[i].numSignUps <== numSignUps; + for (var j = 0; j < STATE_LEAF_LENGTH; j ++) { + processors2[i].stateLeaf[j] <== topupStateLeaves[i][j]; + } + for (var j = 0; j < stateTreeDepth; j ++) { + for (var k = 0; k < TREE_ARITY - 1; k ++) { + processors2[i].stateLeafPathElements[j][k] + <== topupStateLeavesPathElements[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(); @@ -324,6 +358,76 @@ template ProcessMessages( sbCommitmentHasher.hash === newSbCommitment; } +template ProcessTopup(stateTreeDepth) { + var STATE_LEAF_LENGTH = 4; + var MSG_LENGTH = 11; + var TREE_ARITY = 5; + + 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 stateTreeIndex; + signal input amount; + signal input numSignUps; + + signal input stateLeaf[STATE_LEAF_LENGTH]; + signal input stateLeafPathElements[stateTreeDepth][TREE_ARITY - 1]; + + signal output newStateRoot; + + // skip several checkings here, because ProcessOne already checks + + // check stateIndex, if invalid index, set index and amount to zero + component validStateLeafIndex = LessEqThan(252); + validStateLeafIndex.in[0] <== stateTreeIndex; + validStateLeafIndex.in[1] <== numSignUps; + + component indexMux = Mux1(); + indexMux.s <== validStateLeafIndex.out; + indexMux.c[0] <== 0; + indexMux.c[1] <== stateTreeIndex; + + component amtMux = Mux1(); + amtMux.s <== validStateLeafIndex.out; + amtMux.c[0] <== 0; + amtMux.c[1] <== amount; + + + // check less than field size + // amt = 0 for vote type msg (msgType=1); amt not changed for topup msg (msgType=2) + // this is to avoid possible overflow failure for vote type message here + signal newCreditBalance; + signal amt; + amt <== amtMux.out * (msgType - 1); + component validCreditBalance = LessEqThan(252); + newCreditBalance <== stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX] + amt; + validCreditBalance.in[0] <== stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX]; + validCreditBalance.in[1] <== newCreditBalance; + + // update credit voice balance + component newStateLeafHasher = Hasher4(); + newStateLeafHasher.in[STATE_LEAF_PUB_X_IDX] <== stateLeaf[STATE_LEAF_PUB_X_IDX]; + newStateLeafHasher.in[STATE_LEAF_PUB_Y_IDX] <== stateLeaf[STATE_LEAF_PUB_Y_IDX]; + newStateLeafHasher.in[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX] <== newCreditBalance; + newStateLeafHasher.in[STATE_LEAF_TIMESTAMP_IDX] <== stateLeaf[STATE_LEAF_TIMESTAMP_IDX]; + + component stateLeafPathIndices = QuinGeneratePathIndices(stateTreeDepth); + stateLeafPathIndices.in <== indexMux.out; + + 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; +} + template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { /* transform(currentStateLeaves0, cmd0) -> newStateLeaves0, isValid0 @@ -333,7 +437,7 @@ template ProcessOne(stateTreeDepth, voteOptionTreeDepth) { */ var STATE_LEAF_LENGTH = 4; var BALLOT_LENGTH = 2; - var MSG_LENGTH = 10; + var MSG_LENGTH = 11; var PACKED_CMD_LENGTH = 4; var TREE_ARITY = 5; diff --git a/circuits/circom/test/hasher12_test.circom b/circuits/circom/test/hasher13_test.circom similarity index 66% rename from circuits/circom/test/hasher12_test.circom rename to circuits/circom/test/hasher13_test.circom index ad3addadf3..0bc3658b12 100644 --- a/circuits/circom/test/hasher12_test.circom +++ b/circuits/circom/test/hasher13_test.circom @@ -1,4 +1,4 @@ pragma circom 2.0.0; include "../hasherPoseidon.circom"; -component main = Hasher12(); +component main = Hasher13(); diff --git a/circuits/circom/verifySignature.circom b/circuits/circom/verifySignature.circom index 1b3e0f847c..c699978fef 100644 --- a/circuits/circom/verifySignature.circom +++ b/circuits/circom/verifySignature.circom @@ -21,7 +21,7 @@ template EdDSAPoseidonVerifier_patched() { var i; // Ensure S { }) }) - describe('Hasher12', () => { - it('correctly hashes 12 random values', async () => { - const circuit = 'hasher12_test' + describe('Hasher13', () => { + it('correctly hashes 13 random values', async () => { + const circuit = 'hasher13_test' const preImages: any = [] - for (let i = 0; i < 12; i++) { + for (let i = 0; i < 13; i++) { preImages.push(genRandomSalt()) } const circuitInputs = stringifyBigInts({ @@ -161,7 +161,7 @@ describe('Poseidon hash circuits', () => { const witness = await genWitness(circuit, circuitInputs) const output = await getSignalByName(circuit, witness, 'main.hash') - const outputJS = hash12(preImages) + const outputJS = hash13(preImages) expect(output.toString()).toEqual(outputJS.toString()) }) @@ -197,7 +197,7 @@ describe('Poseidon hash circuits', () => { ) & BigInt(genRandomSalt()) } - const command: Command = new Command( + const command: PCommand = new PCommand( random50bitBigInt(), k.pubKey, random50bitBigInt(), diff --git a/circuits/ts/__tests__/MessageToCommand.test.ts b/circuits/ts/__tests__/MessageToCommand.test.ts index 5687fb191f..759d88363c 100644 --- a/circuits/ts/__tests__/MessageToCommand.test.ts +++ b/circuits/ts/__tests__/MessageToCommand.test.ts @@ -11,7 +11,7 @@ import { import { Keypair, - Command, + PCommand, } from 'maci-domainobjs' describe('MessageToCommand circuit', () => { @@ -32,7 +32,7 @@ describe('MessageToCommand circuit', () => { ) & BigInt(genRandomSalt()) } - const command: Command = new Command( + const command: PCommand = new PCommand( random50bitBigInt(), newPubKey, random50bitBigInt(), diff --git a/circuits/ts/__tests__/MessageValidator.test.ts b/circuits/ts/__tests__/MessageValidator.test.ts index fe2bec2597..004678cf31 100644 --- a/circuits/ts/__tests__/MessageValidator.test.ts +++ b/circuits/ts/__tests__/MessageValidator.test.ts @@ -11,7 +11,7 @@ import { } from 'maci-crypto' import { - Command, + PCommand, Keypair, } from 'maci-domainobjs' @@ -23,7 +23,7 @@ describe('MessageValidator circuit', () => { const { privKey, pubKey } = new Keypair() // Note that the command fields don't matter in this test - const command: Command = new Command( + const command: PCommand = new PCommand( BigInt(1), pubKey, BigInt(2), diff --git a/circuits/ts/__tests__/ProcessMessages.test.ts b/circuits/ts/__tests__/ProcessMessages.test.ts index 16064226ee..71586d4bf3 100644 --- a/circuits/ts/__tests__/ProcessMessages.test.ts +++ b/circuits/ts/__tests__/ProcessMessages.test.ts @@ -13,7 +13,7 @@ import { import { PrivKey, Keypair, - Command, + PCommand, Message, VerifyingKey, Ballot, @@ -79,7 +79,7 @@ describe('ProcessMessage circuit', () => { let pollId let poll const messages: Message[] = [] - const commands: Command[] = [] + const commands: PCommand[] = [] let messageTree beforeAll(async () => { @@ -115,7 +115,7 @@ describe('ProcessMessage circuit', () => { ) // First command (valid) - const command = new Command( + const command = new PCommand( stateIndex, //BigInt(1), userKeypair.pubKey, voteOptionIndex, // voteOptionIndex, @@ -139,7 +139,7 @@ describe('ProcessMessage circuit', () => { poll.publishMessage(message, ecdhKeypair.pubKey) // Second command (valid) - const command2 = new Command( + const command2 = new PCommand( stateIndex, userKeypair.pubKey, voteOptionIndex, // voteOptionIndex, @@ -244,7 +244,7 @@ describe('ProcessMessage circuit', () => { let pollId let poll const messages: Message[] = [] - const commands: Command[] = [] + const commands: PCommand[] = [] let messageTree beforeAll(async () => { @@ -286,7 +286,7 @@ describe('ProcessMessage circuit', () => { hash5, ) - const command = new Command( + const command = new PCommand( BigInt(1), userKeypair.pubKey, BigInt(0), // voteOptionIndex, @@ -364,7 +364,7 @@ describe('ProcessMessage circuit', () => { let pollId let poll const messages: Message[] = [] - const commands: Command[] = [] + const commands: PCommand[] = [] let messageTree beforeAll(async () => { @@ -403,7 +403,7 @@ describe('ProcessMessage circuit', () => { ) // Vote for option 0 - const command = new Command( + const command = new PCommand( stateIndex, //BigInt(1), userKeypair.pubKey, BigInt(0), // voteOptionIndex, @@ -427,7 +427,7 @@ describe('ProcessMessage circuit', () => { poll.publishMessage(message, ecdhKeypair.pubKey) // Vote for option 1 - const command2 = new Command( + const command2 = new PCommand( stateIndex, userKeypair2.pubKey, BigInt(1), // voteOptionIndex, @@ -450,7 +450,7 @@ describe('ProcessMessage circuit', () => { poll.publishMessage(message2, ecdhKeypair2.pubKey) // Change key - const command3 = new Command( + const command3 = new PCommand( stateIndex, //BigInt(1), userKeypair2.pubKey, BigInt(1), // voteOptionIndex, @@ -551,7 +551,7 @@ describe('ProcessMessage circuit', () => { // Second batch is not a full batch const numMessages = (messageBatchSize * NUM_BATCHES) - 1 for (let i = 0; i < numMessages; i ++) { - const command = new Command( + const command = new PCommand( stateIndex, userKeypair.pubKey, BigInt(i), //vote option index diff --git a/circuits/ts/__tests__/StateLeafAndBallotTransformer.test.ts b/circuits/ts/__tests__/StateLeafAndBallotTransformer.test.ts index ab4ca04857..152b7e7403 100644 --- a/circuits/ts/__tests__/StateLeafAndBallotTransformer.test.ts +++ b/circuits/ts/__tests__/StateLeafAndBallotTransformer.test.ts @@ -10,7 +10,7 @@ import { } from 'maci-crypto' import { - Command, + PCommand, Keypair, } from 'maci-domainobjs' @@ -34,7 +34,7 @@ const ballotCurrentVotesForOption = BigInt(0) const slTimestamp = 1 const pollEndTimestamp = 2 -const command: Command = new Command( +const command: PCommand = new PCommand( stateIndex, newPubKey, voteOptionIndex, diff --git a/circuits/ts/__tests__/TallyVotes.test.ts b/circuits/ts/__tests__/TallyVotes.test.ts index 94e2098878..0e0a627de1 100644 --- a/circuits/ts/__tests__/TallyVotes.test.ts +++ b/circuits/ts/__tests__/TallyVotes.test.ts @@ -11,7 +11,7 @@ import { import { Keypair, - Command, + PCommand, Message, VerifyingKey, } from 'maci-domainobjs' @@ -78,7 +78,7 @@ describe('TallyVotes circuit', () => { beforeEach(async () => { maciState = new MaciState() const messages: Message[] = [] - const commands: Command[] = [] + const commands: PCommand[] = [] // Sign up and publish const userKeypair = new Keypair() stateIndex = maciState.signUp( @@ -111,7 +111,7 @@ describe('TallyVotes circuit', () => { ) // First command (valid) - const command = new Command( + const command = new PCommand( stateIndex, userKeypair.pubKey, voteOptionIndex, // voteOptionIndex, @@ -243,7 +243,7 @@ describe('TallyVotes circuit', () => { const numMessages = messageBatchSize * NUM_BATCHES for (let i = 0; i < numMessages; i ++) { - const command = new Command( + const command = new PCommand( BigInt(i), userKeypairs[i].pubKey, BigInt(i), //vote option index diff --git a/circuits/ts/__tests__/VerifySignature.test.ts b/circuits/ts/__tests__/VerifySignature.test.ts index 327e44eeaa..1ab3297f00 100644 --- a/circuits/ts/__tests__/VerifySignature.test.ts +++ b/circuits/ts/__tests__/VerifySignature.test.ts @@ -7,7 +7,7 @@ import { import { Keypair, - Command, + PCommand, } from 'maci-domainobjs' import { @@ -22,7 +22,7 @@ describe('Signature verification circuit', () => { it('verifies a valid signature', async () => { const keypair = new Keypair() - const command = new Command( + const command = new PCommand( BigInt(0), keypair.pubKey, BigInt(123), @@ -52,7 +52,7 @@ describe('Signature verification circuit', () => { it('rejects an invalid signature (wrong signer)', async () => { const keypair = new Keypair() - const command = new Command( + const command = new PCommand( BigInt(0), keypair.pubKey, BigInt(123), @@ -95,7 +95,7 @@ describe('Signature verification circuit', () => { it('rejects an invalid signature', async () => { const keypair = new Keypair() - const command = new Command( + const command = new PCommand( BigInt(0), keypair.pubKey, BigInt(123), diff --git a/circuits/ts/index.ts b/circuits/ts/index.ts index 3c1053ab21..8b60c53583 100644 --- a/circuits/ts/index.ts +++ b/circuits/ts/index.ts @@ -56,7 +56,6 @@ const genProof = ( const proof = JSON.parse(fs.readFileSync(proofJsonPath).toString()) const publicInputs = JSON.parse(fs.readFileSync(publicJsonPath).toString()) - // Delete the temp files and the temp directory for (const f of [ proofJsonPath, publicJsonPath, diff --git a/cli/ts/airdrop.ts b/cli/ts/airdrop.ts new file mode 100644 index 0000000000..974c32b937 --- /dev/null +++ b/cli/ts/airdrop.ts @@ -0,0 +1,150 @@ +import { + getDefaultSigner, + parseArtifact, +} from 'maci-contracts' + +import { + validateEthAddress, + contractExists, +} from './utils' + +import {readJSONFile} from 'maci-common' + +const { ethers } = require('hardhat') + +import {contractFilepath} from './config' + +const configureSubparser = (subparsers: any) => { + const parser = subparsers.addParser( + 'airdrop', + { addHelp: true }, + ) + + parser.addArgument( + ['-e', '--erc20-contract'], + { + type: 'string', + help: 'The topup credit contract address', + } + ) + + parser.addArgument( + ['-a', '--amount'], + { + required: true, + type: 'int', + action: 'store', + help: 'The amount of topup' + } + ) + + parser.addArgument( + ['-x', '--contract'], + { + type: 'string', + help: 'The MACI contract address', + } + ) + parser.addArgument( + ['-o', '--poll-id'], + { + type: 'int', + action: 'store', + help: 'poll id' + } + ) + +} + +const airdrop = async (args: any) => { + let contractAddrs = readJSONFile(contractFilepath) + if ((!contractAddrs||!contractAddrs["TopupCredit"]) && !args.erc20_contract) { + console.error('Error: ERC20 contract address is empty') + return 1 + } + const ERC20Address = args.erc20_contract ? args.erc20_contract: contractAddrs["TopupCredit"] + + if (!validateEthAddress(ERC20Address)) { + console.error('Error: invalid topup credit contract address') + return 1 + } + + const signer = await getDefaultSigner() + + if (! await contractExists(signer.provider, ERC20Address)) { + console.error('Error: there is no contract deployed at the specified address') + return 1 + } + + const ERC20ContractAbi = parseArtifact('TopupCredit')[0] + const ERC20Contract = new ethers.Contract( + ERC20Address, + ERC20ContractAbi, + signer, + ) + const amount = args.amount + if (amount < 0) { + console.error('Error: airdrop amount must be greater than 0') + return 1 + } + + let tx + try { + tx = await ERC20Contract.airdrop( + amount.toString(), + { gasLimit: 1000000 } + ) + await tx.wait() + console.log('Transaction hash of airdrop:', tx.hash) + } catch(e) { + console.error('Error: the transaction of airdrop failed') + if (e.message) { + console.error(e.message) + } + return 1 + } + + if (typeof args.poll_id !== 'undefined') { + const pollId = args.poll_id + if (pollId < 0) { + console.error('Error: the Poll ID should be a positive integer.') + return 1 + } + + if ((!contractAddrs["MACI"]) && !args.contract) { + console.error('Error: MACI contract address is empty') + return 1 + } + const maciAddress = args.contract ? args.contract: contractAddrs["MACI"] + const maciContractAbi = parseArtifact('MACI')[0] + const maciContract = new ethers.Contract( + maciAddress, + maciContractAbi, + signer, + ) + + const pollAddr = await maciContract.getPoll(pollId) + let MAXIMUM_ALLOWANCE = "10000000000000000000000000" + try { + tx = await ERC20Contract.approve( + pollAddr, + MAXIMUM_ALLOWANCE, + { gasLimit: 1000000 } + ) + await tx.wait() + console.log('Transaction hash of approve:', tx.hash) + } catch(e) { + console.error('Error: the transaction failed') + if (e.message) { + console.error(e.message) + } + return 1 + } + } + return 0 +} + +export { + airdrop, + configureSubparser, +} diff --git a/cli/ts/create.ts b/cli/ts/create.ts index 43e6068ce0..5c05b539d9 100644 --- a/cli/ts/create.ts +++ b/cli/ts/create.ts @@ -3,6 +3,7 @@ import { deployFreeForAllSignUpGatekeeper, deployMaci, deployVerifier, + deployTopupCredit as deployTopupCreditContract, } from 'maci-contracts' import {readJSONFile, writeJSONFile} from 'maci-common' @@ -63,6 +64,9 @@ const create = async (args: any) => { return 1 } + const TopupCreditContract = await deployTopupCreditContract() + console.log('TopupCredit:', TopupCreditContract.address) + // Initial voice credits const initialVoiceCredits = args.initial_voice_credits ? args.initial_voice_credits : DEFAULT_INITIAL_VOICE_CREDITS @@ -114,6 +118,7 @@ const create = async (args: any) => { initialVoiceCreditProxyContractAddress, verifierContract.address, vkRegistryContractAddress, + TopupCreditContract.address ) console.log('MACI:', maciContract.address) @@ -125,6 +130,7 @@ const create = async (args: any) => { contractAddrs['StateAq'] = stateAqContract.address contractAddrs['PollFactory'] = pollFactoryContract.address contractAddrs['MessageAqFactory'] = messageAqContract.address + contractAddrs['TopupCredit'] = TopupCreditContract.address writeJSONFile(contractFilepath, contractAddrs) return 0 diff --git a/cli/ts/deployPoll.ts b/cli/ts/deployPoll.ts index f92a318446..513a55a881 100644 --- a/cli/ts/deployPoll.ts +++ b/cli/ts/deployPoll.ts @@ -29,6 +29,15 @@ const configureSubparser = (subparsers: any) => { } ) + createParser.addArgument( + ['-e', '--erc20-address'], + { + action: 'store', + type: 'string', + help: 'The topup credit contract address', + } + ) + createParser.addArgument( ['-pk', '--pubkey'], { diff --git a/cli/ts/index.ts b/cli/ts/index.ts index 2257366dcd..4904d94be2 100644 --- a/cli/ts/index.ts +++ b/cli/ts/index.ts @@ -42,6 +42,16 @@ import { configureSubparser as configureSubparserForDeployPoll, } from './deployPoll' +import { + airdrop, + configureSubparser as configureSubparserForAirdrop, +} from './airdrop' + +import { + topup, + configureSubparser as configureSubparserForTopup, +} from './topup' + import { signup, configureSubparser as configureSubparserForSignup, @@ -113,6 +123,12 @@ const main = async () => { // Subcommand: deployPoll configureSubparserForDeployPoll(subparsers) + // Subcommand: airdrop + configureSubparserForAirdrop(subparsers) + + // Subcommand: topup + configureSubparserForTopup(subparsers) + // Subcommand: signup configureSubparserForSignup(subparsers) @@ -154,6 +170,10 @@ const main = async () => { await create(args) } else if (args.subcommand === 'deployPoll') { await deployPoll(args) + } else if (args.subcommand === 'airdrop') { + await airdrop(args) + } else if (args.subcommand === 'topup') { + await topup(args) } else if (args.subcommand === 'signup') { await signup(args) } else if (args.subcommand === 'publish') { diff --git a/cli/ts/publish.ts b/cli/ts/publish.ts index 81e5097006..0b116d939a 100644 --- a/cli/ts/publish.ts +++ b/cli/ts/publish.ts @@ -7,7 +7,7 @@ import { PubKey, PrivKey, Keypair, - Command, + PCommand, } from 'maci-domainobjs' import { @@ -295,7 +295,7 @@ const publish = async (args: any) => { const encKeypair = new Keypair() - const command = new Command( + const command:PCommand = new PCommand( stateIndex, userMaciPubKey, voteOptionIndex, diff --git a/cli/ts/topup.ts b/cli/ts/topup.ts new file mode 100644 index 0000000000..1ac628f038 --- /dev/null +++ b/cli/ts/topup.ts @@ -0,0 +1,145 @@ +import { + getDefaultSigner, + parseArtifact, +} from 'maci-contracts' + +import { + validateEthAddress, + contractExists, +} from './utils' + +import {readJSONFile} from 'maci-common' + +const { ethers } = require('hardhat') + +import {contractFilepath} from './config' + +const configureSubparser = (subparsers: any) => { + const parser = subparsers.addParser( + 'topup', + { addHelp: true }, + ) + + parser.addArgument( + ['-x', '--contract'], + { + type: 'string', + help: 'The MACI contract address', + } + ) + + parser.addArgument( + ['-a', '--amount'], + { + required: true, + type: 'int', + action: 'store', + help: 'The amount of topup' + } + ) + + parser.addArgument( + ['-i', '--state-index'], + { + required: true, + type: 'int', + action: 'store', + help: 'state leaf index' + } + ) + + parser.addArgument( + ['-o', '--poll-id'], + { + required: true, + type: 'int', + action: 'store', + help: 'poll id' + } + ) +} + +const topup = async (args: any) => { + let contractAddrs = readJSONFile(contractFilepath) + if ((!contractAddrs||!contractAddrs["MACI"]) && !args.contract) { + console.error('Error: MACI contract address is empty') + return 1 + } + const maciAddress = args.contract ? args.contract: contractAddrs["MACI"] + + // MACI contract + if (!validateEthAddress(maciAddress)) { + console.error('Error: invalid MACI contract address') + return 1 + } + + const signer = await getDefaultSigner() + + if (! await contractExists(signer.provider, maciAddress)) { + console.error('Error: there is no contract deployed at the specified address') + return 1 + } + + const amount = args.amount + if (amount < 0) { + console.error('Error: topup amount must be greater than 0') + return 1 + } + const stateIndex = BigInt(args.state_index) + if (stateIndex < 0) { + console.error('Error: the state index must be greater than 0') + return 1 + } + const pollId = args.poll_id + if (pollId < 0) { + console.error('Error: the Poll ID should be a positive integer.') + return 1 + } + + const maciContractAbi = parseArtifact('MACI')[0] + const maciContract = new ethers.Contract( + maciAddress, + maciContractAbi, + signer, + ) + const [ pollContractAbi ] = parseArtifact('Poll') + const pollAddr = await maciContract.getPoll(pollId) + if (! (await contractExists(signer.provider, pollAddr))) { + console.error('Error: there is no Poll contract with this poll ID linked to the specified MACI contract.') + return 1 + } + + const pollContract = new ethers.Contract( + pollAddr, + pollContractAbi, + signer, + ) + + + let tx + try { + tx = await pollContract.topup( + stateIndex, + amount.toString(), + { gasLimit: 1000000 }, + ) + await tx.wait() + console.log('Transaction hash:', tx.hash) + } catch(e) { + if (e.message) { + if (e.message.endsWith('PollE03')) { + console.error('Error: the voting period is over.') + } else { + console.error('Error: the transaction failed.') + console.error(e.message) + } + } + return 1 + } + return 0 +} + +export { + topup, + configureSubparser, +} diff --git a/contracts/contracts/DomainObjs.sol b/contracts/contracts/DomainObjs.sol index 0eca2dd31d..2e956a165d 100644 --- a/contracts/contracts/DomainObjs.sol +++ b/contracts/contracts/DomainObjs.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma experimental ABIEncoderV2; pragma solidity ^0.7.2; -import { Hasher } from "./crypto/Hasher.sol"; +import {Hasher} from "./crypto/Hasher.sol"; contract IPubKey { struct PubKey { @@ -14,7 +14,8 @@ contract IMessage { uint8 constant MESSAGE_DATA_LENGTH = 10; struct Message { - uint256[MESSAGE_DATA_LENGTH] data; + uint256 msgType; // 1: vote message (size 10), 2: topup message (size 2) + uint256[MESSAGE_DATA_LENGTH] data; // data length is padded to size 10 } } @@ -25,7 +26,11 @@ contract DomainObjs is IMessage, Hasher, IPubKey { uint256 timestamp; } - function hashStateLeaf(StateLeaf memory _stateLeaf) public pure returns (uint256) { + function hashStateLeaf(StateLeaf memory _stateLeaf) + public + pure + returns (uint256) + { uint256[4] memory plaintext; plaintext[0] = _stateLeaf.pubKey.x; plaintext[1] = _stateLeaf.pubKey.y; diff --git a/contracts/contracts/MACI.sol b/contracts/contracts/MACI.sol index ea8dbd23f7..bafbc44dc5 100644 --- a/contracts/contracts/MACI.sol +++ b/contracts/contracts/MACI.sol @@ -2,32 +2,26 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.7.2; -import { - Poll, - PollFactory, - PollProcessorAndTallyer, - MessageAqFactory -} from "./Poll.sol"; - -import { InitialVoiceCreditProxy } - from "./initialVoiceCreditProxy/InitialVoiceCreditProxy.sol"; - -import { SignUpGatekeeper } from "./gatekeepers/SignUpGatekeeper.sol"; -import { AccQueue, AccQueueQuinaryBlankSl } from "./trees/AccQueue.sol"; -import { IMACI } from "./IMACI.sol"; -import { Params } from "./Params.sol"; -import { DomainObjs } from "./DomainObjs.sol"; -import { VkRegistry } from "./VkRegistry.sol"; -import { SnarkCommon } from "./crypto/SnarkCommon.sol"; -import { SnarkConstants } from "./crypto/SnarkConstants.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import {Poll, PollFactory, PollProcessorAndTallyer, MessageAqFactory} from "./Poll.sol"; + +import {InitialVoiceCreditProxy} from "./initialVoiceCreditProxy/InitialVoiceCreditProxy.sol"; + +import {SignUpGatekeeper} from "./gatekeepers/SignUpGatekeeper.sol"; +import {AccQueue, AccQueueQuinaryBlankSl} from "./trees/AccQueue.sol"; +import {IMACI} from "./IMACI.sol"; +import {Params} from "./Params.sol"; +import {DomainObjs} from "./DomainObjs.sol"; +import {VkRegistry} from "./VkRegistry.sol"; +import {TopupCredit} from "./TopupCredit.sol"; +import {SnarkCommon} from "./crypto/SnarkCommon.sol"; +import {SnarkConstants} from "./crypto/SnarkConstants.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /* * Minimum Anti-Collusion Infrastructure * Version 1 */ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { - // The state tree depth is fixed. As such it should be as large as feasible // so that there can be as many users as possible. i.e. 5 ** 10 = 9765625 uint8 public override stateTreeDepth = 10; @@ -36,23 +30,27 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { // in contracts/ts/genEmptyBallotRootsContract.ts file // if we change the state tree depth! - uint8 constant internal STATE_TREE_SUBDEPTH = 2; - uint8 constant internal STATE_TREE_ARITY = 5; - uint8 constant internal MESSAGE_TREE_ARITY = 5; + uint8 internal constant STATE_TREE_SUBDEPTH = 2; + uint8 internal constant STATE_TREE_ARITY = 5; + uint8 internal constant MESSAGE_TREE_ARITY = 5; // The Keccack256 hash of 'Maci' - uint256 constant internal NOTHING_UP_MY_SLEEVE - = uint256(8370432830353022751713833565135785980866757267633941821328460903436894336785); + uint256 internal constant NOTHING_UP_MY_SLEEVE = + uint256( + 8370432830353022751713833565135785980866757267633941821328460903436894336785 + ); //// The hash of a blank state leaf - uint256 constant internal BLANK_STATE_LEAF_HASH - = uint256(6769006970205099520508948723718471724660867171122235270773600567925038008762); + uint256 internal constant BLANK_STATE_LEAF_HASH = + uint256( + 6769006970205099520508948723718471724660867171122235270773600567925038008762 + ); // Each poll has an incrementing ID uint256 internal nextPollId = 0; // A mapping of poll IDs to Poll contracts. - mapping (uint256 => Poll) public polls; + mapping(uint256 => Poll) public polls; //// A mapping of block timestamps to state roots //mapping (uint256 => uint256) public stateRootSnapshots; @@ -61,7 +59,7 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { uint256 public override numSignUps; // A mapping of block timestamps to the number of state leaves - mapping (uint256 => uint256) public numStateLeaves; + mapping(uint256 => uint256) public numStateLeaves; // The block timestamp at which the state queue subroots were last merged //uint256 public mergeSubRootsTimestamp; @@ -71,6 +69,9 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { // circuit's compile-time parameters, such as tree depths and batch sizes. VkRegistry public override vkRegistry; + // ERC20 contract that hold topup credits + TopupCredit public topupCredit; + PollFactory public pollFactory; MessageAqFactory public messageAqFactory; @@ -125,7 +126,10 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { signUpTimestamp = block.timestamp; // Verify linked poseidon libraries - require(hash2([uint256(1),uint256(1)]) != 0, "MACI: poseidon hash libraries not linked"); + require( + hash2([uint256(1), uint256(1)]) != 0, + "MACI: poseidon hash libraries not linked" + ); } /* @@ -134,12 +138,14 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { */ function init( VkRegistry _vkRegistry, - MessageAqFactory _messageAqFactory + MessageAqFactory _messageAqFactory, + TopupCredit _topupCredit ) public onlyOwner { require(isInitialised == false, "MACI: already initialised"); vkRegistry = _vkRegistry; messageAqFactory = _messageAqFactory; + topupCredit = _topupCredit; // Check that the factory contracts have correct access controls before // allowing any functions in MACI to run (via the afterInit modifier) @@ -192,10 +198,9 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { bytes memory _signUpGatekeeperData, bytes memory _initialVoiceCreditProxyData ) public afterInit { - // The circuits only support up to (5 ** 10 - 1) signups require( - numSignUps < STATE_TREE_ARITY ** stateTreeDepth, + numSignUps < STATE_TREE_ARITY**stateTreeDepth, "MACI: maximum number of signups reached" ); @@ -231,17 +236,17 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { uint256 stateIndex = stateAq.enqueue(stateLeaf); // Increment the number of signups - numSignUps ++; + numSignUps++; emit SignUp(stateIndex, _pubKey, voiceCreditBalance, timestamp); } //function signUpViaRelayer( - //MaciPubKey memory pubKey, - //bytes memory signature, - // uint256 nonce + //MaciPubKey memory pubKey, + //bytes memory signature, + // uint256 nonce //) public { - //// TODO: validate signature and sign up + //// TODO: validate signature and sign up //) /* @@ -256,23 +261,23 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { } function mergeStateAqSubRoots(uint256 _numSrQueueOps, uint256 _pollId) - public - onlyPoll(_pollId) - override - afterInit { + public + override + onlyPoll(_pollId) + afterInit + { stateAq.mergeSubRoots(_numSrQueueOps); emit MergeStateAqSubRoots(_pollId, _numSrQueueOps); } - function mergeStateAq( - uint256 _pollId - ) - public - onlyPoll(_pollId) - override - afterInit - returns (uint256) { + function mergeStateAq(uint256 _pollId) + public + override + onlyPoll(_pollId) + afterInit + returns (uint256) + { uint256 root = stateAq.merge(stateTreeDepth); emit MergeStateAq(_pollId); @@ -304,9 +309,9 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { // The message batch size and the tally batch size BatchSizes memory batchSizes = BatchSizes( - MESSAGE_TREE_ARITY ** uint8(_treeDepths.messageTreeSubDepth), - STATE_TREE_ARITY ** uint8(_treeDepths.intStateTreeDepth), - STATE_TREE_ARITY ** uint8(_treeDepths.intStateTreeDepth) + MESSAGE_TREE_ARITY**uint8(_treeDepths.messageTreeSubDepth), + STATE_TREE_ARITY**uint8(_treeDepths.intStateTreeDepth), + STATE_TREE_ARITY**uint8(_treeDepths.intStateTreeDepth) ); Poll p = pollFactory.deploy( @@ -317,22 +322,20 @@ contract MACI is IMACI, DomainObjs, Params, SnarkCommon, Ownable { _coordinatorPubKey, vkRegistry, this, + topupCredit, owner() ); polls[pollId] = p; // Increment the poll ID for the next poll - nextPollId ++; + nextPollId++; emit DeployPoll(pollId, address(p), _coordinatorPubKey); } function getPoll(uint256 _pollId) public view returns (Poll) { - require( - _pollId < nextPollId, - "MACI: poll with _pollId does not exist" - ); + require(_pollId < nextPollId, "MACI: poll with _pollId does not exist"); return polls[_pollId]; } } diff --git a/contracts/contracts/Poll.sol b/contracts/contracts/Poll.sol index 7967ac22c8..6220ff3559 100644 --- a/contracts/contracts/Poll.sol +++ b/contracts/contracts/Poll.sol @@ -2,23 +2,21 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.7.2; -import { IMACI } from "./IMACI.sol"; -import { Params } from "./Params.sol"; -import { Hasher } from "./crypto/Hasher.sol"; -import { Verifier } from "./crypto/Verifier.sol"; -import { SnarkCommon } from "./crypto/SnarkCommon.sol"; -import { SnarkConstants } from "./crypto/SnarkConstants.sol"; -import { DomainObjs, IPubKey, IMessage } from "./DomainObjs.sol"; -import { AccQueue, AccQueueQuinaryMaci } from "./trees/AccQueue.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { VkRegistry } from "./VkRegistry.sol"; -import { EmptyBallotRoots } from "./trees/EmptyBallotRoots.sol"; +import {IMACI} from "./IMACI.sol"; +import {Params} from "./Params.sol"; +import {Hasher} from "./crypto/Hasher.sol"; +import {Verifier} from "./crypto/Verifier.sol"; +import {SnarkCommon} from "./crypto/SnarkCommon.sol"; +import {SnarkConstants} from "./crypto/SnarkConstants.sol"; +import {DomainObjs, IPubKey, IMessage} from "./DomainObjs.sol"; +import {AccQueue, AccQueueQuinaryMaci} from "./trees/AccQueue.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {VkRegistry} from "./VkRegistry.sol"; +import {EmptyBallotRoots} from "./trees/EmptyBallotRoots.sol"; +import {TopupCredit} from "./TopupCredit.sol"; contract MessageAqFactory is Ownable { - function deploy(uint256 _subDepth) - public - onlyOwner - returns (AccQueue) { + function deploy(uint256 _subDepth) public onlyOwner returns (AccQueue) { AccQueue aq = new AccQueueQuinaryMaci(_subDepth); aq.transferOwnership(owner()); return aq; @@ -30,6 +28,7 @@ contract PollDeploymentParams { VkRegistry vkRegistry; IMACI maci; AccQueue messageAq; + TopupCredit topupCredit; } } @@ -37,13 +36,20 @@ contract PollDeploymentParams { * A factory contract which deploys Poll contracts. It allows the MACI contract * size to stay within the limit set by EIP-170. */ -contract PollFactory is Params, IPubKey, IMessage, Ownable, Hasher, PollDeploymentParams { - +contract PollFactory is + Params, + IPubKey, + IMessage, + Ownable, + Hasher, + PollDeploymentParams +{ MessageAqFactory public messageAqFactory; function setMessageAqFactory(MessageAqFactory _messageAqFactory) - public - onlyOwner { + public + onlyOwner + { messageAqFactory = _messageAqFactory; } @@ -58,6 +64,7 @@ contract PollFactory is Params, IPubKey, IMessage, Ownable, Hasher, PollDeployme PubKey memory _coordinatorPubKey, VkRegistry _vkRegistry, IMACI _maci, + TopupCredit _topupCredit, address _pollOwner ) public onlyOwner returns (Poll) { uint256 treeArity = 5; @@ -71,16 +78,19 @@ contract PollFactory is Params, IPubKey, IMessage, Ownable, Hasher, PollDeployme // of the inputs (aka packedVal) require( - _maxValues.maxMessages <= treeArity ** uint256(_treeDepths.messageTreeDepth) && - _maxValues.maxMessages >= _batchSizes.messageBatchSize && - _maxValues.maxMessages % _batchSizes.messageBatchSize == 0 && - _maxValues.maxVoteOptions <= treeArity ** uint256(_treeDepths.voteOptionTreeDepth) && - _maxValues.maxVoteOptions < (2 ** 50), + _maxValues.maxMessages <= + treeArity**uint256(_treeDepths.messageTreeDepth) && + _maxValues.maxMessages >= _batchSizes.messageBatchSize && + _maxValues.maxMessages % _batchSizes.messageBatchSize == 0 && + _maxValues.maxVoteOptions <= + treeArity**uint256(_treeDepths.voteOptionTreeDepth) && + _maxValues.maxVoteOptions < (2**50), "PollFactory: invalid _maxValues" ); - AccQueue messageAq = - messageAqFactory.deploy(_treeDepths.messageTreeSubDepth); + AccQueue messageAq = messageAqFactory.deploy( + _treeDepths.messageTreeSubDepth + ); ExtContracts memory extContracts; @@ -88,6 +98,7 @@ contract PollFactory is Params, IPubKey, IMessage, Ownable, Hasher, PollDeployme extContracts.vkRegistry = _vkRegistry; extContracts.maci = _maci; extContracts.messageAq = messageAq; + extContracts.topupCredit = _topupCredit; Poll poll = new Poll( _duration, @@ -113,10 +124,16 @@ contract PollFactory is Params, IPubKey, IMessage, Ownable, Hasher, PollDeployme * Do not deploy this directly. Use PollFactory.deploy() which performs some * checks on the Poll constructor arguments. */ -contract Poll is - Params, Hasher, IMessage, IPubKey, SnarkCommon, Ownable, - PollDeploymentParams, EmptyBallotRoots { - +contract Poll is + Params, + Hasher, + IMessage, + IPubKey, + SnarkCommon, + Ownable, + PollDeploymentParams, + EmptyBallotRoots +{ // The coordinator's public key PubKey public coordinatorPubKey; @@ -151,7 +168,7 @@ contract Poll is uint256 internal numMessages; function numSignUpsAndMessages() public view returns (uint256, uint256) { - uint numSignUps = extContracts.maci.numSignUps(); + uint256 numSignUps = extContracts.maci.numSignUps(); return (numSignUps, numMessages); } @@ -173,6 +190,7 @@ contract Poll is uint8 private constant LEAVES_PER_NODE = 5; event PublishMessage(Message _message, PubKey _encPubKey); + event TopupMessage(Message _message); event MergeMaciStateAqSubRoots(uint256 _numSrQueueOps); event MergeMaciStateAq(uint256 _stateRoot); event MergeMessageAqSubRoots(uint256 _numSrQueueOps); @@ -192,11 +210,13 @@ contract Poll is PubKey memory _coordinatorPubKey, ExtContracts memory _extContracts ) { - extContracts = _extContracts; coordinatorPubKey = _coordinatorPubKey; - coordinatorPubKeyHash = hashLeftRight(_coordinatorPubKey.x, _coordinatorPubKey.y); + coordinatorPubKeyHash = hashLeftRight( + _coordinatorPubKey.x, + _coordinatorPubKey.y + ); duration = _duration; maxValues = _maxValues; batchSizes = _batchSizes; @@ -212,10 +232,7 @@ contract Poll is */ modifier isAfterVotingDeadline() { uint256 secondsPassed = block.timestamp - deployTime; - require( - secondsPassed > duration, - ERROR_VOTING_PERIOD_NOT_PASSED - ); + require(secondsPassed > duration, ERROR_VOTING_PERIOD_NOT_PASSED); _; } @@ -224,6 +241,32 @@ contract Poll is return secondsPassed > duration; } + function topup(uint256 stateIndex, uint256 amount) public { + uint256 secondsPassed = block.timestamp - deployTime; + require(secondsPassed <= duration, ERROR_VOTING_PERIOD_PASSED); + require( + numMessages <= maxValues.maxMessages, + ERROR_MAX_MESSAGES_REACHED + ); + extContracts.topupCredit.transferFrom( + msg.sender, + address(this), + amount + ); + uint256[10] memory dat; + dat[0] = stateIndex; + dat[1] = amount; + for(uint i = 2; i< 10; i++) { + dat[i] = 0; + } + PubKey memory _padKey = PubKey(PAD_PUBKEY_X, PAD_PUBKEY_Y); + Message memory _message = Message({msgType: 2, data: dat}); + uint256 messageLeaf = hashMessageAndEncPubKey(_message, _padKey); + extContracts.messageAq.enqueue(messageLeaf); + numMessages++; + emit TopupMessage(_message); + } + /* * Allows anyone to publish a message (an encrypted command and signature). * This function also enqueues the message. @@ -232,27 +275,24 @@ contract Poll is * coordinator's private key to generate an ECDH shared key with which * to encrypt the message. */ - function publishMessage( - Message memory _message, - PubKey memory _encPubKey - ) public { + function publishMessage(Message memory _message, PubKey memory _encPubKey) + public + { uint256 secondsPassed = block.timestamp - deployTime; - require( - secondsPassed <= duration, - ERROR_VOTING_PERIOD_PASSED - ); + require(secondsPassed <= duration, ERROR_VOTING_PERIOD_PASSED); require( numMessages <= maxValues.maxMessages, ERROR_MAX_MESSAGES_REACHED ); require( _encPubKey.x < SNARK_SCALAR_FIELD && - _encPubKey.y < SNARK_SCALAR_FIELD, + _encPubKey.y < SNARK_SCALAR_FIELD, ERROR_INVALID_PUBKEY ); + _message.msgType = 1; uint256 messageLeaf = hashMessageAndEncPubKey(_message, _encPubKey); extContracts.messageAq.enqueue(messageLeaf); - numMessages ++; + numMessages++; emit PublishMessage(_message, _encPubKey); } @@ -261,6 +301,7 @@ contract Poll is Message memory _message, PubKey memory _encPubKey ) public pure returns (uint256) { + require(_message.data.length == 10, "invalid message length"); uint256[5] memory n; n[0] = _message.data[0]; n[1] = _message.data[1]; @@ -275,27 +316,28 @@ contract Poll is m[3] = _message.data[8]; m[4] = _message.data[9]; - return hash4([ - hash5(n), - hash5(m), - _encPubKey.x, - _encPubKey.y - ]); + return + hash5( + [ + _message.msgType, + hash5(n), + hash5(m), + _encPubKey.x, + _encPubKey.y + ] + ); } - /* * The first step of merging the MACI state AccQueue. This allows the * ProcessMessages circuit to access the latest state tree and ballots via * currentSbCommitment. */ - function mergeMaciStateAqSubRoots( - uint256 _numSrQueueOps, - uint256 _pollId - ) - public - onlyOwner - isAfterVotingDeadline { + function mergeMaciStateAqSubRoots(uint256 _numSrQueueOps, uint256 _pollId) + public + onlyOwner + isAfterVotingDeadline + { // This function can only be called once per Poll require(!stateAqMerged, ERROR_STATE_AQ_ALREADY_MERGED); @@ -311,17 +353,19 @@ contract Poll is * ProcessMessages circuit to access the latest state tree and ballots via * currentSbCommitment. */ - function mergeMaciStateAq( - uint256 _pollId - ) - public - onlyOwner - isAfterVotingDeadline { + function mergeMaciStateAq(uint256 _pollId) + public + onlyOwner + isAfterVotingDeadline + { // This function can only be called once per Poll after the voting // deadline require(!stateAqMerged, ERROR_STATE_AQ_ALREADY_MERGED); - require(extContracts.maci.stateAq().subTreesMerged(), ERROR_STATE_AQ_SUBTREES_NEED_MERGE); + require( + extContracts.maci.stateAq().subTreesMerged(), + ERROR_STATE_AQ_SUBTREES_NEED_MERGE + ); extContracts.maci.mergeStateAq(_pollId); stateAqMerged = true; @@ -342,9 +386,10 @@ contract Poll is * ProcessMessages circuit can access the message root. */ function mergeMessageAqSubRoots(uint256 _numSrQueueOps) - public - onlyOwner - isAfterVotingDeadline { + public + onlyOwner + isAfterVotingDeadline + { extContracts.messageAq.mergeSubRoots(_numSrQueueOps); emit MergeMessageAqSubRoots(_numSrQueueOps); } @@ -353,11 +398,10 @@ contract Poll is * The second step in merging the message AccQueue so that the * ProcessMessages circuit can access the message root. */ - function mergeMessageAq() - public - onlyOwner - isAfterVotingDeadline { - uint256 root = extContracts.messageAq.merge(treeDepths.messageTreeDepth); + function mergeMessageAq() public onlyOwner isAfterVotingDeadline { + uint256 root = extContracts.messageAq.merge( + treeDepths.messageTreeDepth + ); emit MergeMessageAq(root); } @@ -365,42 +409,44 @@ contract Poll is * Enqueue a batch of messages. */ function batchEnqueueMessage(uint256 _messageSubRoot) - public - onlyOwner - isAfterVotingDeadline { + public + onlyOwner + isAfterVotingDeadline + { extContracts.messageAq.insertSubTree(_messageSubRoot); // TODO: emit event } /* - * @notice Verify the number of spent voice credits from the tally.json - * @param _totalSpent spent field retrieved in the totalSpentVoiceCredits object - * @param _totalSpentSalt the corresponding salt in the totalSpentVoiceCredit object - * @return valid a boolean representing successful verification - */ + * @notice Verify the number of spent voice credits from the tally.json + * @param _totalSpent spent field retrieved in the totalSpentVoiceCredits object + * @param _totalSpentSalt the corresponding salt in the totalSpentVoiceCredit object + * @return valid a boolean representing successful verification + */ function verifySpentVoiceCredits( uint256 _totalSpent, uint256 _totalSpentSalt ) public view returns (bool) { uint256 ballotRoot = hashLeftRight(_totalSpent, _totalSpentSalt); - return ballotRoot == emptyBallotRoots[treeDepths.voteOptionTreeDepth - 1]; + return + ballotRoot == emptyBallotRoots[treeDepths.voteOptionTreeDepth - 1]; } /* - * @notice Verify the number of spent voice credits per vote option from the tally.json - * @param _voteOptionIndex the index of the vote option where credits were spent - * @param _spent the spent voice credits for a given vote option index - * @param _spentProof proof generated for the perVOSpentVoiceCredits - * @param _salt the corresponding salt given in the tally perVOSpentVoiceCredits object - * @return valid a boolean representing successful verification - */ + * @notice Verify the number of spent voice credits per vote option from the tally.json + * @param _voteOptionIndex the index of the vote option where credits were spent + * @param _spent the spent voice credits for a given vote option index + * @param _spentProof proof generated for the perVOSpentVoiceCredits + * @param _salt the corresponding salt given in the tally perVOSpentVoiceCredits object + * @return valid a boolean representing successful verification + */ function verifyPerVOSpentVoiceCredits( uint256 _voteOptionIndex, uint256 _spent, uint256[][] memory _spentProof, uint256 _spentSalt ) public view returns (bool) { - uint256 computedRoot = computeMerkleRootFromPath( + uint256 computedRoot = computeMerkleRootFromPath( treeDepths.voteOptionTreeDepth, _voteOptionIndex, _spent, @@ -418,16 +464,16 @@ contract Poll is } /* - * @notice Verify the result generated of the tally.json - * @param _voteOptionIndex the index of the vote option to verify the correctness of the tally - * @param _tallyResult Flattened array of the tally - * @param _tallyResultProof Corresponding proof of the tally result - * @param _tallyResultSalt the respective salt in the results object in the tally.json - * @param _spentVoiceCreditsHash hashLeftRight(number of spent voice credits, spent salt) - * @param _perVOSpentVoiceCreditsHash hashLeftRight(merkle root of the no spent voice credits per vote option, perVOSpentVoiceCredits salt) - * @param _tallyCommitment newTallyCommitment field in the tally.json - * @return valid a boolean representing successful verification - */ + * @notice Verify the result generated of the tally.json + * @param _voteOptionIndex the index of the vote option to verify the correctness of the tally + * @param _tallyResult Flattened array of the tally + * @param _tallyResultProof Corresponding proof of the tally result + * @param _tallyResultSalt the respective salt in the results object in the tally.json + * @param _spentVoiceCreditsHash hashLeftRight(number of spent voice credits, spent salt) + * @param _perVOSpentVoiceCreditsHash hashLeftRight(merkle root of the no spent voice credits per vote option, perVOSpentVoiceCredits salt) + * @param _tallyCommitment newTallyCommitment field in the tally.json + * @return valid a boolean representing successful verification + */ function verifyTallyResult( uint256 _voteOptionIndex, uint256 _tallyResult, @@ -435,8 +481,8 @@ contract Poll is uint256 _spentVoiceCreditsHash, uint256 _perVOSpentVoiceCreditsHash, uint256 _tallyCommitment - ) public view returns (bool){ - uint256 computedRoot = computeMerkleRootFromPath( + ) public view returns (bool) { + uint256 computedRoot = computeMerkleRootFromPath( treeDepths.voteOptionTreeDepth, _voteOptionIndex, _tallyResult, @@ -451,7 +497,6 @@ contract Poll is return hash3(tally) == _tallyCommitment; } - function computeMerkleRootFromPath( uint8 _depth, uint256 _index, @@ -464,8 +509,8 @@ contract Poll is uint256[LEAVES_PER_NODE] memory level; - for (uint8 i = 0; i < _depth; i ++) { - for (uint8 j = 0; j < LEAVES_PER_NODE; j ++) { + for (uint8 i = 0; i < _depth; i++) { + for (uint8 j = 0; j < LEAVES_PER_NODE; j++) { if (j == pos) { level[j] = current; } else { @@ -487,8 +532,12 @@ contract Poll is } contract PollProcessorAndTallyer is - Ownable, SnarkCommon, SnarkConstants, IPubKey, PollDeploymentParams{ - + Ownable, + SnarkCommon, + SnarkConstants, + IPubKey, + PollDeploymentParams +{ // Error codes string constant ERROR_VOTING_PERIOD_NOT_PASSED = "PptE01"; string constant ERROR_NO_MORE_MESSAGES = "PptE02"; @@ -539,20 +588,16 @@ contract PollProcessorAndTallyer is Verifier public verifier; - constructor( - Verifier _verifier - ) { + constructor(Verifier _verifier) { verifier = _verifier; } modifier votingPeriodOver(Poll _poll) { - (uint256 deployTime, uint256 duration) = _poll.getDeployTimeAndDuration(); + (uint256 deployTime, uint256 duration) = _poll + .getDeployTimeAndDuration(); // Require that the voting period is over uint256 secondsPassed = block.timestamp - deployTime; - require( - secondsPassed > duration, - ERROR_VOTING_PERIOD_NOT_PASSED - ); + require(secondsPassed > duration, ERROR_VOTING_PERIOD_NOT_PASSED); _; } @@ -580,11 +625,7 @@ contract PollProcessorAndTallyer is Poll _poll, uint256 _newSbCommitment, uint256[8] memory _proof - ) - public - onlyOwner - votingPeriodOver(_poll) - { + ) public onlyOwner votingPeriodOver(_poll) { // There must be unprocessed messages require(!processingComplete, ERROR_NO_MORE_MESSAGES); @@ -592,11 +633,11 @@ contract PollProcessorAndTallyer is require(_poll.stateAqMerged(), ERROR_STATE_AQ_NOT_MERGED); // Retrieve stored vals - ( , , uint8 messageTreeDepth,) = _poll.treeDepths(); - (uint256 messageBatchSize,, ) = _poll.batchSizes(); + (, , uint8 messageTreeDepth, ) = _poll.treeDepths(); + (uint256 messageBatchSize, , ) = _poll.batchSizes(); AccQueue messageAq; - (, , messageAq) = _poll.extContracts(); + (, , messageAq, ) = _poll.extContracts(); // Require that the message queue has been merged uint256 messageRoot = messageAq.getMainRoot(messageTreeDepth); @@ -612,7 +653,8 @@ contract PollProcessorAndTallyer is if (r == 0) { currentMessageBatchIndex = - (numMessages / messageBatchSize) * messageBatchSize; + (numMessages / messageBatchSize) * + messageBatchSize; } else { currentMessageBatchIndex = numMessages; } @@ -660,11 +702,11 @@ contract PollProcessorAndTallyer is uint256 _newSbCommitment, uint256[8] memory _proof ) internal view returns (bool) { - - ( , , uint8 messageTreeDepth, uint8 voteOptionTreeDepth) = _poll.treeDepths(); - (uint256 messageBatchSize,,) = _poll.batchSizes(); + (, , uint8 messageTreeDepth, uint8 voteOptionTreeDepth) = _poll + .treeDepths(); + (uint256 messageBatchSize, , ) = _poll.batchSizes(); (uint256 numSignUps, ) = _poll.numSignUpsAndMessages(); - (VkRegistry vkRegistry, IMACI maci, ) = _poll.extContracts(); + (VkRegistry vkRegistry, IMACI maci, , ) = _poll.extContracts(); // Calculate the public input hash (a SHA256 hash of several values) uint256 publicInputHash = genProcessMessagesPublicInputHash( @@ -712,7 +754,8 @@ contract PollProcessorAndTallyer is _numSignUps ); - (uint256 deployTime, uint256 duration) = _poll.getDeployTimeAndDuration(); + (uint256 deployTime, uint256 duration) = _poll + .getDeployTimeAndDuration(); uint256[] memory input = new uint256[](6); input[0] = packedVals; @@ -731,7 +774,7 @@ contract PollProcessorAndTallyer is * representation of four 50-bit values. This function generates this * 250-bit value, which consists of the maximum number of vote options, the * number of signups, the current message batch index, and the end index of - * the current batch. + * the current batch. */ function genProcessMessagesPackedVals( Poll _poll, @@ -740,7 +783,7 @@ contract PollProcessorAndTallyer is ) public view returns (uint256) { (, uint256 maxVoteOptions) = _poll.maxValues(); (, uint256 numMessages) = _poll.numSignUpsAndMessages(); - (uint8 mbs,,) = _poll.batchSizes(); + (uint8 mbs, , ) = _poll.batchSizes(); uint256 messageBatchSize = uint256(mbs); uint256 batchEndIndex = _currentMessageBatchIndex + messageBatchSize; @@ -748,8 +791,7 @@ contract PollProcessorAndTallyer is batchEndIndex = numMessages; } - uint256 result = - maxVoteOptions + + uint256 result = maxVoteOptions + (_numSignUps << uint256(50)) + (_currentMessageBatchIndex << uint256(100)) + (batchEndIndex << uint256(150)); @@ -765,15 +807,17 @@ contract PollProcessorAndTallyer is sbCommitment = _newSbCommitment; processingComplete = _processingComplete; currentMessageBatchIndex = _currentMessageBatchIndex; - numBatchesProcessed ++; + numBatchesProcessed++; } - - function genSubsidyPackedVals(uint256 _numSignUps) public view returns (uint256) { + function genSubsidyPackedVals(uint256 _numSignUps) + public + view + returns (uint256) + { // TODO: ensure that each value is less than or equal to 2 ** 50 - uint256 result = - (_numSignUps << uint256(100)) + - (rbi << uint256(50))+ + uint256 result = (_numSignUps << uint256(100)) + + (rbi << uint256(50)) + cbi; return result; @@ -787,32 +831,24 @@ contract PollProcessorAndTallyer is uint256[] memory input = new uint256[](4); input[0] = packedVals; input[1] = sbCommitment; - input[2] = subsidyCommitment; - input[3] = _newSubsidyCommitment; + input[2] = subsidyCommitment; + input[3] = _newSubsidyCommitment; uint256 inputHash = sha256Hash(input); return inputHash; } - function updateSubsidy( Poll _poll, uint256 _newSubsidyCommitment, uint256[8] memory _proof - ) - public - onlyOwner - votingPeriodOver(_poll) - { + ) public onlyOwner votingPeriodOver(_poll) { // Require that all messages have been processed - require( - processingComplete, - ERROR_PROCESSING_NOT_COMPLETE - ); + require(processingComplete, ERROR_PROCESSING_NOT_COMPLETE); - - (uint8 intStateTreeDepth,,,uint8 voteOptionTreeDepth) = _poll.treeDepths(); - uint256 subsidyBatchSize = 5 ** intStateTreeDepth; // treeArity is fixed to 5 - (uint256 numSignUps,) = _poll.numSignUpsAndMessages(); + (uint8 intStateTreeDepth, , , uint8 voteOptionTreeDepth) = _poll + .treeDepths(); + uint256 subsidyBatchSize = 5**intStateTreeDepth; // treeArity is fixed to 5 + (uint256 numSignUps, ) = _poll.numSignUpsAndMessages(); uint256 numLeaves = numSignUps + 1; // Require that there are untalied ballots left @@ -825,13 +861,20 @@ contract PollProcessorAndTallyer is ERROR_ALL_SUBSIDY_CALCULATED ); - bool isValid = verifySubsidyProof(_poll,_proof,numSignUps, _newSubsidyCommitment); + bool isValid = verifySubsidyProof( + _poll, + _proof, + numSignUps, + _newSubsidyCommitment + ); require(isValid, ERROR_INVALID_SUBSIDY_PROOF); subsidyCommitment = _newSubsidyCommitment; increaseSubsidyIndex(subsidyBatchSize, numLeaves); } - function increaseSubsidyIndex(uint256 batchSize, uint256 numLeaves) internal { + function increaseSubsidyIndex(uint256 batchSize, uint256 numLeaves) + internal + { if (cbi * batchSize + batchSize < numLeaves) { cbi++; } else { @@ -846,8 +889,9 @@ contract PollProcessorAndTallyer is uint256 _numSignUps, uint256 _newSubsidyCommitment ) public view returns (bool) { - (uint8 intStateTreeDepth,,,uint8 voteOptionTreeDepth) = _poll.treeDepths(); - (VkRegistry vkRegistry, IMACI maci, ) = _poll.extContracts(); + (uint8 intStateTreeDepth, , , uint8 voteOptionTreeDepth) = _poll + .treeDepths(); + (VkRegistry vkRegistry, IMACI maci, , ) = _poll.extContracts(); // Get the verifying key VerifyingKey memory vk = vkRegistry.getSubsidyVk( @@ -857,11 +901,14 @@ contract PollProcessorAndTallyer is ); // Get the public inputs - uint256 publicInputHash = genSubsidyPublicInputHash(_numSignUps, _newSubsidyCommitment); + uint256 publicInputHash = genSubsidyPublicInputHash( + _numSignUps, + _newSubsidyCommitment + ); // Verify the proof return verifier.verify(_proof, vk, publicInputHash); - } + } /* * Pack the batch start index and number of signups into a 100-bit value. @@ -871,10 +918,8 @@ contract PollProcessorAndTallyer is uint256 _batchStartIndex, uint256 _tallyBatchSize ) public pure returns (uint256) { - // TODO: ensure that each value is less than or equal to 2 ** 50 - uint256 result = - (_batchStartIndex / _tallyBatchSize) + + uint256 result = (_batchStartIndex / _tallyBatchSize) + (_numSignUps << uint256(50)); return result; @@ -904,26 +949,16 @@ contract PollProcessorAndTallyer is Poll _poll, uint256 _newTallyCommitment, uint256[8] memory _proof - ) - public - onlyOwner - votingPeriodOver(_poll) - { + ) public onlyOwner votingPeriodOver(_poll) { // Require that all messages have been processed - require( - processingComplete, - ERROR_PROCESSING_NOT_COMPLETE - ); + require(processingComplete, ERROR_PROCESSING_NOT_COMPLETE); - ( , uint256 tallyBatchSize,) = _poll.batchSizes(); + (, uint256 tallyBatchSize, ) = _poll.batchSizes(); uint256 batchStartIndex = tallyBatchNum * tallyBatchSize; - (uint256 numSignUps,) = _poll.numSignUpsAndMessages(); + (uint256 numSignUps, ) = _poll.numSignUpsAndMessages(); // Require that there are untalied ballots left - require( - batchStartIndex <= numSignUps, - ERROR_ALL_BALLOTS_TALLIED - ); + require(batchStartIndex <= numSignUps, ERROR_ALL_BALLOTS_TALLIED); bool isValid = verifyTallyProof( _poll, @@ -937,19 +972,19 @@ contract PollProcessorAndTallyer is // Update the tally commitment and the tally batch num tallyCommitment = _newTallyCommitment; - tallyBatchNum ++; + tallyBatchNum++; } /* - * @notice Verify the tally proof using the verifiying key - * @param _poll contract address of the poll proof to be verified - * @param _proof the proof generated after processing all messages - * @param _numSignUps number of signups for a given poll - * @param _batchStartIndex the number of batches multiplied by the size of the batch - * @param _tallyBatchSize batch size for the tally - * @param _newTallyCommitment the tally commitment to be verified at a given batch index - * @return valid a boolean representing successful verification - */ + * @notice Verify the tally proof using the verifiying key + * @param _poll contract address of the poll proof to be verified + * @param _proof the proof generated after processing all messages + * @param _numSignUps number of signups for a given poll + * @param _batchStartIndex the number of batches multiplied by the size of the batch + * @param _tallyBatchSize batch size for the tally + * @param _newTallyCommitment the tally commitment to be verified at a given batch index + * @return valid a boolean representing successful verification + */ function verifyTallyProof( Poll _poll, uint256[8] memory _proof, @@ -958,9 +993,10 @@ contract PollProcessorAndTallyer is uint256 _tallyBatchSize, uint256 _newTallyCommitment ) public view returns (bool) { - (uint8 intStateTreeDepth,,,uint8 voteOptionTreeDepth) = _poll.treeDepths(); + (uint8 intStateTreeDepth, , , uint8 voteOptionTreeDepth) = _poll + .treeDepths(); - (VkRegistry vkRegistry, IMACI maci, ) = _poll.extContracts(); + (VkRegistry vkRegistry, IMACI maci, , ) = _poll.extContracts(); // Get the verifying key VerifyingKey memory vk = vkRegistry.getTallyVk( diff --git a/contracts/contracts/TopupCredit.sol b/contracts/contracts/TopupCredit.sol new file mode 100644 index 0000000000..3f10a07c56 --- /dev/null +++ b/contracts/contracts/TopupCredit.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.2; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract TopupCredit is ERC20, Ownable { + uint8 private _decimals = 1; + uint256 MAXIMUM_AIRDROP_AMOUNT = 100000 * 10**_decimals; + + constructor() ERC20("TopupCredit", "TopupCredit") { + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function airdropTo(address account, uint256 amount) public { + require(amount < MAXIMUM_AIRDROP_AMOUNT); + _mint(account, amount); + } + + function airdrop(uint256 amount) public { + require(amount < MAXIMUM_AIRDROP_AMOUNT, "amount exceed maximum limit"); + _mint(msg.sender, amount); + } +} diff --git a/contracts/contracts/crypto/SnarkConstants.sol b/contracts/contracts/crypto/SnarkConstants.sol index 1f8eac5a31..351fd658fe 100644 --- a/contracts/contracts/crypto/SnarkConstants.sol +++ b/contracts/contracts/crypto/SnarkConstants.sol @@ -4,4 +4,14 @@ pragma solidity ^0.7.2; contract SnarkConstants { // The scalar field uint256 internal constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + // The public key here is the first Pedersen base + // point from iden3's circomlib implementation of the Pedersen hash. + // Since it is generated using a hash-to-curve function, we are + // confident that no-one knows the private key associated with this + // public key. See: + // https://github.com/iden3/circomlib/blob/d5ed1c3ce4ca137a6b3ca48bec4ac12c1b38957a/src/pedersen_printbases.js + // Its hash should equal + // 6769006970205099520508948723718471724660867171122235270773600567925038008762. + uint256 internal constant PAD_PUBKEY_X = 10457101036533406547632367118273992217979173478358440826365724437999023779287; + uint256 internal constant PAD_PUBKEY_Y = 19824078218392094440610104313265183977899662750282163392862422243483260492317; } diff --git a/contracts/ts/__tests__/MACI.test.ts b/contracts/ts/__tests__/MACI.test.ts index 51798d5132..8753b275ad 100644 --- a/contracts/ts/__tests__/MACI.test.ts +++ b/contracts/ts/__tests__/MACI.test.ts @@ -5,7 +5,7 @@ import { parseArtifact, getDefaultSigner } from '../deploy' import { deployTestContracts } from '../utils' import { genMaciStateFromContract } from '../genMaciState' import { - Command, + PCommand, VerifyingKey, Keypair, } from 'maci-domainobjs' @@ -351,7 +351,7 @@ describe('MACI', () => { it('should publish a message to the Poll contract', async () => { const keypair = new Keypair() - const command = new Command( + const command = new PCommand( BigInt(1), keypair.pubKey, BigInt(0), @@ -381,7 +381,7 @@ describe('MACI', () => { await timeTravel(signer.provider, Number(dd[0]) + 1) const keypair = new Keypair() - const command = new Command( + const command = new PCommand( BigInt(0), keypair.pubKey, BigInt(0), diff --git a/contracts/ts/deploy.ts b/contracts/ts/deploy.ts index 8e9557e45b..2a8bfe2232 100644 --- a/contracts/ts/deploy.ts +++ b/contracts/ts/deploy.ts @@ -24,6 +24,7 @@ const parseArtifact = (filename: string) => { filePath += `${filename}.sol` } + if (filename.includes('Verifier')) { filePath += 'crypto/Verifier.sol/' } @@ -117,6 +118,12 @@ const genJsonRpcDeployer = ( ) } +const deployTopupCredit = async () => { + const signer = await getDefaultSigner() + const topupCreditFactory = await ethers.getContractFactory('TopupCredit', signer) + return await topupCreditFactory.deploy() +} + const deployVkRegistry = async () => { const signer = await getDefaultSigner() const vkRegistryFactory = await ethers.getContractFactory('VkRegistry', signer) @@ -235,6 +242,7 @@ const deployMaci = async ( initialVoiceCreditBalanceAddress: string, verifierContractAddress: string, vkRegistryContractAddress: string, + topupCreditContractAddress: string, quiet = false, ) => { @@ -292,6 +300,7 @@ const deployMaci = async ( await (await (maciContract.init( vkRegistryContractAddress, messageAqContract.address, + topupCreditContractAddress ))).wait() const [ AccQueueQuinaryMaciAbi, ] = parseArtifact('AccQueue') @@ -336,6 +345,7 @@ const writeContractAddress = ( } export { + deployTopupCredit, deployVkRegistry, deployMaci, deploySignupToken, diff --git a/contracts/ts/genMaciState.ts b/contracts/ts/genMaciState.ts index b2dd3b1a40..b597d20af7 100644 --- a/contracts/ts/genMaciState.ts +++ b/contracts/ts/genMaciState.ts @@ -237,6 +237,12 @@ const genMaciStateFromContract = async ( fromBlock: fromBlock, }) + const topupLogs = await provider.getLogs({ + ...pollContract.filters.TopupMessage(), + fromBlock: fromBlock, + }) + + const mergeMaciStateAqSubRootsLogs = await provider.getLogs({ ...pollContract.filters.MergeMaciStateAqSubRoots(), fromBlock: fromBlock, @@ -262,13 +268,15 @@ const genMaciStateFromContract = async ( const event = pollIface.parseLog(log) const message = new Message( - event.args._message[0].map((x) => BigInt(x)), + BigInt(event.args._message[0]), + event.args._message[1].map((x) => BigInt(x)), ) const encPubKey = new PubKey( event.args._encPubKey.map((x) => BigInt(x.toString())) ) + actions.push({ type: 'PublishMessage', // @ts-ignore @@ -282,6 +290,27 @@ const genMaciStateFromContract = async ( }) } + for (const log of topupLogs) { + assert(log != undefined) + const event = pollIface.parseLog(log) + + const message = new Message( + BigInt(event.args._message[0]), + event.args._message[1].map((x) => BigInt(x)), + ) + + actions.push({ + type: 'TopupMessage', + // @ts-ignore + blockNumber: log.blockNumber, + // @ts-ignore + transactionIndex: log.transactionIndex, + data: { + message, + } + }) + } + for (const log of mergeMaciStateAqSubRootsLogs) { assert(log != undefined) const event = pollIface.parseLog(log) @@ -386,6 +415,10 @@ const genMaciStateFromContract = async ( action.data.message, action.data.encPubKey, ) + } else if (action['type'] === 'TopupMessage') { + maciState.polls[pollId].topupMessage( + action.data.message, + ) } else if (action['type'] === 'MergeMaciStateAqSubRoots') { maciState.stateAq.mergeSubRoots( action.data.numSrQueueOps, diff --git a/contracts/ts/index.ts b/contracts/ts/index.ts index 882a8c5496..23fc834b63 100644 --- a/contracts/ts/index.ts +++ b/contracts/ts/index.ts @@ -1,6 +1,7 @@ import { genJsonRpcDeployer, deployMockVerifier, + deployTopupCredit, deployVkRegistry, deployMaci, deploySignupToken, @@ -29,6 +30,7 @@ export { solDir, parseArtifact, genJsonRpcDeployer, + deployTopupCredit, deployVkRegistry, deployMaci, deployMockVerifier, diff --git a/contracts/ts/utils.ts b/contracts/ts/utils.ts index e4f4528a56..c6511f602a 100644 --- a/contracts/ts/utils.ts +++ b/contracts/ts/utils.ts @@ -6,6 +6,7 @@ interface SnarkProof { import { deployVkRegistry, + deployTopupCredit, deployMaci, deployPpt, deployMockVerifier, @@ -50,11 +51,14 @@ const deployTestContracts = async ( const vkRegistryContract = await deployVkRegistry() await vkRegistryContract.deployTransaction.wait() + const topupCreditContract = await deployTopupCredit() + const contracts = await deployMaci( gatekeeperContract.address, constantIntialVoiceCreditProxyContract.address, mockVerifierContract.address, vkRegistryContract.address, + topupCreditContract.address ) const maciContract = contracts.maciContract diff --git a/core/ts/MaciState.ts b/core/ts/MaciState.ts index ec6c32ca59..6501012bc4 100644 --- a/core/ts/MaciState.ts +++ b/core/ts/MaciState.ts @@ -16,6 +16,8 @@ import { PubKey, VerifyingKey, Command, + PCommand, + TCommand, Message, Keypair, StateLeaf, @@ -65,7 +67,6 @@ class Poll { public messageTree: IncrementalQuinTree public commands: Command[] = [] - public signatures: Signature[] = [] public encPubKeys: PubKey[] = [] public STATE_TREE_ARITY = 5 public MESSAGE_TREE_ARITY = 5 @@ -184,6 +185,27 @@ class Poll { this.stateCopied = true } + // Insert topup message into commands + public topupMessage = (_message: Message) => { + assert(_message.msgType == BigInt(2)) + for (const d of _message.data) { + assert(d < SNARK_FIELD_SIZE) + } + this.messages.push(_message) + let padKey = new PubKey([ + BigInt('10457101036533406547632367118273992217979173478358440826365724437999023779287'), + BigInt('19824078218392094440610104313265183977899662750282163392862422243483260492317'), + ]) + + this.encPubKeys.push(padKey) + const messageLeaf = _message.hash(padKey) + this.messageAq.enqueue(messageLeaf) + this.messageTree.insert(messageLeaf) + + const command = new TCommand(_message.data[0],_message.data[1]) + this.commands.push(command) + } + /* * Inserts a Message and the corresponding public key used to generate the * ECDH shared key which was used to encrypt said message. @@ -192,6 +214,7 @@ class Poll { _message: Message, _encPubKey: PubKey, ) => { + assert(_message.msgType == BigInt(1)) assert( _encPubKey.rawPubKey[0] < SNARK_FIELD_SIZE && _encPubKey.rawPubKey[1] < SNARK_FIELD_SIZE @@ -212,9 +235,8 @@ class Poll { this.coordinatorKeypair.privKey, _encPubKey, ) - const { command, signature } = Command.decrypt(_message, sharedKey) + const { command, signature } = PCommand.decrypt(_message, sharedKey) this.commands.push(command) - this.signatures.push(signature) } /* @@ -337,89 +359,191 @@ class Poll { const currentVoteWeights: BigInt[] = [] const currentVoteWeightsPathElements: any[] = [] + const topupAmounts: BigInt[] = [] + const topupStateIndexes: BigInt[] = [] + const topupStateLeaves: StateLeaf[] = [] + const topupStateLeavesPathElements: any[] = [] for (let i = 0; i < batchSize; i ++) { - const m = this.currentMessageBatchIndex + batchSize - i - 1 - - try{ - // If the command is valid - - const r = this.processMessage(m) - // console.log(messageIndex, r ? 'valid' : 'invalid') - // console.log("r:"+r.newStateLeaf ) - // DONE: replace with try/catch after implementing error - // handling - const index = r.stateLeafIndex - - currentStateLeaves.unshift(r.originalStateLeaf) - currentBallots.unshift(r.originalBallot) - currentVoteWeights.unshift(r.originalVoteWeight) - currentVoteWeightsPathElements.unshift(r.originalVoteWeightsPathElements) - - currentStateLeavesPathElements.unshift(r.originalStateLeafPathElements) - currentBallotsPathElements.unshift(r.originalBallotPathElements) - - this.stateLeaves[index] = r.newStateLeaf.copy() - this.stateTree.update(index, r.newStateLeaf.hash()) - - this.ballots[index] = r.newBallot - this.ballotTree.update(index, r.newBallot.hash()) - - }catch(e){ - if (e.message === 'no-op') { - - // Since the command is invalid, use a blank state leaf - currentStateLeaves.unshift(this.stateLeaves[0].copy()) - currentStateLeavesPathElements.unshift( - this.stateTree.genMerklePath(0).pathElements - ) - - currentBallots.unshift(this.ballots[0].copy()) - currentBallotsPathElements.unshift( - this.ballotTree.genMerklePath(0).pathElements - ) - - // Since the command is invalid, use vote option index 0 - currentVoteWeights.unshift(this.ballots[0].votes[0]) - - // No need to iterate through the entire votes array if the - // remaining elements are 0 - let lastIndexToInsert = this.ballots[0].votes.length - 1 - while (lastIndexToInsert > 0) { - if (this.ballots[0].votes[lastIndexToInsert] === BigInt(0)) { - lastIndexToInsert -- - } else { - break - } - } - - const vt = new IncrementalQuinTree( - this.treeDepths.voteOptionTreeDepth, - BigInt(0), - 5, - hash5, - ) - for (let i = 0; i <= lastIndexToInsert; i ++) { - vt.insert(this.ballots[0].votes[i]) - } - currentVoteWeightsPathElements.unshift( - vt.genMerklePath(0).pathElements - ) - - - } else { - throw e - } + const idx = this.currentMessageBatchIndex + batchSize - i - 1 + assert(idx >= 0) + let message + if (idx >= this.messages.length) { + message = new Message(BigInt(1), Array(10).fill(BigInt(0))) // when idx large than actual size, just use something to pass to switch + } else { + message = this.messages[idx] } - - + switch(message.msgType) { + case BigInt(1): + try{ + // still needed for top message + topupAmounts.unshift(BigInt(0)) + topupStateIndexes.unshift(BigInt(0)) + topupStateLeaves.unshift(this.stateLeaves[0].copy()) + topupStateLeavesPathElements.unshift( + this.stateTree.genMerklePath(0).pathElements + ) + + // If the command is valid + const r = this.processMessage(idx) + // console.log(messageIndex, r ? 'valid' : 'invalid') + // console.log("r:"+r.newStateLeaf ) + // DONE: replace with try/catch after implementing error + // handling + const index = r.stateLeafIndex + + currentStateLeaves.unshift(r.originalStateLeaf) + currentBallots.unshift(r.originalBallot) + currentVoteWeights.unshift(r.originalVoteWeight) + currentVoteWeightsPathElements.unshift(r.originalVoteWeightsPathElements) + + currentStateLeavesPathElements.unshift(r.originalStateLeafPathElements) + currentBallotsPathElements.unshift(r.originalBallotPathElements) + + this.stateLeaves[index] = r.newStateLeaf.copy() + this.stateTree.update(index, r.newStateLeaf.hash()) + + this.ballots[index] = r.newBallot + this.ballotTree.update(index, r.newBallot.hash()) + + }catch(e){ + if (e.message === 'no-op') { + // Since the command is invalid, use a blank state leaf + currentStateLeaves.unshift(this.stateLeaves[0].copy()) + currentStateLeavesPathElements.unshift( + this.stateTree.genMerklePath(0).pathElements + ) + + currentBallots.unshift(this.ballots[0].copy()) + currentBallotsPathElements.unshift( + this.ballotTree.genMerklePath(0).pathElements + ) + + // Since the command is invalid, use vote option index 0 + currentVoteWeights.unshift(this.ballots[0].votes[0]) + + // No need to iterate through the entire votes array if the + // remaining elements are 0 + let lastIndexToInsert = this.ballots[0].votes.length - 1 + while (lastIndexToInsert > 0) { + if (this.ballots[0].votes[lastIndexToInsert] === BigInt(0)) { + lastIndexToInsert -- + } else { + break + } + } + + const vt = new IncrementalQuinTree( + this.treeDepths.voteOptionTreeDepth, + BigInt(0), + 5, + hash5, + ) + for (let i = 0; i <= lastIndexToInsert; i ++) { + vt.insert(this.ballots[0].votes[i]) + } + currentVoteWeightsPathElements.unshift( + vt.genMerklePath(0).pathElements + ) + + + } else { + throw e + } + } + break + case BigInt(2): + try { + + // still need to generate vote type message circuit inputs + currentStateLeaves.unshift(this.stateLeaves[0].copy()) + currentStateLeavesPathElements.unshift( + this.stateTree.genMerklePath(0).pathElements + ) + currentBallots.unshift(this.ballots[0].copy()) + currentBallotsPathElements.unshift( + this.ballotTree.genMerklePath(0).pathElements + ) + // Since the command is invalid, use vote option index 0 + currentVoteWeights.unshift(this.ballots[0].votes[0]) + // No need to iterate through the entire votes array if the + // remaining elements are 0 + let lastIndexToInsert = this.ballots[0].votes.length - 1 + while (lastIndexToInsert > 0) { + if (this.ballots[0].votes[lastIndexToInsert] === BigInt(0)) { + lastIndexToInsert -- + } else { + break + } + } + const vt = new IncrementalQuinTree( + this.treeDepths.voteOptionTreeDepth, + BigInt(0), + 5, + hash5, + ) + for (let i = 0; i <= lastIndexToInsert; i ++) { + vt.insert(this.ballots[0].votes[i]) + } + currentVoteWeightsPathElements.unshift( + vt.genMerklePath(0).pathElements + ) + + // -------------------------------------- + // generate topup circuit inputs + let stateIndex = BigInt(message.data[0]) + let amount = BigInt(message.data[1]) + + if ( + stateIndex >= BigInt(this.ballots.length) || + stateIndex < BigInt(1) || + amount < BigInt(1) + ) { + throw Error("no-op") + return {} + } + topupStateIndexes.unshift(stateIndex) + topupAmounts.unshift(amount) + topupStateLeaves.unshift(this.stateLeaves[Number(stateIndex)].copy()) + topupStateLeavesPathElements.unshift( + this.stateTree.genMerklePath(Number(stateIndex)).pathElements + ) + + const newStateLeaf = this.stateLeaves[Number(stateIndex)].copy() + newStateLeaf.voiceCreditBalance = BigInt(newStateLeaf.voiceCreditBalance.valueOf()) + BigInt(amount) + this.stateLeaves[Number(stateIndex)] = newStateLeaf + this.stateTree.update(Number(stateIndex), newStateLeaf.hash()) + + + } catch(e) { + if (e.message === "no-op") { + topupAmounts.unshift(BigInt(0)) + topupStateIndexes.unshift(BigInt(0)) + topupStateLeaves.unshift(this.stateLeaves[0].copy()) + topupStateLeavesPathElements.unshift( + this.stateTree.genMerklePath(0).pathElements + ) + } else { + throw e + } + } + break + default: + break + } // end msgType switch } + + // loop for batch circuitInputs.currentStateLeaves = currentStateLeaves.map((x) => x.asCircuitInputs()) circuitInputs.currentStateLeavesPathElements = currentStateLeavesPathElements circuitInputs.currentBallots = currentBallots.map((x) => x.asCircuitInputs()) circuitInputs.currentBallotsPathElements = currentBallotsPathElements circuitInputs.currentVoteWeights = currentVoteWeights circuitInputs.currentVoteWeightsPathElements = currentVoteWeightsPathElements + circuitInputs.topupAmounts = topupAmounts.map((x) => BigInt(x.toString())) + circuitInputs.topupStateIndexes = topupStateIndexes.map((x) => BigInt(x.toString())) + circuitInputs.topupStateLeaves = topupStateLeaves.map((x) => x.asCircuitInputs()) + circuitInputs.topupStateLeavesPathElements = topupStateLeavesPathElements this.numBatchesProcessed ++ @@ -510,12 +634,6 @@ class Poll { } encPubKeys = encPubKeys.slice(_index, _index + messageBatchSize) - const stateIndices: number[] = [] - for (let i = 0; i < messageBatchSize; i ++) { - const stateIndex = Number(commands[i].stateIndex) - stateIndices.push(stateIndex) - } - const msgRoot = this.messageAq.getRoot(this.treeDepths.messageTreeDepth) const currentStateRoot = this.stateTree.root @@ -593,7 +711,7 @@ class Poll { this.coordinatorKeypair.privKey, encPubKey, ) - const { command, signature } = Command.decrypt(message, sharedKey) + const { command, signature } = PCommand.decrypt(message, sharedKey) const stateLeafIndex = BigInt(`${command.stateIndex}`) @@ -1177,15 +1295,6 @@ class Poll { copied.stateLeaves = this.stateLeaves.map((x: StateLeaf) => x.copy()) copied.messages = this.messages.map((x: Message) => x.copy()) copied.commands = this.commands.map((x: Command) => x.copy()) - copied.signatures = this.signatures.map((x: Signature) => { - return { - R8: [ - BigInt(x.R8[0].toString()), - BigInt(x.R8[1].toString()), - ], - S: BigInt(x.S.toString()), - } - }) copied.ballots = this.ballots.map((x: Ballot) => x.copy()) copied.encPubKeys = this.encPubKeys.map((x: PubKey) => x.copy()) if (this.ballotTree) { diff --git a/core/ts/__tests__/MaciState.test.ts b/core/ts/__tests__/MaciState.test.ts index 58122c904f..97c51e4e46 100644 --- a/core/ts/__tests__/MaciState.test.ts +++ b/core/ts/__tests__/MaciState.test.ts @@ -4,7 +4,7 @@ import { } from '../' import { - Command, + PCommand, Message, Keypair, VerifyingKey, @@ -132,7 +132,7 @@ describe('MaciState', () => { testTallyVk, ) - const command = new Command( + const command = new PCommand( stateIndex, userKeypair.pubKey, voteOptionIndex, @@ -245,7 +245,7 @@ describe('MaciState', () => { for (let i = 0; i < messageBatchSize - 1; i ++) { const userKeypair = users[i] - const command = new Command( + const command = new PCommand( BigInt(i + 1), userKeypair.pubKey, BigInt(i), // vote option index @@ -270,7 +270,7 @@ describe('MaciState', () => { // 24 invalid votes for (let i = 0; i < messageBatchSize - 1; i ++) { const userKeypair = users[i] - const command = new Command( + const command = new PCommand( BigInt(i + 1), userKeypair.pubKey, BigInt(i), // vote option index @@ -417,7 +417,7 @@ describe('MaciState', () => { testProcessVk, testTallyVk, ) - const command = new Command( + const command = new PCommand( BigInt(0), userKeypair.pubKey, BigInt(0), diff --git a/crypto/ts/__tests__/Crypto.test.ts b/crypto/ts/__tests__/Crypto.test.ts index 14e20eba6b..ddec091432 100644 --- a/crypto/ts/__tests__/Crypto.test.ts +++ b/crypto/ts/__tests__/Crypto.test.ts @@ -7,7 +7,7 @@ import { sign, sha256Hash, hash5, - hash12, + hash13, verifySignature, genRandomSalt, } from '../' @@ -57,19 +57,19 @@ describe('Cryptographic operations', () => { ) }) - describe('hash12', () => { + describe('hash13', () => { it('Hashing a smaller array should work', () => { - const h = hash12([BigInt(1), BigInt(2), BigInt(3)]) + const h = hash13([BigInt(1), BigInt(2), BigInt(3)]) expect(h < SNARK_FIELD_SIZE).toBeTruthy() }) - it('Hashing more than 12 elements should throw', () => { + it('Hashing more than 13 elements should throw', () => { const arr: any[] = [] - for (let i = 0; i < 13; i++) { + for (let i = 0; i < 14; i++) { arr.push(BigInt(i)) } - expect(() => hash12(arr)).toThrow(TypeError) + expect(() => hash13(arr)).toThrow(TypeError) }) }) diff --git a/crypto/ts/index.ts b/crypto/ts/index.ts index a7ef5dd282..815ad75a31 100644 --- a/crypto/ts/index.ts +++ b/crypto/ts/index.ts @@ -182,10 +182,10 @@ const hash5 = (elements: Plaintext): BigInt => hashN(5, elements) /* * A convenience function for to use Poseidon to hash a Plaintext with - * no more than 12 elements + * no more than 13 elements */ -const hash12 = (elements: Plaintext): BigInt => { - const max = 12 +const hash13 = (elements: Plaintext): BigInt => { + const max = 13 const elementLength = elements.length if (elementLength > max) { throw new TypeError(`the length of the elements array should be at most 10; got ${elements.length}`) @@ -196,11 +196,12 @@ const hash12 = (elements: Plaintext): BigInt => { elementsPadded.push(BigInt(0)) } } - return poseidonT5([ - poseidonT6(elementsPadded.slice(0, 5)), - poseidonT6(elementsPadded.slice(5, 10)), - elementsPadded[10], + return poseidonT6([ + elementsPadded[0], + poseidonT6(elementsPadded.slice(1, 6)), + poseidonT6(elementsPadded.slice(6, 11)), elementsPadded[11], + elementsPadded[12], ]) } @@ -413,7 +414,7 @@ export { hash3, hash4, hash5, - hash12, + hash13, hashLeftRight, verifySignature, Signature, diff --git a/domainobjs/ts/__tests__/DomainObjs.test.ts b/domainobjs/ts/__tests__/DomainObjs.test.ts index 59243fdf8a..597e3bb4cf 100644 --- a/domainobjs/ts/__tests__/DomainObjs.test.ts +++ b/domainobjs/ts/__tests__/DomainObjs.test.ts @@ -2,7 +2,7 @@ import * as path from 'path' import * as fs from 'fs' import { StateLeaf, - Command, + PCommand, Keypair, PrivKey, PubKey, @@ -31,7 +31,7 @@ describe('Domain objects', () => { ) & BigInt(`${genRandomSalt()}`) } - const command: Command = new Command( + const command: PCommand = new PCommand( random50bitBigInt(), newPubKey, random50bitBigInt(), @@ -216,7 +216,7 @@ describe('Domain objects', () => { describe('Commands and Messages', () => { const signature = command.sign(privKey) const message = command.encrypt(signature, ecdhSharedKey) - const decrypted = Command.decrypt(message, ecdhSharedKey) + const decrypted = PCommand.decrypt(message, ecdhSharedKey) it ('command.sign() should produce a valid signature', () => { expect(command.verifySignature(signature, pubKey)).toBeTruthy() @@ -235,7 +235,7 @@ describe('Domain objects', () => { }) it('Command.copy() should perform a deep copy', () => { - const c1: Command = new Command( + const c1: PCommand = new PCommand( BigInt(10), newPubKey, BigInt(0), diff --git a/domainobjs/ts/index.ts b/domainobjs/ts/index.ts index 9befee4e18..aa1586cc0e 100644 --- a/domainobjs/ts/index.ts +++ b/domainobjs/ts/index.ts @@ -12,7 +12,7 @@ import { decrypt, sign, hashLeftRight, - hash12, + hash13, hash4, hash5, verifySignature, @@ -379,23 +379,27 @@ interface VoteOptionTreeLeaf { * An encrypted command and signature. */ class Message { + public msgType: BigInt public data: BigInt[] - public static DATA_LENGTH = 10 + public static DATA_LENGTH = 10 constructor ( + msgType: BigInt, data: BigInt[], ) { assert(data.length === Message.DATA_LENGTH) + this.msgType = msgType this.data = data } private asArray = (): BigInt[] => { - return this.data + return [this.msgType].concat(this.data) } public asContractParam = () => { return { - data: this.data.map((x: BigInt) => x.toString()), + msgType: this.msgType, + data: this.data.map((x:BigInt) => x.toString()), } } @@ -407,15 +411,17 @@ class Message { public hash = ( _encPubKey: PubKey, ): BigInt => { - return hash12([ - ...this.data, - ..._encPubKey.rawPubKey, - ]) + return hash13([ + ...[this.msgType], + ...this.data, + ..._encPubKey.rawPubKey, + ]) } public copy = (): Message => { return new Message( + BigInt(this.msgType.toString()), this.data.map((x: BigInt) => BigInt(x.toString())), ) } @@ -424,6 +430,9 @@ class Message { if (this.data.length !== m.data.length) { return false } + if (this.msgType !== m.msgType) { + return false + } for (let i = 0; i < this.data.length; i ++) { if (this.data[i] !== m.data[i]) { @@ -646,21 +655,51 @@ class StateLeaf implements IStateLeaf { } } -interface ICommand { - stateIndex: BigInt; - newPubKey: PubKey; - voteOptionIndex: BigInt; - newVoteWeight: BigInt; - nonce: BigInt; +class Command { + public cmdType: BigInt; + constructor() { + } + public copy = (): Command => { + throw new Error("Abstract method!") + } + public equals = (Command): boolean => { + throw new Error("Abstract method!") + } +} + + + +class TCommand extends Command { + public cmdType: BigInt + public stateIndex: BigInt + public amount: BigInt + public pollId: BigInt + + constructor(stateIndex: BigInt, amount: BigInt) { + super() + this.cmdType = BigInt(2) + this.stateIndex = stateIndex + this.amount = amount + } + + public copy = (): TCommand => { + return new TCommand( + BigInt(this.stateIndex.toString()), + BigInt(this.amount.toString()), + ) + } - sign: (PrivKey) => Signature; - encrypt: (EcdhSharedKey, Signature) => Message; + public equals = (command: TCommand): boolean => { + return this.stateIndex === command.stateIndex && + this.amount === command.amount + } } /* * Unencrypted data whose fields include the user's public key, vote etc. */ -class Command implements ICommand { +class PCommand extends Command { + public cmdType: BigInt public stateIndex: BigInt public newPubKey: PubKey public voteOptionIndex: BigInt @@ -678,6 +717,7 @@ class Command implements ICommand { pollId: BigInt, salt: BigInt = genRandomSalt(), ) { + super() const limit50Bits = BigInt(2 ** 50) assert(limit50Bits >= stateIndex) assert(limit50Bits >= voteOptionIndex) @@ -685,6 +725,7 @@ class Command implements ICommand { assert(limit50Bits >= nonce) assert(limit50Bits >= pollId) + this.cmdType = BigInt(1) this.stateIndex = stateIndex this.newPubKey = newPubKey this.voteOptionIndex = voteOptionIndex @@ -694,9 +735,9 @@ class Command implements ICommand { this.salt = salt } - public copy = (): Command => { + public copy = (): PCommand => { - return new Command( + return new PCommand( BigInt(this.stateIndex.toString()), this.newPubKey.copy(), BigInt(this.voteOptionIndex.toString()), @@ -737,8 +778,7 @@ class Command implements ICommand { /* * Check whether this command has deep equivalence to another command */ - public equals = (command: Command): boolean => { - + public equals = (command: PCommand): boolean => { return this.stateIndex === command.stateIndex && this.newPubKey[0] === command.newPubKey[0] && this.newPubKey[1] === command.newPubKey[1] && @@ -805,7 +845,7 @@ class Command implements ICommand { const ciphertext: Ciphertext = encrypt(plaintext, sharedKey, BigInt(0)) - const message = new Message(ciphertext) + const message = new Message(BigInt(1), ciphertext) return message } @@ -852,7 +892,7 @@ class Command implements ICommand { const newPubKey = new PubKey([decrypted[1], decrypted[2]]) const salt = decrypted[3] - const command = new Command( + const command = new PCommand( stateIndex, newPubKey, voteOptionIndex, @@ -871,10 +911,13 @@ class Command implements ICommand { } } + export { StateLeaf, Ballot, VoteOptionTreeLeaf, + PCommand, + TCommand, Command, Message, Keypair, diff --git a/integrationTests/ts/__tests__/suites.ts b/integrationTests/ts/__tests__/suites.ts index bf963582dc..0f836152f1 100644 --- a/integrationTests/ts/__tests__/suites.ts +++ b/integrationTests/ts/__tests__/suites.ts @@ -4,7 +4,7 @@ import { PubKey, PrivKey, Keypair, - Command, + PCommand, } from 'maci-domainobjs' import { @@ -205,7 +205,7 @@ const executeSuite = async (data: any, expect: any) => { const encPrivKey = PrivKey.unserialize(publishRegMatch[2]) const encPubKey = new PubKey(genPubKey(encPrivKey.rawPrivKey)) - const command = new Command( + const command = new PCommand( BigInt(stateIndex), userKeypair.pubKey, BigInt(voteOptionIndex), diff --git a/package.json b/package.json index 48b5a626c5..0b23a7012d 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,14 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "^2.31.0", "@typescript-eslint/parser": "^2.31.0", + "cz-conventional-changelog": "^3.3.0", "eslint": "^6.8.0", "lerna": "^4.0.0", "typescript": "^4.2.3" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } } }