This document explains from a high-level how the SuperNova protocol was implemented in Arecibo. We aim to provide a mathematical description of the protocol, as it is implemented, and highlight the differences with the original paper.
Before delving into the specifics of the implementation, it's crucial to define and clarify some key terms and concepts used throughout this document:
-
Recursive SNARK: A Recursive SNARK is a type of succinct non-interactive argument of knowledge for a circuit
$F$ which can be composed with itself as$z_{i+1} \gets F(z_i)$ . Each iteration proves the verification of a proof for$z_i$ and the correctness of$z_{i+1}$ , ensuring the proving of each step remains constant. -
Augmentation Circuit: In the context of the SuperNova protocol, an augmentation circuit refers to a circuit
$F'$ composing$F$ with a circuit which partially verifies the validity of the previous output$z_i$ before running$F(z_i)$ . -
NIFS Folding Verifier: A non-interactive folding scheme is a protocol for efficiently updating a proof
$\pi_i$ about an iterated function$z_{i+1} \gets F(z_i)$ into a new proof$\pi_{i+1}$ , through a process referred to as "folding". By splitting the proof into an instance/witness pair$(u,w) = \pi$ , the folding verifier describes an algorithm for verifying that the$u$ component was properly updated.
The main improvement of SuperNova, is to allow each iteration to apply one of several functions to the previous output, whereas Nova only supported the iteration of a single function.
Let circuit_supernova::StepCircuit
trait, where the main differences with the existing StepCircuit
trait are
- The circuit
$F_j$ provides itscircuit_index
$j$ - The
synthesize
function upon input$z_i$ returns the nextprogram_counter
$\mathsf{pc}_{i+1}$ alongside the output$z_{i+1}$ . It also accepts the (optional) input program counter$\mathsf{pc}_i$ , which can beNone
when$\ell=1$ . During circuit synthesis, a constraint enforces$\mathsf{pc}_i \equiv j$ . In contrast to the paper, the predicate function$\varphi$ is built into the circuit itself. In other words, we have the signature$(\mathsf{pc}_{i+1}, z_{i+1}) \gets F_{j}(\mathsf{pc}_{i}, z_{i})$ .
The goal is to efficiently prove the following computation:
pc_i = pc_0
z_i = z_0
for i in 0..num_steps
(pc_i, z_i) = F_{pc_i}(z_i)
return z_i
"Cycles of Curves" describes a technique for more efficiently verifying the output
While the original Nova implementation allows computation to be done on both curves, the SuperNova implementation only uses the cycle curve to verify the computation performed on the primary curve.
The prover needs to store data about the previous function iteration. It is defined by the supernova::RecursiveSNARK
struct. It contains:
-
$i$ : the number of iterations performed. Note that thenew
constructor actually performs the first iteration, and the first call toprove_step
simply sets the counter to 1. - Primary curve:
-
$(\mathsf{pc}_i, z_0, z_i)$ : current program counter and inputs for the primary circuit -
$U[\ ],W[\ ]$ : List of relaxed instance/witness pairs for all the circuits on the primary curve. These can beNone
when the circuit for that pair has not yet been executed. The last updated entry is the result of having folded a proof for the correctness of$z_i$ .
-
- Secondary curve
-
$(z_0', z_i')$ : Inputs for the single circuit on the secondary curve. -
$u',w'$ : Proof for the correctness of the circuit that produced$z_i'$ -
$U', W'$ : Relaxed instance/witness pair into which$(u', w')$ will be folded into in the next iteration.
-
Due to the particularities of the cycles of curves implementation, the outputs of the circuits producing
At each step, the prover needs to:
- Create a proof
$T'$ for folding$u'$ into$U'$ , producing$U'_{next}$ . - Create a proof
$(u,w)$ on the primary curve for the statements:$(\mathsf{pc}_{i+1}, z_{i+1}) \gets F_\mathsf{pc_i}(z_i)$ - Verifying the folding of
$u'$ into$U'$ with$T'$
- Create a proof
$T$ for folding$u$ into$U[\mathsf{pc}_i]$ , producing$U_{next}$ - Create a proof
$(u'_{next}, w'_{next})$ for the verification on the secondary curve- Verifying the folding of
$u$ into$U[\mathsf{pc}_i]$ with$T$
- Verifying the folding of
- Update the state of the claims
-
$U' = U'_{next}$ ,$W' = W'_{next}$ -
$U[\mathsf{pc}_i] = U_{next}$ ,$W[\mathsf{pc}_i] = W_{next}$ $(u',w') = (u'_{next}, w'_{next})$
-
- Save
$z_{i+1},z'_{i+1}, \mathsf{pc}_{i+1}$ as inputs for the next iteration.
In pseudocode, prove_step
looks something like:
if i = 0 {
U[] = [ø;l]
// Create a proof for the first iteration of F on the primary curve
(pc_1, z_1), (u_1, w_1) <- Prove(F_{pc0},
i=0,
pc_0,
z_0,
_, // z_i : z_0 is the input used
_, // U' : Existing accumulator is empty
_, // u' : No proof of the secondary curve to verify
_, // T' : Nothing to fold
0, // index of u' in U'
)
// The circuit output is [ vk, i=1, pc_1, z_0, z_1, U'=ø ]
// Update state to catch up with verifier
z_i = z_1
pc_i = pc_1
U' = ø
W' = ø
// Create proof on secondary curve
// verifying the validity of the first proof
z'_1, (u'_1, w'_1) <- Prove(F',
i,
0, // pc is always 0 on secondary curve
z'_0,
_, // z'_i : z'_0 is the input used
_, // U[]: all accumulators on primary curve are empty
u_0, // proof for z1
_, // T: u_0 is directly included into U[pc0]
pc_1, // index of u_0 in U[]
)
// The circuit outputs [ vk, i=1, z'_0, z'_1, U_next[] ]
// Update state to catch up with verifier
z_i' = z_1'
U[pc_1] = u_1
W[pc_1] = w_1
// Save the proof of F' to be folded into U' in the next iteration
u' = u'_1
w' = w'_1
} else {
// Create folding proof for u' into U', producing U'_next
(U'_next, W'_next), T' <- NIFS.Prove(U', W', u', w')
// Create a proof for the next iteration of F on the primary curve
(pc_next, z_next), (u_new, w_new) <- Prove(F_{pc_i},
i,
pc_i,
z_0,
z_i,
[U'],
u',
T',
0, // index of u' in [U'] is always 0
)
// The circuit outputs [ vk, i+1, pc_next, z_0, z_next, U'_next ]
// Update state to catch up with verifier
z_i = z_next
pc_i = pc_next
U' = U'_next
W' = W'_next
// Create folding proof for u_new into U[pci], producing U_next
(U_next, W_next), T <- NIFS.Prove(U[pci], W[pci], u_new, w_new)
// Create proof on secondary curve
// verifying the folding of u_next into
z'_next, (u'_next, w'_next) <- Prove(F',
i,
0, // pc is always 0 on secondary curve
z_0',
z_i',
U[],
u_new,
T,
pc_i, // Index of u_new in U[]
)
// The circuit outputs [ vk, i+1, z'_0, z'_next, U_next[] ]
// Update state to catch up with verifier
z_i' = z'_next
U[pc_next] = U_next
W[pc_next] = W_next
// Save the proof of F' to be folded into U' in the next iteration
u' = u'_next
w' = w'_next
}
Each iteration stops when the prover has produced a valid R1CS instance
During each proof iteration, the circuits evaluated and proved by the prover need to be augmented to include additional constraints which verify that the previous iteration was correctly accumulated.
To minimize code duplication, there is only a single version of the recursive verification circuit. The circuit is customized depending on whether it is synthesized on the primary/secondary curve.
The inputs of provided to the augmented step circuit
Inputs for step circuit
-
$\mathsf{vk}$ : a digest of the verification key for the final compressing SNARK (which includes all public parameters of all circuits) -
$i \in \mathbb{Z}_{\geq 0}$ : the number of iteration of the functions before running$F$ -
$\mathsf{pc}_i \in [\ell]$ : index of the current function being executed-
Primary: The program counter
$\mathsf{pc}_i$ must always beSome
, and through theEnforcingStepCircuit
trait, we enforce$\mathsf{pc}_i \equiv j$ . -
Secondary: Always
None
, and interpreted as$\mathsf{pc}_i \equiv 0$ , since there is only a single circuit.
-
Primary: The program counter
-
$z_0 \in \mathbb{F}^a$ : inputs for the first iteration of$F$ -
$z_i \in \mathbb{F}^a$ : inputs for the current iteration of$F$ -
Base case: Set to
None
, in which case it is allocated as$[0]$ , and$z_0$ is used as$z_i$ .
-
Base case: Set to
-
$U_i[\ ] \in \mathbb{R}'^\ell$ : list of relaxed R1CS instances on the other curve-
Primary: Since there is only a single circuit on the secondary curve, we have
$\ell = 0$ and therefore$U_i[\ ]$ only contains a singleRelaxedR1CSInstance
. -
Secondary: The list of input relaxed instances
$U_i[\ ]$ is initialized by passing a slice[Option<RelaxedR1CSInstance<G>>]
, one for each circuit on the primary curve. Since some of these instances do not exist yet (i.e. for circuits which have not been executed yet), theNone
entries are allocated as a default instance.
-
Primary: Since there is only a single circuit on the secondary curve, we have
To minimize the cost related to handling public inputs/outputs of the circuit, these values are hashed as
Auxiliary inputs for recursive verification of other the curve's circuit
-
$u \in \mathbb{R}'$ : fresh R1CS instance for the previous iteration on the other curve- Contains the public outputs of the 2 previous circuits on the different curves.
-
Base case -- Primary: Set to
None
, since there is no proof of the secondary curve to fold
-
$T \in \mathbb{G}'$ : Proof for folding$u$ into$U[\mathsf{pc}']$ .-
Base case -- Primary: Set to
None
, since there is no proof of the secondary curve to fold
-
Base case -- Primary: Set to
-
$\mathsf{pc}' \in [\ell]$ : index of the previously executed function on the other curve.- Primary: Always 0 since the program counter on the secondary curve is always 0
- Secondary: Equal to the program counter of the last function proved on the primary curve.
These non-deterministic inputs are used to compute the circuit's outputs.
When they are empty, we allocate checked default values instead.
We also check that the computed hash of the inputs matches the hash of the output of the previous iteration contained in
Outputs
-
$\mathsf{vk}$ : passed along as-is -
$i+1 \in \mathbb{Z}_{\geq 0}$ : the incremented number of iterations -
$\mathsf{pc}_{i+1} \in [\ell]$ : index of next function to execute -
$z_0 \in \mathbb{F}^a$ : passed along as-is -
$z_{i+1} \in \mathbb{F}^a$ : output of the execution$F_{\mathsf{pc}_i}$ -
$U_{i+1}[\ ] \in \mathbb{R}'^\ell$ : Updated list of Relaxed R1CS instances, reflecting the folding of$u$ into$U_i[\mathsf{pc}']$ -
Primary: Since no input proof was provided, we set
$U_1$ to the default initial instance.
-
Primary: Since no input proof was provided, we set
All these values should be computed deterministically from the inputs described above (even if just passed along as-is). The actual public output is the hash of these values, to be consistent with the encoding of the inputs.
The circuit has a branching depending on whether it is verifying the first iteration of the IVC chain. Each branch computes the next list of instances
The verification circuit first checks that the public output
This bit is checked later on.
The circuit extracts
The instance new folding instance
A new list of accumulators
If
The output list of instances
-
Primary curve: the incoming proof
$u$ is trivial, so the result of folding two trivial instances is defined as the trivial relaxed instance. -
Secondary curve: the instance
$U_0[\mathsf{pc}']$ is simply replaced with the relaxation of$u$ using conditional selection.
This branch returns
Having run both branches, the circuit has computed
-
$U_{i+1}[\ ], b_{X_0}, s$ from the first branch -
$U_1[\ ]$ from the second branch -
Using the bit
$b_{i=0} \gets i \stackrel{?}{=} 0$ , it needs to conditionally select which list of instance to return.$U_{i+1} \gets b_{i=0} \ \ ?\ \ U_{1}[\ ] \ \ :\ \ U_{i+1}[\ ]$
-
Check that
$(i\neq 0) \implies b_{X_0}$ , enforcing that the hash is correct when not handling the base case$b_{i=0} \lor b_{X_0}$
-
Select
$z_i \gets b_{i=0} \ \ ?\ \ z_0 \ \ :\ \ z_i$
-
Enforce circuit selection
$\mathsf{pc}_{i} \equiv j$
-
Compute next output
$(\mathsf{pc}_{i+1}, z_{i+1}) \gets F_j(z_i)$
The output at this point would be
To keep the number of public outputs small, the outputs of the circuit are hashed into a single field element. We create this hash as
We also return the hash resulting from the output on the other curve,
We can view output as the shared state between the circuits on the two curve. The list of two elements is a queue, where the last inserted element is popped out to be consumed by the verification circuit, and the resulting output is added to the end of the queue.
After any number of iterations of prove_step
, we can check that the current prover state is correct. In particular, we want to ensure that
To verify that
$u'.X_0 \stackrel{?}{=} H(\mathsf{vk}, i, \mathsf{pc}_i, z_0, z_i, U')$ $u'.X_1 \stackrel{?}{=} H'(\mathsf{vk}, i, z'_0, z'_i, U[\ ])$
We then verify that
We then need to verify that all accumulators
Nova | SuperNova | |
---|---|---|
RecursiveSNARK |
lib.rs |
supernova/mod.rs |
CompressedSNARK |
lib.rs |
supernova/snark.rs |
StepCircuit |
traits/circuit.rs |
traits/circuit_supernova.rs |
Augmented Circuit | circuit.rs |
supernova/circuit.rs |
(Batched)RelaxedR1CSSNARKTrait |
traits/snark.rs |
traits/snark.rs |
Direct Spartan | spartan/snark.rs |
spartan/batched.rs |
Spartan with Spark preprocessing | spartan/ppsnark.rs |
spartan/batched_ppsnark.rs |
(batched) Sumcheck primitives | spartan/sumcheck.rs |
spartan/sumcheck.rs |