From fed0e251bf09e1e34ec0a43ba58d89d3a95aa02c Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:56:09 +0000 Subject: [PATCH] refactor: vote sdk (#2089) --- apps/relayer/package.json | 2 +- apps/relayer/tests/deploy.ts | 2 +- apps/relayer/tests/messageBatches.test.ts | 2 +- apps/relayer/tests/messages.test.ts | 3 +- .../message/__tests__/message.guard.test.ts | 2 +- apps/relayer/ts/message/message.guard.ts | 2 +- packages/cli/ts/commands/publish.ts | 74 ++++----------- packages/sdk/ts/index.ts | 1 + packages/sdk/ts/utils/index.ts | 1 + packages/sdk/ts/utils/types.ts | 68 +++++++++++++- packages/sdk/ts/utils/utils.ts | 22 +++++ packages/sdk/ts/verifyingKeys.ts | 2 +- packages/sdk/ts/votes.ts | 92 +++++++++++++++++++ 13 files changed, 207 insertions(+), 66 deletions(-) create mode 100644 packages/sdk/ts/votes.ts diff --git a/apps/relayer/package.json b/apps/relayer/package.json index 8f6f9546a..a330fd3ca 100644 --- a/apps/relayer/package.json +++ b/apps/relayer/package.json @@ -46,9 +46,9 @@ "helmet": "^8.0.0", "lodash": "^4.17.21", "maci-cli": "workspace:^2.5.0", - "maci-contracts": "workspace:^2.5.0", "maci-sdk": "workspace:^0.0.1", "maci-domainobjs": "workspace:^2.5.0", + "maci-contracts": "workspace:^2.5.0", "mongoose": "^8.9.5", "multiformats": "^13.3.1", "mustache": "^4.2.0", diff --git a/apps/relayer/tests/deploy.ts b/apps/relayer/tests/deploy.ts index bea8be330..ca2752cec 100644 --- a/apps/relayer/tests/deploy.ts +++ b/apps/relayer/tests/deploy.ts @@ -1,7 +1,7 @@ import hardhat from "hardhat"; import { deploy, deployPoll, deployVkRegistryContract, joinPoll, setVerifyingKeys, signup } from "maci-cli"; -import { genMaciStateFromContract } from "maci-contracts"; import { Keypair } from "maci-domainobjs"; +import { genMaciStateFromContract } from "maci-sdk"; import { INT_STATE_TREE_DEPTH, diff --git a/apps/relayer/tests/messageBatches.test.ts b/apps/relayer/tests/messageBatches.test.ts index 3cb6640ca..bf0c0187c 100644 --- a/apps/relayer/tests/messageBatches.test.ts +++ b/apps/relayer/tests/messageBatches.test.ts @@ -1,8 +1,8 @@ import { jest } from "@jest/globals"; import { HttpStatus, ValidationPipe, type INestApplication } from "@nestjs/common"; import { Test } from "@nestjs/testing"; -import { formatProofForVerifierContract, genProofSnarkjs } from "maci-contracts"; import { Keypair } from "maci-domainobjs"; +import { formatProofForVerifierContract, genProofSnarkjs } from "maci-sdk"; import request from "supertest"; import type { App } from "supertest/types"; diff --git a/apps/relayer/tests/messages.test.ts b/apps/relayer/tests/messages.test.ts index fcd9657ce..aefcc2b58 100644 --- a/apps/relayer/tests/messages.test.ts +++ b/apps/relayer/tests/messages.test.ts @@ -1,9 +1,8 @@ import { jest } from "@jest/globals"; import { HttpStatus, ValidationPipe, type INestApplication } from "@nestjs/common"; import { Test } from "@nestjs/testing"; -import { formatProofForVerifierContract } from "maci-contracts"; import { Keypair } from "maci-domainobjs"; -import { genProofSnarkjs } from "maci-sdk"; +import { formatProofForVerifierContract, genProofSnarkjs } from "maci-sdk"; import request from "supertest"; import type { App } from "supertest/types"; diff --git a/apps/relayer/ts/message/__tests__/message.guard.test.ts b/apps/relayer/ts/message/__tests__/message.guard.test.ts index bdcf983f7..0af1091fe 100644 --- a/apps/relayer/ts/message/__tests__/message.guard.test.ts +++ b/apps/relayer/ts/message/__tests__/message.guard.test.ts @@ -3,8 +3,8 @@ import { HttpException, type ExecutionContext } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import dotenv from "dotenv"; import { ZeroAddress } from "ethers"; -import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-contracts/typechain-types"; import { Keypair } from "maci-domainobjs"; +import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-sdk"; import { MessageGuard, PUBLIC_METADATA_KEY, Public } from "../message.guard"; diff --git a/apps/relayer/ts/message/message.guard.ts b/apps/relayer/ts/message/message.guard.ts index 8dbbcec9b..1efb279cb 100644 --- a/apps/relayer/ts/message/message.guard.ts +++ b/apps/relayer/ts/message/message.guard.ts @@ -15,7 +15,7 @@ import flatMap from "lodash/flatMap"; import flatten from "lodash/flatten"; import map from "lodash/map"; import values from "lodash/values"; -import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-contracts/typechain-types"; +import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-sdk"; import type { Request as Req } from "express"; diff --git a/packages/cli/ts/commands/publish.ts b/packages/cli/ts/commands/publish.ts index 5c6d205c9..007dbd60b 100644 --- a/packages/cli/ts/commands/publish.ts +++ b/packages/cli/ts/commands/publish.ts @@ -8,6 +8,7 @@ import { PrivKey, PubKey, } from "maci-domainobjs"; +import { generateVote, getCoordinatorPubKey } from "maci-sdk"; import type { IPublishBatchArgs, IPublishBatchData, PublishArgs } from "../utils/interfaces"; @@ -41,7 +42,7 @@ export const publish = async ({ logError("invalid MACI public key"); } // deserialize - const pollPubKey = PubKey.deserialize(pubkey); + const votePubKey = PubKey.deserialize(pubkey); if (!(await contractExists(signer.provider!, maciAddress))) { logError("MACI contract does not exist"); @@ -51,31 +52,7 @@ export const publish = async ({ logError("Invalid MACI private key"); } - const pollPrivKey = PrivKey.deserialize(privateKey); - - // validate args - if (voteOptionIndex < 0) { - logError("invalid vote option index"); - } - - // check < 1 cause index zero is a blank state leaf - if (stateIndex < 1) { - logError("invalid state index"); - } - - if (nonce < 0) { - logError("invalid nonce"); - } - - if (salt && !validateSalt(salt)) { - logError("Invalid salt"); - } - - const userSalt = salt ? BigInt(salt) : genRandomSalt(); - - if (pollId < 0) { - logError("Invalid poll id"); - } + const privKey = PrivKey.deserialize(privateKey); const maciContract = MACIFactory.connect(maciAddress, signer); const pollContracts = await maciContract.getPoll(pollId); @@ -86,40 +63,25 @@ export const publish = async ({ const pollContract = PollFactory.connect(pollContracts.poll, signer); - const maxVoteOptions = Number(await pollContract.maxVoteOptions()); - const coordinatorPubKeyResult = await pollContract.coordinatorPubKey(); + const coordinatorPubKey = await getCoordinatorPubKey(pollContracts.poll, signer); + const maxVoteOptions = await pollContract.maxVoteOptions(); - // validate the vote options index against the max leaf index on-chain - if (maxVoteOptions < voteOptionIndex) { - logError("Invalid vote option index"); - } - - const coordinatorPubKey = new PubKey([ - BigInt(coordinatorPubKeyResult.x.toString()), - BigInt(coordinatorPubKeyResult.y.toString()), - ]); - - const encKeypair = new Keypair(); - - // create the command object - const command: PCommand = new PCommand( - stateIndex, - pollPubKey, + const { message, ephemeralKeypair } = generateVote({ + pollId, voteOptionIndex, - newVoteWeight, + salt, nonce, - BigInt(pollId), - userSalt, - ); - - // sign the command with the poll private key - const signature = command.sign(pollPrivKey); - // encrypt the command using a shared key between the user and the coordinator - const message = command.encrypt(signature, Keypair.genEcdhSharedKey(encKeypair.privKey, coordinatorPubKey)); + privateKey: privKey, + stateIndex, + voteWeight: newVoteWeight, + coordinatorPubKey, + maxVoteOption: maxVoteOptions, + newPubKey: votePubKey, + }); try { // submit the message onchain as well as the encryption public key - const tx = await pollContract.publishMessage(message.asContractParam(), encKeypair.pubKey.asContractParam()); + const tx = await pollContract.publishMessage(message.asContractParam(), ephemeralKeypair.pubKey.asContractParam()); const receipt = await tx.wait(); if (receipt?.status !== 1) { @@ -127,13 +89,13 @@ export const publish = async ({ } logYellow(quiet, info(`Transaction hash: ${receipt!.hash}`)); - logGreen(quiet, info(`Ephemeral private key: ${encKeypair.privKey.serialize()}`)); + logGreen(quiet, info(`Ephemeral private key: ${ephemeralKeypair.privKey.serialize()}`)); } catch (error) { logError((error as Error).message); } // we want the user to have the ephemeral private key - return encKeypair.privKey.serialize(); + return ephemeralKeypair.privKey.serialize(); }; /** diff --git a/packages/sdk/ts/index.ts b/packages/sdk/ts/index.ts index 27c1a23bb..39bb7825a 100644 --- a/packages/sdk/ts/index.ts +++ b/packages/sdk/ts/index.ts @@ -5,6 +5,7 @@ export { isUserRegistered, isJoinedUser, signup } from "./user"; export { getAllOnChainVks, compareVks, extractAllVks } from "./verifyingKeys"; export { isArm } from "./utils"; export { genSignUpTree } from "./trees"; +export { generateVote, getCoordinatorPubKey } from "./votes"; export { EMode, diff --git a/packages/sdk/ts/utils/index.ts b/packages/sdk/ts/utils/index.ts index 3c36f5ba8..f7c9f9c9b 100644 --- a/packages/sdk/ts/utils/index.ts +++ b/packages/sdk/ts/utils/index.ts @@ -28,6 +28,7 @@ export type { IProcessMessagesInputs, ISnarkJSVerificationKey, ITallyVotesInputs, + IVote, } from "./types"; export { verifyPerVOSpentVoiceCredits, verifyTallyResults } from "./verifiers"; export { BLOCKS_STEP } from "./constants"; diff --git a/packages/sdk/ts/utils/types.ts b/packages/sdk/ts/utils/types.ts index bb929fab5..c5817bef2 100644 --- a/packages/sdk/ts/utils/types.ts +++ b/packages/sdk/ts/utils/types.ts @@ -1,10 +1,10 @@ import { MACI, Poll } from "maci-contracts/typechain-types"; -import { PubKey } from "maci-domainobjs"; +import { PubKey, PrivKey } from "maci-domainobjs"; import type { LeanIMT } from "@zk-kit/lean-imt"; import type { Provider, Signer } from "ethers"; import type { EMode } from "maci-contracts"; -import type { IVkContractParams, VerifyingKey } from "maci-domainobjs"; +import type { IVkContractParams, Keypair, Message, VerifyingKey } from "maci-domainobjs"; /** * A circuit inputs for the circom circuit @@ -748,3 +748,67 @@ export interface IGenSignUpTree { */ pubKeys: PubKey[]; } + +/** + * Interface for the arguments for the generateVote function + */ +export interface IGenerateVoteArgs { + /** + * The poll id + */ + pollId: bigint; + /** + * The index of the vote option + */ + voteOptionIndex: bigint; + /** + * The salt for the vote + */ + salt?: bigint; + /** + * The nonce for the vote + */ + nonce: bigint; + /** + * The private key for the vote + */ + privateKey: PrivKey; + /** + * The state index for the vote + */ + stateIndex: bigint; + /** + * The weight of the vote + */ + voteWeight: bigint; + /** + * The coordinator public key + */ + coordinatorPubKey: PubKey; + /** + * The largest vote option index + */ + maxVoteOption: bigint; + /** + * Ephemeral keypair + */ + ephemeralKeypair?: Keypair; + /** + * New key in case of key change message + */ + newPubKey?: PubKey; +} + +/** + * Interface for the vote object + */ +export interface IVote { + /** + * The message to be sent to the contract + */ + message: Message; + /** + * The ephemeral keypair used to generate the shared key for encrypting the message + */ + ephemeralKeypair: Keypair; +} diff --git a/packages/sdk/ts/utils/utils.ts b/packages/sdk/ts/utils/utils.ts index b671e6344..6f54e07db 100644 --- a/packages/sdk/ts/utils/utils.ts +++ b/packages/sdk/ts/utils/utils.ts @@ -1,3 +1,6 @@ +import { SNARK_FIELD_SIZE } from "maci-crypto"; + +import fs from "fs"; import os from "os"; /** @@ -6,6 +9,18 @@ import os from "os"; */ export const isArm = (): boolean => os.arch().includes("arm"); +/** + * Remove a file + * @param filepath - the path to the file + */ +export const unlinkFile = async (filepath: string): Promise => { + const isFileExists = fs.existsSync(filepath); + + if (isFileExists) { + await fs.promises.unlink(filepath); + } +}; + /** * Pause the thread for n milliseconds * @param ms - the amount of time to sleep in milliseconds @@ -15,3 +30,10 @@ export const sleep = async (ms: number): Promise => { setTimeout(resolve, ms); }); }; + +/** + * Run both format check and size check on a salt value + * @param salt the salt to validate + * @returns whether it is valid or not + */ +export const validateSalt = (salt: bigint): boolean => salt < SNARK_FIELD_SIZE; diff --git a/packages/sdk/ts/verifyingKeys.ts b/packages/sdk/ts/verifyingKeys.ts index 85d3c5c03..beebfeabd 100644 --- a/packages/sdk/ts/verifyingKeys.ts +++ b/packages/sdk/ts/verifyingKeys.ts @@ -1,7 +1,7 @@ import { VkRegistry__factory as VkRegistryFactory, extractVk } from "maci-contracts"; import { IVkContractParams, VerifyingKey } from "maci-domainobjs"; -import type { GetAllVksArgs, IExtractAllVksArgs, IMaciVks, IMaciVerifyingKeys } from "./utils"; +import type { GetAllVksArgs, IExtractAllVksArgs, IMaciVks, IMaciVerifyingKeys } from "./utils/types"; /** * Get all the verifying keys from the contract diff --git a/packages/sdk/ts/votes.ts b/packages/sdk/ts/votes.ts new file mode 100644 index 000000000..5e4ec6866 --- /dev/null +++ b/packages/sdk/ts/votes.ts @@ -0,0 +1,92 @@ +import { Signer } from "ethers"; +import { Poll__factory as PollFactory } from "maci-contracts"; +import { genRandomSalt } from "maci-crypto"; +import { Keypair, PCommand, PubKey } from "maci-domainobjs"; + +import type { IGenerateVoteArgs, IVote } from "./utils/types"; + +import { validateSalt } from "./utils/utils"; + +/** + * Get the coordinator public key for a poll + * @param pollAddress - the address of the poll + * @param signer - the signer to use + * @returns the coordinator public key + */ +export const getCoordinatorPubKey = async (pollAddress: string, signer: Signer): Promise => { + const pollContract = PollFactory.connect(pollAddress, signer); + + const coordinatorPubKey = await pollContract.coordinatorPubKey(); + + return new PubKey([coordinatorPubKey.x, coordinatorPubKey.y]); +}; + +/** + * Generate a vote + * @param args - The arguments for the vote + * @returns The vote object + */ +export const generateVote = ({ + pollId, + voteOptionIndex, + salt, + nonce, + privateKey, + stateIndex, + voteWeight, + coordinatorPubKey, + maxVoteOption, + ephemeralKeypair, + newPubKey, +}: IGenerateVoteArgs): IVote => { + const keypair = new Keypair(privateKey); + + // validate args + if (voteOptionIndex < 0 || voteOptionIndex > maxVoteOption) { + throw new Error("invalid vote option index"); + } + + // check < 1 cause index zero is a blank state leaf + if (stateIndex < 1) { + throw new Error("invalid state index"); + } + + if (nonce < 0) { + throw new Error("invalid nonce"); + } + + if (salt && !validateSalt(salt)) { + throw new Error("Invalid salt"); + } + + const userSalt = salt ? BigInt(salt) : genRandomSalt(); + + if (pollId < 0) { + throw new Error("Invalid poll id"); + } + + // if no ephemeral keypair is provided, generate a new one + const encKeypair = ephemeralKeypair ?? new Keypair(); + + // create the command object + const command: PCommand = new PCommand( + stateIndex, + newPubKey ?? keypair.pubKey, + voteOptionIndex, + voteWeight, + nonce, + pollId, + userSalt, + ); + + // sign the command with the poll private key + const signature = command.sign(privateKey); + + // encrypt the command using a shared key between the user and the coordinator + const message = command.encrypt(signature, Keypair.genEcdhSharedKey(encKeypair.privKey, coordinatorPubKey)); + + return { + message, + ephemeralKeypair: encKeypair, + }; +};