How To Use The Noir Groth16 Backend

Noir Groth16

In this tutorial we will be writing circuits in Noir, outputing deterministic Groth16 artifacts, generating proofs with snarkjs, and verifying them inside a Stellar smart contract.

The official Noir backend uses Ultrahonk proofs which are larger and more resource intensive to verify. I built this backend so that Noir could be used with Groth16 which is more widely integrated in various blockchain ecosystems.

James On YouTube

All code is open-source and available here: https://github.com/jamesbachini/Noir-Groth16

Let’s start by creating a simple Noir circuit in circuits/src/main.rs

fn main(x: Field, y: pub Field) {
    assert(x * x == y);
}

Then let’s give it some inputs for x and y, ensuring that x is the square root of y. Save this json to circuits/inputs.json

{"x":"3","y":"9"}

After compiling with nargo, let’s run the all in one script to execute the Groth16 flow:

cd circuits
nargo compile
cd ..
./scripts/run_circuit.sh

The script builds the CLI, compiles the circuit, emits R1CS and witness artifacts, checks the witness with snarkjs, generates a powers of tau file if necessary, runs Groth16 setup, produces a proof, and verifies it locally with snarkjs.

Artifacts are written under target/groth16/ and include the R1CS file, witness file, proof JSON, public signals JSON, and verification key JSON.

For debugging, the exact snarkjs commands can be run manually. The repository does not obscure the underlying tools; it standardises their ordering and output structure.

Noir Groth16 Prover Backend

Verifying On The Stellar Network

The point of using Groth16 is to make it easier and cheaper to verify on-chain proofs.

The repository script scripts/verify_stellar.sh executes the full proof pipeline, re-verifies locally, builds and deploys the Stellar smart contract, encodes verification key and proof artifacts into contract compatible byte sequences, stores the verification key, and calls verify.

Verify Noir Groth16 Proof on Stellar

Encoding is handled by encode_bn254_for_soroban.mjs. This script defines the canonical format contract. Any deviation in endianness, coordinate order, or length prefixes will cause errors in the contract.

The pairing check is standard Groth16 over BN254.

The code for the circuit is available here:

https://github.com/jamesbachini/Noir-Groth16/blob/main/contracts/src/lib.rs

and is based on the groth16 code in Soroban-examples


How The Noir > Groth16 Backend Works

The pipeline is straightforward. A compiled Noir contract is compiled wiht nargo and the artifact and ABI metadata are parsed. Witness values are solved from ABI shaped JSON inputs. Supported ACIR opcodes are lowered to R1CS. Deterministic .r1cs and .wtns files are emitted in iden3 format for snarkjs. Finally, Groth16 artifacts can be encoded for Stellar and verified on the Stellar network.

The workspace is divided by responsibility.

  • crates/noir-acir handles artifact parsing and legacy compatibility.
  • crates/noir-witness performs ABI flattening and ACVM witness solving.
  • crates/noir-r1cs performs strict ACIR lowering and writes .r1cs.
  • crates/noir-cli exposes the command surface.

At the centre of this pipeline is ACIR, Noir’s Abstract Circuit Intermediate Representation. When you compile with nargo, your high level Noir code is reduced to a sequence of ACIR opcodes. Each opcode represents a constraint level operation: arithmetic relations, equality assertions, memory operations, or calls to specialised primitives. These opcodes are not yet R1CS constraints; they are a structured, backend agnostic description of what must be proven.

Some of those opcodes are simple field arithmetic and can be translated directly into R1CS constraints. Others are blackbox operations. A blackbox is a predefined primitive such as hashing, elliptic curve operations, or signature verification. Instead of expanding these into thousands of low level constraints inside Noir itself, they are represented symbolically in ACIR. During witness generation, the ACVM executes these blackboxes using a dedicated solver (for example, a BN254 Poseidon2 implementation) to compute witness values. During lowering, the backend either translates supported opcodes into equivalent R1CS constraints or rejects unsupported ones explicitly. This separation allows Noir to remain expressive while the Groth16 backend remains strict and deterministic about what it can actually encode into R1CS.


Noir in my opinion is the most pleasant language to write zero-knowledge circuits. It’s based on Rust and well suited to smart contract developers who are also using Rust for their contracts.

Having a Groth16 backend makes the process of verifying the proofs and integrating on-chain tooling much easier. Note that this is experimental code by nature, it has not been security audited and is not suitable for production environments.


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.


Posted

in

, , , , ,

by