Skip to content

Commit

Permalink
feat: anonymous poll joining milestone 2 and 3 (#1750)
Browse files Browse the repository at this point in the history
* feat(poll): add chain hash features

BREAKING CHANGE: message processing is changed

* fix(ipoll): add missing parameter

* fix(poll-tests): add missing parameter maxMessagebatchSize

* feat(poll.ts): add chain hash updating

* test(poll tests): add test for checking chain hash computation

* feat(poll.ts): add batch hashes array computation

* feat(poll.sol): pad zeroes to the maximum size of batch

* feat(messageprocessor): update process messages to use chain hash

* refactor(vkregistry): refactor function call

* feat(processmessages.circom): add chainHash feature in circuits and test for that

* test(processmessages): rearrange test for key-change

* refactor(mergemessages): refactor functions calls which include mergemessages

* refactor(mergemessages): add some more changes about functions call which  include mergemessages

* test(all tests): fixing tests after refactoring code

* refactor(accqueue): remove all calls for accqueue

* fix(currentmessagebatchindex): fix message batch indexing

* refactor(circuit tests): refactor code for circuit testing

* test(ceremonyparams.test): correct constants for CeremonyParams test

* perf(processmessages.circom + contracts): optimize last batch padding, remove unused inputs

* docs(padlastbatch method): update doc comment

* docs(poll.ts): remove stale comments

* docs(test comments): fix typos

* ci(treedepths mock): modify interface for mocked function

* fix(ceremony params test): fix circuit inputs

* test(messagevalidator): fix function calls for messagevalidator circuit in tests

* chore(comments): fix unusefull comments

* refactor(poll.sol): replace external contracts with maci only

* perf(messageprocessor.sol): hardcode initialization for batchHashes array

* docs(comments): fix some more comments

* test(test for pr checks): correct some of tests for PR checks

* ci: 🎡 renamed old ProcessMessages_10-2-1-2_test

* ci: 🎡 correct rapidsnark/build/prover path

* style(reviews): solve some reviews for merging

* refactor(messageaqq): remove more message merging and message aqq

* style(messageaqq): remove more message merging and message aqq

* refactor(messageaqq): remove message aqq from subgraph

* test(coordinator): hide NOT_MERGED_MESSAGE_TREE error

* test(coordinator): fix test about message merging

* test(proveonchain): change chainHash calculation

* test(proveonchain): fix chainHashes declaration

* test(proveonchain): fix chainHash calculation

* test(proveonchain): fix chainHashes calculations

* test(proveonchain): fix chainHashes calculation

* test(proveonchain): fix loop limit

* style(review comments): resolve some of review comments

* style(review comments): resolve some of review comments

* test(lint:ts): fix e2e test because of lint:ts check

* docs(wrong changes): fix wrong changes about documentation that is not in our scope

* refactor(batchsizes): change batchSizes struct with messageBatchSize variable

* refactor(contracts): rollback to provide external contract references

* docs(messageprocessor.sol): fix typo

* refactor(messagebatchsize): chenge messageBatchSize location from Params.sol to Poll.sol

* refactor(maxmessages): remove maxMessages from maxValues

* refactor(sltimestemp): remove slTimestamp from circuits

* refactor(review comments): resolve more review comments

* fix(subgraph): fix bug about maxVoteOptions dunction call

* fix(sltimestamp): fix test for removing slTimestap signal

* refactor(promise.all): refactor promise.all for only one async call

* fix(subgraph): try to fix subgraph build

* revert(.nx folder): remove .nx folder from cli folder

* fix(merge): tmp-anon-poll-joining merge

* fix(merge): tmp-anon-poll-joining merge

* test(ceremonyparams): add poll joining in the test

* test(processmessages): add poll joining for the test

without key-change test

* test(polljoining): add poll joining in the test

* test(tallyvotes): add poll joining in the test

* test(core): add joinPoll function in tests

* style(typo): inclusion proof

* style(todo): remove finished todo

* style(merge): after merge style

* style(return): inline return

* style(eslint): remove unnecessary eslint-disable

* refactor(joiningcircuitargs): add interface IJoiningCircuitArgs

* refactor(joinpoll): async read state file

* style(genmacisignup): add function description

* refactor(gensignuptree): add IGenSignUpTreeArgs interface

* style(polljoining): remove extra inlcudes and comments

* feat(pollvkkeys): init

* feat(vkregistry): separate set functions (process/tally/poll)

* test(pollvkkey): adjust test to setPollVkKey

* refactor(vkregistry): use setVerifyingKeys in setVerifyingKeysBatch

* refactor(poll): add verifier and vkRegystry in constructor

* refactor(poll): put verifier and vkRegistry into extContracts

* test(core e2e): fix sanity checks test for incorrect signature

* refactor(test): removing only from tests

* refactor(macistatetree): use LeanIMT instead of QuinTree

* refactor(crypto): export hashLeanIMT from index

* feat(joinpoll): use genSignUpTree instead of genMaciStateFromContract

* feat(joinpoll cli): add optional parameters

* test(coordinator): add pollJoiningZkeyPath in app.test

* refactor(joinpoll): prettier

* test(coordinator): add joinPoll

* fix(poll): joiningCircuitInputs with correct siblings, indices and actualStateTreeDepth

* test(integration): add joinPoll

* build(coordinator): add COORDINATOR_POLL_ZKEY_NAME

* refactor(mergestate): remove Maci from MergeState

* test(e2e): test:e2e add joinPoll

* test(e2e): test:keyChange add joinPoll

* docs(complete documentation): complete documentation of the new workflow

* docs(documentation): add v3 docs, revert v2 docs

* style(docs): prettier

* refactor(joinpoll): add generateAndVerifyProof and getStateIndexAndCreditBalance

* docs(blogpost): blogpost cuvering the latest updates

* docs(blogpost): kudos to our team members!

* style(prettier): blog

* fix(joinpoll): index value of the user in the state tree leaves

* docs(blog): remove poll-joining

---------

Co-authored-by: radojevicMihailo <[email protected]>
Co-authored-by: Aleksandar Veljković <[email protected]>
  • Loading branch information
3 people authored Sep 11, 2024
1 parent e84f610 commit 9577f86
Show file tree
Hide file tree
Showing 161 changed files with 10,431 additions and 1,109 deletions.
1 change: 1 addition & 0 deletions .github/workflows/coordinator-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ env:
COORDINATOR_RPC_URL: "http://localhost:8545"
COORDINATOR_PUBLIC_KEY_PATH: "./pub.key"
COORDINATOR_PRIVATE_KEY_PATH: "./priv.key"
COORDINATOR_POLL_ZKEY_NAME: "PollJoining_10_test"
COORDINATOR_TALLY_ZKEY_NAME: "TallyVotes_10-1-2_test"
COORDINATOR_MESSAGE_PROCESS_ZKEY_NAME: "ProcessMessages_10-20-2_test"
COORDINATOR_ZKEY_PATH: "./zkeys/"
Expand Down
6 changes: 6 additions & 0 deletions circuits/circom/circuits.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{
"PollJoining_10_test": {
"file": "./core/qv/pollJoining",
"template": "PollJoining",
"params": [10],
"pubs": ["inputHash"]
},
"ProcessMessages_10-20-2_test": {
"file": "./core/qv/processMessages",
"template": "ProcessMessages",
Expand Down
81 changes: 81 additions & 0 deletions circuits/circom/core/qv/pollJoining.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
pragma circom 2.0.0;


// circomlib import
include "./mux1.circom";
// zk-kit imports
include "./safe-comparators.circom";
// local imports
include "../../utils/hashers.circom";
include "../../utils/privToPubKey.circom";
include "../../trees/incrementalMerkleTree.circom";

template PollJoining(stateTreeDepth) {

// Constants defining the structure and size of state.
var STATE_LEAF_LENGTH = 4;
var STATE_TREE_ARITY = 2;

// Public key IDs.
var STATE_LEAF_PUB_X_IDX = 0;
var STATE_LEAF_PUB_Y_IDX = 1;
// Voice Credit balance id
var STATE_LEAF_VOICE_CREDIT_BALANCE_IDX = 2;
var N_BITS = 252;

// User's private key
signal input privKey;
// Poll's private key
signal input pollPrivKey;
// Poll's public key
signal input pollPubKey[2];
// The state leaf and related path elements.
signal input stateLeaf[STATE_LEAF_LENGTH];
// Siblings
signal input siblings[stateTreeDepth][STATE_TREE_ARITY - 1];
// Indices
signal input indices[stateTreeDepth];
// User's hashed private key
signal input nullifier;
// User's credits for poll joining (might be <= oldCredits)
signal input credits;
// MACI State tree root which proves the user is signed up
signal input stateRoot;
// The actual tree depth (might be <= stateTreeDepth) Used in BinaryMerkleRoot
signal input actualStateTreeDepth;
// Public input hash (nullifier, credits, stateRoot)
signal input inputHash;

// Check public input hash
var computedInputHash = Sha256Hasher(5)([nullifier, credits, stateRoot, pollPubKey[0], pollPubKey[1]]);
inputHash === computedInputHash;

// User private to public key
var derivedPubKey[2] = PrivToPubKey()(privKey);
derivedPubKey[0] === stateLeaf[STATE_LEAF_PUB_X_IDX];
derivedPubKey[1] === stateLeaf[STATE_LEAF_PUB_Y_IDX];

// Poll private to public key
var derivedPollPubKey[2] = PrivToPubKey()(pollPrivKey);
derivedPollPubKey[0] === pollPubKey[0];
derivedPollPubKey[1] === pollPubKey[1];

// Inclusion proof
var stateLeafHash = PoseidonHasher(4)(stateLeaf);
var stateLeafQip = BinaryMerkleRoot(stateTreeDepth)(
stateLeafHash,
actualStateTreeDepth,
indices,
siblings
);

stateLeafQip === stateRoot;

// Check credits
var isCreditsValid = SafeLessEqThan(N_BITS)([credits, stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX]]);
isCreditsValid === 1;

// Check nullifier
var hashedPrivKey = PoseidonHasher(1)([privKey]);
hashedPrivKey === nullifier;
}
3 changes: 2 additions & 1 deletion circuits/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"test:processMessages": "pnpm run mocha-test ts/__tests__/ProcessMessages.test.ts",
"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:incrementalQuinaryTree": "pnpm run mocha-test ts/__tests__/IncrementalQuinaryTree.test.ts",
"test:pollJoining": "pnpm run mocha-test ts/__tests__/PollJoining.test.ts"
},
"dependencies": {
"@zk-kit/circuits": "^0.4.0",
Expand Down
48 changes: 30 additions & 18 deletions circuits/ts/__tests__/CeremonyParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { MaciState, Poll, packProcessMessageSmallVals, STATE_TREE_ARITY } from "maci-core";
import { MESSAGE_BATCH_SIZE, VOTE_OPTION_TREE_ARITY } from "maci-core/build/ts/utils/constants";
import { hash5, IncrementalQuinTree } from "maci-crypto";
import { hash5, IncrementalQuinTree, poseidon } from "maci-crypto";
import { PrivKey, Keypair, PCommand, Message, Ballot } from "maci-domainobjs";

import { IProcessMessagesInputs, ITallyVotesInputs } from "../types";
Expand Down Expand Up @@ -100,9 +100,7 @@ describe("Ceremony param tests", () => {
before(() => {
// Sign up and publish
const userKeypair = new Keypair(new PrivKey(BigInt(1)));
stateIndex = BigInt(
maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))),
);
maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000)));

pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
Expand All @@ -117,17 +115,26 @@ describe("Ceremony param tests", () => {
// update the state
poll.updatePoll(BigInt(maciState.stateLeaves.length));

// Join the poll
const { privKey } = userKeypair;
const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair();

const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]);
const timestamp = BigInt(Math.floor(Date.now() / 1000));

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

// First command (valid)
const command = new PCommand(
stateIndex, // BigInt(1),
userKeypair.pubKey,
pollPubKey,
voteOptionIndex, // voteOptionIndex,
voteWeight, // vote weight
BigInt(2), // nonce
BigInt(pollId),
);

const signature = command.sign(userKeypair.privKey);
const signature = command.sign(pollPrivKey);

const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
Expand All @@ -140,13 +147,13 @@ describe("Ceremony param tests", () => {
// Second command (valid)
const command2 = new PCommand(
stateIndex,
userKeypair.pubKey,
pollPubKey,
voteOptionIndex, // voteOptionIndex,
BigInt(1), // vote weight
BigInt(1), // nonce
BigInt(pollId),
);
const signature2 = command2.sign(userKeypair.privKey);
const signature2 = command2.sign(pollPrivKey);

const ecdhKeypair2 = new Keypair();
const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey);
Expand All @@ -163,11 +170,10 @@ describe("Ceremony param tests", () => {
const ballotTree = new IncrementalQuinTree(params.stateTreeDepth, emptyBallot.hash(), STATE_TREE_ARITY, hash5);
ballotTree.insert(emptyBallot.hash());

poll.stateLeaves.forEach(() => {
poll.pollStateLeaves.forEach(() => {
ballotTree.insert(emptyBallotHash);
});

const currentStateRoot = poll.stateTree?.root;
const currentStateRoot = poll.pollStateTree?.root;
const currentBallotRoot = ballotTree.root;

const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs;
Expand All @@ -178,7 +184,7 @@ describe("Ceremony param tests", () => {

// The new roots, which should differ, since at least one of the
// messages modified a Ballot or State Leaf
const newStateRoot = poll.stateTree?.root;
const newStateRoot = poll.pollStateTree?.root;
const newBallotRoot = poll.ballotTree?.root;

expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString());
Expand Down Expand Up @@ -258,10 +264,7 @@ describe("Ceremony param tests", () => {
const commands: PCommand[] = [];
// Sign up and publish
const userKeypair = new Keypair();
stateIndex = BigInt(
maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))),
);

maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000)));
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues.maxVoteOptions,
Expand All @@ -275,17 +278,26 @@ describe("Ceremony param tests", () => {
// update the state
poll.updatePoll(BigInt(maciState.stateLeaves.length));

// Join the poll
const { privKey } = userKeypair;
const { privKey: pollPrivKey, pubKey: pollPubKey } = new Keypair();

const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]);
const timestamp = BigInt(Math.floor(Date.now() / 1000));

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

// First command (valid)
const command = new PCommand(
stateIndex,
userKeypair.pubKey,
pollPubKey,
voteOptionIndex, // voteOptionIndex,
voteWeight, // vote weight
BigInt(1), // nonce
BigInt(pollId),
);

const signature = command.sign(userKeypair.privKey);
const signature = command.sign(pollPrivKey);

const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
Expand Down
153 changes: 153 additions & 0 deletions circuits/ts/__tests__/PollJoining.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { MaciState, Poll } from "maci-core";
import { poseidon } from "maci-crypto";
import { Keypair, Message, PCommand } from "maci-domainobjs";

import { IPollJoiningInputs } from "../types";

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

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

const coordinatorKeypair = new Keypair();

type PollJoiningCircuitInputs = [
"privKey",
"pollPrivKey",
"pollPubKey",
"stateLeaf",
"siblings",
"indices",
"nullifier",
"credits",
"stateRoot",
"actualStateTreeDepth",
"inputHash",
];

let circuit: WitnessTester<PollJoiningCircuitInputs>;

before(async () => {
circuit = await circomkitInstance.WitnessTester("pollJoining", {
file: "./core/qv/pollJoining",
template: "PollJoining",
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[] = [];

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

users.forEach((userKeypair) => {
maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000)));
});

pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues.maxVoteOptions,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);

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

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

const nullifier = poseidon([BigInt(privKey.rawPrivKey.toString())]);
const timestamp = BigInt(Math.floor(Date.now() / 1000));

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 stateLeafIndex = BigInt(1);
const credits = BigInt(10);

const inputs = poll.joiningCircuitInputs({
maciPrivKey: privateKey,
stateLeafIndex,
credits,
pollPrivKey,
pollPubKey,
}) as unknown as IPollJoiningInputs;
const witness = await circuit.calculateWitness(inputs);
await circuit.expectConstraintPass(witness);
});

it("should fail for fake witness", async () => {
const privateKey = users[0].privKey;
const stateLeafIndex = BigInt(1);
const credits = BigInt(10);

const inputs = poll.joiningCircuitInputs({
maciPrivKey: privateKey,
stateLeafIndex,
credits,
pollPrivKey,
pollPubKey,
}) as unknown as IPollJoiningInputs;
const witness = await circuit.calculateWitness(inputs);

const fakeWitness = Array(witness.length).fill(1n) as bigint[];
await circuit.expectConstraintFail(fakeWitness);
});

it("should fail for improper credits", () => {
const privateKey = users[0].privKey;
const stateLeafIndex = BigInt(1);
const credits = BigInt(105);

expect(() =>
poll.joiningCircuitInputs({ maciPrivKey: privateKey, stateLeafIndex, credits, pollPrivKey, pollPubKey }),
).to.throw("Credits must be lower than signed up credits");
});
});
});
Loading

0 comments on commit 9577f86

Please sign in to comment.