返回 Expert 笔记
Expert Day 229

Halo2 实战入门 — Rust + Fibonacci

Halo2 列设计(advice/fixed/instance/selector)、custom gates、lookup、IPA vs KZG

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

日期: 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 zkEVMPSE/Ethereum Foundation zkEVMTaiko(部分)都用 Halo2。

核心数据模型:列 / Column model:

列类型用途例子
adviceprover 填值(private)a, b, c
fixed编译期常量constants, ROM
instancepublic inputblock 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

项目Backendk (典型)rowsprove 时间proof size
Scroll zkEVMHalo2 + KZG~2667M数分钟(GPU)~5 KB
PSE zkEVMHalo2 + KZG~2667M数分钟~5 KB
Taiko (legacy)Halo2~224M~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

维度Halo2Plonky2Circom
Fieldbn254/pastaGoldilocksbn254
HashPoseidonPoseidon-GoldilocksPoseidon
Recursionyes极快 (recursion-friendly field)不友好
Lookupyesyesno
Custom gatesyesyesno
Trusted setupoptionalnoyes (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();

面试题

  1. Q: Halo2 为什么用「列」而不是 R1CS? A: 列模型 + custom gate + lookup 让单约束可以跨行(rotation)、可以分摊(一行多约束)、可以查表(lookup)。同样的电路在 PLONKish 中行数比 R1CS 约束数少 5-10×,且 prover MSM/NTT 更友好。

  2. Q: Halo2 的 IPA vs KZG backend 有什么区别?什么时候选哪个? A: IPA 无 trusted setup(Zcash Orchard 选这个);KZG 需要 universal setup 但 verifier gas 低 5×。zkEVM 选 KZG(要上 L1)。

  3. Q: selectorfixed column 在 Halo2 中有什么区别? A: selector 是特殊的 fixed column,值只能是 0/1,可被优化(不进 polynomial commitment)。普通 fixed column 可以是任意常量。

  4. 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 系统性对比成一张表。