From ac1a5028f69227c791100accb601b9cb7c7cba88 Mon Sep 17 00:00:00 2001 From: 0xmad <0xmad@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:09:19 -0600 Subject: [PATCH] feat(cli): add publish batch method for cli sdk --- cli/tests/unit/publish.test.ts | 137 +++++++++++++++++++++++++++++++++ cli/ts/commands/index.ts | 2 +- cli/ts/commands/publish.ts | 116 +++++++++++++++++++++++++++- cli/ts/index.ts | 7 ++ cli/ts/sdk/index.ts | 8 +- cli/ts/utils/index.ts | 3 + cli/ts/utils/interfaces.ts | 79 +++++++++++++++---- 7 files changed, 332 insertions(+), 20 deletions(-) create mode 100644 cli/tests/unit/publish.test.ts diff --git a/cli/tests/unit/publish.test.ts b/cli/tests/unit/publish.test.ts new file mode 100644 index 0000000000..1ab2d9f4a8 --- /dev/null +++ b/cli/tests/unit/publish.test.ts @@ -0,0 +1,137 @@ +import { expect } from "chai"; +import { getDefaultSigner } from "maci-contracts"; +import { Poll__factory as PollFactory } from "maci-contracts/typechain-types"; +import { SNARK_FIELD_SIZE } from "maci-crypto"; +import { Keypair } from "maci-domainobjs"; + +import type { Signer } from "ethers"; + +import { + deploy, + deployPoll, + deployVkRegistryContract, + setVerifyingKeys, + publishBatch, + signup, +} from "../../ts/commands"; +import { DeployedContracts, IPublishBatchArgs, IPublishMessage, PollContracts } from "../../ts/utils"; +import { deployPollArgs, setVerifyingKeysArgs, deployArgs } from "../constants"; + +describe("publish", () => { + let maciAddresses: DeployedContracts; + let pollAddresses: PollContracts; + let signer: Signer; + + const messages: IPublishMessage[] = [ + { + stateIndex: 1n, + voteOptionIndex: 1n, + nonce: 1n, + newVoteWeight: 1n, + salt: 1n, + }, + { + stateIndex: 1n, + voteOptionIndex: 2n, + nonce: 2n, + newVoteWeight: 1n, + }, + ]; + + // before all tests we deploy the vk registry contract and set the verifying keys + before(async () => { + signer = await getDefaultSigner(); + + // we deploy the vk registry contract + await deployVkRegistryContract({ signer }); + // we set the verifying keys + await setVerifyingKeys({ ...setVerifyingKeysArgs, signer }); + }); + + describe("publish batch messages", () => { + const user = new Keypair(); + + let defaultArgs: IPublishBatchArgs; + + before(async () => { + // deploy the smart contracts + maciAddresses = await deploy({ ...deployArgs, signer }); + // deploy a poll contract + pollAddresses = await deployPoll({ ...deployPollArgs, signer }); + + defaultArgs = { + maciContractAddress: maciAddresses.maciAddress, + publicKey: user.pubKey.serialize(), + privateKey: user.privKey.serialize(), + messages, + pollId: 0n, + signer, + }; + + await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: user.pubKey.serialize(), signer }); + }); + + it("should publish messages properly", async () => { + const pollContract = PollFactory.connect(pollAddresses.poll, signer); + const initialNumMessages = await pollContract.numMessages(); + + const { hash } = await publishBatch(defaultArgs); + const numMessages = await pollContract.numMessages(); + + expect(initialNumMessages).to.eq(1n); + expect(hash).to.not.eq(null); + expect(hash).to.not.eq(undefined); + expect(numMessages).to.eq(BigInt(messages.length + 1)); + }); + + it("should throw error if public key is invalid", async () => { + await expect(publishBatch({ ...defaultArgs, publicKey: "invalid" })).eventually.rejectedWith( + "invalid MACI public key", + ); + }); + + it("should throw error if private key is invalid", async () => { + await expect(publishBatch({ ...defaultArgs, privateKey: "invalid" })).eventually.rejectedWith( + "invalid MACI private key", + ); + }); + + it("should throw error if poll id is invalid", async () => { + await expect(publishBatch({ ...defaultArgs, pollId: -1n })).eventually.rejectedWith("invalid poll id -1"); + }); + + it("should throw error if current poll is not deployed", async () => { + await expect(publishBatch({ ...defaultArgs, pollId: 9000n })).eventually.rejectedWith("PollDoesNotExist(9000)"); + }); + + it("should throw error if message is invalid", async () => { + await expect( + publishBatch({ + ...defaultArgs, + messages: [...messages, { ...messages[0], voteOptionIndex: -1n }], + }), + ).eventually.rejectedWith("invalid vote option index"); + + await expect( + publishBatch({ + ...defaultArgs, + messages: [...messages, { ...messages[0], stateIndex: 0n }], + }), + ).eventually.rejectedWith("invalid state index"); + + await expect( + publishBatch({ + ...defaultArgs, + messages: [...messages, { ...messages[0], nonce: -1n }], + }), + ).eventually.rejectedWith("invalid nonce"); + + await expect( + publishBatch({ + ...defaultArgs, + messages: [...messages, { ...messages[0], salt: SNARK_FIELD_SIZE + 1n }], + }), + ).eventually.rejectedWith("invalid salt"); + }); + }); +}); diff --git a/cli/ts/commands/index.ts b/cli/ts/commands/index.ts index 0258170a5f..3b4e38c159 100644 --- a/cli/ts/commands/index.ts +++ b/cli/ts/commands/index.ts @@ -7,7 +7,7 @@ export { genKeyPair } from "./genKeyPair"; export { genMaciPubKey } from "./genPubKey"; export { mergeMessages } from "./mergeMessages"; export { mergeSignups } from "./mergeSignups"; -export { publish } from "./publish"; +export { publish, publishBatch } from "./publish"; export { setVerifyingKeys } from "./setVerifyingKeys"; export { showContracts } from "./showContracts"; export { timeTravel } from "./timeTravel"; diff --git a/cli/ts/commands/publish.ts b/cli/ts/commands/publish.ts index 7c653231a8..32460e2c99 100644 --- a/cli/ts/commands/publish.ts +++ b/cli/ts/commands/publish.ts @@ -1,8 +1,15 @@ import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-contracts/typechain-types"; import { genRandomSalt } from "maci-crypto"; -import { Keypair, PCommand, PrivKey, PubKey } from "maci-domainobjs"; +import { + type IG1ContractParams, + type IMessageContractParams, + Keypair, + PCommand, + PrivKey, + PubKey, +} from "maci-domainobjs"; -import type { PublishArgs } from "../utils/interfaces"; +import type { IPublishBatchArgs, IPublishBatchData, PublishArgs } from "../utils/interfaces"; import { banner } from "../utils/banner"; import { contractExists } from "../utils/contracts"; @@ -129,3 +136,108 @@ export const publish = async ({ // we want the user to have the ephemeral private key return encKeypair.privKey.serialize(); }; + +/** + * Batch publish new messages to a MACI Poll contract + * @param {IPublishBatchArgs} args - The arguments for the publish command + * @returns {IPublishBatchData} The ephemeral private key used to encrypt the message, transaction hash + */ +export const publishBatch = async ({ + messages, + pollId, + maciContractAddress, + publicKey, + privateKey, + signer, + quiet = true, +}: IPublishBatchArgs): Promise => { + banner(quiet); + + if (!PubKey.isValidSerializedPubKey(publicKey)) { + throw new Error("invalid MACI public key"); + } + + if (!PrivKey.isValidSerializedPrivKey(privateKey)) { + throw new Error("invalid MACI private key"); + } + + if (pollId < 0n) { + throw new Error(`invalid poll id ${pollId}`); + } + + const userMaciPubKey = PubKey.deserialize(publicKey); + const userMaciPrivKey = PrivKey.deserialize(privateKey); + const maciContract = MACIFactory.connect(maciContractAddress, signer); + const pollAddress = await maciContract.getPoll(pollId); + + const pollContract = PollFactory.connect(pollAddress, signer); + + const [maxValues, coordinatorPubKeyResult] = await Promise.all([ + pollContract.maxValues(), + pollContract.coordinatorPubKey(), + ]); + const maxVoteOptions = Number(maxValues.maxVoteOptions); + + // validate the vote options index against the max leaf index on-chain + messages.forEach(({ stateIndex, voteOptionIndex, salt, nonce }) => { + if (voteOptionIndex < 0 || maxVoteOptions < voteOptionIndex) { + 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 coordinatorPubKey = new PubKey([ + BigInt(coordinatorPubKeyResult.x.toString()), + BigInt(coordinatorPubKeyResult.y.toString()), + ]); + + const encryptionKeypair = new Keypair(); + + const payload: [IMessageContractParams, IG1ContractParams][] = messages.map( + ({ salt, stateIndex, voteOptionIndex, newVoteWeight, nonce }) => { + const userSalt = salt ? BigInt(salt) : genRandomSalt(); + + // create the command object + const command = new PCommand( + stateIndex, + userMaciPubKey, + voteOptionIndex, + newVoteWeight, + nonce, + BigInt(pollId), + userSalt, + ); + + // sign the command with the user private key + const signature = command.sign(userMaciPrivKey); + + const sharedKey = Keypair.genEcdhSharedKey(encryptionKeypair.privKey, coordinatorPubKey); + const message = command.encrypt(signature, sharedKey); + + return [message.asContractParam(), encryptionKeypair.pubKey.asContractParam()]; + }, + ); + + const receipt = await pollContract + .publishMessageBatch( + payload.map(([message]) => message), + payload.map(([, key]) => key), + ) + .then((tx) => tx.wait()); + + return { + hash: receipt?.hash, + }; +}; diff --git a/cli/ts/index.ts b/cli/ts/index.ts index bdd2d888ca..1d6a433725 100644 --- a/cli/ts/index.ts +++ b/cli/ts/index.ts @@ -725,6 +725,7 @@ export { mergeMessages, mergeSignups, publish, + publishBatch, proveOnChain, setVerifyingKeys, signup, @@ -753,4 +754,10 @@ export type { SubsidyData, IRegisteredUserArgs, IGenKeypairArgs, + IGetPollArgs, + IGetPollData, + IPublishBatchArgs, + IPublishBatchData, + IPublishMessage, + ISignupData, } from "./utils"; diff --git a/cli/ts/sdk/index.ts b/cli/ts/sdk/index.ts index 7d3a818b34..20166b677f 100644 --- a/cli/ts/sdk/index.ts +++ b/cli/ts/sdk/index.ts @@ -1,11 +1,11 @@ import { genKeyPair } from "../commands/genKeyPair"; import { genMaciPubKey } from "../commands/genPubKey"; import { getPoll } from "../commands/poll"; -import { publish } from "../commands/publish"; +import { publish, publishBatch } from "../commands/publish"; import { signup, isRegisteredUser } from "../commands/signup"; import { verify } from "../commands/verify"; -export { genKeyPair, genMaciPubKey, publish, signup, isRegisteredUser, verify, getPoll }; +export { genKeyPair, genMaciPubKey, publish, publishBatch, signup, isRegisteredUser, verify, getPoll }; export type { Signer } from "ethers"; @@ -19,4 +19,8 @@ export type { IGetPollArgs, IGetPollData, IRegisteredUserArgs, + IPublishBatchArgs, + IGenKeypairArgs, + IPublishBatchData, + IPublishMessage, } from "../utils"; diff --git a/cli/ts/utils/index.ts b/cli/ts/utils/index.ts index 6500faa4dd..082cf5a505 100644 --- a/cli/ts/utils/index.ts +++ b/cli/ts/utils/index.ts @@ -43,6 +43,9 @@ export type { IGenKeypairArgs, IGetPollArgs, IGetPollData, + IPublishBatchArgs, + IPublishBatchData, + IPublishMessage, } from "./interfaces"; export { compareVks } from "./vks"; export { delay } from "./time"; diff --git a/cli/ts/utils/interfaces.ts b/cli/ts/utils/interfaces.ts index 9a4101d394..02efcaefb6 100644 --- a/cli/ts/utils/interfaces.ts +++ b/cli/ts/utils/interfaces.ts @@ -708,56 +708,71 @@ export interface ProveOnChainArgs { /** * Interface for the arguments to the publish command */ -export interface PublishArgs { +export interface PublishArgs extends IPublishMessage { /** * The public key of the user */ pubkey: string; /** - * The index of the state leaf + * The private key of the user */ - stateIndex: bigint; + privateKey: string; /** - * The index of the vote option + * The address of the MACI contract */ - voteOptionIndex: bigint; + maciContractAddress: string; /** - * The nonce of the message + * The id of the poll */ - nonce: bigint; + pollId: bigint; /** - * The id of the poll + * A signer object */ - pollId: bigint; + signer: Signer; /** - * The new vote weight + * Whether to log the output */ - newVoteWeight: bigint; + quiet?: boolean; +} +/** + * Interface for the arguments to the batch publish command + */ +export interface IPublishBatchArgs { /** - * A signer object + * User messages */ - signer: Signer; + messages: IPublishMessage[]; + + /** + * The id of the poll + */ + pollId: bigint; /** * The address of the MACI contract */ maciContractAddress: string; + /** + * The public key of the user + */ + publicKey: string; + /** * The private key of the user */ privateKey: string; /** - * The salt of the message + * A signer object */ - salt?: bigint; + signer: Signer; /** * Whether to log the output @@ -765,6 +780,40 @@ export interface PublishArgs { quiet?: boolean; } +/** + * Interface that represents user publish message + */ +export interface IPublishMessage { + /** + * The index of the state leaf + */ + stateIndex: bigint; + + /** + * The index of the vote option + */ + voteOptionIndex: bigint; + + /** + * The nonce of the message + */ + nonce: bigint; + + /** + * The new vote weight + */ + newVoteWeight: bigint; + + /** + * The salt of the message + */ + salt?: bigint; +} + +export interface IPublishBatchData { + hash?: string; +} + /** * Interface for the arguments to the setVerifyingKeys command */