Skip to main content
Version: dev

Generate a Solidity Verifier

Noir is universal. The witness and the compiled program can be fed into a proving backend such as Aztec's Barretenberg, which can then generate a verifier contract for deployment on blockchains.

This allows for a powerful feature set, as one can make use of the conciseness and the privacy provided by Noir in an immutable ledger. Applications can range from simple P2P guessing games, to complex private DeFi interactions.

Although not strictly in the domain of Noir itself, this guide shows how to generate a Solidity Verifier with Barretenberg and deploy it on the Remix IDE. It is assumed that:

  • You will be using Barretenberg as your proving backend
  • You will be using an EVM blockchain to verify your proof
  • You are comfortable with the Solidity programming language and understand how contracts are deployed on the Ethereum network
  • You have Noir installed and you have a Noir program. If you don't, get started with Nargo and the example Hello Noir circuit
  • You are comfortable navigating RemixIDE. If you aren't or you need a refresher, you can find some video tutorials here that could help you.

Rundown

Generating a Solidity Verifier with Barretenberg contract is actually a one-command process. However, compiling it and deploying it can have some caveats. Here's the rundown of this guide:

  1. How to generate a solidity smart contract
  2. How to compile the smart contract in the RemixIDE
  3. How to deploy it to a testnet
Which proving system to use?

Barretenberg currently provides two provers: UltraPlonk and UltraHonk. In a nutshell, UltraHonk is faster and uses less RAM, but its verifier contract is much more expensive. UltraPlonk is optimized for on-chain verification, but proving is more expensive.

In any case, we provide instructions for both. Choose your poison ☠️

Step 1 - Generate a contract

This is by far the most straightforward step. Just run:

nargo compile

This will compile your source code into a Noir build artifact to be stored in the ./target directory. From here on, it's Barretenberg's work. You can generate the smart contract using the commands:

bb write_vk_ultra_keccak_honk -b ./target/<noir_artifact_name>.json
bb contract_ultra_honk

replacing <noir_artifact_name> with the name of your Noir project. A Verifier.sol contract is now in the target folder and can be deployed to any EVM blockchain acting as a verifier smart contract.

You can find more information about bb and the default Noir proving backend on this page.

Step 2 - Compiling

We will mostly skip the details of RemixIDE, as the UI can change from version to version. For now, we can just open Remix and create a blank workspace.

Create Workspace

We will create a new file to contain the contract Nargo generated, and copy-paste its content.

warning

You'll likely see a warning advising you to not trust pasted code. While it is an important warning, it is irrelevant in the context of this guide and can be ignored. We will not be deploying anywhere near a mainnet.

To compile our the verifier, we can navigate to the compilation tab:

Compilation Tab

Remix should automatically match a suitable compiler version. However, hitting the "Compile" button will most likely tell you the contract is too big to deploy on mainnet, or complain about a stack too deep:

Contract code too big Stack too deep

To avoid this, you can just use some optimization. Open the "Advanced Configurations" tab and enable optimization. The default 200 runs will suffice.

Compilation success

Step 3 - Deploying

At this point we should have a compiled contract ready to deploy. If we navigate to the deploy section in Remix, we will see many different environments we can deploy to. The steps to deploy on each environment would be out-of-scope for this guide, so we will just use the default Remix VM.

Looking closely, we will notice that our "Solidity Verifier" is composed on multiple contracts working together. Remix will take care of the dependencies for us so we can simply deploy the Verifier contract by selecting it and hitting "deploy":

Deploying HonkVerifier

A contract will show up in the "Deployed Contracts" section.

Step 4 - Verifying

To verify a proof using the Solidity verifier contract, we call the verify function:

function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool)

First generate a proof with bb. We need a Prover.toml file for our inputs. Run:

nargo check

This will generate a Prover.toml you can fill with the values you want to prove. We can now execute the circuit with nargo and then use the proving backend to prove:

nargo execute <witness-name>
bb prove_ultra_keccak_honk -b ./target/<circuit-name>.json -w ./target/<witness-name> -o ./target/proof
Public inputs

Barretenberg attaches the public inputs to the proof, which in this case it's not very useful. If you're up for some JS, bb.js has a method for it, but in the CLI you can use this ugly snippet:

