今天的练习是动手编码练习。 尝试自己编写以下电路。 如果遇到困难,可以查看解决方案。
如果您需要查找 circom 语言功能或语法,请查看 circom 文档。 我建议尝试在 zkREPL 中构建这些电路,以实现快速迭代。
我建议按顺序进行这些练习,因为后面的电路可能建立在前面的电路之上。
circom 中的所有信号都是素数域中的域元素
r = 21888242871839275222246405745257275088548364400416034343698204186575808495617
这是一个 254 位素数,称为 BabyJubJub 素数。 这是 BN254 的曲线顺序,这是以太坊和(以前的)ZCash 使用的配对友好椭圆曲线。 您可以在 Jonathan Wang 的优秀文档 此处 中阅读更多关于 BN254 的信息。
如果您正在使用 zkREPL 的输入注释功能测试您的电路(特别是如果您使用负数),请注意您需要将数字括在引号中,并且您需要编写 它们作为非负残基。 这是由于 JSON 无法解析大整数。
例如,测试值为-1
的输入信号x
应如下所示:
"x": "21888242871839275222246405745257275088548364400416034343698204186575808495616"
忘记打引号是测试失败的常见原因。
- 参数:
nBits
- 输入信号:
in
- 输出信号:
b[nBits]
输出信号应该是长度为nBits
的位数组,相当于in
的二进制表示。 b[0]
是最低有效位。
- 参数:无
- 输入信号:
in
- 输出信号:
out
要求:如果in
为零,out
应为1
。 如果in
不为零,out
应为0
。 这个有点棘手!
答:这个课上讲过,思路是:
代码见IsZero.circom。关于inv
的提前计算,如果用if判断:
signal input in;
signal inv;
if (in == 0) {
inv <-- 0;
} else {
inv <-- 1 / in;
}
这里会报错error[T3001]: Exception caused by invalid assignment: signal already assigned,因为所有signal都是Unknown的,包括in
,编译时会看到inv
有两个赋值语句,而signal
只能赋值一次,因此会出现编译错误。inv
直接用条件表达式进行赋值:
signal input in;
signal inv <-- in == 0 ? 0 : 1/in;
- 参数:无
- 输入信号:
in[2]
- 输出信号:
out
要求:如果 in[0]
等于 in[1]
,则 out
应为 1
。 否则,out
应该是 0
。
答:判断两个数是否相等就是看这两个数的差是否为0,也就是用in[0] - in[1]
作为IsZero的输入信号。代码见IsEqual.circom. 简洁点直接判断in[0] - in[1]
是否为零,代码见IsEqual2.circom.
- 参数:
nChoices
- 输入信号:
in[nChoices]
,index
- 输出:
out
要求:输出out
应该等于in[index]
。 如果 index
越界(不在 [0, nChoices) 中),out
应该是 0
。
答:参考解决方案,代码见Selector.circom. 思路是循环遍历下标,用i和index是否相等作选择器,如果i和index相等,选择器置1,不等置0。类似于下面这种思路:
最后再做累加。在解决方案中先要求index必须在范围[0,nChoices)。
// Ensure that index < choices
component lessThan = LessThan(4);
lessThan.in[0] <== index;
lessThan.in[1] <== choices;
lessThan.out === 1;
但是本题中允许index越界,如果index越界,约束lessThan.out === 1;
就不会满足。可以不加这个约束,如果index越界,就不会和遍历的每个下标相等,每次累加的都是0,最终累加结果也为0,将累加结果赋给out
,输出0。
注意:信号是模 p(Babyjubjub 素数)的残基,并且没有负
数模 p 的自然概念。 但是,很明显,当我们将p-1
视为-1
时,模运算类似于整数运算。
所以我们定义一个约定:取负
按照惯例认为是 (p/2, p-1] 中的余数,非负是 [0, p/2) 中的任意数
- 参数:无
- 输入信号:
in
- 输出信号:
out
要求:如果根据我们的约定,in
为负数,则 out
应为 1
。 否则,out
应该是 0
。 您可以自由使用CompConstant circuit,它有一个常量参数ct
,如果in
(二进制数组)在解释为整数时严格大于 ct
则输出 1
,否则为 0
。
- 理解检查:为什么我们不能只使用 LessThan 或上一个练习中的比较器电路之一?
答:代码见IsNegative.circom.
template IsNegative() {
signal input in;
signal output out;
var p = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
// (p - 1) \ 2 = 10944121435919637611123202872628637544274182200208017171849102093287904247808
component num2bits = Num2Bits(254);
component compconstant = CompConstant((p - 1) \ 2);
num2bits.in <== in;
compconstant.in <== num2bits.out;
out <== compconstant.out;
}
这里的component compconstant = CompConstant((p - 1) \ 2);
中ComConstant参数是计算出的结果,需要注意这里应该输入(p - 1) \ 2
。如果是p \ 2
,由于在Circom中运算都会进行模p操作,那么p mod p = 0
,其实得到的结果为0。而p
是素数,因此(p - 1) \ 2
计算的就是p/2的值。也可以像解决方案中那样直接将这个值作为ComConstant的参数。
component comp = CompConstant(10944121435919637611123202872628637544274182200208017171849102093287904247808);
如果使用LessThan来解决这个问题,思路是类似这种:
这里就是y = in - p/2 + 2^253
,y二进制化取最高位,如果为1就表示in
负数,如果为1就表示in
是非负数。但是在实际运算中,LessThan中的代码:
n2b.in <== in[0]+ (1<<n) - in[1];
实际上要进行模p运算再赋值(Circom官方文档说明了这点),真实计算的是y = in - p/2 + 2^253 mod p
。如果in
在[0, p/2),y = in - p/2 + 2^253 mod p = 0
。但如果in
在(p/2, p-1],就不一定为1了。因为in - p/2 + 2^253
可能会超过p
,模p之后二进制的最高位就不再保持为1,最后会得到结果0。
因此在与非常大的数进行比较时,尤其是超过p/2的数,应该逐位比较,小心使用直接加减运算,因为在电路中暗含了模p操作。
- 参数:无
- 输入信号:
in[2]
。 假设提前知道这些最多$2^{252} - 1$ 。 - 输出信号:
out
要求:如果 in[0]
严格小于 in[1]
,则 out
应为 1
。 否则,out
应该是 0
。
-
扩展 1:如果您知道输入信号最多为
$2^k - 1 (k ≤ 252)$ ,您如何减少该电路所需的约束总数? 编写一个在k
中参数化的电路版本。 - 扩展 2:编写 LessEqThan(测试 in[0] 是否 ≤ in[1])、GreaterThan 和 GreaterEqThan
答:小于代码见LessThan.circom. 扩展2代码见Compare.circom.
小于等于或者大于等于就用out <== n2b.out[n];
,因为等于情况下为0,应该直接输出1,也就是2^n的二进制表达最高位。小于或者大于用out <== 1 - n2b.out[n];
,等于时反转一下,输出0。n2b.in <== in[0] - in[1] + (2**n);
就看是大于还是小于了。
注意:这个电路非常难!
- 参数:
nbits
。 使用assert
断言这最多为 126! - 输入信号:
dividend
,divisor
(被除数,除数) - 输出信号:
remainder
,quotient
(余数,商)
要求:首先,检查dividend
和divisor
是否最多为nbits
位长。 接下来,计算并约束余数
和商
。
- 扩展:您将如何修改电路以处理负的被除数?
解决方案(忽略第二个参数SQRT_P,这是无关紧要的)
答:除数和被除数均为正数,代码见IntegerDivide.circom. 思路是
(a) 先限制dividend
和divisor
最多为nbits
位.
(b) 限制除数divisor
不能为0.
(c) 计算 商 <-- 被除数 \ 除数
,计算 余数 <== 被除数 - 商 * 除数
。
(d) 检查计算得到的余数范围是否在[0, 除数)
之间。
(e) 约束计算 被除数 === 除数 * 商 + 余数
正确。
- 参数:
N
- 输入信号:
in[N]
- 输出信号:
out[N]
要求:将输入in[N]
的N
个数字按照从小到大进行排列,并输出到out[N]
信号中。