解析 circom 编译过程中的.sym 和 substitutions.json 文件
circom 文档
https://docs.circom.io/circom-language/formats/sym/
https://docs.circom.io/circom-language/formats/constraints-json/
https://docs.circom.io/circom-language/formats/simplification-json/
example1
circuit code
pragma circom 2.0.0;
template Internal() {
signal input in[2];
signal output out;
out <== in[0]*in[1];
}
template Main1() {
signal input in[2];
signal output out;
component c = Internal ();
c.in[0] <== in[0];
c.in[1] <== in[1]+2*in[0]+1;
c.out ==> out;
}
component main = Main1();
compile
❯ circom simplify.circom --r1cs --wasm --simplification_substitution --O2 --sym --inspect
template instances: 2
non-linear constraints: 1
linear constraints: 0
public inputs: 0
private inputs: 2
public outputs: 1
wires: 4
labels: 7
Written successfully: ./simplify.r1cs
Written successfully: ./simplify.sym
Written successfully: ./simplify_js/simplify.wasm
Everything went okay
注意其中的–simplification_substitution –O2 参数
export r1cs.json
❯ snarkjs r1cs export json simplify.r1cs simplify.r1cs.json
[INFO] snarkJS: undefined: Loading constraints: 0/1
[INFO] snarkJS: undefined: Loading map: 0/4
simplify.sym
1,1,1,main.out
2,2,1,main.in[0]
3,3,1,main.in[1]
4,-1,0,main.c.out
5,-1,0,main.c.in[0]
6,-1,0,main.c.in[1]
参照https://docs.circom.io/circom-language/formats/sym/,我们可以得到如下表格
| #s | #w | #c | name |
|---|---|---|---|
| 1 | 1 | 1 | main.out |
| 2 | 2 | 1 | main.in[0] |
| 3 | 3 | 1 | main.in[1] |
| 4 | -1 | 0 | main.c.out |
| 5 | -1 | 0 | main.c.in[0] |
| 6 | -1 | 0 | main.c.in[1] |
-
#c 表示 signal 所属的 component
可以类比为其他编程语言里的变量所属的函数。
因为我们的 circom 代码里有两个 component, 所以可以看到这六个 signal 各有三个属于这两个 component。
-
#s 是 signal 的编号,从 1 开始
-
#w 是 signal 在 witness 数组中的 index
如果使用 –O0 指定不优化, 那么一般来说 #w 和 #s 的值是一样的
因为我们这个例子使用了 –O2, 所以我们可以看到 #s 为 4~6 的三个 signal 其实被优化了, 他们的#w 为-1
换句话说, 这个 circom 编译后的 witness 数组为[ 1, main.out, main.in[0], main.in[1] ]
r1cs
r1cs.json
{
"n8": 32,
"prime": "21888242871839275222246405745257275088548364400416034343698204186575808495617",
"nVars": 4,
"nOutputs": 1,
"nPubInputs": 0,
"nPrvInputs": 2,
"nLabels": 7,
"nConstraints": 1,
"useCustomGates": false,
"constraints": [
[
{
"2": "21888242871839275222246405745257275088548364400416034343698204186575808495616"
},
{
"0": "1",
"2": "2",
"3": "1"
},
{
"1": "21888242871839275222246405745257275088548364400416034343698204186575808495616"
}
]
],
"map": [0, 1, 2, 3],
"customGates": [],
"customGatesUses": []
}
w◯A * w◯B = w◯C
| witness | 1 | main.out | main.in[0] | main.in[1] |
|---|---|---|---|---|
| A | 0 | 0 | -1 | 0 |
| B | 1 | 0 | 2 | 1 |
| C | 0 | -1 | 0 | 0 |
- w◯A: -main.in[0]
- w◯B: 1 + 2*main.in[0] + main.in[1]
-
w◯C: -main.c.out
-
w◯A * w◯B = w◯C
(-main.in[0]) * (1 + 2*main.in[0] + main.in[1]) = -main.c.out
main.c.out = main.in[0] * (1 + 2*main.in[0] + main.in[1])
与 circom 中的语义一致
simplify_substitutions.json
{
"substitution": {
"5": { "2": "1" },
"4": { "1": "1" },
"6": { "0": "1", "2": "2", "3": "1" }
}
}
-
“5”: { “2”: “1” }
5 代表.sym 中的 main.c.in[0] (5,-1,0,main.c.in[0]) 2 代表.sym 中的 main.in[0] (2,2,1,main.in[0]) 1 代表 main.in[0]的系数, 即 main.in[0] * 1
“5”: { “2”: “1” }对应的含义是, main.c.in[0]在编译优化过程中被替换为了 main.in[0] * 1
-
“4”: { “1”: “1” }
4 代表.sym 中的 main.c.out (4,-1,0,main.c.out) “1”: “1”的第一个 1 代表 main.out (1,1,1,main.out) “1”: “1”的第二个 1 代表 main.out 的系数, 即 main.out * 1
“4”: { “1”: “1” }对应的含义是, main.c.out 在编译优化过程中被替换为了 main.out * 1
-
“6”: { “0”: “1”, “2”: “2”, “3”: “1” }
6 代表.sym 中的 main.c.in[1] (6,-1,0,main.c.in[1]) “0”: “1” 代表 1 * 1 (下标为 0 的 witness 是 1) “2”: “2” 代表 main.in[0] * 2 (下标为 2 的 witness 是 main.in[0]: 2,2,1,main.in[0]) “3”: “1” 代表 main.in[1] * 1 (下标为 3 的 witness 是 main.in[1]: 3,3,1,main.in[1])
“6”: { “0”: “1”, “2”: “2”, “3”: “1” }对应的含义是, main.c.in[1] 在编译优化过程中被替换为了 1 * 1 + main.in[0] * 2 + main.in[1] * 1
总结一下
| 原 Signal | 替换后的表达式 |
|---|---|
| main.c.out | main.out |
| main.c.in[0] | main.in[0] |
| main.c.in[1] | 1 + 2 * main.in[0] + main.in[1] |
这和 circom 中的语义以及优化后的 r1cs.json 可以对应上
example2
circuit code
pragma circom 2.0.0;
template calc () {
// Declaration of signals.
signal T2;
signal T1;
signal z;
signal output y;
signal input x;
//y = x ** 3 + 2x + 5
// Constraints.
T1 <== x * x;
z <== T1 * x;
T2 <== z + 2 * x;
y <== T2 + 5;
}
component main = calc();
compile
❯ circom circuit.circom --r1cs --wasm --simplification_substitution --O2 --sym --inspect
template instances: 1
non-linear constraints: 2
linear constraints: 0
public inputs: 0
private inputs: 1
public outputs: 1
wires: 4
labels: 6
Written successfully: ./circuit.r1cs
Written successfully: ./circuit.sym
Written successfully: ./circuit_js/circuit.wasm
Everything went okay
export r1cs.json
❯ snarkjs r1cs export json circuit.r1cs circuit.r1cs.json
[INFO] snarkJS: undefined: Loading constraints: 0/2
[INFO] snarkJS: undefined: Loading map: 0/4
circuit.sym
1,1,0,main.y
2,2,0,main.x
3,-1,0,main.T2
4,3,0,main.T1
5,-1,0,main.z
参照https://docs.circom.io/circom-language/formats/sym/,我们可以得到如下表格
| #s | #w | #c | name |
|---|---|---|---|
| 1 | 1 | 0 | y |
| 2 | 2 | 0 | x |
| 3 | -1 | 0 | T2 |
| 4 | 3 | 0 | T1 |
| 5 | -1 | 0 | z |
编译后的 witness 数组为[ 1, y, x, T1 ]
r1cs
r1cs.json
{
"n8": 32,
"prime": "21888242871839275222246405745257275088548364400416034343698204186575808495617",
"nVars": 4,
"nOutputs": 1,
"nPubInputs": 0,
"nPrvInputs": 1,
"nLabels": 6,
"nConstraints": 2,
"useCustomGates": false,
"constraints": [
[
{
"2": "21888242871839275222246405745257275088548364400416034343698204186575808495616"
},
{
"2": "1"
},
{
"3": "21888242871839275222246405745257275088548364400416034343698204186575808495616"
}
],
[
{
"3": "21888242871839275222246405745257275088548364400416034343698204186575808495616"
},
{
"2": "1"
},
{
"0": "5",
"1": "21888242871839275222246405745257275088548364400416034343698204186575808495616",
"2": "2"
}
]
],
"map": [0, 1, 2, 4],
"customGates": [],
"customGatesUses": []
}
w◯A * w◯B = w◯C
| witness | 1 | y | x | T1 |
|---|---|---|---|---|
| A | 0 | 0 | -1 | 0 |
| B | 3 | 0 | 1 | 0 |
| C | 0 | 0 | 0 | -1 |
w◯A 对应的表达式为 -x
w◯B 对应的表达式为 x
w◯C 对应的表达式为 -T1
w◯A * w◯B = w◯C 也就是 -x * x = -T1
这对应着 circom 中的T1 <== x * x;
| witness | 1 | y | x | T1 |
|---|---|---|---|---|
| A | 0 | 0 | 0 | -1 |
| B | 3 | 0 | 1 | 0 |
| C | 5 | -1 | 2 | 0 |
w◯A 对应的表达式为 -T1
w◯B 对应的表达式为 x
w◯C 对应的表达式为 5 - y + 2x
w◯A * w◯B = w◯C 也就是 -T1 * x = 5 - y + 2x
等式两边移项
y = T1 * x + 2x + 5
对应着 circom 中如下代码优化后的结果
z <== T1 * x;
T2 <== z + 2 * x;
y <== T2 + 5;
simplify_substitutions.json
{
"substitution": {
"5": {
"0": "21888242871839275222246405745257275088548364400416034343698204186575808495612",
"1": "1",
"2": "21888242871839275222246405745257275088548364400416034343698204186575808495615"
},
"3": {
"0": "21888242871839275222246405745257275088548364400416034343698204186575808495612",
"1": "1"
}
}
}
因为 r1cs 中 prime 值如下
"prime": "21888242871839275222246405745257275088548364400416034343698204186575808495617"
可以将 json 理解为如下内容
{
"substitution": {
//z: (5,-1,0,main.z)
"5": {
"0": "-5", // -5 * 1
"1": "1", //1 * y (1,1,0,main.y)
"2": "-2" //-2 * x (2,2,0,main.x)
},
//T2: (3,-1,0,main.T2)
"3": {
"0": "-5", // -5 * 1
"1": "1" //1 * y (1,1,0,main.y)
}
}
}
-
“5”: { “0”: “-5”, “1”: “1”, “2”: “-2” } z 被优化为 -5 + y - 2x 这与 circom 中的如下代码语义一致
T2 <== z + 2 * x; y <== T2 + 5;代入消除 T2 后
y = z + 2x + 5移项后
z = -5 + y - 2x -
“3”: { “0”: “-5”, “1”: “1”} T2 被优化为 -5 + y 这与 circom 中的如下代码语义一致
y <== T2 + 5;
example3
circuit code
pragma circom 2.0.0;
template LinearAdder() {
signal input a;
signal input b;
signal output sum;
signal temp;
temp <== a + b;
sum <== temp * 2;
}
component main{public[a]} = LinearAdder();
compile
❯ circom LinearAdder.circom --r1cs --wasm --simplification_substitution --O2 --sym --inspect
template instances: 1
non-linear constraints: 0
linear constraints: 0
public inputs: 1
private inputs: 1 (none belong to witness)
public outputs: 1
wires: 3
labels: 5
Written successfully: ./LinearAdder.r1cs
Written successfully: ./LinearAdder.sym
Written successfully: ./LinearAdder_js/LinearAdder.wasm
Everything went okay
❯ snarkjs r1cs export json LinearAdder.r1cs LinearAdder.r1cs.json
[INFO] snarkJS: undefined: Loading map: 0/3
从结果看, 约束数量为 0
LinearAdder.sym
1,1,0,main.sum
2,2,0,main.a
3,-1,0,main.b
4,-1,0,main.temp
编译后的 witness 数组为[1, sum, a]
r1cs
r1cs.json
{
"n8": 32,
"prime": "21888242871839275222246405745257275088548364400416034343698204186575808495617",
"nVars": 3,
"nOutputs": 1,
"nPubInputs": 1,
"nPrvInputs": 1,
"nLabels": 5,
"nConstraints": 0,
"useCustomGates": false,
"constraints": [],
"map": [0, 1, 2],
"customGates": [],
"customGatesUses": []
}
同样没有约束
simplify_substitutions.json
{
"substitution": {
"3": {
"0": "0",
"1": "10944121435919637611123202872628637544274182200208017171849102093287904247809",
"2": "21888242871839275222246405745257275088548364400416034343698204186575808495616"
},
"4": {
"0": "0",
"1": "10944121435919637611123202872628637544274182200208017171849102093287904247809",
"2": "0"
}
}
}
因为 r1cs 中 prime 值如下
"prime": "21888242871839275222246405745257275088548364400416034343698204186575808495617"
所以21888242871839275222246405745257275088548364400416034343698204186575808495616可以理解为-1
10944121435919637611123202872628637544274182200208017171849102093287904247809可以理解为0.5
可以将 json 理解为如下内容
{
"substitution": {
// b: (3,-1,0,main.b)
"3": {
"0": "0", // 0 * 1
"1": "0.5", // 0.5 * sum (1,1,0,main.sum)
"2": "-1" //-1 * a (2,2,0,main.a)
},
// temp: (4,-1,0,main.temp)
"4": {
"0": "0", // 0 * 1
"1": "0.5", // 0.5 * sum (1,1,0,main.sum)
"2": "0" //0 * a (2,2,0,main.a)
}
}
}
这里其实已经跟https://docs.circom.io/circom-language/formats/simplification-json/中的where the linear expression is represented by a dictionary with the signal numbers as strings occurring in the linear expression (with non-zero coefficient) as entries and their coefficients (as string) as values: { "sig_num_l1": "coef_1", ... , "sig_num_lm": "coef_m"} 有冲突了
"0": "0" 的第二个”0”已经不是 with non-zero coefficient 了。
-
“3”: { “0”: “0”, “1”: “0.5”, “2”: “-1” }
b被优化为0.5 * sum - a转换后可以得到
sum = 2 * (a + b), 这跟 circom 的语义一致 -
“4”: { “0”: “0”, “1”: “0.5”, “2”: “0” }
temp被优化为0.5 * sum这与 circom 中的sum <== temp * 2;语义一致