
In this tutorial, we implement a ZK-based private voting system, Privote, in which no one can see the voters' choices until the voting has ended
Imagine you are a registered voter voting on a particular issue or candidate. An external party wants you to sell your vote, in other words, they want you to vote for their choice, not your own, in exchange for some incentives. They will need proof of your vote if you succumb to their demand—often through a blockchain transaction receipt. While this might seem like an excellent opportunity if you're inclined to sell your vote, it undermines the integrity of the entire voting process.
How can we enable voters to vote for their preferred candidates or issues while concealing their choices so that no one else can see them? The answer lies in one word: Encryption.
In this tutorial, we’ll explore how to develop a simple voting application—Privote—that utilizes encryption on the blockchain. When the time arrives, the votes are decrypted and tallied, and the results are published back to the blockchain while maintaining the voters' anonymity. The results are verified through a zero-knowledge (ZK) proof. Although not perfect, this example illustrates the underlying concepts.
Please refer to Vitalik Buterin's blog post for an in-depth understanding of blockchain voting.
Before we proceed, here is a breakdown of the main components of the Privote dapp:
Smart Contract: This system stores multiple votes (ballots) and related data such as titles, available choices, and encrypted data.
Trusted Party: An off-chain server that decrypts the votes, calculates the results and publishes them on the blockchain. This server is termed the "Trusted Party" for a reason: it cannot publish incorrect results, either by computing inaccurately or by censoring messages.
Frontend: A platform where users connect their wallets and submit votes. There will also be a separate page for the "admin" to view the number of votes cast and manually release the results.
To successfully build out Privote, we need to address two challenges:
Enable any voter to encrypt their vote while allowing the trusted party to decrypt it?
Ensure the trusted party is reliable and can provide a ZK proof of the calculation, but what exactly are we proving?
To solve this, we can adopt asymmetric encryption. Unlike symmetric encryption, which uses the same key to encrypt and decrypt data, asymmetric encryption uses two different keys, one to encrypt and the other to decrypt. Multiple asymmetric encryption algorithms exist, including ElGamal, RSA, DSA, and elliptic curve techniques. For this tutorial, we’ll use the elliptic curve techniques, specifically the [Diffie-Hellman key exchange](https://www.upguard.com/blog/diffie-hellman)(ECDH).
In ECDH, both the trusted party and each voter have their own key pairs (a public key and a private key), and the choice of elliptic curve, such as theBaby Jubjub
curve used here for its suitability in zero-knowledge proofs, impacts the security and efficiency of cryptographic operations; theBaby Jubjub
curve is defined by the equationy^2 = x^3 + 168698 x 2 + x
, and key pairs are generated as specific pairs of points on this curve:
In the generateKeyPair
function, I first create a secure random private key and then calculate the corresponding public key by performing a specific mathematical operation (scalar multiplication) on a predefined point on the curve, often called a base point or generator point. In the code, this point is named Base8. The result of this multiplication is another point on the curve, denoted by its X and Y coordinates. Subsequently, the point is formatted into a bigint for convenient use with the packPoint function.
The voter creates a cipher by combining their private key with the trusted party's public key. This cipher is used to encrypt the vote. The smart contract then saves the encrypted vote and the voter's public key together.
The trusted party can create a cipher using their private key and each voter's public key to decrypt the data.
In thegenerateKeyPair
function, we formatted the public key for easy use with the packPoint function. In the above code, withunpackPoint
, we convert it back to its original form because we need to create a shared secret by combining the private key and the voter's public key. The shared secret is used to decrypt the vote.
After decrypting each vote, calculating the results is easy! Just loop through the votes array and accumulate votes for a given choice.
If you're unfamiliar with ZK proofs, they are a fairly complex yet fascinating piece of technology. ZK proofs are a method whereby one party can prove to another that they know a value without revealing any information about the value itself. Essentially, it's a way to validate the truth of something without sharing the details. Moreover, anyone can verify whether that proof is correct.
For our application, ideally, the trusted party should prove that they can take the encrypted votes, decrypt them, and calculate the number of votes each choice has received. In this case, the “secret” key is the trusted party's private key. For the sake of simplicity, we are just proving the ownership of a private key that matches the public key committed in the smart contract. As mentioned earlier, the votes use that key to generate a cipher to encrypt the vote.
For this purpose, we’ll use ZoKrates, a toolbox for zkSNARKs on Ethereum. It helps you utilize verifiable computation in your dapp, from specifying your program in a high-level language to generating proofs of computation and verifying those proofs in Solidity.
This is the simple ZK program I use:
As you can see in the arguments list, pk is a public argument, and sk is a private argument. context is also public. Only the prover - the trusted party, knows the sk, but the pk is known to anyone as it is in the smart contract. context
is the parameters related to the curve we have picked to generate a key pair.
In the above snippet, I am using the ZoKrates JS SDK to generate a proof. The flow is similar to what is documented in the ZoKrates documentation. Our goal is to use the ZoKrates program to generate a proof, so we’re passing three arguments to the computeWitness function: pk (a public argument), sk (a private argument), and context. These are used to create a witness, which is then used to generate proof.
You might be wondering about this part of the argument:
Thecontext
provides the data related to the specificBaby JubJub
curve we've been using throughout the application, as many curve variations can be used. Files such as the verification key, proving key and ZoKrates program are kept in a separate directory. I am using the function below to get those:
The snippet below is from the contract in which the contract accepts the results calculated by the trusted party. Only if the verification is successful does the contract accept the result. Outsiders can verify this by passing the pk (which is public and found in the contract) and the proof to the verifier function:
In this blog, we developed Privote, a ZK-based private voting system that keeps voters' choices confidential until the voting period ends. We used elliptic curve techniques to encrypt votes and demonstrated how to decrypt and tally them securely. We also implemented zero-knowledge proofs using ZoKrates to verify the voting process's integrity without revealing private information.
This tutorial provides a basic understanding of creating secure and private voting systems on the blockchain. In the second part, we’ll build out the frontend and show you how users can connect and cast votes using the MetaMask SDK.