Building zk based web3 apps We will be doing walk through of a simple zk-dapp for age verification, that will allow users to prove that they are below a certain age without revealing their actual age. This is a fairly trivial problem to solve and may not necessarily require zero-knowledge proofs but to keep things simple and easy to understand we will go ahead with this idea. Subscribe here for all the cool zk-tech and web3 stuff! The overview of the architecture looks something like the below. We will be using a nextjs app to interact with the smart contract and take user input ( age) which is meant to be private and will not be published on-chain. The proof generation occurs on the frontend using . There are two smart contracts for this dapp, AgeCheck and Verifier: snarkjs takes proof inputs from the frontend app and maintains the mapping of addresses if they are verified or not. AgeCheck.sol has one purpose i.e. to verify the proof. Verifier.sol There are lots of different ways to generate zero-knowledge proofs and these are continuously evolving. There is a lot of advancement going in the zero-knowledge space. For the purpose of this dapp, we will be using SNARK proofs, more specifically Groth16. To generate zero-knowledge proof for our use case we need to define the problem statement in terms of circuits. In this case, our problem statement is verifying that the user's age ( age is private information) should be below a certain age limit ( age limit is public information). To create circuits we will be using library. circom The purpose of the circuit is to take the user’s age as input and check if that is above a certain age limit ( for eg. 18). The circuit is then compiled to create wasm which then can be used in the browser to generate proofs. The input to the circuit includes private and public inputs - in this case, user’s age is private input ( as it doesn’t need to be revealed) and ageLimit is public input ( known to everyone). These inputs are used in generating the witness ( which is a set of signals that satisfies the circuit) you can read more about them . here To summarize in short**, zk-SNARK proofs are a specific type of zero-knowledge proofs that allow you to prove that you know a set of signals i.e. witness that matches all the constraints of a circuit without revealing any of the signals except the public inputs and the outputs.** I know it sounds a bit confusing, but may in a later post I will simplify some math behind it and maybe a circuit primer. For the purpose of this dapp, let's proceed in order to understand how everything works in tandem. Now let’s dive into the code, we will be using boilerplate for zero-knowledge dapps, refer to repo. zk-block this Clone the repository. git clone https://github.com/heypran/zk-block.git Following along with code requires familiarity with react, ethers, and solidity. Note: We will start from the backend ( or if you look at the above diagram, from top to bottom). Here’s what we gonna do: Compile the circuit built using circom and auto-generate the verifier contract using snarkjs cd zk-block cd backend yarn install yarn compile:circuits This will use the circuit present in the folder and compile it using the script in If you know about zero-knowledge proofs, you will know that Groth16 protocol requires performing a trusted setup. This is done in two phases. Phase 1 is independent of the circuit and phase 2 is circuit dependent. You can read about them . backend/circuits scripts/compile-circuits.ts here : Please note that you need to perform MPC to generate trusted setup, do not use the setup in as-is for production. Caution compile-circuits.ts When you compile the circuit, it will create the necessary files in the folder which can be used in the repo to generate proofs. The above compilation also generates the in the folder. The is used to verify the proof on-chain. build/snarks frontend Verifier.sol contracts Verifier.sol Compile the contracts and deploy the smart contract to be able to use with our nextjs app Execute the following command inside the folder to compile the contracts background yarn compile This will compile the contracts in folder, which contains and ( which is generated in the above step). The compilation is done using hardhat configuration and the selected network in the , the compiled code is generated in the folder. backend/contracts AgeCheck.sol Verifier.sol hardhat.config.ts backend/build Deploy contracts to the network of your own choice (Optional) This step is not required, if you don’t want to deploy, you can simply use the existing testnet contract address, which is already configured inside and also mentioned in the github . Note: ui/src/config/constants.ts readme In order to deploy the compiled contract, you will have to create a backend/private.json with your private key inside it ( refer ) and change the selected network in to a network of your choice ( refer to view the networks). private.example.json hardhat.config.ts chain.ts Execute the below command to deploy- yarn deploy:agecheck --network testnet The deployed contract address will be displayed on the terminal, configure it inside ui/src/config/constants.ts Generating Proof from the frontend Now let’s move to the frontend, to understand how the proof is generated on the frontend. Execute the below commands. cd .. cd frontend yarn install yarn dev The above will run the zk-block frontend locally, visit to see the age verification in action. You may connect to network, Polygon, or Harmony ( or where you deployed the smart contract), using Metamask. http://localhost:3000/dapp Input the age in the box, and click verify. This will verify your age as above 18 using the smart contract without publishing your age on the blockchain. Behind the scenes, when the user inputs the age, it will generate the witness using public and private inputs ( ). refer to the below snippet from ui/src/utils/zk/zk-witness.ts The code is commented on for understanding. // read the wasm generated by compiling the circuit in STEP 1 const buffer = await getBinaryPromise(circuitWasm); // generate witness using calculator file generated from circom // and then passing the public and private input const witnessCalculator = await generateWitnessJs(buffer); const buff = await witnessCalculator.calculateWTNSBin(params, 0); const provingKey = await fetch(zkey); const provingKeyBuffer = await provingKey.arrayBuffer(); // Generate groth proof const { proof, publicSignals } = await snarkjs.groth16.prove( new Uint8Array(provingKeyBuffer), buff, null, ); // required to generate solidity call params const editedPublicSignals = unstringifyBigInts(publicSignals); const editedProof = unstringifyBigInts(proof); // Generate solidity compatible params for Verifier.sol const callData = await snarkjs.groth16.exportSolidityCallData( editedProof, editedPublicSignals, ); The are proof parameters generated to be able to verify the proof ( i.e. the age is indeed above a limit, in this case 18) defined using our deployed on-chain. calldata Verifier.sol You can play around with zk age verification . Now if you go through the architecture diagram, you will have a better understanding :). here Please note: This zkblock boilerplate is updated on a regular basis if you find that anything is not working as it suppose to be or you want to suggest any improvement, feel free to create an issue. Also, you can go through the . Any contributions are welcome. readme for all the cool zk-tech and web3 stuff! Subscribe here Happy Hacking! References: https://github.com/heypran/zk-block https://github.com/iden3/circom