Circom 零知识电路简介

本文介绍了 Circom 编程语言,它用于创建 Rank 1 Constraint Systems (R1CS) 并填充 R1CS 的 witness 向量,主要是为了简化约束系统的设计和自动化 witness 的生成。文章还解释了 Circom 存在的意义,以及它如何帮助开发者更轻松地进行零知识证明相关的开发,最后说明了学习 Circom 的理由,并概述了资源结构,包括语法和约束设计。

Circom 是一种用于创建 Rank 1 Constraint Systems (R1CS) 并填充 R1CS 的 witness 向量的编程语言。

R1CS 格式之所以引人关注,是因为该格式对于构建 SNARKs 非常有用,尤其是 Groth16。通过 SNARKs,我们可以实现可验证计算,从而能够证明计算的正确性。在验证时,感兴趣方花费较少的计算资源来确认正确性,而不是自己执行计算。也可以生成证明而不泄露底层数据,在这种情况下,我们将其称为 zkSNARKs。

我们 ZK 书的第一部分侧重于证明给定 R1CS 的 witness 的有效性。本资源侧重于如何以编程方式生成 R1CS,以及如何设计它们来模拟真实的算法,例如虚拟机或密码学哈希函数。

预备知识

我们希望读者已经熟悉我们 ZK 书中的以下章节:

我们将假设读者了解什么是 R1CS 以及它代表什么。这在上面的四个章节中已得到充分解释。

没有必要完全理解 ZK 背后的数学原理才能使用 Circom,但是必须完全掌握一些原则,否则 Circom 将毫无意义。

尽管如此,如果读者想在 ZK 领域有所发展,那么学习 ZK 的基础知识必不可少。为此,我们强烈建议通读 ZK 书籍 的前两部分,并从头开始构建 Groth16 证明系统,以加强学习。

但是,如果读者的目标是快速了解 ZK 应用程序,那么我们建议阅读上面列出的四个章节,然后使用本资源。

为什么存在 Circom

创建 Circom 是为了解决为 SNARKs 开发约束系统中的两个主要问题。

  1. 手动设计约束系统既繁琐又容易出错,尤其是在处理大规模或重复性约束时。
  2. 填充 witness 同样具有挑战性,并且需要手动计算中间值,而这些中间值可以通过编程方式推导出来。

因此,Circom 1) 简化了约束设计,并 2) 自动化了 witness 填充。

1. 设计约束系统很繁琐

手动设计一组(正确的)约束,然后将其转换为 R1CS 的任务既繁琐又容易出错。创建 Circom 是为了通过以编程方式生成约束来使此任务更轻松且不那么繁琐。

例如,要说值 x 只能具有值 $\set{1,2,3}$,我们可以使用约束来表示:

$$ 0 === (x – 1) (x – 2) (x – 3) $$

但是,R1CS 每个约束只能有一个非恒定的乘法,因此我们必须将上述约束分解为两个约束:

$$ \begin{align} s &=== (x – 1)(x – 2) &&= x x – 3 * x + 2 \\

0 &=== s(x – 3) &&= x s – 3 s \end{align*} $$

对于小型系统,这种手动转换是可管理的。但是,如果我们需要为 100 甚至 1000 个变量创建此约束,则手动执行此操作将非常烦人。如果我们有数千个非常相似的约束,最好为约束创建一个“模板”并在 for 循环中生成约束。Circom 允许我们以编程方式创建这些约束。

例如,假设我们要约束 1,000 个变量具有值 $\set{0,1}$。Circom 可以通过循环生成这些值,如下所示:

template Constrain1000Example() {
  signal input in[1000];

  for (var i = 0; i < 1000; i++) {
    0 === in[i] * (in[i] - 1);
  }
}

component main = Constrain1000Example();

我们将在后面的章节中进一步解释语法,但核心思想是我们定义了一个约束 0 === in[i] * (in[i] - 1) 并重复了 1000 次。

2. 填充 witness 很繁琐

ZK 上下文中的 witness 是对变量的赋值,该赋值满足算术电路中的所有约束。

正如我们在 算术电路 的文章中所看到的,证明一个数字小于另一个数字需要将两个数字都转换为二进制,因为“大于”在有限域中没有意义,因为数字会回绕。

以二进制形式表示数字 $x$,假设它适合四个位,则需要 $x$ 满足以下约束:

$$ \begin{align} x&===b_0+2b_1+4b_2+8b_3\\ 0&===b_0(b_0 – 1)\\ 0&===b_1(b_1 – 1)\\ 0&===b_2(b_2 – 1)\\ 0&===b_3(b_3 – 1) \end{align} $$

