Private smart contrcts with Solidity & Circom
Published: 2024-06-28 20:27:46

---
title: Private smart contrcts with Solidity & Circom
published: true
description: 
tags: 
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-05-29 00:02 +0000
---

ZK allows us to create applications with private data and execution. This opens the door to many new use cases, like the one we'll create in this guide: an anonymous and secure voting system combining Circom and Solidity.

## Circom and dependencies

If you don't have Circom installed yet, install it with the following commands. I'm using node v20, but it should work with other versions as well.

```bash
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
git clone https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom
npm install -g snarkjs
```

We'll also use the Circom libraries where the Poseidon function that we'll be using is located.

```bash
git clone https://github.com/iden3/circomlib.git
```

## 1. Public key creation

The method we will use to conduct anonymous and secure voting is by proving that we are part of a group without revealing our identity. For example, I will vote for the president of Honduras, demonstrating that I am a Honduran without revealing which specific Honduran I am. This is called "proof of inclusion in a set."

The most practical way to achieve this in zk and blockchain is through Merkle trees. We will place the voters as leaves in the tree and prove that we are one of them without disclosing which one.

Since the tree is public, we will use a set of public-private key pairs so that each voter can cast their vote only once.

You might wonder if we can use the public keys from our Ethereum wallet (e.g., from MetaMask). In future guides like this one, I'll address that topic just as I did with noir. To reach that point, you'll need the fundamentals from this guide. So stay tuned and subscribe!

Now, let's create the public keys for the following private keys using the circuit privateKeyHasher.circom below:
* `111`
* `222`
* `333`
* `444`

`privateKeyHasher.circom`
```javascript
pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";

template privateKeyHasher() {
    signal input privateKey;
    signal output publicKey;
    component poseidonComponent;
    poseidonComponent = Poseidon(1);
    poseidonComponent.inputs[0] <== privateKey;
    publicKey <== poseidonComponent.out;
    log(publicKey);
}

component main = privateKeyHasher();
```

`input.json`
```json
{
    "privateKey": "111"
}
```

Compile and compute the circuit with the commands below, and you'll see the result in the terminal.

```bash
circom privateKeyHasher.circom --r1cs --wasm --sym --c
node privateKeyHasher_js/generate_witness.js privateKeyHasher_js/privateKeyHasher.wasm input.json witness.wtns
```

The result of the 4 private keys should be as follows:

| Private key | Public key |
| -------- | ------- |
| 111  | 13377623690824916797327209540443066247715962236839283896963055328700043345550    |
| 222 | 3370092681714607727019534888747304108045661953819543369463810453568040251648     |
| 333    | 19430878135540641438890585969007029177622584902384053006985767702837167003933    |
| 444    | 2288143249026782941992289125994734798520452235369536663078162770881373549221    |

Is it necessary to do this through Circom? The answer is no. By using Circom, we're performing a lot of unnecessary computation. For now, we're doing it this way to ensure that the implementation of the Poseidon hashing algorithm we'll use later is compatible. This is not recommended for production projects.

## 2. Tree Creation

Now we have the four leaves of our tree positioned as follows

```
└─ ???
   ├─ ???
   │  ├─ 13377623690824916797327209540443066247715962236839283896963055328700043345550
   │  └─ 3370092681714607727019534888747304108045661953819543369463810453568040251648
   └─ ???
      ├─ 19430878135540641438890585969007029177622584902384053006985767702837167003933
      └─ 2288143249026782941992289125994734798520452235369536663078162770881373549221
```


Next, we're going to generate the Merkle tree branch by branch. Remember that Merkle trees are generated by hashing each of their leaves and branches in pairs until reaching the root.

To generate the complete tree, we'll execute the following function that hashes two leaves to generate their root. We'll do this a total of 3 times because that's what's needed to obtain the root of a tree with 4 leaves: `root = hash(hash(A, B), hash(C, D))`.

`hashLeaves.circom`
```javascript
pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";

template hashLeaves() {
    signal input leftLeaf;
    signal input rightLeaf;
    signal output root;
    component poseidonComponent;
    poseidonComponent = Poseidon(2);
    poseidonComponent.inputs[0] <== leftLeaf;
    poseidonComponent.inputs[1] <== rightLeaf;
    root <== poseidonComponent.out;
    log(root);
}

component main = hashLeaves();
```

