From c9e7efc825be2d0aaabae014ae51a97e2adaa413 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:07:06 +0000 Subject: [PATCH] feat: make nullifier not leak identity between polls (#1974) --- packages/circuits/circom/anon/pollJoining.circom | 9 ++++----- packages/circuits/circom/circuits.json | 2 +- .../circuits/ts/__tests__/PollJoining.test.ts | 1 + packages/circuits/ts/types.ts | 1 + packages/cli/ts/commands/joinPoll.ts | 16 ++++++++++------ packages/contracts/contracts/MACI.sol | 3 ++- packages/contracts/contracts/Poll.sol | 12 ++++++++++-- packages/contracts/contracts/PollFactory.sol | 6 ++++-- .../contracts/interfaces/IPollFactory.sol | 4 +++- packages/contracts/tests/PollFactory.test.ts | 1 + packages/core/ts/Poll.ts | 4 +++- packages/core/ts/utils/types.ts | 1 + 12 files changed, 41 insertions(+), 19 deletions(-) diff --git a/packages/circuits/circom/anon/pollJoining.circom b/packages/circuits/circom/anon/pollJoining.circom index b66513bac..323a3d153 100644 --- a/packages/circuits/circom/anon/pollJoining.circom +++ b/packages/circuits/circom/anon/pollJoining.circom @@ -41,8 +41,11 @@ template PollJoining(stateTreeDepth) { signal input stateRoot; // The actual tree depth (might be <= stateTreeDepth) Used in BinaryMerkleRoot signal input actualStateTreeDepth; + // The poll id + signal input pollId; - var computedNullifier = PoseidonHasher(1)([privKey]); + // Compute the nullifier (hash of private key and poll id) + var computedNullifier = PoseidonHasher(2)([privKey, pollId]); nullifier === computedNullifier; // User private to public key @@ -69,8 +72,4 @@ template PollJoining(stateTreeDepth) { // Check credits var isCreditsValid = SafeLessEqThan(N_BITS)([credits, stateLeaf[STATE_LEAF_VOICE_CREDIT_BALANCE_IDX]]); isCreditsValid === 1; - - // Check nullifier - var hashedPrivKey = PoseidonHasher(1)([privKey]); - hashedPrivKey === nullifier; } diff --git a/packages/circuits/circom/circuits.json b/packages/circuits/circom/circuits.json index bcf56fbad..d9a932841 100644 --- a/packages/circuits/circom/circuits.json +++ b/packages/circuits/circom/circuits.json @@ -3,7 +3,7 @@ "file": "./anon/pollJoining", "template": "PollJoining", "params": [10], - "pubs": ["nullifier", "credits", "stateRoot", "pollPubKey"] + "pubs": ["nullifier", "credits", "stateRoot", "pollPubKey", "pollId"] }, "ProcessMessages_10-20-2_test": { "file": "./core/qv/processMessages", diff --git a/packages/circuits/ts/__tests__/PollJoining.test.ts b/packages/circuits/ts/__tests__/PollJoining.test.ts index 5bfedadd8..eb68e305e 100644 --- a/packages/circuits/ts/__tests__/PollJoining.test.ts +++ b/packages/circuits/ts/__tests__/PollJoining.test.ts @@ -26,6 +26,7 @@ describe("Poll Joining circuit", function test() { "credits", "stateRoot", "actualStateTreeDepth", + "pollId", ]; let circuit: WitnessTester; diff --git a/packages/circuits/ts/types.ts b/packages/circuits/ts/types.ts index 1ac9ba303..2d9bdba10 100644 --- a/packages/circuits/ts/types.ts +++ b/packages/circuits/ts/types.ts @@ -54,6 +54,7 @@ export interface IPollJoiningInputs { credits: bigint; stateRoot: bigint; actualStateTreeDepth: bigint; + pollId: bigint; } /** diff --git a/packages/cli/ts/commands/joinPoll.ts b/packages/cli/ts/commands/joinPoll.ts index a78b52a6d..ab9fa3e13 100644 --- a/packages/cli/ts/commands/joinPoll.ts +++ b/packages/cli/ts/commands/joinPoll.ts @@ -101,6 +101,7 @@ const generateAndVerifyProof = async ( * @param credits Credits for voting * @param pollPrivKey Poll's private key for the poll joining * @param pollPubKey Poll's public key for the poll joining + * @param pollId Poll's id * @returns stringified circuit inputs */ const joiningCircuitInputs = ( @@ -111,6 +112,7 @@ const joiningCircuitInputs = ( credits: bigint, pollPrivKey: PrivKey, pollPubKey: PubKey, + pollId: bigint, ): IPollJoiningCircuitInputs => { // Get the state leaf on the index position const { signUpTree: stateTree, stateLeaves } = signUpData; @@ -146,7 +148,7 @@ const joiningCircuitInputs = ( // Create nullifier from private key const inputNullifier = BigInt(maciPrivKey.asCircuitInputs()); - const nullifier = poseidon([inputNullifier]); + const nullifier = poseidon([inputNullifier, pollId]); // Get pll state tree's root const stateRoot = stateTree.root; @@ -167,6 +169,7 @@ const joiningCircuitInputs = ( credits, stateRoot, actualStateTreeDepth, + pollId, }; return stringifyBigInts(circuitInputs) as unknown as IPollJoiningCircuitInputs; @@ -208,19 +211,19 @@ export const joinPoll = async ({ logError("Invalid MACI private key"); } + if (pollId < 0) { + logError("Invalid poll id"); + } + const userMaciPrivKey = PrivKey.deserialize(privateKey); const userMaciPubKey = new Keypair(userMaciPrivKey).pubKey; - const nullifier = poseidon([BigInt(userMaciPrivKey.asCircuitInputs())]); + const nullifier = poseidon([BigInt(userMaciPrivKey.asCircuitInputs()), pollId]); // Create poll public key from poll private key const pollPrivKeyDeserialized = PrivKey.deserialize(pollPrivKey); const pollKeyPair = new Keypair(pollPrivKeyDeserialized); const pollPubKey = pollKeyPair.pubKey; - if (pollId < 0) { - logError("Invalid poll id"); - } - const maciContract = MACIFactory.connect(maciAddress, signer); const pollContracts = await maciContract.getPoll(pollId); @@ -325,6 +328,7 @@ export const joinPoll = async ({ loadedCreditBalance!, pollPrivKeyDeserialized, pollPubKey, + pollId, ) as unknown as CircuitInputs; } diff --git a/packages/contracts/contracts/MACI.sol b/packages/contracts/contracts/MACI.sol index 3575d6a7c..d25eff705 100644 --- a/packages/contracts/contracts/MACI.sol +++ b/packages/contracts/contracts/MACI.sol @@ -220,7 +220,8 @@ contract MACI is IMACI, DomainObjs, Params, Utilities { _messageBatchSize, _coordinatorPubKey, extContracts, - emptyBallotRoots[voteOptionTreeDepth - 1] + emptyBallotRoots[voteOptionTreeDepth - 1], + pollId ); address mp = messageProcessorFactory.deploy(_verifier, _vkRegistry, p, msg.sender, _mode); diff --git a/packages/contracts/contracts/Poll.sol b/packages/contracts/contracts/Poll.sol index d620c0390..fe4430307 100644 --- a/packages/contracts/contracts/Poll.sol +++ b/packages/contracts/contracts/Poll.sol @@ -93,6 +93,9 @@ contract Poll is Params, Utilities, SnarkCommon, IPoll { /// @notice Poll voting nullifier mapping(uint256 => bool) private pollNullifier; + /// @notice The Id of this poll + uint256 public immutable pollId; + error VotingPeriodOver(); error VotingPeriodNotOver(); error PollAlreadyInit(); @@ -125,13 +128,15 @@ contract Poll is Params, Utilities, SnarkCommon, IPoll { /// @param _coordinatorPubKey The coordinator's public key /// @param _extContracts The external contracts /// @param _emptyBallotRoot The root of the empty ballot tree + /// @param _pollId The poll id constructor( uint256 _duration, TreeDepths memory _treeDepths, uint8 _messageBatchSize, PubKey memory _coordinatorPubKey, ExtContracts memory _extContracts, - uint256 _emptyBallotRoot + uint256 _emptyBallotRoot, + uint256 _pollId ) payable { // check that the coordinator public key is valid if (!CurveBabyJubJub.isOnCurve(_coordinatorPubKey.x, _coordinatorPubKey.y)) { @@ -156,6 +161,8 @@ contract Poll is Params, Utilities, SnarkCommon, IPoll { deployTime = block.timestamp; // store the empty ballot root emptyBallotRoot = _emptyBallotRoot; + // store the poll id + pollId = _pollId; } /// @notice A modifier that causes the function to revert if the voting period is @@ -350,13 +357,14 @@ contract Poll is Params, Utilities, SnarkCommon, IPoll { uint256 _index, PubKey calldata _pubKey ) public view returns (uint256[] memory publicInputs) { - publicInputs = new uint256[](5); + publicInputs = new uint256[](6); publicInputs[0] = _pubKey.x; publicInputs[1] = _pubKey.y; publicInputs[2] = _nullifier; publicInputs[3] = _voiceCreditBalance; publicInputs[4] = extContracts.maci.getStateRootOnIndexedSignUp(_index); + publicInputs[5] = pollId; } /// @inheritdoc IPoll diff --git a/packages/contracts/contracts/PollFactory.sol b/packages/contracts/contracts/PollFactory.sol index 903a5e211..e2ce4d27a 100644 --- a/packages/contracts/contracts/PollFactory.sol +++ b/packages/contracts/contracts/PollFactory.sol @@ -21,7 +21,8 @@ contract PollFactory is Params, DomainObjs, IPollFactory { uint8 _messageBatchSize, DomainObjs.PubKey calldata _coordinatorPubKey, Params.ExtContracts calldata _extContracts, - uint256 _emptyBallotRoot + uint256 _emptyBallotRoot, + uint256 _pollId ) public virtual returns (address pollAddr) { // deploy the poll Poll poll = new Poll( @@ -30,7 +31,8 @@ contract PollFactory is Params, DomainObjs, IPollFactory { _messageBatchSize, _coordinatorPubKey, _extContracts, - _emptyBallotRoot + _emptyBallotRoot, + _pollId ); // init Poll diff --git a/packages/contracts/contracts/interfaces/IPollFactory.sol b/packages/contracts/contracts/interfaces/IPollFactory.sol index 8e2fce385..c52a8f164 100644 --- a/packages/contracts/contracts/interfaces/IPollFactory.sol +++ b/packages/contracts/contracts/interfaces/IPollFactory.sol @@ -14,6 +14,7 @@ interface IPollFactory { /// @param _coordinatorPubKey The coordinator's public key /// @param _extContracts The external contracts interface references /// @param _emptyBallotRoot The root of the empty ballot tree + /// @param _pollId The poll id /// @return The deployed Poll contract function deploy( uint256 _duration, @@ -21,6 +22,7 @@ interface IPollFactory { uint8 _messageBatchSize, DomainObjs.PubKey calldata _coordinatorPubKey, Params.ExtContracts calldata _extContracts, - uint256 _emptyBallotRoot + uint256 _emptyBallotRoot, + uint256 _pollId ) external returns (address); } diff --git a/packages/contracts/tests/PollFactory.test.ts b/packages/contracts/tests/PollFactory.test.ts index 55c358681..e7d08468b 100644 --- a/packages/contracts/tests/PollFactory.test.ts +++ b/packages/contracts/tests/PollFactory.test.ts @@ -52,6 +52,7 @@ describe("pollFactory", () => { coordinatorPubKey.asContractParam(), extContracts, emptyBallotRoot, + 0n, ); const receipt = await tx.wait(); expect(receipt?.status).to.eq(1); diff --git a/packages/core/ts/Poll.ts b/packages/core/ts/Poll.ts index 151141042..e1292e003 100644 --- a/packages/core/ts/Poll.ts +++ b/packages/core/ts/Poll.ts @@ -142,6 +142,7 @@ export class Poll implements IPoll { * @param treeDepths - The depths of the trees used in the poll. * @param batchSizes - The sizes of the batches used in the poll. * @param maciStateRef - The reference to the MACI state. + * @param pollId - The poll id */ constructor( pollEndTimestamp: bigint, @@ -471,7 +472,7 @@ export class Poll implements IPoll { // Create nullifier from private key const inputNullifier = BigInt(maciPrivKey.asCircuitInputs()); - const nullifier = poseidon([inputNullifier]); + const nullifier = poseidon([inputNullifier, this.pollId]); // Get pll state tree's root const stateRoot = this.stateTree!.root; @@ -490,6 +491,7 @@ export class Poll implements IPoll { credits, stateRoot, actualStateTreeDepth, + pollId: this.pollId, }; return stringifyBigInts(circuitInputs) as unknown as IPollJoiningCircuitInputs; diff --git a/packages/core/ts/utils/types.ts b/packages/core/ts/utils/types.ts index d1b9a7970..d2a7e7a58 100644 --- a/packages/core/ts/utils/types.ts +++ b/packages/core/ts/utils/types.ts @@ -156,6 +156,7 @@ export interface IPollJoiningCircuitInputs { credits: string; stateRoot: string; actualStateTreeDepth: string; + pollId: string; } /** * An interface describing the circuit inputs to the ProcessMessage circuit