The Noir Programming Language

This version of the book is being released with the public alpha. There will be a lot of features that are missing in this version, however the syntax and the feel of the language will mostly be completed.

What is Noir?

Noir is a domain specific language for creating and verifying proofs. It's design choices are influenced heavily by Rust.

What's new about Noir?

Noir is much more simple and flexible in design as it does not compile immediately to a fixed NP-complete language. Instead Noir compiles to an intermediate language which itself can be compiled to an arithmetic circuit or a rank-1 constraint system. This in itself brings up a few challenges within the design process, but allows one to decouple the programming language completely from the backend. This is similar in theory to LLVM.

Who is Noir for?

Noir can be used for a variety of purposes.

Ethereum Developers

Noir currently includes a command to publish a contract which verifies your Noir program. This will be modularised in the future, however as of the alpha you can use the contract command to create it.

Protocol Developers

As a protocol developer, you may not want to use the Aztec backend due to it not being a fit for your stack or maybe you simply want to use a different proving system. Since Noir does not compile to a specific proof system, it is possible for protocol developers to replace the PLONK based proving system with a different proving system altogether.

Blockchain developers

As a blockchain developer, you will be constrained by parameters set by your blockchain, ie the proving system and smart contract language has been pre-defined. In order for you to use Noir in your blockchain, a proving system backend must be implemented for it and a smart contract interface must be implemented for it.

Getting Started

In this section we will discuss, installing Noir and running your first program.

Nargo

nargo is a command line tool for interacting with Noir programs (e.g. compiling, proving, verifying and more).

Alternatively, the interactions can also be performed in TypeScript.

Installation

There are two approaches to install Nargo:

Optionally you can also install Noir VS Code extension for syntax highlighting.

Option 1: Binaries

Step 1

Paste and run the following in the terminal to extract and install the binary:

macOS / Linux: If you are prompted with Permission denied when running commands, prepend sudo and re-run it.

macOS (Apple Silicon)

mkdir -p $HOME/.nargo/bin && \
curl -o $HOME/.nargo/bin/nargo-aarch64-apple-darwin.tar.gz -L https://github.com/noir-lang/noir/releases/download/nightly/nargo-aarch64-apple-darwin.tar.gz && \
tar -xvf $HOME/.nargo/bin/nargo-aarch64-apple-darwin.tar.gz -C $HOME/.nargo/bin/ && \
echo '\nexport PATH=$PATH:$HOME/.nargo/bin' >> ~/.zshrc && \
source ~/.zshrc

macOS (Intel)

mkdir -p $HOME/.nargo/bin && \
curl -o $HOME/.nargo/bin/nargo-x86_64-apple-darwin.tar.gz -L https://github.com/noir-lang/noir/releases/download/nightly/nargo-x86_64-apple-darwin.tar.gz && \
tar -xvf $HOME/.nargo/bin/nargo-x86_64-apple-darwin.tar.gz -C $HOME/.nargo/bin/ && \
echo '\nexport PATH=$PATH:$HOME/.nargo/bin' >> ~/.zshrc && \
source ~/.zshrc

Windows (PowerShell)

Open PowerShell as Administrator and run:

