Guide to building ZK Dapp with Noir on Scroll

Guide to building ZK Dapp with Noir on Scroll

Building dapps is cool but do you know building zkdapps is more thrilling, if you are ready to get some sugar rush while building circuits, join me to explore some intricacies about building Zk enabled dapps. At the end of the article we will have built a simple zkdapp for age verification.

If any of the terms sound strange to you don't worry you are in safe hands let's break it down bit by bits

Decentralised Applications(Dapps): These are computer programs built to run on a blockchain usually written in language such as Solidity for EVM chains, Rust for polkadot, solana etc.

Zkdapps: These are also decentralised applications that runs on the blockchain but they have more added functionalities by leveraging on zero knowledge primitives.

What exactly is Zero Knowledge Proof

Zero knowledge proof(ZKP) allows a party(prover) to assert a claim that they know a secret that satisfies some constraints without revealing anything more than the claim to the other entity (verifier). E.g I can claim to be above the minimum voting age without revealing my actual age. So in essence you can say ZK enables communication between systems without revealing more than is needed.

There are 3 components involve in this system
a. Prover: A party claiming to know the witness(the secret data).

b. Verifier: The party verifying the prover’s claim to know witness.

c. Witness: The secret value that is only known to the prover, which should not be shared with the verifier all through their interactions.

A valid Zero knowledge proof should satisfy the following conditions

  • Completeness : If the proof is valid, it must be accepted by the verifier.

  • Soundness: If the proof is invalid, it must be rejected by the verifier.

  • Zero-Knowledge: The verifier does not learn any information about the witness other than the assertion that the prover knows it.

ZKP's can be used to solve privacy and scalability challenges in blockchain, they can be applied in such a way that proof is generated for every block of transaction and instead of operators on the network verifying each transaction they verify the generated proof and this can make operations faster.

Introducing Scroll

Scroll is a Layer 2 Zk-Rollup built to address the scalability problems associated with Ethereum by being able to process more transactions at a very fast rate without sacrificing decentralisation or security. , it has bytecode compatibility with Ethereum Virtual machine(EVM) which means your existing workflow building on EVM chains can also be applied to Scroll. It also uses ETH as its native currency which will be used to pay for executing your transactions.

Configuration

We are going to be deploying on scroll sepolia for this tutorial if you haven't set it up yet, follow this details to add the network to your wallet

Network Name: Scroll Sepolia

RPC URL: sepolia-rpc.scroll.io

Chain ID: 534351

Currency Symbol: ETH

Block Explorer URL: sepolia.scrollscan.com

Faucet

After adding the network you can get faucet funds from here

Introducing Noir lang

Noir is an open-source, general purpose language for writing zero knowledge programs,Noir programs when compiled are deliberately proving system agnostic which makes it compatible with any proving backend system of your choice such as PLONK, Groth16, Marlin etc.

Noir was created to help new developers get started writing circuits easily without having to know about the deep internals of cryptography.

Noir utilizes a unique compilation method that doesn't directly produce circuits or constraints but instead generates an intermediate format known as Abstract Circuit Intermediate Representation (ACIR). Worthy of note here is the flexibility ACIR provides, after creating a program in ACIR, it can be transformed into constraints suitable for any cryptographic backend of choice.

This approach is similar to the structure of computer applications, which are divided into a front end and a back end. Noir serves as the front end with a Rust-inspired syntax, translating programs into the ACIR format. Subsequently, the back end processes ACIR to produce machine code tailored to specific computer architectures. This design enables us to be able to write the program in Noir and have the verifying program based on another architecture. In our tutorial we will write the circuit in Noir and the verification component will be in solidity which we will later deploy on scroll.

Installation

Open a terminal on your machine, and write:

curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
source ~/.bashrc

Close the terminal, open another one, and run

noirup

Done. That's it. You should have the latest version working. You can check with

nargo --version

Your output should be similar to this

Note: Since Noir is a Rust based DSL, you have access to some of rust functionalities here, Nargo is the equivalent of Cargo in rust which serves as the package manager, and you can also write your project configurations in Nargo.toml as you would in Cargo.toml for a rust program.

Vscode Extension: For syntax highlighting and language support you can install the Noirlang extension here

BUIDL TIME

