Skip to content

Commit

Permalink
feat(circuits): add poll joined circuit
Browse files Browse the repository at this point in the history
- [x] Add poll joined circuit
- [x] Add poll joined vk
  • Loading branch information
0xmad committed Jan 23, 2025
1 parent 11e5b8a commit 2407bff
Show file tree
Hide file tree
Showing 27 changed files with 754 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,43 @@ template PollJoining(stateTreeDepth) {

calculatedRoot === stateRoot;
}

template PollJoined(stateTreeDepth) {
// Constants defining the tree structure
var STATE_TREE_ARITY = 2;

// User's private key
signal input privKey;
// Poll's private key
signal input pollPrivKey;
// Poll's public key
signal input pollPubKey[2];
// User's voice credits balance
signal input voiceCreditsBalance;
// Poll's joined timestamp
signal input joinTimestamp;
// Path elements
signal input pathElements[stateTreeDepth][STATE_TREE_ARITY - 1];
// Path indices
signal input pathIndices[stateTreeDepth];
// Poll State tree root which proves the user is joined
signal input stateRoot;
// The poll id
signal input pollId;

// Poll private to public key to verify the correct one is used to join the poll (public input)
var derivedPollPubKey[2] = PrivToPubKey()(pollPrivKey);
derivedPollPubKey[0] === pollPubKey[0];
derivedPollPubKey[1] === pollPubKey[1];

var stateLeaf = PoseidonHasher(4)([derivedPollPubKey[0], derivedPollPubKey[1], voiceCreditsBalance, joinTimestamp]);

// Inclusion proof
var stateLeafQip = MerkleTreeInclusionProof(stateTreeDepth)(
stateLeaf,
pathIndices,
pathElements
);

stateLeafQip === stateRoot;
}
8 changes: 7 additions & 1 deletion packages/circuits/circom/circuits.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
{
"PollJoining_10_test": {
"file": "./anon/pollJoining",
"file": "./anon/poll",
"template": "PollJoining",
"params": [10],
"pubs": ["nullifier", "stateRoot", "pollPubKey", "pollId"]
},
"PollJoined_10_test": {
"file": "./anon/poll",
"template": "PollJoined",
"params": [10],
"pubs": ["stateRoot", "pollPubKey", "pollId"]
},
"ProcessMessages_10-20-2_test": {
"file": "./core/qv/processMessages",
"template": "ProcessMessages",
Expand Down
3 changes: 2 additions & 1 deletion packages/circuits/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"test:tallyVotes": "pnpm run mocha-test ts/__tests__/TallyVotes.test.ts",
"test:ceremonyParams": "pnpm run mocha-test ts/__tests__/CeremonyParams.test.ts",
"test:incrementalQuinaryTree": "pnpm run mocha-test ts/__tests__/IncrementalQuinaryTree.test.ts",
"test:pollJoining": "pnpm run mocha-test ts/__tests__/PollJoining.test.ts"
"test:pollJoining": "pnpm run mocha-test ts/__tests__/PollJoining.test.ts",
"test:pollJoined": "pnpm run mocha-test ts/__tests__/PollJoined.test.ts"
},
"dependencies": {
"@zk-kit/circuits": "^0.4.0",
Expand Down
134 changes: 134 additions & 0 deletions packages/circuits/ts/__tests__/PollJoined.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { type WitnessTester } from "circomkit";
import { MaciState, Poll } from "maci-core";
import { poseidon } from "maci-crypto";
import { Keypair, Message, PCommand } from "maci-domainobjs";

import type { IPollJoinedInputs } from "../types";

import { STATE_TREE_DEPTH, duration, messageBatchSize, treeDepths, voiceCreditBalance } from "./utils/constants";
import { circomkitInstance } from "./utils/utils";

describe("Poll Joined circuit", function test() {
this.timeout(900000);
const NUM_USERS = 50;

const coordinatorKeypair = new Keypair();

type PollJoinedCircuitInputs = [
"privKey",
"pollPrivKey",
"pollPubKey",
"voiceCreditsBalance",
"joinTimestamp",
"stateLeaf",
"pathElements",
"pathIndices",
"stateRoot",
"pollId",
];

let circuit: WitnessTester<PollJoinedCircuitInputs>;

before(async () => {
circuit = await circomkitInstance.WitnessTester("pollJoined", {
file: "./anon/poll",
template: "PollJoined",
params: [STATE_TREE_DEPTH],
});
});

describe(`${NUM_USERS} users, 1 join`, () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
let pollId: bigint;
let poll: Poll;
let users: Keypair[];
const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair();
const messages: Message[] = [];
const commands: PCommand[] = [];

const timestamp = BigInt(Math.floor(Date.now() / 1000));

before(() => {
// Sign up
users = new Array(NUM_USERS).fill(0).map(() => new Keypair());

users.forEach((userKeypair) => {
maciState.signUp(userKeypair.pubKey);
});

pollId = maciState.deployPoll(timestamp + BigInt(duration), treeDepths, messageBatchSize, coordinatorKeypair);

poll = maciState.polls.get(pollId)!;
poll.updatePoll(BigInt(maciState.pubKeys.length));

// Join the poll
const { privKey } = users[0];

const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]);

const stateIndex = BigInt(poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp));

// First command (valid)
const command = new PCommand(
stateIndex,
pollPubKey,
BigInt(0), // voteOptionIndex,
BigInt(9), // vote weight
BigInt(1), // nonce
BigInt(pollId),
);

const signature = command.sign(pollPrivKey);

const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
messages.push(message);
commands.push(command);

poll.publishMessage(message, ecdhKeypair.pubKey);

// Process messages
poll.processMessages(pollId);
});

it("should produce a proof", async () => {
const privateKey = users[0].privKey;
const nullifier = poseidon([BigInt(privateKey.asCircuitInputs()), poll.pollId]);

const stateLeafIndex = poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp);

const inputs = poll.joinedCircuitInputs({
maciPrivKey: privateKey,
stateLeafIndex: BigInt(stateLeafIndex),
pollPrivKey,
pollPubKey,
voiceCreditsBalance: voiceCreditBalance,
joinTimestamp: timestamp,
}) as unknown as IPollJoinedInputs;

const witness = await circuit.calculateWitness(inputs);
await circuit.expectConstraintPass(witness);
});

it("should fail for fake witness", async () => {
const privateKey = users[1].privKey;
const nullifier = poseidon([BigInt(privateKey.asCircuitInputs()), poll.pollId]);

const stateLeafIndex = poll.joinPoll(nullifier, pollPubKey, voiceCreditBalance, timestamp);

const inputs = poll.joinedCircuitInputs({
maciPrivKey: privateKey,
stateLeafIndex: BigInt(stateLeafIndex),
pollPrivKey,
pollPubKey,
voiceCreditsBalance: voiceCreditBalance,
joinTimestamp: timestamp,
}) as unknown as IPollJoinedInputs;
const witness = await circuit.calculateWitness(inputs);

const fakeWitness = Array(witness.length).fill(1n) as bigint[];
await circuit.expectConstraintFail(fakeWitness);
});
});
});
2 changes: 1 addition & 1 deletion packages/circuits/ts/__tests__/PollJoining.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe("Poll Joining circuit", function test() {

before(async () => {
circuit = await circomkitInstance.WitnessTester("pollJoining", {
file: "./anon/pollJoining",
file: "./anon/poll",
template: "PollJoining",
params: [STATE_TREE_DEPTH],
});
Expand Down
17 changes: 17 additions & 0 deletions packages/circuits/ts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ export interface IPollJoiningInputs {
pollId: bigint;
}

/**
* Inputs for circuit PollJoined
*/
export interface IPollJoinedInputs {
privKey: bigint;
pollPrivKey: bigint;
pollPubKey: bigint[][];
voiceCreditsBalance: bigint;
joinTimestamp: bigint;
stateLeaf: bigint[];
pathElements: bigint[][];
pathIndices: bigint[];
credits: bigint;
stateRoot: bigint;
pollId: bigint;
}

/**
* Inputs for circuit ProcessMessages
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/tests/ceremony-params/ceremonyParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
ceremonyProcessMessagesNonQvWasmPath,
ceremonyTallyVotesNonQvWasmPath,
ceremonyPollJoiningZkeyPath,
ceremonyPollJoinedZkeyPath,
} from "../constants";
import { clean, isArm } from "../utils";

Expand All @@ -68,6 +69,7 @@ describe("Stress tests with ceremony params (6,3,2,20)", function test() {
voteOptionTreeDepth,
messageBatchSize,
pollJoiningZkeyPath: ceremonyPollJoiningZkeyPath,
pollJoinedZkeyPath: ceremonyPollJoinedZkeyPath,
processMessagesZkeyPathQv: ceremonyProcessMessagesZkeyPath,
tallyVotesZkeyPathQv: ceremonyTallyVotesZkeyPath,
processMessagesZkeyPathNonQv: ceremonyProcessMessagesNonQvZkeyPath,
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/tests/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const coordinatorPubKey = coordinatorKeypair.pubKey.serialize();
export const coordinatorPrivKey = coordinatorKeypair.privKey.serialize();

export const pollJoiningTestZkeyPath = "./zkeys/PollJoining_10_test/PollJoining_10_test.0.zkey";
export const pollJoinedTestZkeyPath = "./zkeys/PollJoined_10_test/PollJoined_10_test.0.zkey";
export const processMessageTestZkeyPath = "./zkeys/ProcessMessages_10-20-2_test/ProcessMessages_10-20-2_test.0.zkey";
export const tallyVotesTestZkeyPath = "./zkeys/TallyVotes_10-1-2_test/TallyVotes_10-1-2_test.0.zkey";
export const processMessageTestNonQvZkeyPath =
Expand All @@ -47,6 +48,7 @@ export const testTallyVotesWasmPath =
"./zkeys/TallyVotes_10-1-2_test/TallyVotes_10-1-2_test_js/TallyVotes_10-1-2_test.wasm";
export const testRapidsnarkPath = `${homedir()}/rapidsnark/build/prover`;
export const ceremonyPollJoiningZkeyPath = "./zkeys/PollJoining_10_test/PollJoining_10_test.0.zkey";
export const ceremonyPollJoinedZkeyPath = "./zkeys/PollJoined_10_test/PollJoined_10_test.0.zkey";
export const ceremonyProcessMessagesZkeyPath = "./zkeys/ProcessMessages_6-9-2-3/processMessages_6-9-2-3.zkey";
export const ceremonyProcessMessagesNonQvZkeyPath =
"./zkeys/ProcessMessagesNonQv_6-9-2-3/processMessagesNonQv_6-9-2-3.zkey";
Expand Down Expand Up @@ -98,6 +100,7 @@ export const setVerifyingKeysArgs: Omit<SetVerifyingKeysArgs, "signer"> = {
voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH,
messageBatchSize: MESSAGE_BATCH_SIZE,
pollJoiningZkeyPath: pollJoiningTestZkeyPath,
pollJoinedZkeyPath: pollJoinedTestZkeyPath,
processMessagesZkeyPathQv: processMessageTestZkeyPath,
tallyVotesZkeyPathQv: tallyVotesTestZkeyPath,
};
Expand All @@ -109,6 +112,7 @@ export const setVerifyingKeysNonQvArgs: Omit<SetVerifyingKeysArgs, "signer"> = {
voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH,
messageBatchSize: MESSAGE_BATCH_SIZE,
pollJoiningZkeyPath: pollJoiningTestZkeyPath,
pollJoinedZkeyPath: pollJoinedTestZkeyPath,
processMessagesZkeyPathNonQv: processMessageTestNonQvZkeyPath,
tallyVotesZkeyPathNonQv: tallyVotesTestNonQvZkeyPath,
};
Expand All @@ -119,6 +123,7 @@ export const checkVerifyingKeysArgs: Omit<CheckVerifyingKeysArgs, "signer"> = {
voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH,
messageBatchSize: MESSAGE_BATCH_SIZE,
pollJoiningZkeyPath: pollJoiningTestZkeyPath,
pollJoinedZkeyPath: pollJoinedTestZkeyPath,
processMessagesZkeyPath: processMessageTestZkeyPath,
tallyVotesZkeyPath: tallyVotesTestZkeyPath,
};
Expand Down
31 changes: 15 additions & 16 deletions packages/cli/ts/commands/checkVerifyingKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const checkVerifyingKeys = async ({
processMessagesZkeyPath,
tallyVotesZkeyPath,
pollJoiningZkeyPath,
pollJoinedZkeyPath,
vkRegistry,
signer,
useQuadraticVoting = true,
Expand Down Expand Up @@ -69,30 +70,28 @@ export const checkVerifyingKeys = async ({
// extract the verification keys from the zkey files
const processVk = VerifyingKey.fromObj(await extractVk(processMessagesZkeyPath));
const tallyVk = VerifyingKey.fromObj(await extractVk(tallyVotesZkeyPath));
const pollVk = VerifyingKey.fromObj(await extractVk(pollJoiningZkeyPath));
const pollJoiningVk = VerifyingKey.fromObj(await extractVk(pollJoiningZkeyPath));
const pollJoinedVk = VerifyingKey.fromObj(await extractVk(pollJoinedZkeyPath));

try {
logYellow(quiet, info("Retrieving verifying keys from the contract..."));
// retrieve the verifying keys from the contract

const pollVkOnChain = await vkRegistryContractInstance.getPollVk(stateTreeDepth, voteOptionTreeDepth);
const mode = useQuadraticVoting ? EMode.QV : EMode.NON_QV;

const processVkOnChain = await vkRegistryContractInstance.getProcessVk(
stateTreeDepth,
voteOptionTreeDepth,
messageBatchSize,
useQuadraticVoting ? EMode.QV : EMode.NON_QV,
);

const tallyVkOnChain = await vkRegistryContractInstance.getTallyVk(
stateTreeDepth,
intStateTreeDepth,
voteOptionTreeDepth,
useQuadraticVoting ? EMode.QV : EMode.NON_QV,
);
const [pollJoiningVkOnChain, pollJoinedVkOnChain, processVkOnChain, tallyVkOnChain] = await Promise.all([
vkRegistryContractInstance.getPollJoiningVk(stateTreeDepth, voteOptionTreeDepth),
vkRegistryContractInstance.getPollJoinedVk(stateTreeDepth, voteOptionTreeDepth),
vkRegistryContractInstance.getProcessVk(stateTreeDepth, voteOptionTreeDepth, messageBatchSize, mode),
vkRegistryContractInstance.getTallyVk(stateTreeDepth, intStateTreeDepth, voteOptionTreeDepth, mode),
]);

// do the actual validation
if (!compareVks(pollVk, pollVkOnChain)) {
if (!compareVks(pollJoiningVk, pollJoiningVkOnChain)) {
logError("Poll verifying keys do not match");
}

if (!compareVks(pollJoinedVk, pollJoinedVkOnChain)) {
logError("Poll verifying keys do not match");
}

Expand Down
8 changes: 5 additions & 3 deletions packages/cli/ts/commands/extractVkToFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ export const extractVkToFile = async ({
processMessagesZkeyPathQv,
tallyVotesZkeyPathQv,
processMessagesZkeyPathNonQv,
tallyVotesZkeyPathNonQv,
pollJoinedZkeyPath,
pollJoiningZkeyPath,
tallyVotesZkeyPathNonQv,
outputFilePath,
}: ExtractVkToFileArgs): Promise<void> => {
const [processVkQv, tallyVkQv, processVkNonQv, tallyVkNonQv, pollVk] = await Promise.all([
const [processVkQv, tallyVkQv, processVkNonQv, tallyVkNonQv, pollJoiningVk, pollJoinedVk] = await Promise.all([
extractVk(processMessagesZkeyPathQv),
extractVk(tallyVotesZkeyPathQv),
extractVk(processMessagesZkeyPathNonQv),
extractVk(tallyVotesZkeyPathNonQv),
extractVk(pollJoiningZkeyPath),
extractVk(pollJoinedZkeyPath),
]);

await fs.promises.writeFile(
outputFilePath,
JSON.stringify({ processVkQv, tallyVkQv, processVkNonQv, tallyVkNonQv, pollVk }),
JSON.stringify({ processVkQv, tallyVkQv, processVkNonQv, tallyVkNonQv, pollJoiningVk, pollJoinedVk }),
);
};
Loading

0 comments on commit 2407bff

Please sign in to comment.