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.