What Barretenberg Is Actually Doing

The black box you've been using

Every time you run bb prove, something takes your compiled circuit and your witness and produces a cryptographic proof in seconds. That something is Barretenberg, a C++ proving backend developed by Aztec Labs.

Most developers treat Barretenberg as a black box. This lesson opens it. Understanding what Barretenberg does internally changes how you reason about proof generation time, circuit size, verification cost, and what --zk actually means.

What Barretenberg is

Barretenberg is a C++ library that implements UltraHonk, MegaHonk, and Client IVC as its proving systems. It is the cryptographic engine behind every Noir application. When you install bb via bbup, you are installing a CLI wrapper around this library.

It is also available as bb.js, a TypeScript package that wraps the C++ implementation via WebAssembly. This is what NoirJS uses to generate proofs in the browser or Node.js without requiring a native binary.

The primary curve Barretenberg uses is BN254, the same curve used by Ethereum's pairing precompiles. It uses Grumpkin as an embedded curve — a curve that cycles with BN254, meaning the scalar field of BN254 is the base field of Grumpkin and vice versa. This cycling property is what makes efficient recursive proof composition possible.

ACIR: the interface between Noir and Barretenberg

When you run nargo compile, the output is an ACIR file — Abstract Circuit Intermediate Representation. ACIR is a backend-agnostic IR that sits between Noir and any proving backend.

ACIR is analogous to LLVM IR in traditional compilers. It is not specific to Barretenberg. The same ACIR output could theoretically be consumed by a different backend. The purpose is to decouple the frontend language from the proving system.

ACIR represents computation as a sequence of opcodes operating on witnesses. There are three categories of opcodes:

Arithmetic opcodes encode polynomial constraints of the form a * b + c * d + ... = 0. These map directly to the constraint system's gate structure.

Directive opcodes instruct the ACVM (the ACIR virtual machine) to compute values for the witness without generating constraints. These are the circuit-level equivalent of Circom's <-- operator. They are used for operations that are not polynomial, like field element inversion or bit decomposition. The computed value must then be constrained by a subsequent arithmetic opcode.

Black box function calls are operations that Barretenberg implements natively using optimized cryptographic primitives: range checks, bitwise AND, XOR, SHA256, Keccak256, Pedersen commitments, ECDSA verification, Schnorr verification, and multi-scalar multiplication. These are not compiled into standard arithmetic gates. They are handled by dedicated circuits inside Barretenberg that are more efficient than the generic arithmetic encoding would be.

You can inspect the ACIR opcodes your circuit generates:

``bash nargo info --print-acir ``

This shows the exact sequence of ACIR instructions before Barretenberg translates them into its internal gate representation.

ACIR to UltraHonk gates

When Barretenberg receives the ACIR file, the first step is translating ACIR opcodes into its internal circuit representation. This is handled by AcirFormat, a C++ component that parses the ACIR constraint system and populates the UltraCircuitBuilder.

UltraHonk uses a PLONKish arithmetization with a width-4 constraint system. Each gate satisfies:

`` w₁ · w₂ · qₘ + w₁ · q₁ + w₂ · q₂ + w₃ · q₃ + w₄ · q₄ + qc = 0 ``

Where w₁ through w₄ are witness values and the q values are selector polynomials that encode the gate type. This is more flexible than R1CS which uses a width-3 system. A width-4 system can encode more complex relationships in a single gate, which reduces total gate count for the same computation.

UltraHonk extends vanilla PLONK with:

Custom gates that encode specific operations more efficiently than generic arithmetic gates. Range checks, elliptic curve additions, and lookup table queries each have dedicated gate types.

Lookup tables that allow Barretenberg to check that a value exists in a precomputed table in a single gate. This is how bitwise operations, range checks, and other non-arithmetic computations are efficiently encoded. Without lookups, a single 8-bit range check requires 8 constraints. With lookups, it requires one.

The circuit size Barretenberg reports — the number you see when it prints Finalized circuit size: 312 — is the count of these UltraHonk gates after translating from ACIR. This number is always larger than the ACIR opcode count because one ACIR opcode can expand into multiple gates.

The Structured Reference String

UltraHonk requires a Structured Reference String (SRS) — a set of elliptic curve points used in the KZG polynomial commitment scheme. The SRS encodes powers of a secret value τ committed to the BN254 curve:

`` [g, g^τ, g^τ², ..., g^τⁿ] ``

The SRS must be at least as large as the circuit. Barretenberg downloads the SRS from Aztec's servers on first use and caches it locally. The SRS it uses was generated through a public ceremony with many participants. The same SRS is reused for any circuit up to the supported size, which is the key advantage of UltraHonk's universal setup over Groth16's circuit-specific setup.

When you run bb write_vk, Barretenberg: - Determines the circuit size - Loads the portion of the SRS needed for that size - Computes the verification key, which encodes the circuit structure as committed polynomial evaluations