在这里,$b_0$ 是最低有效位,$b_3$ 是最高有效位。证明者必须提供 $b_0, b_1, b_2, b_3$,它们是 $x$ 的二进制位,以及 $x$ 本身。

在这种情况下,证明 $x$ 是一个四位数字变得更加繁琐,因为除了 $x$ 之外,我们还必须提供 $x$ 的二进制值,即使它们可以确定性且直接地导出。Circom 自动化了此过程,并允许我们编写代码以根据其他变量填充 witness 中的变量。例如,要填充二进制变量,我们可以编写以下 Circom 代码(以下代码缺少一些必要的安全功能 - 请勿盲目复制):

b_0 <-- x & 1;        // get the first bit of x via bitmask
b_1 <-- (x >> 1) & 1; // get the second bit of x
b_2 <-- (x >> 2) & 1; // get the third bit of x
b_3 <-- (x >> 3) & 1; // get the fourth bit of x

上面的代码生成了 witness,但没有创建公式中的约束:

$$ \begin{align} x&===b_0+2b_1+4b_2+8b_3\\ 0&===b_0(b_0 – 1)\\ 0&===b_1(b_1 – 1)\\ 0&===b_2(b_2 – 1)\\ 0&===b_3(b_3 – 1) \end{align} $$

上面的电路翻译成 Circom 将是(语法将在稍后解释):

template BinaryConstraint() {

  // assign the values to b_0,...,b_3
  x === b_0 + 2*b_1 + 4*b_2 + 8*b_3;
  0 === b_0*(b_0 - 1);
  0 === b_1*(b_1 - 1);
  0 === b_2*(b_2 - 1);
  0 === b_3*(b_3 - 1);
}

Circom 的一个主要便利之处在于,它的代码类似于算术电路中的数学,因此很容易将方程组转换为 Circom。

我们的想法是,无需向电路提供 $(x,b_0,b_1,b_2,b_3)$,我们只需提供 $x$。Circom 将为我们计算二进制值,然后用计算出的值填充约束。

除了自动化约束生成之外,Circom 还通过其“赋值和约束”运算符 \<==改进了填充 witness 的过程。

Circom 中 \<== 赋值和约束的优势

Circom 通过其“赋值和约束”运算符 \<==进一步简化了 witness 填充。假设我们有以下约束:

z === x * y

如果我们提供 xy 的值,那么还必须提供 z 的值会有点烦人,因为 z 只有一个可能的解。使用 Circom,我们按如下方式使用 \<==

z <== x * y

这样,变量 z 不再需要作为输入提供,因为 Circom 会为我们填充它,并且它的值将在电路的其余部分锁定为 $x\cdot y$。

因此,Circom 使用户免于显式提供 witness 中每个元素的值的麻烦,这是 Circom 便利性的主要卖点。

Circom 既是 DSL 又是编程语言

在 Circom 中编程时,最大的困惑是它既是一种编程语言(类似于 Javascript),又是一种编译为 R1CS 的 DSL。从这个意义上讲,它有点像 Solidity。Solidity 可以通过转移 Ether 来影响底层区块链状态,但它也可以像常规编程语言一样运行。Circom 的“编程语言”部分是为了帮助自动填充 witness,如前所述。但是,对于新手来说,并非总是清楚 Circom 的哪些部分会影响底层的 R1CS。

例如,以下是计算数字幂的有效 Circom 代码:

function power(base, exp) {
  return base ** exp;
}

template Power() {
  signal input base;
  signal input exp;
  signal output out;

  out <-- power(base, exp);
}

component main = Power();

/* INPUT = {
  "base": "3",
  "exp": "2"
} */

但是,上面的代码不会生成任何约束(因此它对于证明任何事情都没有用)。正如我们将在后面了解的那样,\<-- 运算符的唯一目的是生成 witness,而不是生成约束。

为什么要学习 Circom

作为最古老的 ZK 领域特定语言 (DSL) 之一,Circom 拥有最多的可用项目,你可以从中学习,并且经过了实战检验

我们认为,如果我们首先教授 Circom,那么学习更现代的 ZK DSL(例如 Halo2Plonky3)会容易得多,因此我们这样做。

为了了解原因,这里是用于计算 Halo2 中的斐波那契数列 的代码和用于计算 Plonky3 中的斐波那契数列 的代码。粗略地看一下这些示例应该会让读者相信这些 DSL 可能不是初学者的最佳起点。这是 Circom 代码,用于证明 out 是正确的第 n 个斐波那契数。相比之下,它更容易理解:

