Skip to content

Commit

Permalink
feat(circuits): add poll joined circuit
Browse files Browse the repository at this point in the history
  • Loading branch information
0xmad committed Jan 22, 2025
1 parent 00a4f78 commit e6b8012
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,49 @@ template PollJoining(stateTreeDepth) {

stateLeafQip === 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];
// User's hashed private key
signal input nullifier;
// Poll State tree root which proves the user is joined
signal input stateRoot;
// The poll id
signal input pollId;

// Compute the nullifier (hash of private key and poll id)
var computedNullifier = PoseidonHasher(2)([privKey, pollId]);
nullifier === computedNullifier;

// 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": ["nullifier", "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
135 changes: 135 additions & 0 deletions packages/circuits/ts/__tests__/PollJoined.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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",
"nullifier",
"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 @@ -31,7 +31,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
18 changes: 18 additions & 0 deletions packages/circuits/ts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ 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[];
nullifier: bigint;
credits: bigint;
stateRoot: bigint;
pollId: bigint;
}

/**
* Inputs for circuit ProcessMessages
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/contracts/Poll.sol
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ contract Poll is Params, Utilities, SnarkCommon, IPoll {
isValid = extContracts.verifier.verify(_proof, vk, circuitPublicInputs);
}

/// @notice Get public circuit inputs for poll joining circuit
/// @notice Get public circuit inputs for poll joining and joined circuits
/// @param _nullifier Hashed user's private key to check whether user has already voted
/// @param _index Index of the MACI's stateRootOnSignUp when the user signed up
/// @param _pubKey Poll user's public key
Expand Down
52 changes: 51 additions & 1 deletion packages/core/ts/Poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type {
IJoiningCircuitArgs,
} from "./index";
import type { MaciState } from "./MaciState";
import type { IJoinedCircuitArgs, IPollJoinedCircuitInputs } from "./utils/types";
import type { PathElements } from "maci-crypto";

import { STATE_TREE_ARITY, VOTE_OPTION_TREE_ARITY } from "./utils/constants";
Expand Down Expand Up @@ -468,7 +469,7 @@ export class Poll implements IPoll {
const inputNullifier = BigInt(maciPrivKey.asCircuitInputs());
const nullifier = poseidon([inputNullifier, this.pollId]);

// Get pll state tree's root
// Get state tree's root
const stateRoot = this.stateTree!.root;

// Set actualStateTreeDepth as number of initial siblings length
Expand All @@ -489,6 +490,55 @@ export class Poll implements IPoll {
return stringifyBigInts(circuitInputs) as unknown as IPollJoiningCircuitInputs;
};

/**
* Create circuit input for pollJoined
* @param maciPrivKey User's private key for signing up
* @param stateLeafIndex Index where the user is stored in the state leaves
* @param pollPrivKey Poll's private key for the poll joining
* @param pollPubKey Poll's public key for the poll joining
* @returns stringified circuit inputs
*/
joinedCircuitInputs = ({
maciPrivKey,
stateLeafIndex,
pollPrivKey,
pollPubKey,
voiceCreditsBalance,
joinTimestamp,
}: IJoinedCircuitArgs): IPollJoinedCircuitInputs => {
// copy a poll state tree
const pollStateTree = new IncrementalQuinTree(this.stateTreeDepth, blankStateLeafHash, STATE_TREE_ARITY, hash2);

this.pollStateLeaves.forEach((stateLeaf) => {
pollStateTree.insert(stateLeaf.hash());
});

// calculate the path elements for the state tree given the original state tree
const { pathElements, pathIndices } = pollStateTree.genProof(Number(stateLeafIndex));

// Create nullifier from private key
const inputNullifier = BigInt(maciPrivKey.asCircuitInputs());
const nullifier = poseidon([inputNullifier, this.pollId]);

// Get poll state tree's root
const stateRoot = pollStateTree.root;

const circuitInputs = {
privKey: maciPrivKey.asCircuitInputs(),
pollPrivKey: pollPrivKey.asCircuitInputs(),
pollPubKey: pollPubKey.asCircuitInputs(),
pathElements: pathElements.map((item) => item.toString()),
voiceCreditsBalance: voiceCreditsBalance.toString(),
joinTimestamp: joinTimestamp.toString(),
pathIndices: pathIndices.map((item) => item.toString()),
nullifier,
stateRoot,
pollId: this.pollId,
};

return stringifyBigInts(circuitInputs) as unknown as IPollJoinedCircuitInputs;
};

/**
* Pad last unclosed batch
*/
Expand Down
31 changes: 31 additions & 0 deletions packages/core/ts/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,19 @@ export interface IJoiningCircuitArgs {
pollPrivKey: PrivKey;
pollPubKey: PubKey;
}

/**
* An interface describing the joinedCircuitInputs function arguments
*/
export interface IJoinedCircuitArgs {
maciPrivKey: PrivKey;
stateLeafIndex: bigint;
pollPrivKey: PrivKey;
pollPubKey: PubKey;
voiceCreditsBalance: bigint;
joinTimestamp: bigint;
}

/**
* An interface describing the circuit inputs to the PollJoining circuit
*/
Expand All @@ -157,6 +170,24 @@ export interface IPollJoiningCircuitInputs {
actualStateTreeDepth: string;
pollId: string;
}

/**
* An interface describing the circuit inputs to the PollJoined circuit
*/
export interface IPollJoinedCircuitInputs {
privKey: string;
pollPrivKey: string;
pollPubKey: string[];
voiceCreditsBalance: string;
joinTimestamp: string;
stateLeaf: string[];
pathElements: string[][];
pathIndices: string[];
nullifier: string;
stateRoot: string;
pollId: string;
}

/**
* An interface describing the circuit inputs to the ProcessMessage circuit
*/
Expand Down

0 comments on commit e6b8012

Please sign in to comment.