An example of how generate zero-knowledge proofs and verify using an Ethereum smart contract.
-
npm install -g circom npm install -g snarkjs
NOTE: for this demo I used
[email protected]
and[email protected]
- Circom is a language for writing arithmetic circuits that can be used in zero-knowledge proofs. Circom stands for circuit compiler.
- Snarkjs is a JavaScript implementation of zkSNARK schemes which is used to generate proofs given circuits generated with circom.
-
Create a circuit:
We'll create a circuit that tries to prove we are able to factor a number.
- The circuit will have 2 private inputs and 1 output.
- The output signal
c
must be the value of signala
times signalb
. - We instantiate the circuit and assign it to the main component. The component
main
must always exist.
circuit.circom
template Multiplier() { signal private input a; signal private input b; signal output c; c <== a*b; } component main = Multiplier();
Ths only thing the circuit enforces is that
c
equalsa
timesb
with the statementc <== a*b
which defines the constraint. -
Compile the circuit with circom:
circom factor/circuit.circom -o circuit.json
This will output
circuit.json
-
Run setup for the circuit:
Running the setup method will instantiate key files we'll need:
snarkjs setup --circuit=circuit.json
The setup will generate a proving key and a verification key,
proving_key.json
andverification_key.json
respectively. -
Calculate a witness:
We'll create a file containing the input parameters.
- Provide a file with the inputs and it will execute the circuit.
- Snarkjs will calculate the intermediate signals and the output.
- The calculated set of signals is the witness.
For this example, we'll be proving we can factor
56
without revealing the two factor numbers.We'll prove that we know that
7
and8
multiply to56
:input.json
{ "a": 7, "b": 8 }
- The zero knowledge proofs prove that we know a set of signals that match the constraints.
- These signals are the witness.
- The witness doesn't reveal any of the signals except the public inputs and the outputs.
We can calculate the witness now:
snarkjs calculatewitness --circuit=circuit.json --input=input.json
This generates
witness.json
with all the signals. -
Generate proof:
We'll use the proving key and witness to generate a proof that we the inputs that factor into
56
:snarkjs proof --witness=witness.json --provingkey=proving_key.json
This generates
proof.json
andpublic.json
which is the values of the public inputs and outputs. In this case it's just the output because our inputs were private.[ "56" ]
-
Verify proof:
We can verify a proof using snarkjs:
$ snarkjs verify --proof=proof.json --verificationkey=verification_key.json --public=public.json OK
This will output
OK
if correct orINVALID
if incorrect. -
Smart contract to verify proofs:
snarkjs provides a method to generate a contract to verify proofs onchain:
snarkjs generateverifier --verificationkey=verification_key.json
This outputs
verifier.sol
The contract method we'll be using is
verifyProof
:function verifyProof( uint[2] a, uint[2] a_p, uint[2][2] b, uint[2] b_p, uint[2] c, uint[2] c_p, uint[2] h, uint[2] k, uint[1] input ) view public returns (bool r) { Proof memory proof; proof.A = Pairing.G1Point(a[0], a[1]); proof.A_p = Pairing.G1Point(a_p[0], a_p[1]); proof.B = Pairing.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]); proof.B_p = Pairing.G1Point(b_p[0], b_p[1]); proof.C = Pairing.G1Point(c[0], c[1]); proof.C_p = Pairing.G1Point(c_p[0], c_p[1]); proof.H = Pairing.G1Point(h[0], h[1]); proof.K = Pairing.G1Point(k[0], k[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; } }
As you can see there is a lot going on. Snarkjs conveniently provides a method to generate the transaction input parameters.
snarkjs generatecall --proof=proof.json --public=public.json
The output will look similar to this:
["0x225074f4c52d6abfbe39070488e9ad4a614c17e954f6dd1f95df298a0eef0a6c", "0x16c7e4b0c01a120fe48c8eb2339a7209e77796328a61e56547b73567016fd75e"],["0x250a2c4eb6c8d434b3fa3c408c1a3ab4a2db6d63c7ba917c9849083c86812832", "0x2f9c617819fec52b3417745eee59319eb56d846b5e428cb81d90a9a1f09beaf4"],[["0x19ab9c29a7e580507cc3054f36f12506d429112faa75e751ddc4c6237344ab1a", "0x099afd70d77234c35b15112b7a9352f6ec2b3030408125b66654631de21c078e"],["0x2d67175e322ab6e6a98921e29c94cdd1ab07db28d62de7e104199361e5092851", "0x0bdc9fd000cdbc79b344fb307c1ea679b2a14f1756a47cbccefe6349265fdd9d"]],["0x2f17a8a96f4f1a37dcf99fceddbb942e94a3a83547b80f4061eaa295d969dccc", "0x086cc7c64c083849eba6161be31f794b359e735e83c25a7ee9475d9943d9b8f5"],["0x26f172ffa1f027799863c43c640ccb632fc664c918138fea026127bdf07c6c0a", "0x0e0dd693317beda6b8bf1ac9024af15a35bee071cc5bdcb2784926df18ba3fb0"],["0x1c23be59028e8095a430b2e5b3768303ec5163a49e2a9b4da7f3a6b30b78a5f1", "0x0318a6a3bd9a7c36e35fc44030fda9dd594965b1cba90c942211028c3f308fb6"],["0x1f84acf96a6d7b112c097feb438dadd8bc56c59f5e466e7d17e33560df87c893", "0x257a236d53fb9a129d5e8239d592bc3173f99ab378df5f5c5a00d4e3fb6983b2"],["0x1c55a602b2237a439ccc899ae6a49e1d08ca66300283500188e5823d246a9321", "0x1c297b898c386684bc32e504317dbfdb09971227d413541f74659fcc2cd8a9a5"],["0x0000000000000000000000000000000000000000000000000000000000000038"]
The verify function returns
true
if the public inputs satisfy the proof. -
Compiling verifier contract:
Head over to the Ethereum Remix IDE and paste in the smart contract.
- Compile the smart contract using the correct solidity compiler version.
-
Deploying verifier contract:
Use the MetaMask extension and connect to a testnet. I chose the Rinkeby testnet for this example.
- Select Injected Web3 as the environment.
- Select the Verifier contract and click Deploy.
Deployment transaction: https://rinkeby.etherscan.io/tx/0x6b7c99bc9b6972e11c3a2918d8fe00b40f0036ebf18d28918a20a80b8d306947
-
Verify proofs using the deployed contract:
Use the
generatecall
helper like we did in a previous step and paste it in to the verifyProof input field under Deployed Contracts:This is a pure function meaning it doesn't perform any writes. You'll get back a boolean response as you can see in the bottom right below the method input field.
-
To show general statistics of a circuit, you can run:
snarkjs info --circuit=circuit.json
-
Print the constraints of the circuit by running:
snarkjs printconstraints --circuit=circuit.json