This article will analysis Nova's code implementation, and explain how the code works based on the relevant papers.
I will first deduce the formula according to the papers, until it corresponds to the code, and then explain the code, which will be clearer and easier to understand.
As of August 30, 2023, I refer to the code corresponding to the latest commit, and considering that the code comments should be located near the code as much as possible, so I created the Nova-Analysis repo to save the code comments. When there are code comments, I will prompt in this article
// private modules
mod bellpepper;
mod circuit;
mod constants;
mod nifs;
mod r1cs;
// public modules
pub mod errors;
pub mod gadgets;
pub mod provider;
pub mod spartan;
pub mod traits;
According to its lib.rs, we know that Nova has these different mods. we explain these mods below.
Here is a example on how r1cs works.
the relation:
private inputs
Let's try to represent this relation in terms of R1CS.
the circuit:
the R1CS struct:
let private input
and intermediate values
, that is public inputs
and outputs
, that is 1
can be used to encode them.
Write the
for multiplication gate #1
, the left input is
for multiplication gate #2
, the left input is
So, we get the left input matrix:
Matrix
Using the same approach, we can get the right input matrix:
Matrix
Matrix
Let's check:
According to Definition 10 (R1CS) in Nova paper:
Consider a finite field
. Let the public parameters consist of size bounds where . The R1CS structure consists of sparse matrices with at most non-zero entries in each matrix. An instance consists of public inputs and outputs and is satisfied by a witness if , where .
There are different vector
It can be seen that matrix
So,
failed because
According to Definition 11 (Relaxed R1CS) in Nova paper:
Consider a finite field
. Let the public parameters consist of size bounds where . The relaxed R1CS structure consists of sparse matrices with at most non-zero entries in each matrix. A relaxed R1CS instance consists of an error vector , a scalar , and public inputs and outputs . An instance is satisfied by a witness if , where .
From the definition, we can get:
Choose a random value
We can deduce the following result:
Success! we only need to verify whether
According to Definition 12 (Committed Relaxed R1CS) in Nova paper:
Consider a finite field
and a commitment scheme over . Let the public parameters consist of size bounds where , and commitment parameters and for vectors of size and respectively. The committed relaxed R1CS structure consists of sparse matrices with at most non-zero entries in each matrix. A committed relaxed R1CS instance is a tuple , where and are commitments, , and are public inputs and outputs. An instance is satisfied by a witness if , and , where .
For more introduction to the commitment, please refer to the wiki.
According to Construction 1 (A Folding Scheme for Committed Relaxed R1CS) in Nova paper:
Consider a finite field
and a succinct, hiding, and homomorphic commitment scheme over . We define the generator and the encoder as follows.
–: output size bounds , and commitment parameters and for vectors of size and respectively.
–: output and . The verifier
takes two committed relaxed R1CS instances and . The prover , in addition to the two instances, takes witnesses to both instances, and . Let and . The prover and the verifier proceed as follows.
Send , wherer and with cross term
Sample and send challenge . Output the folded instance where
Output the folded witness , where
Next, we analyze the code of r1cs mod
according to the above theory, located in src/r1cs.rs
.
pub struct R1CS<G: Group> {
_p: PhantomData<G>,
}
This structure only retains a generic G, which needs to implement the methods in the Group trait, and has no valid data other than that.
There is a method for R1CS
:
commitment_key
, for a given circuit(certain CE
type's setup
method to get a CommitmentKey
and return it.
pub struct R1CSShape<G: Group> {
pub(crate) num_cons: usize,
pub(crate) num_vars: usize,
pub(crate) num_io: usize,
pub(crate) A: Vec<(usize, usize, G::Scalar)>,
pub(crate) B: Vec<(usize, usize, G::Scalar)>,
pub(crate) C: Vec<(usize, usize, G::Scalar)>,
}
R1CSShape
means matrix
There are some fields in this structure:
num_cons
: Length of a column in the matrix, also the number of multiplication gates.num_vars
: Length of witness num_io
: Length of public input and output A, B, C
: Represent the position and value of the non-zero value in the sparse matrix.We can get length of a column in the matrix by num_vars + num_io + 1
, so the three matrices R1CSShape
.
There are some methods for R1CSShape
:
new()
: Receive num_cons
, num_vars
, num_io
, A
, B
, C
as parameters, then do some checks,and return R1CSShape
. The checks's comments.check_regular_shape()
: Checks regularity conditions on the R1CSShape, required in Spartan-class SNARKs. The checks's comments.multiply_vec()
: Calculate and return them: is_sat_relaxed()
: Check is_sat()
: pass CommitmentKey, R1CSInstance and R1CSWitness as parameters, check commit_T()
: pad()
: make witness.len() = M.colume.len() = 2^k
, required in Spartan-class SNARKs.
pub struct R1CSWitness<G: Group> {
W: Vec<G::Scalar>,
}
R1CSWitness
means
There are some methods for R1CSWitness
:
new()
: Pass in an R1CSShape
example S and a vector W. Because creating a Witness needs to know which R1CS's witness it is, and it will check that the length of W is equal to S.num_vars.commit()
: Calculate ck
, a CommitmentKey
, means
pub struct R1CSInstance<G: Group> {
pub(crate) comm_W: Commitment<G>,
pub(crate) X: Vec<G::Scalar>,
}
R1CSInstance
means
There are a method for R1CSInstance
:
new()
: A.num_io should == x.len()
pub struct RelaxedR1CSWitness<G: Group> {
pub(crate) W: Vec<G::Scalar>,
pub(crate) E: Vec<G::Scalar>,
}
RelaxedR1CSWitness
means
There are some methods for RelaxedR1CSWitness
:
default()
, use zero value to init the from_r1cs_witness()
, use zero value to init the commit()
, return ck
.fold()
, Calculate r
. Why is not R1CSWitness
is pad()
: Pads the provided witness to the correct length.
pub struct RelaxedR1CSInstance<G: Group> {
pub(crate) comm_W: Commitment<G>,
pub(crate) comm_E: Commitment<G>,
pub(crate) X: Vec<G::Scalar>,
pub(crate) u: G::Scalar,
}
RelaxedR1CSInstance
means
There are some methods for RelaxedR1CSInstance
:
default()
: Produces a default RelaxedR1CSInstance
given R1CSGens
and R1CSShape
from_r1cs_instance()
: Initializes a new RelaxedR1CSInstance
from an R1CSInstance
, the u
use one
valuefrom_r1cs_instance_unchecked()
: Initializes a new RelaxedR1CSInstance
from an R1CSInstance
, the u
use one
valuefold()
: Folds an incoming R1CSInstance
into the current one, calculate r
. Why is not R1CSInstance
is R1CSInstance
is 1
in default.According to Construction 2 (A Non-Interactive Folding Scheme) in Nova paper:
We achieve non-interactivity in the random oracle model using the strong Fiat-Shamir transform. Let
denote a random oracle sampled during parameter generation and provided to all parties. Let represent our interactive folding scheme (Construction 1). We construct a non-interactive folding scheme as follows:
–: output .
–: and ; output .
–: runs to retrieve its first message , and sends to ; computes , forwards this to , and outputs the resulting output.
–: runs with as the message from the prover and with randomness , and outputs the resulting output.
Assumption 1 (RO instantiation):
Construction 2 is a non-interactive folding scheme that satisfies completeness, knowledge soundness, and zero-knowledge in the standard model when
is instantiated with a cryptographic hash function.
Next, we analyze the code of nifs mod, located in src/nifs.rs.
pub struct NIFS<G: Group> {
pub(crate) comm_T: CompressedCommitment<G>,
}
This structure contains NISF
:
prove()
: Given ck
, ro_consts
, pp_digest
, S
U1
W1
U2
W2
(NIFS
, r = ρ(ro_consts, pp_digest, U1, U2, comm_T)
verify()
: Given ro_consts
, pp_digest
, U1
U2
self.comm_T
, calculate random r = ρ(ro_consts, pp_digest, U1, U2, comm_T)
, return the new U = U1.fold(U2)
.
pub(crate) const NUM_CHALLENGE_BITS: usize = 128;
pub(crate) const NUM_HASH_BITS: usize = 250;
pub(crate) const BN_LIMB_WIDTH: usize = 64;
pub(crate) const BN_N_LIMBS: usize = 4;
pub(crate) const NUM_FE_WITHOUT_IO_FOR_CRHF: usize = 17;
pub(crate) const NUM_FE_FOR_RO: usize = 24;
NUM_CHALLENGE_BITS
: Valid output bits when a random oracle is used to generate challenge value r
.NUM_HASH_BITS
: Valid output bits when a random oracle is used to generate hashBN_LIMB_WIDTH
and BN_N_LIMBS
: Use 4 * 64 bits to represent 256 bits.NUM_FE_WITHOUT_IO_FOR_CRHF
NUM_FE_FOR_RO
According to Construction 3 (IVC) in Nova paper:
Let
be the non-interactive folding scheme for committed relaxed R1CS (Construction 2). Consider a polynomialtime function F that takes non-deterministic input, and a cryptographic hash function . We define our augmented function as follows (all arguments to are taken as non-deterministic advice):
If
is , output ; otherwise,
check that , where is the public IO of ,
check that ,
compute , and
output .
Because can be computed in polynomial time, it can be represented as a committed relaxed R1CS structure . Let
denote the satisfying committed relaxed R1CS instance-witness pairfor the execution of on non-deterministic advice .
We define the IVC scheme as follows.
: Output .
: Compute
and output .
: Parse
as and then
if is , compute ;
otherwise, compute ,
compute , and
output .
: If
is , check that ;
otherwise,
parse as ,
check that ,
check that , and
check that and are satisfying witnesses to and respectively.
pub struct NovaAugmentedCircuitParams {
limb_width: usize, // word size
n_limbs: usize, // how many words
is_primary_circuit: bool, // A boolean indicating if this is the primary circuit
}
pub struct NovaAugmentedCircuitInputs<G: Group> {
params: G::Scalar, // Hash(Shape of u2, Gens for u2). Needed for computing the challenge.
i: G::Base,
z0: Vec<G::Base>,
zi: Option<Vec<G::Base>>,
U: Option<RelaxedR1CSInstance<G>>,
u: Option<R1CSInstance<G>>,
T: Option<Commitment<G>>,
}
params
:
lib.rs
Construction 4 (A zkSNARK of a Valid IVC Proof). Let
denote the IVC scheme in Construction 3, let denote the non-interactive folding scheme in Construction 2, and let denote a randomized cryptographic hash function. Assume a zero-knowledge succinct non-interactive argument of knowledge (Definition 2), , for committed relaxed R1CS. That is, given public parameters , structure , and instance , can convince in zero-knowledge and with a succinct proof (e.g., -sized proof) that it knows a corresponding witness such that is a satisfying committed relaxed R1CS tuple.
Consider a polynomial-time computable function . Suppose and . Suppose the prover and verifier are provided an instance . We construct a zkSNARK that allows the prover to show that it knows an IVC proof such that .
In a nutshell, we leverage the fact that is two committed relaxed R1CS instance-witness pairs. So, first folds instance-witness pairs and to produce a folded instance-witness pair , using . Next, runs to prove that it knows a valid witness for . In more detail, for polynomial-time computable function and corresponding function as defined in Construction 3 (and instantiated with ), we define as follows.
:
Compute
Compute
Output (
:
Compute .
Compute .
Output .
:
Ifis , output ;
otherwise,
parse as
compute
compute
output .
:
Ifis , check that ;
otherwise,
parse as ,
check that ,
check that ,
compute , and
check that .
Polynomial Extension(MLE) see Notes for PAZK(MLE)
The Sum-Check Protocol see Notes for PAZK(Sum-Check)
Definition 14 (Idealized Relaxed R1CS). Consider a finite field
. Let the public parameters consist of size bounds where The idealized relaxed R1CS structure consists of sparse matrices with at most non-zero entries in each matrix. A idealized relaxed R1CS instance consists of an error vector , a scalar , witness vector , and public inputs and outputs . An instance is satisfying if , where .
Construction 5 (Polynomial IOP for Idealized Relaxed R1CS). Consider an idealized relaxed R1CS statement
consisting of public parameters , structure , and instance , Without loss of generality, we assume that and are powers of and that .
Let . We interpret the matrices as functions with signature in a natural manner. In particular, an input in is interpreted as the binary representation of an index , where and the function outputs th entry of the matrix. As such, let , and denote multilinear extensions of and interpreted as functions, so they are -variate sparse multilinear polynomials of size . Similarly, we interpret and as functions with respective signatures and . Furthermore, let and denote the multilinear extensions of and interpreted as functions, so they are multilinear polynomials in and variables respectively.
As noted earlier, the verifier has an oracle access to the following polynomials: , and . Additionally, the verifier reads and in entirety.
Let . Similar to how we interpret matrices as functions, we interpret and as functions with the following respective signatures: and . Observe that the of satisfies
Similar to [40, Theorem 4.1], checking if is satisfiable is equivalent, except for a soundness error of over the choice of , to checking if the following identity holds: where and is the multilinear extension of where if and otherwise.
That is, if is satisfiable, then Equation (2) holds with probability over the choice of , and if not, then Equation (2) holds with probability at most over the random choice of .
To compute the right-hand side in Equation (2), the prover and the verifier apply the sum-check protocol to the following polynomial: From the verifier’s perspective, this reduces the task of computing the right-hand side of Equation (2) to the task of evaluating at a random input . Note that the verifier can locally evaluate in field operations via With in hand, can be computed in time given the four quantities: , and .
The last quantity can be computed with a single query to polynomial . Furthermore, the first three quantities can be computed by applying the sum-check protocol three more times in parallel, once to each of the following three polynomials (using the same random vector of field elements, , in each of the three invocations): , and .
To perform the verifier’s final check in each of these three invocations of the sum-check protocol, it suffices for the verifier to evaluate each of the above three polynomials at the random vector , which means it suffices for the verifier to evaluate , and . The first three evaluations can be obtained via the verifier’s assumed query access to . can be computed (via Equation (1)) from a query to and from computing .
In summary, we have the following polynomial IOP.
: run the sum-check protocol to reduce the check in Equation (2) to checking if the following hold, where are vectors in chosen at random by the verifier over the course of the sum-check protocol:
–, and ;
–and
–. :
– check if, and , with a query to at ;
– check ifwith an oracle query to ; and
– check ifby checking if: , where refers to a slice of without the first element of , and via an oracle query (see Equation (1)).)
see