1. 引言
前序博客:
基于Nova的SHA256证明,开源代码见:
以SHA-256哈希运算输入为input
为例,其中SHA-256算法及示例 中的:
- 1)“4. 填充”对应
add_sha256_padding(input)
函数,返回padded_input
。 - 2)每个512 bit(64字节)block对应
padded_input_to_blocks(padded_input)
函数返回block数组blocks_vec
,假设该数组长度为N。 - 3)每个block压缩调用compress256,其中
state
为前一block压缩结果:
let mut state = IV; \text{let mut state = IV;} let mut state = IV;
For t = 1 to N \text{For } t=1 \text{ to } N For t=1 to N
compress256(&mut state, &[blocks_vec[t]]); \ \ \ \ \text{compress256(\&mut state, \&[blocks\_vec[t]]);} compress256(&mut state, &[blocks_vec[t]]); // 对应为 z i + 1 = F ( z i , w i ) z_{i+1}=F(z_i, w_i) zi+1=F(zi,wi)。
End for t \text{End for } t End for t- 3.1)取每个block调用
compress256()
时的state
输入,构成digest_sequence数组——对应长度为N+1,对应测试用例可参看test_digest_sequence_generation()
。
- 3.1)取每个block调用
- 4)当处理完最后一个block之后,将
H
j
(
N
)
H_j^{(N)}
Hj(N)变量拼接在一起,即为
input
的哈希值——对应sha256_state_to_bytes()
函数返回的hash_bytes
,做十六进制编码(hex::encode(hash_bytes)
)即为input
的哈希值。
以上四个步骤,对应一个函数:sha256_state_sequence()
,返回:
- 1)block_sequence:为将填充后input切分的block数组
- 2)digest_sequence:为处理每个block时的previous 32-byte digest(即
compress256()
时的state
输入)。
当构建约束时,compress256()
对应SHA256CompressionCircuit
,根据 Nova代码解析 可知,需对其实现StepCircuit trait:
/// A helper trait for a step of the incremental computation (i.e., circuit for F)
pub trait StepCircuit<F: PrimeField>: Send + Sync + Clone {
/// Return the the number of inputs or outputs of each step
/// (this method is called only at circuit synthesis time)
/// `synthesize` and `output` methods are expected to take as
/// input a vector of size equal to arity and output a vector of size equal to arity
fn arity(&self) -> usize;
/// Sythesize the circuit for a computation step and return variable
/// that corresponds to the output of the step z_{i+1}
fn synthesize<CS: ConstraintSystem<F>>(
&self,
cs: &mut CS,
z: &[AllocatedNum<F>],
) -> Result<Vec<AllocatedNum<F>>, SynthesisError>;
/// return the output of the step when provided with the step's input
fn output(&self, z: &[F]) -> Vec<F>;
}
impl<F> StepCircuit<F> for SHA256CompressionCircuit<F>
where
F: PrimeField + PrimeFieldBits,
{
fn arity(&self) -> usize {
2
}
fn synthesize<CS: ConstraintSystem<F>>(
&self,
cs: &mut CS,
z: &[AllocatedNum<F>],
) -> Result<Vec<AllocatedNum<F>>, SynthesisError> {
assert_eq!(z.len(), 2);
let initial_curr_digest_bits = z[0]
.to_bits_le(cs.namespace(|| "initial current digest bits"))
.unwrap();
let remaining_curr_digest_bits = z[1]
.to_bits_le(cs.namespace(|| "remaining current digest bits"))
.unwrap();
let mut current_digest_bits = vec![];
for i in 0..F::CAPACITY as usize {
current_digest_bits.push(initial_curr_digest_bits[i].clone());
}
let num_bits_remaining = DIGEST_LENGTH_BYTES*8 - (F::CAPACITY as usize);
for i in 0..num_bits_remaining {
current_digest_bits.push(remaining_curr_digest_bits[i].clone());
}
let mut current_state: Vec<UInt32> = vec![];
for c in current_digest_bits.chunks(32) {
current_state.push(UInt32::from_bits_be(c));
}
assert_eq!(current_state.len(), 8); //对应为上面的mut state,也即$(H_1^{(t-1)},H_2^{(t-1)},H_3^{(t-1)},H_4^{(t-1)},H_5^{(t-1)},H_6^{(t-1)},H_7^{(t-1)},H_8^{(t-1)})$
let input_bit_values = bytes_to_bits(&self.input);
assert_eq!(input_bit_values.len(), BLOCK_LENGTH_BYTES*8); //每个block blocks_vec[t]固定为512bit。
let input_bits: Vec<Boolean> = input_bit_values
.iter()
.enumerate()
.map(
|(i, b)| Boolean::from(
AllocatedBit::alloc(
cs.namespace(|| format!("input bit {i}")),
Some(*b),
)
.unwrap()
)
)
.collect();
// SHA256 compression function application
let next_state: Vec<UInt32> = sha256_compression_function(&mut *cs, &input_bits, ¤t_state)?;
assert_eq!(next_state.len(), 8);
let next_digest_bits: Vec<Boolean> = next_state
.into_iter()
.map(|u| u.into_bits_be())
.flatten()
.collect();
assert_eq!(next_digest_bits.len(), DIGEST_LENGTH_BYTES*8); //当前block处理完毕之后,输出为256bit。
let mut z_out: Vec<AllocatedNum<F>> = vec![]; //将当前block处理之后的输出转换为scalar值表示。
let (initial_next_digest_bits, remaining_next_digest_bits)
= next_digest_bits.split_at(F::CAPACITY as usize);
z_out.push(pack_bits(
cs.namespace(|| "Packing initial preimage bits into scalar"),
initial_next_digest_bits
)
.unwrap());
z_out.push(pack_bits(
cs.namespace(|| "Packing remaining preimage bits into scalar"),
remaining_next_digest_bits
)
.unwrap());
Ok(z_out)
}
fn output(&self, z: &[F]) -> Vec<F> {
assert_eq!(z.len(), 2);
assert_eq!(z[0], self.current_digest[0]);
assert_eq!(z[1], self.current_digest[1]);
// Compute output using non-deteriministic advice
self.next_digest.to_vec()
}
}
impl<F> Default for SHA256CompressionCircuit<F>
where F: PrimeField + PrimeFieldBits,
{
fn default() -> Self {
Self {
input: [0u8; BLOCK_LENGTH_BYTES],
current_digest: [F::zero(); 2],
next_digest: [F::zero(); 2],
}
}
}
其中:
- 由于SHA-256输出为256bit,而所使用曲线scalar域小于256bit,需以2个scalar域元素(因此arity设置为2)来表示一个SHA-256 digest值,具体可见
test_scalar_digest_roundtrip()
测试用例。 sha256_compression_function()
函数为真正compress256()
计算逻辑:
- 详细测试用例见:
test_sha256_compression_constraints()
,对应约束数为27218个:Num constraints = 27218 Num inputs = 1 test sha256_step::circuit::tests::test_sha256_compression_constraints ... ok
当使用Nova来证明时,具体见example/sha256.rs
。实际https://github.com/Microsoft/Nova 中做了2种Nova IVC proof压缩SNARK方案实现:
- 1)spartan::RelaxedR1CSSNAR:使用Spartan + IPA-PC作为多项式承诺方案。
- 2)spartan::pp::RelaxedR1CSSNARK:使用Spartan + sum-check,并将向量承诺看成是对多项式的承诺。
type S1<G1> = spartan::RelaxedR1CSSNARK<G1, EE1<G1>>;
type S2<G2> = spartan::RelaxedR1CSSNARK<G2, EE2<G2>>;
type S1Prime<G1> = spartan::pp::RelaxedR1CSSNARK<G1, EE1<G1>>;
type S2Prime<G2> = spartan::pp::RelaxedR1CSSNARK<G2, EE2<G2>>;
以input为 2 6 2^6 26个字节0为例,有:
Nova-based SHA256 compression function iterations
=========================================================
Producing public parameters...
PublicParams::setup, took 57.5854094s
Number of constraints per step (primary circuit): 37034
Number of constraints per step (secondary circuit): 10347
Number of variables per step (primary circuit): 37000
Number of variables per step (secondary circuit): 10329
Generating a RecursiveSNARK...
RecursiveSNARK::prove_step 0: true, took 891.396614ms
RecursiveSNARK::prove_step 1: true, took 1.298414351s
Total time taken by RecursiveSNARK::prove_steps: 2.189852312s
Verifying a RecursiveSNARK...
RecursiveSNARK::verify: true, took 417.041ms
Generating a CompressedSNARK using Spartan with IPA-PC...
CompressedSNARK::prove: true, took 83.01777488s
Total proving time is 148.783484203s
CompressedSNARK::len 10038 bytes
Verifying a CompressedSNARK...
CompressedSNARK::verify: true, took 695.121631ms
=========================================================
Public parameters generation time: 57.5854094s
Total proving time (excl pp generation): 148.783484203s
Total verification time: 695.121631ms
=========================================================
Expected value of final hash = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"
Actual value of final hash = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"
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实现