Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

zcash.github.io/halo2

译自 halo2 book:README.md

halo2

用法(Usage)

本仓库包含 halo2_proofshalo2_gadgets 两个 crate,应直接使用它们。

最低支持的 Rust 版本(Minimum Supported Rust Version)

需要 Rust 1.60 或更高版本。

最低支持的 Rust 版本将来可能变更,但变更时会伴随一次次版本号(minor version)的提升。

控制并行(Controlling parallelism)

halo2 目前使用 rayon 进行并行计算。可以通过 RAYON_NUM_THREADS 环境变量来设置线程数。

你可以通过禁用 "multicore" 特性(feature)来禁用 rayon。 警告!如果禁用 "multicore" 特性,Halo2 将无法使用并行能力。 这会显著降低性能。

许可证(License)

按以下任一许可证授权

  • Apache License, Version 2.0, (LICENSE-APACHE 或 http://www.apache.org/licenses/LICENSE-2.0)
  • MIT license (LICENSE-MIT 或 http://opensource.org/licenses/MIT)

由你自行选择。

贡献(Contribution)

除非你明确另行声明,否则你有意提交以纳入本作品的任何贡献(按 Apache-2.0 许可证中的定义),都将如上所述进行双重许可,且不附加任何额外条款或条件。

译自 halo2 book:concepts.md

概念

我们首先介绍零知识证明系统(zero-knowledge proof system)背后的概念;Halo 2 所使用的算术化(arithmetization)(一种电路描述方式);以及我们用来构建电路实现的各种抽象。

译自 halo2 book:concepts/proofs.md

证明系统

任何证明系统(proof system)的目标都是能够证明有意思的数学或密码学陈述(statement)

通常,在一个给定的协议中,我们会想要证明一族陈述,这些陈述的区别在于它们的公开输入(public inputs)。证明者(prover)还需要表明他们知道某些使该陈述成立的私有输入(private inputs)

为此,我们写下一个关系(relation) ,它指定了公开输入与私有输入的哪些组合是有效的。

上述术语意在与 ZKProof Community Reference 保持一致。

准确地说,我们应当区分关系 与它在证明系统中所用的实现。我们称后者为电路(circuit)

我们用来为特定证明系统表达电路的语言被称为算术化(arithmetization)。通常,一种算术化会以某个域(field)上变量的多项式约束(polynomial constraints)的形式来定义电路。

将某个特定关系表达为电路的这一_过程_有时也被称为“算术化”,但我们会避免这种用法。

为了创建对某个陈述的证明,证明者需要知道私有输入,以及电路所使用的中间值,这些中间值被称为***建议(advice)***值。

我们假设可以根据私有输入和公开输入高效地计算出建议值。具体的建议值取决于我们如何编写电路,而不仅仅取决于高层的陈述。

私有输入与建议值合起来被称为见证(witness)

有些作者把“见证”仅仅当作私有输入的同义词。但在我们的用法中,见证包含建议值,即它包含证明者提供给电路的所有值。

例如,假设我们想要证明知道一个哈希函数 对于摘要 的某个原像(preimage)

  • 私有输入是原像

  • 公开输入是摘要

  • 关系是

  • 对于某个特定的公开输入 ,陈述是:

  • 建议值是实现该哈希函数的电路中的所有中间值。见证则是 加上建议值。

非交互式论证(Non-interactive Argument)允许证明者(prover)为给定的陈述和见证创建一个证明(proof)。证明是一些数据,可用来使验证者(verifier)相信:_存在_一个见证使该陈述成立。使这类证明无法虚假地说服验证者的那条安全性质称为可靠性(soundness)

非交互式知识论证(Non-interactive Argument of Knowledge)NARK)进一步使验证者相信证明者_知道_一个使该陈述成立的见证。这条安全性质称为知识可靠性(knowledge soundness),它蕴含可靠性。

在实践中,知识可靠性对密码学协议比可靠性更有用:例如,若我们关心 Alice 在某个协议中是否持有一个密钥,我们需要 Alice 证明_她知道_这个密钥,而不仅仅是它存在。

知识可靠性的形式化表述是:必须存在一个能够精确观察证明是如何生成的提取器(extractor),它能够计算出见证。

鉴于证明可能是可塑的(malleable),这条性质相当微妙。也就是说,依赖于具体的证明系统,可能可以在不知道见证的情况下,取一个已有的证明(或一组证明)并对其进行修改,从而产生一个针对相同或相关陈述的不同证明。使用可塑证明系统的更高层协议需要把这一点考虑进去。

即便没有可塑性,证明也可能被重放(replayed)。例如,在我们的例子中,我们不希望 Alice 能够出示由别人生成的证明,并使其被当作她知道该密钥的凭证。

如果一个证明除了表明见证存在且为证明者所知之外,不泄露关于见证的任何信息,那么我们就说该证明系统是***零知识(zero knowledge)***的。

如果一个证明系统产生简短的证明——即长度关于电路规模是多对数(polylogarithmic)的——那么我们就说它是简洁(succinct)的。简洁的 NARK 被称为SNARK简洁非交互式知识论证(Succinct Non-Interactive Argument of Knowledge))。

按照这个定义,SNARK 不一定要求验证时间关于电路规模是多对数的。有些论文用***高效(efficient)***一词来描述具有该性质的 SNARK,但我们会避免使用这个词,因为对于支持摊销式或递归式验证的 SNARK(后面会谈到),它含义不清。

zk-SNARK 是一种零知识的 SNARK。

译自 halo2 book:concepts/arithmetization.md

PLONKish 算术化

Halo 2 所使用的算术化(arithmetization)来自 PLONK,更确切地说是它的扩展 UltraPLONK,后者支持自定义门(custom gates)和查找论证(lookup arguments)。我们称之为 PLONKish

PLONKish 电路是以一个值的矩形矩阵来定义的。我们用通常的含义来指代该矩阵的行(rows)列(columns)单元格(cells)

一个 PLONKish 电路依赖于一个配置(configuration)

  • 一个有限域(finite field) ,其中单元格的值(对于给定的陈述和见证)将是 中的元素。

  • 矩阵中列的数量,以及对每一列的规约——它是固定(fixed)、***建议(advice)还是实例(instance)***列。固定列由电路固定;建议列对应见证值;实例列通常用于公开输入(从技术上讲,它们可用于证明者与验证者之间共享的任何元素)。

  • 可以参与相等约束(equality constraints)的列的一个子集。

  • 一个最大约束次数(maximum constraint degree)

  • 一系列多项式约束(polynomial constraints)。这些是 上的多元多项式(multivariate polynomials),它们对每一行都必须求值为零。多项式约束中的变量可以指代当前行某给定列中的一个单元格,或相对于当前行的另一行某给定列中的单元格(带回绕(wrap-around),即对 取模)。每个多项式的最大次数由最大约束次数给出。

  • 一系列查找论证(lookup arguments),它们定义在输入表达式(input expressions)(如上所述的多元多项式)和***表列(table columns)***的元组之上。

一个 PLONKish 电路还定义:

  • 矩阵中的行数 必须对应于 的某个乘法子群(multiplicative subgroup)的大小;通常是 2 的幂。

  • 一系列相等约束(equality constraints),它们指定两个给定的单元格必须具有相等的值。

  • 各行处固定列的值。

根据一个电路描述,我们可以生成一个证明密钥(proving key)和一个验证密钥(verification key),它们是对该电路进行证明和验证操作所需要的。

注意,我们指定了列、多项式约束、查找论证和相等约束的顺序,尽管这些顺序并不影响电路的含义。这样做使得我们更容易将证明密钥和验证密钥的生成定义为一个确定性的过程。

通常,一个配置会定义一些多项式约束,这些约束由定义在固定列中的选择子(selectors)来开启和关闭。例如,约束 可以通过令 来对某个特定的行 关闭。在这种情况下,我们有时会把一组由若干选择子列控制、被设计为一起使用的约束称为一个门(gate)。通常会有一个标准门(standard gate),它支持诸如域乘法和域除法之类的通用操作,可能还会有支持更专门化操作的自定义门(custom gates)

译自 halo2 book:concepts/chips.md

芯片

上一节给出了对电路相当底层的描述。在实现电路时,我们通常会使用一个更高层的 API,它旨在追求可审计性(auditability)、效率(efficiency)、模块化(modularity)和表达力(expressiveness)等理想特性。

这个 API 中使用的一些术语和概念取自与集成电路设计和布局的类比。正如对集成电路而言,通过组合那些为特定功能提供高效预制实现的芯片(chips),上述理想特性更容易获得。

例如,我们可能会有实现特定密码学原语的芯片,比如哈希函数或密码(cipher),或者诸如标量乘法(scalar multiplication)或配对(pairings)之类的算法。

在 PLONKish 电路中,仅凭做域乘法和域加法的标准门(standard gates)就可以搭建出任意的逻辑。然而,使用自定义门(custom gates)可以获得非常显著的效率提升。

利用我们的 API,我们定义那些“知道”如何使用特定自定义门集合的芯片。这创建了一个抽象层,将高层电路的实现与直接使用自定义门的复杂性隔离开来。

即便我们有时需要“身兼两职”,既实现一个高层电路,又实现它所使用的芯片,其用意在于:这种分离会带来更易于理解、审计、维护/复用的代码。部分原因在于,某些潜在的实现错误会由于这种构造方式而被排除掉。

PLONKish 电路中的门通过相对引用(relative references)来指代单元格,即指代某给定列、相对于门的选择子被设置的那一行处于某给定偏移的行中的单元格。当偏移非零时,我们称之为偏移引用(offset reference)(即偏移引用是相对引用的一个子集)。

相对引用与相等约束中使用的***绝对引用(absolute references)***形成对比,绝对引用可以指向任意单元格。

引入偏移引用的动机是减少配置中所需列的数量,从而减小证明大小。如果我们没有偏移引用,那么我们就需要一列来保存自定义门所引用的每一个值,并且需要使用相等约束把电路中其他单元格的值复制到该列中。有了偏移引用,我们不仅需要更少的列;而且也不需要对所有这些列都支持相等约束,这提升了效率。

在 R1CS(另一种算术化,某些读者可能更熟悉它,不熟悉也没关系)中,一个电路由一片“门之海(sea of gates)”构成,它们没有语义上有意义的顺序。而由于偏移引用,PLONKish 电路中行的顺序有意义的。我们将做一些简化假设并定义一些抽象,以驯服由此产生的复杂性:目标是,在小工具层级(gadget level)——即我们进行大部分电路构造的地方——我们将不必显式地处理相对引用或门的布局。

我们会把一个电路划分为若干区域(regions),每个区域包含一个互不相交的单元格子集,并且相对引用永远只指向某个区域内部。芯片实现的部分职责就是确保进行偏移引用的门被布局在某个区域内的正确位置上。

给定各区域及其***形状(shapes)的集合,我们将使用一个独立的布局规划器(floor planner)***来决定每个区域被放置在何处(即放在哪一行作为起始行)。有一个默认的布局规划器,它实现了一个非常通用的算法,但如果需要,你也可以编写自己的布局规划器。

布局规划一般会在矩阵中留下空隙,因为某给定行中的门并没有用到所有可用的列。这些空隙会——尽可能地——由那些不需要偏移引用的门来填补,这些门可以被放置在任意行上。

芯片也可以定义查找表(lookup tables)。如果对同一个查找论证定义了不止一个表,我们可以使用一个***标签列(tag column)***来指定每一行使用哪个表。也可以在若干个表的并集中执行查找(受多项式次数上界的限制)。

组合芯片

为了组合来自若干芯片的功能,我们把它们组合成一棵树。顶层芯片定义一组固定列、建议列和实例列,然后指定这些列应如何在更底层的芯片之间分配。

在最简单的情形下,每个更底层的芯片将使用与其他芯片互不相交的列。然而,也允许在芯片之间共享某一列。尤其重要的是要优化建议列的数量,因为它会影响证明大小。

其结果(可能在优化之后)是一个 PLONKish 配置。我们的电路实现将以一个芯片作为参数,并且可以通过顶层芯片使用所支持的更底层芯片的任何特性。

我们的期望是,不那么专业的用户通常能够找到一个已有的、支持其所需操作的芯片,或者只需对已有芯片做些微小修改。专家用户则将拥有完全的控制权,去做那种 ECC 公司以之闻名电路优化 🙂。

译自 halo2 book:concepts/gadgets.md

小工具

在实现一个电路时,我们本可以直接使用我们所选芯片(chips)的特性。不过,通常我们会通过***小工具(gadgets)***来使用它们。这种间接层是有用的,因为出于效率原因以及 PLONKish 电路所施加的限制,芯片接口往往会依赖于底层的实现细节。小工具接口可以提供一个更便利、更稳定的 API,把那些无关紧要的细节抽象掉。

例如,考虑一个像 SHA-256 这样的哈希函数。一个支持 SHA-256 的芯片的接口可能依赖于该哈希函数设计的内部细节,比如消息调度(message schedule)与压缩函数(compression function)之间的划分。相应的小工具接口可以提供一个更便利、更熟悉的 update/finalize API,并且还可以处理哈希函数中不需要芯片支持的部分,比如填充(padding)。这类似于 CPU 上密码学原语的加速指令通常是通过软件库来访问,而不是直接访问。

小工具还可以为更高层级的电路编程提供模块化、可复用的抽象,类似于它们在 libsnarkbellman 等库中的用法。除了抽象函数之外,它们还可以抽象类型,比如椭圆曲线点(elliptic curve points)或特定大小的整数。

译自 halo2 book:user.md

用户文档

你来这里大概是想写电路(circuit)吧?太好了!

本节将引导你完成用 halo2 创建电路的整个流程。

译自 halo2 book:user/dev-tools.md

开发者工具

halo2 crate 包含若干实用工具,帮助你设计和实现电路(circuit)。

模拟证明器(Mock prover)

halo2_proofs::dev::MockProver 是一个用于调试电路的工具,也可以在单元测试中以很低的成本验证电路的正确性。电路的私有输入和公开输入按照通常生成证明(proof)的方式构造,但 MockProver::run 不会真的生成证明,而是创建一个对象来直接测试电路中的每一条约束(constraint)。它会返回细粒度的错误消息,指出具体是哪一条约束(如果有的话)未被满足。

电路可视化

dev-graph 特性标志(feature flag)暴露了若干辅助方法,用于创建电路的图形化表示。

在 Debian 系统上,你需要安装以下额外的软件包:

sudo apt install cmake libexpat1-dev libfreetype6-dev libcairo2-dev

电路布局(Circuit layout)

halo2_proofs::dev::CircuitLayout 将电路布局渲染为一张网格:

fn main() {
    // Prepare the circuit you want to render.
    // You don't need to include any witness variables.
    let a = Fp::random(OsRng);
    let instance = Fp::ONE + Fp::ONE;
    let lookup_table = vec![instance, a, a, Fp::zero()];
    let circuit: MyCircuit<Fp> = MyCircuit {
        a: Value::unknown(),
        lookup_table,
    };

    // Create the area you want to draw on.
    // Use SVGBackend if you want to render to .svg instead.
    use plotters::prelude::*;
    let root = BitMapBackend::new("layout.png", (1024, 768)).into_drawing_area();
    root.fill(&WHITE).unwrap();
    let root = root
        .titled("Example Circuit Layout", ("sans-serif", 60))
        .unwrap();

    halo2_proofs::dev::CircuitLayout::default()
        // You can optionally render only a section of the circuit.
        .view_width(0..2)
        .view_height(0..16)
        // You can hide labels, which can be useful with smaller areas.
        .show_labels(false)
        // Render the circuit onto your area!
        // The first argument is the size parameter for the circuit.
        .render(5, &circuit, &root)
        .unwrap();
}
  • 列(column)从左到右依次排布为 instance、advice 和 fixed。除此之外,列的顺序没有特定含义。
    • Instance 列采用白色背景。
    • Advice 列采用红色背景。
    • Fixed 列采用蓝色背景。
  • 区域(region)显示为带标签的绿色方框(叠加在背景色之上)。如果某个区域的部分列恰好不相邻,它可能会显示为多个方框。
  • 被电路赋值过的单元格(cell)会被涂成灰色。如果某些单元格被赋值了不止一次(这通常是个错误),它们会被涂得比周围的单元格更深。

电路结构

halo2_proofs::dev::circuit_dot_graph 会构建一个表示给定电路的 DOT 图字符串,然后可以用各种布局程序来渲染它。该图是根据电路内部以及它所使用的小工具(gadget)和芯片(chip)内部对 Layouter::namespace 的调用构建的。

fn main() {
    // Prepare the circuit you want to render.
    // You don't need to include any witness variables.
    let a = Fp::rand();
    let instance = Fp::one() + Fp::one();
    let lookup_table = vec![instance, a, a, Fp::zero()];
    let circuit: MyCircuit<Fp> = MyCircuit {
        a: None,
        lookup_table,
    };

    // Generate the DOT graph string.
    let dot_string = halo2_proofs::dev::circuit_dot_graph(&circuit);

    // Now you can either handle it in Rust, or just
    // print it out to use with command-line tools.
    print!("{}", dot_string);
}

成本估算器(Cost estimator)

cost-model 二进制程序接收电路设计的高层参数,并估算验证成本以及最终的证明大小。

Usage: cargo run --example cost-model -- [OPTIONS] k

Positional arguments:
  k                       2^K bound on the number of rows.

Optional arguments:
  -h, --help              Print this message.
  -a, --advice R[,R..]    An advice column with the given rotations. May be repeated.
  -i, --instance R[,R..]  An instance column with the given rotations. May be repeated.
  -f, --fixed R[,R..]     A fixed column with the given rotations. May be repeated.
  -g, --gate-degree D     Maximum degree of the custom gates.
  -l, --lookup N,I,T      A lookup over N columns with max input degree I and max table degree T. May be repeated.
  -p, --permutation N     A permutation over N columns. May be repeated.

例如,要估算一个具有三个 advice 列和一个 fixed 列(带各种旋转(rotation))、且自定义门(custom gate)最大次数(degree)为 4 的电路的成本:

