Nova-Scotia代码解析

1. 引言

前序博客:

开源代码见:

Nova-Scotia定位为中间件,本质为:

  • 1)读取以Circom编写的电路文件,将Circom电路解析为CircomCircuit结构体。
  • 2)基于CircomCircuit结构体,实现Nova的StepCircuit trait中的arity()、synthesize()、output()函数。

从而打通了将Circom生态接入Nova证明系统的通道。

Justin Drake指出

  • 可将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:
      let input = CircomInput {
          step_in: decimal_stringified_input.clone(),
          extra: private_inputs[i].clone(),
      };
      
      将CircomInput转换为json表示。
    • 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表示。】

字节数字段名数据类型描述
4versionint32_t区块版本号表示了区块验证所应遵循的规则集。
32previous block header hashchar[32]为SHA256(SHA256(前一区块头))的字节表示。从而可确保若不修改当前区块头,则无法修改前一区块。
4merkle root hashchar[32]为当前区块内所有交易哈希所构建的merkle tree root。子节点哈希运算为H()=SHA256(SHA256())。
4timeuint32_t为矿工开始对区块进行哈希的Unix epoch时间。需严格大于前11个区块的平均时间。
4nBitsuint32_t为SHA256(SHA256(当前区块头))必须小于等于的阈值的编码表示。
4nonceuint32_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;
      

基本性能情况为:

Number of recursion stepsBlocks verified per stepProver timeVerifier time (uncompressed)
120157.33s197.20ms
60246.11s307.08ms
40343.60s449.02ms
30441.17s560.53ms
24539.73s728.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系列博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值