cat ./target/proof | od -An -v -t x1 | tr -d $' \n' | sed 's/^.\{8\}//' | (read hex; echo "${hex:0:192}${hex:256}")

Beautiful. This assumes a circuit with one public input (32 bytes, for Barretenberg). For more inputs, you can just increment hex:256 with 32 more bytes for each public input.

Remix expects that the public inputs will be split into an array of bytes32 values so HEX_PUBLIC_INPUTS needs to be split up into 32 byte chunks which are prefixed with 0x accordingly.

A programmatic example of how the verify function is called can be seen in the example zk voting application here:

function castVote(bytes calldata proof, uint proposalId, uint vote, bytes32 nullifierHash) public returns (bool) {
// ...
bytes32[] memory publicInputs = new bytes32[](4);
publicInputs[0] = merkleRoot;
publicInputs[1] = bytes32(proposalId);
publicInputs[2] = bytes32(vote);
publicInputs[3] = nullifierHash;
require(verifier.verify(proof, publicInputs), "Invalid proof");
Return Values

A circuit doesn't have the concept of a return value. Return values are just syntactic sugar in Noir.

Under the hood, the return value is passed as an input to the circuit and is checked at the end of the circuit program.

For example, if you have Noir program like this:

fn main(
// Public inputs
pubkey_x: pub Field,
pubkey_y: pub Field,
// Private inputs
priv_key: Field,
) -> pub Field

the verify function will expect the public inputs array (second function parameter) to be of length 3, the two inputs and the return value.

Passing only two inputs will result in an error such as PUBLIC_INPUT_COUNT_INVALID(3, 2).

In this case, the inputs parameter to verify would be an array ordered as [pubkey_x, pubkey_y, return].

Structs

You can pass structs to the verifier contract. They will be flattened so that the array of inputs is 1-dimensional array.

For example, consider the following program:

struct Type1 {
val1: Field,
val2: Field,
}

struct Nested {
t1: Type1,
is_true: bool,
}

fn main(x: pub Field, nested: pub Nested, y: pub Field) {
//...
}

The order of these inputs would be flattened to: [x, nested.t1.val1, nested.t1.val2, nested.is_true, y]

The other function you can call is our entrypoint verify function, as defined above.

tip

It's worth noticing that the verify function is actually a view function. A view function does not alter the blockchain state, so it doesn't need to be distributed (i.e. it will run only on the executing node), and therefore doesn't cost any gas.

This can be particularly useful in some situations. If Alice generated a proof and wants Bob to verify its correctness, Bob doesn't need to run Nargo, NoirJS, or any Noir specific infrastructure. He can simply make a call to the blockchain with the proof and verify it is correct without paying any gas.

It would be incorrect to say that a Noir proof verification costs any gas at all. However, most of the time the result of verify is used to modify state (for example, to update a balance, a game state, etc). In that case the whole network needs to execute it, which does incur gas costs (calldata and execution, but not storage).

A Note on EVM chains

Noir proof verification requires the ecMul, ecAdd and ecPairing precompiles. Not all EVM chains support EC Pairings, notably some of the ZK-EVMs. This means that you won't be able to use the verifier contract in all of them. You can find an incomplete list of which EVM chains support these precompiles here.

For example, chains like zkSync ERA and Polygon zkEVM do not currently support these precompiles, so proof verification via Solidity verifier contracts won't work. Here's a quick list of EVM chains that have been tested and are known to work:

  • Optimism
  • Arbitrum
  • Polygon PoS
  • Scroll
  • Celo
  • BSC
  • Blast L2
  • Avalanche C-Chain
  • Mode
  • Linea
  • Moonbeam

If you test any other chains, please open a PR on this page to update the list. See this doc for more info about testing verifier contracts on different EVM chains.

What's next

Now that you know how to call a Noir Solidity Verifier on a smart contract using Remix, you should be comfortable with using it with some programmatic frameworks, such as hardhat and foundry.

You can find other tools, examples, boilerplates and libraries in the awesome-noir repository.

You should also be ready to write and deploy your first NoirJS app and start generating proofs on websites, phones, and NodeJS environments! Head on to the NoirJS tutorial to learn how to do that.