Here are the inputs needed to generate the first branch. Similarly, you can generate the other branch and the root.

`input.json`
```json
{
    "leftLeaf": "13377623690824916797327209540443066247715962236839283896963055328700043345550",
    "rightLeaf": "3370092681714607727019534888747304108045661953819543369463810453568040251648"
}
```

Similar to the previous step, with the following commands, the circuit will be compiled and the root will be printed given its two leaves.

```bash
circom hashLeaves.circom --r1cs --wasm --sym --c
node hashLeaves_js/generate_witness.js hashLeaves_js/hashLeaves.wasm input.json witness.wtns
```

This is how the full tree looks like:

```
└─ 172702405816516791996779728912308790882282610188111072512380034048458433129
   ├─ 8238706810845716733547504554580992539732197518335350130391048624023669338026
   │  ├─ 13377623690824916797327209540443066247715962236839283896963055328700043345550
   │  └─ 3370092681714607727019534888747304108045661953819543369463810453568040251648
   └─ 11117482755699627218224304590393929490559713427701237904426421590969988571596
      ├─ 19430878135540641438890585969007029177622584902384053006985767702837167003933
      └─ 2288143249026782941992289125994734798520452235369536663078162770881373549221
```

## 3. Generate Proof of an Anonymous Vote

To generate a vote, we need to pass the following parameters to the circuit:

* `privateKey`: The user's private key.
* `root`: The root of the tree ensures that we are operating within the correct set. Additionally, for clarity, we could add the contract and the chain where the vote will be executed. This variable will be public and accessible to the smart contract.
* `proposalId` and `vote`: The vote chosen by the user.
* `pathElements` and `pathIndices`: The minimal information needed to reconstruct the root. This includes `pathElements`, which are the leaf or branch nodes, and pathIndices, which show which path to take for hashing, where 0 represents nodes on the left and 1 represents nodes on the right.

`proveVote.circom`
```javascript
pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";

template switchPosition() {
    signal input in[2];
    signal input s;
    signal output out[2];

    s * (1 - s) === 0;
    out[0] <== (in[1] - in[0])*s + in[0];
    out[1] <== (in[0] - in[1])*s + in[1];
}

template privateKeyHasher() {
    signal input privateKey;
    signal output publicKey;
    component poseidonComponent;
    poseidonComponent = Poseidon(1);
    poseidonComponent.inputs[0] <== privateKey;
    publicKey <== poseidonComponent.out;
}

template nullifierHasher() {
    signal input root;
    signal input privateKey;
    signal input proposalId;
    signal output nullifier;
    component poseidonComponent;
    poseidonComponent = Poseidon(3);
    poseidonComponent.inputs[0] <== root;
    poseidonComponent.inputs[1] <== privateKey;
    poseidonComponent.inputs[2] <== proposalId;
    nullifier <== poseidonComponent.out;
}

template proveVote(levels) {
    signal input privateKey;
    signal input root;
    signal input proposalId;
    signal input vote;
    signal input pathElements[levels];
    signal input pathIndices[levels];
    signal output nullifier;

    signal leaf;
    component hasherComponent;
    hasherComponent = privateKeyHasher();
    hasherComponent.privateKey <== privateKey;
    leaf <== hasherComponent.publicKey;

    component selectors[levels];
    component hashers[levels];

    signal computedPath[levels];

    for (var i = 0; i < levels; i++) {
        selectors[i] = switchPosition();
        selectors[i].in[0] <== i == 0 ? leaf : computedPath[i - 1];
        selectors[i].in[1] <== pathElements[i];
        selectors[i].s <== pathIndices[i];

        hashers[i] = Poseidon(2);
        hashers[i].inputs[0] <== selectors[i].out[0];
        hashers[i].inputs[1] <== selectors[i].out[1];
        computedPath[i] <== hashers[i].out;
    }
    root === computedPath[levels - 1];

    component nullifierComponent;
    nullifierComponent = nullifierHasher();
    nullifierComponent.root <== root;
    nullifierComponent.privateKey <== privateKey;
    nullifierComponent.proposalId <== proposalId;
    nullifier <== nullifierComponent.nullifier;
}

component main {public [root, proposalId, vote]} = proveVote(2);
```

