由于已有些许基于circom circuit的zkp开发经验,所以本文就围绕circom开发来做一个入门级report。首先,对circom的特点、开发流程做了一个简要介绍;而后,分享了一些常用的circom库及用例;最后,以Tornado为例做了一个应用案例分析。
Circom是一个底层用rust实现的编译器,它可以编译用circom语言实现的circuit。它将circuit编译的结果以contraints的形式输出,这些constraints能被用于计算相应生成逻辑的proof。
上图的circuit在最后一行 s3 <= 1 - s1 * s2 设置了constraint,并将 s3 输出为计算结果。需要注意的是,circom的input默认为private,output默认为public,同时,这些信号的可视性都可以通过显式定义进行修改。
如上图,通过对单个电路的组合,可以实现功能各异的复杂电路,并且在工程上也能通过调用circom库避免重复造轮子。
配置开发环境
开发和编译circuit
(1) --r1cs生成二进制的constraint文件; --wasm生成用于产生witness的文件.
circom xxx.circom --r1cs --wasm --sym --c
(2) 生成描述circuit描述
snarkjs r1cs info xxx.r1cs
js生成witness (以groth16为例), 用于链下测试circuit运算正确性
const circuit = await wasm_tester("xx.circom");
const INPUT = {
"xx": xx,
"xx": xx
}
const witness = await circuit.calculateWitness(INPUT, true);
生成proof
(1) 下载trusted setup phase1 参数
wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_10.ptau
(2) line1: 导入trusted setup phase 1参数
(3) line2: 进行phase2 trusted setup
snarkjs groth16 setup xxx.r1cs powersOfTau28_hez_final_10.ptau circuit_0000.zkey
snarkjs zkey contribute circuit_0000.zkey circuit_final.zkey --name="1st Contributor Name" -v -e="random text"
snarkjs zkey export verificationkey circuit_final.zkey verification_key.json
(4) 编译输出solidity verifier脚本
snarkjs zkey export solidityverifier circuit_final.zkey Verifier.sol
(5) js脚本生成proof,public signal
const { proof, publicSignals } = await groth16.fullProve(Input{xxx}, "xxx.wasm","circuit_final.zkey");
链下验证proof
snarkjs groth16 prove xxx.zkey witness.wtns proof.json public.json
链上验证proof
(1) 生成solidity calldata
const calldata = await groth16.exportSolidityCallData(proof, publicSignals);
包括加密原语的Circom实现、条件语法实现:
矩阵计算实现:
zkSnark的js和Web Assembly实现:
Tornado cash的core版本只支持少数可选面值的匿名转账;在升级到nova版本后,由于通过UTXO的账本数据结构进行记账,在功能上实现突破,目前可以实现自定义任意面值的匿名转账。此外,有很重要的一个技术点:不论是core版本还是nova版本都使用了Merkle Tree Root数组链上存证校验。
Tornado-core的账本数据结构较为简单,使用包含nullifier和secret信息(这两者都在链下随机生成,并由用户保管)的commitment作为转账凭证,通过验证nullifier状态避免double spending。链上链下的数据一致性则通过MerkleTree Root数组链上存证的方式进行校验。
function generateDeposit() {
let deposit = {
secret: rbigint(31),
nullifier: rbigint(31),
}
const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(preimage)
return deposit
}
// verify nullifier
component hasher = CommitmentHasher();
hasher.nullifier <== nullifier;
hasher.secret <== secret;
hasher.nullifierHash === nullifierHash;
// verify commitment based on hash(secret, nullifier), so which commitment is used for withdrawal is not revealed
component tree = MerkleTreeChecker(levels);
tree.leaf <== hasher.commitment;
tree.root <== root;
for (var i = 0; i < levels; i++) {
tree.pathElements[i] <== pathElements[i];
tree.pathIndices[i] <== pathIndices[i];
}
由于core版本的记账数据结构不能对金额进行拆分,如果不使用固定面值,则会由于面值信息将存款人和提款人的信息联系上,从而使匿名转账在实作中失效。
相较于core版本,nova版本在设计理念上的创新,主要在于借鉴了Bitcoin的UTXO账本数据结构。它将每一笔转账以UTXO链式转移的方式实现,这样就使得转账金额可拆分,即时存款人和提款人将任意的存款金额信息公开,也可以通过拆分金额交易的方式实现匿名转账。(由于nova版本代码较长,在此就不贴出)
UTXO的数据结构包含:金额、随机数salt、index;生成上链存证MerkleTree Root的叶子节点为UTXO的commitment(即金额、随机数salt以及公钥的poseidon哈希);nullifier为commitment、index以及私钥的poseidon哈希。
TX中未解锁的outputs(即在使用时作为下次交易的inputs)需要使用仅由用户生成并持有的keypair进行解锁。
校验commitment、nullifier以及与MerkleTree Root的一致性。