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(contracts): allow for dynamic emptyBallotRoots #1695

Merged
merged 1 commit into from
Jul 25, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ on:
branches: [dev]
pull_request:

env:
STATE_TREE_DEPTH: ${{ vars.STATE_TREE_DEPTH }}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ on:
push:
tags: ["*"]

env:
STATE_TREE_DEPTH: ${{ vars.STATE_TREE_DEPTH }}

jobs:
draft-release:
runs-on: ubuntu-22.04
Expand Down
30 changes: 21 additions & 9 deletions contracts/contracts/MACI.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ contract MACI is IMACI, DomainObjs, Params, Utilities {
uint256 internal constant BLANK_STATE_LEAF_HASH =
uint256(6769006970205099520508948723718471724660867171122235270773600567925038008762);

/// @notice The roots of the empty ballot trees
uint256[5] public emptyBallotRoots;

/// @notice Each poll has an incrementing ID
uint256 public nextPollId;

Expand Down Expand Up @@ -96,13 +99,15 @@ contract MACI is IMACI, DomainObjs, Params, Utilities {
/// @param _signUpGatekeeper The SignUpGatekeeper contract
/// @param _initialVoiceCreditProxy The InitialVoiceCreditProxy contract
/// @param _stateTreeDepth The depth of the state tree
/// @param _emptyBallotRoots The roots of the empty ballot trees
constructor(
IPollFactory _pollFactory,
IMessageProcessorFactory _messageProcessorFactory,
ITallyFactory _tallyFactory,
SignUpGatekeeper _signUpGatekeeper,
InitialVoiceCreditProxy _initialVoiceCreditProxy,
uint8 _stateTreeDepth
uint8 _stateTreeDepth,
uint256[5] memory _emptyBallotRoots
) payable {
// initialize and insert the blank leaf
InternalLazyIMT._init(lazyIMTData, _stateTreeDepth);
Expand All @@ -115,6 +120,7 @@ contract MACI is IMACI, DomainObjs, Params, Utilities {
initialVoiceCreditProxy = _initialVoiceCreditProxy;
stateTreeDepth = _stateTreeDepth;
maxSignups = uint256(TREE_ARITY) ** uint256(_stateTreeDepth);
emptyBallotRoots = _emptyBallotRoots;

// Verify linked poseidon libraries
if (hash2([uint256(1), uint256(1)]) == 0) revert PoseidonHashLibrariesNotLinked();
Expand Down Expand Up @@ -191,18 +197,24 @@ contract MACI is IMACI, DomainObjs, Params, Utilities {
revert InvalidPubKey();
}

uint256 voteOptionTreeDepth = _treeDepths.voteOptionTreeDepth;

MaxValues memory maxValues = MaxValues({
maxMessages: uint256(MESSAGE_TREE_ARITY) ** _treeDepths.messageTreeDepth,
maxVoteOptions: uint256(MESSAGE_TREE_ARITY) ** _treeDepths.voteOptionTreeDepth
maxVoteOptions: uint256(MESSAGE_TREE_ARITY) ** voteOptionTreeDepth
});

// the owner of the message processor and tally contract will be the msg.sender
address _msgSender = msg.sender;

address p = pollFactory.deploy(_duration, maxValues, _treeDepths, _coordinatorPubKey, address(this));

address mp = messageProcessorFactory.deploy(_verifier, _vkRegistry, p, _msgSender, _mode);
address tally = tallyFactory.deploy(_verifier, _vkRegistry, p, mp, _msgSender, _mode);
address p = pollFactory.deploy(
_duration,
maxValues,
_treeDepths,
_coordinatorPubKey,
address(this),
emptyBallotRoots[voteOptionTreeDepth - 1]
);

address mp = messageProcessorFactory.deploy(_verifier, _vkRegistry, p, msg.sender, _mode);
address tally = tallyFactory.deploy(_verifier, _vkRegistry, p, mp, msg.sender, _mode);

polls[pollId] = p;

Expand Down
13 changes: 9 additions & 4 deletions contracts/contracts/Poll.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import { Params } from "./utilities/Params.sol";
import { SnarkCommon } from "./crypto/SnarkCommon.sol";
import { EmptyBallotRoots } from "./trees/EmptyBallotRoots.sol";
import { IPoll } from "./interfaces/IPoll.sol";
import { Utilities } from "./utilities/Utilities.sol";
import { CurveBabyJubJub } from "./crypto/BabyJubJub.sol";
Expand All @@ -13,241 +12,247 @@
/// which can be either votes or key change messages.
/// @dev Do not deploy this directly. Use PollFactory.deploy() which performs some
/// checks on the Poll constructor arguments.
contract Poll is Params, Utilities, SnarkCommon, EmptyBallotRoots, IPoll {
contract Poll is Params, Utilities, SnarkCommon, IPoll {
/// @notice Whether the Poll has been initialized
bool internal isInit;

/// @notice The coordinator's public key
PubKey public coordinatorPubKey;

/// @notice Hash of the coordinator's public key
uint256 public immutable coordinatorPubKeyHash;

/// @notice the state root of the state merkle tree
uint256 public mergedStateRoot;

// The timestamp of the block at which the Poll was deployed
uint256 internal immutable deployTime;

// The duration of the polling period, in seconds
uint256 internal immutable duration;

/// @notice The root of the empty ballot tree at a given voteOptionTree depth
uint256 public immutable emptyBallotRoot;

/// @notice Whether the MACI contract's stateAq has been merged by this contract
bool public stateMerged;

/// @notice Get the commitment to the state leaves and the ballots. This is
/// hash3(stateRoot, ballotRoot, salt).
/// Its initial value should be
/// hash(maciStateRootSnapshot, emptyBallotRoot, 0)
/// Each successful invocation of processMessages() should use a different
/// salt to update this value, so that an external observer cannot tell in
/// the case that none of the messages are valid.
uint256 public currentSbCommitment;

/// @notice The number of messages that have been published
uint256 public numMessages;

/// @notice The number of signups that have been processed
/// before the Poll ended (stateAq merged)
uint256 public numSignups;

/// @notice The actual depth of the state tree
/// to be used as public input for the circuit
uint8 public actualStateTreeDepth;

/// @notice Max values for the poll
MaxValues public maxValues;

/// @notice Depths of the merkle trees
TreeDepths public treeDepths;

/// @notice The contracts used by the Poll
ExtContracts public extContracts;

error VotingPeriodOver();
error VotingPeriodNotOver();
error PollAlreadyInit();
error TooManyMessages();
error InvalidPubKey();
error StateAlreadyMerged();
error InvalidBatchLength();

event PublishMessage(Message _message, PubKey _encPubKey);
event MergeMaciState(uint256 indexed _stateRoot, uint256 indexed _numSignups);
event MergeMessageAqSubRoots(uint256 indexed _numSrQueueOps);
event MergeMessageAq(uint256 indexed _messageRoot);

/// @notice Each MACI instance can have multiple Polls.
/// When a Poll is deployed, its voting period starts immediately.
/// @param _duration The duration of the voting period, in seconds
/// @param _maxValues The maximum number of messages and vote options
/// @param _treeDepths The depths of the merkle trees
/// @param _coordinatorPubKey The coordinator's public key
/// @param _extContracts The external contracts
constructor(
uint256 _duration,
MaxValues memory _maxValues,
TreeDepths memory _treeDepths,
PubKey memory _coordinatorPubKey,
ExtContracts memory _extContracts
ExtContracts memory _extContracts,
uint256 _emptyBallotRoot
) payable {
// check that the coordinator public key is valid
if (!CurveBabyJubJub.isOnCurve(_coordinatorPubKey.x, _coordinatorPubKey.y)) {
revert InvalidPubKey();
}

// store the pub key as object then calculate the hash
coordinatorPubKey = _coordinatorPubKey;
// we hash it ourselves to ensure we store the correct value
coordinatorPubKeyHash = hashLeftRight(_coordinatorPubKey.x, _coordinatorPubKey.y);
// store the external contracts to interact with
extContracts = _extContracts;
// store duration of the poll
duration = _duration;
// store max values
maxValues = _maxValues;
// store tree depth
treeDepths = _treeDepths;
// Record the current timestamp
deployTime = block.timestamp;
// store the empty ballot root
emptyBallotRoot = _emptyBallotRoot;
}

/// @notice A modifier that causes the function to revert if the voting period is
/// not over.
modifier isAfterVotingDeadline() {
uint256 secondsPassed = block.timestamp - deployTime;
if (secondsPassed <= duration) revert VotingPeriodNotOver();
_;
}

/// @notice A modifier that causes the function to revert if the voting period is
/// over
modifier isWithinVotingDeadline() {
uint256 secondsPassed = block.timestamp - deployTime;
if (secondsPassed >= duration) revert VotingPeriodOver();
_;
}

/// @notice The initialization function.
/// @dev Should be called immediately after Poll creation
/// and messageAq ownership transferred
function init() public {
if (isInit) revert PollAlreadyInit();
// set to true so it cannot be called again
isInit = true;

unchecked {
numMessages++;
}

// init messageAq here by inserting placeholderLeaf
uint256[2] memory dat;
dat[0] = NOTHING_UP_MY_SLEEVE;
dat[1] = 0;

(Message memory _message, PubKey memory _padKey, uint256 placeholderLeaf) = padAndHashMessage(dat);
extContracts.messageAq.enqueue(placeholderLeaf);

emit PublishMessage(_message, _padKey);
}

/// @inheritdoc IPoll
function publishMessage(Message memory _message, PubKey calldata _encPubKey) public virtual isWithinVotingDeadline {
// we check that we do not exceed the max number of messages
if (numMessages >= maxValues.maxMessages) revert TooManyMessages();

// check if the public key is on the curve
if (!CurveBabyJubJub.isOnCurve(_encPubKey.x, _encPubKey.y)) {
revert InvalidPubKey();
}

// cannot realistically overflow
unchecked {
numMessages++;
}

uint256 messageLeaf = hashMessageAndEncPubKey(_message, _encPubKey);
extContracts.messageAq.enqueue(messageLeaf);

emit PublishMessage(_message, _encPubKey);
}

/// @notice submit a message batch
/// @dev Can only be submitted before the voting deadline
/// @param _messages the messages
/// @param _encPubKeys the encrypted public keys
function publishMessageBatch(Message[] calldata _messages, PubKey[] calldata _encPubKeys) external {
if (_messages.length != _encPubKeys.length) {
revert InvalidBatchLength();
}

uint256 len = _messages.length;
for (uint256 i = 0; i < len; ) {
// an event will be published by this function already
publishMessage(_messages[i], _encPubKeys[i]);

unchecked {
i++;
}
}
}

/// @inheritdoc IPoll
function mergeMaciState() public isAfterVotingDeadline {
// This function can only be called once per Poll after the voting
// deadline
if (stateMerged) revert StateAlreadyMerged();

// set merged to true so it cannot be called again
stateMerged = true;

uint256 _mergedStateRoot = extContracts.maci.getStateTreeRoot();
mergedStateRoot = _mergedStateRoot;

// Set currentSbCommitment
uint256[3] memory sb;
sb[0] = _mergedStateRoot;
sb[1] = emptyBallotRoots[treeDepths.voteOptionTreeDepth - 1];
sb[1] = emptyBallotRoot;
sb[2] = uint256(0);

currentSbCommitment = hash3(sb);

// get number of signups and cache in a var for later use
uint256 _numSignups = extContracts.maci.numSignUps();
numSignups = _numSignups;

// dynamically determine the actual depth of the state tree
uint8 depth = 1;
while (uint40(2) ** uint40(depth) < _numSignups) {
depth++;
}

actualStateTreeDepth = depth;

emit MergeMaciState(_mergedStateRoot, _numSignups);
}

/// @inheritdoc IPoll
function mergeMessageAqSubRoots(uint256 _numSrQueueOps) public isAfterVotingDeadline {
extContracts.messageAq.mergeSubRoots(_numSrQueueOps);
emit MergeMessageAqSubRoots(_numSrQueueOps);
}

/// @inheritdoc IPoll
function mergeMessageAq() public isAfterVotingDeadline {
uint256 root = extContracts.messageAq.merge(treeDepths.messageTreeDepth);
emit MergeMessageAq(root);
}

/// @inheritdoc IPoll
function getDeployTimeAndDuration() public view returns (uint256 pollDeployTime, uint256 pollDuration) {
pollDeployTime = deployTime;
pollDuration = duration;
}

/// @inheritdoc IPoll
function numSignUpsAndMessages() public view returns (uint256 numSUps, uint256 numMsgs) {
numSUps = numSignups;
numMsgs = numMessages;
}
}

Check warning

Code scanning / Slither

Contracts that lock Ether Medium

Contract locking ether found:
Contract Poll has payable functions:
- Poll.constructor(uint256,Params.TreeDepths,DomainObjs.PubKey,Params.ExtContracts,uint256)
But does not have a function to withdraw the ether
5 changes: 3 additions & 2 deletions contracts/contracts/PollFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
MaxValues calldata _maxValues,
TreeDepths calldata _treeDepths,
PubKey calldata _coordinatorPubKey,
address _maci
address _maci,
Dismissed Show dismissed Hide dismissed
uint256 _emptyBallotRoot
Dismissed Show dismissed Hide dismissed
) public virtual returns (address pollAddr) {
/// @notice Validate _maxValues
/// maxVoteOptions must be less than 2 ** 50 due to circuit limitations;
Expand All @@ -43,7 +44,7 @@
ExtContracts memory extContracts = ExtContracts({ maci: IMACI(_maci), messageAq: messageAq });

// deploy the poll
Poll poll = new Poll(_duration, _maxValues, _treeDepths, _coordinatorPubKey, extContracts);
Poll poll = new Poll(_duration, _maxValues, _treeDepths, _coordinatorPubKey, extContracts, _emptyBallotRoot);

// Make the Poll contract own the messageAq contract, so only it can
// run enqueue/merge
Expand Down
4 changes: 3 additions & 1 deletion contracts/contracts/interfaces/IPollFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ interface IPollFactory {
/// @param _treeDepths The depths of the merkle trees
/// @param _coordinatorPubKey The coordinator's public key
/// @param _maci The MACI contract interface reference
/// @param _emptyBallotRoot The root of the empty ballot tree
/// @return The deployed Poll contract
function deploy(
uint256 _duration,
Params.MaxValues memory _maxValues,
Params.TreeDepths memory _treeDepths,
DomainObjs.PubKey memory _coordinatorPubKey,
address _maci
address _maci,
uint256 _emptyBallotRoot
) external returns (address);
}
1 change: 0 additions & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@
"@nomicfoundation/hardhat-ethers": "^3.0.6",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@openzeppelin/contracts": "^5.0.2",
"@zk-kit/imt.sol": "2.0.0-beta.12",
"circomlibjs": "^0.1.7",
"ethers": "^6.13.1",
"hardhat": "^2.22.6",
Expand Down
5 changes: 0 additions & 5 deletions contracts/scripts/compileSol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import fs from "fs";
import path from "path";

import { buildPoseidonT3, buildPoseidonT4, buildPoseidonT5, buildPoseidonT6 } from "../ts/buildPoseidon";
import { genEmptyBallotRootsContract } from "../ts/genEmptyBallotRootsContract";
import { genZerosContract } from "../ts/genZerosContract";

const PATHS = [
Expand Down Expand Up @@ -75,10 +74,6 @@ async function main(): Promise<void> {
),
);

await genEmptyBallotRootsContract().then((text) =>
fs.promises.writeFile(path.resolve(__dirname, "..", "contracts/trees/EmptyBallotRoots.sol"), `${text}\n`),
);

await hre.run("compile");

await Promise.all([buildPoseidonT3(), buildPoseidonT4(), buildPoseidonT5(), buildPoseidonT6()]);
Expand Down
5 changes: 5 additions & 0 deletions contracts/tasks/deploy/maci/08-maci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
SemaphoreGatekeeper,
} from "../../../typechain-types";

import { genEmptyBallotRoots } from "../../../ts/genEmptyBallotRoots";
import { ContractStorage } from "../../helpers/ContractStorage";
import { Deployment } from "../../helpers/Deployment";
import { EContracts, IDeployParams } from "../../helpers/types";
Expand Down Expand Up @@ -62,6 +63,8 @@ deployment.deployTask("full:deploy-maci", "Deploy MACI contract").then((task) =>
const stateTreeDepth =
deployment.getDeployConfigField<number | null>(EContracts.MACI, "stateTreeDepth") ?? DEFAULT_STATE_TREE_DEPTH;

const emptyBallotRoots = genEmptyBallotRoots(stateTreeDepth);

const maciContract = await deployment.deployContractWithLinkedLibraries<MACI>(
{ contractFactory: maciContractFactory },
pollFactoryContractAddress,
Expand All @@ -70,6 +73,7 @@ deployment.deployTask("full:deploy-maci", "Deploy MACI contract").then((task) =>
gatekeeperContractAddress,
constantInitialVoiceCreditProxyContractAddress,
stateTreeDepth,
emptyBallotRoots,
);

if (gatekeeper === EContracts.EASGatekeeper) {
Expand Down Expand Up @@ -115,6 +119,7 @@ deployment.deployTask("full:deploy-maci", "Deploy MACI contract").then((task) =>
gatekeeperContractAddress,
constantInitialVoiceCreditProxyContractAddress,
stateTreeDepth,
emptyBallotRoots,
],
network: hre.network.name,
});
Expand Down
26 changes: 0 additions & 26 deletions contracts/templates/EmptyBallotRoots.sol.template

This file was deleted.

9 changes: 7 additions & 2 deletions contracts/tests/PollFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ import { expect } from "chai";
import { BaseContract, Signer, ZeroAddress } from "ethers";
import { Keypair } from "maci-domainobjs";

import { deployPollFactory, getDefaultSigner } from "../ts";
import { deployPollFactory, genEmptyBallotRoots, getDefaultSigner } from "../ts";
import { PollFactory } from "../typechain-types";

import { maxValues, treeDepths } from "./constants";
import { maxValues, STATE_TREE_DEPTH, treeDepths } from "./constants";

describe("pollFactory", () => {
let pollFactory: PollFactory;
let signer: Signer;

const { pubKey: coordinatorPubKey } = new Keypair();

const emptyBallotRoots = genEmptyBallotRoots(STATE_TREE_DEPTH);
const emptyBallotRoot = emptyBallotRoots[treeDepths.voteOptionTreeDepth];

before(async () => {
signer = await getDefaultSigner();
pollFactory = (await deployPollFactory(signer, true)) as BaseContract as PollFactory;
Expand All @@ -26,6 +29,7 @@ describe("pollFactory", () => {
treeDepths,
coordinatorPubKey.asContractParam(),
ZeroAddress,
emptyBallotRoot,
);
const receipt = await tx.wait();
expect(receipt?.status).to.eq(1);
Expand All @@ -42,6 +46,7 @@ describe("pollFactory", () => {
treeDepths,
coordinatorPubKey.asContractParam(),
ZeroAddress,
emptyBallotRoot,
),
).to.be.revertedWithCustomError(pollFactory, "InvalidMaxValues");
});
Expand Down
4 changes: 4 additions & 0 deletions contracts/ts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
SemaphoreGatekeeper,
} from "../typechain-types";

import { genEmptyBallotRoots } from "./genEmptyBallotRoots";
import { log } from "./utils";

/**
Expand Down Expand Up @@ -271,6 +272,8 @@ export const deployMaci = async ({
stateTreeDepth = 10,
quiet = true,
}: IDeployMaciArgs): Promise<IDeployedMaci> => {
const emptyBallotRoots = genEmptyBallotRoots(stateTreeDepth);

const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } =
await deployPoseidonContracts(signer, poseidonAddresses, quiet);

Expand Down Expand Up @@ -328,6 +331,7 @@ export const deployMaci = async ({
signUpTokenGatekeeperContractAddress,
initialVoiceCreditBalanceAddress,
stateTreeDepth,
emptyBallotRoots,
);

return {
Expand Down
22 changes: 22 additions & 0 deletions contracts/ts/genEmptyBallotRoots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { STATE_TREE_ARITY } from "maci-core";
import { IncrementalQuinTree, hash2 } from "maci-crypto";
import { Ballot } from "maci-domainobjs";

/**
* Generate empty ballot roots for a given state tree depth
* @param stateTreeDepth The depth of the state tree
* @returns The empty ballot roots
*/
export const genEmptyBallotRoots = (stateTreeDepth: number): bigint[] => {
const roots: bigint[] = [];

for (let i = 0; i < 5; i += 1) {
const ballot = new Ballot(0, i + 1);
// The empty Ballot tree root
const ballotTree = new IncrementalQuinTree(stateTreeDepth, ballot.hash(), STATE_TREE_ARITY, hash2);

roots.push(ballotTree.root);
}

return roots;
};
27 changes: 0 additions & 27 deletions contracts/ts/genEmptyBallotRootsContract.ts

This file was deleted.

1 change: 1 addition & 0 deletions contracts/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
deployVerifier,
} from "./deploy";
export { genMaciStateFromContract } from "./genMaciState";
export { genEmptyBallotRoots } from "./genEmptyBallotRoots";
export { formatProofForVerifierContract, getDefaultSigner, getDefaultNetwork, getSigners } from "./utils";
export { EMode } from "./constants";
export { Deployment } from "../tasks/helpers/Deployment";
Expand Down
Loading
Loading