penumbra_sdk_proof_setup/single/
mod.rs

1//! This module contains definition for a single circuit setup.
2#![deny(clippy::unwrap_used)]
3// Todo: prune public interface once we know exactly what's needed.
4mod dlog;
5pub(crate) mod group;
6pub mod log;
7mod phase1;
8mod phase2;
9
10use ark_ec::{CurveGroup, Group};
11use ark_ff::Zero;
12use ark_groth16::data_structures::ProvingKey;
13use ark_groth16::VerifyingKey;
14use ark_poly::EvaluationDomain;
15use ark_poly::Radix2EvaluationDomain;
16use ark_relations::r1cs::ConstraintMatrices;
17
18use ark_serialize::CanonicalDeserialize;
19use ark_serialize::CanonicalSerialize;
20use decaf377::Bls12_377;
21
22pub use dlog::Proof as DLogProof;
23
24pub use phase1::CRSElements as Phase1CRSElements;
25pub use phase1::Contribution as Phase1Contribution;
26pub(crate) use phase1::LinkingProof;
27pub use phase1::RawCRSElements as Phase1RawCRSElements;
28pub use phase1::RawContribution as Phase1RawContribution;
29
30pub use phase2::CRSElements as Phase2CRSElements;
31pub use phase2::Contribution as Phase2Contribution;
32pub use phase2::RawCRSElements as Phase2RawCRSElements;
33pub use phase2::RawContribution as Phase2RawContribution;
34
35use group::{F, G1, G2};
36
37use anyhow::{anyhow, Result};
38
39#[derive(Clone, Debug, CanonicalSerialize, CanonicalDeserialize)]
40pub struct ExtraTransitionInformation {
41    /// The u polynomials evaluated at [x].
42    u_1: Vec<G1>,
43    /// The v polynomials evaluted at [x].
44    v_1: Vec<G1>,
45    /// The v polynomials evaluted at [x]_2.
46    v_2: Vec<G2>,
47    /// The p polynomials evaluated at [x]
48    p_1: Vec<G1>,
49}
50
51// Compute the degree associated with a given circuit.
52//
53// This degree can then be used for both phases.
54pub fn circuit_degree(circuit: &ConstraintMatrices<F>) -> Result<usize> {
55    let circuit_size = circuit.num_constraints + circuit.num_instance_variables;
56    Radix2EvaluationDomain::<group::F>::compute_size_of_domain(circuit_size)
57        .ok_or_else(|| anyhow!("Circuit of size {} is too large", circuit_size))
58}
59
60/// Transition between phase1 and phase2, using the circuit.
61///
62/// This will also produce extra elements needed later when combining the elements
63/// into the actual proving key.
64pub fn transition(
65    phase1: &phase1::CRSElements,
66    circuit: &ConstraintMatrices<F>,
67) -> Result<(ExtraTransitionInformation, phase2::CRSElements)> {
68    // To understand this function, it can be good to recap the relationship between
69    // R1CS constraints and QAP constraints.
70    //
71    // While we call the constraints a "circuit", they're really an R1CS system.
72    // The circuit contains matrices A, B, C. Each of these matrices has the same size.
73    // The number of columns is the number of variables in our circuit,
74    // along with an additional column for each constraint, representing an
75    // "internal variable".
76    // The number of rows is simply the number of constraints in our circuit.
77    //
78    // To transform the circuit into a QAP, each column of a matrix becomes a polynomial.
79    // For the matrix A, we have uᵢ(X), for B, vᵢ(X), and C, wᵢ(X).
80    // We also have a domain, a list of field elements, such that evaluating each polynomial
81    // on the domain produces the entries of the corresponding matrix column.
82    //
83    // Furthermore, we also pad the matrices before this transformation.
84    // The bottom of A is filled with:
85    //   1 0 0 ...
86    //   0 1 0 ...
87    //   0 0 1 ...
88    //   .........
89    // based on the number of instance variables. This is to avoid
90    // potential malleability issues. See: https://geometry.xyz/notebook/groth16-malleability.
91    // B and C are simply padded with 0 to match this size.
92    //
93    // Finally, the domain might be larger than the size of these matrices,
94    // and so we simply add rows filled with 0 to match the size of the domain.
95    //
96    // Now, we don't need to calculate these polynomials directly, instead what we care about
97    // is the polynomials pᵢ(X) = β uᵢ(X) + α vᵢ(X) + wᵢ(X), evaluated at [x],
98    // and the evaluations [t(x)xⁱ], where t is a polynomial that's 0 everywhere over the
99    // domain.
100    //
101    // Our strategy here is to make use of lagrange polynomials Lⱼ(X), which
102    // are 0 everywhere over the domain except for the jth entry.
103    // Specifically, if we have the evaluations Lⱼ([x]), we can then calculate
104    // each pᵢ([x]) by a linear combination over these evaluations,
105    // using the coefficients in that matrix column.
106    // Note that the matrices are very sparse, so there are only a linear
107    // number of such operations to do, instead of a quadratic number.
108    //
109    // For calculating the lagrange polynomials, we want to avoid a quadratic
110    // solution. To do so, we need to exploit the structure of the domain
111    // we use: specifically, the fact that we can do fast fourier transforms (FFTs)
112    // in it. (See section 3 of https://eprint.iacr.org/2017/602 for the origin
113    // of most of this exposition).
114    //
115    // Our domain consists of successive powers of a root of unity: 1, ω, ω², ...,
116    // such that ωᵈ = 1. The number of entries in the domain is d, in total.
117    // This is useful, because we can quickly convert a polynomial p(X), represented
118    // as a table of coefficients, into an evaluation table p(ωⁱ) (i ∈ [d]).
119    //
120    // We can also use this for evaluating the lagrange polynomials.
121    // First, note the following equality:
122    //  Lⱼ(X) = d⁻¹∑ᵢ (Xω⁻ʲ)ⁱ
123    // (because the sum over all roots of unity is 0).
124    // Defining:
125    //  Pₓ(Y) := d⁻¹∑ᵢ xⁱ Yⁱ
126    // we can write Lⱼ(x) = Lⱼ(x) = Pₓ(ω⁻ʲ).
127    //
128    // What we want is [Lⱼ(x)] for each j. With this equality, we see that we
129    // can get this by doing an FFT over Pₓ(Y). This will also work even if
130    // the coefficients are group elements, and not scalars.
131    // (We also need to reverse the order of the resulting FFT).
132    //
133    // This is also equivalent to doing an inverse FFT, and not including
134    // the inverse of d in the polynomial.
135    //
136    // The structure of the domain also helps us with the vanishing polynomial, t.
137    // The polynomial (Xᵈ − 1) is 0 everywhere over the domain, and give us
138    // a simple expression for t(X). The evaluations [t(x)xⁱ] can then be obtained
139    // by using simple indexing.
140    //
141    // Ok, that was the explanation, now onto the code.
142    let circuit_size = circuit.num_constraints + circuit.num_instance_variables;
143    let domain: Radix2EvaluationDomain<F> =
144        Radix2EvaluationDomain::new(circuit_size).ok_or_else(|| {
145            anyhow!(
146                "Failed to create evaluation domain size (at least) {}",
147                circuit_size
148            )
149        })?;
150    let domain_size = domain.size();
151    // 0. Check that the phase1 degree is large enough.
152    if phase1.degree < domain_size {
153        return Err(anyhow!(
154            "Phase1 elements not large enough: expected >= {}, found {}",
155            domain_size,
156            phase1.degree
157        ));
158    }
159
160    // 1. Get the lagrange coefficients over [x].
161    // 1.1. Setup a polynomial that's x^i at each coefficient.
162    let mut extracting_poly: Vec<_> = phase1.raw.x_1.iter().copied().take(domain_size).collect();
163    domain.ifft_in_place(&mut extracting_poly);
164    let lagrange_1 = extracting_poly;
165
166    // 2. Do the same for [x]_2.
167    let mut extracting_poly: Vec<_> = phase1.raw.x_2.iter().copied().take(domain_size).collect();
168    domain.ifft_in_place(&mut extracting_poly);
169    let lagrange_2 = extracting_poly;
170
171    // 3. Do the same for [αx].
172    let mut extracting_poly: Vec<_> = phase1
173        .raw
174        .alpha_x_1
175        .iter()
176        .copied()
177        .take(domain_size)
178        .collect();
179    domain.ifft_in_place(&mut extracting_poly);
180    let alpha_lagrange_1 = extracting_poly;
181
182    // 4. Do the same for [βx].
183    let mut extracting_poly: Vec<_> = phase1
184        .raw
185        .beta_x_1
186        .iter()
187        .copied()
188        .take(domain_size)
189        .collect();
190    domain.ifft_in_place(&mut extracting_poly);
191    let beta_lagrange_1 = extracting_poly;
192
193    // 5. Accumulate the p_i polynomials evaluated over [x].
194    // This indexing is copied from ark_groth16/r1cs_to_qap.rs.html#106.
195    // (I spent a full massage chair cycle thinking about this and couldn't figure out
196    // why exactly they do it this way, but mirroring the code we're trying to be
197    // compatible with is a good idea).
198    let qap_num_variables = (circuit.num_instance_variables - 1) + circuit.num_witness_variables;
199    let mut p_1 = vec![G1::zero(); qap_num_variables + 1];
200    // Also take this opportunity to accumulate the raw u_1, v_1, and v_2 polynomials
201    let mut u_1 = vec![G1::zero(); qap_num_variables + 1];
202    let mut v_1 = vec![G1::zero(); qap_num_variables + 1];
203    let mut v_2 = vec![G2::zero(); qap_num_variables + 1];
204
205    // This is where we add the identity matrix block at the end to avoid malleability
206    // shenanigans.
207    {
208        let start = 0;
209        let end = circuit.num_instance_variables;
210        let num_constraints = circuit.num_constraints;
211        // One deviation if you're reading the arkworks code is that we're modifying
212        // the entire p polynomial, and not u (which they call 'a'), but this effectively does
213        // the same thing, because the other polynomials are set to 0 at these points.
214        p_1[start..end]
215            .copy_from_slice(&beta_lagrange_1[(start + num_constraints)..(end + num_constraints)]);
216        // We also modify u in the same way
217        u_1[start..end]
218            .copy_from_slice(&lagrange_1[(start + num_constraints)..(end + num_constraints)])
219    }
220
221    // Could zip here, but this looks cleaner to me.
222    for i in 0..circuit.num_constraints {
223        for &(ref coeff, j) in &circuit.a[i] {
224            p_1[j] += beta_lagrange_1[i] * coeff;
225            u_1[j] += lagrange_1[i] * coeff;
226        }
227        for &(ref coeff, j) in &circuit.b[i] {
228            p_1[j] += alpha_lagrange_1[i] * coeff;
229            v_1[j] += lagrange_1[i] * coeff;
230            v_2[j] += lagrange_2[i] * coeff;
231        }
232        for &(ref coeff, j) in &circuit.c[i] {
233            p_1[j] += lagrange_1[i] * coeff;
234        }
235    }
236
237    // 5. Calculate the t polynomial, evaluated multiplied by successive powers.
238    let t: Vec<_> = (0..(domain_size - 1))
239        .map(|i| phase1.raw.x_1[i + domain_size] - phase1.raw.x_1[i])
240        .collect();
241
242    Ok((
243        ExtraTransitionInformation {
244            u_1,
245            v_1,
246            v_2,
247            p_1: p_1.clone(),
248        },
249        phase2::CRSElements {
250            raw: phase2::RawCRSElements {
251                delta_1: G1::generator(),
252                delta_2: G2::generator(),
253                inv_delta_p_1: p_1,
254                inv_delta_t_1: t,
255            },
256        },
257    ))
258}
259
260/// Combine the outputs from all phases into a proving key.
261///
262/// This proving key also contains a verifying key inherently.
263pub fn combine(
264    circuit: &ConstraintMatrices<F>,
265    phase1out: &Phase1CRSElements,
266    phase2out: &Phase2CRSElements,
267    extra: &ExtraTransitionInformation,
268) -> ProvingKey<Bls12_377> {
269    let vk = VerifyingKey {
270        alpha_g1: phase1out.raw.alpha_1.into_affine(),
271        beta_g2: phase1out.raw.beta_2.into_affine(),
272        gamma_g2: G2::generator().into_affine(),
273        delta_g2: phase2out.raw.delta_2.into_affine(),
274        gamma_abc_g1: G1::normalize_batch(&extra.p_1[..circuit.num_instance_variables]),
275    };
276    ProvingKey {
277        vk,
278        beta_g1: phase1out.raw.beta_1.into_affine(),
279        delta_g1: phase2out.raw.delta_1.into_affine(),
280        a_query: G1::normalize_batch(&extra.u_1),
281        b_g1_query: G1::normalize_batch(&extra.v_1),
282        b_g2_query: G2::normalize_batch(&extra.v_2),
283        h_query: G1::normalize_batch(&phase2out.raw.inv_delta_t_1),
284        l_query: G1::normalize_batch(
285            &phase2out.raw.inv_delta_p_1[circuit.num_instance_variables..],
286        ),
287    }
288}
289
290#[cfg(test)]
291mod test {
292    use ark_ff::One;
293    use ark_groth16::{r1cs_to_qap::LibsnarkReduction, Groth16};
294    use ark_r1cs_std::{
295        eq::EqGadget,
296        fields::fp::FpVar,
297        prelude::{AllocVar, Boolean},
298    };
299    use ark_relations::r1cs::{self, ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef};
300    use ark_snark::SNARK;
301    use rand_core::{OsRng, SeedableRng};
302
303    use crate::single::log::Hashable;
304
305    use super::*;
306
307    #[derive(Clone, Copy, Debug)]
308    struct TestCircuit {
309        x: F,
310        y: F,
311        pub x_plus_y: F,
312    }
313
314    impl ConstraintSynthesizer<F> for TestCircuit {
315        fn generate_constraints(self, cs: ConstraintSystemRef<F>) -> r1cs::Result<()> {
316            // Witnesses
317            let x_var = FpVar::new_witness(cs.clone(), || Ok(self.x))?;
318            let y_var = FpVar::new_witness(cs.clone(), || Ok(self.y))?;
319
320            // Public Inputs
321            let x_plus_y_var = FpVar::new_input(cs, || Ok(self.x_plus_y))?;
322
323            let add_output = x_var + y_var;
324
325            x_plus_y_var.conditional_enforce_equal(&add_output, &Boolean::TRUE)
326        }
327    }
328
329    #[test]
330    fn test_can_generate_keys_through_ceremony() -> anyhow::Result<()> {
331        let circuit = TestCircuit {
332            x: F::one(),
333            y: F::one(),
334            x_plus_y: F::from(2u64),
335        };
336        let cs = ConstraintSystem::new_ref();
337        cs.set_optimization_goal(r1cs::OptimizationGoal::Constraints);
338        cs.set_mode(r1cs::SynthesisMode::Setup);
339        circuit.generate_constraints(cs.clone())?;
340        cs.finalize();
341
342        let matrices = cs
343            .to_matrices()
344            .ok_or_else(|| anyhow!("Failed to generate constraint matrices."))?;
345
346        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1776);
347
348        let degree = circuit_degree(&matrices)?;
349        let phase1root = Phase1CRSElements::root(degree);
350
351        // Doing two contributions to make sure there's not some weird bug there
352        let phase1contribution = Phase1Contribution::make(&mut rng, phase1root.hash(), &phase1root);
353        let phase1contribution = Phase1Contribution::make(
354            &mut rng,
355            phase1contribution.hash(),
356            &phase1contribution.new_elements,
357        );
358
359        let (extra, phase2root) = transition(&phase1contribution.new_elements, &matrices)?;
360
361        let phase2contribution = Phase2Contribution::make(&mut rng, phase2root.hash(), &phase2root);
362        let phase2contribution = Phase2Contribution::make(
363            &mut rng,
364            phase2contribution.hash(),
365            &phase2contribution.new_elements,
366        );
367
368        let pk = combine(
369            &matrices,
370            &phase1contribution.new_elements,
371            &phase2contribution.new_elements,
372            &extra,
373        );
374
375        let proof = Groth16::<Bls12_377, LibsnarkReduction>::prove(&pk, circuit, &mut OsRng)
376            .map_err(|err| anyhow!(err))?;
377
378        let public_inputs = vec![circuit.x_plus_y];
379        let ok = Groth16::<Bls12_377, LibsnarkReduction>::verify(&pk.vk, &public_inputs, &proof)?;
380        assert!(ok);
381
382        Ok(())
383    }
384}