Skip to content

Commit

Permalink
docs(documentation): add v3 docs, revert v2 docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Aleksandar Veljković committed Aug 27, 2024
1 parent f6be34a commit 28c6f64
Show file tree
Hide file tree
Showing 87 changed files with 6,818 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ sidebar_label: Key change
sidebar_position: 8
---

MACI's voters are globally identified by their MACI public key. Together with their private key, they can join live Polls. For each live Poll, the user wants to join, the user generates a new key pair. With the help of ZK proofs, this schema prevents the coordinator from creating a link between the user's public MACI key and the Poll-specific keys. With the Poll keys, the user can sign and submit messages to live Polls.
MACI's voters are identified by their MACI public key. Together with their private key, they can sign and submit messages to live Polls.

As MACI's main property is to provide collusion resistance in digital voting applications, it is important to have a mechanism for a user to change their Poll voting key, should this become compromised, or they wish to revoke past actions.
As MACI's main property is to provide collusion resistance in digital voting applications, it is important to have a mechanism for a user to change their voting key, should this become compromised, or they wish to revoke past actions.

## How MACI messages are processed

Expand All @@ -23,38 +23,31 @@ Reverse processing was introduced to prevent a type of attack where a briber wou

Let's take as an example the following:

1. Alice signs up with pub key $pub1$ and joins $Poll$ with pub key $pollPub1$
2. Bob (Briber) bribes Alice and asks her to submit a Poll key change message to $pollPub2$ (owned by Bob)
3. Bob submits a vote with $pollPub2$
4. Alice submits a vote with $pollPub1$
1. Alice signs up with pub key $pub1$
2. Bob (Briber) bribes Alice and asks her to submit a key change message to $pub2$ (owned by Bob)
3. Bob submits a vote with $pub2$
4. Alice submits a vote with $pub1$

If messages were processed in the same order as they were submitted, Alice's vote would not be valid, due to it being signed with a private key $pollPriv1$ - which now would not be valid.
If messages were processed in the same order as they were submitted, Alice's vote would not be valid, due to it being signed with a private key $priv1$ - which now would not be valid.

On the other hand, due to messages being processed in reverse order, Alice's last message would be counted as valid as the key change would have not been processed yet. Then, Bob's vote would not be counted as valid as the current key for Alice would be $pollPub1$.
On the other hand, due to messages being processed in reverse order, Alice's last message would be counted as valid as the key change would have not been processed yet. Then, Bob's vote would not be counted as valid as the current key for Alice would be $pub1$.

> Note that a key change message should have the nonce set to 1 in order for it to be valid. We'll see a code example in the next sections.
## Then how can a voter change their key and submit a new vote?

A user can submit a key change message by simply sending a new message signed with their signup key, and setting the nonce to 1. This is because the code checks that the first message to be processed has the nonce set to 1.
A user, can submit a key change message, by simply sending a new message signed with their signup key, and setting the nonce to 1. This is because the code checks that the first message to be processed has the nonce set to 1.

Let's take a look into a code example:

> We have two users, and five keypairs
> We have two users, and three keypairs
- Create five keypairs
- Create three keypairs

```ts
// Two MACI keypairs
const user1Keypair = new Keypair();
const user2Keypair = new Keypair();

// Two Poll keypairs for each user respectively
const pollUser1Keypair = new Keypair();
const pollUser2Keypair = new Keypair();

// Second Poll keypair
const secondPollKeypair = new Keypair();
const secondKeyPair = new Keypair();
```

- Votes will be
Expand All @@ -81,11 +74,10 @@ project 1 = 3 * 3 -> 9

As seen above, we expect the first vote weight 9 to not be counted, but instead the second vote weight 5 to be counted.

- Deploy a MaciState locally, sign up, and join the poll
- Deploy a MaciState locally and sign up

```ts
const maciState: MaciState = new MaciState(STATE_TREE_DEPTH);
const maciState: MaciState = new MaciState(STATE_TREE_DEPTH);
// Sign up
user1StateIndex = maciState.signUp(user1Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000)));
user2StateIndex = maciState.signUp(user2Keypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000)));
Expand All @@ -100,29 +92,20 @@ pollId = maciState.deployPoll(
);
```