Now that we have installed the needed tools we are ready to build our zk program, Our program will allow a user to generate proof that they are older than 21 years old without revealing their exact age to the public.

To initialise our project, run this in your terminal

nargo new zkAgeProof

This will generate the boiler plate of a noir project, open the folder zkAgeProof in your editor. You should see something like this

Nargo.toml holds the details and configurations of our project

main.nr is the entry point of your project and it contains a dummy project generated for us when we initialise the project. The code here accepts two inputs, for the constraints system to be satisfied, the two inputs shouldn't be the same. Variables are private by default in Noir, so to make them visible to others you have to add the pub keyword to the variable.

Now run

nargo check

to evaluate if the constraint system has any error, this will generate two(2) new files for you which are important for the next steps, this files are

  • Prover.toml This is where we will add the input variables needed by the constraints, your file should look like this at the moment.

  • Verifier.toml If you notice this file has only one variable y, this is because it's the only input marked public in our constraints.

Now add 2 different numbers of your choice to Prover.toml

nargo prove

Run in your terminal to generate the proofs for this system

Now we have successfully generated the proof for this constraint system using the values we sent in our Prover.toml . You can notice that the Verifier.toml was updated with the public input we passed in, so anytime a verifier wants to verify a claim they will need to pass in the public input and the proof to know if a claim is valid or not.

To get a detailed information of the circuits, type this to your terminal

nargo info

Now lets make some adjustment to the main.nr to include our logic for age proof.

use dep::std;

fn main(age: u32, birth_year: Field, ageHash: pub Field) {
    assert(2024 - age == birth_year as u32);
    assert(age >= 21);

    // Check integrity of birth year hash
    let computed_hash = std::hash::pedersen_commitment([birth_year]);
    assert(computed_hash.x == ageHash);
}

#[test]
fn test_main() {
    let birth_hash = std::hash::pedersen_commitment([2000]);
    std::println(birth_hash);
    main(24, 2000, birth_hash.x);
}

Replace the content with the following

We want this program to be used to generate proof that a certain person is above the age of 21 as at 2024, In order to generate the proof firstly we need to make use of noir standard library so we can make use of the functionality it provides.

The prover is required to pass in three (3) inputs their age, birth year and the hash of the age, since we don't want to reveal their current age and birth year to the public we will make those private, for the needed public input we require them to pass in the hash of their age and the circuits will evaluate the inputs to see that they satisfy the required conditions.

**Note:**If your zk program require hashing, its advised to use hash functions that has homomorphic properties like pedersen, poseidon etc. to enable generation of succinct and computationally efficient hash so as to prevent longer proving time of your system.

Now to test the functionality of our constraint system run this in your terminal

nargo test --show-output

you should have this in your console. Take note of pedersen.x it will be needed to generate the proof as that is the hash of the input age.

To generate our proof we need to make adjustments to Prover.toml to accommodate the required input, using what's in our test your Prover.toml should be like this

You can go ahead and generate the prove using the nargo prove

Verifier Contract

Now that we have generated proof for our claim, to get the verifier contract, run

nargo codegen-verifier

this will generate a solidity code for you in contract/zkAgeProof/plonk_vk.sol

Now using remix injected provider connected to scroll sepolia, deploy this contract and copy the address, so we can use it in our custom logic contract.

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

interface INoirVerifier {
    function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool);
}

contract AgeVerifier {
    INoirVerifier noirVerifier;
    uint public publicInput;

    constructor(address noirVeriferAddress) {
        noirVerifier = INoirVerifier(noirVeriferAddress);
    }

    function sendProof(bytes calldata _proof, bytes32[] calldata _publicInputs) public {
        // ZK verification
        noirVerifier.verify(_proof, _publicInputs);

        // Your custom logic
        publicInput = uint(_publicInputs[0]);
    }
}

To deploy this contract pass in the address of the first contract deployed above. Now call the sendproof function and pass in the proof from your proofs/zkAgeProof.proof and the public input from Verifier.sol

In order to verify a claim go to the deployed Ultraverifier contract and pass in the neccessary inputs to know if a claim is valid or not.

Summary

Now we have successfully built a zk program to prove that a person is above 21 without revealing their actual age in Noirlang, and deploy the verification and custom contract on scroll.
You can access the code here

Resources

Let’s Connect:
Twitter: @mr_abims
Linkedin: Abimbola Adebayo