The verification key is circuit-specific. A different circuit, even with one changed constraint, produces a different verification key.

The proving pipeline

After the circuit is built and the SRS is loaded, the UltraProver runs. This implements the UltraHonk interactive oracle proof protocol, made non-interactive via the Fiat-Shamir transformation.

The steps at a high level:

Commit to the witness polynomials. The witness values are encoded as polynomials over the domain of circuit size. Each polynomial is committed using KZG. The commitments are sent as the first message.

Receive a random challenge. In interactive Honk, the verifier would send a random field element. In the non-interactive version, the challenge is derived by hashing all prior transcript elements using a hash function. For EVM-compatible proofs this hash is Keccak256 so the Solidity verifier can reproduce it. For non-EVM proofs it uses Poseidon2.

Run sumcheck. The core of the Honk protocol is a sumcheck argument that reduces the multi-variate polynomial identity to an evaluation claim at a random point. Each round of sumcheck sends a univariate polynomial and the verifier sends a new random challenge. This continues for log₂(circuit_size) rounds.

Open the polynomial commitments. After sumcheck, the prover must prove that the committed polynomials evaluate to the claimed values at the challenge point. This is a KZG opening proof. Multiple polynomials are batched into a single opening using a random linear combination to minimize the number of pairing operations the verifier needs to perform.

Output the proof. The proof is a sequence of field elements and group elements encoding all the commitments and evaluations. For a typical small circuit it is a few kilobytes. Larger circuits produce larger proofs because more witness polynomials need to be committed.

The ZK flag

By default, Barretenberg produces a SNARK — a Succinct Non-Interactive Argument of Knowledge. The witness values themselves are not cryptographically hidden in the proof. An adversary with the proof and the verification key cannot extract the witness, but the hiding property is not formally guaranteed.

When you add --zk to the prove command:

``bash bb prove --zk -b ./target/circuit.json -w ./target/circuit.gz -o ./target/proof ``

Barretenberg adds a blinding polynomial to the witness polynomials before committing. This blinding ensures that the commitment to the witness is computationally hiding — learning the commitment reveals nothing about the witness values. This upgrades the proof from a SNARK to a zkSNARK.

For applications where the witness values are already public or where hiding is not required, omitting --zk produces a marginally smaller and faster proof. For privacy applications, --zk is required.

Verification

The UltraVerifier in Barretenberg runs the verifier side of the sumcheck protocol and checks the KZG opening proofs using BN254 pairings. Local verification with bb verify is a native C++ operation and completes in milliseconds.

On-chain verification with the generated Solidity contract implements the same algorithm in Solidity. The contract uses Ethereum's ecPairing precompile for the pairing checks. The gas cost is dominated by these pairing operations. A typical UltraHonk verification costs between 250,000 and 400,000 gas depending on the number of public inputs and the circuit size.

For EVM verification, the verification key must be generated with the Keccak oracle:

``bash bb write_vk --oracle_hash keccak -b ./target/circuit.json -o ./target/vk bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol ``

The --oracle_hash keccak flag ensures that the Fiat-Shamir challenges in the proof transcript are derived using Keccak256, which the Solidity verifier can reproduce using the EVM's native keccak opcode. Without this flag, the proof uses Poseidon2 for challenges and the Solidity verifier cannot verify it.

Client IVC: the recursive path

For applications that need to aggregate multiple proofs — transaction proofs being combined into a block proof in a rollup — Barretenberg implements Client IVC (Incremental Verifiable Computation).

Client IVC uses folding schemes to accumulate multiple circuit executions without generating a full proof at each step. Only the final accumulated instance requires a full SNARK proof. This is significantly cheaper than recursively verifying one proof inside another for long chains of computation.

Client IVC is the proving scheme Aztec uses internally for its private kernel circuits. For most standalone Noir applications you will not interact with it directly, but understanding that it exists explains why Barretenberg is a more complex system than a simple PLONK prover.

Memory and performance

Barretenberg is optimized for performance across environments. Recent versions reduced UltraHonk memory usage by 26% through polynomial batching and structured storage. For browser environments, the WASM SRS size is configurable to avoid loading the full SRS when proving small circuits.

Proving time scales approximately as O(n log n) in the circuit size, where the dominant cost is the FFT operations used to compute polynomial evaluations over the circuit domain. A 10,000 gate circuit on modern hardware takes roughly 1 to 2 seconds to prove. A 1,000,000 gate circuit takes roughly 100 to 200 seconds.

Verification time is constant regardless of circuit size. The verifier always performs the same number of pairing checks. This asymmetry — expensive proving, cheap verification — is the fundamental property that makes Barretenberg useful for blockchain applications.

Answer the quiz correctly to continue →

Quiz · Multiple Choice1 / 3

What does passing `--zk` to `bb prove` actually change about the proof Barretenberg generates?