Standard Noir Example

This project demonstrates a basic example of how to prove and verify Noir circuits in typescript. The test file is purposefully bloated to show the multiple methods available for how to prove/verify circuits in Typescript. This repo will continue to be updated as changes are made to the Noir tooling. If you want a plain template to act as a starting point you can reference the two implementation below:

Requirements

  • Noir is based upon Rust, and we will need to Noir's package manager nargo in order to compile our circuits. Further installation instructions for can be found here.
    • If there are troubles installing nargo due to the C++ backend, replace the aztec_backend dependency in the nargo crate's Cargo.toml with this line:
    aztec_backend = { optional = true, git = "https://github.com/noir-lang/aztec_backend", rev = "d91c69f2137777cec37f692f98d075ae10e7a584", default-features = false, features = [
        "wasm-base",
    ] }
    
  • The typescript tests and contracts live within a hardhat project, where we use yarn as the package manager.

Development

Start by installing all the packages specified in the package.json

yarn install

After installing nargo it should be placed in our path and can be called from inside the circuits folder. We will then compile our circuit. This will generate an intermediate representation that is called the ACIR. More information on this can be found here. p in nargo compile p is simply the name of the ACIR and witness files generated by Noir when compiling the circuit. These will be used by the tests.

cd circuits/
nargo compile p --witness

The --witness flag uses the inputs from the Prover.toml to solve the witness and write it to file. Only one of the example tests requires that the solved witness is also read from file. If you prefer to specify your circuit inputs in Typescript you can use just nargo compile p which will only write the ACIR to file and the witness will be solved when creating the proof.

We use these three packages to interact with the ACIR. @noir-lang/noir_wasm, @noir-lang/barretenberg, and @noir-lang/aztec_backend.

@noir-lang/noir_wasm is used to serialize the ACIR from file.

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

It is also possible to instead compile the program in Typescript. This can be seen inside the test file.

let compiled_program = compile(resolve(__dirname, '../circuits/src/main.nr);
const acir = compiled_program.circuit;

Then @noir-lang/barretenberg is used to generate a proof and verify that proof. We first specify the ABI for the circuit. This contains all the public and private inputs to the program and is generated by the prover. In the case of our typescript tests, each test acts as both the prover and the verifier, and only passes if the proof passes verification.

These values in the abi are all calculated inside the test file for each test, but they are written out here.

let abi = {
    x: 3,
    y: 4,
    return: 12,
}

We will then construct our prover and verifier from the ACIR, and generate a proof from the prover, ACIR, and newly specified ABI.

let [prover, verifier] = await setup_generic_prover_and_verifier(acir);

const proof = await create_proof(prover, acir, abi);

The verify_proof method then takes in the previously generated verifier and proof and returns either true or false. A verifier also needs to accept the circuits public inputs in order to be valid. Our prover prepends the public inputs to the proof.

const verified = await verify_proof(verifier, proof);

The tests also show how to use the @noir-lang/aztec_backend package for interactions with the witness. You must use this package to accurately read in the witness computed by nargo compile

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

The barretenberg_witness_arr is what will be used as such:

const proof = await create_proof_with_witness(prover, barretenberg_witness_arr);

You can also use the @noir-lang/aztec_backend package for computing the witness directly in Typescript.

let initial_js_witness = ["0x03", "0x04", "0x0c"];
// NOTE: breaks without even number of bytes specified, the line below does not work
// let initial_js_witness = ["0x3", "0x4", "0x5100"];

let barretenberg_witness_arr = compute_witnesses(acir, initial_js_witness);

Note though that unlike the abi shown previously, the circuit inputs provided must be in a flattened array rather than a JS object.

Solidity Verifier

Once we have compiled our program and generated an ACIR, we can generate a Solidity verifier rather than having to use the verifier provided by nargo or Noir's typescript wrapper.

In the scripts folder you will find a script for compiling a program and generating the Solidity verifier. You can call it using the command below (assuming you are in the root directory of the project).

npx ts-node ./scripts/generate_sol_verifier.ts

This script uses the compile method to generate the ACIR directly in Typescript. It can be changed to use the ACIR from file, just make sure in your tests that the proof being generated to verify against the Solidity verifier uses the same method for fetching the ACIR. This will help avoid any chance of a mismatch between the ACIR generated in Typescript or natively through nargo.

Running tests

The tests show both the method of compiling the circuit using nargo and in Typescript. The tests also show how to complete proof verification using Typescript as well as with the Solidity verifier. Thus, to have all tests pass, it is necessary you follow all the commands listed or change the tests to your preferred method of compilation and/or proof verification.

This command will compile the Solidity verifier within the contracts folder and run all tests inside ./test/1_mul.ts.

npx hardhat test