- User1 and user2 join the poll and submit their first votes
- User1 and user2 submit their first votes

```ts
const poll = maciState.polls[pollId];

// Join poll
const nullifier1 = poseidon([user1Keypair.privKey.asCircuitInputs()]);
const nullifier2 = poseidon([user2Keypair.privKey.asCircuitInputs()]);

poll.joinPoll(nullifier1, pollUser1Keypair.pubKey, newVoiceCreditBalance, Date.now());
poll.joinPoll(nullifier2, pollUser2Keypair.pubKey, newVoiceCreditBalance, Date.now());

// Prepare and cast votes
const command1 = new PCommand(
BigInt(user1PollStateIndex),
pollUser1Keypair.pubKey,
BigInt(user1StateIndex),
user1Keypair.pubKey,
user1VoteOptionIndex,
user1VoteWeight,
BigInt(1),
BigInt(pollId),
);

const signature1 = command1.sign(pollUser1Keypair.privKey);
const signature1 = command1.sign(user1Keypair.privKey);

const ecdhKeypair1 = new Keypair();
const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey);
Expand All @@ -132,14 +115,14 @@ poll.publishMessage(message1, ecdhKeypair1.pubKey);

const command2 = new PCommand(
BigInt(user2StateIndex),
pollUser2Keypair.pubKey,
user2Keypair.pubKey,
user2VoteOptionIndex,
user2VoteWeight,
BigInt(1),
BigInt(pollId),
);

const signature2 = command2.sign(pollUser2Keypair.privKey);
const signature2 = command2.sign(user2Keypair.privKey);

const ecdhKeypair2 = new Keypair();
const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey);
Expand All @@ -153,15 +136,15 @@ poll.publishMessage(message2, ecdhKeypair2.pubKey);
```ts
const poll = maciState.polls[pollId];
const command = new PCommand(
BigInt(user1PollStateIndex),
secondPollKeypair.pubKey,
BigInt(user1StateIndex),
secondKeyPair.pubKey,
user1VoteOptionIndex,
user1NewVoteWeight,
BigInt(1),
BigInt(pollId),
);

const signature = command.sign(pollUser1Keypair.privKey);
const signature = command.sign(user1Keypair.privKey);