pragma circom 2.1.6;

// proves `out` is the nth
// fibonnaci number
template Fibonacci(n) {
  var offset = n + 1;
  assert(n > 2);

  signal fib[offset];
  signal output out;

  fib[0] <== 0;
  fib[1] <== 1;

  for (var i = 2; i < offset; i++) {
    fib[i] <== fib[i-1] + fib[i - 2];
  }

  out <== fib[n];
}

// 5th fibonnaci number is 5
// 0 1 1 2 3 5
component main = Fibonacci(5);

相比之下,对于初学者而言,Circom 具有相对简单的学习曲线,可以进入 ZK 开发。

Noir、Cairo 和 Leo 是否不抽象掉学习约束编写的需要?

你可以使用类似于 Rust 的语言(例如 Noir、Cairo 和 Leo)在 ZK 区块链或Layer2上编写智能合约,这些语言旨在从程序员那里“隐藏”约束生成。如果你的目标只是为这些区块链编写应用程序,则严格来说没有必要了解 ZK 约束在底层是如何工作的。

但是,请考虑一下,每个严肃的 Solidity 程序员都对以太坊虚拟机 (EVM) 的工作方式有很好的掌握,并且可以编写基本的汇编代码。了解幕后发生的事情将帮助你编写更高效的代码,而本资源就可以实现该目标。

此外,在这些执行环境中会出现许多错误,这是由于底层的 ZK 执行模型造成的。了解实际的私有内容、控制流可能存在的限制、使用字段时的常见错误,或者安全地使用 Noir 中的无约束函数或 o1js 中的自定义约束的能力,都需要一个低级的理解。

本系列的目标

尽管如此,高级 ZK 语言并没有使约束编写过时 - 事实上 - 它们增加了对真正了解它们的工作原理的专家的需求。本资源的目的是让更多的高级开发人员和安全审计员能够开发和保护这些高级 ZK 语言使用的底层区块链、虚拟机和编译器环境。

本资源的结构

本资源分为两个主要部分:

  1. 第一部分教授 Circom 的语法。具体来说,我们教授如何编写约束并对 Circom 进行编程,以便为我们填充大部分 witness 值。

  2. 本资源的第二部分教授如何为一般的 ZK 应用程序设计约束。我们将使用 Circom 作为示例,但该内容适用于其他 ZK DSL,例如 Halo2 或 Plonky3。

我们还将在整个内容中讨论 ZK 应用程序中的安全问题。

学习不仅来自学习,还来自实践

许多章节都包含明确的练习或一些未完成的代码,这些代码“留给读者练习”。如果你解决这些问题,你的学习之旅将会更加有效。我们设计这些问题是为了回顾你刚刚阅读的内容,以加强学习。如果你正确理解书面资源,则解决这些问题不需要任何特殊的“洞察力”或“聪明才智”。我们希望在阅读完材料后,最终的练习会感觉有些“显而易见”(如果不是,请提出问题或在练习的存储库中打开一个 pull request!)

安装 Circom

安装 Circom 的说明位于此处:<https://docs.circom.io/getting-started/installation/#installing-dependencies>

此处还有一个 Circom 的在线 IDE:<https://zkrepl.dev/>

附录:Circom 的 Plonk 与 Groth16

对于熟悉 Plonk 证明系统的读者,值得注意的是,我们为 Plonk 证明系统和 Groth16 证明系统编写了相同的电路。

Groth16 允许每个约束进行无限次加法运算,但只允许一次非恒定乘法(请注意,Rank 1 Constraint System 每行只有一个乘法)。相比之下,Plonk 每个约束只允许一次乘法或一次加法,而不能同时允许两者。当我们探索 Circom 时,每个约束一个乘法的限制将变得显而易见。

但是,与 Groth16 兼容的 Circom 电路也适用于 Plonk。snarkjs 库使用 Rank 1 Constraints Systems 作为输入,如果开发人员愿意,可以将其转换为 Plonk 约束系统。

因此,Circom 与预期的底层证明系统是 Groth16 还是 Plonk 无关。只要电路与 Groth16 兼容,它也可以与 Plonk 兼容,而无需开发人员进行任何其他更改。

作者和鸣谢

Calnix 撰写了本书的第一部分,并对本书的整体结构产生了重大影响。请 在 X 上关注 Calnix,并向他表示感谢。

我们感谢 VeridisePrivacy Scaling ExplorationszkSecurity 的 Marco Besier 和 Chainlight 对这项工作的有益评论。

  • 原文链接: rareskills.io/post/circo...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/