Noir 基础 — Aztec 的 ZK DSL
Noir 语法、ACIR、Barretenberg backend、与 Circom 对比
日期: 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
| 维度 | Circom | Noir |
|---|---|---|
| 设计灵感 | 数学 R1CS | Rust |
| 类型 | 单一 felt | u8/u32/u64/Field/Array/Struct |
| 约束运算符 | <== <-- === | 自动(编译期插入) |
| 标准库 | circomlib | std::hash, std::ec |
| Backend | snarkjs (Groth16) | Barretenberg (UltraPlonk) |
| 工具 | circom + snarkjs | nargo (一站式) |
| Setup | 每电路 trusted setup | universal setup(PLONK) |
| 最大约束 | 2^28 | 2^28 |
| 学习曲线 | 陡(量子约束概念) | 缓 |
| 生态 | Tornado/Semaphore/Hermez | Aztec/zkPassport/Aztec Connect |
| Debugger | 仅 sym trace | nargo 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 constraintif/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 constraintassert(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):
| 特性 | UltraPlonk | Groth16 |
|---|---|---|
| Setup | universal | per-circuit |
| Lookup | ✓ | ✗ |
| Custom gates | ✓ | ✗ |
| Proof size | ~1 KB | 800 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
| 操作 | Circom | Noir |
|---|---|---|
| Poseidon-2 hash | 215 约束 | 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 sec | 1.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 # 约束统计
面试题
-
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 好。
-
Q: 什么是 ACIR?它的存在意义是什么? A: Abstract Circuit IR,Noir 编译输出。把 DSL 与 backend 解耦:同一份 ACIR 可以让 Barretenberg 跑 UltraPlonk、Halo2 跑 PLONKish、Plonky2 跑 STARK 风格。类似 LLVM IR。
-
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。