diff --git a/cli/package.json b/cli/package.json index df04fcaf29..ab6a26e981 100644 --- a/cli/package.json +++ b/cli/package.json @@ -16,7 +16,8 @@ "postbuild": "cp package.json ./build", "test": "ts-mocha --exit tests/*.test.ts", "test:e2e": "ts-mocha --exit tests/e2e.test.ts", - "test:e2e-subsidy": "ts-mocha --exit tests/e2e.subsidy.test.ts" + "test:e2e-subsidy": "ts-mocha --exit tests/e2e.subsidy.test.ts", + "test:keyChange": "ts-mocha --exit tests/keyChange.test.ts" }, "dependencies": { "@nomicfoundation/hardhat-toolbox": "^4.0.0", diff --git a/cli/tests/keyChange.test.ts b/cli/tests/keyChange.test.ts new file mode 100644 index 0000000000..57a6d13041 --- /dev/null +++ b/cli/tests/keyChange.test.ts @@ -0,0 +1,322 @@ +import { isArm } from "maci-circuits"; +import { + deploy, + deployPoll, + deployVkRegistryContract, + genProofs, + mergeMessages, + mergeSignups, + proveOnChain, + publish, + setVerifyingKeys, + signup, + timeTravel, + verify, +} from "../ts/commands"; +import { + INT_STATE_TREE_DEPTH, + MSG_BATCH_DEPTH, + MSG_TREE_DEPTH, + STATE_TREE_DEPTH, + VOTE_OPTION_TREE_DEPTH, + coordinatorPrivKey, + coordinatorPubKey, + processMessageTestZkeyPath, + tallyVotesTestZkeyPath, + testProcessMessagesWasmPath, + testProcessMessagesWitnessPath, + testProofsDirPath, + testRapidsnarkPath, + testTallyFilePath, + testTallyVotesWasmPath, + testTallyVotesWitnessPath, +} from "./constants"; +import { Keypair } from "maci-domainobjs"; +import { cleanVanilla } from "./utils"; +import { readFileSync } from "fs"; +import { expect } from "chai"; + +describe("keyChange tests", function () { + const useWasm = isArm(); + this.timeout(900000); + + // before all tests we deploy the vk registry contract and set the verifying keys + before(async () => { + // we deploy the vk registry contract + await deployVkRegistryContract(true); + // we set the verifying keys + await setVerifyingKeys( + STATE_TREE_DEPTH, + INT_STATE_TREE_DEPTH, + MSG_TREE_DEPTH, + VOTE_OPTION_TREE_DEPTH, + MSG_BATCH_DEPTH, + processMessageTestZkeyPath, + tallyVotesTestZkeyPath, + ); + }); + + describe("keyChange and new vote (new vote has same nonce)", () => { + after(async () => { + cleanVanilla(); + }); + const keypair1 = new Keypair(); + const keypair2 = new Keypair(); + const initialNonce = 1; + const initialVoteOption = 0; + const initialVoteAmount = 9; + const pollId = 0; + let stateIndex = 0; + const expectedTally = initialVoteAmount - 1; + const expectedPerVOteOptionTally = (initialVoteAmount - 1) ** 2; + + before(async () => { + // deploy the smart contracts + await deploy(STATE_TREE_DEPTH); + // deploy a poll contract + await deployPoll( + 90, + 25, + 25, + INT_STATE_TREE_DEPTH, + MSG_BATCH_DEPTH, + MSG_TREE_DEPTH, + VOTE_OPTION_TREE_DEPTH, + coordinatorPubKey, + ); + stateIndex = parseInt(await signup(keypair1.pubKey.serialize())); + await publish( + keypair1.pubKey.serialize(), + stateIndex, + initialVoteOption, + initialNonce, + pollId, + initialVoteAmount, + undefined, + undefined, + keypair1.privKey.serialize(), + ); + }); + it("should publish a message to change the user maci key and cast a new vote", async () => { + await publish( + keypair2.pubKey.serialize(), + stateIndex, + initialVoteOption, + initialNonce, + pollId, + initialVoteAmount - 1, + undefined, + undefined, + keypair1.privKey.serialize(), + ); + }); + it("should generate zk-SNARK proofs and verify them", async () => { + await timeTravel(90, true); + await mergeMessages(0, undefined, undefined, true); + await mergeSignups(0, undefined, undefined, true); + await genProofs( + testProofsDirPath, + testTallyFilePath, + tallyVotesTestZkeyPath, + processMessageTestZkeyPath, + 0, + undefined, + undefined, + testRapidsnarkPath, + testProcessMessagesWitnessPath, + testTallyVotesWitnessPath, + undefined, + coordinatorPrivKey, + undefined, + undefined, + testProcessMessagesWasmPath, + testTallyVotesWasmPath, + undefined, + useWasm, + ); + await proveOnChain("0", testProofsDirPath); + await verify("0", testTallyFilePath); + }); + it("should confirm the tally is correct", () => { + const tallyData = JSON.parse(readFileSync(testTallyFilePath).toString()); + expect(tallyData.results.tally[0]).to.equal(expectedTally.toString()); + expect(tallyData.perVOSpentVoiceCredits.tally[0]).to.equal(expectedPerVOteOptionTally.toString()); + }); + }); + + describe("keyChange and new vote (new vote has greater nonce and different vote option)", () => { + after(async () => { + cleanVanilla(); + }); + const keypair1 = new Keypair(); + const keypair2 = new Keypair(); + const initialNonce = 1; + const initialVoteOption = 0; + const initialVoteAmount = 9; + const pollId = 0; + let stateIndex = 0; + const expectedTally = initialVoteAmount; + const expectedPerVOteOptionTally = initialVoteAmount ** 2; + + before(async () => { + // deploy the smart contracts + await deploy(STATE_TREE_DEPTH); + // deploy a poll contract + await deployPoll( + 90, + 25, + 25, + INT_STATE_TREE_DEPTH, + MSG_BATCH_DEPTH, + MSG_TREE_DEPTH, + VOTE_OPTION_TREE_DEPTH, + coordinatorPubKey, + ); + stateIndex = parseInt(await signup(keypair1.pubKey.serialize())); + await publish( + keypair1.pubKey.serialize(), + stateIndex, + initialVoteOption, + initialNonce, + pollId, + initialVoteAmount, + undefined, + undefined, + keypair1.privKey.serialize(), + ); + }); + it("should publish a message to change the user maci key and cast a new vote", async () => { + await publish( + keypair2.pubKey.serialize(), + stateIndex, + initialVoteOption + 1, + initialNonce + 1, + pollId, + initialVoteAmount - 1, + undefined, + undefined, + keypair1.privKey.serialize(), + ); + }); + it("should generate zk-SNARK proofs and verify them", async () => { + await timeTravel(90, true); + await mergeMessages(0, undefined, undefined, true); + await mergeSignups(0, undefined, undefined, true); + await genProofs( + testProofsDirPath, + testTallyFilePath, + tallyVotesTestZkeyPath, + processMessageTestZkeyPath, + 0, + undefined, + undefined, + testRapidsnarkPath, + testProcessMessagesWitnessPath, + testTallyVotesWitnessPath, + undefined, + coordinatorPrivKey, + undefined, + undefined, + testProcessMessagesWasmPath, + testTallyVotesWasmPath, + undefined, + useWasm, + ); + await proveOnChain("0", testProofsDirPath); + await verify("0", testTallyFilePath); + }); + it("should confirm the tally is correct", () => { + const tallyData = JSON.parse(readFileSync(testTallyFilePath).toString()); + expect(tallyData.results.tally[0]).to.equal(expectedTally.toString()); + expect(tallyData.perVOSpentVoiceCredits.tally[0]).to.equal(expectedPerVOteOptionTally.toString()); + }); + }); + + describe("keyChange and new vote (new vote has same nonce and different vote option)", () => { + after(async () => { + cleanVanilla(); + }); + const keypair1 = new Keypair(); + const keypair2 = new Keypair(); + const initialNonce = 1; + const initialVoteOption = 0; + const initialVoteAmount = 9; + const pollId = 0; + let stateIndex = 0; + const expectedTally = initialVoteAmount - 3; + const expectedPerVOteOptionTally = (initialVoteAmount - 3) ** 2; + + before(async () => { + // deploy the smart contracts + await deploy(STATE_TREE_DEPTH); + // deploy a poll contract + await deployPoll( + 90, + 25, + 25, + INT_STATE_TREE_DEPTH, + MSG_BATCH_DEPTH, + MSG_TREE_DEPTH, + VOTE_OPTION_TREE_DEPTH, + coordinatorPubKey, + ); + stateIndex = parseInt(await signup(keypair1.pubKey.serialize())); + await publish( + keypair1.pubKey.serialize(), + stateIndex, + initialVoteOption, + initialNonce, + pollId, + initialVoteAmount, + undefined, + undefined, + keypair1.privKey.serialize(), + ); + }); + it("should publish a message to change the user maci key, and a new vote", async () => { + await publish( + keypair2.pubKey.serialize(), + stateIndex, + initialVoteOption + 2, + initialNonce, + pollId, + initialVoteAmount - 3, + undefined, + undefined, + keypair1.privKey.serialize(), + ); + }); + it("should generate zk-SNARK proofs and verify them", async () => { + await timeTravel(90, true); + await mergeMessages(0, undefined, undefined, true); + await mergeSignups(0, undefined, undefined, true); + await genProofs( + testProofsDirPath, + testTallyFilePath, + tallyVotesTestZkeyPath, + processMessageTestZkeyPath, + 0, + undefined, + undefined, + testRapidsnarkPath, + testProcessMessagesWitnessPath, + testTallyVotesWitnessPath, + undefined, + coordinatorPrivKey, + undefined, + undefined, + testProcessMessagesWasmPath, + testTallyVotesWasmPath, + undefined, + useWasm, + ); + await proveOnChain("0", testProofsDirPath); + await verify("0", testTallyFilePath); + }); + it("should confirm the tally is correct", () => { + const tallyData = JSON.parse(readFileSync(testTallyFilePath).toString()); + expect(tallyData.results.tally[2]).to.equal(expectedTally.toString()); + expect(tallyData.perVOSpentVoiceCredits.tally[2]).to.equal(expectedPerVOteOptionTally.toString()); + }); + }); +}); diff --git a/core/ts/__tests__/MaciState.test.ts b/core/ts/__tests__/MaciState.test.ts index 6f55d0c79e..1156a515ae 100644 --- a/core/ts/__tests__/MaciState.test.ts +++ b/core/ts/__tests__/MaciState.test.ts @@ -421,4 +421,118 @@ describe("MaciState", function () { expect(state.equals(m1)).to.be.true; }); }); + + describe("key changes", () => { + const user1Keypair = new Keypair(); + const user2Keypair = new Keypair(); + const secondKeyPair = new Keypair(); + let pollId: number = 0; + let user1StateIndex: number = 0; + let user2StateIndex: number = 0; + const user1VoteOptionIndex = BigInt(0); + const user2VoteOptionIndex = BigInt(1); + const user1VoteWeight = BigInt(9); + const user2VoteWeight = BigInt(3); + const user1NewVoteWeight = BigInt(5); + + describe("only user 1 changes key", () => { + const maciState: MaciState = new MaciState(STATE_TREE_DEPTH); + + before(() => { + // Sign up + user1StateIndex = maciState.signUp( + user1Keypair.pubKey, + voiceCreditBalance, + BigInt(Math.floor(Date.now() / 1000)), + ); + user2StateIndex = maciState.signUp( + user2Keypair.pubKey, + voiceCreditBalance, + BigInt(Math.floor(Date.now() / 1000)), + ); + + // deploy a poll + pollId = maciState.deployPoll( + duration, + BigInt(Math.floor(Date.now() / 1000) + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinatorKeypair, + ); + }); + it("should submit a vote for each user", () => { + const poll = maciState.polls[pollId]; + const command1 = new PCommand( + BigInt(user1StateIndex), + user1Keypair.pubKey, + user1VoteOptionIndex, + user1VoteWeight, + BigInt(1), + BigInt(pollId), + ); + + const signature1 = command1.sign(user1Keypair.privKey); + + const ecdhKeypair1 = new Keypair(); + const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey); + + const message1 = command1.encrypt(signature1, sharedKey1); + poll.publishMessage(message1, ecdhKeypair1.pubKey); + + const command2 = new PCommand( + BigInt(user2StateIndex), + user2Keypair.pubKey, + user2VoteOptionIndex, + user2VoteWeight, + BigInt(1), + BigInt(pollId), + ); + + const signature2 = command2.sign(user2Keypair.privKey); + + const ecdhKeypair2 = new Keypair(); + const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey); + + const message2 = command2.encrypt(signature2, sharedKey2); + poll.publishMessage(message2, ecdhKeypair2.pubKey); + }); + + it("user1 sends a keychange message with a new vote", () => { + const poll = maciState.polls[pollId]; + const command = new PCommand( + BigInt(user1StateIndex), + secondKeyPair.pubKey, + user1VoteOptionIndex, + user1NewVoteWeight, + BigInt(1), + BigInt(pollId), + ); + + const signature = command.sign(user1Keypair.privKey); + + const ecdhKeypair = new Keypair(); + const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); + + const message = command.encrypt(signature, sharedKey); + poll.publishMessage(message, ecdhKeypair.pubKey); + }); + + it("should perform the processing and tallying correctly", () => { + const poll = maciState.polls[pollId]; + poll.processMessages(pollId); + poll.tallyVotes(); + expect(poll.perVOSpentVoiceCredits[0].toString()).to.eq((user1NewVoteWeight * user1NewVoteWeight).toString()); + expect(poll.perVOSpentVoiceCredits[1].toString()).to.eq((user2VoteWeight * user2VoteWeight).toString()); + }); + + it("should confirm that the user key pair was changed (user's 2 one has not)", () => { + const poll = maciState.polls[pollId]; + const stateLeaf1 = poll.stateLeaves[user1StateIndex]; + const stateLeaf2 = poll.stateLeaves[user2StateIndex]; + expect(stateLeaf1.pubKey.toString()).to.eq(secondKeyPair.pubKey.toString()); + expect(stateLeaf2.pubKey.toString()).to.eq(user2Keypair.pubKey.toString()); + }); + }); + }); });