Halo2 实战入门 — Rust + Fibonacci
Halo2 列设计(advice/fixed/instance/selector)、custom gates、lookup、IPA vs KZG
日期: 2026-12-16 方向: ZK工程 / 电路开发 阶段: Phase 4 - ZK电路开发实战 (Day 223-243) 标签: #ZK #Halo2 #Rust #PLONKish #Scroll #zkEVM
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | Halo2 列设计(advice/fixed/instance/selector)、custom gates、lookup、IPA vs KZG |
| 实操 | 用 halo2_proofs 写 Fibonacci 电路,跑 MockProver 测试 |
| 产出 | halo2_fib/(Cargo.toml + src/) |
背景与定位
Halo2 是什么?/ What is Halo2?
- Zcash 团队基于 PLONK + IPA 的 ZK 框架(2020 公布,2021 用于 Orchard 升级)。
- 名字来自 PLONK 的 HAlo + 2 — 同时支持 IPA(无 trusted setup)和 KZG(with setup)两种 backend。
- 目前 zkEVM 主力:Scroll zkEVM、PSE/Ethereum Foundation zkEVM、Taiko(部分)都用 Halo2。
核心数据模型:列 / Column model:
| 列类型 | 用途 | 例子 |
|---|---|---|
| advice | prover 填值(private) | a, b, c |
| fixed | 编译期常量 | constants, ROM |
| instance | public input | block hash, root |
| selector | 启用某个 gate 的开关(特殊 fixed) | s_add, s_mul |
约束以 gate 形式声明:s_add * (a + b - c) === 0,意思是「当 selector s_add = 1 时,a+b 必须等于 c」。
Halo2 vs Circom 心智模型:
- Circom:写一个 template,每行是一个约束。
- Halo2:先定义列(电路宽度),再说哪些行启用哪些 gate。电路就像一张表格。
完整代码实现
Cargo.toml
[package]
name = "halo2_fib"
version = "0.1.0"
edition = "2021"
[dependencies]
halo2_proofs = { git = "https://github.com/zcash/halo2", rev = "v2024_01_31" }
halo2curves = "0.6"
ff = "0.13"
group = "0.13"
rand = "0.8"
[dev-dependencies]
plotters = "0.3"
src/lib.rs — Fibonacci 电路
use halo2_proofs::{
arithmetic::Field,
circuit::{AssignedCell, Layouter, SimpleFloorPlanner, Value},
pasta::Fp,
plonk::{
Advice, Circuit, Column, ConstraintSystem, Error, Instance, Selector,
},
poly::Rotation,
};
/// Fibonacci config:
/// columns: a (advice), b (advice), c (advice), instance, selector
/// gate: a_next = b, b_next = c, c = a + b
#[derive(Clone, Debug)]
pub struct FiboConfig {
pub a: Column<Advice>,
pub b: Column<Advice>,
pub c: Column<Advice>,
pub selector: Selector,
pub instance: Column<Instance>,
}
#[derive(Default)]
pub struct FiboCircuit {
pub a: Value<Fp>,
pub b: Value<Fp>,
}
impl Circuit<Fp> for FiboCircuit {
type Config = FiboConfig;
type FloorPlanner = SimpleFloorPlanner;
fn without_witnesses(&self) -> Self { Self::default() }
fn configure(meta: &mut ConstraintSystem<Fp>) -> Self::Config {
let a = meta.advice_column();
let b = meta.advice_column();
let c = meta.advice_column();
let instance = meta.instance_column();
let selector = meta.selector();
meta.enable_equality(a);
meta.enable_equality(b);
meta.enable_equality(c);
meta.enable_equality(instance);
// Gate: when selector enabled,
// c = a + b
meta.create_gate("fibonacci", |meta| {
let s = meta.query_selector(selector);
let a_v = meta.query_advice(a, Rotation::cur());
let b_v = meta.query_advice(b, Rotation::cur());
let c_v = meta.query_advice(c, Rotation::cur());
vec![s * (a_v + b_v - c_v)]
});
FiboConfig { a, b, c, selector, instance }
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<Fp>,
) -> Result<(), Error> {
// Goal: compute fib(10), expose at instance[0]
let nrows = 10;
let mut cur_a = self.a;
let mut cur_b = self.b;
let mut last_c: Option<AssignedCell<Fp, Fp>> = None;
layouter.assign_region(
|| "fibonacci",
|mut region| {
for offset in 0..nrows {
config.selector.enable(&mut region, offset)?;
let a_cell = region.assign_advice(
|| "a", config.a, offset, || cur_a,
)?;
let b_cell = region.assign_advice(
|| "b", config.b, offset, || cur_b,
)?;
let c_val = cur_a + cur_b;
let c_cell = region.assign_advice(
|| "c", config.c, offset, || c_val,
)?;
cur_a = cur_b;
cur_b = c_val;
last_c = Some(c_cell);
}
Ok(())
},
)?;
// Expose last_c as instance[0]
layouter.constrain_instance(
last_c.unwrap().cell(),
config.instance,
0,
)?;
Ok(())
}
}
src/main.rs — MockProver 测试
use halo2_proofs::{circuit::Value, dev::MockProver, pasta::Fp};
use halo2_fib::FiboCircuit;
fn main() {
let k = 5; // 2^5 = 32 rows, plenty for fib(10)
let a = Fp::from(1);
let b = Fp::from(1);
// fib(2..12) sequence; fib(11) = 89, fib(12) = 144
let expected_last = compute_fib(11);
let circuit = FiboCircuit { a: Value::known(a), b: Value::known(b) };
let public_inputs = vec![Fp::from(expected_last)];
let prover = MockProver::run(k, &circuit, vec![public_inputs]).unwrap();
prover.assert_satisfied();
println!("MockProver passed: fib(11) = {}", expected_last);
}
fn compute_fib(n: usize) -> u64 {
let (mut a, mut b) = (1u64, 1u64);
for _ in 0..n - 1 { let c = a + b; a = b; b = c; }
a
}
运行
$ cargo run --release
Compiling halo2_fib v0.1.0
...
Finished release [optimized] target(s) in 14.8s
Running `target/release/halo2_fib`
MockProver passed: fib(11) = 89
Halo2 关键概念深入
1. 列(Column)
| a | b | c | s | instance
row0 | 1 | 1 | 2 | 1 | -
row1 | 1 | 2 | 3 | 1 | -
row2 | 2 | 3 | 5 | 1 | -
row3 | 3 | 5 | 8 | 1 | -
...
row9 | ... | ... | 89 | 1 | 89 ← public
- 每列长度 =
2^k(FFT 友好) - 多余的行用 0 填充
k选太小 → not enough rows;选太大 → prove 时间翻倍
2. Custom Gate 与 Rotation
meta.create_gate("transition", |meta| {
let s = meta.query_selector(selector);
let a_cur = meta.query_advice(a, Rotation::cur()); // row i
let a_next = meta.query_advice(a, Rotation::next()); // row i+1
vec![s * (a_next - a_cur - 1)] // a[i+1] = a[i] + 1
});
Rotation 让 gate 跨行约束,是 Halo2 比 R1CS 强的地方。
3. Lookup Argument
let table = meta.lookup_table_column();
let advice = meta.advice_column();
meta.lookup("byte range", |meta| {
let v = meta.query_advice(advice, Rotation::cur());
vec![(v, table)]
});
把所有 0..256 的字节加载到 table,然后 lookup 验证 advice 列的值都是字节。
这是 zkEVM 性能的关键:每条 EVM opcode 用 lookup 索引查表,比展开成 thousands of constraints 高效得多。
真实 zkEVM benchmarks
| 项目 | Backend | k (典型) | rows | prove 时间 | proof size |
|---|---|---|---|---|---|
| Scroll zkEVM | Halo2 + KZG | ~26 | 67M | 数分钟(GPU) | ~5 KB |
| PSE zkEVM | Halo2 + KZG | ~26 | 67M | 数分钟 | ~5 KB |
| Taiko (legacy) | Halo2 | ~22 | 4M | ~1 min | ~5 KB |
Scroll batch prover 实测:
- 单 batch ~100 tx, 约束 ~100M
- prover GPU (RTX 4090): 5-10 min
- Solidity verify gas: ~600k
Halo2 vs Plonky2 vs Circom
| 维度 | Halo2 | Plonky2 | Circom |
|---|---|---|---|
| Field | bn254/pasta | Goldilocks | bn254 |
| Hash | Poseidon | Poseidon-Goldilocks | Poseidon |
| Recursion | yes | 极快 (recursion-friendly field) | 不友好 |
| Lookup | yes | yes | no |
| Custom gates | yes | yes | no |
| Trusted setup | optional | no | yes (Groth16) |
| 学习曲线 | 陡 | 陡 | 中等 |
常见陷阱
陷阱 1:region 划分
assign_region 内部多个 column 必须在同一行赋值。跨 region 的引用要通过 copy_advice / constrain_equal。
陷阱 2:k 选错
k = 5 给 32 行;如果 fib 要 100 行,会 panic Plonk(NotEnoughRowsAvailable)。
陷阱 3:Selector 不启用
如果忘记 selector.enable(&mut region, offset),对应 gate 不会被执行(可能让 prover 偷偷跳过约束)。MockProver 会发现,但生产 prover 不一定。
陷阱 4:版本碎片化
Halo2 fork 多:
- zcash/halo2 — 原版,IPA + pasta
- privacy-scaling-explorations/halo2 — KZG + bn254(zkEVM 用)
- axiom-crypto/halo2 — Axiom 优化版
API 略有差异,引入时要选对。
生产经验
- GPU 加速必备:Scroll prover 用 ICICLE/Sppark 在 GPU 跑 MSM/NTT,比 CPU 快 50×。
- Aggregator/recursion:Halo2 支持 recursive aggregation(把多个 proof 压缩成一个)。Scroll 用 ~3 层递归把 100 个 batch proof 聚合成 1 个 final proof 提交 L1。
- debug 工具:
use halo2_proofs::dev::CircuitLayout; CircuitLayout::default().render(k, &circuit, &root).unwrap(); // 输出可视化布局图(PNG)
关键速查
// 几乎每个 Halo2 电路都有的样板
struct Config { a: Column<Advice>, ... }
struct Circuit { ... }
impl Circuit<F> for ... {
fn configure(meta) -> Config { ... }
fn synthesize(&self, config, layouter) -> Result<(), Error> { ... }
}
// 测试
MockProver::run(k, &circuit, vec![public]).unwrap().assert_satisfied();
面试题
-
Q: Halo2 为什么用「列」而不是 R1CS? A: 列模型 + custom gate + lookup 让单约束可以跨行(rotation)、可以分摊(一行多约束)、可以查表(lookup)。同样的电路在 PLONKish 中行数比 R1CS 约束数少 5-10×,且 prover MSM/NTT 更友好。
-
Q: Halo2 的 IPA vs KZG backend 有什么区别?什么时候选哪个? A: IPA 无 trusted setup(Zcash Orchard 选这个);KZG 需要 universal setup 但 verifier gas 低 5×。zkEVM 选 KZG(要上 L1)。
-
Q:
selector与fixed column在 Halo2 中有什么区别? A: selector 是特殊的 fixed column,值只能是 0/1,可被优化(不进 polynomial commitment)。普通 fixed column 可以是任意常量。 -
Q: 为什么 Scroll/PSE 的 zkEVM 选 Halo2 而不是 Plonky2? A: bn254 曲线兼容 Ethereum precompile(直接验证 KZG),Plonky2 用 Goldilocks 没法在 EVM 上 cheap 地 verify。但 Polygon zkEVM 用了不同方案(用 bn254 的 PLONK + STARK 包装)。
明日预告
Day 230 — Week 34 复习:ZK 工具链对比。把 Circom / Noir / Halo2 / Plonky2 / Plonk2 系统性对比成一张表。