# ZK Anonymous Voting Protocol with Circom

By [NenaRapsody](https://paragraph.com/@nenarapsody) · 2024-12-02

---

TLDR - Guide content:
---------------------

Step-by-step guide/tutorial for an **anonymous voting protocol** based on Circom language.

> ⚠️ **DISCLAIMER** ⚠️
> 
> This is “first tutorial attempt”. Remember first time on a bike? _Exactly_.
> 
> The guide is **incomplete** → it **has all the code**, but execution is _on hold_ because the Poseidon hash generated in Circom _doesn’t match_ the one calculated using the JavaScript libraries
> 
> **I’m digging into it and investigating to figure out the issue—stay tuned!**

### → For a silly overview of this guide, go to **Context**. To jump straight to the implementation, skip ahead to the **Step-by-Step guide - The Why.**

Context
-------

_“Build a portfolio”_ - they said.

It’s clear in the web3 space, especially if you are a developer, or a tech person, you will probably end up hearing those words, and needing a portfolio. A visual proof of work.

Being and working in the crypto means having a portfolio, a GitHub repo for developers. Now, I’m one of those engineers who just doesn’t have the time. My extra time is reserved for **offline activities**. And if you don’t exactly have the best storytelling skills and your written output isn’t all that impressive either—jackpot! I also barely tweet. And when I do tweet, not counting photos of sunsets or food, it’s probably just to appease the “build a portfolio” mantra and scrape by with a passing grade.

Just acknowledging someone physically attends an hackathon feels like a portfolio itself. I mean, between spending a 4am Berlin Friday night deploying a smart contract at **ETHBerlin** or going to a party — are those two things even comparable? Exactly. Just participating is portfolio-worthy. The proof-of-work-est pow thing ever. But reality check says it’s a **no**. Gotta need the material proof.

Also engineers are engineers for a reason. I guess the engineer-to-storyteller connection is not bidirectional. Sometimes A implies B but B doesn’t imply A. That said, I’m a huge fan of optimisation and automation, and since not being at **Berghain** at 4 a.m. on a Friday night is not enough, here a gamified way for the proof of portfolio. A combination of mirror.xyz master, as a pro DevRel, the line by line documentation as the most skilled and meticulous developer and the vain satisfaction and pleasure of developing a ZK guide with a Hello Kitty cover image and _every_ single implementation step included. _Build a portfolio they said._

This is a kind invite to join this “engineer-to-storyteller” bridge attempt. **Welcome**.

Step-by-step guide - The Why
----------------------------

This guide is born from my interest in **privacy-preserving tools** applied to potential mundane use cases (legal contracts, voting, official identity documentation) and revisiting decentralization concepts through the lens of a non-crypto person. One of those which might be useful in the future, is using a mathematical **zero knowledge proof** to verify vote (whatever type of vote, politics, initiatives, events, etc).

While researching [Noir](https://aztec.network/noir), a general purpose programming language for privacy-preserving ZK programs, I found this [Circom Multiplexer](https://github.com/iden3/circomlib/blob/master/circuits/mux1.circom). The maxi of automation I am asked [Claude AI](https://claude.ai/) (Remember the maxi of automation) to tailor the Mux code and sketch the initial draft of this privacy-preserving voting system.

The general explanation:

![](https://storage.googleapis.com/papyrus_images/caa13a72165d9ff9f03b33f886c05529e46b0f4e96daadad5584084e02ab947e.png)

The context explanation:

![](https://storage.googleapis.com/papyrus_images/be4ffe2170f27efadd5e17940ecc98267db5c04df0b39843d56f53f0fb2b1cef.png)

The voting Circom code:

    pragma circom 2.0.0;
    
    include "node_modules/circomlib/circuits/poseidon.circom";
    include "node_modules/circomlib/circuits/comparators.circom";
    
    template PrivateVoteSelection(candidates) {
        // Private inputs 
        signal input voter_secret;        // Voter's private key
        signal input vote_commitment;     // Cryptographic commitment of vote
        signal input selected_candidate_index;  // Index of selected candidate
    
        // Public inputs
        signal input candidate_list[candidates];  // List of candidates
        signal output vote_valid;                 // Validation output
    
        // Commitment verification component
        component hasher = Poseidon(2);  // Cryptographic hash function
    
        // Candidate index validation
        component index_check = LessThan(candidates);
    
        // Commitment generation
        hasher.inputs[0] <== voter_secret;
        hasher.inputs[1] <== selected_candidate_index;
    
        // Verify commitment matches generated hash
        vote_commitment === hasher.out;
    
        // Validate candidate index is within range
        index_check.in[0] <== selected_candidate_index;
        index_check.in[1] <== candidates;
        index_check.out === 1;
    
        // Ensure single candidate selection
        signal one_hot[candidates]; // Predeclare the array of signals
        signal temp_sum[candidates]; // Temporary signal array for sum
        signal one_hot_sum;
    
        // Initialize signals
        for (var i = 0; i < candidates; i++) {
            one_hot[i] <== 0; // Explicitly initialize each signal
    
            // Enforce (selected_candidate_index - i) * one_hot[i] = 0
            (selected_candidate_index - i) * one_hot[i] === 0;
    
            // Enforce one_hot[i] is binary (0 or 1)
            one_hot[i] * (one_hot[i] - 1) === 0;
        }
    
        // Compute the cumulative sum
        temp_sum[0] <== one_hot[0];
        for (var i = 1; i < candidates; i++) {
            temp_sum[i] <== temp_sum[i - 1] + one_hot[i];
        }
    
        // Assign the final sum
        one_hot_sum <== temp_sum[candidates - 1];
    
        // Ensure exactly one candidate is selected
        one_hot_sum === 1;
    
        // Output validation flag
        vote_valid <== 1;
    }
    
    
    // Example usage template
    template VotingProtocol() {
        var NUM_CANDIDATES = 3;
        signal input candidates[NUM_CANDIDATES];
        signal input voter_secret;
        signal input vote_commitment;
        signal input selected_candidate_index;
    
        component voteCircuit = PrivateVoteSelection(NUM_CANDIDATES);
        
        // Wire up inputs
        for (var i = 0; i < NUM_CANDIDATES; i++) {
            voteCircuit.candidate_list[i] <== candidates[i];
        }
        voteCircuit.voter_secret <== voter_secret;
        voteCircuit.vote_commitment <== vote_commitment;
        voteCircuit.selected_candidate_index <== selected_candidate_index;
    }
    
    component main = VotingProtocol();
    

Intro part is over, let’s start with the real implementation. (I’d 100% rather run a thousand Tau ceremonies than write intro—also I won’t even pretend I’m okay with it)

Step-by-step guide - Installation
---------------------------------

### Required Tools

1.  **Circom Compiler**
    
2.  **Snarkjs** (JavaScript library for zk-SNARKs)
    
3.  **Node.js**
    
4.  **Trusted Setup Tools**
    

### 1\. Circom Compiler

For the Circom compiler, Claude provided a deprecated version with _npm_. I then followed the official [Circom guide](https://docs.circom.io/getting-started/installation/#installing-dependencies):

*   I create main folder for the whole project: `mkdir voting-circuit-test`
    
*   Locate myself in that directory: `cd voting-circuit-test`
    

![](https://storage.googleapis.com/papyrus_images/ac15ed078412f56e604d074c8b7798f44ce88433cbf60672232e97c604a532ab.png)

*   Run the first command from the official guide: `curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh`
    
*   Install circom: `git clone https://github.com/iden3/circom.git`
    
    *   This command created a subdirectory called 'circom' inside the main folder.
        

![](https://storage.googleapis.com/papyrus_images/e5998704e6ab5cafb18bc25efedecb151ed172e714b9d99326c6f7d5af9aba0a.png)

*   Enter circom directory: `cd circom`
    
*   Execute `cargo build --release`
    
    *   This will generate the folder “_circom_” in the directory `target/release`
        

![](https://storage.googleapis.com/papyrus_images/a1b21a00e31e7f8a79d4851f0e05def1994f30b85ec26c8174343380fe357d26.png)

*   At this point you might encounter this terminal error “_zsh: command not found: cargo_”, as it happened to me. I solved it by following this suggestion:
    
    *   If for any reason you still encounter '_zsh: command not found: cargo_,' it's probably a path issue. I recommend checking on developer platforms (Stackoverflow, etc) or asking an AI
        

![](https://storage.googleapis.com/papyrus_images/3115baf2f7b4b2062c5513b1788400779ce0ec12ca7ab850c13ce0caa75f4f74.png)

*   Install this binary with command `cargo install --path circom` , it will install the circom binary in the directory _$HOME/.cargo/bin_
    
*   Install snarkjs with `npm install -g snarkjs`
    
    *   This command requires “_npm_” and _Node.js_ installed. If you don’t have, Node.js website has a handy guide in their [download section](https://nodejs.org/en/download/package-manager).
        

Time to test the voting system!

Step-by-step guide - Testing
----------------------------

*   In the main folder _voting-circuit-test_, I saved the voting code in a file called _private\_vote\_selection.circom_, feel free to adopt your preferred naming convention
    

![](https://storage.googleapis.com/papyrus_images/5d041610bbc78db8fd2018dddfef1de842ea1acf0af1fbd949c2cac9a318240a.png)

*   Compile `circom private_vote_selection.circom --r1cs --wasm --sym`
    
    *   Generated file _private\_vote\_selection.r1cs_, _private\_vote\_selection.sym_ and a folder _private\_vote\_selection\_js_ with file _private\_vote\_selection.wasm_
        

![](https://storage.googleapis.com/papyrus_images/70bea847989cc608b5ab9d640f582fe035c47a9b43b4b9514829434f02d4db14.png)

*   I started the power of tau ceremony: `snarkjs powersoftau new bn128 12 pot12_0000.ptau -v` and generated _pot12\_0000.ptau_
    

![](https://storage.googleapis.com/papyrus_images/46a4563b43c38c2c00939389f5a3c85c2ecc982d6d44ca1553336dcb4bdcdcd1.png)

![](https://storage.googleapis.com/papyrus_images/a10e5d5100169833bf17297622f54e8bfa5e6c56d30e6da15388e0e18818c113.png)

*   Power of tau ceremony part 2: `snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v`.
    
*   This command will require a random text for entropy, I choose “_sea_”. This generates file `pot12_0001.ptau`
    
*   Setup phase of power of tau ceremony: `snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v`.
    
    This creates file `pot12_final.ptau` for Groth16 circuit setup
    
*   Generate the proving and verification keys using the finalised `pot12_final.ptau` file: `snarkjs groth16 setup private_vote_selection.r1cs pot12_final.ptau circuit_0000.zkey`
    
*   I added randomness at the initial .zkey file `circuit_0000.zkey` with this command: `snarkjs zkey contribute circuit_0000.zkey circuit_final.zkey --name="Second contribution" -v` , which asked another random text. I typed “_sea2_”. This generates file `circuit_final.zkey`
    
*   After finalizing the zkey file, extract the verification key required for verifying proofs by creating `verification_key.json` with this command: `snarkjs zkey export verificationkey circuit_final.zkey verification_key.json`
    

### **Time to generate a proof for testing!**

Now that it’s all set, a proof has to be generated to test the circuit, as explained in [computing-the-witness section of Circom docs](https://docs.circom.io/getting-started/computing-the-witness/#what-is-a-witness).

*   Prepare an input file (e.g., _input.json_) as unit test with the inputs required by your circuit. The format of input.json should match the signals defined in your initial _private\_vote\_selection.circom_ file.
    

Based on our voting circom code we defined at the beginning, the circuite had these four requirements:

![](https://storage.googleapis.com/papyrus_images/88f070e696eca82c7dd2db351f89edf0093ad729c26dc09afa3eb1c634411386.png)

and it’s expecting a match between _the voter\_secret - selected\_candidate\_index_ Poseidon hash and the _vote\_commitment:_

![](https://storage.googleapis.com/papyrus_images/64d90075cc7506ee289616dbfccc2a8efb30b150bc0bc1dc9e2953b25f5b61ca.png)

*   To define the _vote\_commitment_, we need to calculate the _Poseidon hash_ of the two input signals. Both _vote\_signers_ and _select\_candidate\_index_ were picked randomly. Hash them through website [poseidon-hash.online](https://poseidon-hash.online/).
    
    *   `vote_signers = 12345`
        
    *   `select_candidates_index = 1`
        

![](https://storage.googleapis.com/papyrus_images/407fa501217818ec389c468b37c64874b31c8997d551e35f522f6bc0cef9717a.png)

*   Combine all these data into a _input.json_ file. This is how it should look like:
    
        {
          "candidates": [111, 222, 333],
          "voter_secret": 12345,
          "vote_commitment": 4213355460611018654523924795294902999663126022355729006200928612083214729114,
          "selected_candidate_index": 1
        }
        
    
    We now have everything to proceed with the witness.
    
*   Use circuit's WASM to generate witness:
    
    `node private_vote_selection_js/generate_witness.js private_vote_selection_js/private_vote_selection.wasm input.json witness.wtns`
    
*   After a few attempts of _witness calculation_, the hash generated by Circom doesn’t match the one from the online generator. This hash inconsistency raises this assert error:
    

![Hash inconsistency error](https://storage.googleapis.com/papyrus_images/4858cdd12e5a3df6ea36617ab4a91fa9fed716bce830dfefff77e54e2a109386.png)

Hash inconsistency error

**Investigating Potential Causes of hash mismatch**

*   To troubleshoot the mismatches, I explored the solutions raised from main developer platforms:
    
    1.  **Endianess Handling**: This involves how the circuit expects inputs—either little-endian or big-endian.
        
        1.  I systematically tested every possible combination: converting signals from big-endian to little-endian (and vice versa), before and after hashing, as well as transforming the output itself. Despite thorough testing, none of these combinations has proven to be the correct solution so far.
            
    2.  **Computing the Witness with C++**: Instead of Node.js, using C++ as an alternative approach.
        
    
    Further investigation is needed to pinpoint the exact cause.
    
    This guide, more than a ready-to-use tutorial, aims to offer fresh insights and support the advancement of research around zero-knowledge proofs, with the hope of fostering collaboration on novel perspectives and facilitating dynamic exchanges of ideas 💡
    
*   **For now this step-by-step walkthrough is paused here. Investigation still going, as soon as the issue will be fixed, guide will be further developed. Stay tuned!**

---

*Originally published on [NenaRapsody](https://paragraph.com/@nenarapsody/zk-anonymous-voting-protocol-with-circom)*
