Actual implementation of Nova uses EC group Gp​=E(Fq​) over a field Fqâ€‹î€ =Fp​ defined as:
Gp​={(x,y)∈Fq​∣E:y2=x3+5}
so each element in Gp​ consists of 2 elements from base field Fq​. Therefore, constraints in Fp​ will translate into Fq​ operations (non-native) which turns out to be quite expensive. So instead, we try to express elliptic curve group operations over the base field only which turns out to be quite efficient. But to incrementally prove and verify, we need something called a cycle of curves. In a cycle of curves, there are 2 elliptic curve groups that are base fields of the other. For example,
G(1):={(x,y)∈F(2)∣ y2=x3+5}
G(2):={(x,y)∈F(1)∣ y2=x3+5}
where F(1) and F(2) are different fields. are one example of a cycle of curves.
Efficient folding over cycles of curves
IVC Construction
To construct the folding described above, the original Nova implementation uses:
Check if the input used in this step matches the output from previous step.
Check if the output element hash is done correctly.
VC Verification
Faulty version
Attack overview
And the other part would be:
So they can be from completely 2 different runs and this check will still pass. An actual attack is implemented as follows (Instances highlighted red are not satisfied malicious instances and green ones are satisfied instances):
This will give us:
Now, the faulty verification check will:
Check if instances from step 2 and 3 are satisfied. Since they are a result of an honest prover, it passes without problem.
Fixed version
References
Let's define functions FoldV​(U1​,U2​),FoldP​((U1​,W1​),(U2​,W2​)) as the verifier and prover parts of folding committed relaxed R1CS, respectively. Suppose you have a R1CS(1) instance over a certain scalar fieldof G(1) which is F(1). To check a folded instance from R1CS(1) we need to check if the folded instance is the result of correct execution FoldV​. This constraint can be only be implemented efficiently over the base field of G(1) which is F(2).
For example, with four R1CS(1) instances, the folding process would occur in two steps:
First fold: Fold two pairs of instances, resulting in two R1CS(1) instances but to verify if each of them are constructed correctly, we will need two R1CS(2) instances.
Second fold: Fold the R1CS(2) instances into a single R1CS(2) instance but to verify if it is constructed correctly, we will need one R1CS(1) instance.
R1CS(1) to do i-th step in F(1) and accumulate the instances of R1CS(2) (denoted as Uacc,i+1(2)​).
R1CS(2) to do i-th step in F(2) and accumulate the instances of R1CS(1) (denoted as Uacc,i+1(1)​ ).
So if we denote the step function in F(1) as F1​ and F(2) as F2​. The high level overview of the IVC will be as shown below:
This is the high-level view of R1CS(1) but R1CS(2) would also look the same:
To summarize what we need to do in R1CS(1) is:
Check if the public instance x0​ of Ui(2)​ is indeed as given.
Check IsStrict(Ui(2)​) (IsStrict checks if the error term is 0 and scalar is 1).
Calculate zi+1(1)​F(1) and accumulate Ui(2)​.
NOTE: In R1CS(2), it will fold Ui+1(1)​ and Uacc,i(1)​ (instead of instances of same index). Also, hash assignments will be a bit different in terms of indexing:
Given a proof π=(Ui(1)​,Ui(2)​,Uacc,i(1)​,Uacc,i(2)​), Verify(i,(z0(1)​,z0(2)​),(zi(1)​,zi(2)​),π) checks the following:
Check if Ui(1)​, Ui(2)​, Uacc,i(1)​ and Uacc,i(2)​ instances are satisfied.
Check if Ui(1)​.x1​=hash(i(1),z0(1)​,zi(1)​,Uacc,i(2)​). This ensures that this output hash is derived from inputs zi(1)​,Uacc,i(2)​.
Check if Ui(2)​.x1​=hash(i(2),z0(2)​,zi(2)​,Uacc,i(1)​).
In this version, Wilson Nguyen et al. showed that it is possible to generate proof of evaluation of 275 rounds of the Minroot VDF in only 1.46 seconds and proposed a fix which has been reflected to Nova implementation.
This attack mainly stems from the fact that there is no constraints on the x0​. Notice that the verification in the faulty version can be split into 2 parts:
Check if Ui(2)​ and Uacc,i(1)​ instances are satisfied.
Check IsStrict(Ui(2)​)
Check if Ui(2)​.x1​=hash(i(2),z0(2)​,zi(2)​,Uacc,i(1)​).
Check if Ui(1)​ and \Uacc,i(2)​ instances are satisfied.
Check IsStrict(Ui(1)​)
Check if Ui(1)​.x1​=hash(i(1),z0(1)​,zi(1)​,Uacc,i(2)​).
Step 1: Generate malicious instance Ui−1(2)​=(0,0,(x0​,x1​),1) where x0​=hash(⋅,Uacc,i−1(2)​) is a trash value and x1​=hash((i−1)(2),z0(2)​,zi−1(2)​,U⊥(1)​).
Step 2: Run the honest prover to get Ui(1)​ and Uacc,i(2)​.
Step 3: Do step 1 and 2 for the other field to get (Ui(2)​,Uacc,i(1)​).
It is very well explained in the presentation by Wilson Nguyen . It is highly recommended to watch it if you want to fully understand how it works. To elaborate a bit more on this video:
The honest prover will run R1CS(1) with Ui−1(2)​ and some fake accumulation claim Uacc,i−1(2)​, and some arbitrary zi−1(1)​.
Ui(1)​ with Ui(1)​.x0​=Ui−1(2)​.x1​=hash((i−1)(2),z0(2)​,zi−1(2)​,U⊥(1)​).
This makes it possible to use U⊥(1)​ in R1CS(2) and it will output:
Uacc,i(1)​:=FoldV​(Ui(1)​,U⊥(1)​)
Ui(2)​ with Ui(2)​.x0​=Ui−1(1)​.x1​=hash(i(1),z0(1)​,zi(1)​,Uacc,i(2)​).
With this, we have a valid Ui(2)​ and Uacc,i(1)​.
The Uacc,i(2)​ generated here is invalid so Ui(1)​ cannot be used as well since the Ui(1)​.x1​ hash has Uacc,i(2)​. But we will just discard them both and generate them in Step 3.
Check Ui(1)​.x1​=hash(i(1),z0(1)​,zi(1)​,Uacc,i(2)​) passes since this hash was also properly constructed by the honest prover.
Check Ui(2)​.x1​=hash(i(2),z0(2)​,zi(2)​,Uacc,i(1)​) passes as well similarly.
Given a proof π=(Ui(2)​,Uacc,i(1)​,Uacc,i(2)​), Verify(i,(z0(1)​,z0(2)​),(zi(1)​,zi(2)​),π) checks the following:
Check if Ui(2)​, Uacc,i(1)​ and Uacc,i(2)​ instances are valid.
Check if Ui(2)​.x1​=hash(i(2),z0(2)​,zi(2)​,Uacc,i(1)​).
Check if Ui(2)​.x0​=hash(i(1),z0(1)​,zi(1)​,Uacc,i(2)​).
Here, the previous attack passes the check 1 and 2 but for the last check, it becomes tricky since now we can't just discard the invalid instances and generate another one because Ui(2)​.x0​ contains Uacc,i(2)​.