noir by example

zk voting og source

the classic “hello world” case-study for noir

say a group is coordinating on some votes, and they want to keep their individual choices secret during the process

the voter set is represented by a merkle tree, where each leaf is a “commitment” of a voter’s H(private key + some group secret)

a smol helper can generate the tree (assuming private keys 0, 1, 2, 3 and a secret 9)

use dep::std;

fn test_build_merkle_tree() {
    let secret = 9;
    let commitment_0 =  std::hash::pedersen([0, secret])[0];
    let commitment_1 =  std::hash::pedersen([1, secret])[0];
    let commitment_2 =  std::hash::pedersen([2, secret])[0];
    let commitment_3 =  std::hash::pedersen([3, secret])[0];

    let left_branch = std::hash::pedersen([commitment_0, commitment_1])[0];
    let right_branch = std::hash::pedersen([commitment_2, commitment_3])[0];

    let root = std::hash::pedersen([left_branch, right_branch])[0];

    std::println("Merkle Tree:");
    std::println([left_branch, right_branch]);
    std::println([commitment_0, commitment_1, commitment_2, commitment_3]);

yields these branches, where only the root 0x20c77... is publicly known


in order to cast a vote from an anonymized account, the voter will need to prove their membership in this set, without revealing which member they are

phrased differently, when casting a vote from some random account, they need to prove knowledge of:

fn main(
    root: pub Field,         // the "group id" for the voters
    proposal_id: pub Field,  // the individual topic being voted on
    vote: pub u8,            // yes (1) / no (0)
    index: Field,
    hash_path: [Field; 2],
    secret: Field,
    priv_key: Field,
) -> pub Field {

    // prove knowledge of some private key & group secret
    let note_commitment = std::hash::pedersen([priv_key, secret])[0];

    // ensure the voter is part of the group they claim to be voting in
    let computed_root = std::merkle::compute_merkle_root(note_commitment, index, hash_path);
    assert(root == computed_root);

    // ensure the vote is valid (only 0 or 1)
    assert(vote <= 1);

    // publicly return a hash making sure the same private key won't
    // vote multiple times for the same proposal, in the same group
    let nullifier = std::hash::pedersen([root, priv_key, proposal_id]);

the functionality can be exercised with a simplified test:

fn test_main() {
    // prove a vote for the 0th voter
    let group_root = 0x20c77d6d51119d86868b3a37a64cd4510abd7bdb7f62a9e78e51fe8ca615a194;
    let hash_path = [
    let private_key = 0;
    let group_secret = 9;

    let want_nullifier = 0x05c982d312d3c204160b6a692fb7b6e129235599f6c6e4077e794fe1cd7e7a63;
    let got_nullifier = main(
    assert(want_nullifier == got_nullifier);

the rest of the application logic would live in a more traditional contract (EVM or otherwise) – keeping track of groups, their merkle roots, active votes, and used nullifiers

to see this example integrated in a foundry project, check out the official noir-starter

try it out install the noir toolchain if you haven't yet:
        curl -L | bash
        noirup --nightly
then checkout and run the example:
        git clone
        cd noir-by-example/circuits/gadgets/zk-voting
        nargo test --show-output


next (zk age verification)