const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
Expand All @@ -184,16 +167,14 @@ expect(poll.perVOSpentVoiceCredits[1].toString()).to.eq((user2VoteWeight * user2

```ts
const poll = maciState.polls[pollId];
const pollStateLeaf1 = poll.stateLeaves[user1PollStateIndex];
const pollStateLeaf2 = poll.stateLeaves[user2PollStateIndex];
expect(pollStateLeaf1.pubKey.equals(secondPollKeypair.pubKey)).to.eq(true);
expect(pollStateLeaf2.pubKey.equals(pollUser2Keypair.pubKey)).to.eq(true);
const stateLeaf1 = poll.stateLeaves[user1StateIndex];
const stateLeaf2 = poll.stateLeaves[user2StateIndex];
expect(stateLeaf1.pubKey.equals(user1SecondKeypair.pubKey)).to.eq(true);
expect(stateLeaf2.pubKey.equals(user2Keypair.pubKey)).to.eq(true);
```

We see that is important that we set the final message (the one with the new vote) with nonce 1, as this vote would be counted as the first vote.

:::info
Tests related to key changes have been added to the [core package](https://github.com/privacy-scaling-explorations/maci/blob/dev/core/ts/__tests__/) and to the [cli package](https://github.com/privacy-scaling-explorations/maci/blob/dev/cli/tests/).
:::


Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ A command represents an action that a user may take, such as casting a vote in a

| Symbol | Name | Size | Description |
| ------------ | ----------------------- | ---- | --------------------------------------------------------------------------------------------------- |
| $cm_i$ | Poll state index | 50 | Poll state leaf index where the signing key is located |
| $cm_{p_{x}}$ | Public key x-coordinate | 253 | If no change is necessary this parameter should reflect the current poll public key's x-coordinate |
| $cm_{p_{y}}$ | Public key y-coordinate | 253 | If no change is necessary this parameter should reflect the current poll public key's y-coordinate |
| $cm_i$ | State index | 50 | State leaf index where the signing key is located |
| $cm_{p_{x}}$ | Public key x-coordinate | 253 | If no change is necessary this parameter should reflect the current public key's x-coordinate |
| $cm_{p_{y}}$ | Public key y-coordinate | 253 | If no change is necessary this parameter should reflect the current public key's y-coordinate |
| $cm_{i_{v}}$ | Vote option index | 50 | Option state leaf index of preference to assign the vote for |
| $cm_w$ | Voting weight | 50 | Voice credit balance allocation, this is an arbitrary value dependent on a user's available credits |
| $cm_n$ | Nonce | 50 | Poll state leaf's index of actions committed plus one |
| $cm_n$ | Nonce | 50 | State leaf's index of actions committed plus one |
| $cm_{id}$ | Poll id | 50 | The poll's identifier to cast in regard to |
| $cm_s$ | Salt | 253 | An entropy value to inhibit brute force attacks |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct MaxValues {
/// deployment
struct ExtContracts {
IMACI maci;
AccQueue messageAq;
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,75 +9,39 @@ This contract allows users to submit their votes.

The main functions of the contract are as follows:

- `joinPoll` - This function allows users to join the poll by adding new leaf to the poll state tree. Users submit a poll-specific public key, a nullifier derived from their MACI private key, and a ZK proof that proves correctness of the nullifier computation and inclusion of the original MACI public key in MACI state tree.
- `publishMessage` - This function allows anyone to publish a message, and it accepts the message object as well as an ephemeral public key. This key together with the coordinator public key will be used to generate a shared ECDH key that will encrypt the message.
The function will check that the voting deadline has not passed, as well as the max number of messages was not reached. If everything is correct, the message chain hash is updated as $hash(currentChainHash, newMessageHash)$. If the new message order number is greater than the batch size for message processing, the message batch chain hash is also logged.
Before saving the message, the function will check that the voting deadline has not passed, as well as the max number of messages was not reached.
- `publisMessageBatch` - This function allows to submit a batch of messages, and it accepts an array of messages with their corresponding public keys used in the encryption step. It will call the `publishMessage` function for each message in the array.

## JoinPoll

The `joinPoll` function looks as follows:

```js
function joinPoll(
uint256 _nullifier,
PubKey memory _pubKey,
uint256 _newVoiceCreditBalance,
uint256 _stateRootIndex,
uint256[8] memory _proof
) external {
// Whether the user has already joined
if (pollNullifier[_nullifier]) {
revert UserAlreadyJoined();
}

// Verify user's proof
if (!verifyPollProof(_nullifier, _newVoiceCreditBalance, _stateRootIndex, _pubKey, _proof)) {
revert InvalidPollProof();
}

// Store user in the pollStateTree
uint256 timestamp = block.timestamp;
uint256 stateLeaf = hashStateLeaf(StateLeaf(_pubKey, _newVoiceCreditBalance, timestamp));
InternalLazyIMT._insert(pollStateTree, stateLeaf);

// Set nullifier for user's private key
pollNullifier[_nullifier] = true;

uint256 pollStateIndex = pollStateTree.numberOfLeaves - 1;
emit PollJoined(_pubKey.x, _pubKey.y, _newVoiceCreditBalance, timestamp, _nullifier, pollStateIndex);
}
```

## PublishMessage

The `publishMessage` function looks as follows:

```js
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();
revert InvalidPubKey();
}

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

// compute current message hash
uint256 messageHash = hashMessageAndEncPubKey(_message, _encPubKey);

// update current message chain hash
updateChainHash(messageHash);
uint256 messageLeaf = hashMessageAndEncPubKey(_message, _encPubKey);
extContracts.messageAq.enqueue(messageLeaf);

emit PublishMessage(_message, _encPubKey);
}
}
```

## MergeMaciState

After a Poll's voting period ends, the coordinator's job is to store the main state root, as well as some more information on the Poll contract using `mergeMaciState` and, if needed, pad last message hash batch using `padLastBatch`:
After a Poll's voting period ends, the coordinator's job is to store the main state root, as well as some more information on the Poll contract using `mergeMaciState`:

```js
function mergeMaciState() public isAfterVotingDeadline {
Expand Down Expand Up @@ -114,15 +78,6 @@ function mergeMaciState() public isAfterVotingDeadline {
}
```

```js
function padLastBatch() external isAfterVotingDeadline isNotPadded {
if (numMessages % messageBatchSize != 0) {
batchHashes.push(chainHash);
}
isBatchHashesPadded = true;
}
```

The function will store the state root from the MACI contract, create a commitment by hashing this merkle root, an empty ballot root stored in the `emptyBallotRoots` mapping, and a zero as salt. The commitment will be stored in the `currentSbCommitment` variable. Finally, it will store the total number of signups, and calculate the actual depth of the state tree. This information will be used when processing messages and tally to ensure proof validity.

The `emptyBallotRoots` mapping is used to store the empty ballot roots for each vote option tree depth. For instance, if the vote option tree depth is 5, a build script will generate the following values (this assumes that the state tree depth is 10):
Expand All @@ -135,6 +90,11 @@ emptyBallotRoots[3] = uint256(49048286193070910082046722392313772904950026265341
emptyBallotRoots[4] = uint256(18694062287284245784028624966421731916526814537891066525886866373016385890569);
```

At the same time, the coordinator can merge the message accumulator queue and generate its merkle root. This is achieved by calling the following functions:

- `mergeMessageAqSubRoots` - merges the Poll's messages tree subroot
- `mergeMessageAq` - merges the Poll's messages tree

:::info
Please be advised that the number of signups in this case includes the zero leaf. For this reason, when accounting for the real users signed up to the Poll, you should subtract one from the value stored in the Poll contract.
:::
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,40 @@ sidebar_position: 3

```ts
function deploy(
uint256 _duration,
uint256 _maxVoteOptions,
_duration,
MaxValues calldata _maxValues,
TreeDepths calldata _treeDepths,
uint8 _messageBatchSize,
PubKey calldata _coordinatorPubKey,
ExtContracts calldata _extContracts
) public virtual returns (address pollAddr) {
/// @notice Validate _maxVoteOptions
address _maci
) public virtual returns (address pollAddr) {
/// @notice Validate _maxValues
/// maxVoteOptions must be less than 2 ** 50 due to circuit limitations;
/// it will be packed as a 50-bit value along with other values as one
/// of the inputs (aka packedVal)
if (_maxVoteOptions >= (2 ** 50)) {
revert InvalidMaxVoteOptions();
if (_maxValues.maxVoteOptions >= (2 ** 50)) {
revert InvalidMaxValues();
}

/// @notice deploy a new AccQueue contract to store messages
AccQueue messageAq = new AccQueueQuinaryMaci(_treeDepths.messageTreeSubDepth);

/// @notice the smart contracts that a Poll would interact with
ExtContracts memory extContracts = ExtContracts({ maci: IMACI(_maci), messageAq: messageAq });

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

// Make the Poll contract own the messageAq contract, so only it can
// run enqueue/merge
messageAq.transferOwnership(address(poll));

// init Poll
poll.init();

pollAddr = address(poll);
}
```
}
```

Upon deployment, the following will happen:

- ownership of the `messageAq` contract is transferred to the deployed poll contract
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ maci-cli <command> --help
| `genMaciKeyPair` | Generate a new MACI key pair |
| `show` | Show the deployed contract addresses |
| `publish` | Publish a new message to a MACI Poll contract |
| `mergeMessages` | Merge the message accumulator queue |
| `mergeSignups` | Merge the signups accumulator queue |
| `timeTravel` | Fast-forward the time (only works for local hardhat testing) |
| `extractVkToFile` | Extract verification keys (vKey) from zKey files |
| `signup` | Sign up to a MACI contract
| `joinPoll` | Join to a specific MACI Poll |
| `signup` | Sign up to a MACI contract |
| `isRegisteredUser` | Checks if user is registered with public key |
| `isJoinedUser` | Checks if user is joined to a poll with poll public key |
| `fundWallet` | Fund a wallet with Ether |
| `verify` | Verify the results of a poll on-chain |
| `genProofs` | Generate the proofs for a poll |
Expand Down
Loading

0 comments on commit 28c6f64

Please sign in to comment.