返回 Expert 笔记
Expert Day 228

Noir 基础 — Aztec 的 ZK DSL

Noir 语法、ACIR、Barretenberg backend、与 Circom 对比

2026-12-15
Phase 4 - ZK电路开发实战 (Day 223-243)
ZKNoirAztecUltraPlonkACIR

日期: 2026-12-15 方向: ZK工程 / 电路开发 阶段: Phase 4 - ZK电路开发实战 (Day 223-243) 标签: #ZK #Noir #Aztec #UltraPlonk #ACIR


今日目标

类型内容
学习Noir 语法、ACIR、Barretenberg backend、与 Circom 对比
实操写 3 个 Noir 电路:hash check / merkle proof / range check
产出noir_demos/(含 Nargo.toml + src/)

背景与定位

Noir 是什么?/ What is Noir?

  • Aztec Labs 开发的 ZK DSL(2023 公开),灵感来自 Rust。
  • 编译目标是 ACIR(Abstract Circuit Intermediate Representation),可以接多种 backend:
    • Barretenberg(默认,UltraPlonk + AVM)
    • Halo2(实验)
    • Plonky2(实验)
  • 与 Circom 相比:
    • 类型系统强(u8/u32/u64/Field/Array),编译期能抓 bug
    • 自动 range check(as u32 隐式插入)
    • 标准 cargo-like 工具链 nargo
    • 但生态不如 Circom 成熟

为什么学 Noir?

  • Aztec 主网 2024 上线,是 privacy L2 的核心。
  • zkRouter、Mina zkApps 等也开始 adopt Noir。
  • 写法接近现代语言,比 Circom 学习曲线缓。

Noir vs Circom

维度CircomNoir
设计灵感数学 R1CSRust
类型单一 feltu8/u32/u64/Field/Array/Struct
约束运算符<== <-- ===自动(编译期插入)
标准库circomlibstd::hash, std::ec
Backendsnarkjs (Groth16)Barretenberg (UltraPlonk)
工具circom + snarkjsnargo (一站式)
Setup每电路 trusted setupuniversal setup(PLONK)
最大约束2^282^28
学习曲线陡(量子约束概念)
生态Tornado/Semaphore/HermezAztec/zkPassport/Aztec Connect
Debugger仅 sym tracenargo prove --show-ssa

完整代码实现

1. Hash Check(demo_1_hash)

Nargo.toml

[package]
name = "demo_1_hash"
type = "bin"
authors = [""]
compiler_version = "0.36.0"

[dependencies]

src/main.nr

use dep::std;

// 证明: 知道一个 secret,且 Poseidon(secret) == expected_hash
// public:  expected_hash
// private: secret
fn main(secret: Field, expected_hash: pub Field) {
    let hash = std::hash::poseidon::bn254::hash_1([secret]);
    assert(hash == expected_hash);
}

#[test]
fn test_hash_check() {
    let secret = 12345;
    let h = std::hash::poseidon::bn254::hash_1([secret]);
    main(secret, h);
}

Prover.toml(输入文件)

secret = "12345"
expected_hash = "0x1c3a..."   # precomputed

命令

nargo new demo_1_hash
cd demo_1_hash

nargo check                     # 类型检查
nargo test                      # 跑 #[test]
nargo execute                   # 生成 witness(不证明)
nargo prove                     # 生成 proof
nargo verify                    # 链下验证
nargo codegen-verifier          # 输出 Solidity verifier

2. Merkle Proof(demo_2_merkle)

src/main.nr

use dep::std;

global LEVELS: u32 = 8;

fn compute_merkle_root(
    leaf: Field,
    path_indices: [u1; LEVELS],
    path_elements: [Field; LEVELS],
) -> Field {
    let mut current = leaf;
    for i in 0..LEVELS {
        let sibling = path_elements[i];
        let is_right = path_indices[i];
        // mux: order
        let (left, right) = if is_right == 0 {
            (current, sibling)
        } else {
            (sibling, current)
        };
        current = std::hash::poseidon::bn254::hash_2([left, right]);
    }
    current
}

fn main(
    leaf: Field,                              // private
    path_indices: [u1; 8],                    // private
    path_elements: [Field; 8],                // private
    expected_root: pub Field,                 // public
) {
    let root = compute_merkle_root(leaf, path_indices, path_elements);
    assert(root == expected_root);
}

#[test]
fn test_merkle() {
    let leaf: Field = 99;
    let path_indices = [0; 8];
    let path_elements = [0; 8];
    // compute expected
    let mut cur = leaf;
    for i in 0..8 {
        cur = std::hash::poseidon::bn254::hash_2([cur, 0]);
    }
    main(leaf, path_indices, path_elements, cur);
}

关键观察 / Key observations

  • u1 类型自动约束 0/1,无需手写 boolean constraint
  • if/else 内部自动转 mux(Circom 必须手写 DualMux)
  • for i in 0..LEVELS 在编译期 unroll;LEVELS 必须是 global 常量

3. Range Check(demo_3_range)

src/main.nr

// 证明: x ∈ [min, max]
// public:  min, max
// private: x
fn main(x: u32, min: pub u32, max: pub u32) {
    assert(x >= min);
    assert(x <= max);
}

// 等价电路: 用 Field 类型 + 显式 bit decomp
fn range_check_field(x: Field, n: u32) {
    let _ = x.to_le_bits(n);   // 自动加 N+1 约束
}

#[test]
fn test_range() {
    main(50, 1, 100);
    // main(150, 1, 100);  // ← 取消注释会 fail
}