`input.json`
```json
{
    "privateKey": "111",
    "root": "172702405816516791996779728912308790882282610188111072512380034048458433129",
    "proposalId": "0",
    "vote": "1",
    "pathElements": ["3370092681714607727019534888747304108045661953819543369463810453568040251648", "11117482755699627218224304590393929490559713427701237904426421590969988571596"],
    "pathIndices": ["0","0"]
}
```

Let's test if everything works correctly:

```bash
circom proveVote.circom --r1cs --wasm --sym --c
node proveVote_js/generate_witness.js proveVote_js/proveVote.wasm input.json witness.wtns
```

If there were no issues, nothing should be printed in the terminal.

## 4. Verify an on-chain vote, from Soldity

With the following commands, we carry out the initial ceremony, also known as the trusted setup.

```
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup proveVote.r1cs pot12_final.ptau proveVote_0000.zkey
snarkjs zkey contribute proveVote_0000.zkey proveVote_0001.zkey --name="1st Contributor Name" -v
snarkjs zkey export verificationkey proveVote_0001.zkey verification_key.json
```

Next, we generate the verifier contract in Solidity.

```bash
snarkjs zkey export solidityverifier proveVote_0001.zkey verifier.sol
```

Upon executing this command, a verifier contract will be generated in the file verifier.sol. Now deploy that contract on-chain.

Next, deploy the following contract on-chain, which contains the logic for voting and proof verification. Pass the address of the verifier contract we just deployed as a parameter in the constructor.

```javascript
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

interface ICircomVerifier {
    function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals) external view returns (bool);
}

contract CircomVoter {
    ICircomVerifier circomVerifier;
    uint public publicInput;

    struct Proposal {
        string description;
        uint deadline;
        uint forVotes;
        uint againstVotes;
    }

    uint merkleRoot;
    uint proposalCount;
    mapping (uint proposalId => Proposal) public proposals;
    mapping (uint nullifier => bool isNullified) public nullifiers;

    constructor(uint _merkleRoot, address circomVeriferAddress) {
        merkleRoot = _merkleRoot;
        circomVerifier = ICircomVerifier(circomVeriferAddress);
    }

    function propose(string memory description, uint deadline) public {
        proposals[proposalCount] = Proposal(description, deadline, 0, 0);
        proposalCount += 1;
    }

    function castVote(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals) public {
        circomVerifier.verifyProof(_pA, _pB, _pC, _pubSignals);
        uint nullifier = _pubSignals[0];
        uint merkleRootPublicInput = _pubSignals[1];
        uint proposalId = uint(_pubSignals[2]);
        uint vote = uint(_pubSignals[3]);

        require(block.timestamp < proposals[proposalId].deadline, "Voting period is over");
        require(merkleRoot == merkleRootPublicInput, "Invalid merke root");
        require(!nullifiers[nullifier], "Vote already casted");

        nullifiers[nullifier] = true;

        if(vote == 1)
            proposals[proposalId].forVotes += 1;
        else if (vote == 2)
            proposals[proposalId].againstVotes += 1;
    }
}
```

Now, create the first proposal for voting by calling the propose() function. For example, you can test by creating a vote with "Do we eat pizza?" as the description and with 1811799232 as the deadline, which expires in 2027.

Next, let's generate a proof in the format required to verify it in Remix.

```bash
snarkjs groth16 prove proveVote_0001.zkey witness.wtns proof.json public.json
snarkjs groth16 verify verification_key.json public.json proof.json
snarkjs generatecall
```

Let's pass the result from the terminal as a parameter in Remix, and we'll see how the vote was executed by accessing the data of proposal 0 through the proposals mapping.

![Votos anónimos con Circom y Solidity](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxtljhmmkyv2zh30i7tz.png)

We observe that our vote was counted without revealing who the sender was. Try to cast the same vote again, and you'll see that it won't be possible—the transaction will revert. This is because we nullified the vote so that each voter can only cast one vote.

Sources and official documentation:
* [Circom official documentation](https://docs.circom.io/)
* [Merkle inclusion proof example](https://github.com/thor314/circuit-examples/blob/main/circom-merkle-proof/verify-merkle-path.circom)
* [Noir anonymous voting](https://dev.to/turupawn/votos-anonimos-usando-zk-39j)

**Thanks for reading this guide!**

Follow Filosofía Código on dev.to and in [Youtube]( https://www.youtube.com/channel/UCbF5CFuXv2vTkcGfepWV2nA) for everything related to Blockchain development.