5/5
## Generating a Solidity Verifier for Your ZK Circuit with Nargo and Barretenberg This lesson walks you through the essential process of generating a Solidity smart contract capable of verifying zero-knowledge proofs for a custom circuit. We'll be using the Nargo toolchain for Noir circuit compilation and the Barretenberg (`bb`) backend for generating the verification key and the verifier contract. The ultimate goal is to produce a `Verifier.sol` file ready for deployment and integration into your decentralized applications. ### Step 1: Compile Your Noir ZK Circuit The journey begins with your Zero-Knowledge circuit, likely written in Noir (e.g., in a `circuits/src/main.nr` file). The first step is to compile this circuit into an intermediate representation that the backend tools can understand. **Command:** To compile your Noir circuit, navigate to your project directory in the terminal and execute: ```bash nargo compile ``` **Addressing Compiler Warnings (Optional but Recommended):** During compilation, you might encounter warnings. For instance, the compiler might flag an unused variable: `warning: unused variable recipient_binding src/main.nr:26:9` While such warnings might not halt compilation, it's good practice to address them for cleaner code and to avoid potential oversights. In the example scenario, a variable `recipient_binding` was declared but not used: ```noir // In fn main() let recipient_binding: Field = recipient * recipient; ``` To silence this warning, the variable was subsequently used in a trivial assertion: ```noir // In fn main(), after the let recipient_binding line assert recipient_binding == recipient * recipient; ``` It's important to note that this specific assertion adds a minor overhead to the on-chain proof verification cost. In a production scenario, you would either find a meaningful use for the variable or remove it entirely. **Re-compilation and Output:** After making any necessary code changes (like addressing warnings), it's a good idea to remove the previous build artifacts to ensure a clean compilation. You can delete the `target` directory and then re-run the compile command: ```bash rm -rf target/ nargo compile ``` Successful compilation will create a `target` directory if it doesn't already exist. Inside this directory, you'll find a crucial file: `circuits.json` (e.g., `target/circuits.json`). This JSON file contains the Arithmetic Circuit Intermediate Representation (ACIR) of your compiled circuit, which is a standardized format describing the circuit's mathematical structure. ### Step 2: Generate the Verification Key (VK) With the compiled circuit (ACIR) in hand, the next step is to generate a Verification Key (VK). The VK is a piece of cryptographic data specifically tailored to your circuit. The on-chain Verifier Smart Contract will use this VK to validate proofs. **Command:** To generate the VK using the Barretenberg backend, run the following command: ```bash bb write_vk --oracle_hash keccak -b ./target/circuits.json -o ./target ``` **Command Breakdown:** * `bb`: This invokes the Barretenberg backend command-line tool. * `write_vk`: This specific subcommand instructs Barretenberg to generate and write a verification key. * `--oracle_hash keccak`: This is a critical flag for Ethereum Virtual Machine (EVM) compatibility. It specifies that Keccak256 hashing should be used for any internal cryptographic operations or oracle calls that need to align with Ethereum's native hashing algorithm. This ensures efficiency and correctness when verifying proofs on-chain. * `-b ./target/circuits.json`: The `-b` flag points to the bytecode of your compiled circuit, which is the `circuits.json` file generated in the previous step. * `-o ./target`: The `-o` flag specifies the output directory where the generated verification key will be saved. **Output:** This command will create a file named `vk` (with no file extension) inside the `target` directory (i.e., `target/vk`). The terminal output from this command might also provide information like: "Scheme is: ultra_honk" "Finalized circuit size: 1814" This indicates that "UltraHonk" is the ZK proving system being used for this circuit. The circuit size gives an idea of its complexity. ### Step 3: Generate the Verifier Smart Contract (Solidity) Now that you have the verification key, you can generate the Solidity smart contract that will perform the on-chain proof verification. **Command:** Use the following Barretenberg command: ```bash bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol ``` **Command Breakdown:** * `bb write_solidity_verifier`: This command tells Barretenberg to generate the Solidity verifier contract. * `-k ./target/vk`: The `-k` flag specifies the path to the verification key file (`target/vk`) created in the previous step. * `-o ./target/Verifier.sol`: The `-o` flag defines the output path and filename for the generated Solidity contract. Here, it will be saved as `Verifier.sol` in the `target` directory. **Output:** This command generates the `Verifier.sol` file. The terminal output will confirm the action: "Scheme is: ultra_honk" "Solidity verifier saved to './target/Verifier.sol'" ### Step 4: Organizing and Understanding the Generated Verifier Contract It's common practice to organize your project files. You'll likely want to move the generated `Verifier.sol` from the `target` directory (which is typically for build artifacts) into your main contracts source directory, for example, `contracts/src/`. **Command (Example for moving the file):** ```bash mv ./target/Verifier.sol ./contracts/src/ ``` **Inside `Verifier.sol`:** The generated `Verifier.sol` contract contains all the necessary cryptographic logic to verify proofs for your specific ZK circuit. It will typically include: * SPDX license identifier and pragma statement. * Constants related to the circuit's structure and the proving system. For example: ```solidity // SPDX-License-Identifier: Apache-2.0 // Copyright 2022 Aztec pragma solidity >0.8.21; uint256 constant N = 2048; // Relates to circuit size/polynomial degree uint256 constant LOG_N_OF_PUBLIC_INPUTS = 3; uint256 constant NUMBER_OF_PUBLIC_INPUTS = 3; // The number of public inputs your circuit expects library HonkVerificationKey { // ... struct Honk.VerificationKey and other cryptographic data ... } // ... other internal functions and the main verification logic ... ``` The `NUMBER_OF_PUBLIC_INPUTS` constant is particularly important, as it defines how many public inputs the `verify` function will expect. * A public `verify` function (or a similarly named function). This function will take the proof bytes and the public inputs as arguments. It returns `true` if the proof is valid for the given public inputs and the circuit associated with the VK embedded in the contract; otherwise, it will return `false` or revert the transaction. ### Step 5: Integrating the Verifier into Your dApp (Example) The generated `Verifier.sol` is designed to be used by other smart contracts. For instance, you might have a primary application contract (e.g., a `Mixer.sol` contract) that needs to verify ZK proofs before performing certain actions. Here's a conceptual snippet of how `Mixer.sol` might import and use the `Verifier.sol`: ```solidity // In Mixer.sol import "./Verifier.sol"; // Adjust path as necessary contract Mixer { IVerifier public immutable i_verifier; // IVerifier is often an interface defined in or alongside Verifier.sol constructor(IVerifier _verifierAddress) { i_verifier = _verifierAddress; // Store the deployed Verifier contract's address } function withdraw( bytes calldata _proof, bytes32 _root, // Example public input bytes32 _nullifierHash, // Example public input address payable _recipient // Example public input for the Noir circuit // Note: The public inputs must match what Verifier.sol expects ) public { // Construct the public inputs array for the verifier // The order and type must exactly match the circuit's public inputs bytes32[] memory publicInputs = new bytes32[](NUMBER_OF_PUBLIC_INPUTS); publicInputs[0] = _root; publicInputs[1] = _nullifierHash; publicInputs[2] = bytes32(uint256(uint160(_recipient))); // Ensure correct casting for addresses require(i_verifier.verify(_proof, publicInputs), "Proof verification failed"); // ... rest of the withdraw logic ... } } ``` In this example, the `Mixer` contract's constructor receives the address of a deployed `Verifier` contract. The `withdraw` function then calls the `verify` method on this `i_verifier` instance, passing the proof and the necessary public inputs. ### Key Takeaways * **EVM Compatibility is Key:** Always use the `--oracle_hash keccak` flag when generating the verification key with `bb write_vk` to ensure compatibility and efficiency on EVM-based chains. * **Clean Compilation:** Address compiler warnings in your Noir code to maintain code quality and avoid potential issues. * **Build Artifacts Management:** The `target` directory houses intermediate build files. Your final contracts should be moved to a dedicated source directory. * **Proving System Awareness:** The tools will indicate the ZK proving system used (e.g., UltraHonk). While often abstracted, different systems can have different performance characteristics and verifier structures. * **Public Inputs Alignment:** The public inputs passed to the `verify` function in your Solidity contract must exactly match the number, order, and types defined in your Noir circuit and reflected in the `Verifier.sol` constants. By following these steps, you can successfully generate and integrate an on-chain verifier for your ZK circuits, enabling privacy-preserving and computationally verifiable applications on the blockchain. This process is fundamental to deploying practical ZK-SNARK solutions.
A foundational breakdown of Generating a Solidity Verifier for Your ZK Circuit with Nargo and Barretenberg - Step through compiling Noir circuits with Nargo and using Barretenberg to produce both verification keys and the crucial `Verifier.sol` contract. This lesson paves the way for deploying smart contracts that verify zero-knowledge proofs directly on the blockchain.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025
Duration: 6min
Duration: 1h 11min
Duration: 2h 12min
Duration: 3h 19min
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025