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

  1. npm : https://docs.npmjs.com/downloading-and-installing-node-js-and-npm

  2. yarn : https://yarnpkg.com/getting-started/install

  3. circom : https://docs.circom.io/getting-started/installation/

  4. snarkjs : https://github.com/iden3/snarkjs#install-snarkjs

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 the parameters.txt file generated before, the verifyProof function will return true. If you change an element of the callDataSudoku variable, the verify 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

  1. Use snark.min.js file

     cp ./node_modules/snarkjs/build/snarkjs.min.js ./public
    

    Add <Script id="snarkjs" src="/snarkjs.min.js" /> in layout.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;

  1. 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.

  2. The contractsaddress.json file that contains all the necessary smart contracts addresses, in this case, the Sudoku 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 the pages folder.

    • The favicon.ico file, inside the public folder.

    • The vercel.svg file, inside the public 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 and socialMedia.png files, inside the public 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 and frontend as ROOT DIRECTORY) and click Deploy:

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 inside contracts/contracts.

    • sudoku.wasm and sudoku_final.zkey files inside contracts/zkproof.

    • sudoku.wasm and sudoku_final.zkey files inside frontend/public/zkproof.

  • If you change smart contracts you should update if necessary:

    • The Sudoku.json abi file inside frontend/utils/abiFiles.

    • The contractaddress.json file inside frontend/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 inside frontend/utils/networks.json.

  • To copy or update all the zk elements to use and test smart contracts, you can create the copyZkFiles.sh file inside Projects/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 inside Projects/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 .

0
Subscribe to my newsletter

Read articles from Deogracious Aggrey directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Deogracious Aggrey
Deogracious Aggrey