mkdir -f -p "$env:USERPROFILE\.nargo\bin\"; `
Invoke-RestMethod -Method Get -Uri https://github.com/noir-lang/noir/releases/download/nightly/nargo-x86_64-pc-windows-msvc.zip -Outfile "$env:USERPROFILE\.nargo\bin\nargo-x86_64-pc-windows-msvc.zip"; `
Expand-Archive -Path "$env:USERPROFILE\.nargo\bin\nargo-x86_64-pc-windows-msvc.zip" -DestinationPath "$env:USERPROFILE\.nargo\bin\"; `
$Reg = "Registry::HKLM\System\CurrentControlSet\Control\Session Manager\Environment"; `
$OldPath = (Get-ItemProperty -Path "$Reg" -Name PATH).Path; `
$NewPath = $OldPath + ’;’ + "$env:USERPROFILE\.nargo\bin\"; `
Set-ItemProperty -Path "$Reg" -Name PATH –Value "$NewPath"; `
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")

Linux (Bash)

See GitHub Releases for additional platform specific binaries.

mkdir -p $HOME/.nargo/bin && \
curl -o $HOME/.nargo/bin/nargo-x86_64-unknown-linux-gnu.tar.gz -L https://github.com/noir-lang/noir/releases/download/nightly/nargo-x86_64-unknown-linux-gnu.tar.gz && \
tar -xvf $HOME/.nargo/bin/nargo-x86_64-unknown-linux-gnu.tar.gz -C $HOME/.nargo/bin/ && \
echo '\nexport PATH=$PATH:$HOME/.nargo/bin' >> ~/.bashrc && \
source ~/.bashrc

Step 2

Check if the installation was successful by running nargo --help.

macOS: If you are prompted with an OS alert, right-click and open the nargo executable from Finder. Close the new terminal popped up and nargo should now be accessible.

For a successful installation, you should see something similar to the following after running the command:

$ nargo --help
Noir's package manager

Usage: nargo <COMMAND>

Commands:
  check             Checks the constraint system for errors
  codegen-verifier  Generates a Solidity verifier smart contract for the program
  compile           Compile the program and its secret execution trace into ACIR format
  new               Create a new binary project
  execute           Executes a circuit to calculate its return value
  prove             Create proof for this program. The proof is returned as a hex encoded string
  verify            Given a proof and a program, verify whether the proof is valid
  test              Run the tests for this program
  gates             Counts the occurrences of different gates in circuit
  help              Print this message or the help of the given subcommand(s)

Option 2: Compile from Source

Setup

  1. Install Git and Rust.

  2. Download Noir's source code from Github by running:

    git clone [email protected]:noir-lang/noir.git
    
  3. Change directory into the Noir project by running:

    cd noir
    

There are then two approaches to proceed, differing in how the proving backend is installed:

Option 2.1: Install Executable with WASM backend

  1. Install Nargo by running:

    cargo install --locked --path=crates/nargo --no-default-features --features plonk_bn254_wasm
    

Option 2.2: Install Executable with Native Backend

The barretenberg proving backend is written in C++, hence compiling it from source would first require certain dependencies to be installed.

  1. Install CMake, LLVM and OpenMP:

    macOS

    Installing through Homebrew is recommended:

    brew install cmake llvm libomp
    

    Ubuntu (Linux)

    sudo apt update && sudo apt install clang lld cmake libomp-dev
    

    Other variants of Linux will need to adjust the commands for their package manager.

    Windows

    TBC
    
  2. Install Nargo by running:

    cargo install --locked --path=crates/nargo
    

Verify Installation

  1. Check if the installation was successful by running nargo --help:

    $ nargo --help
    Noir's package manager
    
    Usage: nargo <COMMAND>
    
    Commands:
      check             Checks the constraint system for errors
      codegen-verifier  Generates a Solidity verifier smart contract for the program
      compile           Compile the program and its secret execution trace into ACIR format
      new               Create a new binary project
      execute           Executes a circuit to calculate its return value
      prove             Create proof for this program. The proof is returned as a hex encoded string
      verify            Given a proof and a program, verify whether the proof is valid
      test              Run the tests for this program
      gates             Counts the occurrences of different gates in circuit
      help              Print this message or the help of the given subcommand(s)
    

Commands

nargo help [subcommand]

Prints the list of available commands or specific information of a subcommand.

Arguments

  • <subcommand> - The subcommand whose help message to display

nargo new <package_name> [path]

Creates a new Noir project.

Arguments

  • <package_name> - Name of the package
  • [path] - The path to save the new project

nargo check

Generate the Prover.toml and Verifier.toml files for specifying prover and verifier in/output values of the Noir program respectively.

nargo execute

Runs the Noir program and prints its return value.

  • <witness_name> - The name of the witness

nargo prove <proof_name>

Creates a proof for the program.

Arguments

  • <proof_name> - The name of the proof

nargo verify <proof>

Given a proof and a program, verify whether the proof is valid.

Arguments

  • <proof> - The proof to verify

nargo codegen-verifier

Generate a Solidity verifier smart contract for the program.

nargo preprocess <build_artifact>

Generate proving and verification keys from a build artifact file.

nargo compile <circuit_name>

Compile the program and its secret execution trace into a JSON build artifact file containing the ACIR representation, and the ABI of the circuit. This build artifact can then be used to generate and verify proofs.

Arguments

  • <circuit_name> - The name of the circuit file

The files compiled can be passed into a TypeScript project for proving and verification. See the TypeScript section to learn more.

Hello, World

Now that we have installed Nargo, it is time to make our first hello world program!

Create a Project Directory

Noir code can live anywhere on your computer. Let us create a projects folder in the home directory to house our Noir programs.

For Linux, macOS, and Windows PowerShell, create the directory and change directory into it by running:

$ mkdir ~/projects
$ cd ~/projects

For Windwows CMD, run:

> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"

Create Our First Nargo Project

Now that we are in the projects directory, create a new Nargo project by running:

$ nargo new hello_world

Note: hello_world can be any arbitrary project name, we are simply using hello_world for demonstration.

In production, the common practice is to name the project folder as circuits for better identifiability when sitting alongside other folders in the codebase (e.g. contracts, scripts, test).

A hello_world folder would be created. Similar to Rust, the folder houses src/main.nr and Nargo.toml that contains the source code and environmental options of your Noir program respectively.

Intro to Noir Syntax

Let us take a closer look at main.nr. The default main.nr generated should look like this:

fn main(x : Field, y : pub Field) {
    constrain x != y;
}

The first line of the program specifies the program's inputs:


#![allow(unused)]
fn main() {
x : Field, y : pub Field
}

Program inputs in Noir are private by default (e.g. x), but can be labeled public using the keyword pub (e.g. y). To learn more about private and public values, check the Data Types section.

The next line of the program specifies its body:


#![allow(unused)]
fn main() {
constrain x != y;
}

The Noir syntax constrain can be interpreted as something similar to assert in other languages.

For more Noir syntax, check the Language Concepts chapter.

Build In/Output Files

Change directory into hello_world and build in/output files for your Noir program by running:

$ cd hello_world
$ nargo check

Two additional files would be generated in your project directory:

Prover.toml houses input values, and Verifier.toml houses public values.

Prove Our Noir Program

Now that the project is set up, we can create a proof of correct execution on our Noir program.

Fill in input values for execution in the Prover.toml file. For example:

x = "1"
y = "2"

Prove the valid execution of your Noir program with your preferred proof name, for example p:

$ nargo prove p

A new folder proofs would then be generated in your project directory, containing the proof file p.proof.

The Verifier.toml file would also be updated with the public values computed from program execution (in this case the value of y):

y = "0x0000000000000000000000000000000000000000000000000000000000000002"

Note: Values in Verifier.toml are computed as 32-byte hex values.

Verify Our Noir Program

Once a proof is generated, we can verify correct execution of our Noir program by verifying the proof file.

Verify your proof of name p by running:

$ nargo verify p

The verification will complete in silence if it is successful. If it fails, it will log the corresponding error instead.

Congratulations, you have now created and verified a proof for your very first Noir program!

In the next section, we will go into more detail on each step performed.

Breakdown

This section breaks down our hello world program in section 1.2. We elaborate on the project structure and what the prove and verify commands did in the previous section.

Anatomy of a Nargo Project

Upon creating a new project with nargo new and building the in/output files with nargo check commands, you would get a minimal Nargo project of the following structure:

- src
- Prover.toml
- Verifier.toml
- Nargo.toml

The source directory src holds the source code for your Noir program. By default only a main.nr file will be generated within it.

Prover.toml is used for specifying the input values for executing and proving the program. Optionally you may specify expected output values for prove-time checking as well.

Verifier.toml contains public in/output values computed when executing the Noir program.

Nargo.toml contains the environmental options of your project.

proofs and contract directories will not be immediately visible until you create a proof or verifier contract respectively.

main.nr

The main.nr file contains a main method, this method is the entry point into your Noir program.

In our sample program, main.nr looks like this:

fn main(x : Field, y : Field) {
    constrain x != y;
}

The parameters x and y can be seen as the API for the program and must be supplied by the prover. Since neither x nor y is marked as public, the verifier does not supply any inputs, when verifying the proof.

The prover supplies the values for x and y in the Prover.toml file.

As for the program body, constrain ensures the satisfaction of the condition (e.g. x != y) is constrained by the proof of the execution of said program (i.e. if the condition was not met, the verifier would reject the proof as an invalid proof).

Prover.toml

The Prover.toml file is a file which the prover uses to supply his witness values(both private and public).

In our hello world program the Prover.toml file looks like this:

x = "1"
y = "2"

When the command nargo prove my_proof is executed, two processes happen:

  1. Noir creates a proof that x which holds the value of 1 and y which holds the value of 2 is not equal. This not equal constraint is due to the line constrain x != y.

  2. Noir creates and stores the proof of this statement in the proofs directory and names the proof file my_proof. Opening this file will display the proof in hex format.

Verifying a Proof

When the command nargo verify my_proof is executed, two processes happen:

  1. Noir checks in the proofs directory for a file called my_proof

  2. If that file is found, the proof's validity is checked

Note: The validity of the proof is linked to the current Noir program; if the program is changed and the verifier verifies the proof, it will fail because the proof is not valid for the modified Noir program.

In production, the prover and the verifier are usually two separate entities. A prover would retrieve the necessary inputs, execute the Noir program, generate a proof and pass it to the verifier. The verifier would then retrieve the public inputs from usually external sources and verifies the validity of the proof against it.

Take a private asset transfer as an example:

A user on browser as the prover would retrieve private inputs (e.g. the user's private key) and public inputs (e.g. the user's encrypted balance on-chain), compute the transfer, generate a proof and submit it to the verifier smart contract.

The verifier contract would then draw the user's encrypted balance directly from the blockchain and verify the proof submitted against it. If the verification passes, additional functions in the verifier contract could trigger (e.g. approve the asset transfer).

Solidity Verifier

For certain applications, it may be desirable to run the verifier as a smart contract instead of on a local machine.

Compile a Solidity verifier contract for your Noir program by running:

nargo codegen-verifier

A new contract folder would then be generated in your project directory, containing the Solidity file plonk_vk.sol. It can be deployed on any EVM blockchain acting as a verifier smart contract.

Note: It is possible to compile verifier contracts of Noir programs for other smart contract platforms as long as the proving backend supplies an implementation.

Barretenberg, the default proving backend Nargo is integrated with, supports compilation of verifier contracts in Solidity only for the time being.

TypeScript

Interactions with Noir programs can also be performed in TypeScript, which can come in handy when writing tests or when working in TypeScript-based projects like Hardhat.

The following sections use the Standard Noir Example as an example to dissect a typical Noir workflow in TypeScript, with specific focus on its:

You are also welcome to revisit the full scripts 1_mul.ts of Standard Noir Example and mm.ts of Mastermind in Noir anytime for inspirations.

Setup

Install Yarn and Node.js.

Install Noir dependencies in your project by running:

$ yarn add @noir-lang/noir_wasm @noir-lang/barretenberg @noir-lang/aztec_backend

And import the applicable functions into your TypeScript file by adding:

// 1_mul.ts
import { compile, acir_read_bytes } from '@noir-lang/noir_wasm';
import { setup_generic_prover_and_verifier, create_proof, verify_proof, create_proof_with_witness } from '@noir-lang/barretenberg/dest/client_proofs';
import { packed_witness_to_witness, serialise_public_inputs, compute_witnesses } from '@noir-lang/aztec_backend';

Compiling

To begin proving and verifying a Noir program, it first needs to be compiled by calling noir_wasm's compile function:

// 1_mul.ts
const compiled_program = compile(path.resolve(__dirname, "../circuits/src/main.nr"));

The compiled_program returned by the function contains the ACIR and the Application Binary Interface (ABI) of your Noir program. They shall be stored for proving your program later:

// 1_mul.ts
let acir = compiled_program.circuit;
const abi = compiled_program.abi;

Note: Compiling with noir_wasm may lack some of the newer features that nargo compile offers. See the Proving and Verifying Externally Compiled Files section to learn more.

Specifying Inputs

Having obtained the compiled program, the program inputs shall then be specified in its ABI.

Standard Noir Example is a program that multiplies input x with input y and returns the result:

# main.nr
fn main(x: u32, y: pub u32) -> pub u32 {
    let z = x * y;
    z
}

Hence, one valid scenario for proving could be x = 3, y = 4 and return = 12:

// 1_mul.ts
abi.x = 3;
abi.y = 4;
abi.return = 12;

Info: Return values are also required to be specified, as they are merely syntax sugar of inputs with equality constraints.

Tip: To best protect the private inputs in your program (if applicable) from public knowledge, you should consider minimizing any passing around of inputs and deleting the inputs on the prover instance once the proof is created when designing your program.

Initializing Prover & Verifier

Prior to proving and verifying, the prover and verifier have to first be initialized by calling barretenberg's setup_generic_prover_and_verifier with your Noir program's ACIR:

// 1_mul.ts
let [prover, verifier] = await setup_generic_prover_and_verifier(acir);

Proving

The Noir program can then be executed and proved by calling barretenberg's create_proof function:

// 1_mul.ts
const proof = await create_proof(prover, acir, abi);

Verifying

The proof obtained can be verified by calling barretenberg's verify_proof function:

// 1_mul.ts
const verified = await verify_proof(verifier, proof);

The function should return true if the entire process is working as intended, which can be asserted if you are writing a test script:

// 1_mul.ts
expect(verified).eq(true);

Verifying with Smart Contract

Alternatively, a verifier smart contract can be generated and used for verifying Noir proofs in TypeScript as well.

This could be useful if the Noir program is designed to be decentrally verified and/or make use of decentralized states and logics that is handled at the smart contract level.

To generate the verifier smart contract:

// generate_sol_verifier.ts

// Imports
import { writeFileSync } from 'fs';

...

// Generate verifier contract
const sc = verifier.SmartContract();
syncWriteFile("../contracts/plonk_vk.sol", sc);

...

function syncWriteFile(filename: string, data: any) {
    writeFileSync(join(__dirname, filename), data, {
      flag: 'w',
    });
}

To verify a Noir proof using the verifier contract:

// 1_mul.ts

// Imports
import { ethers } from "hardhat";
import { Contract, ContractFactory, utils } from 'ethers';

...

// Deploy verifier contract
let Verifier: ContractFactory;
let verifierContract: Contract;

before(async () => {
    Verifier = await ethers.getContractFactory("TurboVerifier");
    verifierContract = await Verifier.deploy();
});

...

// Verify proof
const sc_verified = await verifierContract.verify(proof);
expect(sc_verified).eq(true)

Proving and Verifying Externally Compiled Files

In some cases, noir_wasm may lack some of the newer features for compiling Noir programs due to separated upgrade workflows.

To benefit from the best of both worlds, a Noir program can be compiled with nargo compile, with the .acir and .tr files then passed into your TypeScript project for proving and verifying:

// 1_mul.ts

// Parse acir
let acirByteArray = path_to_uint8array(path.resolve(__dirname, '../circuits/build/p.acir'));
let acir = acir_read_bytes(acirByteArray);

// Parse witness
let witnessByteArray = path_to_uint8array(path.resolve(__dirname, '../circuits/build/p.tr'));
const barretenberg_witness_arr = await packed_witness_to_witness(acir, witnessByteArray);

...

// Create proof
const proof = await create_proof_with_witness(prover, barretenberg_witness_arr);

Info: The .acir file is the ACIR of your Noir program, and the .tr file is the witness file. The witness file can be considered as program inputs parsed for your program's ACIR.

See the Commands section to learn more about the nargo compile command.

Merkle Proof

Let's walk through an example of a merkle membership proof in Noir that proves that a given leaf is in a merkle tree.

use dep::std;

fn main(message : [Field; 62], index : Field, hashpath : [Field; 40], root : Field) {
    let leaf = std::hash::hash_to_field(message);
    let is_member = std::merkle::check_membership(root, leaf, index, hashpath);
    constrain is_member == 1;
}

The above code uses the noir standard library to call both of the aforementioned components.

   let leaf = std::hash::hash_to_field(message);

The message is hashed using hash_to_field. The specific hash function that is being used is chosen by the backend. The only requirement is that this hash function can heuristically be used as a random oracle. If only collision resistance is needed, then one can call std::hash::pedersen instead.

    let is_member = std::merkle::check_membership(root, leaf, index, hashpath);

The leaf is then passed to a check_membership proof with the root, index and hashpath. is_member returns 1 if the leaf is a member of the merkle tree with the specified root, at the given index.

Note: It is possible to re-implement the merkle tree implementation without standard library. However, for most usecases, it is enough. In general, the standard library will always opt to be as conservative as possible, while striking a balance between efficiency.

An example, the merkle membership proof, only requires a hash function that has collision resistance, hence a hash function like Pedersen is allowed, which in most cases is more efficient than the even more conservative sha256.

    constrain is_member == 1;

This last line, constrains the variable to be equal to 1. If 1 was changed to 0, this would create a proof that the leaf was not present at that specific index for that specific root. Importantly, it would not prove that this leaf was not in the merkle tree.

Example Project: https://github.com/vezenovm/simple_shield

Language Concepts

In this chapter, we will go over the concepts that are being used in Noir. Specifically, we will learn about types, control flow, comments, functions and declarations. For some concepts, we also explain the rationale as to why they were designed in this particular way. Recurring themes you will encounter in this section are simplicity and safety.

Mutability

Variables in noir can be declared mutable via the mut keyword. Mutable variables can be reassigned to via an assignment expression.

let x = 2;
x = 3; // error: x must be mutable to be assigned to

let mut y = 3;
let y = 4; // OK

The mut modifier can also apply to patterns:

let (a, mut b) = (1, 2);
a = 11; // error: a must be mutable to be assigned to
b = 12; // OK

let mut (c, d) = (3, 4);
c = 13; // OK
d = 14; // OK

// etc.
let MyStruct { x: mut y } = MyStruct { x: a }
// y is now in scope

Note that mutability in noir is local and everything is passed by value, so if a called function mutates its parameters then the parent function will keep the old value of the parameters.

fn main() -> Field {
    let x = 3;
    helper(x);
    x // x is still 3
}

fn helper(mut x: i32) {
    x = 4;
}

Constants

A constant type is a value that does not change per circuit instance. This is different to a witness which changes per proof. If a constant type that is being used in your program is changed, then your circuit will also change.

Below we show how to declare a constant value:

fn main() {
    let a: comptime Field = 5;

    // `comptime Field` can also be inferred:
    let a = 5;
}

Note that variables declared as mutable may not be constants:

fn main() {
    // error: Cannot mark a comptime type as mutable - any mutation would remove its const-ness
    let mut a: comptime Field = 5;

    // a inferred as a private Field here
    let mut a = 5;
}

Globals

Noir also supports global variables. However, they must be compile-time variables. If comptime is not explicitly written in the type annotation the compiler will implicitly specify the declaration as compile-time. They can then be used like any other compile-time variable inside functions. The global type can also be inferred by the compiler entirely. Globals can also be used to specify array annotations for function parameters and can be imported from submodules.

Globals are currently limited to Field, integer, and bool literals.

global N: Field = 5; // Same as `global N: comptime Field = 5`

fn main(x : Field, y : [Field; N]) {
    let res = x * N;

    constrain res == y[0];

    let res2 = x * mysubmodule::N;
    constrain res != res2;
}

mod mysubmodule {
    use dep::std;

    global N: Field = 10;

    fn my_helper() -> comptime Field {
        let x = N;
        x
    }
}

Why only local mutability?

Witnesses in a proving system are immutable in nature. Noir aims to closely mirror this setting without applying additional overhead to the user. Modeling a mutable reference is not as straightforward as on conventional architectures and would incur some possibly unexpected overhead.

Data Types

Every value in Noir has a type, which determines which operations are valid for it.

All values in Noir are fundamentally composed of Field elements. For a more approachable developing experience, abstractions are added on top to introduce different data types in Noir.

Noir has two category of data types: primitive types (e.g. Field, integers, bool) and compound types that group primitive types (e.g. arrays, tuples, structs). Each value can either be private or public.

Private & Public Types

A private value is known only to the Prover, while a public value is known by both the Prover and Verifier. All primitive types (including individual fields of compound types) in Noir are private by default, and can be marked public when certain values are intended to be revealed to the Verifier.

Note: For public values defined in Noir programs paired with smart contract verifiers, once the proofs are verified on-chain the values can be considered known to everyone that has access to that blockchain.

Public data types are treated no differently to private types apart from the fact that their values will be revealed in proofs generated. Simply changing the value of a public type will not change the circuit (where the same goes for changing values of private types as well).

Private values are also referred to as witnesses sometimes.

Note: The terms private and public when applied to a type (e.g. pub Field) have a different meaning than when applied to a function (e.g. pub fn foo() {}).

The former is a visibility modifier for the Prover to interpret if a value should be made known to the Verifier, while the latter is a visibility modifier for the compiler to interpret if a function should be made accessible to external Noir programs like in other languages.

pub Modifier

All data types in Noir are private by default. Types are explicitly declared as public using the pub modifier:

fn main(x : Field, y : pub Field) -> pub Field {
    x + y
}

In this example, x is private while y and x + y (the return value) are public. Note that visibility is handled per variable, so it is perfectly valid to have one input that is private and another that is public.

Note: Public types can only be declared through parameters on main.

Primitive Types

A primitive type represents a single value. They can be private or public.

The Field Type

The field type corresponds to the native field type of the proving backend.

The size of a Noir field depends on the elliptic curve's finite field for the proving backend adopted. For example, a field would be a 254-bit integer when paired with the default TurboPlonk backend that spans the Grumpkin curve.

Fields support integer arithmetic and are often used as the default numeric type in Noir:

fn main(x : Field, y : Field)  {
    let z = x + y;
}

x, y and z are all private fields in this example. Using the let keyword we defined a new private value z constrained to be equal to x + y.

If proving efficiency is of priority, fields should be used as a default for solving problems. Smaller integer types (e.g. u64) incur extra range constraints.

Integer Types

An integer type is a range constrained field type. The Noir frontend currently supports unsigned, arbitrary-sized integer types.

An integer type is specified first with the letter u, indicating its unsigned nature, followed by its length in bits (e.g. 32). For example, a u32 variable can store a value in the range of \([0,2^{32}-1]\):

fn main(x : Field, y : u32) {
    let z = x as u32 + y;
}

x, y and z are all private values in this example. However, x is a field while y and z are unsigned 32-bit integers. If y or z exceeds the range \([0,2^{32}-1]\), proofs created will be rejected by the verifier.

Note: The default TurboPlonk backend supports both even (e.g. u16, u48) and odd (e.g. u5, u3) sized integer types.

The Boolean Type

The bool type in Noir has two possible values: true and false:

fn main() {
    let t = true;
    let f: bool = false;
}

Note: When returning a boolean value, it will show up as a value of 1 for true and 0 for false in Verifier.toml.

The boolean type is most commonly used in conditionals like if expressions and constrain statements. More about conditionals is covered in the Control Flow and Constrain Statement sections.

The String Type

Strings in Noir are fairly basic with their main use being for debugging via std::println.. Since circuit inputs need to be known at compile time, the string length for an input must be hardcoded into the circuit, like so:

// **input**
// field = "hello"

fn main(string: str<5>) {
    let hello = "hello";
    constrain string == hello;
}

String manipulation isn't available at this time, but as long as you make the variable mut, you can replace it.

Compound Types

A compound type groups together multiple values into one type. Elements within a compound type can be private or public.

The Array Type

An array is one way of grouping together values into one compound type. Array types can be inferred or explicitly specified via the syntax [<Type>; <Size>]:

fn main(x : Field, y : Field) {
    let my_arr = [x, y];
    let your_arr: [Field; 2] = [x, y];
}

Here, both my_arr and your_arr are instantiated as an array containing two Field elements.

Array elements can be accessed using indexing:

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

All elements in an array must be of the same type (i.e. homogeneous). That is, an array cannot group a Field value and a u8 value together for example.

The Tuple Type

A tuple collects multiple values like an array, but with the added ability to collect values of different types:

fn main() {
    let tup: (u8, u64, Field) = (255, 500, 1000);
}

One way to access tuple elements is via destructuring using pattern matching:

fn main() {
    let tup = (1, 2);

    let (one, two) = tup;

    let three = one + two;
}

Another way to access tuple elements is via direct member access, using a period (.) followed by the index of the element we want to access. Index 0 corresponds to the first tuple element, 1 to the second and so on:

fn main() {
    let tup = (5, 6, 7, 8);

    let five = tup.0;
    let eight = tup.3;
}

Structs

A struct also allows for grouping multiple values of different types. Unlike tuples, we can also name each field.

Note: The usage of field here refers to each element of the struct and is unrelated to the field type of Noir.

Defining a struct requires giving it a name and listing each field within as <Key>: <Type> pairs:

struct Animal {
    hands: Field,
    legs: Field,
    eyes: u8,
}

An instance of a struct can then be created with actual values in <Key>: <Value> pairs in any order. Struct fields are accessible using their given names:

fn main() {
    let legs = 4;

    let dog = Animal {
        eyes: 2,
        hands: 0,
        legs,
    };

    let zero = dog.hands;
}

Structs can also be destructured in a pattern, binding each field to a new variable:

fn main() {
    let Animal { hands, legs: feet, eyes } = get_octopus();

    let ten = hands + feet + eyes as u8;
}

fn get_octopus() -> Animal {
    let octopus = Animal {
        hands: 0,
        legs: 8,
        eyes: 2,
    };

    octopus
}

The new variables can be bound with names different from the original struct field names, as showcased in the legs --> feet binding in the example above.

Functions

Functions in Noir follow the same semantics of Rust, though Noir does not support early returns.

To declare a function the fn keyword is used.

fn foo() {}

All parameters in a function must have a type and all types are known at compile time. The parameter is pre-pended with a colon and the parameter type. Multiple parameters are separated using a comma.

fn foo(x : Field, y : pub Field){}

The return type of a function can be stated by using the -> arrow notation. The function below states that the foo function must return a Field. If the function returns no value, then the arrow is omitted.

fn foo(x : Field, y : pub Field) -> Field {
    x + y
}

Note that a return keyword is unneeded in this case - the last expression in a function's body is returned.

Call Expressions

Calling a function in Noir is executed by using the function name and passing in the necessary arguments.

Below we show how to call the foo function from the main function using a call expression:

fn main(x : Field, y : Field) {
    let z = foo(x);
}

fn foo(x : Field) -> Field {
    x + x
}

Methods

You can define methods in Noir on any struct type in scope.

struct MyStruct {
    foo: Field,
    bar: Field,
}

impl MyStruct {
    fn new(foo: Field) -> MyStruct {
        MyStruct {
            foo,
            bar: 2,
        }
    }

    fn sum(self) -> Field {
        self.foo + self.bar
    }
}

fn main() {
    let s = MyStruct::new(40);
    constrain s.sum() == 42;
}

Methods are just syntactic sugar for functions, so if we wanted to we could also call sum as follows:

constrain MyStruct::sum(s) == 42

Comments

A comment is a line in your codebase which the compiler ignores, however it can be read by programmers.

Here is a single line comment:

// This is a comment and is ignored

// is used to tell the compiler to ignore the rest of the line.

Noir doesn't have multi-line comments, but you can emulate them via using // on each line

// This is a multi line
// comment, that is ignored by 
// the compiler

Control Flow

Loops

Noir has one kind of loop: the for loop. for loops allow you to repeat a block of code multiple times.

The following block of code between the braces is run 10 times.

for i in 0..10 {
    // do something
};

If Expressions

Noir supports if-else statements. The syntax is most similar to Rust's where it is not required for the statement's conditonal to be surrounded by parentheses.

let a = 0;
let mut x: u32 = 0;

if a == 0 {
    if a != 0 {
        x = 6;
    } else {
        x = 2;
    }
} else {
    x = 5;
    constrain x == 5;
}
constrain x == 2;

Operations

Table of Supported Operations

OperationDescriptionRequirements
+Adds two concealed types togetherTypes must be concealed
-Subtracts two concealed types togetherTypes must be concealed
*Multiplies two concealed types togetherTypes must be concealed
/Divides two concealed types togetherTypes must be concealed
^XOR two concealed types togetherTypes must be integer
&AND two concealed types togetherTypes must be integer
<<Left shift an integer by another integer amountTypes must be integer
>>Right shift an integer by another integer amountTypes must be integer
!Bitwise not of a valueType must be integer or boolean
<returns a bool if one value is less than the otherUpper bound must have a known bit size
<=returns a bool if one value is less than or equal to the otherUpper bound must have a known bit size
>returns a bool if one value is more than the otherUpper bound must have a known bit size
>=returns a bool if one value is more than or equal to the otherUpper bound must have a known bit size
==returns a bool if one value is equal to the otherBoth types must not be constants
!=returns a bool if one value is not equal to the otherBoth types must not be constants

Predicate Operators

<,<=, !=, == , >, >= are known as predicate/comparison operations because they compare two values. This differs from the operations such as + where the operands are used in computation.

Bitwise Operations Example

fn main(x : Field) {
    let y = x as u32;
    let z = y & y;
}

z is implicitly constrained to be the result of y & y. The & operand is used to denote bitwise &.

x & x would not compile as x is a Field and not an integer type.

Logical Operators

Noir has no support for the logical operators || and &&. This is because encoding the short-circuiting that these operators require can be inefficient for Noir's backend. Instead you can use the bitwise operators | and & which operate indentically for booleans, just without the short-circuiting.

let my_val = 5;

let mut flag = 1;
if (my_val > 6) | (my_val == 0) {
    flag = 0;
}
constrain flag == 1;

if (my_val != 10) & (my_val < 50) {
    flag = 0;
}
constrain flag == 0;

Constrain Statement

Noir includes a special keyword constrain which will explicitly constrain the predicate/comparison expression that follows to be true. If this expression is false at runtime, the program will fail to be proven.

Constrain statement example

fn main(x : Field, y : Field) {
    constrain x == y;
}

The above snippet compiles because == is a predicate operation. Conversely, the following will not compile:

fn main(x : Field, y : Field) {
    constrain x + y;
}

The rationale behind this not compiling is due to ambiguity. It is not clear if the above should equate to x + y == 0 or if it should check the truthiness of the result.

Noir Standard Library

Noir features a standard library with some ready-to-use, built-in structures. To use them, you should import the std library, like so:


#![allow(unused)]

fn main() {
use dep::std;
}

You should then have these constructs available. For example, you can now call the std::hash::pedersen function like so:


#![allow(unused)]
fn main() {
let data : [Field; 2] = [42, 42];
std::hash::pedersen(data);
}

Logging

The standard library provides a familiar println statement you can use. Despite being a limited implementation of rust's println! macro, this construct can be useful for debugging.

use dep::std;

fn main(string: pub str<5>) {
    let x = 5;
    std::println(x)
}

Field

After declaring a Field, you can use these common methods on it 1:

to_le_bits

Transforms the field into an array of bits, Little Endian.


#![allow(unused)]
fn main() {
fn to_le_bits<N>(_x : Field, _bit_size: u32) -> [u1; N]
}

example:

fn main() {
    let field = 2
    let bits = field.to_le_bits(32);
}

to_le_bytes

Transforms into an array of bytes, Little Endian


#![allow(unused)]
fn main() {
fn to_le_bytes(_x : Field, byte_size: u32) -> [u8]
}

example:

fn main() {
    let field = 2
    let bytes = field.to_le_bytes(4);
}

to_le_radix

Decomposes into a vector over the specificed base, Little Endian


#![allow(unused)]
fn main() {
fn to_le_radix(_x : Field, _radix: u32, _result_len: u32) -> [u8]
}

example:

fn main() {
    let field = 2
    let radix = field.to_le_radix(256, 4);
}

to_be_radix

Decomposes into a vector over the specificed base, Big Endian


#![allow(unused)]
fn main() {
fn to_be_radix(_x : Field, _radix: u32, _result_len: u32) -> [u8]
}

example:

fn main() {
    let field = 2
    let radix = field.to_be_radix(256, 4);
}

pow_32

Returns the value to the power of the specificied exponent


#![allow(unused)]
fn main() {
fn pow_32(self, exponent: Field) -> Field
}

example:

fn main() {
    let field = 2
    let pow = field.pow_32(4);
    constrain pow == 16;
}

Array

For convenience, the STD provides some ready-to-use, common methods for arrays1:

len

Returns the length of an array


#![allow(unused)]
fn main() {
fn len<T, N>(_array: [T; N]) -> comptime Field
}

example

fn main() {
    let array = [42, 42]
    constrain arr.len() == 2;
}

sort

Returns a new sorted array. The original array remains untouched. Notice that this function will only work for arrays of fields or integers, not for any arbitrary type. This is because the sorting logic it uses internally is optimized specifically for these values. If you need a sort function to sort any type, you should use the function sort_via described below.


#![allow(unused)]
fn main() {
fn sort<T, N>(_array: [T; N]) -> [T; N]
}

example

fn main() {
    let arr = [42, 32]
    let sorted = arr.sort();
    constrain sorted == [32, 42];
}

sort_via

Sorts the array with a custom comparison function


#![allow(unused)]
fn main() {
fn sort_via<T, N>(mut a: [T; N], ordering: fn(T, T) -> bool) -> [T; N]
}

example

fn main() {
    let arr = [42, 32]
    let sorted_ascending = arr.sort_via(|a, b| a < b);
    constrain sorted_ascending == [32, 42]; // verifies

    let sorted_descending = arr.sort_via(|a, b| a > b);
    constrain sorted_descending == [32, 42]; // does not verify
}

fold

Applies a function to each element of the array, returning the final accumulated value. The first parameter is the initial value.


#![allow(unused)]
fn main() {
fn fold<U>(mut accumulator: U, f: fn(U, T) -> U) -> U
}

This is a left fold, so the given function will be applied to the accumulator and first element of the array, then the second, and so on. For a given call the expected result would be equivalent to:


#![allow(unused)]
fn main() {
let a1 = [1];
let a2 = [1, 2];
let a3 = [1, 2, 3];

let f = |a, b| a - b;
a1.fold(10, f)  //=> f(10, 1)
a2.fold(10, f)  //=> f(f(10, 1), 2)
a3.fold(10, f)  //=> f(f(f(10, 1), 2), 3)
}

example:


fn main() {
    let arr = [2,2,2,2,2]
    let folded = arr.fold(0, |a, b| a + b);
    constrain folded == 10;
}

reduce

Same as fold, but uses the first element as starting element.


#![allow(unused)]
fn main() {
fn reduce<T, N>(f: fn(T, T) -> T) -> T 
}

example:

fn main() {
    let arr = [2,2,2,2,2]
    let reduced = arr.reduce(|a, b| a + b);
    constrain reduced == 10;
}

all

Returns true if all the elements satisfy the given predicate


#![allow(unused)]
fn main() {
fn all<T, N>(predicate: fn(T) -> bool) -> bool
}

example:

fn main() {
    let arr = [2,2,2,2,2]
    let all = arr.all(|a| a == 2);
    constrain all;
}

any

Returns true if any of the elements satisfy the given predicate


#![allow(unused)]
fn main() {
fn any<T, N>(predicate: fn(T) -> bool) -> bool
}

example:

fn main() {
    let arr = [2,2,2,2,5]
    let any = arr.any(|a| a == 5);
    constrain any;
}

1

Migration Note: These methods were previously free functions, called via std::array::len(). For the sake of ease of use and readability, these functions are now methods and the old syntax for them is now deprecated.

Merkle Trees

check_membership

Returns 1 if the specified leaf is at the given index on a tree


#![allow(unused)]
fn main() {
fn check_membership(_root : Field, _leaf : Field, _index : Field, _hash_path: [Field]) -> Field
}

example:

/**
 * 
    index = "0"
    priv_key = "0x000000000000000000000000000000000000000000000000000000616c696365"
    secret = "0x1929ea3ab8d9106a899386883d9428f8256cfedb3c4f6b66bf4aa4d28a79988f"
    root = "0x2f36d4404719a30512af45be47c9732e916cb131933102b04ba6432602db209c"
    hash_path = [
    "0x1e61bdae0f027b1b2159e1f9d3f8d00fa668a952dddd822fda80dc745d6f65cc",
    "0x0e4223f3925f98934393c74975142bd73079ab0621f4ee133cee050a3c194f1a",
    "0x2fd7bb412155bf8693a3bd2a3e7581a679c95c68a052f835dddca85fa1569a40"
    ]
 */

fn main(root : Field, index : Field, hash_path : [Field; 3], secret: Field, priv_key: Field) {
    constrain index == index;

    let pubkey = std::scalar_mul::fixed_base(priv_key);
    let pubkey_x = pubkey[0];
    let pubkey_y = pubkey[1];
    let note_commitment = std::hash::pedersen([pubkey_x, pubkey_y, secret]);

    let root = std::merkle::check_membership(root, note_commitment[0], index, hash_path);
    std::println(root);
}

check_membership_in_noir

Behaves exactly the same as above, but it's computed in Noir in order to accept many backends.


#![allow(unused)]
fn main() {
fn check_membership_in_noir(root : Field, leaf : Field, index : Field, hash_path: [Field]) -> Field
}

For examples, you can literally replace check_membership for this method, in the above example.

compute_root_from_leaf

Returns the root of the tree from the provided leaf and its hashpath, using a pedersen hash


#![allow(unused)]
fn main() {
fn compute_root_from_leaf(leaf : Field, index : Field, hash_path: [Field]) -> Field
}

example:

/**
    index = "0"
    priv_key = "0x000000000000000000000000000000000000000000000000000000616c696365"
    secret = "0x1929ea3ab8d9106a899386883d9428f8256cfedb3c4f6b66bf4aa4d28a79988f"
    note_hash_path = [
    "0x1e61bdae0f027b1b2159e1f9d3f8d00fa668a952dddd822fda80dc745d6f65cc",
    "0x0e4223f3925f98934393c74975142bd73079ab0621f4ee133cee050a3c194f1a",
    "0x2fd7bb412155bf8693a3bd2a3e7581a679c95c68a052f835dddca85fa1569a40"
    ]
 */
fn main(index : Field, priv_key : Field, secret : Field, note_hash_path : [Field; 3]) {
    constrain index == index;

    let pubkey = std::scalar_mul::fixed_base(priv_key);
    let pubkey_x = pubkey[0];
    let pubkey_y = pubkey[1];
    let note_commitment = std::hash::pedersen([pubkey_x, pubkey_y, secret]);

    let root = std::merkle::compute_root_from_leaf(note_commitment[0], index, note_hash_path);
    std::println(root);
}

Cryptographic primitives

Some cryptographic primitives are already developed and ready-to-use for any Noir project:

sha256

Given an array of bytes, returns the sha256 of it:

fn main() {
    let x = [163, 117, 178, 149] // some random bytes
    let hash = std::hash::sha256(x);
}

blake2s

Given an array of bytes, returns the Blake2 of it:

fn main() {
    let x = [163, 117, 178, 149] // some random bytes
    let hash = std::hash::blake2s(x);
}

pedersen

Given an array of Fields, returns the Pedersen hash of it:

fn main() {
    let x = [163, 117, 178, 149] // some random bytes
    let hash = std::hash::pedersen(x);
}

mimc_bn254 and mimc

mimc_bn254 is mimc, but with hardcoded parameters for the BN254 curve. You can use it by providing an array of Fields, and it returns a Field with the hash. You can use the mimc method if you're willing to input your own constants:


#![allow(unused)]
fn main() {
fn mimc<N>(x: Field, k: Field, constants: [Field; N], exp : Field) -> Field
}

otherwise, use the mimc_bn254 method:


#![allow(unused)]
fn main() {
fn mimc_bn254<N>(array: [Field; N]) -> Field
}

example:


fn main() {
    let x = [163, 117, 178, 149] // some random bytes
    let hash = std::hash::mimc_bn254(x);
}

scalar_mul::fixed_base

Performs scalar multiplication over the embedded curve whose coordinates are defined by the configured noir field. For the BN254 scalar field, this is BabyJubJub or Grumpkin.


#![allow(unused)]
fn main() {
fn fixed_base(_input : Field) -> [Field; 2]
}

example

fn main(x : Field) {
    let scal = std::scalar_mul::fixed_base(x);
    std::println(scal);
}

schnorr::verify_signature

Verifier for Schnorr signatures


#![allow(unused)]
fn main() {
fn verify_signature(_public_key_x: Field, _public_key_y: Field, _signature: [u8; 64], _message: [u8]) -> Field
}

ecdsa_secp256k1::verify_signature

Verifier for ECDSA Secp256k1 signatures


#![allow(unused)]
fn main() {
fn verify_signature(_public_key_x : [u8; 32], _public_key_y : [u8; 32], _signature: [u8; 64], _message: [u8]) -> Field
}

Modules, Packages, Crates

In this section, we describe the package, crate and module system. As mentioned in the introduction, this will largely follow the design choice chosen by Rust.

Crate

A crate is the compilation unit used in Noir.

Crate Root

Every crate has a root, which is the source file that the compiler starts, this is also known as the root module. The Noir compiler does not enforce any conditions on the name of the file which is the crate root, however if you are compiling via Nargo. The Crate Root, must be called lib.nr or main.nr.

Packages

A Nargo Package is a collection of one of more crates. A Package must include a Nargo.toml file.

A Package must contain either a library or a binary crate.

Creating a new package

A new package is created using the new command.

$ nargo new my-project
$ ls my-project
Nargo.toml
src
$ ls my-project/src
main.nr

Binary vs Library

Similar to Cargo, Nargo follows the convention that if there is a src/main.nr then the project is a binary. If it contains a src/lib.nr then it is a library.

However, note that dissimilar to Cargo, we cannot have both a binary and library in the same project.

Modules

Noir's module system follows the same convention as the newer version of Rust's module system.

Purpose of Modules

Modules are used to organise files. Without modules all of your code would need to live in a single file. In Noir, the compiler does not automatically scan all of your files to detect modules. This must be done explicitly by the developer.

Examples

Importing a module in the crate root

Filename : src/main.nr

mod foo;

fn main() {
    foo::hello_world();
}

Filename : src/foo.nr

fn from_foo() {}

In the above snippet, the crate root is the src/main.nr file. The compiler sees the module declaration mod foo which prompts it to look for a foo.nr file.

Visually this module hierarchy looks like the following :

crate
 ├── main
 │
 └── foo
      └── from_foo
      

Sub-modules

Filename : src/main.nr

mod foo;

fn main() {
    foo::from_foo();
}

Filename : src/foo.nr

mod bar;
fn from_foo() {}

Filename : src/foo/bar.nr

fn from_bar() {}

In the above snippet, we have added an extra module to the module tree; bar. bar is a submodule of foo hence we declare bar in foo.nr with mod bar. Since foo is not the crate root, the compiler looks for the file associated with the bar module in src/foo/bar.nr

Visually the module hierarchy looks as follows:

crate
 ├── main
 │
 └── foo
      ├── from_foo
      └── bar
           └── from_bar

Dependencies

Nargo allows you to upload packages to GitHub and use them as dependencies.

Specifying a dependency

hello_world = { tag = "v0.5", git = "https://github.com/kevaundray/hello-world-noir"}

Specifying a dependency requires a tag to a specific commit and the git url to the url containing the package.

Currently, there are no requirements on the tag contents. If requirements are added, it would follow semver 2.0 guidelines.

Note: Without a tag , there would be no versioning and dependencies would change each time you compile your project.

ACIR (Abstract Circuit Intermediate Representation)

The purpose of ACIR is to act as an intermediate layer between the proof system that Noir chooses to compile to and the Noir syntax. This separation between proof system and programming language, allows those who want to integrate proof systems to have a stable target, moreover it allows the frontend to compile to any ACIR compatible proof system.

ACIR additionally allows proof systems to supply a fixed list of optimised blackbox functions that the frontend can access. Examples of this would be SHA256, PEDERSEN and SCHNORRSIGVERIFY.

Compiling a Proof

When inside of a given Noir project the command nargo compile my_proof will perform two processes.

  • First, compile the Noir program to its ACIR and solve the circuit's witness.

  • Second, create a new build/ directory to store the ACIR, my_proof.acir, and the solved witness, my_proof.tr

These can be used by the Noir Typescript wrapper to generate a prover and verifier inside of Typescript rather than in Nargo. This will be discussed further in another section.

Features Coming Soon

Isize

Signed integers such as i32 and i64 allow one to express more circuits. They are partially supported but need to be finalised.

Recursion

Recursion is becoming feasible in circuits, hence Noir will have native support for it. Currently, only composition is supported.

LICENSE

Noir will be dual licensed under MIT/Apache (Version 2.0).