Circom 中的公开输入是 witness 中的一个信号,它将向验证者公开。
例如,假设我们要创建一个 ZK 证明,其声明为:“我们知道生成了 0x492c…9254 的哈希输入。”为了使这一声明有意义,值 0x492c…9254(目标哈希输出)必须是公开的。否则,我们在语义上只是在声明“我们哈希了某个东西”,这并没有什么用处。
下面的电路声明:“我将两个数字相乘并得到了第三个数字:”
template Main() {
signal input a;
signal input b;
signal input c;
a * b === c;
}
component main = Main();
下一个电路做出了类似的声明,但不同之处在于结果是公开的:“我将两个数字相乘并得到了第三个数字,且其值是公开已知的:”
template Main() {
signal input a;
signal input b;
signal input c;
a * b === c;
}
component main {public [c]} = Main();
- 默认情况下,所有输入信号都是私有的,除非使用
component main {public [c]}语法明确将其设为公开。main组件是我们定义哪些输入为公开的唯一位置。 - 列表
[c]是要设为公开的信号列表。如果我们也想让a公开,它可以包含更多信号,例如[a,c]。 - 只有输入信号可以被指定为公开,中间信号不可以。
上述模板编译后生成的 Rank-1 Constraint System (R1CS) 与以下内容完全相同,我们在 main 组件中引入了 output 关键字:
template Main() {
signal input a;
signal input b;
signal output c;
a * b ==> c;
}
component main = Main();
在上述两个模板中,c 是公开的,并被约束为 a 和 b 的乘积。因此,底层的 R1CS 是相同的。然而,第二个版本更“方便”,因为我们不需要显式提供 c。在第一个使用 component main {public [c]} 的电路中,如果我们提供的 c 不符合约束条件,将无法生成 witness。而在第二个将 c 作为输出的电路中,witness 生成器会自动计算出 c 的正确值,从而免去了手动输入的麻烦。
既然 c 完全由 a 和 b 决定,就没有理由显式为 c 提供一个值,因此首选 output 表示法。
请注意,输出是公开的。
对于输入而言,如果我们想让其中一些公开,这就意味着我们有一个信号的值不完全由其他信号的值决定。在这种情况下,我们必须使用 public 修饰符的方法。例如,如果我们声明“我将 a、b 和 c 相乘得到 d,其中 a 和 d 是公开的,但 b 和 c 是私有的”,我们将该电路结构化为:
template Main() {
signal input a; // explicitly public
signal input b;
signal input c;
signal output d; // implicitly public
signal s <== a * b; // intermediate signal
d <== c * s;
}
component main{public [a]} = Main();
以下是理解 output 信号的方法:
- 对于子组件,
output是一个将被其他输入赋值的信号,并可能在随后被实例化该子组件的组件使用。 - 对于
main组件,output是 witness 中的一个公开信号,其值应该完全由其他输入信号决定。声明一个 output 信号而不为其赋值可能会产生漏洞,因为证明者可以为其分配任何他们想要的值。我们将在后续章节中展示这种漏洞利用的机制。
尽管名字叫“output”,但 并没有机制可以从 main 组件获取“output” —— Circom 不能返回任何内容。其他代码库也无法读取“output”的值。
它只生成一个 R1CS,并帮助计算该 R1CS 的 witness。然后 Snarkjs 使用 Circom 代码生成一个 ZK 证明,用来证明 witness 满足 R1CS。
Circom 并不是在被“执行”,这就是为什么它不“返回”任何东西。你并不是在“运行” Circom,你仅仅是在描述一个抽象电路,该电路正被转化为两个部分:R1CS 和 witness 生成器,这两部分被分开使用。
main 组件中的 output 信号可以被看作是对验证者公开的中间信号。
Witness layout with public signals
Circom 按如下方式排列 witness 向量:
[constant, public signals, private signals]
让我们以“我将隐藏的值 a、b 与公开的值 c 相乘,得到一个公开的值 d”为例:
// assert that a*b === c*d
template Example() {
signal input a;
signal input b;
signal input c;
signal input d;
signal s;
s <== a * b;
d === s * c;
}
component main {public [c, d]} = Example();
请注意,我们可以通过将 d 设为 output 来节省一些代码,但在这里我们没有这样做,是为了让接下来的演示更清晰。
要查看 witness 的结构:
- 将上面的文件保存为
Example.circom - 使用
circom Example.circom --sym --r1cs --wasm编译它 - 创建
input.json:echo '{"a": "3", "b": "4", "c":"2", "d":"24"}' > input.json cd example_js- 计算 witness:
node generate_witness.js example.wasm ../input.json witness.wtns - 将 witness 转换为 json 并查看内容:
snarkjs wej witness.wtns && cat witness.json
我们应该会得到以下结果。请注意,这与我们为 input.json 提供的值相匹配:
[
"1", // constant
"2", // c (public signal)
"24", // d (public signal)
"3", // a
"4", // b
"12" // s
]
因此,我们可以看到 witness 的布局始终为:
- witness 中的 constant(常量)条目(始终为 1)
- 公开信号(
c,d) - 输入信号(
a,b) - 中间信号(
s)。
Summary
- 输入默认是私有的
- 我们可以使用
component main {public [in1, in2]} = Main();语法使输入公开 - 输出是公开信号
- 输出是根据其他输入为用户计算出的信号