#[test(should_fail)]
fn test_range_fails() {
    main(150, 1, 100);
}

约束分析:

  • u32 类型在 Noir 内部自动展开为 32 个 bit + sum constraint
  • assert(x >= min) 编译为 x - min >= 0,转成 32-bit range check(N+1 约束)

ACIR 中间表示 / ACIR IR

Noir 编译产物是 ACIR JSON:

nargo compile
ls target/
# demo_1_hash.json    ← ACIR
# demo_1_hash.toml    ← circuit metadata

ACIR 是「opcode + 约束」的 IR,可被多种 backend 消费:

ACIR opcodes:
  AssertZero  — a + b*c + ... === 0
  RangeCheck  — x ∈ [0, 2^N]
  BlackBox    — Poseidon, Pedersen, ECDSA, etc.
  MemoryOp    — RAM (zkVM-like)
  Brillig     — unconstrained pre-computation

Barretenberg Backend 与 UltraPlonk

Aztec 的 backend 用 UltraPlonk(PLONK + lookup + custom gates):

特性UltraPlonkGroth16
Setupuniversalper-circuit
Lookup
Custom gates
Proof size~1 KB800 B
Verify gas~300k~230k
Recursive不友好

真实 proof 数据:

$ nargo prove
Generating proof...    [############] 100% (1.2 sec)
$ ls target/
proofs/p.proof   = 2,144 bytes  (UltraPlonk)
verifier.toml    = ...

与 Aztec 集成 / Integration with Aztec

Noir 不止用于 standalone proof,更是 Aztec L2 的智能合约语言:

// Aztec contract example
contract PrivateToken {
    use dep::aztec::prelude::*;

    storage {
        balances: Map<AztecAddress, PrivateMutable<Field>>,
    }

    #[private]
    fn transfer(to: AztecAddress, amount: Field) {
        let from_bal = storage.balances.at(context.msg_sender()).read();
        assert(from_bal >= amount);
        storage.balances.at(context.msg_sender()).write(from_bal - amount);
        storage.balances.at(to).write(
            storage.balances.at(to).read() + amount
        );
    }
}

#[private] 函数在客户端生成 ZK proof,链上只看到 nullifier 和 commitment 变化(不暴露 amount/from/to)。


真实数据 / Benchmarks

操作CircomNoir
Poseidon-2 hash215 约束215 约束
Merkle proof depth-8~1.7k 约束~1.7k 约束
ECDSA verify~200k 约束~200k 约束
Compile time (small)~1 sec~5 sec
Compile time (large)数分钟数分钟
Prove time (depth-20 merkle)1.2 sec1.0 sec (Barretenberg)

常见陷阱

陷阱 1:自动 range check 的隐藏成本

// 看起来便宜,实际有 32 个约束
let x: u32 = some_field as u32;

养成读 ACIR 的习惯:nargo compile && cat target/x.json | jq '.opcodes | length'

陷阱 2:unsafe 关键字

Noir 提供 unsafe { ... } 块(叫 unconstrained 函数 / Brillig),用于不需要 ZK 约束的预计算(如除法、平方根)。滥用会导致 underconstrained 漏洞

// SAFE: x and y both constrained
let y = x * x;

// UNSAFE: y is set in Brillig but no constraint relates it to x
unsafe { let y = sqrt(x); }
// 必须额外加  assert(y * y == x)

陷阱 3:版本变化大

Noir 0.x 仍在快速迭代,0.30 → 0.36 syntax 改了好几处。生产项目要 pin compiler version。


生产经验

  • Aztec Connect 用 Noir:实现了 zkSyncAnalogue(私有 DeFi router)。约束总量 ~30M,prove 时间 ~30 sec(专用 prover 服务器)。
  • zkPassport (zkpassport.id) 用 Noir 验证护照芯片签名(RSA + ECDSA),约束 ~5M。
  • debug 友好nargo prove --show-ssa 输出 SSA IR,比 Circom 的 sym trace 易读。

关键速查

nargo new <name>          # 创建项目
nargo check               # 类型 + 语法
nargo test                # 跑 #[test]
nargo execute             # 计算 witness
nargo prove               # 生成 proof
nargo verify              # 验证
nargo compile             # 输出 ACIR
nargo codegen-verifier    # Solidity verifier
nargo info                # 约束统计

面试题

  1. Q: Noir 与 Circom 的核心区别?为什么 Aztec 选 Noir? A: Noir 是 Rust-like,类型系统强(编译期 catch bug)、自动插入 range check、universal setup(PLONK 替代 Groth16)、ACIR IR 可换 backend。Aztec 的 privacy contracts 需要可读、可重用、可组合,这些 Noir 都比 Circom 好。

  2. Q: 什么是 ACIR?它的存在意义是什么? A: Abstract Circuit IR,Noir 编译输出。把 DSL 与 backend 解耦:同一份 ACIR 可以让 Barretenberg 跑 UltraPlonk、Halo2 跑 PLONKish、Plonky2 跑 STARK 风格。类似 LLVM IR。

  3. Q: Noir 中 unsafe { ... } 块是什么?怎么避免漏洞? A: Brillig(unconstrained)函数,witness-only 计算无约束。生产代码必须在 unsafe 后加 assert(...) 验证结果与输入的关系,否则 prover 可以造假。


明日预告

Day 229 — Halo2 实战入门。Rust + halo2_proofs,列设计(advice / fixed / instance),写一个 fibonacci 电路。Halo2 是 zkEVM(Scroll、Privacy & Scaling Explorations)的主力 backend。