About
zkC.R.E.A.M stands for Confidential Reliable Ethereum Anonymous Mixer. It is a protocol being developed as part of a pilot program to develop a more robust, accessible, secure, anonymous and verifiable voting technology for elections and other situations that require voting.
The protocol consists of a smart contract and a zero-knowledge component. The zkC.R.E.A.M smart contract handles the validation of voting status, permissions and proofs on-chain. The zero-knowledge component works off-chain, allowing the user to generate proofs, and if these proofs are valid, the smart contract can update the state.
System design
zkC.R.E.A.M aims to be simple in design, with the basic functions being "deposit" which means reception of the token used in the vote, and "drawer" which means casting the vote.
To ensure the secrecy of the vote, the voter generates a random value on their device when they make a deposit. Anyone who knows this random value can withdraw it, making the source of the token (i.e. the address where the deposit was made) secret.
Basic features
Here's what you'll need to do to set it up in a nutshell:
- Set the required deposit amount.
- Set the recipients' (candidates') ethereum addresses.
All other election-specific parameters are automatically passed to the constructor at contract deployment time. You can check out the configuration in the source code here.
Quick start
This page is basically ported from README.md.
Requirement
node
>=v11.x
Setup
Config file
Check out the packages/config/test.yml file for how to configure the settings:
cream:
merkleTrees: 4
zeroValue: "2558267815324835836571784235309882327407732303445109280607932348234378166811"
maci:
initialVoiceCreditBalance: 100
signUpDurationInSeconds: 3600 # 1 hour
votingDurationInSeconds: 3600 # 1 hour
coordinatorPrivKey: "2222222222263902553431241761119057960280734584214105336279476766401963593688"
tallyBatchsize: 4
messageBatchSize: 4
quadVoteTallyBatchSize: 4
voteOptionsMaxLeafIndex: 3
merkleTrees:
stateTreeDepth: 4
messageTreeDepth: 4
voteOptionTreeDepth: 2
chain:
privateKeysPath: './'
Circuit
Make sure you set the same value of merkleTrees depth on both config/test.yml and circuits/circom.circom.
After you finish setting the configuration, you can run:
$ yarn && \
$ yarn build
$ cd packages/contracts && npm run ganache
# In up another terminal
$ cd packages/contracts && npm run migrate
Test
# after finished setting:
$ yarn test
Usage
Simple anonymous voting
Let's take a simple vote as an example.
Configurations
The following are the minimum settings needed to deploy the contract and use zkC.R.E.A.M.
cream:
merkleTrees: 4
recipients: [
"0x65A5B0f4eD2170Abe0158865E04C4FF24827c529",
"0x9cc9C78eDA7c7940f968eF9D8A90653C47CD2a5e",
"0xb97796F8497bb84C63e650E9527Be587F18c09f8"
]
zeroValue: "2558267815324835836571784235309882327407732303445109280607932348234378166811"
maci:
initialVoiceCreditBalance: 100
signUpDurationInSeconds: 3600 # 1 hour
votingDurationInSeconds: 3600 # 1 hour
coordinatorPrivKey: "2222222222263902553431241761119057960280734584214105336279476766401963593688"
tallyBatchsize: 4
messageBatchSize: 4
quadVoteTallyBatchSize: 4
voteOptionsMaxLeafIndex: 3
merkleTrees:
stateTreeDepth: 4
messageTreeDepth: 4
voteOptionTreeDepth: 2
chain:
privateKeysPath: './'
Property | Description |
---|---|
merkleTree | Specify the size of the merkle tree for managing the history of deposits. The size of the tree is 2**N . |
denomination | The total amount of tokens needed for the deposit() function call. |
recipients | An array of ethereum addresses to be candidates for the ballot. |
zeroValue | Zero value which defined at Cream.sol . It is pre-calculated as uint256(keccak256(abi.encodePacked('cream'))) % FIELD_SIZE . |
Deposit
The implementation of deposits is described in detail in the #Deposit section of the Contract API.
Withdraw
The implementation of withdrawal is described in detail in the #Withdraw section of the Contract API.
Circuits
zkC.R.E.A.M. uses four different circuits, and the detailed APIs of each circuit are described in the links below:
Hasher circuit
The hasher circuit computes the Pedersen hash value of the given inputs.
Inputs
Both nullifier
and secret
values are generated when a user deposits a token into a voting contract.
Pseudocode name | zk-SNARK input type | Description |
---|---|---|
nullifier | Public | A random ð value such that ð â ðđ248 . |
secret | Public | A random ð value generated such that ð â ðđ248 . |
Outputs
The outputs of the Hasher circuit are computed outputs of hash function ð1
such that ð1:ðđ â âĪp
.
Let ð1
be the Pedersen hash function which is imported from circomlib
library here.
Pesudocode name | zk-SNARK input type | Description |
---|---|---|
commitment | Public | A value ðķ such that ðķ = ð1(ðâĨð) . |
nullifierHash | Public | A value â output of â = ð1(ð) . |
Merkle Tree circuit
Inputs
Let ð(ð, ð)
be the path of the merkle tree ð
represented by the root hash ð
with the index ð
.
Pseudocode name | zk-SNARK input type | Description |
---|---|---|
leaf | Public | An output of Pedersen hash function ðķ such that ðķ = ð1(ðâĨð) |
path_elements[levels] | Public | Path elements to prove the existence of the current leaf represented byð(ð, ð) . |
path_index[levels] | Public | A path index ð from the merkle tree. |
Outputs
Pseudocode name | zk-SNARK input type | Description |
---|---|---|
root | Public | A value of current merkle root ð
. |
Vote
Inputs
Pseudocode name | zk-SNARK input type | Description |
---|---|---|
root | Public | A merkle root of the tree. |
nullifierHash | Public | A value â output of â = ð1(ð) . |
nullifier | Private | A private known ð value at the time of deposit. |
secret | Private | A private known ð value at the time of deposit. |
path_elements[levels] | Private | Private path elements to prove the existence of the current leaf represented by ð(ð, ð) at the time of deposit. |
path_index[levels] | Private | A private path index ð from merkle tree at the time of deposit. |
recipient | Public | A recipient ethereum address. |
Outputs
Pseudocode name | zk-SNARK input type | Description |
---|---|---|
new_root | Public | An updated merkle root of the tree. |
Contract API
You can always get the latest version of the zkC.R.E.A.M. contract source code here.
Constructor
When you deploy the contract, you need to pass the following elements as arguments.
constructor(
IVerifier _verifier,
SignUpToken _signUpToken,
uint256 _denomination,
uint32 _merkleTreeHeight,
address[] memory _recipients
)
Argument details
Argument | Description |
---|---|
_verifier | Verifier contract address. This contract can be updated later with the updateVerifer(address) method. |
_signUpToken | SignUpToken contract address of the token to be used for voting. |
_denomination | The total amount of tokens needed for the deposit() function call. |
_merkleTreeHeight | Specify the size of the merkle tree for managing the history of deposits. The size of the tree is 2**N . |
_recipients | An array of ethereum addresses to be candidates for the ballot. These arrays are passed as an argument to the method setRecipients() when the contract is deployed, and cannot be changed after. |
Deposit
The call to the deposit function is a very simple process of passing a locally generated value, in this case _commitment
, but you should not forget to send the value of _denomination
and have a token from the _signUpToken
token contract.
function deposit (
bytes32 _commitment
)
Argument details
Argument | Description |
---|---|
_commitment | The value of a client-generated commitment. The function pedersenHash(nullifier, secret) is used to generate the value of this commitment. |
Usage
const instance = await Cream.deployed()
const tokenContract = await SignUpToken.deployed()
// setApprovalforall for token transfer
await tokenContract.giveToken(voter)
await tokenContract.setApprovalForAll(instance.address, true, { from: voter })
// deposit
const deposit = createDeposit(rbigInt(31), rbigInt(31))
const tx = await instance.deposit(toHex(deposit.commitment), { from: voter })
truffleAssert.eventEmitted(tx, 'Deposit')
Withdraw
function withdraw (
bytes calldata _proof,
bytes32 _root,
bytes32 _nullifierHash,
address payable _recipient,
address payable _relayer,
uint256 _fee
)
Argument details
Argument | Description |
---|---|
_proof | A zk-SNARK proof. |
_root | The value of the root hash of the Merkle Tree. |
_nullifierHash | The value of X of BabyJubJub (see here) from the return value of nullifier = ð passed to pedersenHash() . |
_recipient | Candidate's Ethereum address. Passing an address other than the one specified in the constructor call will cause the transaction to be reverted. |
_relayer | Relayer address. |
_fee | Relayer fee. |
Usage
// Deposit
const deposit = createDeposit(rbigInt(31), rbigInt(31))
tree.insert(deposit.commitment)
await instance.deposit(toHex(deposit.commitment), { from: voter })
// Update tree
const root = tree.root
const merkleProof = tree.getPathUpdate(0)
// Create an input
const input = {
root,
nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)).babyJubX,
relayer: relayer,
recipient,
fee,
nullifier: deposit.nullifier,
secret: deposit.secret,
path_elements: merkleProof[0],
path_index: merkleProof[1]
}
let isSpent = await instance.isSpent(toHex(input.nullifierHash))
assert.isFalse(isSpent)
const {
proof,
} = await genProofAndPublicSignals(
input,
'prod/vote.circom',
'build/vote.zkey',
'circuits/vote.wasm'
)
const args = [
toHex(input.root),
toHex(input.nullifierHash),
toHex(input.recipient, 20),
toHex(input.relayer, 20),
toHex(input.fee)
]
const proofForSolidityInput = toSolidityInput(proof)
// Create withdraw tx
const tx = await instance.withdraw(proofForSolidityInput, ...args, { from: relayer })
truffleAssert.eventEmitted(tx, 'Withdrawal')
libcream
Types
SnarkBigint
A big integer type compatible with the cream-merkle-tree
library. This type is ported from snarkjs =< 0.1.20
library.
Interfaces
PedersenHash
Encapsulates the outputs of the pedersenHash()
function.
interface PedersenHash {
babyJubX: SnarkBigInt,
babyJubY: SnarkBigInt
}
Deposit
Encapsulates some of the information essential to create a Snark proof. It provides an output interface to the createDeposit()
method.
interface Deposit {
nullifier: SnarkBigInt,
secret: SnarkBigInt,
preimage: SnarkBigInt,
commitment: SnarkBigInt,
nullifierHash: SnarkBigInt
}
Functions
pedersenHash
Function to hash the given value and return a value of the type of the PedersenHash
interface.
const pedersenHash (
value: SnarkBigInt
): PedersenHash => {}
createDeposit
Function to return a value of the type of the Deposit
interface given 2 values, nullifier and secret.
const createDeposit (
nullifier: SnarkBigInt,
secret: SnarkBigInt
): Deposit => {}
createMessage
Function to return a value of the type of the Message
interface and PubKey
.
const createMessage(
userStateIndex: number,
userKeypair: Keypair,
newUserKeypair: Keypair | null,
coordinatorPubKey: PubKey,
voteOptionIndex: number | null,
voiceCredits: BigNumber | null,
nonce: number,
_salt?: BigInt
): [Message, PubKey] => {}
generateDeposit
Generates a type of the Deposit interface from a given string.
const generateDeposit (
note: string
): Deposit => {}
toHex
Returns a string in hexadecimal format from the passed values and optionally specifies the length of the string (default length = 32
).
const toHex (
n: SnarkBigInt,
length = 32
): string => {}
rbigInt
Function to return a random integer of type SnarkBigInt
for a given number of bytes.
const rbigInt = (
nbytes: number
): SnarkBigInt => {}