Grab the source code for the blogpost here.
Recommended prereading:
For the past few months, I've been building a couple of toy dApp projects on Ethereum that ultilize zero knowledge proofs, specifically zk-SNARKs.
As there is little material out there regarding building dApps that ultilizes zero knowledge proofs, I thought I would share my experience building one in a blog post.
The goal of this blog post is to act as a practical guide to help readers build their first zero knowledge dApp (i.e. no maths here sorry).
Note: This blog post assumes that the reader has a basic understanding of public-key cryptography, and how to deploy and interact with smart contracts in JavaScript.
We will be building a zk-dApp that proves if a user belongs to a certain group or not, without revealing who that particular user is.
The user flow of said zk-dApp might look like:
While the development pipeline looks something like:
solidity
library to verify the written zero knowledge circuits.solidity
library from step 2.Just like how you don't need to understand the HTTP procotol to do web development these days, zero knowledge dApp development has sufficient modern tooling that allows developers who don't necessarily have the math background in cryptography (e.g. me), to build applications ultilizing zero knowledge proofs.
My recommended programming languages/tools would be:
JavaScript/TypeScript
as it has a very rich ecosystem and support for EthereumSolidity
for smart contracts due to its maturity and communityTruffle
to deploy your smart contractsCircom
for writing zero knowledge proof circuitsOur goal here is to create a circuit which when supplied with a private key
, and an array of public keys
, constructs a proof if and only if the private key corresponds to one of the public keys (i.e. it will fail if the private key
does not correspond to one of the public keys
as the constraint fails and no proof can be generated).
In pseudocode land, it will be something like:
// Note that a private key is a scalar value (int)
// whereas a public key is a point in space (Tuple[int, int])
const zk_identity = (private_key, public_keys) => {
// derive_public_from_private is a function that
// returns a public key given a private key
derived_public_key = derive_public_from_private(private_key)
for (let pk in public_keys):
if derived_public_key === pk:
return true
return false
}
We will now start writing the zero knowledge circuits using circom
. For an overview of the circom
syntax read the circom tutorial.
We will first install the necessary dependencies and create the project folders which will house our zero knowledge circuit logic: circuits/circuit.circom
.
npm install circom circomlib snarkjs websnark
mkdir contracts
mkdir circuits
mkdir -p build/circuits
touch circuits/circuit.circom
We will start off by including the necessary building blocks:
include "../node_modules/circomlib/circuits/bitify.circom";
include "../node_modules/circomlib/circuits/escalarmulfix.circom";
include "../node_modules/circomlib/circuits/comparators.circom";
template PublicKey() {
// Note: private key needs to be hashed, and then pruned
// to make sure its compatible with the babyJubJub curve
signal private input in;
signal output out[2];
component privBits = Num2Bits(253);
privBits.in <== in;
var BASE8 = [
5299619240641551281634865583518297030282874472190772894086521144482721001553,
16950150798460657717958625567821834550301663161624707787222815936182638968203
];
component mulFix = EscalarMulFix(253, BASE8);
for (var i = 0; i < 253; i++) {
mulFix.e[i] <== privBits.out[i];
}
out[0] <== mulFix.out[0];
out[1] <== mulFix.out[1];
}
What the PublicKey
template does is it derives the public key (out
) from the supplied private key (in
) on the babyJubJub curve (i.e. it's the derive_public_from_private
function from the pseudocode above).
Once we have the building blocks, we can now construct the main logic for our zero knowledge circuit: verifying if the user is within a group or not:
include ...
template PublicKey() {
...
}
template ZkIdentity(groupSize) {
// Public Keys in the smart contract
// Note: this assumes that the publicKeys
// are all unique
signal input publicKeys[groupSize][2];
// Prover's private key
signal private input privateKey;
// Prover's derived public key
component publicKey = PublicKey();
publicKey.in <== privateKey;
// Make sure that derived public key needs to
// matche to at least one public key in the
// smart contract to validate their identity
var sum = 0;
// Create a component to check if two values are
// equal
component equals[groupSize][2];
for (var i = 0; i < groupSize; i++) {
// Helper component to check if two
// values are equal
// We don't want to use ===
// as that will fail immediately if
// the predicate doesn't hold true
equals[i][0] = IsEqual();
equals[i][1] = IsEqual();
equals[i][0].in[0] <== publicKeys[i][0];
equals[i][0].in[1] <== publicKey.out[0];
equals[i][1].in[0] <== publicKeys[i][1];
equals[i][1].in[1] <== publicKey.out[1];
sum += equals[i][0].out;
sum += equals[i][1].out;
}
// equals[i][j].out will return 1 if the values are equal
// and 0 if the values are not equal
// Therefore, if the derived public key (a point in space)
// matches a public keys listed in the smart contract, the sum of
// all the equals[i][j].out should be equal to 2
sum === 2;
}
// Main entry point
component main = ZkIdentity(2);
You can now compile, setup, and generate a verifier (solidity
library) for your circuit:
$(npm bin)/circom circuits/circuit.circom -o build/circuits/circuit.json
## snarkjs setup might take a few seconds
$(npm bin)/snarkjs setup --protocol groth -c build/circuits/circuit.json --pk build/circuits/provingKey.json --vk build/circuits/verifyingKey.json
## Generate solidity lib to verify proof
$(npm bin)/snarkjs generateverifier --pk build/circuits/provingKey.json --vk build/circuits/verifyingKey.json -v contracts/Verifier.sol
## You should now have a new "Verifier.sol" in your contracts directory
## $ ls contracts
## Migrations.sol Verifier.sol
Note that we're generating our provingKey
and verifyingKey
with the groth
protocol as we want to be able to use websnark to generate the proofs as they are significantly faster than snarkjs.
Once you've done the above, we have finished a zero knowledge logic. The next section (Smart Contract Verifier), we will be looking at the generated Verifier.sol
and how we can interact with it nicely.
Note: I've also added some faq below regarding zero knowledge circuits.
Noticed we specified the privateKey
's signal to be a private
one. And because of that, the generated proof will not contain any information about private
signals, but it will about public
ones.
We will talk more about that in the Smart Contract Verifier section below.
After completing the zero knowledge circuits step, a solidity
library named Verifier.sol
should be generated. If you inspect the file's contents, you should be able to see the following function inside:
...
function verifyProof(
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[4] memory input
) public view returns (bool r) {
Proof memory proof;
proof.A = Pairing.G1Point(a[0], a[1]);
proof.B = Pairing.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]);
proof.C = Pairing.G1Point(c[0], c[1]);
uint[] memory inputValues = new uint[](input.length);
for(uint i = 0; i < input.length; i++){
inputValues[i] = input[i];
}
if (verify(inputValues, proof) == 0) {
return true;
} else {
return false;
}
}
...
That is the helper to verify the validity of the proof. The verifyProof
function accepts 4 parameters, but we're only interested in the input
parameter as it represents the public signals (i.e. input signals that are not private inside of template ZkIdentity
in <PROJECT_ROOT>/circuits/circuit.circom
).
Using the input
parameter, we can validate against the existing set of public keys in the smart contract logic to negate the prover generating proofs with invalid public keys.
This concept will be more concrete once we write the logic to validate user identity:
pragma solidity 0.5.11;
import "./Verifier.sol";
contract ZkIdentity is Verifier {
address public owner;
uint256[2][2] public publicKeys;
constructor() public {
owner = msg.sender;
publicKeys = [
[
11588997684490517626294634429607198421449322964619894214090255452938985192043,
15263799208273363060537485776371352256460743310329028590780329826273136298011
],
[
3554016859368109379302439886604355056694273932204896584100714954675075151666,
17802713187051641282792755605644920157679664448965917618898436110214540390950
]
];
}
function isInGroup(
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c,
uint256[4] memory input // public inputs
) public view returns (bool) {
if (
input[0] != publicKeys[0][0] &&
input[1] != publicKeys[0][1] &&
input[2] != publicKeys[1][0] &&
input[3] != publicKeys[1][1]
) {
revert("Supplied public keys do not match contracts");
}
return verifyProof(a, b, c, input);
}
}
We created a new contract ZkIdentity.sol
which inherits from the generated Verifier.sol
, with a initial group of 2 people (publicKeys
). It contains a function called isInGroup
, which given a proof, first validates that the public signals matches the specified group in the smart contract. If it doesn't, revert. If it does, then return if the proof passed or not.
And that is pretty much it. The logic above satisfies our goal of proving if a user belong to a certain group or not, without revealing who that particular user is.
Deploy your contracts to your preferred network before moving on.
Once you have written your zero knowledge circuits and written your smart contract logic, all that is left to do is to generate the proofs and call the smart contract function isInGroup
.
Since there is a lot of boilerplate code to generate the proofs and instantiate smart contracts in JS, I will be demonstrating the pseudocode for generating the proof, and validating the proof on the smart contract side. If you want the complete version, you can find it in this file.
// Assuming below already exists
const provingKey // provingKey.json
const circuit // zero-knowledge circuit we wrote
const zkIdentityContract // Zk-Identity contract instance
const privateKey // Private key that corresponds to one of the public key in the smart contract
const publicKeys = [
[
11588997684490517626294634429607198421449322964619894214090255452938985192043n,
15263799208273363060537485776371352256460743310329028590780329826273136298011n
],
[
3554016859368109379302439886604355056694273932204896584100714954675075151666n,
17802713187051641282792755605644920157679664448965917618898436110214540390950n
]
]
const circuitInputs = {
privateKey,
publicKeys
}
const witness = circuit.calculateWitness(circuitInputs)
const proof = groth16GenProof(witness, provingKey)
const isInGroup = zkIdentityContract.isInGroup(
proof.a,
proof.b,
proof.c,
witness.publicSignals
)
Once you've converted that into JavaScript and executed it, you've just proved that your user belongs in a group, without revealing who that user is!
Zero knowledge tooling has come very far in the past 3 years. You don't need a phd in cryptography to start building zero knowledge applications these days (tho it'll help while debugging).