Skip to content

Commit

Permalink
Merge pull request #1251 from privacy-scaling-explorations/feature/vo…
Browse files Browse the repository at this point in the history
…ting

feat(cli): add publish batch method for cli sdk
  • Loading branch information
0xmad committed Feb 27, 2024
2 parents d3952dd + 843fe7e commit 8b9a018
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 20 deletions.
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();

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

0 comments on commit 8b9a018

Please sign in to comment.