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:

  1. Set the required deposit amount.
  2. 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.

Actions Status

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: './'
PropertyDescription
merkleTreeSpecify the size of the merkle tree for managing the history of deposits. The size of the tree is 2**N.
denominationThe total amount of tokens needed for the deposit() function call.
recipientsAn array of ethereum addresses to be candidates for the ballot.
zeroValueZero 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:

  1. Hasher
  2. MerkleTree
  3. Vote

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 namezk-SNARK input typeDescription
nullifierPublicA random 𝑘 value such that 𝑘 ∈ ð”đ248.
secretPublicA 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 namezk-SNARK input typeDescription
commitmentPublicA value ðķ such that ðķ = 𝘏1(𝑘âˆĨ𝑟).
nullifierHashPublicA 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 namezk-SNARK input typeDescription
leafPublicAn output of Pedersen hash function ðķ such that ðķ = 𝘏1(𝑘âˆĨ𝑟)
path_elements[levels]PublicPath elements to prove the existence of the current leaf represented by𝑂(𝜏, 𝜄).
path_index[levels]PublicA path index 𝜄 from the merkle tree.

Outputs

Pseudocode namezk-SNARK input typeDescription
rootPublicA value of current merkle root 𝑅.

Vote

Inputs

Pseudocode namezk-SNARK input typeDescription
rootPublicA merkle root of the tree.
nullifierHashPublicA value ℎ output of ℎ = 𝘏1(𝑘).
nullifierPrivateA private known 𝑘 value at the time of deposit.
secretPrivateA private known 𝑟 value at the time of deposit.
path_elements[levels]PrivatePrivate path elements to prove the existence of the current leaf represented by 𝑂(𝜏, 𝜄) at the time of deposit.
path_index[levels]PrivateA private path index 𝜄 from merkle tree at the time of deposit.
recipientPublicA recipient ethereum address.

Outputs

Pseudocode namezk-SNARK input typeDescription
new_rootPublicAn 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

ArgumentDescription
_verifierVerifier contract address. This contract can be updated later with the updateVerifer(address) method.
_signUpTokenSignUpToken contract address of the token to be used for voting.
_denominationThe total amount of tokens needed for the deposit() function call.
_merkleTreeHeightSpecify the size of the merkle tree for managing the history of deposits. The size of the tree is 2**N.
_recipientsAn 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

ArgumentDescription
_commitmentThe 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

ArgumentDescription
_proofA zk-SNARK proof.
_rootThe value of the root hash of the Merkle Tree.
_nullifierHashThe value of X of BabyJubJub (see here) from the return value of nullifier = 𝑘 passed to pedersenHash().
_recipientCandidate's Ethereum address. Passing an address other than the one specified in the constructor call will cause the transaction to be reverted.
_relayerRelayer address.
_feeRelayer 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 => {}