> cargo run --example cost-model -- -a 0,1 -a 0 -a-0,-1,1 -f 0 -g 4 11
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/examples/cost-model -a 0,1 -a 0 -a 0,-1,1 -f 0 -g 4 11`
Circuit {
    k: 11,
    max_deg: 4,
    advice_columns: 3,
    lookups: 0,
    permutations: [],
    column_queries: 7,
    point_sets: 3,
    estimator: Estimator,
}
Proof size: 1440 bytes
Verification: at least 81.689ms

译自 halo2 book:user/simple-example.md

一个简单的示例

让我们从一个简单的电路(circuit)开始,向你介绍常用的 API 及其使用方式。这个电路将接收一个公开输入 ,并证明它知道两个满足如下关系的私有输入

其中 是一个固定的数(域元素),例如

定义指令(instructions)

首先,我们需要定义电路所依赖的指令(instruction)。指令是高层小工具(gadget)与底层电路操作之间的边界。指令可以是粗粒度的,也可以是细粒度的,但在实践中你需要在两者之间取得平衡:指令要足够大,以便能有效地优化其实现;同时又要足够小,以便有意义地被复用。

对于我们的电路,我们将使用三个指令:

  • 将一个私有数值加载到电路中。
  • 将两个数相乘。
  • 将一个数作为公开输入暴露给电路。

我们还需要一个类型来表示一个数的变量。指令接口为它们的输入和输出提供了关联类型(associated type),使得各种实现可以以最符合其优化目标的方式来表示这些类型。

trait NumericInstructions<F: Field>: Chip<F> {
    /// Variable representing a number.
    type Num;

    /// Loads a number into the circuit as a private input.
    fn load_private(&self, layouter: impl Layouter<F>, a: Value<F>) -> Result<Self::Num, Error>;

    /// Loads a number into the circuit as a fixed constant.
    fn load_constant(&self, layouter: impl Layouter<F>, constant: F) -> Result<Self::Num, Error>;

    /// Returns `c = a * b`.
    fn mul(
        &self,
        layouter: impl Layouter<F>,
        a: Self::Num,
        b: Self::Num,
    ) -> Result<Self::Num, Error>;

    /// Exposes a number as a public input to the circuit.
    fn expose_public(
        &self,
        layouter: impl Layouter<F>,
        num: Self::Num,
        row: usize,
    ) -> Result<(), Error>;
}

定义芯片(chip)实现

对于我们的电路,我们将构建一个芯片(chip),为有限域提供上述数值指令。

/// The chip that will implement our instructions! Chips store their own
/// config, as well as type markers if necessary.
struct FieldChip<F: Field> {
    config: FieldConfig,
    _marker: PhantomData<F>,
}

每个芯片都需要实现 Chip trait。它定义了芯片的属性,布局器(Layouter)在综合(synthesize)电路时可以依赖这些属性,同时也使得芯片所需的任何初始状态能够被加载到电路中。

impl<F: Field> Chip<F> for FieldChip<F> {
    type Config = FieldConfig;
    type Loaded = ();

    fn config(&self) -> &Self::Config {
        &self.config
    }

    fn loaded(&self) -> &Self::Loaded {
        &()
    }
}

配置芯片

芯片需要配置好列(column)、置换(permutation)和门(gate),这些是实现所有所需指令所必需的。

/// Chip state is stored in a config struct. This is generated by the chip
/// during configuration, and then stored inside the chip.
#[derive(Clone, Debug)]
struct FieldConfig {
    /// For this chip, we will use two advice columns to implement our instructions.
    /// These are also the columns through which we communicate with other parts of
    /// the circuit.
    advice: [Column<Advice>; 2],

    /// This is the public input (instance) column.
    instance: Column<Instance>,

    // We need a selector to enable the multiplication gate, so that we aren't placing
    // any constraints on cells where `NumericInstructions::mul` is not being used.
    // This is important when building larger circuits, where columns are used by
    // multiple sets of instructions.
    s_mul: Selector,
}

impl<F: Field> FieldChip<F> {
    fn construct(config: <Self as Chip<F>>::Config) -> Self {
        Self {
            config,
            _marker: PhantomData,
        }
    }

    fn configure(
        meta: &mut ConstraintSystem<F>,
        advice: [Column<Advice>; 2],
        instance: Column<Instance>,
        constant: Column<Fixed>,
    ) -> <Self as Chip<F>>::Config {
        meta.enable_equality(instance);
        meta.enable_constant(constant);
        for column in &advice {
            meta.enable_equality(*column);
        }
        let s_mul = meta.selector();

        // Define our multiplication gate!
        meta.create_gate("mul", |meta| {
            // To implement multiplication, we need three advice cells and a selector
            // cell. We arrange them like so:
            //
            // | a0  | a1  | s_mul |
            // |-----|-----|-------|
            // | lhs | rhs | s_mul |
            // | out |     |       |
            //
            // Gates may refer to any relative offsets we want, but each distinct
            // offset adds a cost to the proof. The most common offsets are 0 (the
            // current row), 1 (the next row), and -1 (the previous row), for which
            // `Rotation` has specific constructors.
            let lhs = meta.query_advice(advice[0], Rotation::cur());
            let rhs = meta.query_advice(advice[1], Rotation::cur());
            let out = meta.query_advice(advice[0], Rotation::next());
            let s_mul = meta.query_selector(s_mul);

            // Finally, we return the polynomial expressions that constrain this gate.
            // For our multiplication gate, we only need a single polynomial constraint.
            //
            // The polynomial expressions returned from `create_gate` will be
            // constrained by the proving system to equal zero. Our expression
            // has the following properties:
            // - When s_mul = 0, any value is allowed in lhs, rhs, and out.
            // - When s_mul != 0, this constrains lhs * rhs = out.
            vec![s_mul * (lhs * rhs - out)]
        });

        FieldConfig {
            advice,
            instance,
            s_mul,
        }
    }
}

实现芯片 trait

/// A variable representing a number.
#[derive(Clone)]
struct Number<F: Field>(AssignedCell<F, F>);

impl<F: Field> NumericInstructions<F> for FieldChip<F> {
    type Num = Number<F>;

    fn load_private(
        &self,
        mut layouter: impl Layouter<F>,
        value: Value<F>,
    ) -> Result<Self::Num, Error> {
        let config = self.config();

        layouter.assign_region(
            || "load private",
            |mut region| {
                region
                    .assign_advice(|| "private input", config.advice[0], 0, || value)
                    .map(Number)
            },
        )
    }

    fn load_constant(
        &self,
        mut layouter: impl Layouter<F>,
        constant: F,
    ) -> Result<Self::Num, Error> {
        let config = self.config();

        layouter.assign_region(
            || "load constant",
            |mut region| {
                region
                    .assign_advice_from_constant(|| "constant value", config.advice[0], 0, constant)
                    .map(Number)
            },
        )
    }

    fn mul(
        &self,
        mut layouter: impl Layouter<F>,
        a: Self::Num,
        b: Self::Num,
    ) -> Result<Self::Num, Error> {
        let config = self.config();

        layouter.assign_region(
            || "mul",
            |mut region: Region<'_, F>| {
                // We only want to use a single multiplication gate in this region,
                // so we enable it at region offset 0; this means it will constrain
                // cells at offsets 0 and 1.
                config.s_mul.enable(&mut region, 0)?;

                // The inputs we've been given could be located anywhere in the circuit,
                // but we can only rely on relative offsets inside this region. So we
                // assign new cells inside the region and constrain them to have the
                // same values as the inputs.
                a.0.copy_advice(|| "lhs", &mut region, config.advice[0], 0)?;
                b.0.copy_advice(|| "rhs", &mut region, config.advice[1], 0)?;

                // Now we can assign the multiplication result, which is to be assigned
                // into the output position.
                let value = a.0.value().copied() * b.0.value();

                // Finally, we do the assignment to the output, returning a
                // variable to be used in another part of the circuit.
                region
                    .assign_advice(|| "lhs * rhs", config.advice[0], 1, || value)
                    .map(Number)
            },
        )
    }

    fn expose_public(
        &self,
        mut layouter: impl Layouter<F>,
        num: Self::Num,
        row: usize,
    ) -> Result<(), Error> {
        let config = self.config();

        layouter.constrain_instance(num.0.cell(), config.instance, row)
    }
}

构建电路

现在我们已经有了所需的指令,以及一个实现了这些指令的芯片,终于可以构建我们的电路了!

/// The full circuit implementation.
///
/// In this struct we store the private input variables. We use `Option<F>` because
/// they won't have any value during key generation. During proving, if any of these
/// were `None` we would get an error.
#[derive(Default)]
struct MyCircuit<F: Field> {
    constant: F,
    a: Value<F>,
    b: Value<F>,
}

impl<F: Field> Circuit<F> for MyCircuit<F> {
    // Since we are using a single chip for everything, we can just reuse its config.
    type Config = FieldConfig;
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        Self::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        // We create the two advice columns that FieldChip uses for I/O.
        let advice = [meta.advice_column(), meta.advice_column()];

        // We also need an instance column to store public inputs.
        let instance = meta.instance_column();

        // Create a fixed column to load constants.
        let constant = meta.fixed_column();

        FieldChip::configure(meta, advice, instance, constant)
    }

    fn synthesize(
        &self,
        config: Self::Config,
        mut layouter: impl Layouter<F>,
    ) -> Result<(), Error> {
        let field_chip = FieldChip::<F>::construct(config);

        // Load our private values into the circuit.
        let a = field_chip.load_private(layouter.namespace(|| "load a"), self.a)?;
        let b = field_chip.load_private(layouter.namespace(|| "load b"), self.b)?;

        // Load the constant factor into the circuit.
        let constant =
            field_chip.load_constant(layouter.namespace(|| "load constant"), self.constant)?;

        // We only have access to plain multiplication.
        // We could implement our circuit as:
        //     asq  = a*a
        //     bsq  = b*b
        //     absq = asq*bsq
        //     c    = constant*asq*bsq
        //
        // but it's more efficient to implement it as:
        //     ab   = a*b
        //     absq = ab^2
        //     c    = constant*absq
        let ab = field_chip.mul(layouter.namespace(|| "a * b"), a, b)?;
        let absq = field_chip.mul(layouter.namespace(|| "ab * ab"), ab.clone(), ab)?;
        let c = field_chip.mul(layouter.namespace(|| "constant * absq"), constant, absq)?;

        // Expose the result as a public input to the circuit.
        field_chip.expose_public(layouter.namespace(|| "expose c"), c, 0)
    }
}

测试电路

halo2_proofs::dev::MockProver 可用于测试电路是否正确工作。电路的私有输入和公开输入按照我们生成证明时的方式构造,但通过把它们传给 MockProver::run,我们会得到一个对象,它可以测试电路中的每一条约束(constraint),并准确告诉我们哪里出了问题(如果有的话)。

    // The number of rows in our circuit cannot exceed 2^k. Since our example
    // circuit is very small, we can pick a very small value here.
    let k = 4;

    // Prepare the private and public inputs to the circuit!
    let constant = Fp::from(7);
    let a = Fp::from(2);
    let b = Fp::from(3);
    let c = constant * a.square() * b.square();

    // Instantiate the circuit with the private inputs.
    let circuit = MyCircuit {
        constant,
        a: Value::known(a),
        b: Value::known(b),
    };

    // Arrange the public input. We expose the multiplication result in row 0
    // of the instance column, so we position it there in our public inputs.
    let mut public_inputs = vec![c];

    // Given the correct public input, our circuit will verify.
    let prover = MockProver::run(k, &circuit, vec![public_inputs.clone()]).unwrap();
    assert_eq!(prover.verify(), Ok(()));

    // If we try some other public input, the proof will fail!
    public_inputs[0] += Fp::one();
    let prover = MockProver::run(k, &circuit, vec![public_inputs]).unwrap();
    assert!(prover.verify().is_err());

完整示例

你可以在这里找到本示例的源代码。

译自 halo2 book:user/lookup-tables.md

查表(lookup tables)

在普通程序中,你可以通过预计算并存储某部分计算的查表(lookup table),用内存换取 CPU 来提升性能。在 halo2 电路(circuit)中我们也可以做同样的事情!

可以把查表理解为在变量之间强制实施一种关系(relation),这种关系以表的形式表达。假设我们的约束系统(constraint system)中只有一个查表论证(lookup argument),那么表的总大小受电路大小的限制:每一个表项占用一行,而每一次查表(lookup)也要占用一行。

TODO

译自 halo2 book:user/gadgets.md

小工具(Gadgets)

译自 halo2 book:user/tips-and-tricks.md

技巧与窍门

本节包含各种你在编写 halo2 电路(circuit)时可能会觉得有用的想法和代码片段。

小范围约束

R1CS 电路中常用的一种约束是布尔约束(boolean constraint):。这个约束只能被 满足。

在 halo2 电路中,你可以用类似的方式约束某个单元格(cell)取一小组值中的某一个。例如,要把 约束到范围 ,你可以创建如下形式的门(gate):

而要把 约束为 7 或 13,你可以使用:

这里的底层原理是:我们构造一个多项式约束(polynomial constraint),让它在我们想要允许的那组可能取值中的每一个值处都有一个根。在 R1CS 电路中,所支持的多项式最大次数(degree)为 2(因为所有约束都是 的形式)。在 halo2 电路中,你可以使用任意次数的多项式——但前提是,次数越高的约束使用起来成本越高。

注意,这些根不必是常数;例如 会把 约束为等于 中的某一个,其中后者可以是任意多项式,只要整个表达式保持在最大次数界限之内即可。

小集合插值

我们可以使用拉格朗日插值(Lagrange interpolation)构造一个多项式约束,对于小集合 实现映射

例如,假设我们想把一个 2 比特的值映射到一个与零交错排列的"展开(spread)"版本。我们首先预计算每个点处的取值:

然后,我们使用如下恒等式为每个点构造拉格朗日基多项式(Lagrange basis polynomial): 其中 是数据点的个数。(在上面的例子中 。)

回顾一下,拉格朗日基多项式 处取值为 ,在所有其他 处取值为

继续我们的例子,我们得到四个拉格朗日基多项式:

于是我们的多项式约束为

译自 halo2 book:user/wasm-port.md

在 WASM 中使用 halo2

由于 halo2 是用 Rust 编写的,你可以把它编译成 WebAssembly (wasm),这样你就能在浏览器应用中为你的电路(circuit)使用证明器(prover)和验证器(verifier)了。本教程会带你了解将电路编译成 wasm 所需的全部知识。

在整个教程中,我们会参考 Zordle 的代码仓库作为示例,它是已知最早一批基于 Halo 2 电路的 webapp 之一。Zordle 是 ZK 版的 Wordle,其电路把玩家输入的单词以及玩家的分享格子(灰色、黄色和绿色方块)作为 advice 值,并验证它们是否正确匹配。因此,该证明(proof)验证了玩家知道输出分享表的一个"原像(preimage)",之后仅凭这份 ZK 证明就可以完成验证。

电路代码设置

第一步是在 Rust 中创建与浏览器应用对接的函数。对于证明器,这通常会输入某种形式的 advice 和 instance 数据,用它生成完整的见证(witness),然后输出一份证明。对于验证器,这通常会输入一份证明和某种形式的 instance,然后输出一个布尔值,表示该证明是否验证通过。

在 Zordle 的例子里,这部分代码位于 wasm.rs,由两个主要函数组成:

证明器(Prover)

#[wasm_bindgen]
pub async fn prove_play(final_word: String, words_js: JsValue, params_ser: JsValue) -> JsValue {
  // Steps:
  // - Deserialise function parameters
  // - Generate the instance and advice columns using the words
  // - Instantiate the circuit and generate the witness
  // - Generate the proving key from the params
  // - Create a proof
}

虽然具体的输入及其序列化方式取决于你的电路和 webapp 设置,但了解 Zordle 这个特定场景下的格式还是有用的,因为你的使用场景很可能与之类似:

这个函数把用户想要凑出的 final_word,以及他们尝试使用的单词(以 words_js 的形式)作为输入。它还把电路的参数作为输入,这些参数序列化在 params_ser 中。我们将在下面的 Params 小节中展开说明。

注意,函数参数是以与 wasm_bindgen 兼容的格式传入的:StringJsValueJsValue 类型来自 Serde 库。你可以在这里的文档中找到关于这个类型及其用法的更多细节。

输出是一个用 Serde 转换成 JSValueVec<u8>。之后它会作为输入传给验证器函数。

验证器(Verifier)

#[wasm_bindgen]
pub fn verify_play(final_word: String, proof_js: JsValue, diffs_u64_js: JsValue, params_ser: JsValue) -> bool {
  // Steps:
  // - Deserialise function parameters
  // - Generate the instance columns using the diffs representation of the columns
  // - Generate the verifying key using the params
  // - Verify the proof
}

与证明器类似,我们接收输入并输出一个布尔值 true/false,表示证明的正确性。diffs_u64_js 对象是一个二维 JS 数组,由每个单元格(cell)的值组成,这些值指示颜色:灰色、黄色或绿色。它们被用来组装电路的 instance 列。

参数(Params)

此外,证明器和验证器函数都会输入 params_ser,这是多项式承诺方案(polynomial commitment scheme)的公开参数的序列化形式。作为一种性能优化,这些参数是作为输入传入的(而不是在 prove/verify 函数中重新生成),因为它们仅根据电路的 K 值确定,是常量。我们可以把它们单独存放在一个静态 web 服务器上,然后作为输入传给 WASM。要生成这些参数的二进制序列化形式(在 WASM 函数之外单独生成),你可以运行类似下面的代码:

fn write_params(K: u32) {
    let mut params_file = File::create("params.bin").unwrap();
    let params: Params<EqAffine> = Params::new(K);
    params.write(&mut params_file).unwrap();
}

之后,我们可以在 JavaScript 中从 web 服务器以 Uint8Array 这种字节序列化的格式读取 params.bin 文件,并把它作为 params_ser 传给 WASM,再在 Rust 中使用 js_sys 库进行反序列化。

理想情况下,将来我们应该能够不必序列化这些参数,而是直接序列化并使用电路的证明密钥(proving key)和验证密钥(verifying key),但目前该库尚不支持这一点,相关追踪见 issue #449#443

Rust 与 WASM 环境设置

通常,Rust 代码会用 wasm-pack 工具编译成 WASM,只需改改一些构建命令即可,非常简单。然而,对于 halo2 的证明器/验证器函数,我们需要对构建过程做一些额外的改动。具体来说,主要有两处改动:

  • 并行(Parallelism):halo2 使用 rayon 库来实现并行,而 WASM 并不直接支持它。不过,Chrome 团队提供了一个适配器,可以在浏览器中使用 Web Workers 实现类 rayon 的并行:wasm-bindgen-rayon。我们将用它在 WASM 证明器/验证器中启用并行。
  • WASM 最大内存wasm-bindgen 下 WASM 的默认内存上限被设为 2GB,这不足以为大型电路(K 大于 10 左右)运行 halo2 证明器。我们需要把这个上限提高到 WASM 允许的最大值(4GB!),以支持更大的电路(直到 K = 15 左右)。

首先,把你的 WASM 对接函数所特有的所有依赖添加到 Cargo.toml 文件中。你可以通过 WASM 目标特性标志(target feature flag)把这些依赖限定在 WASM 编译范围内。在 Zordle 的例子中,它看起来是这样的

[target.'cfg(target_family = "wasm")'.dependencies]
getrandom = { version = "0.2", features = ["js"]}
wasm-bindgen = { version = "0.2.81", features = ["serde-serialize"]}
console_error_panic_hook = "0.1.7"
rayon = "1.5"
wasm-bindgen-rayon = { version = "1.0"}
web-sys = { version = "0.3", features = ["Request", "Window", "Response"] }
wasm-bindgen-futures = "0.4"
js-sys = "0.3"

接下来,让我们把 wasm-bindgen-rayon 集成到代码中。该库的 README 很好地概述了如何做到这一点。特别要注意对 Rust 编译流程的改动。你需要切换到 nightly 版本的 Rust,并启用对 WASM atomics 的支持。此外,记得在 Rust 代码中导出 init_thread_pool

接下来,我们要把 wasm-pack 默认的 2GB 最大内存上限提上去。为此,在 .cargo/config 文件中为 wasm 目标添加 Rust 标志 "-C", "link-arg=--max-memory=4294967296"。完成 wasm-bindgen-rayon 的设置并提高内存上限后,.cargo/config 文件现在应该是这样的:

[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals", "-C", "link-arg=--max-memory=4294967296"]
...

特别感谢 @mattgibb,他一个不起眼的 GitHub issue 里记录了这个晦涩的、用于提高最大内存的改动。1

现在 Rust 这边已经设置好了,你应该可以直接用 wasm-pack build --target web --out-dir pkg 构建出一个 WASM 包,并在你的 webapp 中使用输出的 WASM 包了。

Webapp 设置

Zordle 附带了一个极简的 React 测试客户端作为示例(它只是在默认的 create-react-app 模板上加入了 WASM 支持)。你可以在这里找到这个测试客户端的代码。我建议你为自己的应用 fork 这个测试客户端,并以此为起点开始开发。

该测试客户端包含一个干净的 WebWorker,用于与 Rust WASM 包对接。把对接逻辑放进 WebWorker 中可以避免阻塞浏览器主线程,并为 React/应用逻辑提供一个干净的接口。查看 halo-worker.ts 了解 WebWorker 代码,并在 App.tsx 中看看如何从 React 与该 web worker 对接。

如果到目前为止你都做对了,那么你现在应该可以在浏览器中生成并验证证明了!在 Zordle 的例子里,一个 K = 14 的电路在我的笔记本上生成证明大约需要一分钟左右。在生成证明期间,如果你打开 Chrome/Firefox 的任务管理器,应该还能看到类似下面的画面:

Example halo2 proof generation in-browser

Zordle 及其 test-client 默认把并行度设置为机器上可用的核心数。如果你想减少它,可以通过修改 initThreadPool 的参数来实现。

如果你更愿意使用自己的 Worker/React 设置,那么获取并序列化参数、证明以及其他 instance 和 advice 值的代码可能仍然值得一看!

Safari

注意,wasm-bindgen-rayon 库不被 Safari 支持,因为它会从一个 Web Worker 内部再派生(spawn)出新的 Web Workers。根据相关的 Webkit issue,对该特性的支持已于 2022 年 11 月进入 Safari Technology Preview,而且 Safari Technology Preview Release 155 的发行说明确实声称已支持,所以如果这一点对你很重要,值得去确认它是否已经进入正式版 Safari。

调试

你常常会遇到 Rust 代码出问题,看到 WASM 执行时报错 Uncaught (in promise) RuntimeError: unreachable,这是一个对调试完全没有帮助的错误。这是因为代码是以 release 模式编译的,作为性能优化它去掉了错误消息。要调试,你可以用 wasm-pack build 加上 --dev 标志,以 debug 模式构建 WASM 包。这会以 debug 模式构建,会显著拖慢执行速度,但能让你在浏览器控制台中看到任何运行时错误消息。此外,你可以安装 console_error_panic_hook crate(Zordle 就是这么做的),以便在发生运行时 panic 时也能得到有用的调试消息。

致谢

本指南由 Nalin 编写。另外感谢 UmaBlaine 在弄清这些步骤上所做的大量工作。如果你在任何步骤上遇到困难,欢迎随时联系我。


  1. 题外话,但让我相当惊讶的是,WASM 有一个 4GB 内存的硬性上限。这是因为 WASM 目前是 32 位架构,对于这样一门崭新的、面向未来的汇编语言来说,这一点让我颇为意外。不过,确实有一些公开的提案,主张把 WASM 迁移到更大的地址空间

译自 halo2 book:dev.md

开发者文档(Developer Documentation)

你想为 Halo 2 的各个 crate 做贡献?太棒了!

本节介绍我们的开发流程与审查标准的相关信息,以及在维护和扩展代码库时有用的一些技巧。

译自 halo2 book:dev/features.md

特性开发(Feature development)

有时特性开发(feature development)需要随时间不断迭代某个设计。尽早在下游(downstream) crate 中开始使用某个特性,对积累 API 与功能上的使用经验很有帮助,而这些经验又能在特性稳定化之前反馈到它的设计中。为实现这一点,我们采用一种受 Rust 编译器启发(但并不完全相同)的三阶段 nightly -> beta -> stable 开发模式。

特性标志(Feature flags)

每个尚未稳定化的特性都有一个默认关闭的特性标志(feature flag)来启用它,形式为 unstable-*。当该特性标志被禁用时,各 crate 的稳定 API(stable API)绝不能受到影响,除非是某些会按具体情况逐一考量的复杂特性。

我们还提供了两个元标志(meta-flag),用于启用某个稳定化级别的全部特性:

  • beta 启用所有处于 "beta" 阶段的特性(并隐式启用所有处于 "stable" 阶段的特性)。
  • nightly 启用所有处于 "beta" 和 "nightly" 阶段的特性(并隐式启用所有处于 "stable" 阶段的特性),即启用全部特性。
  • 当两个标志都未启用(且没有任何特性专属标志被启用)时,实际上只启用处于 "stable" 阶段的特性。

特性工作流(Feature workflow)

  • 如果维护者们达成了大致共识(rough consensus),认为某个实验性特性总体上是受欢迎的,那么可以乐观地以较低的审查标准,把它的初始实现合并进代码库,并置于一个该特性专属的特性标志之后。该特性的标志会被加入 nightly 特性标志集合。
    • halo2 各 crate 的下一次常规发布(general release)中,该特性即可被下游已发布的 crate 使用。
    • 该特性后续的开发与完善可以通过额外的 PR 就地(in-situ)进行,并配以额外的审查。
    • 如果该特性最终与其他特性(尤其是已稳定化的特性)产生不良交互,那么之后可以将其移除,而不影响 stable 或 beta 的 API。
  • 一旦该特性经过了充分审查,并且达到了某个 halo2 用户认为其已可用于生产(production-ready)(且愿意或计划将其部署到生产环境)的程度,该特性的特性标志就会被移入 beta 特性标志集合。
  • 一旦该特性经过了等同于 stable 审查策略的审查,并且达成了大致共识,认为该特性对更广泛的 halo2 用户群体有用,该特性的特性标志就会被移除,该特性也随之成为主维护代码库的一部分。

对于更复杂的特性,上述工作流可能会辅以 betanightly 分支;这一点将在某个需要此机制的特性被提名为纳入候选时再行确定。

进行中的特性(In-progress features)

特性标志阶段备注
unstable-sha256-gadgetnightlySHA-256 gadget 与 chip。

译自 halo2 book:https://zcash.github.io/halo2/design.html

设计

关于术语的说明

我们使用与他人略有不同的术语来描述 PLONK 概念。以下是概览:

  1. 我们倾向于把类 PLONK 的论证(argument)看作一张表格,其中每一列对应一条"线"(wire)。我们把表格中的条目称为"单元格"(cell)。
  2. 我们倾向于把"选择子多项式"(selector polynomial)之类的东西改称为"固定列"(fixed column)。当一个固定列中的单元格被用来控制某条约束(constraint)是否在该行启用时,我们会专门称之为"选择子约束"(selector constraint)。
  3. 当另一些多项式由证明者(prover)填充时,我们通常把它们称为"建议列"(advice column)。
  4. 我们使用术语"规则"(rule)来指代像下面这样的"门"(gate):
    • TODO:检查我们在这一点上的一致性,并更新代码和文档以保持匹配。

译自 halo2 book:https://zcash.github.io/halo2/design/proving-system.html

证明系统(Proving system)

Halo 2 的证明系统可以分解为五个阶段:

  1. 对编码电路主要组成部分的多项式做承诺(commit):
    • 单元格赋值(cell assignment)。
    • 每个查表论证(lookup argument)的置换值(permuted value)与乘积(product)。
    • 等式约束置换(equality constraint permutation)。
  2. 构造消失论证(vanishing argument),以将所有电路关系约束为零:
    • 标准门(standard gate)与自定义门(custom gate)。
    • 查表论证规则。
    • 等式约束置换规则。
  3. 在所有必要的点上对上述多项式求值:
    • 所有列上、所有自定义门所用的相对旋转(relative rotation)。
    • 消失论证的各个分片。
  4. 构造多点打开论证(multipoint opening argument),以检查所有求值与各自的承诺保持一致。
  5. 运行内积论证(inner product argument),为多点打开论证的多项式创建一个多项式承诺打开证明(polynomial commitment opening proof)。

本章接下来将依次介绍这些阶段。

示例

为了便于讲解,我们有时会引用如下示例约束系统(constraint system):

  • 四个建议列(advice column)
  • 一个固定列(fixed column)
  • 三个自定义门:

tl;dr(太长不看)

下表对 Halo 2 协议给出一个(可能过于)简洁的描述。这一描述很可能会被 Halo 2 论文及其安全性证明所取代,但目前可作为接下来各小节内容的摘要。

Prover(证明者)Verifier(验证者)
检查
构造多点打开多项式

然后证明者与验证者:

  • 使用 的幂次,将 构造为 的线性组合;
  • 构造为 的等价线性组合;并
  • 执行

TODO:撰写提供零知识(zero-knowledge)的协议组件。

译自 halo2 book:https://zcash.github.io/halo2/design/proving-system/lookup.html

查表论证(Lookup argument)

Halo 2 使用以下查表技术,它允许在任意集合中进行查表,并且可以说比 Plookup 更简单。

关于术语的说明

除了关于术语的通用说明之外:

  • 我们把 多项式(置换论证中的大乘积论证多项式,grand product argument polynomial)称为"置换乘积"(permutation product)列。

技术描述

为了便于讲解,我们先描述一个忽略零知识(zero knowledge)的简化版本论证。

我们用一个"子集论证"(subset argument)来表达查表,该论证作用于一张具有 行(从 0 开始编号)、含有列 的表格之上。

子集论证的目标是强制 中的每个单元格都等于 中的_某个_单元格。这意味着 中的多个单元格可以等于 中的_同一个_单元格,而 中的某些单元格不需要等于 中的任何单元格。

  • 可以是固定的,但不必如此。也就是说,我们既支持在固定表中查值,也支持在变量表(后者包含建议列)中查值。
  • 可以包含重复项。如果 和/或 所表示的集合的大小不天然为 ,我们就用重复项扩充 ,并用已知存在于 中的占位值(dummy value)扩充
    • 或者,我们可以添加一个"查表选择子"(lookup selector),用来控制 列中的哪些元素参与查表。这会修改下面置换规则中 的出现:当某行未被选中进行查表时,把 替换为(例如)

为拉格朗日基(Lagrange basis)多项式,它在第 行求值为 ,其余处为

我们先允许证明者提供 的置换列。分别称之为 。我们可以用一个带乘积列 的置换论证来强制它们确实是置换,规则如下:

即:只要不发生除以零的情况,对所有 我们有:

这是置换论证的一个版本,它允许 分别是 的置换,但并不指定具体的置换。 是两个独立的挑战(challenge),这样我们就可以把这两个置换论证合并为一个,而不必担心它们彼此干扰。

这些置换的目标,是允许证明者以特定方式排列 :

  1. 列中的所有单元格被排列成:取值相同的单元格在竖直方向上相邻。这可以通过某种排序算法完成,但真正重要的只是:取值相同的单元格在 列中位于连续的行上,且 的一个置换。
  2. 中一段相同取值的序列里,第一行就是 中具有相应取值的那一行。除此约束之外, 可以是 的任意置换。

现在,我们用如下规则来强制要么 ,要么 :

此外,我们用如下规则强制 :

(尽管 会"绕回",这里第一条规则中的 项在第 行也不起作用,因为有第二条规则。)

这些约束合在一起,有效地强制 (从而 )中的每个元素都至少等于 (从而 )中的一个元素。证明:对各行前缀做归纳。

零知识调整

为了让基于 PLONK 的证明系统实现零知识,我们需要把每一列的最后 行填充为随机值。这要求对查表论证作出调整,因为这些随机值并不满足上面描述的约束。

我们把可用行(usable row)的数量限制为 。我们添加两个选择子:

  • 在最后 行上置为 ,其余处为 ;
  • 仅在第 行置为 ,其余处为 (即它被置于可用行与盲化行之间的那一行)。

我们仅对可用行启用上面的约束:

在第 行启用的规则保持不变:

由于我们不能再依赖绕回(wraparound)来保证乘积 处重新变为 ,我们改为需要把 约束为 。然而,这里存在一个潜在困难:如果对某个 , 中有任何一个为零,那么可能无法满足置换论证。这种情况在 的选择上以可忽略的概率发生,但它既是实现完美零知识的障碍(因为对手可以排除掉会导致这种情况的见证值),也是实现完美完备性(perfect completeness)的障碍。

为了同时确保完美完备性和完美零知识,我们允许 取零或一:

现在,如果对某个 , 为零,我们就可以对 ,从而满足约束系统。

注意,挑战 是在对 (以及 )做出承诺之后才选定的,因此证明者无法强行制造出某个 为零的情况。由于这种情况以可忽略的概率发生,可靠性(soundness)不受影响。

开销

  • 有原始列 和固定列
  • 有一个置换乘积列
  • 有两个置换
  • 这些门的次数(degree)都很低。

推广

Halo 2 的查表论证实现以如下方式推广了上述技术:

  • 可以扩展为多列,并用一个随机挑战组合起来。 仍保持为单列。
    • 各列的承诺可以预先计算,然后在挑战已知后,利用 Pedersen 承诺的同态(homomorphic)性质廉价地组合起来。
    • 的各列可以用相对引用以任意多项式表达式给出。这些表达式会被代入乘积列约束中,但要受最大次数界(maximum degree bound)的限制。这有可能节省一个或多个建议列。
  • 然后,任意宽度关系的查表论证可以用子集论证来实现,即:为了在每一行约束 ,把 看作一个元组集合 (使用上一点的方法),并检查
    • 表示一个函数的情况下,这隐式地也检查了输入是否在定义域内。这通常正是我们想要的,且常常能省掉一次额外的范围检查(range check)。
  • 我们可以在同一个电路中支持多张表,办法是把它们合并成单张表,其中包含一个标签列(tag column)来标识原始表。
    • 如果前面提到的"查表选择子"得以实现,标签列可以与之合并。

这些推广与 Plookup 论文第 4、5 节中的推广类似。也就是说,与 Plookup 的差异在于子集论证。这一论证随后可以用于所有相同的用途;例如,Plookup 论文第 5 节中优化的范围检查技术也可以与这个子集论证一起使用。

译自 halo2 book:https://zcash.github.io/halo2/design/proving-system/permutation.html

置换论证(Permutation argument)

鉴于 halo2 电路中的门是"局部地"操作的(作用于当前行或定义好的相对行中的单元格),因此常常需要把某个任意单元格中的值复制到当前行中,以供门使用。这是通过等式约束(equality constraint)来完成的,该约束强制源单元格与目标单元格包含相同的值。

我们实现这些等式约束的方式,是构造一个表示这些约束的置换,然后在证明中使用置换论证来强制它们成立。

记号

置换(permutation)是一个集合到其自身的一一(one-to-one)且映满(onto)的映射。一个置换可以唯一地分解为若干循环(cycle)的复合(在循环的排序以及每个循环的旋转意义下)。

我们有时使用循环记号(cycle notation)来书写置换。设 表示一个循环,其中 映到 , 映到 , 映到 (对任意大小的循环有显然的推广)。把两个或多个循环并排书写表示对应置换的复合。例如, 表示这样的置换:把 映到 , 映到 , 映到 , 映到

构造置换

目标

我们想要构造一个置换,使得处于同一等式约束集合中的每一组变量构成一个循环。例如,假设我们有一个电路,定义了如下等式约束:

由此我们得到等式约束集合 。我们想要构造置换:

它定义了从 的映射。

算法

我们需要跟踪一组循环,这是一个不相交集合的集合(set of disjoint sets)。针对这一问题已有高效的数据结构;为简单起见,我们选择一个并非渐近最优、但易于实现的数据结构。

我们将当前状态表示为:

  • 一个数组 ,表示置换本身;
  • 一个辅助数组 ,跟踪每个循环的一个特定代表元素;
  • 另一个数组 ,跟踪每个循环的大小。

我们有这样一个不变式:对于给定循环 中的每个元素 , 都指向同一个元素 。这使我们能够通过检查 来快速判定给定的两个元素 是否在同一循环中。此外, 给出包含 的循环的大小。(这一点仅对 成立,而不对 成立。)

算法以恒等置换(identity permutation)的表示作为起点:对所有 ,我们设 ,,

要添加一个等式约束 :

  1. 检查 是否已经在同一循环中,即是否 。若是,则无需做任何事。
  2. 否则, 属于不同的循环。让 为较大的循环、 为较小的循环:当 时把二者交换。
  3. 沿着映射绕较小(右侧)循环走一圈,对每个元素
  4. 通过交换 ,把较小的循环拼接(splice)进较大的循环中。

例如,给定两个不相交的循环 :

A +---> B
^       +
|       |
+       v
D <---+ C       E +---> F
                ^       +
                |       |
                +       v
                H <---+ G

在添加约束 之后,上述算法产生如下循环:

A +---> B +-------------+
^                       |
|                       |
+                       v
D <---+ C <---+ E       F
                ^       +
                |       |
                +       v
                H <---+ G

错误的替代做法

如果我们不检查 是否已经在同一循环中,那么我们可能会撤销一个等式约束。例如,如果我们有如下约束:

而我们试图仅用上述算法的第 5 步来实现等式约束的添加,那么我们最终会构造出循环 ,而不是正确的

论证规约(Argument specification)

我们需要检查 列中单元格的一个置换,这些列以拉格朗日基表示为多项式

我们将给这 列中的每个单元格标注一个 中唯一的元素。

假设我们在这些标签上有一个置换, 其中各循环对应于等式约束集合。

如果我们考虑序对的集合 ,那么每个循环中的各个值彼此相等,当且仅当用 对每个序对中的标签进行置换后,得到的是同一个集合:

一个关于循环 (A B C D) 的示例。置换标签之前的集合是 {(A, 7), (B, 7), (C, 7), (D, 7)},置换之后是 {(D, 7), (A, 7), (B, 7), (C, 7)},二者相同。如果其中一个 7 被替换为 3,那么置换标签之后的集合就不再相同。

由于标签互不相同,集合相等等同于多重集合(multiset)相等,而后者可以用乘积论证(product argument)来检查。

为一个 次单位根(root of unity),设 为一个 次单位根,其中 , 为奇数且 我们将用 作为置换论证中第 列第 行单元格的标签。

我们用一个由 个多项式 构成的向量来表示 ,使得

注意,恒等置换可以用由 个多项式 构成的向量来表示,使得

我们将用一个挑战 把每个 序对压缩为 正如我们在查表中所用的乘积论证一样,我们还使用一个挑战 来随机化乘积的每一项。

现在,给定由 表示的置换,作用于由 表示的各列,我们想要确保:

这里 表示未置换的 序对,而 表示已置换的 序对。

满足 ,且对 :

那么,强制如下规则就足够了:

这里假设列数 使得上述第一条规则中的多项式能够落在 PLONK 配置的次数界之内。我们将在下文看到如何处理更多列数的情况。

用于得到恒等置换简洁表示的这一优化由 Vitalik Buterin 为 PLONK 提出,描述在 PLONK 论文第 8 节末尾。注意,只要启用等式约束的列数不超过 ,这些 就都是各不相同的二次非剩余(quadratic non-residue);对于 Halo 2 所用的曲线,这一点在实践中总是成立。

零知识调整

查表论证类似,我们需要对上述论证作出调整,以应对每一列最后 行被填充为随机值的情况。

我们把可用行的数量限制为 。我们添加两个选择子,其定义方式与查表论证中相同:

  • 在最后 行上置为 ,其余处为 ;
  • 仅在第 行置为 ,其余处为 (即它被置于可用行与盲化行之间的那一行)。

我们仅对可用行启用上面的乘积规则:

在第 行启用的规则保持不变:

由于我们不能再依赖绕回来保证每个乘积 处重新变为 ,我们改为需要约束 。这就引出了与查表论证中所描述的相同的问题。因此我们允许 取零或一:

这给出了完美完备性和零知识。

跨越大量列

halo2 的实现在实践中并不限制可启用等式约束的列数。因此,它必须解决一个问题:上述方法可能产生一个乘积规则,其中的多项式超出了 PLONK 配置的次数界。次数界可以提高,但如果没有其他规则需要更大的次数,这样做会效率低下。

取而代之,我们把乘积拆分到 个由 列组成的列集(column set)上,使用乘积列 ,并用另一条规则把乘积从一个列集的末端复制到下一个列集的开端。

也就是说,对 我们有:

为简单起见,这里的书写假设启用等式约束的列数是 的倍数;如果不是,那么最后一个列集的乘积将少于 项。

对于第一个列集我们有:

对于每个后续列集 ,我们用如下规则把 复制到下一个列集的起点 :

对于最后一个列集,我们允许 取零或一:

这与之前一样给出了完美完备性和零知识。

译自 halo2 book:https://zcash.github.io/halo2/design/proving-system/circuit-commitments.html

电路承诺(Circuit commitments)

对电路赋值做承诺

在证明创建之初,证明者持有一张单元格赋值表,它声称这些赋值满足约束系统。该表有 行,并被划分为建议(advice)列、实例(instance)列和固定(fixed)列。我们定义 为第 个固定列第 行的赋值。不失一般性,我们类似地定义 来表示建议列和实例列的赋值。

我们在这里把固定列单独区分出来,因为它们由验证者提供,而建议列和实例列由证明者提供。在实践中,对实例列和固定列的承诺由证明者和验证者双方各自计算,只有建议列的承诺被存入证明中。

为了对这些赋值做承诺,我们在一个大小为 的求值域(evaluation domain)上,为每一列构造次数为 的拉格朗日多项式(其中 是第 个本原单位根,primitive root of unity):

  • 插值得到
  • 插值得到

然后,我们对每一列的多项式创建一个盲化承诺(blinding commitment):

作为密钥生成(key generation)的一部分构造,使用值为 的盲化因子(blinding factor)。 由证明者构造并发送给验证者。

对查表置换做承诺

验证者首先采样 ,它用于保持查表内部各个列彼此独立。然后,证明者按如下方式对每个查表的置换做承诺:

  • 给定一个查表,其输入列多项式为 ,表列多项式为 ,证明者构造两个压缩多项式

  • 然后证明者按照查表论证的规则 做置换,得到

证明者为所有查表创建盲化承诺

并将它们发送给验证者。

在验证者收到 之后,它采样挑战 ,这两个挑战将用于置换论证以及下文中查表论证的剩余部分。(由于这些论证彼此独立,这些挑战可以复用。)

对等式约束置换做承诺

为启用了等式约束的列数。

为在不超过 PLONK 配置的最大约束次数的前提下,一个列集所能容纳的最大列数。

置换论证一节中定义的"可用"行数。

证明者构造一个长度为 的向量 ,使得对每个列集 和每一行 ,

然后,证明者从 开始计算 的连续乘积(running product),并按照置换论证一节所述,得到一个多项式向量 ,其中每个多项式的拉格朗日基表示对应于该连续乘积的一个大小为 的切片。

证明者对每个 多项式创建盲化承诺:

并将它们发送给验证者。

对查表置换乘积列做承诺

除了对各个置换后的查表做承诺之外,对于每个查表,证明者还需要对置换乘积列做承诺:

  • 证明者构造一个向量 :

  • 证明者构造一个多项式 ,其拉格朗日基表示对应于 的连续乘积,从 开始。

用于组合关于 的置换论证,同时保持它们彼此独立。这里重要的一点是:验证者在证明者已创建 (从而已对查表列中用到的所有单元格值、以及每个查表的 做出承诺)之后,才采样

与之前一样,证明者对每个 多项式创建盲化承诺:

并将它们发送给验证者。

译自 halo2 book:https://zcash.github.io/halo2/design/proving-system/vanishing.html

消失论证(Vanishing argument)

在对电路赋值完成承诺后,证明者(prover)现在需要证明各种电路关系都被满足:

  • 自定义门(custom gate),由多项式 表示。
  • 查找论证(lookup argument)的规则。
  • 等式约束置换(equality constraint permutation)的规则。

这些关系中的每一个都被表示为一个关于电路列的、度数为 (任意关系的最大度数)的多项式。鉴于每一列的赋值多项式度数为 ,这些关系多项式关于 的度数为

在我们的示例中,这些就是门多项式,度数为 :

如果一个关系的多项式等于零,则该关系被满足。证明这一点的一种方式是把每个关系多项式除以消失多项式(vanishing polynomial),它是在每个 处都有根的最低度数多项式。如果某个关系的多项式能被 整除,那么它在整个域(domain)上等于零(正如所期望的)。

这个简单的构造将需要对每个关系做一次多项式承诺(polynomial commitment)。我们改为同时对所有电路关系做承诺:验证者(verifier)采样 ,然后证明者构造商多项式(quotient polynomial)

其中分子是各电路关系的一个随机(证明者在验证者采样 之前已对单元格赋值做出承诺)线性组合。

  • 如果分子多项式(以形式不定元 表示)能被 完美整除,那么以高概率所有关系都被满足。
  • 反之,如果至少有一个关系不被满足,那么以高概率 将不等于分子在 处的求值。在这种情况下,分子多项式将不能被 完美整除。

做承诺

的度数为 (因为除数 的度数为 )。然而,我们在 Halo 2 中使用的多项式承诺方案只支持对度数为 的多项式做承诺(这是协议其余部分需要承诺的最大度数)。为了不增加多项式承诺方案的成本,证明者把 拆分成多个度数为 的片段

并对每个片段生成盲化承诺(blinding commitment)

对多项式求值

到此为止,我们已经对电路的所有性质做出了承诺。验证者现在想要查看证明者是否承诺了正确的 多项式。验证者采样 ,证明者生成各个多项式在 处的声称求值,涵盖电路中使用的所有相对偏移,以及

在我们的示例中,这将是:

  • ,
  • ,
  • , ...,

验证者检查这些求值是否满足 的形式:

现在确信这些求值整体上满足门约束后,验证者需要检查这些求值本身与原始的电路承诺以及 是一致的。为了高效地实现这一点,我们使用多点打开论证(multipoint opening argument)

译自 halo2 book:https://zcash.github.io/halo2/design/proving-system/multipoint-opening.html

多点打开论证(Multipoint opening argument)

考虑对多项式 的承诺 。假设 在点 处被查询,而 两个点处都被查询。(这里, 是我们用来构造这些多项式的乘法子群中的本原单位根(primitive root of unity)。)

为了打开(open)这些承诺,我们可以为每个查询的点(对应电路中使用的每个相对旋转)创建一个多项式 。但这在电路中并不高效;例如, 会出现在多个多项式中。

我们改为按照查询点的集合对承诺进行分组:

对于这些分组中的每一个,我们把它们合并成一个多项式集合,并为该集合创建单个 ,然后在每个旋转处打开它。

优化步骤

多点打开优化以下列内容作为输入:

  • 由验证者采样的随机 ,我们在该点对 求值。
  • 由证明者提供的、每个多项式在每个相关点处的求值:

这些是消失论证(vanishing argument)的输出。

多点打开优化按如下方式进行:

  1. 采样随机 ,以保持 线性无关。

  2. 根据多项式被查询时所在的点集,累加多项式及其对应的求值: q_polys: q_eval_sets:

            [
                [a(x) + x_1 b(x)],
                [
                    c(x) + x_1 d(x),
                    c(\omega x) + x_1 d(\omega x)
                ]
            ]
    

    注:q_eval_sets 是一个由求值集合构成的向量,其中外层向量对应点集(本例中为 ),内层向量对应每个集合中的点。

  3. q_eval_sets 中的每个值集合做插值(interpolate): r_polys:

  4. 构造 f_polys,用于检查 q_polys 的正确性: f_polys

    如果 ,那么 应当是一个多项式。 如果 , 那么 应当是一个多项式。

  5. 采样随机 以保持 f_polys 线性无关。

  6. 构造

  7. 采样随机 ,我们在该点对 求值:

  8. 采样随机 以保持 q_polys 线性无关。

  9. 构造 final_poly, 这就是我们在内积论证(inner product argument)中要承诺的多项式。

译自 halo2 book:https://zcash.github.io/halo2/design/proving-system/inner-product.html

内积论证(Inner product argument)

Halo 2 使用了一种多项式承诺方案(polynomial commitment scheme),我们可以基于内积论证(Inner Product Argument)为其创建多项式承诺打开证明(opening proof)。

TODO: 解释 Halo 2 的 IPA 变体。

它与 BCMS20 附录 A.2 中的 非常相似。 详见此对比

译自 halo2 book:https://zcash.github.io/halo2/design/proving-system/comparison.html

与其他工作的对比

BCMS20 附录 A.2

BCMS20 的附录 A.2 描述了一种多项式承诺方案(polynomial commitment scheme),它与 BGH19 中描述的方案相似(BCMS20 是原始 Halo 论文的推广)。Halo 2 建立在这两项工作之上,因此其自身使用的多项式承诺方案与 BCMS20 中的方案非常相似。

下表给出了 BCMS20 中的变量名与 Halo 2 中等价对象(其建立在 Halo 论文的命名法之上)之间的映射:

BCMS20Halo 2
msm
challenge_i
s_poly
s_poly_blind
s_poly_commitment
blind /

Halo 2 的多项式承诺方案与 BCMS20 附录 A.2 在两个方面有所不同:

  1. 算法的第 8 步在内积论证(inner product argument)之前计算一个"非隐藏(non-hiding)"承诺 ,它打开到与 相同的值,但它是对一个随机抽取的多项式所做的承诺。协议的其余部分不涉及任何盲化(blinding)。相比之下,在 Halo 2 中我们对我们做出的每一个承诺都进行盲化(即便是对实例(instance)和固定(fixed)多项式也是如此,不过对固定多项式使用盲化因子 1);这使得协议更易于推理分析。由此带来的结果是,验证者需要在协议末尾处理累积的盲化因子,因此无需在协议开始时推导出等价于 的对象。

    • 也是 的随机预言机(random oracle)的一个输入;在 Halo 2 中,我们使用的副本(transcript)在采样 之前已经对 的等价组成部分做出了承诺。
  2. 子例程(BCMS20 的图 2)通过加上 来计算初始群元素 ,这需要两次标量乘法。我们改为从原始承诺 中减去 ,从而实际上将多项式在该点打开到值零。计算 在递归(recursion)语境下更高效,因为 是一个固定基(fixed base)(所以我们可以使用查找表)。

译自 halo2 book:https://zcash.github.io/halo2/design/protocol.html

协议描述(Protocol Description)

预备知识

我们取 作为安全参数(security parameter),除非另有明确说明,所有算法和敌手(adversary)都是在该安全参数下以多项式时间运行的概率性(交互式)图灵机。我们用 表示一个在 上可忽略(negligible)的函数。

密码学群(Cryptographic Groups)

我们令 表示一个素数阶 的循环群。群的单位元写作 。我们把 中元素的标量(scalar)称为大小为 的标量域(scalar field) 中的元素。群元素用大写字母书写,标量用小写字母或希腊字母书写。标量向量或群元素向量用粗体书写,即 。群运算以加法形式书写,群元素 乘以标量 写作

我们经常用记号 来表示两个等长标量向量 的内积(inner product)。我们也用这个记号来表示群元素的线性组合,例如 ,其中 ,在实践中通过多标量乘法(multiscalar multiplication)来计算。

我们用 表示一个长度为 、仅包含 中零元素的向量。

离散对数关系问题(Discrete Log Relation Problem)。 优势度量(advantage metric) 是相对于以下博弈(game)定义的。

给定一个 长度的群元素向量 ,离散对数关系问题 要求找出 使得 ,我们称之为一个 非平凡的 离散对数关系。如 [JT20] 的引理 3 所示,该问题的困难性可由群中离散对数问题的困难性紧致地推出。形式上,我们使用上面定义的博弈 来刻画该问题。

交互式证明(Interactive Proofs)

交互式证明 是一个三元组算法 。算法 输出某些 公共参数(public parameters),通常用 来指代。证明者 和验证者 是交互式机器(可访问 ),我们用 表示一个在它们之间以输入 执行两方协议的算法。该协议的输出是它们交互的一个 副本(transcript),包含 之间发送的所有消息。在协议结束时,验证者输出一个决策比特。

零知识知识论证(Zero knowledge Arguments of Knowledge)

知识证明(proofs of knowledge)是这样的交互式证明:证明者旨在使验证者相信,对于某个陈述 和多项式时间可判定关系 ,他们知道一个证据(witness) 使得 。我们将处理 论证(arguments) 形式的知识证明,它假设证明者在计算上是有界的。

我们将通过四种安全概念来分析知识论证。

  • 完备性(Completeness): 如果证明者持有一个有效的证据,他们能 总是 说服验证者吗?理解这一性质很有用,因为它可能会对其他安全概念产生影响。
  • 可靠性(Soundness): 一个作弊的证明者能否虚假地使验证者相信一个实际上并不正确的陈述的正确性?我们把作弊证明者能虚假地说服验证者的概率称为 可靠性误差(soundness error)
  • 知识可靠性(Knowledge soundness): 当验证者被说服相信陈述是正确的时,证明者是否确实持有("知道")一个有效的证据?我们把作弊证明者虚假地使验证者相信这种知识的概率称为 知识误差(knowledge error)
  • 零知识(Zero knowledge): 验证者除了能从陈述的正确性以及证明者拥有有效证据这一事实推断出来的内容之外,是否还学到了别的东西?

首先,我们来看完备性的简单定义。

完美完备性(Perfect Completeness)。 一个交互式论证 具有 完美完备性,如果对于所有多项式时间可判定关系 以及所有非一致(non-uniform)多项式时间敌手

可靠性

使我们的分析复杂化的是,尽管我们的协议被描述为一个交互式论证,但它在实践中是通过使用 Fiat-Shamir 变换实现为一个 非交互式论证(non-interactive argument) 的。

公开掷币(Public coin)。 我们说一个交互式论证是 公开掷币(public coin) 的,当验证者发送的所有消息都各自以新鲜随机性采样时。

Fiat-Shamir 变换(Fiat-Shamir transformation)。 在这个变换中,一个交互式、公开掷币的论证可以在 随机预言机模型(random oracle model) 下被变为 非交互式,方法是用一个产生足够随机外观输出的密码学强哈希函数替换验证者算法。

这一变换意味着,在具体协议中,一个作弊的证明者可以通过分叉副本(forking the transcript)并向验证者发送新消息来轻易地"回退(rewind)"验证者。研究我们的构造在应用这一变换 之后 的具体安全性很重要。幸运的是,我们能够遵循 Ghoshal 和 Tessaro([GT20])的一套分析框架,它已被应用于与我们类似的构造。

我们将通过 状态恢复可靠性(state-restoration soundness) 这一概念来研究我们的协议。在这个模型中,允许(作弊的)证明者将验证者回退到它之前所处的任意状态。如果证明者能够产生一个被接受的副本,则他们获胜。

状态恢复可靠性(State-Restoration Soundness)。 是一个具有 个验证者挑战(challenge)的交互式论证,并设第 个挑战从 中采样。状态恢复证明者 的优势度量 是相对于以下博弈定义的。

如 [GT20](定理 1)所示,状态恢复可靠性与应用 Fiat-Shamir 变换之后的可靠性紧致相关。

知识可靠性

我们将证明,我们的协议满足一种被称为 证据扩展模拟(witness extended emulation) 的、被强化的知识可靠性概念。非形式地说,这一概念指出:对于任何成功的证明者算法,都存在一个高效的 模拟器(emulator),它能通过回退该算法并向其提供新鲜随机性来从中提取出一个证据。

然而,我们必须对证据扩展模拟的定义稍作调整,以考虑到我们的证明者是状态恢复证明者并且能够回退验证者这一事实。此外,为了避免在证据提取过程中需要回退状态恢复证明者,我们在代数群模型(algebraic group model)下研究我们的协议。

代数群模型(Algebraic Group Model,AGM)。 一个敌手 被称为 代数的(algebraic),如果每当它输出一个群元素 时,它也输出一个 表示(representation) 使得 ,其中 迄今为止所见到的群元素向量。 在记号上,我们写 来表示带有该表示增强的群元素 。我们也写 来标识 的表示中对应于 的分量。换言之,

代数群模型允许我们对某些协议执行所谓的"在线(online)"提取:提取器(extractor)可以从单个(被接受的)副本的表示本身中获得证据。

状态恢复证据扩展模拟(State Restoration Witness Extended Emulation) 是一个针对关系 、具有 个挑战的交互式论证。我们对于所有非一致的代数证明者 、提取器 ,以及计算上无界的区分器(distinguisher) 定义优势度量 是相对于以下博弈定义的。

零知识

我们说一个知识论证是 零知识(zero knowledge) 的,如果验证者除了从有效 存在这一事实所能学到的内容之外,也不从他们的交互中学到任何其他东西。更形式地说,

完美特殊诚实验证者零知识(Perfect Special Honest-Verifier Zero Knowledge)。 一个公开掷币的交互式论证 具有 完美特殊诚实验证者零知识(PSHVZK),如果对于所有多项式时间可判定关系 、所有 ,以及所有非一致多项式时间敌手 ,都存在一个概率多项式时间模拟器(simulator) 使得 其中 是验证者的内部随机性。

在这个(常见的)零知识定义中,验证者被期望"诚实地"行动,并发送仅与其内部随机性相对应的挑战;他们不能基于证明者的消息自适应地响应证明者。我们使用这一定义的一种强化形式,它强制模拟器输出一个带有验证者算法发送给证明者的相同(由敌手提供的)挑战的副本。

协议

是一个 次本原单位根(primitive root of unity),构成域 , 是该域上的消失多项式(vanishing polynomial)。设 为正整数,满足 。我们针对关系 给出一个交互式论证 ,其中 是关于 度数为 的(多元)多项式, 关于任意不定元 的度数至多为

返回

对于所有 :

  • 为所有整数 (模 )的穷举集合,使得 作为一项出现在 中。
  • 是一个由互异整数集合构成的列表,包含 以及集合
  • 时设

表示 的大小,并不失一般性地设 表示每个 的大小。

在以下协议中,我们将默认每个多项式 的定义方式使得:有 个盲化因子(blinding factor)由证明者新鲜采样,并各自作为 在域 上的一个求值出现。在以下所有内容中,验证者的挑战不能为零或 中的元素,此外对某些特定挑战还设置了额外的限制。

  1. 进行以下 轮交互,在第 轮(从 开始)中
  • 发送一个隐藏承诺(hiding commitment),其中 是一元多项式 的系数,而 是某个为简化叙述而省略的、随机且独立采样的盲化因子。(这种省略记号在本协议描述中通篇使用以简化叙述。)
  • 以挑战 作出响应。
  1. 发送一个承诺 ,其中 是一个随机采样的、度数为 的一元多项式 的系数。
  2. 计算一元多项式 ,其度数为
  3. 计算至多 度的多项式 使得
  4. 对所有 发送承诺 ,其中 表示 的系数向量。
  5. 以挑战 作出响应,并计算
  6. 发送 ,并对所有 发送 使得对所有
  7. 对所有 , 为最低度数的一元多项式,其定义满足对所有
  8. 以挑战 作出响应,并初始化
  • 开始到 ,
  • 最后设
  1. 初始化
  • 开始到 ,
  • 最后设
  1. 初始化
  • 开始到 ,
  • 最后 ,其中 使用 提供的值 计算为
  1. 发送 ,其中 定义了多项式 的系数。
  2. 以挑战 作出响应。
  3. 发送 使得对所有
  4. 以挑战 作出响应。
  5. 以及
  6. 采样一个度数为 且在 处有根的随机多项式 ,并发送承诺 ,其中 定义了 的系数。
  7. 以挑战 作出响应。
  8. (其中 应与验证者计算出的值 相对应)。
  9. 初始化为 的系数,并设 以及 将进行以下 轮交互,在第 轮(从第 轮开始到第 轮结束)中:
  • 发送
  • 以挑战 作出响应,该挑战的选取使得 非零。
  • 以及
  1. 发送 以及由被省略的盲化因子计算出的合成盲化因子(synthetic blinding factor)
  2. 仅当 时接受。

零知识与完备性

我们声称该协议是 完美完备的(perfectly complete)。这可以通过对协议的检视来验证;给定一个有效的证据 ,证明者以概率 成功说服验证者。

我们声称该协议是 完美特殊诚实验证者零知识(perfect special honest-verifier zero knowledge) 的。我们通过证明存在一个模拟器 来做到这一点,该模拟器能够产生一个被接受的副本,其分布与一个有效证明者在相同公开掷币下与验证者交互的分布相同。该模拟器将如同一个诚实的证明者那样行动,但有以下例外:

  1. 在协议的第 步中, 选择随机的度数为 的多项式(关于 )
  2. 在协议的第 步中, 选择随机的 度多项式
  3. 在协议的第 步中, 选择一个随机的 度多项式
  4. 在协议的第 步中, 利用其对验证者所选 的预知,生成一个度数为 的多项式 ,其唯一约束条件是 处有根。

首先,让我们考虑为什么这个模拟器总能成功产生一个 被接受的 副本。 缺乏一个有效的证据,并且每当诚实证明者需要有效证据的知识时,它都只是简单地对随机多项式做承诺。验证者对副本中的标量值不设任何条件。 只须保证协议第 步中的检查成功。它通过利用其对挑战 的知识,生成一个与 相互作用的多项式,以确保后者在 处有根来做到这一点。因此,由于完美完备性,该副本将总是被接受。

为了理解为什么 产生的副本与诚实证明者的副本分布完全相同,我们将逐一考察副本的每个部分并比较其分布。首先,注意 (正如诚实证明者那样)对副本中的每个群元素都使用一个新鲜随机的盲化因子,因此我们只需考虑副本中的 标量(scalars) 的行为与证明者完全相同,除了上述提到的几种情况,因此我们将逐一分析每种情况:

  1. 与诚实证明者都揭示每个多项式 个打开值,并在第 步中至多揭示每个 的一个额外打开值。然而,诚实证明者用 个在域 上的随机求值来盲化他们的多项式 (关于 )。因此, 在挑战 (协议禁止其为 或位于域 中)处的打开值,在 与诚实证明者之间的分布完全相同。
  2. 和诚实证明者都不揭示 ,因为它由验证者计算。然而,诚实证明者可能会揭示 ——它与 有一种非平凡的关系——若不是因为诚实证明者还在第 步中对一个随机的度数为 的多项式 做了承诺,产生一个承诺 ,从而确保在第 步当证明者设 的分布是均匀随机的。因此, 永远不会被诚实证明者也不会被 揭示。
  3. 的期望值由验证者计算(在第 步中),因此模拟器对 的实际选择是无关紧要的。
  4. 被约束为在 处有根,但除此之外对 不设其他条件,因此无论 是否在 处有根,度数为 的多项式 的分布都是均匀随机的。因此,第 步中产生的 的分布在 与诚实证明者之间完全相同。第 步中也揭示的合成盲化因子 是证明者其他盲化因子的一个平凡函数,因此在 与诚实证明者之间分布完全相同。

注:

  1. 在我们协议的较早版本中,作为多点打开论证(multipoint opening argument)的一部分,证明者会在 处打开每个单独的承诺 ,而验证者会确认这些打开值的一个线性组合(以 的幂为系数)与 的期望值相符。这样做是因为它在递归证明中更高效。然而,我们当时不清楚这些承诺 的打开值的期望分布是什么,因此证明该论证是零知识的很困难。于是我们改变了论证,使得 验证者 计算这些承诺的一个线性组合,而这个线性组合在 处被打开。这避免了泄露
  2. 如前所述,在第 步中证明者对一个随机多项式做承诺,作为一种确保 不会在多重打开(multiopen)论证中被揭示的方法。这样做是因为不清楚 的分布会是什么。
  3. 从技术上讲,我们也可以用一个模拟器来证明零知识,该模拟器利用其对挑战 的预知来承诺一个 ,使其在 处与它将被期望的值相符。这将省去协议中对随机多项式 的需求。不过这可能会使协议其余部分的零知识分析变得有些棘手,所以我们没有走这条路。
  4. 群元素盲化因子在第 步(此处多项式被完全随机化)之后 在技术上 不再必要。然而,在实践中,确保协议中的每个群元素都被随机盲化对我们来说更简单,这使得涉及无穷远点(point at infinity)的边界情况更难出现。
  5. 至关重要的是,验证者不能挑战证明者在 中的点处打开多项式,否则诚实证明者的副本将被迫包含可能是证明者证据一部分的内容。因此我们将挑战空间限制为包含域中除 之外的所有元素,并且为简单起见,我们还禁止挑战为

证据扩展模拟

为上述针对关系 、群 (其标量域为 )的交互式论证。我们总能构造一个提取器 ,使得对于任何对其预言机做至多 次查询的非一致代数证明者 ,都存在一个非一致敌手 ,具有这样的性质:对于任何计算上无界的区分器

其中

证明。 我们将通过援引 [GT20] 的定理 1 来证明这一点。首先,我们注意到所有轮次的挑战空间是相同的,即 。定理 1 要求我们定义:

  • 对所有部分副本 定义 使得
  • 一个提取器函数 ,它以一个被接受的扩展副本 作为输入,并返回一个有效证据或失败。
  • 一个返回概率的函数

我们说一个被接受的扩展副本 包含"坏挑战(bad challenges)",当且仅当存在一个部分扩展副本 、一个挑战 ,以及某个证明者消息和挑战序列 使得

定理 1 要求:当给定一个不包含"坏挑战"的被接受扩展副本 时, 返回该副本的一个有效证据,除非概率上界为 的情形发生。

我们的策略如下:我们将定义 ,相对于一个参与 博弈的敌手 建立 的一个上界,将这些代入定理 1,然后逐步走过协议以确定 大小的上界。敌手 按如下方式参与 博弈:给定输入 ,敌手 模拟博弈 ,使用来自 博弈的输入作为公共参数。如果 设法产生一个被接受的扩展副本 , 上调用一个函数 并返回其输出。我们将以这样的方式定义 :对于一个不包含"坏挑战"的被接受扩展副本 ,只要 返回一个非平凡的离散对数关系,总是 返回一个有效证据。这意味着概率 不大于 ,从而确立了我们的声称。

有用的替换

我们将执行一些替换以辅助叙述。首先,让我们定义多项式

这样我们可以写 的系数向量 定义为

其中 的第 个比特被置位时返回 ,否则返回 。我们也可以写

函数 的描述

回忆一个被接受的副本 满足

通过检视群元素关于 的表示(回忆 是代数的,因此 拥有它们),我们得到 个等式

以及这些等式

我们定义线性时间函数 ,它返回如下表达式的表示

它总是一个离散对数关系。如果上述任何等式不被满足,那么这个离散对数关系就是非平凡的。这就是 所调用的函数。

提取器函数

提取器函数 简单地从表示 中返回 ,。由于我们将对每一轮中坏挑战空间施加限制,只要敌手的函数 返回的离散对数关系是平凡的,我们就能保证得到这样的多项式: 上消失。这平凡地给出:提取器函数 成功的概率上界为所要求的

定义

回忆前面所述,以下 个等式成立:

以及等式

为方便起见,让我们引入以下记号

这样我们可以把上面的式子(在对 展开后)重写为

我们可以通过将第一个等式的每个实例的两边都乘以 (因为 永不为零)并代入第二个等式中的 ,来合并这些方程,得到以下 个等式:

引理 1。 如果 ,那么对于所有不包含坏挑战的副本,可推出

证明。 引入又一个抽象会很有用,它首先定义为 然后对所有满足 的整数 递归定义 这允许我们把上面的等式重写为

现在我们将证明:对于所有满足 的整数 ,每当以下式子对 成立时 同样的式子 对以下成立

对于所有满足 的整数 ,根据 的定义我们有 。由于 中没有任何值、也没有任何挑战 为零,这给我们 。我们可以用这一点把一半的等式与另一半关联起来,如下:

注意 可以对所有 重写为 。因此我们可以把上式重写为

现在让我们把这些等式中的 替换为形式不定元 来重写它们。

现在让我们把所有式子按 重新缩放以消除负指数。

这给了我们 个三元组,每组是关于 的最高度数为 的多项式,尽管它们的系数在 选定之前就已确定,但它们在 处取值相等。根据 Schwartz-Zippel 引理,这其中两个多项式会在 处取值相等却又互异的概率为 ,因此由联合界(union bound),这三个多项式取值相等却又其中任意一个与另一个不同的概率为 。再次由联合界, 个三元组中任意一个含有多个互异多项式的概率为 。通过相应地限制 的挑战空间,我们对整数 得到 ,从而

现在我们可以得出多项式相等的结论,从而得出系数相等。首先考虑常数项的系数,这给我们 个等式

中没有任何值为零, 永远不会被选为 ,且每个 的选取使得 非零,因此我们可以得出

对于这些等式中 项的系数,可以遵循一个相同的过程来确立 ,这取决于 非零(它总是非零)。把这些代入我们的等式得到更简单的式子

现在我们将考虑 中的系数,它们给出等式

由与之前类似的推理,它给出等式

最后我们将考虑 中的系数,它们给出等式

它通过代入给我们

注意到根据 的定义,我们可以把它重写为

这正是我们着手要证明的形式。

我们现在从已知成立的 情形出发通过归纳法进行,直到达到 ,这给我们

而因为 ,我们得到 ,这就完成了证明。

在确立了 之后,并鉴于 选定之前就已固定,我们有:至多存在一个 的值(非零)使得 却又 。通过相应地限制 ,我们得到 ,从而由 定义的多项式在 处有根。

根据构造 ,这给我们由 定义的多项式在点 处求值为 。我们有 选定之前就已固定,因此要么由 定义的多项式在 处有根(这蕴含由 定义的多项式在点 处求值为 ),要么 中使得 在点 处求值为 、而 本身却并非如此的唯一解。我们通过相应地限制 来避免后一种情况,从而可以得出由 定义的多项式在 处求值为

剩余的工作严格地处理证明者先前发送的群元素的表示及其与 的关系,以及协议每一轮中所选的挑战。我们首先通过用 表示由 定义的多项式来简化处理,因为这个 恰好对应于协议本身中同名的多项式。我们将对其他群元素(及其对应的多项式)做类似的替换以辅助叙述,因为这个证明的剩余部分主要是繁琐地应用 Schwartz-Zippel 引理来对协议中剩余每个挑战的坏挑战空间大小取上界。

回忆 ,因此通过代入我们有 。还回忆

我们已经确立了 。注意上述关于 的表达式中的系数在 选定之前就已固定。由 Schwartz-Zippel 引理我们有:至多存在 个可能的 选择,使得这些表达式被满足却又对某个 ,或

通过限制 ,我们可以得出上述所有不等式都不成立。现在我们可以对所有 替换 以得到

假设 (它是由 定义的多项式,度数至多为 ) 具有如下形式

然而正如我们上面所确立的, 处与此表达式取值相符。由 Schwartz-Zippel 引理,这至多只能对 的选择发生,因此通过限制 ,我们得到

接下来我们将通过再次相对于 应用 Schwartz-Zippel 引理,来提取这个关于 的多项式的系数(它们本身是关于形式不定元 的多项式);同样,这导致限制 ,并且我们对所有 得到以下度数至多为 的多项式

在确立了这些各自是度数至多为 的非有理多项式之后,我们便可以说(由因式定理,factor theorem):对于每个 , 处有根。注意,我们可以把每个 解释为一个 二元 多项式在点 处的限制,该二元多项式关于 的度数至多为 ,其系数由各个多项式 (来自表示 )以及 (来自表示 )和 (来自表示 )构成。通过类似地应用 Schwartz-Zippel 引理并以 限制挑战空间,我们(由协议第 12、13 步中每个 的构造)得到:证明者在第 9 步声称的 值等于 ;验证者在第 13 步计算的值 等于 ;并且对所有 ,证明者声称的值 对所有 成立。

由第 7 步中 (来自表示 )的构造,我们知道 ,其中我们用 指代度数至多为 的多项式,其系数对应于每个 拼接后的表示。如前所述,假设 具有形式 。那么因为 选定之前就已确定,由 Schwartz-Zippel 引理我们知道:如果这两个多项式不相等,它将至多在 个点处与 相符。通过再次限制 ,我们得到 ,并且因为 是一个非有理多项式,由因式定理我们得到 在域 上消失。

我们现在有 上消失,但希望证明 上的所有点处都消失,以完成证明。这只涉及对每个挑战重复应用同一技术;由于多项式 按定义关于任意不定元的度数至多为 ,并且因为每个多项式 在具体挑战 选定之前就已确定,通过类似地限制 ,我们确保 上消失,从而完成证明。

译自 halo2 book:https://zcash.github.io/halo2/design/implementation.html

实现(Implementation)

译自 halo2 book:https://zcash.github.io/halo2/design/implementation/proofs.html

Halo 2 证明(proofs)

证明作为不透明的字节流

在诸如 bellman 这样的证明系统实现中,存在一个具体的 Proof 结构体,它封装了证明数据,由证明者(prover)返回,并可被传递给验证者(verifier)。

halo2 出于以下几个原因,并不包含任何类似证明的结构:

  • 证明结构会包含(若干)曲线点(curve point)和标量(scalar)的向量。这会使证明的序列化/反序列化变得复杂,因为这些向量的长度取决于电路(circuit)的配置。然而,我们不希望把向量的长度编码进证明里,因为在运行时电路是固定的,因此证明大小也是固定的。
  • 很容易意外地把某些没有同时放入文字记录(transcript)的东西放进证明结构里,这在开发与实现证明系统时是一种隐患。
  • 我们需要能够同时创建多个 PLONK 证明;当这些证明针对同一电路时,它们会共享许多不同的子结构。

因此,halo2 把证明对象当作不透明的字节流来处理。这些字节流的创建与消费都通过文字记录(transcript)进行:

  • TranscriptWrite trait 表示我们可以(在证明时)向其写入证明组件的某个对象。
  • TranscriptRead trait 表示我们可以(在验证时)从其读取证明组件的某个对象。

至关重要的一点是,TranscriptWrite 的实现负责在把内容哈希进文字记录的同时,也向某个 std::io::Write 缓冲区写入;TranscriptRead/std::io::Read 同理。

作为额外的好处,把证明当作不透明的字节流可确保验证过程把反序列化的开销也计算在内——由于点压缩(point compression),这个开销并不可忽略。

证明编码

一个在曲线 上构造的 Halo 2 证明,被编码为如下内容组成的流:

  • (用于对多项式的承诺,commitment),以及
  • 标量 (用于多项式的求值,以及盲化值,blinding values)。

对于 Pallas 和 Vesta 曲线,点和标量都采用 32 字节编码,这意味着证明的大小总是 32 字节的倍数。

halo2 crate 支持同时证明一个电路的多个实例,以便共享公共的证明组件和协议逻辑。

在下面的编码描述中,我们将使用以下与电路相关的常量:

  • —— 电路的大小参数(电路有 行)。
  • —— 建议列(advice column)的数量。
  • —— 固定列(fixed column)的数量。
  • —— 实例列(instance column)的数量。
  • —— 查找论证(lookup argument)的数量。
  • —— 置换论证(permutation argument)的数量。
  • —— 参与置换论证 的列的数量。
  • —— 商多项式(quotient polynomial)的最大次数。
  • —— 建议列查询(query)的数量。
  • —— 固定列查询的数量。
  • —— 实例列查询的数量。
  • —— 同时被证明的电路实例的数量。

由于证明编码直接遵循文字记录,我们可以把编码拆分成与 Halo 2 协议相对应的若干部分:

  • PLONK 承诺:

    • 个点(重复 次)。
    • 个点(重复 次)。
    • 个点(重复 次)。
    • 个点(重复 次)。
  • 消去论证(vanishing argument):

    • 个点。
    • 个标量(重复 次)。
    • 个标量(重复 次)。
    • 个标量。
    • 个标量。
  • PLONK 求值:

    • 个标量(重复 次)。
    • 个标量(重复 次)。
  • 多点打开论证(multiopening argument):

    • 1 个点。
    • 多点打开论证中每组点对应 1 个标量。
  • 多项式承诺方案(polynomial commitment scheme):

    • 个点。
    • 个标量。

译自 halo2 book:https://zcash.github.io/halo2/design/implementation/fields.html

域(Fields)

我们在 halo2 中使用的 Pasta 曲线被设计为高度 2-adic 的,这意味着每个域(field)中都存在一个较大的 乘法子群(multiplicative subgroup)。也就是说,我们可以写成 ,其中 为奇数。对于 Pallas 和 Vesta,;这有助于简化域的实现。

Sarkar 平方根算法(基于表的变体)

我们使用来自 Sarkar2020 的一种技术来在 halo2 中计算平方根(square roots)。该算法背后的直觉是:我们可以把任务拆分为在每个乘法子群中分别计算平方根。

假设我们想求 模某个 Pasta 素数 的平方根,其中 中的一个非零平方数(square)。我们定义一个 单位根(root of unity) ,其中 中的一个非平方数(non-square),并预计算以下各表:

。我们便可定义 ,它是 乘法子群中的一个元素。

i = 0, 1

使用 ,我们查找 使得

定义

i = 2

查找 使得

定义

i = 3

查找 使得

定义

最终结果

查找 使得

现在我们可以写出

对右端(RHS)取平方,我们观察到 因此, 的平方根就是 ;其中第一部分我们在前面已经计算过,第二部分可以利用 中的查找,通过三次乘法计算得到。

译自 halo2 book:https://zcash.github.io/halo2/design/implementation/selector-combining.html

选择子合并(Selector combining)

大量使用自定义门(custom gate)会导致一个电路定义出许多二元选择子(binary selector),这会增加证明大小和验证时间。

本节描述一种由 halo2 自动应用的优化,它把二元选择子列合并为更少的固定列(fixed column)。

其基本思想是:如果我们有 个标记为 的二元选择子,且它们在互不相交的行集合上被启用(enabled),那么在某些附加条件下,我们可以把它们合并为单个固定列,记作 ,使得:

然而,魔鬼藏在细节之中。

halo2 的 API 允许把某些选择子定义为「简单选择子(simple selectors)」,但须满足以下条件:

每一个涉及简单选择子 的多项式约束(polynomial constraint)都必须具有 的形式,其中 是一个涉及任何简单选择子的多项式。

假设 在某组 个简单选择子中标记为 ,这组选择子如上所述被合并为 。那么上述条件可确保,把 替换为 不会改变任何约束的含义。

也可以通过确保对二元选择子的每一次使用都被替换为它的值由相应合并选择子精确插值(interpolation)得到的表达式,来放宽这个条件。然而,

  • 这条限制简化了实现、开发者工具链,以及人类对所得约束系统的理解与调试;
  • 对于典型电路而言,这条限制并未很大程度上妨碍该优化的适用范围。

注意,把 替换为 会使由 选中的约束的次数(degree)增加 ,因此我们必须以不超过最大次数界(maximum degree bound)的方式来选取要合并的选择子。

识别可被合并的选择子

我们需要对整个选择子集合 进行一个划分,分成若干子集(称为「组合,combinations」),使得同一组合中没有两个选择子在同一行上被启用。

标记(label)在一个组合内必须唯一,但跨组合并不唯一。不要把选择子的索引(index)与它的标记混淆。

假设我们已给定 ,即电路的次数界。

我们使用以下算法:

  1. 不优化非简单选择子,即把它们每一个都映射到一个单独的固定列。
  2. 检查(或通过构造确保)涉及每个简单选择子 的所有多项式约束都具有 的形式,其中 不涉及任何简单选择子。对每个 ,把任意 的最大次数记为
  3. 计算一个二元的「排斥矩阵(exclusion matrix)」,使得当 在同一行上被启用时 ;否则为

    由于 是对称的且对角线为零,我们可以只用它的上三角或下三角项来表示它。算法的其余部分保证只访问 的那些项

  4. 把一个布尔数组 初始化为全

    将记录 是否已被纳入任何组合。

  5. 遍历那些尚未被加入任何组合的 :
    • a. 把 加入一个全新的组合 ,并令
    • b. 令 mut

      用于跟踪到目前为止组合 中所涉及的任何门的最大次数,不包括选择子表达式。

    • c. 遍历所有 且有可能加入 的选择子 ,即 为 false 的那些:
      • i.(优化)如果 ,跳出到外层循环,因为不能再向 添加更多选择子。
      • ii. 令
      • iii. 如果对 中任意 ,或者 ,跳出到外层循环。

        是把 加入组合 后所产生的任何约束的最大次数,包括选择子表达式。

      • iv. 令
      • v. 把 加入 并令
    • d. 分配一个固定列 ,初始化为全零。
    • e. 对每个选择子 :
      • i. 用一个互不相同的索引 标记 ,其中
      • ii. 记录:在所有门约束中, 应被替换为
      • iii. 对每一个使 在该行被启用的行 ,把值 赋给该行 上的

上述算法实现于 halo2_proofs/src/plonk/circuit/compress_selectors.rs。 它由 halo2_proofs/src/plonk/circuit.rscompress_selectors 函数使用,后者执行实际的替换。

编写电路以最大程度利用选择子合并

对于这项优化而言,一个电路尽可能使用简单选择子而非固定列是有益的。手动合并选择子通常没有好处,因为所得到的固定列无法参与自动合并。这意味着,要得到可比的结果,你将需要手动进行全局优化,而这会干扰可组合小工具(composable gadgets)的编写。

两个选择子是否在同一行上被启用(从而被阻止合并),取决于布局规划器(floor planner)如何排布各个区域(region)。当前已实现的布局规划器并不尝试把这一点纳入考虑。我们建议不要在此过分纠结——通过哄劝布局规划器来回挪动区域以改善合并所能获得的收益,很可能相对较小。

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets.html

小工具(Gadgets)

在本节中,我们记录 halo2_gadgets crate 所提供的小工具(gadget)与芯片(chip)设计。

这些小工具及其实现都尚未经过审查,不应在生产环境中使用。

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/ecc.html

椭圆曲线(Elliptic Curves)

EccChip

halo2_gadgets 提供了一个芯片(chip),它使用 10 个 advice 列实现了 EccInstructions。 该芯片目前仅限于 Pallas 曲线,但在不久的将来会扩展以支持 Vesta 曲线

芯片假设(Chip assumptions)

EccChip 所做假设的一个非穷尽列表:

  • 不是曲线上任何有效点(point)的 坐标。
    • 对 Pallas 成立,因为 中不是平方数。
  • 不是曲线上任何有效点的 坐标。
    • 对 Pallas 成立,因为 中不是立方数。

布局(Layout)

下表展示了各个芯片子区域的门(gate)如何使用各列:

  • - 见证点(witnessing points)。
  • - 不完全点加法(incomplete point addition)。
  • - 完全点加法(complete point addition)。
  • - 定基标量乘(Fixed-base scalar multiplication)。
  • - 变基标量乘(variable-base scalar multiplication),不完全轮(incomplete rounds)。
  • - 变基标量乘,完全轮(complete rounds)。
  • - 变基标量乘,溢出检查(overflow check)。

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/ecc/witnessing-points.html

见证点(Witnessing points)

我们在电路中用仿射表示(affine representation) 来表示椭圆曲线点(point)。 单位元(identity)用伪坐标(pseudo-coordinate) 表示,我们 假设它不是曲线上的有效点。

非单位元点(Non-identity points)

为了约束坐标对 表示曲线上的一个有效点,我们直接检查曲线方程。 对于 Pallas 和 Vesta,曲线方程为:

包含单位元的点(Points including the identity)

为了允许 表示曲线上的一个有效点,或者表示伪坐标 , 我们定义一个单独的门(gate),除非 都为零,否则强制执行曲线方程检查。

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/ecc/addition.html

我们将使用短 Weierstrass 曲线上仿射坐标(affine coordinates)的曲线算术公式, 推导自 Hüseyin Hışıl 博士论文 的第 4.1 节。

不完全加法(Incomplete addition)

  • 输入:
  • 输出:

Hışıl 论文中的公式为:

重命名为 ,把 重命名为 ,把 重命名为 ,得到

这等价于

假设 ,我们有

于是我们得到约束:

    • 注意此约束对于 (当 时)是不可满足的, 因此不能用于任意输入。

约束(Constraints)

完全加法(Complete addition)

假设我们把 表示为 。( 不是有效点的 坐标,因为那将需要 ,而 中不是平方数。同样 也不是有效点的 坐标,因为 中不是立方数。)

对于倍点(doubling)情形,Hışıl 的论文告诉我们 必须改为计算为

定义

见证(Witness),其中:

约束(Constraints)

最大次数(Max degree):6

约束分析(Analysis of constraints)

命题(Propositions):

情形(Cases):

注意我们依赖于这样一个事实: 不是 Pallas 曲线上除 之外任何点的有效 坐标或 坐标。

    • 完备性(Completeness):

    • 可靠性(Soundness): 的唯一解。

  • ,其中

    • 完备性(Completeness):

    • 可靠性(Soundness): 的唯一解。

  • ,其中

    • 完备性(Completeness):

    • 可靠性(Soundness): 的唯一解。

  • ,其中

    • 完备性(Completeness):

    • 可靠性(Soundness): 被正确计算,且 是唯一解。

  • ,其中

    • 完备性(Completeness):

    • 可靠性(Soundness): 的唯一解。

  • ,其中

    • 完备性(Completeness):

    • 可靠性(Soundness): 被正确计算,且 是唯一解。

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/ecc/fixed-base-scalar-mul.html

定基标量乘(Fixed-base scalar multiplication)

Orchard 协议中有 个定基(fixed base):

  • ,用于推导 nullifier;
  • ,用于 spend authorization(花费授权);
  • 基,用于 ;
  • 基,用于 ;以及
  • 基,用于

分解标量(Decompose scalar)

我们支持三种类型标量(scalar)的定基标量乘:

全宽标量(Full-width scalar)

一个来自 位标量。我们把一个全宽标量 分解为 位窗口(window):

对于表示 范围内任意整数的 ,标量乘都将被正确计算——也就是说,允许标量是非规范(non-canonical)的。

我们使用一个多项式范围检查约束(polynomial range-check constraint)对标量分解的每个 位字(word)进行范围约束: 其中

基域元素(Base field element)

我们支持使用一个基域元素(base field element)作为定基乘法中的标量。例如,这出现在 Action 电路 nullifier 计算的标量乘中::这里,标量 是一次基域加法的结果。

把基域元素 分解为三位窗口,并对每个窗口进行范围约束,使用严格模式(strict mode)下的短范围分解(short range decomposition) gadget,其中

如果 是直接见证的,那么不会出现规范性(canonicity)问题。然而,因为这里标量是作为一个基域元素给出的,必须谨慎确保规范表示,因为 。也就是说,我们必须检查 其中 是 Pallas 基域模数 注意

为此,我们把 分解为三段:

我们通过以下方式检查此分解的正确性: 如果最高有效位(MSB) 未被置位,那么 然而,在 的情况下,我们必须检查:

  • :
    • ,

为了检查 我们利用三位移动求和(running sum)分解:

  • 首先,我们通过强制 的高 位全为零,把 约束为一个 位值。我们可以从分解中得到 :
  • 然后,我们约束 的第 位为零;换句话说,我们约束三位字 我们利用移动求和分解来得到

定义 。为了检查 我们使用 13 个十位查找(lookups),在其中我们约束查找的 移动求和输出在 时为

短有符号标量(Short signed scalar)

一个短有符号标量被见证为一个幅值(magnitude) 和一个符号(sign),使得

这用于 。我们想要计算 ,其中

各自已经被约束到 位(由于它们被用作 的输入)。

把幅值 分解为三位窗口,并对每个窗口进行范围约束,使用严格模式下的短范围分解(short range decomposition) gadget,其中

我们有两个额外的约束: 其中

加载定基(Load fixed base)

然后,我们为每个窗口预计算定基 的若干倍数。这采取窗口表(window table)的形式:,使得:

  • 对于前 (W-1) 行 :
  • 在最后一行 :

额外的 项让我们在 的情况下避免加上无穷远点(point at infinity)。我们通过在最后一个窗口中减去这些累加项来抵消它们,即我们减去

注:虽然 的偏移量朴素地看似乎就足够了,但它会在 时引入一个边界情形(edge case)。 在这种情况下,窗口表项求值为同一个点:

在定基标量乘中,我们使用不完全加法(incomplete addition)对每个窗口(除最后一个之外)的 的倍数求和。 由于倍点(point doubling)情形不被不完全加法处理,我们通过使用 的偏移量来避免它。

对于每个定基倍数窗口 :

  • 定义一个 Lagrange 插值多项式(interpolation polynomial),它把 映射到倍数 坐标,即
  • 找到一个值 ,使得 在域中是一个平方数 ,但错误符号的 坐标 不产生平方数。

对全部 个窗口重复此操作,我们最终得到:

  • 一个 的表 ,为每个窗口存储 个对 坐标进行插值的系数。每个 坐标插值多项式将具有如下形式 其中 ,且 各次幂的系数;以及
  • 一个长度为 的数组 ,存储各个

每当我们在电路中进行定基标量乘时,就把这些预计算值加载到固定列(fixed columns)中。

定基标量乘(Fixed-base scalar multiplication)

给定一个分解后的标量 和一个定基 ,我们如下计算 :

  1. 对于标量分解中的每个 ,见证 坐标和 坐标
  2. 检查 在曲线上:
  3. 见证 ,使得
  4. 对于除最后一个之外的所有窗口,使用不完全加法(incomplete addition)对各个 求和,得到
  5. 对于最后一个窗口,使用完全加法(complete addition) 并返回最终结果。

注:最后一步需要完全加法,以便正确地把 映射到无穷远点的表示 ;并且也用于处理最后一步是倍点的一个极端情形(corner case)。

约束(Constraints):

其中 (来自 Pallas 曲线方程)。

有符号短指数(Signed short exponent)

回想一下,有符号短指数被见证为一个 位幅值 和一个符号 使用上述算法,我们计算 。然后,为了得到最终结果 我们使用 进行条件取负(conditionally negate)。

约束(Constraints):

布局(Layout)

注:这不包括使用完全加法(complete addition)的最后一行。在实现中这是在一个不同的 region 中分配的。

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/ecc/var-base-scalar-mul.html

变基标量乘(Variable-base scalar multiplication)

在 Orchard 电路中,我们需要检查 ,其中 ,且标量域(scalar field)为 ,

我们有 ,其中

见证标量(Witness scalar)

我们试图计算 ,其中 。设 。那么我们可以计算

前提是 ,即 ,这覆盖了我们需要的整个范围,因为事实上

因此,给定一个标量 ,我们见证 的布尔分解(boolean decomposition)。(为便于输入到变基标量乘算法中,我们使用大端(big-endian)位序。)

变基标量乘(Variable-base scalar multiplication)

我们使用一个优化过的倍加(double-and-add)算法,从 《Faster variable-base scalar multiplication in zk-SNARK circuits》 复制而来,并做了一些变量名修改:

Acc := [2] T
for i from n-1 down to 0 {
    P := k_{i+1} ? T : −T
    Acc := (Acc + P) + Acc
}
return (k_0 = 0) ? (Acc - T) : Acc

剩下要检查的是,每对要相加的点的 x 坐标都互不相同。

当在素数阶群(prime-order group)中相加点时,我们可以依赖 Halo 论文 附录 C 中的定理 3,它表明:如果我们有两个这样的点,它们相对于给定的奇素数阶基有非零索引(index),且这些索引取在 范围内时,忽略符号是互不相同的,那么它们就有不同的 x 坐标。这很有帮助,因为对标量乘算法中出现的点的索引进行推理,比直接对它们的 x 坐标进行推理要容易。

因此,所需的检查等价于说,上述算法的下列"带索引版本"永远不会触发 assert:

acc := 2
for i from n-1 down to 0 {
    p = k_{i+1} ? 1 : −1
    assert acc ≠ ± p
    assert (acc + p) ≠ acc    // X
    acc := (acc + p) + acc
    assert 0 < acc ≤ (q-1)/2
}
if k_0 = 0 {
    assert acc ≠ 1
    acc := acc - 1
}

acc 的最大值是:

    <--- n 1s --->
  1011111...111111
= 1100000...000000 - 1

=

标记为 X 的 assert 显然不可能失败,因为 。可以看出,除了最后那个条件分支外,acc 是单调递增的。它在 取最大值时达到其最大值,即

所以,为了完全避免例外情形(exceptional cases),我们将需要 。但如果最后 次迭代使用完全加法(complete addition),我们就可以把 增大

使用不完全加法(incomplete addition)时算法第一个失败的 将是 ,因为 。我们需要 才能使上述环绕(wraparound)技巧奏效。

sage: q = 0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001
sage: 2^253 + 2^252 - 1 < (q-1)//2
False
sage: 2^252 + 2^251 - 1 < (q-1)//2
True

所以循环的最后三次迭代()需要使用完全加法(complete addition),末尾的条件减法也是如此。用 ⸭ 表示不完全加法(如我们在规范中所做),把它写出来,我们有:

Acc := [2] T
for i from 253 down to 3 {
    P := k_{i+1} ? T : −T
    Acc := (Acc ⸭ P) ⸭ Acc
}
for i from 2 down to 0 {
    P := k_{i+1} ? T : −T
    Acc := (Acc + P) + Acc  // complete addition
}
return (k_0 = 0) ? (Acc + (-T)) : Acc  // complete addition

优化倍加的约束程序(Constraint program for optimized double-and-add)

定义一个移动求和(running sum),其中 且:

辅助函数 。 在代入 之后,这变为:

这里, 被分配到一个单元格(cell)中。这与之前的各个 不同,后者是从 隐式推导出来的,但从未真正被分配过。

被用于另外三个步骤中,使用完全加法(complete addition):

如果最低有效位(least significant bit) 我们置 否则我们置 。然后我们使用完全加法返回

输出

(注意 表示 。)

不完全加法(Incomplete addition)

我们需要六个 advice 列来见证 。然而,由于 是相同的,我们可以在一行中执行两次不完全加法,复用相同的 。我们把不完全加法中使用的标量位分成 两半并行处理。这意味着我们实际上有两个 for 循环:

  • 第一个,覆盖 半部分,,在 处有一个特殊情形;以及
  • 第二个,覆盖 半部分,即剩余的 ,在 处有一个特殊情形。

对于每个 半部分,我们有三组门(gate)。注意 是从 取值; 不是对行做索引。

这个门仅用于第一行(在 for 循环之前)。我们检查 被初始化为与初始 一致的值。 其中

这个门用于对应 for 循环的所有行,除最后一行外。

其中

这个门用于 for 循环的最后一次迭代,处理特殊情形,其中我们检查输出 被正确见证。 其中

完全加法(Complete addition)

我们复用完全加法(complete addition)约束来实现倍加的最后 轮。这需要每轮两行,因为每次完全加法都需要 9 个 advice 列。在第 10 个 advice 列中,我们暂存为正确实现倍加所需的其他单元格:

  • 基点的 坐标,这样我们可以对它进行条件取负,作为其中一次完全加法的输入。
  • 移动求和(running sum),我们在两行上而非顺序地对它进行约束。

布局(Layout)

约束(Constraints)

除了完全加法约束之外,我们定义以下门(gate):

其中

LSB(最低有效位)

布局(Layout)

约束(Constraints)

其中

溢出检查(Overflow check)

对于任何 , 都不会溢出,因为它是仅到 为止的若干位的加权求和,而 小于 (也小于 )。

然而, 可能会溢出

注:对于全宽标量乘,可能无法在基域中表示 (例如当基域是 Pasta 的 时)。 在这种情况下,我们需要对涉及 的那一行进行特殊处理,使其对于我们用于全宽标量的任何表示都是正确的。 我们对 的表示将是这个对:。 我们将用 代替 作为 ,并把 约束到 254 位,使它能放入一个 元素。 然后我们只需把下面的论证推广到适用于 (因为 的最大值是 )。

由于溢出只可能发生在约束 的最后一步,我们有 。那么只需再检查 (使得 )以及 即可。这些条件合在一起就意味着 作为整数成立,因此如所需有

注:位 并不表示一个对 取模归约后的值,而是表示未归约的 的一个表示。

的优化检查(Optimized check)

由于 (若 互换也成立),我们有

我们可以假设

(对于 Orchard 中变基标量乘的使用而言这是真的,在那里我们知道 。如果我们互换 使得 ,这也成立。 但对于一个全宽标量 时,它成立。)

因此,

给定 ,我们如下证明 的等价性:

  • 把该区间平移 ,得到 ;
  • 观察到 保证落在 内,因此对 取模时既不会溢出也不会下溢;
  • 利用 这一事实,观察到

(我们可以用另一种方式看出这是正确的:观察到它检查的是 ,所以上界如我们所预期地对齐。)

现在,我们可以从 继续优化:

约束为全 或非全 ,几乎可以"免费"实现,方法如下。

回想 ,所以我们有:

所以 全为 当且仅当

最后,我们可以通过检查 ,把 两种情形的 位分解合并起来。

溢出检查约束(Overflow check constraints)

。溢出检查的约束为:

定义

见证 ,并把 分解为

那么所需的门(gate)为:

其中 可以由另一个移动求和计算得到。注意 这个因子对约束没有影响,因为右边为零。

移动求和范围检查(Running sum range check)

我们在电路中利用一个 查找范围检查(lookup range check)来减去 的低 位。该范围检查减去 的前 位,并把结果右移,得到

溢出检查(通用版)(Overflow check (general))

回想我们定义了 ,其中

对于任何 , 都不会溢出,因为它是仅到并包括 为止的若干位的加权求和。当 时,此求和至多为 ,小于 (也小于 )。

然而,对于全宽标量乘,可能无法在基域中表示 (例如当基域是 Pasta 的 时)。在这种情况下, 可能会溢出

所以,我们需要对涉及 的那一行进行特殊处理,使其对于我们用于全宽标量的任何表示都是正确的。

我们对 的表示将是这个对:。我们将用 代替 作为 ,并把 约束到 254 位,使它能放入一个 元素。

然后我们只需把 Orchard 电路中变基标量乘所使用的溢出检查 推广到适用于 (因为 的最大值是 )。

注:位 并不表示一个对 取模归约后的值,而是表示未归约的 的一个表示。

溢出只可能发生在约束 的最后一步,并且只有在 设置了权重为 的那一位时(即 时)才会发生。如果我们改为设置 ,那么现在 就不会溢出,并且应当等于

那么只需再检查 作为整数成立(其中 )即可。

表示为 ,其中我们约束 ,并约束 为布尔值。为使这是一个规范表示,我们还需要

如果 :

  • 约束 。这不会溢出,因为在这种情况下 ,因此

如果 :

  • 我们应当在 时且仅在此时有 ,即把 见证为布尔值,然后
    • 如果 ,则约束
      • 这可以通过约束 二者之一来完成。( 不会溢出。)
    • 如果 ,则约束
      • 这可以通过约束 来完成。

溢出检查约束(通用版)(Overflow check constraints (general))

如上把 表示为

溢出检查的约束为:

注意标记为 的四个 130 位约束分成两对,出现在不相交(disjoint)的情形中。因此我们可以用一个新的见证变量 把它们合并为两个 130 位约束;另一个约束始终是对 的约束:

( 是无约束的,在 的情况下可以见证为 。)

成本(Cost)

  • 25 次 10 位范围检查和一次 3 位范围检查,用于把 约束到 253 位;
  • 25 次 10 位范围检查和一次 3 位范围检查,用于在 时把 约束到 253 位;
  • 两次 13 个 10 位范围检查。

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/sinsemilla.html

Sinsemilla

概述

Sinsemilla 是一种抗碰撞(collision-resistant)的哈希(hash)函数和承诺(commitment)方案,其设计目标是在支持查表(lookups)的代数电路模型(如 PLONK 或 Halo 2)中保持高效。

Sinsemilla 的安全性质与 Pedersen 哈希类似;它并非设计用于需要随机预言机(random oracle)、PRF 或抗原像(preimage-resistant)哈希的场合。该哈希函数唯一声称的安全性质是对定长输入的抗碰撞性。

在电路内部,Sinsemilla 的效率大约比代数哈希 Rescue 和 Poseidon 低 4 倍,但在电路外部却比 Rescue 高约 19 倍。与这两种哈希不同的是,Sinsemilla 的抗碰撞性质可以基于已确立至少 20 年的密码学假设来证明。Sinsemilla 还可用作一个计算上绑定(computationally binding)且完美隐藏(perfectly hiding)的承诺方案。

总体思路是把消息切分成 比特的小片,对每一片,从我们密码学群中的一张含 个基底(bases)的表里选出一个。我们用倍加(double-and-add)算法把所选的基底组合起来。最终其安全性可证明地等同于一个向量 Pedersen 哈希,并且充分利用了 Halo 2 所支持的查表功能。

描述

本节是 Sinsemilla 工作原理的概述:规范性的定义请参阅协议规范中的 §5.4.1.9 Sinsemilla 哈希函数。我们下文使用的不完全点加(incomplete point addition)运算符 ⸭ 也在那里定义。

是一个素数阶 的密码学群。我们用加法记号书写 ,其单位元为 ,并用 表示 作标量乘法。

是一个基于效率考量选取的整数(表的大小将为 )。设 是一个整数,对每个实例化是固定的,使得消息为 比特,其中 。必要时我们用零填充(zero-padding)补到下一个 比特的整数倍。

:选取 作为 个独立的、可验证随机(verifiably random)的生成元,方法是使用一个合适的"哈希进 "(hash into )函数,使得 都不等于

在 Orchard 中,我们将 定义为依赖于一个域分隔符(domain separator)。协议规范用 替代 ,用 替代

:

  • 切分成 组,每组 比特。把每组解释为一个 比特小端(little-endian)整数
  • :
  • 返回

坐标。(这里假定 是短 Weierstrass 形式(short Weierstrass form)的素数阶椭圆曲线,对 Pallas 和 Vesta 而言确实如此。)

把一个倍加 表示为 会稍微高效一些。我们也使用不完全加法:Sinsemilla 安全性论证中证明了,在 为素数阶短 Weierstrass 椭圆曲线的情形下,加法的一个例外情形(exceptional case)将会导致求出一个离散对数(discrete logarithm),而即便对于敌手构造的输入,这也可以假定以可忽略的概率发生。

用作承诺方案

独立于 另选一个生成元

承诺的随机量 上均匀选取。

。(这同样假定 是短 Weierstrass 形式的素数阶椭圆曲线。)

注意,与简单的 Pedersen 承诺不同,这个承诺方案()不是加法同态(additively homomorphic)的。

高效实现

设计的目标是,在给定表大小的前提下,优化算法每一步(每步需要在 中作一次倍乘和一次加法)所能处理的比特数。使用一张大小为 个群元素的单表,我们每次可以处理 比特。

不完全加法

在 Sinsemilla 的每一步中,我们要计算 。设 为中间结果,使得 。 回顾不完全加法公式:

。依次代入每个不完全加法的坐标并整理,我们得到

以及

约束程序

输入:。(这里消息字(message words)的下标从 1 开始,与协议规范一致,但我们让循环从 开始,使得 对应协议规范中的 。)

输出:

  • :

PLONK / Halo 2 约束

消息分解(decomposition)

我们有一条 比特的消息 。(注意消息字的下标从 1 开始,与协议规范一致。)

将累加和(running sum)初始化为 ,并定义 。最终我们将得到

整理后得到原消息每个字的表达式 ,我们可以在表中对它进行查表。我们把 放在同一列的相邻两行,这样就能沿整条消息顺序地施加该约束。

换言之,

对于此处所用的小端分解,累加和初始化为该标量并以 0 结束。而对于变基标量乘法(variable-base scalar multiplication)中所用的大端(big-endian)分解,累加和会从 0 开始,并以恢复出原标量结束。

高效打包(packing)

累加和只适用于单个域元素(field element)内部的消息字。这意味着,如果 ,我们就需要多个互不相交的累加和。更长的消息可以通过把消息字拆分到多个域元素中来构造,然后运行下面这些约束的多个实例。

上面 的表达式在 列中需要 行,这会在相邻列里留下一行空隙,使得 更难累加。为了支持把多个域元素串联起来而不留空隙,我们对 使用一个稍复杂、包含一个选择子(selector)的表达式:

这实际上把每个域元素最后一步的 强制为零,从而让本应作为 的那个单元(cell)可以被用来为下一个域元素重新初始化累加和。

这样安排好之后,不完全加法累加器几乎可以完全消去 ,办法是在当前行和下一行中用 的值进行代换。两个例外是在 Sinsemilla 的起点(那里我们需要约束累加器的初始值为 )和终点(那里我们需要见证(witness) 以供 Sinsemilla 之外使用)。

选择子

我们总共需要四个逻辑选择子,用于:

  • 控制 Sinsemilla 门(gate)和查表。
  • 区分一个累加和中的最后一个消息字与它之前的各个字。
  • 标记 Sinsemilla 的开始。
  • 标记 Sinsemilla 的结束。

我们对 Sinsemilla 门选择子 和 Sinsemilla 起始选择子 使用普通的选择子列。另外两个选择子从单一固定列 按如下方式合成(synthesized):

我们在大多数 Sinsemilla 行上把 设为 ,在每个域元素的最后一步设为 ,但最后一个域元素例外,那里设为 。然后我们可以用 在两种约束之间切换:约束相邻两行上经代换的 ,以及约束 Sinsemilla 末尾处所见证的 :

生成元查表表

Sinsemilla 电路使用了 个预计算的随机生成元。它们被装入一张查表表中:

布局(Layout)

等通过相等约束(equality constraints)拷入。

优化后的 Sinsemilla 门

Halo 2 电路 API 可以自动代换 ,所以我们无需手动去做。

  • :

注意,相对于前面给出的约束程序,最后一个约束的每一项都乘以了 。这是一个小的优化,可以避免除以

通过用 来门控(gating)查表表达式,我们避免了为了让查表论证(lookup argument)通过而用哑值(dummy values)去填充未用单元的需要。优化后的查表值(使用默认下标 )为:

这把查表论证的次数(degree)提高到了

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/sinsemilla/merkle-crh.html

MerkleCRH

消息分解(decomposition)

被用在 哈希函数中。 的输入是:

其中:

  • ,
  • ,
  • ,

其中 允许是 的非规范(non-canonical) 比特编码。

Sinsemilla 以 10 比特的整数倍进行操作,所以我们首先把消息分解(decomposing)成若干块(chunks):

然后我们把这些块重新组合(recompose)成若干 MessagePiece(消息片):

每个消息片都被 约束到其所声明的长度。此外, 作为域元素(field elements)被见证(witnessed),所以我们知道它们 是规范的(canonical)。然而,我们还需要额外的约束来确保这些块具有正确的比特长度(否则它们可能在分解中相互重叠,从而允许证明者(prover)见证出任意的 消息)。

其中一些约束可以用可复用的电路 gadget 来实现。我们定义一个由选择子(selector) 控制的自定义门(custom gate)来承载其余的约束。

比特长度约束

由 Sinsemilla 直接约束。我们用以下约束来约束其余的块:

,即 的下标为 1 的累加和(running sum)输出,被拷入门中。 已被 约束为 比特,并且恰好就是 。我们利用 来恢复块 :

,即 的下标为 1 的累加和输出,被拷入门中。 已被 约束为 比特。我们在该门之外见证子片(subpieces),并各自约束它们为 比特。在门内,我们检查 我们还利用 恢复子片 :

约束

其中 是一个 短查表范围检查(short lookup range check)

分解约束

至此我们已经导出或见证了每个子片,并对每个子片做了范围约束:

  • ( 比特),导出为 ;
  • ( 比特),等于 ;
  • ( 比特),导出为 ;
  • ( 比特)在门外被见证并约束;
  • ( 比特)在门外被见证并约束;
  • ( 比特)在门外被见证并约束。
  • 被约束为等于

现在我们可以用它们来重建原始的域元素输入:

区域布局(Region layout)

电路组件

Orchard 电路横跨 个建议列(advice columns),而 芯片(chip)只使用 个建议列。我们把路径哈希(path hashing)均匀地分摊到两个 芯片上,以便更好地利用可用的电路面积。由于上一层哈希的输出会被拷入下一层哈希,即使在从一个芯片转到另一个芯片时,我们也能保持连续性。

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/decomposition.html

分解(Decomposition)

给定一个域元素(field element),这些 gadget 把它分解(decompose)成 比特的窗口(windows) 其中每个 是一个 比特的值。

这是用一个累加和(running sum) 来完成的。我们将累加和初始化为 并计算后续各项 这给出:

严格模式(Strict mode)

严格模式把累加和输出 约束为零,从而把该域元素范围约束(range-constrain)在 比特之内。

在严格模式下,我们还能确信 给出了分解中的最后一个窗口。

查表分解(Lookup decomposition)

这个 gadget 利用一张 比特的查表表把域元素 分解成 比特的字(words)。每个 比特字 都通过在 比特表中查表而被范围约束。

查表分解的区域布局使用单个建议列(advice column),以及两个选择子(selectors)

短范围检查(Short range check)

使用两次 比特查表,我们可以把一个域元素 范围约束为 比特,其中 做法是:

  1. 用一次 比特查表把 约束在 比特之内。
  2. 用一次 比特查表把 约束在 比特之内。

查表分解的短变体(short variant)引入了一个 选择子。这里把同一个建议列 重命名为 以便表述清晰:

其中 注意 在密钥生成(keygen)时被赋到一个固定列里,并在证明(proving)时拷入。它被用在由 选择子启用的门中,以检查 是否被正确地移位(shifted):

合并的查表表达式

由于查表分解及其短变体都使用同一张查表表,我们把它们的查表输入表达式合并成一个:

其中 是同一个单元(cell)(此处为了用法上的清晰而加以区分)。

短范围分解(Short range decomposition)

对于一个短范围(例如 ,其中 ),我们可以用一个 次多项式约束来范围约束每个字,而不必用查表:

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/sha256.html

SHA-256

规范(Specification)

SHA-256 在 NIST FIPS PUB 180-4 中定义。

与该规范不同,我们用 表示模 加法,用 表示域加法(field addition)。 用于表示异或(XOR)。

Gadget 接口

SHA-256 在八个 32 比特变量中维护状态。它以 512 比特的块(blocks)为单位处理输入,但在内部把这些块切分成 32 比特的小块(chunks)。因此我们把 SHA-256 gadget 设计成以 32 比特小块为单位消费输入。

芯片指令(Chip instructions)

SHA-256 gadget 需要一个具备以下指令的芯片(chip):

#![allow(unused)]
fn main() {
extern crate halo2_proofs;
use halo2_proofs::plonk::Error;
use std::fmt;

trait Chip: Sized {}
trait Layouter<C: Chip> {}
const BLOCK_SIZE: usize = 16;
const DIGEST_SIZE: usize = 8;

pub trait Sha256Instructions: Chip {
    /// Variable representing the SHA-256 internal state.
    type State: Clone + fmt::Debug;
    /// Variable representing a 32-bit word of the input block to the SHA-256 compression
    /// function.
    type BlockWord: Copy + fmt::Debug;

    /// Places the SHA-256 IV in the circuit, returning the initial state variable.
    fn initialization_vector(layouter: &mut impl Layouter<Self>) -> Result<Self::State, Error>;

    /// Starting from the given initial state, processes a block of input and returns the
    /// final state.
    fn compress(
        layouter: &mut impl Layouter<Self>,
        initial_state: &Self::State,
        input: [Self::BlockWord; BLOCK_SIZE],
    ) -> Result<Self::State, Error>;

    /// Converts the given state into a message digest.
    fn digest(
        layouter: &mut impl Layouter<Self>,
        state: &Self::State,
    ) -> Result<[Self::BlockWord; DIGEST_SIZE], Error>;
}
}

TODO:添加用于计算填充(padding)的指令。

这套指令的选取是为了在指令的可复用性和芯片对其进行内部优化的空间之间取得平衡。具体来说,我们考虑过把压缩函数(compression function)拆分成其组成部分(Ch、Maj 等),并提供一个实现轮逻辑(round logic)的压缩函数 gadget。然而,这会阻止芯片在一轮压缩(round compression)的各个部分之间使用相对引用(relative references)。提供一条实现全部压缩轮的指令,也与 Intel SHA 扩展(extensions)类似——后者提供了一条执行多轮压缩的指令。

译自 halo2 book:https://zcash.github.io/halo2/design/gadgets/sha256/table16.html

用于 SHA-256 的 16 比特表芯片(table chip)

这个芯片(chip)实现围绕单张 16 比特查表表(lookup table)构建。它至少需要 个电路行(circuit rows),因此适合用在较大的电路中。

我们的目标是最大约束次数(constraint degree)为 。这将允许我们在一行内处理对进位(carries)以及"小片(small pieces)"约束到 范围内的约束。

轮压缩(Compression round)

共有 轮压缩(compression rounds)。每一轮以 32 比特值 作为输入,并执行以下运算:

其中 必须处理一个满足 的进位(carry)。

The SHA-256 compression function

定义为一张表,它把一个 比特的输入映射到一个用零比特交错(interleaved)的输出。我们不需要为范围检查(range checks)单独建表,因为可以使用

模加法(Modular addition)

为实现模 加法,我们注意到这等价于用域加法(field addition)把操作数相加,然后把结果除最低 32 比特之外的全部比特都掩掉(mask away)。例如,若我们有两个操作数 :

我们把每个操作数(连同结果)分解(decompose)成 16 比特的小块(chunks):

然后用域加法重新表述该约束:

更一般地,输出的任意比特分解(bit-decomposition)都可以使用,而不仅限于分解成 16 比特小块。注意这正确地处理了来自 的进位。

这个约束要求每个小块都被正确地范围检查(否则一次赋值可能溢出该域(overflow the field))。

  • 操作数小块和结果小块可以用 来约束,办法是在表的某个子集中,在"密集(dense)"列里对每个小块查表。这样我们还能免费得到输出的"展开(spread)"形式;特别地,对于右下角那个 的输出(它会成为 ),以及最左边那个 的输出(它会成为 ),都是如此。我们将在下文用这一点来优化

  • 必须被约束到与操作数个数相对应的、允许的进位值的精确范围内。我们用一个 小范围约束(small range constraint)来做这件事。

Maj 函数

可以用 次查表完成: 个小块

  • 如上所述,第一轮之后我们已经拥有展开形式的 ,即 。 类似地, 分别等于上一轮的 , 因此在稳态(steady state)下,我们已经拥有它们的展开形式 。事实上 我们也可以假定在第一轮就拥有它们的展开形式,要么来自固定的 IV,要么来自用 去归约(reduce)上一块前馈(feedforward)的输出。
  • 把这些展开形式在域中相加:;
    • 我们可以把它们当作 比特字相加,也可以分片相加;两者等价。
  • 见证(witness)压缩偶数比特 和压缩奇数比特 ,;
  • 约束 ,其中 函数的输出。

注:我们所说的"偶数(even)"比特指的是权重为 的偶数次幂的比特,即权重为 的比特。类似地,"奇数(odd)"比特指的是权重为 的奇数次幂的比特。

Ch 函数

TODO:借助一张额外的表,可能可以优化到 次查表。

可以用 次查表完成: 个小块

  • 如上所述,第一轮之后我们已经拥有展开形式的 ,即 。 类似地, 分别等于上一轮的 , 因此在稳态下,我们已经拥有它们的展开形式 。事实上 我们也可以假定在第一轮就拥有它们的展开形式,要么来自固定的 IV,要么来自用 去归约上一块前馈的输出。
  • 计算 ,其中
    • 我们可以把它们当作 比特字相加,也可以分片相加;两者等价。
    • 之所以能算出 的展开形式,尽管取反(negation)与 一般并不交换(commute)。它之所以有效,是因为 中的每个展开比特 都从 中减去,所以不会发生借位(borrows)。
  • 见证 ,使得 ,对 同理。
  • 就是 函数的输出。

Σ_0 函数

可以用 次查表完成。

为此,我们首先把 切分成片 ,长度分别为 比特,从低端(little end)开始计数。同时我们得到这些片的展开 形式。这一切都可以在两个 PLONK 行内完成,因为 比特和 比特的片可以用 查表来处理,而 比特的片可以 切分成 比特的子片(subpieces)。后者和剩下的 比特片可以 与这两次查表并行地用多项式约束(polynomial constraints)做范围检查,每行做两个小片 。这些小片的展开形式通过插值(interpolation)求得。

注意,把它切分成片这件事可以与 的归约(reduction)合并,即 后者不需要额外的查表。在最后一轮中,我们在加上前馈之后再归约 (需要处理一个至多为 的进位,这没问题)。

等价于 :

然后,再用 查表,我们把结果作为这些片的某个 线性组合(linear combination)的偶数比特而得到:

也就是说,我们见证压缩偶数比特 和压缩奇数比特 ,并约束 其中 函数的输出。

Σ_1 函数

可以用 次查表完成。

为此,我们首先把 切分成片 ,长度分别为 比特,从低端开始计数。同时我们得到这些片的展开 形式。这一切都可以在两个 PLONK 行内完成,因为 比特和 比特的片可以用 查表来处理, 比特的片可以 切分成 比特和 比特的子片,而 比特的片可以切分成 比特 的子片。这四个小片可以与这两次查表 并行地用多项式约束做范围检查,每行做两个小片。这些 小片的展开形式通过插值求得。

注意,把它切分成片这件事可以与 的归约合并,即 后者不需要额外的查表。在最后一轮中,我们在加上前馈 之后再归约 (需要处理一个至多为 的进位,这没问题)。

等价于

然后,再用 查表,我们以与 相同的方式,把结果作为这些片的某个线性组合的偶数比特而得到:

也就是说,我们见证压缩偶数比特 和压缩奇数比特 ,并约束 其中 函数的输出。

块分解(Block decomposition)

对于已填充消息(padded message)的每一个块 ,要构造 个各 比特的字 ,方法如下:

  • 个通过把 切分成 比特的块来得到
  • 其余 个字用以下公式构造:

注: 字的下标采用从 开始的编号。

注: 是右移位(shift),不是旋转(rotation)。

σ_0 函数

等价于

与上面类似,但片 的长度为 ,从低端 开始计数。把 切分成两个 比特子片。

σ_1 函数

等价于

TODO:这张图与右侧的表达式不符。这里只是为了与 其他图保持一致。

与上面类似,但片 的长度为 ,从低端 开始计数。把 切分成 比特子片。

消息调度(Message scheduling)

我们对 应用 ,对 应用 。为了避免 对 的冗余应用,在 的情形下,我们可以把 的切片合并。把片长度 合并后,得到长度为 的片。

如果我们能用 行完成这个合并的切分(而不是分别为 切分时总共需要的 行),我们就节省了 行。

这些甚至或许能用 行做到;不确定。 ——Daira

被用于计算后续的字时,我们可以把它们的模 归约 合并进它们的切分中,类似于我们对轮函数中的 所做的。

我们仍然需要归约 ,因为它们没有被切分。(从技术上讲我们可以 让它们保持未归约,因为稍后用它们来计算 时它们会被归约——但那会需要处理一个至多为 而不是 的进位,所以不值得为此增加复杂度。)

由此得到的消息调度代价为:

  • 行用于把 约束为 比特
    • 这从技术上讲是可选的,但为了健壮性我们还是这么做,因为输入的其余部分 是免费被约束的。
  • 行用于把 切分成 比特的片
  • 行用于把 切分成 比特的片(对 合并了一次归约)
  • 行用于把 切分成 比特的片(合并了一次 归约)
  • 行用于提取 结果
  • 行用于提取 结果
  • 行用于归约
  • 行。

总体代价(Overall cost)

对每一轮:

  • 行用于
  • 行用于
  • 行用于
  • 行用于
  • 始终是免费的
  • 每轮

这给整个"步骤 3"带来 行,在此之上我们还需加上:

  • 行用于消息调度
  • 行用于"步骤 4"中 次模 归约

总计 行。

表(Tables)

我们只需要一张表 ,它有 行和 列。我们需要一个 标签(tag)列,以便为 选取表的 比特子集。

spread

rowtagtable (16b)spread (32b)
0000000000000000000000000000000000000000000000000
0000000000000000100000000000000000000000000000001
0000000000000001000000000000000000000000000000100
0000000000000001100000000000000000000000000000101
...0......
0000000000111111100000000000000000001010101010101
1000000001000000000000000000000000100000000000000
...1......
1000000111111111100000000000001010101010101010101
...2......
2000001111111111100000000010101010101010101010101
...3......
3000111111111111100000001010101010101010101010101
...4......
4001111111111111100000101010101010101010101010101
...5......
5111111111111111101010101010101010101010101010101

例如,要做一次 比特的 查表,我们用多项式约束把标签 约束到 内。对于最常见的 比特查表的情形,我们不需要 约束标签。注意我们可以用一个重复条目(例如全零)来填充超出 的任何 未用行。

门(Gates)

选择门(Choice gate)

来自先前运算的输入:

  • 即 32 比特字 的 64 比特展开形式,假定已被先前运算所约束
    • 在实践中,把 分解成 16 比特子片之后,我们才会有它们的展开形式
  • 定义为

E ∧ F

s_ch
0{0,1,2,3,4,5}
1{0,1,2,3,4,5}
0{0,1,2,3,4,5}
0{0,1,2,3,4,5}

¬E ∧ G

s_ch_neg
0{0,1,2,3,4,5}
1{0,1,2,3,4,5}
0{0,1,2,3,4,5}
0{0,1,2,3,4,5}

约束:

  • s_ch(选择,choice):
  • s_ch_neg(取反,negation):带一个额外取反检查的 s_ch
  • 查表
  • 之间的置换(permutation)

输出:

多数门(Majority gate)

来自先前运算的输入:

  • 即 32 比特字 的 64 比特展开形式,假定已被先前运算所约束
    • 在实践中,把 分解成 比特子片之后,我们才会有它们的展开形式
s_maj
0{0,1,2,3,4,5}
1{0,1,2,3,4,5}
0{0,1,2,3,4,5}
0{0,1,2,3,4,5}

约束:

  • s_maj(多数,majority):
  • 查表
  • 之间的置换

输出:

Σ_0 门

是一个 32 比特字,被切分成 比特的小块,从低端开始。我们分别把这些小块称为 ,并进一步把 切分成三个 3 比特小块 。我们见证这些小块的展开版本。

s_upp_sigma_0
0{0,1,2,3,4,5}
1{0,1,2,3,4,5}
0{0,1,2,3,4,5}
0{0,1,2,3,4,5}

约束:

  • s_upp_sigma_0( 约束):

  • 查表
  • 做 2 比特范围检查和 2 比特展开检查
  • 做 3 比特范围检查和 3 比特展开检查

(参见辅助门(Helper gates)一节)

输出:

Σ_1 门

是一个 32 比特字,被切分成 比特的小块,从低端开始。我们分别把这些小块称为 ,并进一步把 切分成两个 3 比特小块 ,把 切分成 (2,3) 比特小块 。我们见证这些小块的展开版本。

s_upp_sigma_1
0{0,1,2,3,4,5}
1{0,1,2,3,4,5}
0{0,1,2,3,4,5}
0{0,1,2,3,4,5}

约束:

  • s_upp_sigma_1( 约束):

  • 查表
  • 做 2 比特范围检查和 2 比特展开检查
  • 做 3 比特范围检查和 3 比特展开检查

(参见辅助门(Helper gates)一节)

输出:

σ_0 门

v1

门的 v1 版本接收一个被切分成 比特小块的字(已由消息调度约束)。我们分别把这些小块称为 被进一步切分成两个 2 比特小块 我们见证这些小块的展开版本。我们已经从消息调度得到了

等价于

s_low_sigma_0
0{0,1,2,3,4,5}
1{0,1,2,3,4,5}
0{0,1,2,3,4,5}
0{0,1,2,3,4,5}

约束:

  • s_low_sigma_0( v1 约束):

  • 检查 b 是否被正确地切分成 4 比特片的各子段。
  • 做 2 比特范围检查和 2 比特展开检查
  • 做 3 比特范围检查和 3 比特展开检查

v2

门的 v2 版本接收一个被切分成 比特小块的字(已由消息调度约束)。我们分别把这些小块称为 我们已经从消息调度得到了 。1 比特的 经展开运算后保持不变,可以直接使用。我们进一步把 切分成两个 2 比特小块 我们见证这些小块的展开版本。

等价于

s_low_sigma_0_v2
0{0,1,2,3,4,5}
1{0,1,2,3,4,5}
0{0,1,2,3,4,5}
0{0,1,2,3,4,5}

约束:

  • s_low_sigma_0_v2( v2 约束):

  • 检查 b 是否被正确地切分成 4 比特片的各子段。
  • 做 2 比特范围检查和 2 比特展开检查
  • 做 3 比特范围检查和 3 比特展开检查

σ_1 门

v1

门的 v1 版本接收一个被切分成 比特小块的字(已由消息调度约束)。我们分别把这些小块称为 被进一步切分成 比特小块 我们见证这些小块的展开版本。我们已经从消息调度得到了

等价于

s_low_sigma_1
0{0,1,2,3,4,5}
1{0,1,2,3,4,5}
0{0,1,2,3,4,5}
0{0,1,2,3,4,5}

约束:

  • s_low_sigma_1( v1 约束):

  • 检查 b 是否被正确地切分成 7 比特片的各子段。

  • 做 2 比特范围检查和 2 比特展开检查

  • 做 3 比特范围检查和 3 比特展开检查

v2

门的 v2 版本接收一个被切分成 比特小块的字(已由消息调度约束)。我们分别把这些小块称为 我们已经从消息调度得到了 。1 比特的 经展开运算后保持不变,可以直接使用。我们进一步把 切分成两个 2 比特小块 我们见证这些小块的展开版本。

等价于

s_low_sigma_1_v2
0{0,1,2,3,4,5}
1{0,1,2,3,4,5}
0{0,1,2,3,4,5}
0{0,1,2,3,4,5}

约束:

  • s_low_sigma_1_v2( v2 约束):

  • 检查 b 是否被正确地切分成 4 比特片的各子段。
  • 做 2 比特范围检查和 2 比特展开检查
  • 做 3 比特范围检查和 3 比特展开检查

辅助门(Helper gates)

小范围约束(Small range constraints)

。把这个表达式约束为零就强制 位于 内。

2 比特范围检查

sr2
1a

2 比特展开

ss2
1aa'

插值多项式(interpolation polynomials)为:

  • ()
  • ()
  • ()
  • ()

3 比特范围检查

sr3
1a

3 比特展开

ss3
1aa'

插值多项式为:

  • ()
  • ()
  • ()
  • ()
  • ()
  • ()
  • ()
  • ()

reduce_6 门

6 个元素的模 加法

输入:

检查:

假定输入已被约束为 16 比特。

  • 加法门(sa):
  • 进位门(sc):
sasc
10
11

假定输入已被约束为 16 比特。

  • 加法门(sa):
  • 进位门(sc):
sasc
00
11
00
10

reduce_7 门

7 个元素的模 加法

输入:

检查:

假定输入已被约束为 16 比特。

  • 加法门(sa):
  • 进位门(sc):
sasc
10
11

消息调度区域(Message scheduling region)

对于已填充消息的每一个块 ,要构造 个各 比特的字,方法如下:

  • 个通过把 切分成 比特的块来得到
  • 其余 个字用以下公式构造:
swsd0sd1sd2sd3ss0ss0_v2ss1ss1_v2
010000000{0,1,2,3,4,5}
100000000{0,1,2,3,4,5}
011000000{0,1,2,3,4}
100000000{0,1,2}
000000000{0,1,2,3,4,5}
000001000{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
.....................................................
000000000{0,1,2,3}
0101000000
1000000000
000000000{0,1,2,3,4,5}
000000100{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
000000001{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
.....................................................
010010000{0,1,2,3}
000000000{0,1}
000000000{0,1,2,3,4,5}
000000001{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
.....................................................
010000000{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}
010000000{0,1,2,3,4,5}
000000000{0,1,2,3,4,5}

约束:

  • sw:用 构造字
  • sd0: 的分解门
  • sd1: 的分解门(切分成 比特的片)
  • sd2: 的分解门(切分成 比特的片)
  • sd3: 的分解门(切分成 比特的片)

压缩区域(Compression region)

+----------------------------------------------------------+
|                                                          |
|          decompose E,                                    |
|          Σ_1(E)                                          |
|                                                          |
|                  +---------------------------------------+
|                  |                                       |
|                  |        reduce_5() to get H'           |
|                  |                                       |
+----------------------------------------------------------+
|          decompose F, decompose G                        |
|                                                          |
|                        Ch(E,F,G)                         |
|                                                          |
+----------------------------------------------------------+
|                                                          |
|          decompose A,                                    |
|          Σ_0(A)                                          |
|                                                          |
|                                                          |
|                  +---------------------------------------+
|                  |                                       |
|                  |        reduce_7() to get A_new,       |
|                  |              using H'                 |
|                  |                                       |
+------------------+---------------------------------------+
|          decompose B, decompose C                        |
|                                                          |
|          Maj(A,B,C)                                      |
|                                                          |
|                  +---------------------------------------+
|                  |        reduce_6() to get E_new,       |
|                  |              using H'                 |
+------------------+---------------------------------------+

初始轮(Initial round):

s_digestsd_abcdsd_efghss0ss1s_majs_ch_negs_chs_a_news_e_news_h_prime
00100000000{0,1,2}
00000000000{0,1}
00000000000{0,1,2,3,4,5}
00001000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00100000000{0,1,2}
00000000000{0,1}
00100000000{0,1,2}
00000000000{0,1}
00000000000{0,1,2,3,4,5}
00000001001{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000010{0,1,2,3,4,5}
00000010000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
01000000000{0,1,2}
00000000000{0,1}
00000000000{0,1,2,3,4,5}
00010000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
01000000000{0,1,2}
00000000000{0,1}
01000000000{0,1,2}
00000000000{0,1}
00000000000{0,1,2,3,4,5}
00000100100{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}

稳态(Steady-state):

s_digestsd_abcdsd_efghss0ss1s_majs_ch_negs_chs_a_news_e_news_h_prime
00100000000{0,1,2}
00000000000{0,1}
00000000000{0,1,2,3,4,5}
00001000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000001001{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000010{0,1,2,3,4,5}
00000010000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
01000000000{0,1,2}
00000000000{0,1}
00000000000{0,1,2,3,4,5}
00010000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000100100{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}
00000000000{0,1,2,3,4,5}

最终摘要(Final digest):

s_digestsd_abcdsd_efghss0ss1s_majs_ch_negs_chs_a_news_e_news_h_prime
10000000000000
00000000000000
10000000000000
00000000000000

译自 halo2 book:background.md

背景知识

本节涵盖理解 Halo 2 证明系统所需的背景材料。它的讲解目标是 ELI15(Explain It Like I'm 15,像对 15 岁的人那样解释)的水平;如果你觉得哪里还需要补充说明,请告诉我们

译自 halo2 book:background/fields.md

域(field)

许多密码学协议的一个基础组成部分,是被称为 域(field) 的代数结构。域是一些对象(通常是数)的集合,配备两个相关联的二元运算符 ,使得各种 域公理(field axioms) 成立。实数 就是一个具有不可数多个元素的域的例子。

Halo 使用 有限域(finite field),它只有有限个元素。有限域可以完全分类如下:

  • 如果 是一个有限域,那么对某个整数 和某个素数 ,它包含 个元素;
  • 任意两个具有相同元素个数的有限域是同构的。特别地,素域(prime field) 中的所有算术运算,都与整数模 的加法和乘法同构,即与 中的运算同构。这就是我们常把 称为 模(modulus) 的原因。

我们将域写作 ,其中 。素数 称为它的 特征(characteristic)。在 的情形下,域 是域 次扩张(extension)。(作个类比,复数 是实数的扩张。)然而在 Halo 中我们不使用扩域(extension field)。每当我们写 时,指的都是我们所说的 素域(prime field),它有素数 个元素,即

重要说明:

  • 任何域中都有两个特殊元素:,即加法单位元(additive identity);以及 ,即乘法单位元(multiplicative identity)。
  • 当域元素以二进制整数形式表示时,其最低有效位(least significant bit)可以被解释为它的"符号(sign)",以帮助把它与其加法逆元(negation)区分开。这是因为对于某个最低有效位为 的非零元素 ,我们有 的最低有效位为 ,反之亦然。我们也可以用一个元素是否大于 来赋予它一个"符号"。

有限域稍后将用于构造 多项式(polynomial)椭圆曲线(elliptic curve)。椭圆曲线是群(group)的例子,我们接下来讨论群。

群(group)

群比域更简单也更受限;它们只有一个二元运算符 ,公理也更少。它们也有一个单位元,我们记作

群中任意非零元素 都有一个 逆元(inverse) ,它是 唯一 满足 的元素

例如, 的非零元素的集合构成一个群,其中群运算由域上的乘法给出。

(旁注)加法记法 vs 乘法记法

如果像我们上面那样把 写成 或者省略(即把 写作 ),把单位元写作 ,把求逆写作 ,那么我们说这个群是"用乘法记法书写的"。如果把 写成 ,把单位元写作 ,把求逆写作 ,那么我们说它是"用加法记法书写的"。

习惯上,椭圆曲线群用加法记法,而当元素来自有限域时用乘法记法。

使用加法记法时,对于非负的 ,我们还写

并称之为"标量乘法(scalar multiplication)";我们也常用大写字母作为表示群元素的变量。使用乘法记法时,我们还写

并称之为"幂运算(exponentiation)"。无论哪种情形,我们把满足 的标量 称为 为底的"离散对数(discrete logarithm)"。我们可以通过求逆把标量扩展到负整数,即

有限群的元素 阶(order) 定义为使得 (乘法记法)或 (加法记法)成立的最小正整数 群的阶 是群中元素的个数。

群总有一个 生成集(generating set),即这样一个元素集合:我们能(用乘法术语来说)把群中任意元素表示为这些元素的幂的乘积。所以若生成集是 ,我们就能把群中任意元素表示为 。对于给定的群,可以有许多不同的生成集。

如果一个群有一个仅含单个元素(记为 )的(不一定唯一的)生成集,则称它为 循环群(cyclic)。在这种情形下,我们可以说 生成该群,并且 的阶就是该群的阶。

任意 阶有限循环群 同构(isomorphic) 于整数模 (记作 ),使得:

  • 中的运算 对应于模 加法;
  • 中的单位元对应于
  • 某个生成元 对应于

给定一个生成元 ,这个同构在 方向上总是易于计算的;它就是 (或在加法记法下,)。但在 方向上一般可能很难计算;当我们讲到 椭圆曲线(elliptic curve) 时会进一步讨论这一点。

如果有限群的阶 是素数,那么这个群是循环群,并且每个非单位元都是生成元。

有限域的乘法群(multiplicative group)

我们用记号 表示集合 上的乘法群(即群运算是 中的乘法)。

中求逆的一个快捷方式是 。其原因源自 费马小定理(Fermat's little theorem),该定理指出对任意整数 都有 。如果 非零,我们可以把它除两次 ,得到

的一个生成元,那么它的阶为 (等于 中元素的个数)。因此,对任意元素 ,都存在唯一的整数 使得

注意 (其中 )实际上可以被解释为 ,其中 。事实上,对所有 都有 成立。因此,非零域元素的乘法可以被解释为相对于某个固定生成元 的模 加法。加法只是发生"在指数上"。

这也是看待计算域中逆元为何用 的另一种角度:

所以

蒙哥马利技巧(Montgomery's Trick)

蒙哥马利技巧(以 Peter Montgomery(安息)命名)是一种同时计算多个群逆元的方法。它常用于计算 中的逆元,相对于乘法而言,这些逆元的计算开销相当大。

设想我们需要计算三个非零元素 的逆元。我们改为先计算乘积 ,然后计算逆元

现在我们可以把 乘以 得到 ,把 乘以 得到 ,随后再把它乘以 就能得到它们各自的逆元。

这个技巧可以推广到任意数量的群元素,而只需要做一次求逆。

乘法子群(multiplicative subgroups)

(运算为 )的一个 子群(subgroup),是 中这样一个元素子集:它在 下也构成一个群。

在上一节中我们说过 阶乘法群 的一个生成元。这个群的阶是 合数(composite),因此由中国剩余定理1,它有真子群(strict subgroups)。举例来说,设想 ,于是 分解为 。这样就存在一个 阶子群的生成元 和一个 阶子群的生成元 。因此 中的所有元素都可以唯一地写成 ,其中 (模 )、(模 )。

如果我们有 ,注意当我们计算

时会发生什么;我们实际上"杀死"了 阶子群分量,产生了一个 阶子群中的值。

拉格朗日定理(群论)(Lagrange's theorem (group theory)) 指出,有限群 的任意子群 的阶整除 的阶。因此, 任意子群的阶必定整除

像 Halo 2 这样基于 PLONK 的证明系统,更便于在具有大量乘法子群且其分布"光滑(smooth)"的域上使用(这使得随着电路规模增大,性能悬崖更小、更细粒度)。Pallas 和 Vesta 曲线特别地具有如下形式的素数

其中 为奇数(即 有 32 个低位零比特)。这意味着对所有 ,它们都有 阶的乘法子群。这些 2-adic 子群非常适合 高效的 FFT,同时也支持种类繁多的电路规模。

平方根(square roots)

在域 中,恰好一半的非零元素是平方数(squares);其余的是非平方数,或称"二次非剩余(quadratic non-residues)"。为了理解原因,考虑一个生成 阶乘法子群的 (之所以存在,是因为 是大于 的素数,所以 能被 整除),以及一个生成 阶乘法子群的 ,其中 。那么每个元素 都可以唯一地写成 ,其中 。所有元素中有一半满足 ,另一半满足

让我们考虑 的简单情形,此时 为奇数(如果 为偶数,那么 就能被 整除,这与 矛盾)。如果 是平方数,那么必定存在 使得 。但这意味着

换言之,在这个特定的域中所有平方数都不生成 阶乘法子群,于是由于一半的元素生成 阶子群,那么至多一半的元素是平方数。事实上恰好一半的元素是平方数(因为把每个非平方元素平方都给出一个唯一的平方数)。这意味着我们可以假定所有平方数都可以写成 (对某个 ),因此求平方根的问题就归结为做 的幂运算。

的情形下,事情变得更复杂,因为 不存在。我们把 写成 ,其中 为奇数。 的情形不可能, 的情形就是我们已经描述过的,所以考虑 生成一个 阶乘法子群, 生成奇数阶 阶乘法子群。那么每个元素 都可以写成 ,其中 。如果该元素是平方数,那么存在某个 ,它可以写成 ,其中 。这意味着 ,因此我们有 ,且 。在这种情形下 必须为偶数,否则就不可能对任何 都有 。在 不是平方数的情形下, 为奇数,所以一半的元素是平方数。

为了计算平方根,我们可以先把元素 升到 次幂以"杀死" 阶分量,得到

然后把这个结果升到 次幂,以撤销原来的幂运算对 阶分量的影响:

(因为 互素)。这就裸露出 值,我们可以平凡地处理它。我们可以类似地杀死 阶分量以得到 ,再把这些值组合起来得到平方根。

事实证明,在 的情形下,有更简单的算法把其中几个幂运算合并在一起以提高效率。对于其他的 值,已知唯一的方法是通过反复平方来手动提取 ,直到对 的每一位都得到单位元为止。这就是 Tonelli-Shanks 平方根算法(Tonelli-Shanks square root algorithm) 的精髓,描述了通用策略。(还有另一种使用二次扩域的平方根算法,但直到素数变得相当大之前,它在效率上都不划算。)

单位根(roots of unity)

在前面几节中,我们把 写成 ,其中 为奇数,并说明了元素 生成了 阶子群。为方便起见,记 元素 被称为 单位根(roots of unity)

本原单位根(primitive root of unity) 是这样一个 次单位根:除非 ,否则

重要说明:

  • 如果 次单位根, 满足 如果 那么

  • 等价地,单位根是方程 的解。

  • ("取负引理(Negation lemma)")。证明: 由于 的阶是 因此,

  • ("减半引理(Halving lemma)")。证明: 换言之,如果我们把 次单位根中每个元素都平方,我们只会得到一半的元素,(即 次单位根)。元素与其平方之间是二对一的映射。

参考文献


  1. Friedman, R. (n.d.) "Cyclic Groups and Elementary Number Theory II" (p. 5).

译自 halo2 book:background/polynomials.md

多项式(polynomial)

上以形式不定元(formal indeterminate) 表示的多项式。举例来说,

定义了一个 次多项式。 称为常数项(constant term)。 次多项式有 个系数。我们常常想要计算把形式不定元 替换成某个具体值 的结果,记作

在数学中这通常被称为"在点 处求值 "。这里"点(point)"一词源自多项式 形式的几何用法,其中 是二维空间中某点的坐标。然而,我们处理的多项式几乎总是被约束为等于零,并且 将是 某个域的元素。这不应与 椭圆曲线(elliptic curve) 上的点混淆,我们也会用到椭圆曲线,但绝不会在多项式求值的语境中使用。

重要说明:

  • 多项式相乘产生的乘积多项式,其次数是各因子次数之和。多项式除法则从次数中作减法。
  • 给定一个 次多项式 ,如果我们在不同的点上得到该多项式的 个求值,那么这些求值就完美地确定了该多项式。换言之,给定这些求值,我们可以通过多项式插值(interpolation)得到唯一的 次多项式
  • 是多项式 系数表示(coefficient representation)。等价地,我们可以使用它在 个不同点上的 求值表示(evaluation representation) 两种表示都唯一地确定同一个多项式。

(旁注)霍纳法则(Horner's rule)

霍纳法则允许高效地求一个 次多项式的值,只需 次乘法和 次加法。它就是下面这个恒等式:

快速傅里叶变换(Fast Fourier Transform, FFT)

FFT 是在多项式的系数表示与求值表示之间转换的一种高效方法。它在 次单位根 处对多项式求值,其中 是本原 次单位根。通过利用单位根中的对称性,FFT 的每一轮都把求值问题缩减为一个只有原来一半大小的问题。最常见的是,我们使用长度为 2 的某个幂次 的多项式,并递归地应用这个减半缩减。

动机:快速多项式乘法

在系数表示下,把两个多项式相乘 需要 次运算:

其中第一个多项式中的 项每一项都必须乘以第二个多项式的 项。

然而在求值表示下,多项式乘法只需要 次运算:

其中每个求值是逐点(pointwise)相乘的。

这提示了如下快速多项式乘法的策略:

  1. 在全部 个点处对多项式求值;
  2. 在求值表示中执行快速逐点乘法();
  3. 转换回系数表示。

现在的挑战是如何高效地 求值(evaluate)插值(interpolate) 多项式。朴素地说,在 个点处对一个多项式求值需要 次运算(我们在每个点处使用 的霍纳法则):

为方便起见,我们将上述矩阵记为:

被称为 离散傅里叶变换(Discrete Fourier Transform) 也称为 范德蒙德矩阵(Vandermonde matrix)。)

(基-2)Cooley-Tukey 算法

我们的策略是把一个大小为 的 DFT 分成两个交织(interleaved)的大小为 的 DFT。给定多项式 我们把它拆分成偶次项和奇次项:

为恢复原始多项式,我们做

在点 )上试一下,我们开始注意到一些对称性:

注意我们只在半个定义域 (减半引理)上对 求值。这给了我们重构 在整个定义域 上所需的所有项:这意味着我们已经把一个长度为 的 DFT 转换成了两个长度为 的 DFT。

我们选择 为 2 的某个幂次(必要时通过补零),并递归地应用这个分治策略。由主定理(Master Theorem)1,这给了我们一个具有 次运算的求值算法,也就是快速傅里叶变换(FFT)。

逆 FFT(Inverse FFT)

至此我们已经对多项式求了值并逐点相乘。剩下的就是把乘积从求值表示转换回系数表示。为此,我们只需对求值表示再调用一次 FFT。不过这一次我们还要:

  • 在范德蒙德矩阵中把 替换为 ,并且
  • 把最终结果乘以一个因子

换言之:

(要理解逆 FFT 为何与 FFT 形式相似,请参阅 2 的 Slide 13-1。下图同样取自 2。)

Schwartz-Zippel 引理

Schwartz-Zippel 引理非形式地指出"不同的多项式在大多数点上都不同"。形式上,它可以写成如下:

是一个 元、次数为 的非零多项式。设 是一个至少含有 个元素的有限数集。如果我们从 中随机选取 ,那么

在我们熟悉的单变量情形 下,这归结为:一个 次非零多项式至多有 个根。

Schwartz-Zippel 引理用于多项式相等性测试。给定两个多变量多项式 ,次数分别为 ,我们可以对随机的 测试是否 ,其中 的大小至少为 如果两个多项式相同,这将永远成立;而如果两个多项式不同,那么等式成立的概率至多为

消失多项式(Vanishing polynomial)

考虑以本原单位根 生成的 阶乘法子群 。对所有 我们有 换言之, 的每个元素都满足方程

也就是说每个元素都是 的根。我们称 上的 消失多项式(vanishing polynomial),因为它在 的所有元素上求值都为零。

这在检查多项式约束时尤其方便。例如,要检查 上成立,我们只需检查 的某个倍数。换言之,如果把我们的约束除以消失多项式仍然得到某个多项式 我们就确信 上成立。

拉格朗日基函数(Lagrange basis functions)

TODO:(简要地)解释一般意义上的基(basis)是什么。

多项式通常用单项式基(monomial basis)书写(例如 )。然而,当在 阶乘法子群上工作时,我们发现用拉格朗日基(Lagrange basis)表达更为自然。

考虑以本原单位根 生成的 阶乘法子群 。对应于这个子群的拉格朗日基是一组函数 ,其中

我们可以更紧凑地写成 其中 是克罗内克 delta 函数(Kronecker delta function)。

现在,我们可以把多项式写成拉格朗日基函数的线性组合,

这等价于说 处求值为 ,在 处为 ,在 处为 依此类推。

当在乘法子群上工作时,拉格朗日基函数有一种便利的稀疏表示,形如

其中 是重心权重(barycentric weight)。(要理解这个形式是如何推导出来的,请参阅 3。)对于 我们有

假设我们给定了一组求值点 。由于我们不能假定这些 构成一个乘法子群,我们也考虑一般情形下的拉格朗日多项式 。那么我们可以构造:

这里,每个 都会产生一个为零的分子项 使得整个乘积求值为零。另一方面, 会在每一项处求值为 ,导致整体乘积为一。这就在集合 上给出了所需的克罗内克 delta 行为

拉格朗日插值(Lagrange interpolation)

给定一个处于求值表示下的多项式

我们可以在拉格朗日基下重构它的系数形式:

其中

参考文献


  1. Dasgupta, S., Papadimitriou, C. H., & Vazirani, U. V. (2008). "Algorithms" (ch. 2). New York: McGraw-Hill Higher Education.

  2. Golin, M. (2016). "The Fast Fourier Transform and Polynomial Multiplication" [lecture notes], COMP 3711H Design and Analysis of Algorithms, Hong Kong University of Science and Technology. ↩2

  3. Berrut, J. and Trefethen, L. (2004). "Barycentric Lagrange Interpolation."

译自 halo2 book:background/groups.md

密码学群(cryptographic groups)

逆元与群(Inverses and groups) 一节中,我们引入了 群(group) 的概念。群有一个单位元和一个群运算。在本节中,我们将以加法记法书写群,即单位元是 ,群运算是

某些群可以用作 密码学群(cryptographic group)。冒着过度简化的风险来说,这意味着求群元素 以给定基 为底的离散对数(discrete logarithm)的问题——即求满足 ——在一般情况下是困难的。

Pedersen 承诺(Pedersen commitment)

Pedersen 承诺 [P99] 是一种以可验证的方式承诺(commit)某条秘密消息的方法。它使用两个随机的公开生成元 其中 是阶为 的密码学群。在 中选取一个随机秘密 ,要承诺的消息 来自 的任意子集。承诺为

为打开(open)承诺,承诺者揭示 从而允许任何人验证 确实是对 的承诺。

注意 Pedersen 承诺方案是同态的(homomorphic):

假定离散对数假设成立,Pedersen 承诺还是完美隐藏(perfectly hiding)和计算绑定(computationally binding)的:

  • 隐藏(hiding):敌手选择消息 承诺者承诺其中一条消息 给定 敌手猜中正确的 的概率不超过
  • 绑定(binding):敌手无法选出两条不同的消息 以及随机数 使得

向量 Pedersen 承诺(Vector Pedersen commitment)

我们可以使用 Pedersen 承诺方案的一个变体,一次性承诺多条消息 。这一次,我们必须采样相应数量的随机公开生成元 外加像之前一样的单个随机生成元 (用于隐藏)。那么,我们的承诺方案为:

TODO:这是否是位置绑定的(positionally binding)?

Diffie–Hellman

使用密码学群的协议的一个例子是 Diffie–Hellman 密钥协商 [DH1976]。Diffie–Hellman 协议是供两个用户 Alice 和 Bob 生成共享私钥的方法。它的过程如下:

  1. Alice 和 Bob 公开地约定两个素数 其中 很大, 是一个本原根 (注意 是群 的一个生成元。)
  2. Alice 选择一个大随机数 作为她的私钥。她计算她的公钥 并把 发送给 Bob。
  3. 类似地,Bob 选择一个大随机数 作为他的私钥。他计算他的公钥 并把 发送给 Alice。
  4. 现在 Alice 和 Bob 都计算他们的共享密钥 Alice 计算为 Bob 计算为

潜在的窃听者只知道 ,却需要推导出 :换言之,他们需要从 得到离散对数 ,或从 得到 ,而我们假定这在 中计算上是不可行的。

更一般地,使用与 Diffie–Hellman 类似思想的协议在整个密码学领域随处可见。实例化密码学群的一种方式是用 椭圆曲线(elliptic curve)。在深入讨论椭圆曲线之前,我们先描述一些可以用于任意群的算法。

多标量乘法(Multiscalar multiplication)

TODO:Pippenger 算法(Pippenger's algorithm)

参考:https://jbootle.github.io/Misc/pippenger.pdf

译自 halo2 book:background/curves.md

椭圆曲线(elliptic curve)

在有限域上构造的椭圆曲线是另一种重要的密码学工具。

我们使用椭圆曲线,因为它们提供了一个密码学 群(group),即一个其中离散对数问题(下面讨论)困难的群。

定义曲线方程有若干种方式,但就我们的目的而言,设 是一个大的(255 位的)域,再让 (对某个常数 )的解 的集合定义椭圆曲线 上的 -有理点(-rational points)。这些 坐标称为"仿射坐标(affine coordinates)"。每个 -有理点,连同充当群单位元的"无穷远点(point at infinity)" ,都可以被解释为某个群的元素。按惯例,椭圆曲线群用加法记法书写。

"一条直线上的三个点之和为零,即无穷远点。"

群加法法则很简单:要把两个点相加,找到同时经过这两个点的直线并求出第三个点,然后对其 坐标取负。一个点与自身相加的情形,称为倍点(point doubling),需要特殊处理:我们找到与该点相切的直线,然后找到与这条直线相交的另一个唯一的点并取负。此外,在一个点与其负元"相加"的情况下,结果是无穷远点。

把点相加和倍乘的能力自然地给了我们用整数(称为 标量(scalars))来缩放它们的方法。曲线上点的个数即群的阶。如果这个数是素数 ,那么这些标量可以被视为某个 标量域(scalar field) 的元素。

椭圆曲线在正确设计时具有一个重要的安全性质。给定两个随机元素 ,求满足 (也就是 关于 的离散对数),在经典计算机上被认为是计算上不可行的。这称为椭圆曲线离散对数假设(elliptic curve discrete log assumption)。

如果一个椭圆曲线群 具有素数阶 (就像 Halo 2 中使用的那些),那么它是一个有限循环群。回忆 群(groups) 一节中讲过,这意味着它同构于 ,或等价地同构于标量域 。每个可能的生成元 都确定了这个同构;于是标量那一侧的某个元素,恰好就是对应群元素关于 的离散对数。在密码学安全的椭圆曲线的情形下,这个同构在 方向上难以计算,因为椭圆曲线离散对数问题是困难的。

利用这个同构、用标量而不是用群元素来思考基于群的密码学协议和算法,有时是有帮助的。这能使证明和记法更简单。

例如,在证明系统的论文中,用记号 来表示离散对数为 的群元素(其中生成元是隐含的)已变得很常见。

我们在 "distinct-x 定理" 中也用到了这个想法,以证明 Sapling 中 椭圆曲线标量乘法 的优化的正确性,以及原始 Halo 论文 附录 C 中一个基于自同态(endomorphism)的优化。

曲线算术(Curve arithmetic)

倍点(Point doubling)

最简单的情形是给一个点 做倍点。继续我们的例子 ,这首先通过计算导数来完成

为得到 的表达式,我们考虑

为得到 的表达式,我们把 代入椭圆曲线方程:

比较 项的系数给出

射影坐标(Projective coordinates)

不幸的是,这需要对 做一次开销很大的求逆。我们可以通过安排我们的方程来"推迟"逆的计算,从而避免这一点,因为在单个曲线运算之后,我们通常不需要立即得到所得点的实际仿射 坐标。我们引入第三个坐标 ,并像下面这样把曲线方程用 缩放:

我们原来的曲线就是这条曲线在 限制下的样子。如果我们允许仿射点 表示,那么我们就有了 齐次射影曲线(homogenous projective curve)

时,从 得到 就像计算 那么简单。(当 时,我们处理的是无穷远点 。)在这种形式下,我们现在有了一种便利的方法来推迟倍点所需的求逆。一般策略是用 表达为有理函数(rational function),重新整理以使它们的分母相同,然后取所得点 ,使 为公共分母且

射影坐标往往(但并非总是)比仿射坐标更高效。当我们有不同的方式应用蒙哥马利技巧时,或者当我们处在电路环境中(其中乘法和求逆的开销大致相当——至少就电路规模而言)时,可能会有例外。

下面展示一个不做求逆给点 做倍点的例子。代入 给出

并给出

注意 的分母是相同的。因此,我们不必计算 ,而是可以计算 ,其中 ,且 设为相应的分子,使得 。这完全避免了倍点时执行求逆的需要,而当把两个不同的点相加时也可以做类似的事情。

点加法(Point addition)

现在我们把两个具有不同 坐标的点 (其中 )相加,得到 直线 的斜率为

利用 的表达式,我们计算 坐标 为:

的表达式代入曲线方程 得到

比较 项的系数给出


重要说明:

  • 存在不带边界情形的点加法高效公式1(即所谓的"完备(complete)"公式),它把加法和倍点两种情形统一在一起。用这些公式把一个点与其负元相加,结果产生 ,它表示无穷远点。
  • 此外,还有其他模型,比如雅可比表示(Jacobian representation),其中 ,曲线用 而不是 重新缩放,这种表示具有甚至更高效的算术,但没有统一/完备公式。
  • 我们可以在齐次射影坐标空间中通过把两个曲线点 的 Z 坐标"齐次化(homogenizing)"来轻松地比较它们是否相等;检查变为

曲线自同态(Curve endomorphisms)

设想 有一个本原三次单位根,换言之 因而存在一个元素 生成一个 阶乘法子群。注意我们的示例椭圆曲线 上的一个点 有两个"表亲"点:,因为计算 实际上杀死了 坐标的 分量。应用映射 就是对曲线应用一个自同态(endomorphism)。其中涉及的精确机制很复杂,但当曲线有素数 个点(因而有素数"阶")时,这个自同态的效果就是把该点乘以 中的一个标量,而这个标量也是标量域中的一个本原三次根

曲线点压缩(Curve point compression)

给定曲线上的一个点 ,我们知道它的负元 也在曲线上。要唯一地确定一个点,我们只需编码它的 坐标连同其 坐标的符号。

序列化(Serialization)

正如 域(Fields) 一节中提到的,我们可以把域元素的最低有效位解释为它的"符号",因为它的加法逆元总有相反的最低有效位。所以我们把 坐标的最低有效位记为 sign

Pallas 和 Vesta 定义在 域上,其元素可以用 位表示。这恰好在 32 字节表示中留出一个未用的位。我们把 坐标的 sign 位塞进 坐标表示的最高位:

         <----------------------------------- x --------------------------------->
Enc(P) = [_ _ _ _ _ _ _ _] [_ _ _ _ _ _ _ _] ... [_ _ _ _ _ _ _ _] [_ _ _ _ _ _ _ sign]
          ^                <------------------------------------->                 ^
         LSB                              30 bytes                                MSB

充当群单位元的"无穷远点" 没有仿射 表示。然而事实证明,Pallas 或 Vesta 曲线上都没有 的点。因此我们使用"伪造的"仿射坐标 来编码 ,其结果是全零的 32 字节数组。

反序列化(Deserialization)

反序列化一个压缩曲线点时,我们首先读取最高有效位作为 ysign,即 坐标的符号。然后,我们把这一位置零以恢复原始的 坐标。

如果 我们返回"无穷远点" 。否则,我们继续计算 这里,我们读取 的最低有效位作为 sign。如果 sign == ysign,我们已经有了正确的符号,直接返回曲线点 。否则,我们对 取负并返回

曲线环(Cycles of curves)

是有限域 上的椭圆曲线,其中 为素数。我们把它记为 并把 上的点构成的群记出来,其阶为 对于这条曲线,我们称 为"基域(base field)",称 为"标量域(scalar field)"。

我们在椭圆曲线 上实例化我们的证明系统。这使我们能够证明关于 -算术电路可满足性(arithmetic circuit satisfiability)的命题。

(旁注)如果我们的曲线 上,为什么算术电路反而在 中? 证明系统基本上是在电路中标量的编码(或更确切地说,是对系数为标量的多项式的承诺)上工作。当标量的编码/承诺是 中的椭圆曲线点时,这些标量就在 中。

然而,验证者(verifier)的大部分算术计算都在基域 上,因此可以高效地表达为一个 -算术电路。

(旁注)为什么验证者的计算(主要)在 上? Halo 2 验证者实际上必须使用电路输出的信息执行群运算。像倍点和点加法这样的群运算使用 中的算术,因为点的坐标在 中。

这促使我们构造另一条标量域为 的曲线,它有一个 -算术电路,能够高效地验证来自第一条曲线的证明。作为额外好处,如果这第二条曲线的基域是 它就会生成可以在第一条曲线的 -算术电路中被高效验证的证明。换言之,我们在 上实例化第二个证明系统,与第一个形成一个 2-环(2-cycle):

TODO:Pallas-Vesta 曲线

参考:https://github.com/zcash/pasta

哈希到曲线(Hashing to curves)

有时,能够针对某个输入产生椭圆曲线 上的一个随机点,并且使任何人都不会知道它(关于任何其他基的)离散对数,是很有用的。

这在 关于哈希到椭圆曲线的互联网草案(Internet draft on Hashing to Elliptic Curves) 中有详细描述。根据效率和安全要求的不同,可以使用若干种算法。该互联网草案使用的框架用到了几个函数:

  • hash_to_field:接受一个字节序列输入,并把它映射到基域 中的一个元素;
  • map_to_curve:接受一个 元素,并把它映射到

TODO:简化 SWU(Simplified SWU)

参考:https://eprint.iacr.org/2019/403.pdf

参考文献


  1. Renes, J., Costello, C., & Batina, L. (2016, May). "Complete addition formulas for prime order elliptic curves." In Annual International Conference on the Theory and Applications of Cryptographic Techniques (pp. 403-428). Springer, Berlin, Heidelberg.

译自 halo2 book:background/pc-ipa.md

使用内积论证(inner product argument)的多项式承诺(polynomial commitment)

我们想要承诺(commit)某个多项式 ,并能够可证明地在任意点处对所承诺的多项式求值。朴素的方案是让证明者(prover)直接把多项式的系数发给验证者(verifier):然而,这需要 的通信量。我们的多项式承诺方案则用 的通信量完成这件事。

Setup

给定参数 我们生成公共参考串(common reference string) ,它为本方案定义了若干常数:

  • 是一个素数阶 的群;
  • 是一个由 个随机群元素组成的向量;
  • 是一个随机群元素;以及
  • 是阶为 的有限域。

Commit

Pedersen 向量承诺 定义为

其中 是某个多项式, 是某个盲化因子(blinding factor)。这里,向量的每个元素 次项的系数,而 的最高次数为

Open(证明者)与 OpenVerify(验证者)

改进版的内积论证是关于如下关系的知识论证(argument of knowledge)

其中 由求值点 的递增幂次组成。这使得证明者能向验证者证明:承诺 "内部"所含的多项式在 处求值为 而且,所承诺的多项式具有最高次数

内积论证进行 轮。就我们的目的而言,只需了解它的最终输出即可,对于中间各轮我们仅提供直觉。(完整解释请参阅 Halo 论文第 3 节。)

在开始论证之前,验证者选取一个随机群元素 并把它发给证明者。我们在第 轮初始化论证,使用向量 在每一轮 中:

  • 证明者通过取 的某个内积来计算两个值 。注意它们在某种意义上是"交叉项(cross-terms)": 的下半部分与 的上半部分一起使用,反之亦然:

  • 验证者发出一个随机挑战
  • 证明者使用 来压缩 的下半部分和上半部分,从而产生一个长度为原来一半的新向量 向量 被类似地压缩以给出 (使用 而不是 )。
  • 被输入到下一轮

注意在最后一轮 结束时,我们剩下 它们各自的长度都为 1。其直觉是,这些最终的标量,连同每一轮的挑战 和"交叉项" ,编码了每一轮中的压缩。由于证明者事先并不知道挑战 ,他们将无法操纵各轮的压缩。因此,对这些最终项检查一个约束,应当能强制保证压缩是正确执行的,并且原始的 在经历压缩之前满足该关系。

注意 只是公开已知的 的重新排列,其中混入了各轮挑战 :这意味着验证者可以独立地计算 ,并验证证明者提供的是同样的值。

译自 halo2 book:background/recursion.md

递归(recursion)

替代术语:归纳(Induction);累加方案(Accumulation scheme);携带证明的数据(Proof-carrying data)

然而, 的计算需要一个长度为 的多重幂运算(multiexponentiation) 其中 由按二进制计数结构排列的各轮挑战 组成。这就是我们想要在一批证明实例上分摊(amortise)的线性时间计算。我们注意到,与其计算 不如把 表达为对某个多项式的承诺

其中 是一个次数为 的多项式。

由于 是一个承诺,它可以在一个内积论证中被检查。验证者电路见证(witness) ,并把 作为公开输入(public inputs)带出到证明 下一个验证者实例使用内积论证检查 ;这包括检查 在某个随机点处求值为给定挑战 所对应的期望值。回忆 上一节 中讲过,这个检查只需要 的工作量。

在检查完 之后,电路剩下一个新的 连同为该检查采样的 挑战。要完全接受 为有效,我们应当执行 的线性时间计算。我们再次延迟这个计算,方法是见证 并把 作为公开输入带出到证明

这样从一个证明实例进行到下一个,直到我们对这批证明的规模感到满意为止。最后我们执行单个线性时间计算,从而判定整批证明的有效性。

我们回忆 曲线环(Cycles of curves) 一节,我们可以在一个 2-环上实例化这个协议,其中由一条曲线产生的证明能在另一条曲线的电路中被高效验证。然而,其中某些验证者检查实际上可以在本地电路(native circuit)中高效执行;这些检查被"推迟(deferred)"到下一个本地电路(见下图),而不是立即传递给另一条曲线。

译自 halo2 book:background/plonkish.md

[WIP] PLONKish 算术化(arithmetization)

我们把电路所定义于其上的域称为

,并假定 阶的本原单位根,使得 有一个乘法子群 。这构成一个对应于该子群中元素的拉格朗日基(Lagrange basis)。

多项式规则(Polynomial rules)

一条多项式规则定义了一个约束,该约束必须在每一行(即在乘法子群中的每个元素处)于其指定的各列之间成立。

例如:

a * sa + b * sb + a * b * sm + c * sc + PI = 0

列(Columns)

  • 固定列(fixed columns):对某个特定电路的所有实例都是固定的。这包括选择子列(selector columns),它们把多项式规则的某些部分"打开"或"关闭"以形成一个"自定义门(custom gate)"。它们也可以包含任何其他固定数据。
  • 辅助列(advice columns):在电路的每个实例中分配的可变值。对应于证明者的秘密见证(secret witness)。
  • 公开输入(public input):类似于辅助列,但是公开已知的值。

每一列都是一个由 个值组成的向量,例如 。我们可以把这个向量看作列多项式 的求值形式。要恢复系数形式,我们可以使用 拉格朗日插值(Lagrange interpolation),使得

相等性约束(Equality constraints)

  • 在一组列之间定义置换(permutation),例如
  • 断言这些列中特定单元格之间的相等性,例如
  • 构造置换后的列,它们应当求值为与原始列相同的值

置换总乘积(Permutation grand product)

其中 在乘法子群的大小上索引, 在参与置换的辅助列上索引。这是一个滚动乘积(running product),其中每一项都包含其之前各项的累积乘积。

TODO: 是什么?保持各列线性无关

检查约束:

  1. 第一项等于一

  2. 滚动乘积构造良好。对每一行,我们检查下式成立: 重新整理得到 这正是我们最初定义总乘积多项式的方式。

查找(Lookup)

参考:Generic Lookups with PLONK (DRAFT)

消失论证(Vanishing argument)

我们想要检查由门约束(gate constraints)、置换约束(permutation constraints)和查找约束(lookup constraints)所定义的表达式,在乘法子群的所有元素处求值都为零。为此,证明者把所有表达式坍缩(collapse)成一个多项式 其中 是表达式的个数, 是一个用于保持各约束线性无关的随机挑战。然后证明者把它除以消失多项式(见小节:消失多项式(Vanishing polynomial)),并对所得的商承诺

验证者以一个随机求值点 回应,对此证明者回复声称的求值 现在,验证者剩下要检查的就是这些求值是否满足

注意我们还没有检查所承诺的多项式在 处确实求值为所声称的值,即 这个检查由多项式承诺方案处理(在下一节描述)。