1. 引言
前序博客:
开源代码见:
- https://github.com/nalinbhardwaj/Nova-Scotia(JavaScript+Rust)
Nova-Scotia定位为中间件,本质为:
- 1)读取以Circom编写的电路文件,将Circom电路解析为
CircomCircuit
结构体。 - 2)基于
CircomCircuit
结构体,实现Nova的StepCircuit trait中的arity()、synthesize()、output()函数。
从而打通了将Circom生态接入Nova证明系统的通道。
- 可将Nova可成是具有大量重复结构zkSNARK的预处理器。
- Nova可将 检查某problem的 N N N个instance 收缩为 检查同一problem的约1个instance,从而压缩了R1CS约束数量,进而降低了开销。
- 以Nova的输出(Nova proof) 作为 某”real“ zkSNARK(如PLONK/Groth16/Spartan)的输入,从而可实现最小化的proof size(即可与单个instance size呈sublinear关系)。类似的思路 也在 Polygon zkEVM 中使用了,只不过其中fast prover生成的为STARK proof,而不是Nova proof:
不过与STARK相比,Nova(特别是其folding scheme),具有更多作为预处理层的特性:
- 快速压缩
- 最小的密码学假设
- 低递归开销
2. 使用Circom语言来编写Nova Step Circuit
不同于https://github.com/microsoft/Nova中使用Rust语言来编写step circuit,https://github.com/nalinbhardwaj/Nova-Scotia 支持以Circom来编写step电路。
在Nova-Scotia接口中:
- 以
step_in
名来对应一组公开输入。 - 以
step_out
名来对应一组公开输出,与step_in
具有相同的数量。 step_out
会作为下一次递归的step_in
,直到递归结束。- Circom电路可以有额外的隐私输入(可 以Circom能接受的 任意名称或任意结构表示)。
Circom支持2种Witness generator:
- C++ Witness generator:见
circom ./examples/toy/toy.circom --r1cs --wasm --sym --c --output ./examples/toy/ --prime vesta
中的关键字--c
。需额外借助nlohmann-json做make操作。 - Wasm Witness generator:见
circom ./examples/toy/toy.circom --r1cs --wasm --sym --c --output ./examples/toy/ --prime vesta
中的关键字--wasm
。
2.1 toy示例
https://github.com/nalinbhardwaj/Nova-Scotia/blob/main/examples/toy/toy.circom中的toy circom电路为:
pragma circom 2.0.3;
// include "https://github.com/0xPARC/circom-secp256k1/blob/master/circuits/bigint.circom";
template Example () {
signal input step_in[2];
signal output step_out[2];
signal input adder;
step_out[0] <== step_in[0] + adder;
step_out[1] <== step_in[0] + step_in[1];
}
component main { public [step_in] } = Example();
/* INPUT = {
"step_in": [1, 1],
"step_out": [1, 2],
"adder": 0
} */
其中:
- step_in[2]:为public input公开输入。
- step_out[2]:为public output公开输出。【Circom语法中,
signal output
表示公开输出】 - adder:为private witness隐私输入。
支持2种格式的witness generator,分别见:
- toy.rs:对应C++ Witness generator。
- toy_wasm.rs:对应Wasm Witness generator。
Wasm与C++方式对比为:
- 1)Nova证明:
- Wasm方式生成Nova proof用时略长一点,但验证Nova proof速度要快很多。
- 2)压缩Nova证明
- 对Wasm方式生成的Nova proof,使用Spartan + IPA-PC压缩用时更短,验证时长也更短。
2.1.1 C++ Witness generator版本toy示例
fn main() {
let iteration_count = 5; //迭代次数
let root = current_dir().unwrap();
let circuit_file = root.join("examples/toy/toy.r1cs");
let r1cs = load_r1cs(&FileLocation::PathBuf(circuit_file)); //读取Circom编译的R1CS电路文件
let witness_generator_file = root.join("examples/toy/toy_cpp/toy"); //指定C++ witness generator
let mut private_inputs = Vec::new();
for i in 0..iteration_count { //为每次迭代时的private witness赋值
let mut private_input = HashMap::new();
private_input.insert("adder".to_string(), json!(i));
private_inputs.push(private_input);
}
let start_public_input = vec![F1::from(10), F1::from(10)]; //初始public input
// 构建Nova中所需的circuit_primary(基于r1cs)和 circuit_secondary(为TrivialTestCircuit),并据此setup public params
let pp = create_public_params(r1cs.clone());
// 分别打印circuit_primary和circuit_secondary中的约束数。
println!(
"Number of constraints per step (primary circuit): {}",
pp.num_constraints().0
);
println!(
"Number of constraints per step (secondary circuit): {}",
pp.num_constraints().1
);
// 分别打印circuit_primary和circuit_secondary中的变量数。
println!(
"Number of variables per step (primary circuit): {}",
pp.num_variables().0
);
println!(
"Number of variables per step (secondary circuit): {}",
pp.num_variables().1
);
println!("Creating a RecursiveSNARK...");
let start = Instant::now();
/*
创建Nova proof。
*/
let recursive_snark = create_recursive_circuit(
FileLocation::PathBuf(witness_generator_file),
r1cs,
private_inputs,
start_public_input.clone(),
&pp,
)
.unwrap();
println!("RecursiveSNARK creation took {:?}", start.elapsed());
// TODO: empty?
let z0_secondary = vec![<G2 as Group>::Scalar::zero()];
// 验证Nova proof
// verify the recursive SNARK
println!("Verifying a RecursiveSNARK...");
let start = Instant::now();
let res = recursive_snark.verify(
&pp,
iteration_count,
start_public_input.clone(),
z0_secondary.clone(),
);
println!(
"RecursiveSNARK::verify: {:?}, took {:?}",
res,
start.elapsed()
);
assert!(res.is_ok());
// produce a compressed SNARK。采用Spartan+IPA-PC压缩Nova proof
println!("Generating a CompressedSNARK using Spartan with IPA-PC...");
let start = Instant::now();
let (pk, vk) = CompressedSNARK::<_, _, _, _, S1, S2>::setup(&pp).unwrap();
let res = CompressedSNARK::<_, _, _, _, S1, S2>::prove(&pp, &pk, &recursive_snark);
println!(
"CompressedSNARK::prove: {:?}, took {:?}",
res.is_ok(),
start.elapsed()
);
assert!(res.is_ok());
let compressed_snark = res.unwrap();
// verify the compressed SNARK。验证压缩证明
println!("Verifying a CompressedSNARK...");
let start = Instant::now();
let res = compressed_snark.verify(
&vk,
iteration_count,
start_public_input.clone(),
z0_secondary,
);
println!(
"CompressedSNARK::verify: {:?}, took {:?}",
res.is_ok(),
start.elapsed()
);
assert!(res.is_ok());
}
其中,create_recursive_circuit()
用于创建Nova proof。基本思路为:
- 1)将初始public input转换为十六进制string表示。
- 2)设置circuit_secondary的z0_secondary为[0]。
- 3)初始化recursive_snark为None。
- 4)每次迭代时:
- 4.1)将初始public input转换为十进制string表示(decimal_stringified_input)
- 4.2)基于公开输入和隐私输入构建CircomInput:
将CircomInput转换为json表示。let input = CircomInput { step_in: decimal_stringified_input.clone(), extra: private_inputs[i].clone(), };
- 4.3)取C++或Wasm witness generator,分别调用
generate_witness_from_bin()
或generate_witness_from_wasm()
生成witness数组。 - 4.4)基于r1cs和witness构建CircomCircuit circuit(作为后续
prove_step()
的c_primary):【并为CircomCircuit实现了调用https://github.com/microsoft/Nova所需的StepCircuit
trait。】let circuit = CircomCircuit { r1cs: r1cs.clone(), witness: Some(witness), };
- 4.5)从CircomCircuit circuit中
get_public_outputs()
读取当前迭代的public output current_public_output。【此处假设电路中public input和public output数量相等】将当前迭代的current_public_output转换为十六进制strIng表示,并作为下一次迭代的current_public_input。 - 4.6)调用
RecursiveSNARK::prove_step()
,将其结果作为recursive_snark,供下一次迭代使用。
- 5)迭代结束后,返回最终的recursive_snark作为Nova proof。
运行输出为:
# cargo run --example toy
Number of constraints per step (primary circuit): 9819
Number of constraints per step (secondary circuit): 10347
Number of variables per step (primary circuit): 9814
Number of variables per step (secondary circuit): 10329
Creating a RecursiveSNARK...
RecursiveSNARK creation took 6.195609361s
Verifying a RecursiveSNARK...
RecursiveSNARK::verify: Ok(([0x0000000000000000000000000000000000000000000000000000000000000014, 0x0000000000000000000000000000000000000000000000000000000000000046], [0x0000000000000000000000000000000000000000000000000000000000000000])), took 138.825847ms
Generating a CompressedSNARK using Spartan with IPA-PC...
CompressedSNARK::prove: true, took 55.66993169s
Verifying a CompressedSNARK...
CompressedSNARK::verify: true, took 1.044083085s
2.1.2 Wasm Witness generator版本toy示例
fn main() {
let iteration_count = 5;
let root = current_dir().unwrap();
let circuit_file = root.join("examples/toy/toy.r1cs");
let r1cs = load_r1cs(&FileLocation::PathBuf(circuit_file));
let witness_generator_wasm = root.join("examples/toy/toy_js/toy.wasm"); //使用Wasm witness generator
// ........
}
运行输出为:
# cargo run --example toy_wasm
Number of constraints per step (primary circuit): 9819
Number of constraints per step (secondary circuit): 10347
Number of variables per step (primary circuit): 9814
Number of variables per step (secondary circuit): 10329
Creating a RecursiveSNARK...
RecursiveSNARK creation took 7.779853963s
Verifying a RecursiveSNARK...
RecursiveSNARK::verify: Ok(([0x0000000000000000000000000000000000000000000000000000000000000014, 0x0000000000000000000000000000000000000000000000000000000000000046], [0x0000000000000000000000000000000000000000000000000000000000000000])), took 59.05424ms
Generating a CompressedSNARK using Spartan with IPA-PC...
CompressedSNARK::prove: true, took 33.562620125s
Verifying a CompressedSNARK...
CompressedSNARK::verify: true, took 134.254919ms
2.2 bitcoin示例
bitcoin示例本质为借鉴https://github.com/dcposch/btcmirror/tree/master实现了Bitcoin light client验证逻辑。背景资料可参看:
Bitcoin的区块头共有80个字节:【所有哈希值按内部字节顺序表示,其它值以little-endian表示。】
字节数 | 字段名 | 数据类型 | 描述 |
---|---|---|---|
4 | version | int32_t | 区块版本号表示了区块验证所应遵循的规则集。 |
32 | previous block header hash | char[32] | 为SHA256(SHA256(前一区块头))的字节表示。从而可确保若不修改当前区块头,则无法修改前一区块。 |
4 | merkle root hash | char[32] | 为当前区块内所有交易哈希所构建的merkle tree root。子节点哈希运算为H()=SHA256(SHA256())。 |
4 | time | uint32_t | 为矿工开始对区块进行哈希的Unix epoch时间。需严格大于前11个区块的平均时间。 |
4 | nBits | uint32_t | 为SHA256(SHA256(当前区块头))必须小于等于的阈值的编码表示。 |
4 | nonce | uint32_t | 为矿工可修改的任意值,使得SHA256(SHA256(当前区块头))小于等于 nBits所表示的目标阈值。若所有32-bit nonce值试过都不行,则可以更新time,或者修改coinbase交易并更新merkle root hash。 |
一个比特币区块头示例为:
02000000 ........................... Block version: 2
b6ff0b1b1680a2862a30ca44d346d9e8
910d334beb48ca0c0000000000000000 ... Hash of previous block's header
9d10aa52ee949386ca9385695f04ede2
70dda20810decd12bc9b048aaab31471 ... Merkle root
24d95a54 ........................... [Unix time][unix epoch time]: 1415239972
30c31b18 ........................... Target: 0x1bc330 * 256**(0x18-3)
fe9f0864 ........................... Nonce
迭代时:
- 1)初始公开输入step_in[2]对应为所信任区块的区块哈希——拆分为2个128bit值表示,对应circom中:
signal input step_in[2]; signal prevBlockHash[2]; prevBlockHash[0] <== step_in[0]; prevBlockHash[1] <== step_in[1];
- 2)每一轮迭代的隐私输入private witness为:下一区块的区块头 以及 区块哈希,分别对应circom中:
signal input blockHashes[BLOCK_COUNT][2]; signal input blockHeaders[BLOCK_COUNT][80];
- 3)当前区块的区块哈希会作为 下一次迭代的 prevBlockHash:
if (i == 0) { for (var j = 0;j < 2;j++) checker[i].prevBlockHash[j] <== prevBlockHash[j]; } else { for (var j = 0;j < 2;j++) checker[i].prevBlockHash[j] <== blockHashes[i-1][j]; }
- 4)最终的公开输出为最后一个区块的区块哈希:
signal output lastBlockHash[2]; for (var j = 0;j < 2;j++) lastBlockHash[j] <== blockHashes[BLOCK_COUNT - 1][j];
- 5)每一次迭代,step circuit计算的逻辑为:
- 5.1)当前区块哈希 等于 SHA256(SHA256(当前区块头)):
for (var i = 0;i < 256;i++) { // log(secondHash.out[i]); secondHash.out[i] === inputBlockHashBits[i]; }
- 5.2)当前区块中的previous block header hash值 等于 prevBlockHash值:
var bitIdx = 0; for (var byteIdx = 4;byteIdx < 36;byteIdx++) { for (var i = 0;i < 8;i++) { // log(blockHeaderToBits[byteIdx].out[7 - i]); blockHeaderToBits[byteIdx].out[7 - i] === inputPrevBlockHashBits[bitIdx]; bitIdx++; } }
- 5.3)当前区块哈希 小于等于 区块中nBits字段所代表的目标阈值:
component computeFlippedBlockHash = Bits2Num(250); for (var i = 0;i < 80;i++) { for (var j = 0;j < 8;j++) { if (i * 8 + j < 250) { computeFlippedBlockHash.in[i * 8 + j] <== inputBlockHashBits[i * 8 + (7 - j)]; } } } // log("computeFlippedBlockHash", computeFlippedBlockHash.out); component blockHashMatchTarget = LessThan(252); blockHashMatchTarget.in[0] <== computeFlippedBlockHash.out; blockHashMatchTarget.in[1] <== targetComputer.target; blockHashMatchTarget.out === 1;
- 5.1)当前区块哈希 等于 SHA256(SHA256(当前区块头)):
基本性能情况为:
Number of recursion steps | Blocks verified per step | Prover time | Verifier time (uncompressed) |
---|---|---|---|
120 | 1 | 57.33s | 197.20ms |
60 | 2 | 46.11s | 307.08ms |
40 | 3 | 43.60s | 449.02ms |
30 | 4 | 41.17s | 560.53ms |
24 | 5 | 39.73s | 728.09ms |
实际运行时:【有报错待解决。。。】
# cargo run --example bitcoin
Number of constraints per step (primary circuit): 110566
Number of constraints per step (secondary circuit): 10347
Number of variables per step (primary circuit): 108355
Number of variables per step (secondary circuit): 10329
Creating a RecursiveSNARK...
RecursiveSNARK creation took 411.287737879s
Verifying a RecursiveSNARK...
RecursiveSNARK::verify: Ok(([0x000000000000000000000000000000001c106695014e03000000000000000000, 0x0000000000000000000000000000000023261735a2927d063178f9e2558ee98f], [0x0000000000000000000000000000000000000000000000000000000000000000])), took 550.123867ms
Number of constraints per step (primary circuit): 110566
Number of constraints per step (secondary circuit): 10347
Number of variables per step (primary circuit): 108355
Number of variables per step (secondary circuit): 10329
Creating a RecursiveSNARK...
stdout: stderr: terminate called after throwing an instance of 'std::runtime_error'
what(): Error loading signal blockHashes: Too many values
thread 'main' panicked at 'unable to open.: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/circom/reader.rs:109:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
3. 浏览器内生成和验证proof
https://github.com/nalinbhardwaj/Nova-Scotia 支持在浏览器中生成和验证proof。
背景资料:
用最新的nightly rust,可运行:
cd browser-test && wasm-pack build --target web --out-dir test-client/public/pkg
cd browser-test/test-client && yarn install && CI=false yarn build
Nova系列博客
- Nova: Recursive Zero-Knowledge Arguments from Folding Schemes学习笔记
- Nova 和 SuperNova:无需通用电路的通用机器执行证明系统
- Sangria:类似Nova folding scheme的relaxed PLONK for PLONK
- 基于Nova/SuperNova的zkVM
- SuperNova:为多指令虚拟机执行提供递归证明
- Lurk——Recursive zk-SNARKs编程语言
- Research Day 2023:Succinct ZKP最新进展
- 2023年 ZK Hack以及ZK Summit 亮点记
- 基于cycle of curves的Nova证明系统(1)
- 基于cycle of curves的Nova证明系统(2)
- Nova代码解析
- Nova中 Vitalik R1CS例子 的 folding scheme
- 基于Nova的MinRoot VDF实现
- 基于Nova的SHA256证明