Tutorial: How to develop a Zero Knowledge App using Groth16
This tutorial will outline the process of developing a Zero Knowledge(zk) Decentralized Application (DApp) from inception to deployment. The objective is to demonstrate the workflow of a zkDApp and its deployment for users to use it.
In this tutorial, we'll develop a zkDApp to demonstrate that someone knows how to solve a Sudoku puzzle without revealing the answer. Utilizing Circom for circuits, Solidity for smart contracts, and JavaScript for frontend development, this guide encompasses essential aspects such as circuit and smart contract testing. It also offers practical tips for the efficient organization and construction of projects of this nature.
The code used in this tutorial is part of a project called zkArcade which can be found here:
The Sudoku contract and verifier are both deployed on the Polygon Mumbai network and the frontend is hosted on vercel at https://zk-arcade.vercel.app/sudoku
Dependencies
Follow the guides provided to install the following dependencies
npm : https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
circom : https://docs.circom.io/getting-started/installation/
Circuits
a) Creating the Circuit
Create a Projects folder and inside the folder create a circuits folder
$ mkdir Projects
$ cd Projects
$ mkdir circuits
$ cd circuits
Run the following command to create a package.json file inside the circuits folder.
yarn init -y
Add the circomlib library to use circuits from there: circomlib docs
yarn add circomlib
Create the sudoku folder and open Visual Studio while inside the folder: ( Note: You can use any code editor you are comfortable with).
$ mkdir sudoku
$ cd sudoku
$ code .
Create a sudoku.circom file and write the following circom code :
pragma circom 2.0.0;
include "../node_modules/circomlib/circuits/comparators.circom";
template Sudoku() {
signal input unsolved[9][9];
signal input solved[9][9];
// Check if the numbers of the solved sudoku are >=1 and <=9
// Each number in the solved sudoku is checked to see if it is >=1 and <=9
component getone[9][9];
component letnine[9][9];
for (var i = 0; i < 9; i++) {
for (var j = 0; j < 9; j++) {
letnine[i][j] = LessEqThan(32);
letnine[i][j].in[0] <== solved[i][j];
letnine[i][j].in[1] <== 9;
getone[i][j] = GreaterEqThan(32);
getone[i][j].in[0] <== solved[i][j];
getone[i][j].in[1] <== 1;
letnine[i][j].out === getone[i][j].out;
}
}
// Check if unsolved is the initial state of solved
// If unsolved[i][j] is not zero, it means that solved[i][j] is equal to unsolved[i][j]
// If unsolved[i][j] is zero, it means that solved [i][j] is different from unsolved[i][j]
component ieBoard[9][9];
component izBoard[9][9];
for (var i = 0; i < 9; i++) {
for (var j = 0; j < 9; j++) {
ieBoard[i][j] = IsEqual();
ieBoard[i][j].in[0] <== solved[i][j];
ieBoard[i][j].in[1] <== unsolved[i][j];
izBoard[i][j] = IsZero();
izBoard[i][j].in <== unsolved[i][j];
ieBoard[i][j].out === 1 - izBoard[i][j].out;
}
}
// Check if each row in solved has all the numbers from 1 to 9, both included
// For each element in solved, check that this element is not equal
// to previous elements in the same row
component ieRow[324];
var indexRow = 0;
for (var i = 0; i < 9; i++) {
for (var j = 0; j < 9; j++) {
for (var k = 0; k < j; k++) {
ieRow[indexRow] = IsEqual();
ieRow[indexRow].in[0] <== solved[i][k];
ieRow[indexRow].in[1] <== solved[i][j];
ieRow[indexRow].out === 0;
indexRow ++;
}
}
}
// Check if each column in solved has all the numbers from 1 to 9, both included
// For each element in solved, check that this element is not equal
// to previous elements in the same column
component ieCol[324];
var indexCol = 0;
for (var i = 0; i < 9; i++) {
for (var j = 0; j < 9; j++) {
for (var k = 0; k < i; k++) {
ieCol[indexCol] = IsEqual();
ieCol[indexCol].in[0] <== solved[k][j];
ieCol[indexCol].in[1] <== solved[i][j];
ieCol[indexCol].out === 0;
indexCol ++;
}
}
}
// Check if each square in solved has all the numbers from 1 to 9, both included
// For each square and for each element in each square, check that the
// element is not equal to previous elements in the same square
component ieSquare[324];
var indexSquare = 0;
for (var i = 0; i < 9; i+=3) {
for (var j = 0; j < 9; j+=3) {
for (var k = i; k < i+3; k++) {
for (var l = j; l < j+3; l++) {
for (var m = i; m <= k; m++) {
for (var n = j; n < l; n++){
ieSquare[indexSquare] = IsEqual();
ieSquare[indexSquare].in[0] <== solved[m][n];
ieSquare[indexSquare].in[1] <== solved[k][l];
ieSquare[indexSquare].out === 0;
indexSquare ++;
}
}
}
}
}
}
}
// unsolved is a public input signal. It is the unsolved sudoku
component main {public [unsolved]} = Sudoku();
b) Compiling the circuit
After writing our circuit using circom, its now time to compile it to get a system of arithmetic equations representing it. As a result of the compilation, we will also obtain programs to compute the witness. To make our work easier, lets create some bash files that are generic and that we'll use every time we want to compile our circuits.
To make use of the compile.sh
script, run the file and specify the circuit name, such as ./compile.sh
sudoku. Alternatively, you can edit the CIRCUIT
variable within the compile.sh
file with your circuit's name and then run ./compile.sh
. Don't forget to run chmod u+x compile.sh
the first time you execute the script.
#!/bin/bash
# Variable to store the name of the circuit
CIRCUIT=sudoku
# In case there is a circuit name as input
if [ "$1" ]; then
CIRCUIT=$1
fi
# Compile the circuit
circom ${CIRCUIT}.circom --r1cs --wasm --sym --c
Next we run the file:
If running for the first time use this command:
chmod u+x compile.sh
After that, use this command:
./compile.sh
Running this produces an output like the one shown below:
c) Creating the input file
Create an input.json file and in it write the following code:
{
"unsolved": [
[0, 0, 0, 0, 0, 6, 0, 0, 0],
[0, 0, 7, 2, 0, 0, 8, 0, 0],
[9, 0, 6, 8, 0, 0, 0, 1, 0],
[3, 0, 0, 7, 0, 0, 0, 2, 9],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[4, 0, 0, 5, 0, 0, 0, 7, 0],
[6, 5, 0, 1, 0, 0, 0, 0, 0],
[8, 0, 1, 0, 5, 0, 3, 0, 0],
[7, 9, 2, 0, 0, 0, 0, 0, 4]
],
"solved": [
[1, 8, 4, 3, 7, 6, 2, 9, 5],
[5, 3, 7, 2, 9, 1, 8, 4, 6],
[9, 2, 6, 8, 4, 5, 7, 1, 3],
[3, 6, 5, 7, 1, 8, 4, 2, 9],
[2, 7, 8, 4, 6, 9, 5, 3, 1],
[4, 1, 9, 5, 3, 2, 6, 7, 8],
[6, 5, 3, 1, 2, 4, 9, 8, 7],
[8, 4, 1, 9, 5, 7, 3, 6, 2],
[7, 9, 2, 6, 8, 3, 1, 5, 4]
]
}
d) Computing the witness
Before creating the proof, we need to calculate all the signals of the circuit that match all the constraints of the circuit. For that, we will use the Wasm
module generated bycircom
that helps to do this job.
Let us start with the Wasm
code. Using the generated Wasm
binary and three JavaScript files, we simply need to provide a file with the inputs and the module will execute the circuit and calculate all the intermediate signals and the output. The set of inputs, intermediate signals and output is called witness.
Create the file executeGroth16.sh
and append the following:
This file is versatile and compatible with any circuit (utilizing groth16). For instance, to execute a circuit named circuit.circom
with ptau
set to 12, run the script as follows: ./executeGroth16.sh circuit 12
. Alternatively, customize the circuit and ptau
variables like so: CIRCUIT=circuit
and PTAU=12.
For detailed information refer to the snarkjs documentation.
#!/bin/bash
# Variable to store the name of the circuit
CIRCUIT=sudoku
# Variable to store the number of the ptau file
PTAU=14
# In case there is a circuit name as an input
if [ "$1" ]; then
CIRCUIT=$1
fi
# In case there is a ptau file number as an input
if [ "$2" ]; then
PTAU=$2
fi
# Check if the necessary ptau file already exists. If it does not exist, it will be downloaded from the data center
if [ -f ./ptau/powersOfTau28_hez_final_${PTAU}.ptau ]; then
echo "----- powersOfTau28_hez_final_${PTAU}.ptau already exists -----"
else
echo "----- Download powersOfTau28_hez_final_${PTAU}.ptau -----"
wget -P ./ptau https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_${PTAU}.ptau
fi
# Compile the circuit
circom ${CIRCUIT}.circom --r1cs --wasm --sym --c
# Generate the witness.wtns
node ${CIRCUIT}_js/generate_witness.js ${CIRCUIT}_js/${CIRCUIT}.wasm input.json ${CIRCUIT}_js/witness.wtns
echo "----- Generate .zkey file -----"
# Generate a .zkey file that will contain the proving and verification keys together with all phase 2 contributions
snarkjs groth16 setup ${CIRCUIT}.r1cs ptau/powersOfTau28_hez_final_${PTAU}.ptau ${CIRCUIT}_0000.zkey
echo "----- Contribute to the phase 2 of the ceremony -----"
# Contribute to the phase 2 of the ceremony
snarkjs zkey contribute ${CIRCUIT}_0000.zkey ${CIRCUIT}_final.zkey --name="1st Contributor Name" -v -e="some random text"
echo "----- Export the verification key -----"
# Export the verification key
snarkjs zkey export verificationkey ${CIRCUIT}_final.zkey verification_key.json
echo "----- Generate zk-proof -----"
# Generate a zk-proof associated to the circuit and the witness. This generates proof.json and public.json
snarkjs groth16 prove ${CIRCUIT}_final.zkey ${CIRCUIT}_js/witness.wtns proof.json public.json
echo "----- Verify the proof -----"
# Verify the proof
snarkjs groth16 verify verification_key.json public.json proof.json
echo "----- Generate Solidity verifier -----"
# Generate a Solidity verifier that allows verifying proofs on Ethereum blockchain
snarkjs zkey export solidityverifier ${CIRCUIT}_final.zkey ${CIRCUIT}Verifier.sol
# Update the solidity version in the Solidity verifier
sed -i 's/0.6.11;/0.8.4;/g' ${CIRCUIT}Verifier.sol
# Update the contract name in the Solidity verifier
sed -i "s/contract Verifier/contract ${CIRCUIT^}Verifier/g" ${CIRCUIT}Verifier.sol
echo "----- Generate and print parameters of call -----"
# Generate and print parameters of call
snarkjs generatecall | tee parameters.txt
Next we run the file: (Note: This process takes a lot of time so patience is advised)
If running for the first time use this command:
chmod u+x executeGroth16.sh
After that, use this command
./executeGroth16.sh
Running this produces an output like the one shown below, with success message [INFO] snarkJS: OK!
:
e) Testing the Circuits
Inside the circuits folder, install chai and install circom_tester:
$yarn add -D chai
$yarn add circom_tester
Create a test folder and inside the test folder create a circuit.js file and add to it:
const { assert } = require("chai");
const wasm_tester = require("circom_tester").wasm;
describe("Sudoku circuit", function () {
let sudokuCircuit;
before(async function () {
sudokuCircuit = await wasm_tester("sudoku/sudoku.circom");
});
it("Should generate the witness successfully", async function () {
let input = {
unsolved: [
[0, 0, 0, 0, 0, 6, 0, 0, 0],
[0, 0, 7, 2, 0, 0, 8, 0, 0],
[9, 0, 6, 8, 0, 0, 0, 1, 0],
[3, 0, 0, 7, 0, 0, 0, 2, 9],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[4, 0, 0, 5, 0, 0, 0, 7, 0],
[6, 5, 0, 1, 0, 0, 0, 0, 0],
[8, 0, 1, 0, 5, 0, 3, 0, 0],
[7, 9, 2, 0, 0, 0, 0, 0, 4],
],
solved: [
[1, 8, 4, 3, 7, 6, 2, 9, 5],
[5, 3, 7, 2, 9, 1, 8, 4, 6],
[9, 2, 6, 8, 4, 5, 7, 1, 3],
[3, 6, 5, 7, 1, 8, 4, 2, 9],
[2, 7, 8, 4, 6, 9, 5, 3, 1],
[4, 1, 9, 5, 3, 2, 6, 7, 8],
[6, 5, 3, 1, 2, 4, 9, 8, 7],
[8, 4, 1, 9, 5, 7, 3, 6, 2],
[7, 9, 2, 6, 8, 3, 1, 5, 4],
],
};
const witness = await sudokuCircuit.calculateWitness(input);
await sudokuCircuit.assertOut(witness, {});
});
it("Should fail because there is a number out of bounds", async function () {
// The number 10 in the first row of solved is > 9
let input = {
unsolved: [
[0, 0, 0, 0, 0, 6, 0, 0, 0],
[0, 0, 7, 2, 0, 0, 8, 0, 0],
[9, 0, 6, 8, 0, 0, 0, 1, 0],
[3, 0, 0, 7, 0, 0, 0, 2, 9],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[4, 0, 0, 5, 0, 0, 0, 7, 0],
[6, 5, 0, 1, 0, 0, 0, 0, 0],
[8, 0, 1, 0, 5, 0, 3, 0, 0],
[7, 9, 2, 0, 0, 0, 0, 0, 4],
],
solved: [
[1, 8, 4, 3, 7, 6, 2, 9, 10],
[5, 3, 7, 2, 9, 1, 8, 4, 6],
[9, 2, 6, 8, 4, 5, 7, 1, 3],
[3, 6, 5, 7, 1, 8, 4, 2, 9],
[2, 7, 8, 4, 6, 9, 5, 3, 1],
[4, 1, 9, 5, 3, 2, 6, 7, 8],
[6, 5, 3, 1, 2, 4, 9, 8, 7],
[8, 4, 1, 9, 5, 7, 3, 6, 2],
[7, 9, 2, 6, 8, 3, 1, 5, 4],
],
};
try {
await sudokuCircuit.calculateWitness(input);
} catch (err) {
// console.log(err);
assert(err.message.includes("Assert Failed"));
}
});
it("Should fail because unsolved is not the initial state of solved", async function () {
// unsolved is not the initial state of solved
let input = {
unsolved: [
[0, 0, 0, 0, 0, 6, 0, 0, 0],
[0, 0, 7, 2, 0, 0, 8, 0, 0],
[9, 0, 6, 8, 0, 0, 0, 1, 0],
[3, 0, 0, 7, 0, 0, 0, 2, 9],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[4, 0, 0, 5, 0, 0, 0, 7, 0],
[6, 5, 0, 1, 0, 0, 0, 0, 0],
[8, 0, 1, 0, 5, 0, 3, 0, 0],
[7, 9, 2, 0, 0, 0, 0, 0, 4],
],
solved: [
[1, 2, 7, 5, 8, 4, 6, 9, 3],
[8, 5, 6, 3, 7, 9, 1, 2, 4],
[3, 4, 9, 6, 2, 1, 8, 7, 5],
[4, 7, 1, 9, 5, 8, 2, 3, 6],
[2, 6, 8, 7, 1, 3, 5, 4, 9],
[9, 3, 5, 4, 6, 2, 7, 1, 8],
[5, 8, 3, 2, 9, 7, 4, 6, 1],
[7, 1, 4, 8, 3, 6, 9, 5, 2],
[6, 9, 2, 1, 4, 5, 3, 8, 7],
],
};
try {
await sudokuCircuit.calculateWitness(input);
} catch (err) {
// console.log(err);
assert(err.message.includes("Assert Failed"));
}
});
it("Should fail due to repeated numbers in a row", async function () {
// The number 1 in the first row of solved is twice
let input = {
unsolved: [
[0, 0, 0, 0, 0, 6, 0, 0, 0],
[0, 0, 7, 2, 0, 0, 8, 0, 0],
[9, 0, 6, 8, 0, 0, 0, 1, 0],
[3, 0, 0, 7, 0, 0, 0, 2, 9],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[4, 0, 0, 5, 0, 0, 0, 7, 0],
[6, 5, 0, 1, 0, 0, 0, 0, 0],
[8, 0, 1, 0, 5, 0, 3, 0, 0],
[7, 9, 2, 0, 0, 0, 0, 0, 4],
],
solved: [
[1, 8, 4, 3, 7, 6, 2, 9, 1],
[5, 3, 7, 2, 9, 1, 8, 4, 6],
[9, 2, 6, 8, 4, 5, 7, 1, 3],
[3, 6, 5, 7, 1, 8, 4, 2, 9],
[2, 7, 8, 4, 6, 9, 5, 3, 1],
[4, 1, 9, 5, 3, 2, 6, 7, 8],
[6, 5, 3, 1, 2, 4, 9, 8, 7],
[8, 4, 1, 9, 5, 7, 3, 6, 2],
[7, 9, 2, 6, 8, 3, 1, 5, 4],
],
};
try {
await sudokuCircuit.calculateWitness(input);
} catch (err) {
// console.log(err);
assert(err.message.includes("Assert Failed"));
}
});
it("Should fail due to repeated numbers in a column", async function () {
// The number 4 in the first column of solved is twice and the number 7 in the last column of solved is twice too
let input = {
unsolved: [
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
],
solved: [
[1, 8, 4, 3, 7, 6, 2, 9, 5],
[5, 3, 7, 2, 9, 1, 8, 4, 6],
[9, 2, 6, 8, 4, 5, 7, 1, 3],
[3, 6, 5, 7, 1, 8, 4, 2, 9],
[2, 7, 8, 4, 6, 9, 5, 3, 1],
[4, 1, 9, 5, 3, 2, 6, 7, 8],
[6, 5, 3, 1, 2, 4, 9, 8, 7],
[8, 4, 1, 9, 5, 7, 3, 6, 2],
[4, 9, 2, 6, 8, 3, 1, 5, 7],
],
};
try {
await sudokuCircuit.calculateWitness(input);
} catch (err) {
// console.log(err);
assert(err.message.includes("Assert Failed"));
}
});
it("Should fail due to repeated numbers in a square", async function () {
// The number 1 in the first square (top-left) of solved is twice
let input = {
unsolved: [
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
],
solved: [
[1, 8, 4, 3, 7, 6, 2, 9, 5],
[5, 3, 7, 2, 9, 1, 8, 4, 6],
[9, 2, 1, 8, 4, 5, 7, 6, 3],
[3, 6, 5, 7, 1, 8, 4, 2, 9],
[2, 7, 8, 4, 6, 9, 5, 3, 1],
[4, 1, 9, 5, 3, 2, 6, 7, 8],
[6, 5, 3, 1, 2, 4, 9, 8, 7],
[8, 4, 6, 9, 5, 7, 3, 1, 2],
[7, 9, 2, 6, 8, 3, 1, 5, 4],
],
};
try {
await sudokuCircuit.calculateWitness(input);
} catch (err) {
// console.log(err);
assert(err.message.includes("Assert Failed"));
}
});
});
Add the mocha
test script to the package.json file which will look like this:
{
"name": "circuits",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "mocha"
},
"dependencies": {
"circom_tester": "^0.0.11",
"circomlib": "^2.0.3"
},
"devDependencies": {
"chai": "^4.3.6"
}
}
Run the tests using yarn:
yarn test
Smart Contracts
a) Initializing
We are going to use the hardhat
framework to work with smart contracts. You can read more about hardhat in the hardhat documentation.
Inside the projects folder, create a contracts folder and inside the contracts folder create a package.json file:
$ mkdir contracts
$ cd contracts
$yarn init -y
Add the hardhat
library and initialize the default hardhat project(create a basic sample project
).
$yarn add -D hardhat
$npx hardhat
You can delete the sample files that have been provided, don't delete the folders.
b) Creating the smart contracts
Run the following command to copy the sudokuVerifier.sol
contract generated before into the contracts folder:
cp ../circuits/sudoku/sudokuVerifier.sol contracts
Inside the Projects/contracts/contracts folder create the Sudoku.sol file and add to it:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;
interface IVerifier {
function verifyProof(
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c,
uint256[81] memory input
) external view returns (bool);
}
contract Sudoku {
address public verifierAddr;
uint8[9][9][3] sudokuBoardList = [
[
[1, 2, 7, 5, 8, 4, 6, 9, 3],
[8, 5, 6, 3, 7, 9, 1, 2, 4],
[3, 4, 9, 6, 2, 1, 8, 7, 5],
[4, 7, 1, 9, 5, 8, 2, 3, 6],
[2, 6, 8, 7, 1, 3, 5, 4, 9],
[9, 3, 5, 4, 6, 2, 7, 1, 8],
[5, 8, 3, 2, 9, 7, 4, 6, 1],
[7, 1, 4, 8, 3, 6, 9, 5, 2],
[6, 9, 2, 1, 4, 5, 3, 0, 7]
],
[
[0, 2, 7, 5, 0, 4, 0, 0, 0],
[0, 0, 0, 3, 7, 0, 0, 0, 4],
[3, 0, 0, 0, 0, 0, 8, 0, 0],
[4, 7, 0, 9, 5, 8, 0, 3, 6],
[2, 6, 8, 7, 1, 0, 0, 4, 9],
[0, 0, 0, 0, 0, 2, 0, 1, 8],
[0, 8, 3, 0, 9, 0, 4, 0, 0],
[7, 1, 0, 0, 0, 0, 9, 0, 2],
[0, 0, 0, 0, 0, 5, 0, 0, 7]
],
[
[0, 0, 0, 0, 0, 6, 0, 0, 0],
[0, 0, 7, 2, 0, 0, 8, 0, 0],
[9, 0, 6, 8, 0, 0, 0, 1, 0],
[3, 0, 0, 7, 0, 0, 0, 2, 9],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[4, 0, 0, 5, 0, 0, 0, 7, 0],
[6, 5, 0, 1, 0, 0, 0, 0, 0],
[8, 0, 1, 0, 5, 0, 3, 0, 0],
[7, 9, 2, 0, 0, 0, 0, 0, 4]
]
];
constructor(address _verifierAddr) {
verifierAddr = _verifierAddr;
}
function verifyProof(
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c,
uint256[81] memory input
) public view returns (bool) {
return IVerifier(verifierAddr).verifyProof(a, b, c, input);
}
function verifySudokuBoard(uint256[81] memory board)
private
view
returns (bool)
{
bool isEqual = true;
for (uint256 i = 0; i < sudokuBoardList.length; ++i) {
isEqual = true;
for (uint256 j = 0; j < sudokuBoardList[i].length; ++j) {
for (uint256 k = 0; k < sudokuBoardList[i][j].length; ++k) {
if (board[9 * j + k] != sudokuBoardList[i][j][k]) {
isEqual = false;
break;
}
}
}
if (isEqual == true) {
return isEqual;
}
}
return isEqual;
}
function verifySudoku(
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c,
uint256[81] memory input
) public view returns (bool) {
require(verifySudokuBoard(input), "This board does not exist");
require(verifyProof(a, b, c, input), "Filed proof check");
return true;
}
function pickRandomBoard(string memory stringTime)
private
view
returns (uint8[9][9] memory)
{
uint256 randPosition = uint256(
keccak256(
abi.encodePacked(
block.difficulty,
block.timestamp,
msg.sender,
stringTime
)
)
) % sudokuBoardList.length;
return sudokuBoardList[randPosition];
}
function generateSudokuBoard(string memory stringTime)
public
view
returns (uint8[9][9] memory)
{
return pickRandomBoard(stringTime);
}
}
c) Testing the smart contract
Add the same version of the snarkjs library to test the computing of the proof:
Note: Ensure that the global and the local version of snarkjs are the same
$yarn add snarkjs
Create a zkProof folder and copy the sudoku.wasm
and sudoku_final.zkey
files into the zkProof folder.
cp ../circuits/sudoku/sudoku_js/sudoku.wasm zkproof
cp ../circuits/sudoku/sudoku_final.zkey zkproof
Create a test.js
in the test folder and add to it:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { exportCallDataGroth16 } = require("./utils/utils");
describe("Sudoku", function () {
let SudokuVerifier, sudokuVerifier, Sudoku, sudoku;
before(async function () {
SudokuVerifier = await ethers.getContractFactory("SudokuVerifier");
sudokuVerifier = await SudokuVerifier.deploy();
await sudokuVerifier.deployed();
Sudoku = await ethers.getContractFactory("Sudoku");
sudoku = await Sudoku.deploy(sudokuVerifier.address);
await sudoku.deployed();
});
it("Should generate a board", async function () {
let board = await sudoku.generateSudokuBoard(new Date().toString());
expect(board.length).to.equal(9);
});
it("Should return true for valid proof on-chain", async function () {
const unsolved = [
[1, 2, 7, 5, 8, 4, 6, 9, 3],
[8, 5, 6, 3, 7, 9, 1, 2, 4],
[3, 4, 9, 6, 2, 1, 8, 7, 5],
[4, 7, 1, 9, 5, 8, 2, 3, 6],
[2, 6, 8, 7, 1, 3, 5, 4, 9],
[9, 3, 5, 4, 6, 2, 7, 1, 8],
[5, 8, 3, 2, 9, 7, 4, 6, 1],
[7, 1, 4, 8, 3, 6, 9, 5, 2],
[6, 9, 2, 1, 4, 5, 3, 0, 7],
];
const solved = [
[1, 2, 7, 5, 8, 4, 6, 9, 3],
[8, 5, 6, 3, 7, 9, 1, 2, 4],
[3, 4, 9, 6, 2, 1, 8, 7, 5],
[4, 7, 1, 9, 5, 8, 2, 3, 6],
[2, 6, 8, 7, 1, 3, 5, 4, 9],
[9, 3, 5, 4, 6, 2, 7, 1, 8],
[5, 8, 3, 2, 9, 7, 4, 6, 1],
[7, 1, 4, 8, 3, 6, 9, 5, 2],
[6, 9, 2, 1, 4, 5, 3, 8, 7],
];
const input = {
unsolved: unsolved,
solved: solved,
};
let dataResult = await exportCallDataGroth16(
input,
"./zkproof/sudoku.wasm",
"./zkproof/sudoku_final.zkey"
);
// Call the function.
let result = await sudokuVerifier.verifyProof(
dataResult.a,
dataResult.b,
dataResult.c,
dataResult.Input
);
expect(result).to.equal(true);
});
it("Should return false for invalid proof on-chain", async function () {
let a = [0, 0];
let b = [
[0, 0],
[0, 0],
];
let c = [0, 0];
let Input = new Array(81).fill(0);
let dataResult = { a, b, c, Input };
// Call the function.
let result = await sudokuVerifier.verifyProof(
dataResult.a,
dataResult.b,
dataResult.c,
dataResult.Input
);
expect(result).to.equal(false);
});
it("Should verify Sudoku successfully", async function () {
const unsolved = [
[1, 2, 7, 5, 8, 4, 6, 9, 3],
[8, 5, 6, 3, 7, 9, 1, 2, 4],
[3, 4, 9, 6, 2, 1, 8, 7, 5],
[4, 7, 1, 9, 5, 8, 2, 3, 6],
[2, 6, 8, 7, 1, 3, 5, 4, 9],
[9, 3, 5, 4, 6, 2, 7, 1, 8],
[5, 8, 3, 2, 9, 7, 4, 6, 1],
[7, 1, 4, 8, 3, 6, 9, 5, 2],
[6, 9, 2, 1, 4, 5, 3, 0, 7],
];
const solved = [
[1, 2, 7, 5, 8, 4, 6, 9, 3],
[8, 5, 6, 3, 7, 9, 1, 2, 4],
[3, 4, 9, 6, 2, 1, 8, 7, 5],
[4, 7, 1, 9, 5, 8, 2, 3, 6],
[2, 6, 8, 7, 1, 3, 5, 4, 9],
[9, 3, 5, 4, 6, 2, 7, 1, 8],
[5, 8, 3, 2, 9, 7, 4, 6, 1],
[7, 1, 4, 8, 3, 6, 9, 5, 2],
[6, 9, 2, 1, 4, 5, 3, 8, 7],
];
const input = {
unsolved: unsolved,
solved: solved,
};
let dataResult = await exportCallDataGroth16(
input,
"./zkproof/sudoku.wasm",
"./zkproof/sudoku_final.zkey"
);
// Call the function.
let result = await sudoku.verifySudoku(
dataResult.a,
dataResult.b,
dataResult.c,
dataResult.Input
);
expect(result).to.equal(true);
});
it("Should be reverted on Sudoku verification because the board is not in the board list", async function () {
const unsolved = [
[1, 2, 7, 5, 8, 4, 6, 9, 3],
[8, 5, 6, 3, 7, 9, 1, 2, 4],
[3, 4, 9, 6, 2, 1, 8, 7, 5],
[4, 7, 1, 9, 5, 8, 2, 3, 6],
[2, 6, 8, 7, 1, 3, 5, 4, 9],
[9, 3, 5, 4, 6, 2, 7, 1, 8],
[5, 8, 3, 2, 9, 7, 4, 6, 1],
[7, 1, 4, 8, 3, 6, 9, 5, 2],
[6, 9, 2, 1, 4, 5, 3, 8, 0],
];
const solved = [
[1, 2, 7, 5, 8, 4, 6, 9, 3],
[8, 5, 6, 3, 7, 9, 1, 2, 4],
[3, 4, 9, 6, 2, 1, 8, 7, 5],
[4, 7, 1, 9, 5, 8, 2, 3, 6],
[2, 6, 8, 7, 1, 3, 5, 4, 9],
[9, 3, 5, 4, 6, 2, 7, 1, 8],
[5, 8, 3, 2, 9, 7, 4, 6, 1],
[7, 1, 4, 8, 3, 6, 9, 5, 2],
[6, 9, 2, 1, 4, 5, 3, 8, 7],
];
const input = {
unsolved: unsolved,
solved: solved,
};
let dataResult = await exportCallDataGroth16(
input,
"./zkproof/sudoku.wasm",
"./zkproof/sudoku_final.zkey"
);
await expect(
sudoku.verifySudoku(
dataResult.a,
dataResult.b,
dataResult.c,
dataResult.Input
)
).to.be.reverted;
});
});
Create a utils folder inside the test folder. Create a utils.js
file in the utils folder and add to it:
const { groth16 } = require("snarkjs");
async function exportCallDataGroth16(input, wasmPath, zkeyPath) {
const { proof: _proof, publicSignals: _publicSignals } =
await groth16.fullProve(input, wasmPath, zkeyPath);
const calldata = await groth16.exportSolidityCallData(_proof, _publicSignals);
const argv = calldata
.replace(/["[\]\s]/g, "")
.split(",")
.map((x) => BigInt(x).toString());
const a = [argv[0], argv[1]];
const b = [
[argv[2], argv[3]],
[argv[4], argv[5]],
];
const c = [argv[6], argv[7]];
const Input = [];
for (let i = 8; i < argv.length; i++) {
Input.push(argv[i]);
}
return { a, b, c, Input };
}
module.exports = {
exportCallDataGroth16,
};
Some additional libraries and packages that will be useful in testing the contracts could be added as shown below:
hardhat-gas-reporter:
$yarn add -D hardhat-gas-reporter
Add it to the hardhat.config.js
file after the last require :
require("hardhat-gas-reporter");
Optimizer: Modify the hardhat.config.js
file:
solidity: {
version: "0.8.4",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
To test the contracts run the following command:
$npx hardhat test
You will see an output similar to this: (Note: This is inclusive of the whole zkArcade project so yours might look a bit different)
d) Run Smart Contracts
Inside the scripts folder, create a run.js
file and add to it:
const main = async () => { const SudokuVerifier = await hre.ethers.getContractFactory("SudokuVerifier"); const sudokuVerifier = await SudokuVerifier.deploy(); await sudokuVerifier.deployed(); console.log("SudokuVerifier Contract deployed to:", sudokuVerifier.address); const Sudoku = await hre.ethers.getContractFactory("Sudoku"); const sudoku = await Sudoku.deploy(sudokuVerifier.address); await sudoku.deployed(); console.log("Sudoku Contract deployed to:", sudoku.address); let board = await sudoku.generateSudokuBoard(new Date().toString()); console.log(board); let callDataSudoku = [ [ "0x2c5defbc1706b51a941bff57cc1e40f2c941fccbbf13c587de8bf36dc1017b56", "0x10c1184d623f5f8efa15052e3c59613a80507c11f0605c998c36a0336bb4012f", ], [ [ "0x21fcd189100631f163579f405875def48a41c0f6b1dae436a71feebcc84af110", "0x0ac4517b8693891029159dd4d5b15e17fbe0ac27068cf14a9781e3a347352cd6", ], [ "0x0f43d6e3ace2228a67578fcb7778d248b274f6342ed14828454d343c0a933110", "0x075df9f66bb65d47e04caa80d6f8e99fbd8db5217d259a3aac80709b5cb37347", ], ], [ "0x05f56ad16658e304882d42230c634626848f981c51d7a34f5f9833f7272447fb", "0x1405302f7a377aa913513f026ea72e3544484d66ce27204e72787ce4bf3229b4", ], [ "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000006", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000007", "0x0000000000000000000000000000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000008", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000009", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000006", "0x0000000000000000000000000000000000000000000000000000000000000008", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000003", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000007", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000000000000000000000000000009", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000004", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000005", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000007", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000006", "0x0000000000000000000000000000000000000000000000000000000000000005", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000008", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000005", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000003", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000007", "0x0000000000000000000000000000000000000000000000000000000000000009", "0x0000000000000000000000000000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000004", ], ]; // Call the function. let result = await sudokuVerifier.verifyProof( callDataSudoku[0], callDataSudoku[1], callDataSudoku[2], callDataSudoku[3] ); console.log("Result", result); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain();
callDataSudoku
is the data of theparameters.txt
file generated before, theverifyProof
function will return true. If you change an element of thecallDataSudoku
variable, theverify Proof
function will return false.
Run the run.js
file
$npx hardhat run scripts/run.js
e) Deploying the Smart contracts
We are going to deploy the contracts on Polygon Mumbai. Ensure you have Metamask installed and have added the polygon mumbai network to Metamask.
Inside the scripts folder, create a deploy.js
file and add to it:
const main = async () => {
const SudokuVerifier = await hre.ethers.getContractFactory("SudokuVerifier");
const sudokuVerifier = await SudokuVerifier.deploy();
await sudokuVerifier.deployed();
console.log("SudokuVerifier Contract deployed to:", sudokuVerifier.address);
const Sudoku = await hre.ethers.getContractFactory("Sudoku");
const sudoku = await Sudoku.deploy(sudokuVerifier.address);
await sudoku.deployed();
console.log("Sudoku Contract deployed to:", sudoku.address);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
Create a .env
file and add to it. Add your private key
Warning: Ensure that the .env
file is in the .gitignore
file and don't use any real crypto in this wallet account
PRIVATE_KEY=
Add .env
file to the .gitignore
file
node_modules
.env
coverage
coverage.json
typechain
#Hardhat files
cache
artifacts
Install the dotenv library and edit the hardhatconfig.js
file to read the private key from .env
$yarn add dotenv
hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require("hardhat-gas-reporter");
require("dotenv").config();
// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: {
version: "0.8.4",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
sepolia: {
url: "https://rpc.sepolia.org/",
accounts: [process.env.PRIVATE_KEY],
},
mumbai: {
url: "https://rpc-mumbai.maticvigil.com",
accounts: [process.env.PRIVATE_KEY],
},
},
};
Get MATIC from the Mumbai faucet: https://faucet.polygon.technology/
Run the deploy.js
file:
$npx hardhat run scripts/deploy.js --network mumbai
You can see the transactions on the mumbai explorer: https://mumbai.polygonscan.com/
Creating the Frontend
a) Initializing the project
Now lets create the frontend to interact with our zkDApp.
We are going to use Next.js. More information about it can be found here: https://nextjs.org/
Inside the Projects folder run this command:
yarn create next-app frontend
Go inside the frontend folder and start a server to check that everything is installed correctly:
cd frontend
$yarn dev
Open a browser and go to http://localhost:3000/
Something similar should appear
b) Adding Libraries
Add Tailwind for page styling: https://tailwindcss.com/docs/guides/nextjs
Add the following libraries
yarn add wagmi ethers
yarn add snarkjs
Import the snarkjs to the client side using either of these methods
Use snark.min.js file
cp ./node_modules/snarkjs/build/snarkjs.min.js ./public
Add
<Script id="snarkjs" src="/snarkjs.min.js" />
inlayout.js
You can access the library using
window.snarkjs
.
2. As a package using yarn as shown above.
Configure webpack in the next.config.js
file to use snarkjs
:
const nextConfig = {
reactStrictMode: true,
webpack: function (config, options) {
if (!options.isServer) {
config.resolve.fallback.fs = false;
}
config.experiments = { asyncWebAssembly: true };
return config;
},
};
module.exports = nextConfig;
Note: The config.experiments = { asyncWebAssembly: true };
line is for using wasm files.
c) Adding configuration
To generate the proof, create a .babelrc
file and add to it
{
"presets": [
[
"next/babel",
{
"preset-env": {
// "debug": true,
"targets": [
"last 2 Edge versions",
"last 2 Opera versions",
"last 2 Safari versions",
"last 2 Chrome versions",
"last 2 Firefox versions"
]
}
}
]
]
}
d) Adding Zero Knowledge to the frontend
Inside the public folder, create a zkProof folder and copy the sudoku.wasm
and sudoku_final.key
files there:
cp ../../circuits/sudoku/sudoku_js/sudoku.wasm ./public/zkproof
cp ../../circuits/sudoku/sudoku_final.zkey ./public/zkproof
In the frontend folder, create a zkproof folder and inside the zkproof folder create the snarkjsZkproof.js
file and add to it:
import { groth16 } from "snarkjs";
export async function exportCallDataGroth16(input, wasmPath, zkeyPath) {
const { proof: _proof, publicSignals: _publicSignals } =
await groth16.fullProve(input, wasmPath, zkeyPath);
const calldata = await groth16.exportSolidityCallData(_proof, _publicSignals);
const argv = calldata
.replace(/["[\]\s]/g, "")
.split(",")
.map((x) => BigInt(x).toString());
const a = [argv[0], argv[1]];
const b = [
[argv[2], argv[3]],
[argv[4], argv[5]],
];
const c = [argv[6], argv[7]];
const Input = [];
for (let i = 8; i < argv.length; i++) {
Input.push(argv[i]);
}
return { a, b, c, Input };
}
Some errors may occur.
If you get an error importing groth16
from snarkjs
that says:
./node_modules/fastfile/src/fastfile.js
Can't import the named export 'O_TRUNC' (imported as 'O_TRUNC') from default-exporting module (only default export is available)
This is solve by importing groth16 this way:
const groth16 = require("snarkjs").groth16;
or you can try this way
const { groth16 } = require("snarkjs");
Another error may be like this:
./node_modules/snarkjs/build/main.cjs:8:0
Module not found: Can't resolve 'readline'
Then add the config.resolve.fallback.readline = false;
line in the next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
webpack: function (config, options) {
if (!options.isServer) {
config.resolve.fallback.fs = false;
config.resolve.fallback.readline = false;
}
config.experiments = { asyncWebAssembly: true };
return config;
},
};
module.exports = nextConfig;
Okay, now away from troubleshooting errors, inside the frontend folder create the sudoku
folder and inside this new folder created, create the snarkjsSudoku.js
file and add to it:
import { exportCallDataGroth16 } from "../snarkjsZkproof";
export async function sudokuCalldata(unsolved, solved) {
const input = {
unsolved: unsolved,
solved: solved,
};
let dataResult;
try {
dataResult = await exportCallDataGroth16(
input,
"/zkproof/sudoku.wasm",
"/zkproof/sudoku_final.zkey"
);
} catch (error) {
// console.log(error);
window.alert("Wrong answer");
}
return dataResult;
}
e) Connecting the smart contracts to verify the proof
Connection of the contracts using ABI
const contract = useContract({
addressOrName: contractAddress.sudokuContract,
contractInterface: sudokuContractAbi.abi,
signerOrProvider: signer || provider,
});
Now to use the verifySudoku
function
result = await contract.verifySudoku(
calldata.a,
calldata.b,
calldata.c,
calldata.Input
);
f) Adding utility files
Create a utils folder inside the frontend folder. Inside the utils folder add;
The
abiFiles
folder which contains all the abi files needed for the frontend application. You can find the abi file here:Projects/contracts/artifacts/contracts/Sudoku.sol/Sudoku.json
. The abi file is a file generated when the smart contract is compiled.The
contractsaddress.json
file that contains all the necessary smart contracts addresses, in this case, theSudoku
contract:
{
"sudokuContract": "0xD0276C2f1353157A562400309560C9cdCBA47212"
}
3. The networks.json
file which contains all the chains that we can use in the app and the selectedChain
which is the network used in this project (Mumbai):
{
"selectedChain": "11155111",
"1337": {
"chainId": "1337",
"chainName": "Localhost 8545",
"rpcUrls": ["http://localhost:8545"],
"nativeCurrency": {
"symbol": "ETH"
},
"blockExplorerUrls": []
},
"11155111": {
"chainId": "11155111",
"chainName": "Sepolia",
"rpcUrls": ["https://rpc.sepolia.org"],
"nativeCurrency": {
"symbol": "ETH"
},
"blockExplorerUrls": ["https://sepolia.etherscan.io/"]
},
"80001": {
"chainId": "80001",
"chainName": "Mumbai",
"rpcUrls": ["https://rpc-mumbai.maticvigil.com"],
"nativeCurrency": {
"symbol": "MATIC"
},
"blockExplorerUrls": ["https://mumbai.polygonscan.com/"]
}
}
The
switchNetwork.js
file to switch to the network used in the projects if necessary. (In this project we are using Mumbai)import networks from "../utils/networks.json"; export const switchNetwork = async () => { if (window.ethereum) { try { // Try to switch to the chain await ethereum.request({ method: "wallet_switchEthereumChain", params: [ { chainId: `0x${parseInt(networks.selectedChain).toString(16)}` }, ], }); } catch (switchError) { // This error code indicates that the chain has not been added to MetaMask. if (switchError.code === 4902) { try { await ethereum.request({ method: "wallet_addEthereumChain", params: [ { chainId: `0x${parseInt(networks.selectedChain).toString(16)}`, chainName: networks[networks.selectedChain].chainName, rpcUrls: networks[networks.selectedChain].rpcUrls, nativeCurrency: { symbol: networks[networks.selectedChain].nativeCurrency.symbol, decimals: 18, }, blockExplorerUrls: networks[networks.selectedChain].blockExplorerUrls, }, ], }); } catch (addError) { console.log(addError); } } // handle other "switch" errors } } else { // If window.ethereum is not found then MetaMask is not installed alert( "MetaMask is not installed. Please install it to use this app: https://metamask.io/download/" ); } };
g) Some Code cleanup
Delete:
The
api
folder inside thepages
folder.The
favicon.ico
file, inside thepublic
folder.The
vercel.svg
file, inside thepublic
folder.
Create:
The
assets
folder and add the image inside it.The
components
folder and copy all the code inside that folder.
Copy:
All the files inside the
pages
folder.All the files inside the
styles
folder.The
favicon.ico
andsocialMedia.png
files, inside thepublic
folder.
h) Deploying the frontend
In this tutorial, we are going to deploy the frontend on vercel
Create a new Github repository and push the project into it.
Go to Vercel:
Create a new project.
Import the Github repo created before.
Configure the project (select
Next.js
as FRAMEWORK PRESET andfrontend
as ROOT DIRECTORY) and clickDeploy
:
You can read more about vercel here: https://vercel.com/docs
zkDApp DEMO
The deployed website of the of this tutorial alongside 2 other games can be found here: http://zk-arcade.vercel.app/
Tips and Conclusion
If you change the circuit, you should update:
sudokuVerifier.sol
insidecontracts/contracts
.sudoku.wasm
andsudoku_final.zkey
files insidecontracts/zkproof
.sudoku.wasm
andsudoku_final.zkey
files insidefrontend/public/zkproof
.
If you change smart contracts you should update if necessary:
The
Sudoku.json
abi file insidefrontend/utils/abiFiles
.The
contractaddress.json
file insidefrontend/utils/
(with the new Sudoku smart contract address in case smart contracts are deployed again).
If you change the network used to deploy you should update
selectedChain
insidefrontend/utils/networks.json
.To copy or update all the zk elements to use and test smart contracts, you can create the
copyZkFiles.sh
file insideProjects/contracts/contracts/scripts
and add to it:#!/bin/bash # Copy the verifier cp ../circuits/sudoku/sudokuVerifier.sol contracts # Create the zkproof folder if it does not exist mkdir -p zkproof # Copy the wasm file to test smart contracts cp ../circuits/sudoku/sudoku_js/sudoku.wasm zkproof # Copy the final zkey file to test smart contracts cp ../circuits/sudoku/sudoku_final.zkey zkproof
Running it for the first time
chmod u+x scripts/copyZkFiles.sh
Subsequent times
./scripts/copyZkFiles.sh
To copy or update all the zk elements to use in the frontend, you can create the
copyZkFiles.sh
file insideProjects/frontend/scripts
and add to it:#!/bin/bash # Create the zkproof folder inside the public folder if it does not exist mkdir -p public/zkproof # Copy the wasm file cp ../circuits/sudoku/sudoku_js/sudoku.wasm public/zkproof # Copy the final zkey cp ../circuits/sudoku/sudoku_final.zkey public/zkproof # Create the abiFiles folder inside the utils folder if it does not exist mkdir -p utils/abiFiles # Copy the abi file cp ../contracts/artifacts/contracts/Sudoku.sol/Sudoku.json utils/abiFiles
Running it for the first time
chmod u+x scripts/copyZkFiles.sh
Subsequent times
./scripts/copyZkFiles.sh
This completes the tutorial on how to create a fullstack zkDApp from start to finish using Groth16. Another way to have achieved this would have been to use Plonk instead of Groth16. Using Plonk, we can avoid a trusted ceremony for every circuit but it is slower than GRoth16 because the zkey file is larger .
Subscribe to my newsletter
Read articles from Deogracious Aggrey directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by