Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add publish batch method for cli sdk #1251

Merged
merged 1 commit into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
137 changes: 137 additions & 0 deletions cli/tests/unit/publish.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
2 changes: 1 addition & 1 deletion cli/ts/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
116 changes: 114 additions & 2 deletions cli/ts/commands/publish.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<IPublishBatchData> => {
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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the point of using the same key here for all messages in the batch, we just assume that all messages in the batch are from the same user either way for simplicity sake (though that doesn't necessarily have to be the case), so I would suggest we move line 226 out of the loop to avoid computing the ECDH key every time; and also we should probably return this key like we do in publishMessage.


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 preparedMessages = payload.map(([message]) => message);
const preparedKeys = payload.map(([, key]) => key);

const receipt = await pollContract
.publishMessageBatch(preparedMessages.reverse(), preparedKeys.reverse())
.then((tx) => tx.wait());

return {
hash: receipt?.hash,
};
};
7 changes: 7 additions & 0 deletions cli/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ export {
mergeMessages,
mergeSignups,
publish,
publishBatch,
proveOnChain,
setVerifyingKeys,
signup,
Expand Down Expand Up @@ -753,4 +754,10 @@ export type {
SubsidyData,
IRegisteredUserArgs,
IGenKeypairArgs,
IGetPollArgs,
IGetPollData,
IPublishBatchArgs,
IPublishBatchData,
IPublishMessage,
ISignupData,
} from "./utils";
8 changes: 6 additions & 2 deletions cli/ts/sdk/index.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -19,4 +19,8 @@ export type {
IGetPollArgs,
IGetPollData,
IRegisteredUserArgs,
IPublishBatchArgs,
IGenKeypairArgs,
IPublishBatchData,
IPublishMessage,
} from "../utils";
3 changes: 3 additions & 0 deletions cli/ts/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export type {
IGenKeypairArgs,
IGetPollArgs,
IGetPollData,
IPublishBatchArgs,
IPublishBatchData,
IPublishMessage,
} from "./interfaces";
export { compareVks } from "./vks";
export { delay } from "./time";
Expand Down