MACI Primitives
MACI primitives
This section provides a short introduction to the main primitives used by MACI.
Elliptic Curves
MACI uses the Baby Jubjub Elliptic Curve. The p
scalar field of choosing is:
with generator:
and within the finite field with modulo .
Key Pairs
MACI uses Node.js's crypto.randomBytes(32)
function to generate a cryptographically strong pseudorandom 32-byte value, as well as an algorithm to prevent modulo bias. In pseudocode this is:
lim = 2 ** 256
min = lim - p
rand = null
while true:
rand = BigInt(crypto.getRandomBytes(32))
if rand >= min:
break
privKey = rand % p
A public key is a point on the Baby Jubjub curve, which is deterministically derived from a private key s
.
Message Signing
To sign messages, MACI uses the Edwards-curve Digital Signature Algorithm (EdDSA), implemented by iden3.
Hash Functions
MACI uses the Poseidon hash function, which is proven to be very efficient in ZK applications. Poseidon accepts inputs and produces 1 output:
Also, SHA256 is used to compress public inputs to a circuit into a single field element in the finite field mod .
Message Encryption
In order to encrypt messages, MACI uses Poseidon in DuplexSponge mode. This provides an encryption function and a decryption function:
- as
In more details,
- is the shared key, a point on the Baby Jubjub curve
- is the nonce, which we hardcode to 0
- is the length of the plaintext
The implementation can be found here.
Shared Key Generation
The ECDH algorithm is used to generate a shared key, which is then used to encrypt each message. This allows to create messages which are only decryptable by the coordinator and the person sending the message.
In more details:
-
The coordinator's public key is known to all. Their private key is secret.
-
When the user publishes a message (i.e. casts a vote), they generate an ephemeral keypair with private key and public key .
-
The user generates the shared key using the coordinator's public key and the user's ephemeral private key .
-
The user encrypts the command and signature with to form a message.
-
The user sends their ephemeral public key along with the ciphertext. The coordinator can recover the same shared key using their private key and the given ephemeral public key .
Merkle Trees
Rather than using Binary merkle trees, MACI uses Quinary merkle trees (5 leaves per node). This allows for more gas efficient computation using the Poseidon hash function.
Accumulator queue
This contract holds user sign-ups and messages. When a leaf is inserted into the AccQueue
, the merkle root is not updated yet, instead the leaf is updated or the root of a subtree is re-computed. The smart contract exposes three functions:
enqueue(leaf)
: Enqueues a leaf into a subtree four out of five times it is invoked, an enqueue operation may or may not require the contract to perform a hash function. When it does, only up to required number of hashes need to be computedmergeSubRoots()
: Merge all subtree roots into the shortest possible Merkle tree to fit Before computing the main Merkle root, it is necessary to compute the smallSRTroot (the smallest subroot tree root). This is the Merkle root of a tree which is small enough to fit all the subroots function which allows the coordinator to specify the number of queue operations to execute. The entire tree may be merged in a single transaction, or it may not.merge()
: Calculate the Merkle root of all the leaves at height
Domain Objects
Verifying Keys
A verifying key is comprised of the following elements:
- , a point in the curve on which is defined
- , a point in the curve on which is defined
- , a point in the curve on which is defined
- , a point in the curve on which is defined
- , a list of points in the curve on which is defined
A verifying key is used to validate a zk-SNARK proof. Each unique permutation of parameters to a particular circuit has a different verifying key.
Private Keys
MACI's private keys allow users to send and decrypt messages. This key translates to a scalar point on the Baby Jubjub elliptic curve. All keys are serialized with the prefix macisk
.
Public Keys
Public keys also translate to a point on the Baby Jubjub elliptic curve, and is derived from the private key . These are serialized with the prefix macipk
.
Key Pair
A Key Pair is a private key and its corresponding public key.
Command
A command represents an action that a user may take. Such as casting a vote in a poll or changing their public key if bribed. It is made up of the following parameters:
Symbol | Name | Size | Description |
---|---|---|---|
State index | 50 | State leaf index where the signing key is located | |
Public key x-coordinate | 253 | If no change is necessary this parameter should reflect the current public key's x-coordinate | |
Public key y-coordinate | 253 | If no change is necessary this parameter should reflect the current public key's y-coordinate | |
Vote option index | 50 | Option state leaf index of preference to assign the vote for | |
Voting weight | 50 | Voice credit balance allocation, this is an arbitrary value dependent on a user's available credits | |
Nonce | 50 | State leaf's index of actions committed plus one | |
Poll id | 50 | The poll's identifier to cast in regard to | |
Salt | 253 | An entropy value to inhibit brute force attacks |
Message
A message is an encrypted command using the shared key between the voter and the coordinator. The plaintext is computed as such:
While the message can be computed with the formula below:
=
Decrypting a message
To decrypt a message using is expressed as
=
To unpack to its original five parameters, it must be separated into 50 bit values from the parent 250 bit value. To extract 50 bits at byte , we:
- initialise 50 bits
- shift left by bits
- bitwise AND with
- shift right by bits
Ballot
A Ballot represents a particular user's votes in a poll, as well as their next valid nonce. It is akin to a voting slip, which belongs to only one voter and contains a list of their choices.
Symbol | Name | Comments |
---|---|---|
An array of vote weights | refers to the vote weights assigned to vote option | |
The current nonce | Starts from 0 and increments, so the first valid command must have nonce 1 | |
The vote option tree depth |
The hash is computed as such:
- Compute the Merkle root of , arity 5, of a tree of depth ; let this value be
- Compute
State leaf
A state leaf represents a user's participation declared through an identity (their public key) and information relevant to their ability or right to cast votes in a poll (their voice credit balance and the block timestamp at which they signed up).
We define a state leaf as the hash of the following:
Symbol | Name | Comments |
---|---|---|
Public key's x-coordinate | ||
Public key's y-coordinate | ||
Voice credit balance | ||
Block timestamp | In Unix time (seconds since Jan 1 1970 UTC) |
The hash is computed as such:
Blank state leaf
A blank state leaf has the following value:
This value is computed as such:
The code to derive and is here. The function call required is pedersenHash.getBasePoint('blake', 0)
- Hash the string
PedersenGenerator_00000000000000000000000000000000_00000000000000000000000000000000
with . In big-endian hexadecimal format, the hash should be1b3ef77ef2cd620fd2358e69dd564f35556aad552fdd7f06b777bd3a1d697160
. - Set the 255th bit to 0. The result should be
1b3ef77ef2cd620fd2358e69dd564f35556aad552fdd7f06b777bd3a1d697120
. - Use the method to convert a buffer to a point on the BabyJub curve described in [2.3.2].
- Multiply the point by 8. The result is the point with x-value and y-value
Given the elliptic curve discrete logarithm problem, we assume that no one knows the private key and by using the public key generation procedure in [1.4], we can derive and . Furthermore, the string above (PedersenGenerator...
) acts as a nothing-up-my-sleeve value.