//! This module contains an implementation of the polynomial commitment scheme //! described in the [Halo][halo] paper. //! //! [halo]: https://eprint.iacr.org/2019/1021 use crate::arithmetic::{ best_fft, best_multiexp, compute_inner_product, get_challenge_scalar, parallelize, Challenge, Curve, CurveAffine, Field, }; use crate::transcript::Hasher; /// This is a proof object for the polynomial commitment scheme opening. #[derive(Debug, Clone)] pub struct OpeningProof { fork: u8, rounds: Vec<(C, C)>, delta: C, z1: C::Scalar, z2: C::Scalar, } /// These are the public parameters for the polynomial commitment scheme. #[derive(Debug)] pub struct Params { pub(crate) k: u32, pub(crate) n: u64, pub(crate) g: Vec, pub(crate) g_lagrange: Vec, pub(crate) h: C, } impl Params { /// Initializes parameters for the curve, given a random oracle to draw /// points from. pub fn new>(k: u32) -> Self { // This is usually a limitation on the curve, but we also want 32-bit // architectures to be supported. assert!(k < 32); // No goofy hardware please. assert!(core::mem::size_of::() >= 4); let n: u64 = 1 << k; let g = { let hasher = &H::init(C::Base::zero()); let mut g = Vec::with_capacity(n as usize); g.resize(n as usize, C::zero()); parallelize(&mut g, move |g, start| { let mut cur_value = C::Base::from(start as u64); for g in g.iter_mut() { let mut hasher = hasher.clone(); hasher.absorb(cur_value); cur_value += &C::Base::one(); loop { let x = hasher.squeeze().to_bytes(); let p = C::from_bytes(&x); if bool::from(p.is_some()) { *g = p.unwrap(); break; } } } }); g }; // Let's evaluate all of the Lagrange basis polynomials // using an inverse FFT. let mut alpha_inv = C::Scalar::ROOT_OF_UNITY_INV; for _ in k..C::Scalar::S { alpha_inv = alpha_inv.square(); } let mut g_lagrange_projective = g.iter().map(|g| g.to_projective()).collect::>(); best_fft(&mut g_lagrange_projective, alpha_inv, k); let minv = C::Scalar::TWO_INV.pow_vartime(&[k as u64, 0, 0, 0]); parallelize(&mut g_lagrange_projective, |g, _| { for g in g.iter_mut() { *g *= minv; } }); let g_lagrange = { let mut g_lagrange = vec![C::zero(); n as usize]; parallelize(&mut g_lagrange, |g_lagrange, starts| { C::Projective::batch_to_affine( &g_lagrange_projective[starts..(starts + g_lagrange.len())], g_lagrange, ); }); drop(g_lagrange_projective); g_lagrange }; let h = { let mut hasher = H::init(C::Base::zero()); hasher.absorb(-C::Base::one()); let x = hasher.squeeze().to_bytes(); let p = C::from_bytes(&x); p.unwrap() }; Params { k, n, g, g_lagrange, h, } } /// This computes a commitment to a polynomial described by the provided /// slice of coefficients. The commitment will be blinded by the blinding /// factor `r`. pub fn commit(&self, poly: &[C::Scalar], r: C::Scalar) -> C::Projective { let mut tmp_scalars = Vec::with_capacity(poly.len() + 1); let mut tmp_bases = Vec::with_capacity(poly.len() + 1); tmp_scalars.extend(poly.iter()); tmp_scalars.push(r); tmp_bases.extend(self.g.iter()); tmp_bases.push(self.h); best_multiexp::(&tmp_scalars, &tmp_bases) } /// This commits to a polynomial using its evaluations over the $2^k$ size /// evaluation domain. The commitment will be blinded by the blinding factor /// `r`. pub fn commit_lagrange(&self, poly: &[C::Scalar], r: C::Scalar) -> C::Projective { let mut tmp_scalars = Vec::with_capacity(poly.len() + 1); let mut tmp_bases = Vec::with_capacity(poly.len() + 1); tmp_scalars.extend(poly.iter()); tmp_scalars.push(r); tmp_bases.extend(self.g_lagrange.iter()); tmp_bases.push(self.h); best_multiexp::(&tmp_scalars, &tmp_bases) } /// Create a polynomial commitment opening proof for the polynomial defined /// by the coefficients `px`, the blinding factor `blind` used for the /// polynomial commitment, and the point `x` that the polynomial is /// evaluated at. /// /// This function will panic if the provided polynomial is too large with /// respect to the polynomial commitment parameters. /// /// **Important:** This function assumes that the provided `transcript` has /// already seen the common inputs: the polynomial commitment P, the claimed /// opening v, and the point x. It's probably also nice for the transcript /// to have seen the elliptic curve description and the SRS, if you want to /// be rigorous. pub fn create_proof>( &self, transcript: &mut H, px: &[C::Scalar], mut blind: C::Scalar, x: C::Scalar, ) -> Result, ()> { // We're limited to polynomials of degree n - 1. assert!(px.len() <= self.n as usize); let mut fork = 0; // TODO: remove this hack and force the caller to deal with it loop { let mut transcript = transcript.clone(); transcript.absorb(C::Base::from_u64(fork as u64)); let u_x = transcript.squeeze(); // y^2 = x^3 + B let u_y2 = u_x.square() * &u_x + &C::b(); let u_y = u_y2.deterministic_sqrt(); if u_y.is_none() { fork += 1; } else { break; } } transcript.absorb(C::Base::from_u64(fork as u64)); // Compute U let u = { let u_x = transcript.squeeze(); // y^2 = x^3 + B let u_y2 = u_x.square() * &u_x + &C::b(); let u_y = u_y2.deterministic_sqrt().unwrap(); C::from_xy(u_x, u_y).unwrap() }; // Initialize the vector `a` as the coefficients of the polynomial, // rounding up to the parameters. let mut a = px.to_vec(); a.resize(self.n as usize, C::Scalar::zero()); // Initialize the vector `b` as the powers of `x`. The inner product of // `a` and `b` is the evaluation of the polynomial at `x`. let mut b = Vec::with_capacity(1 << self.k); { let mut cur = C::Scalar::one(); for _ in 0..(1 << self.k) { b.push(cur); cur *= &x; } } // Initialize the vector `G` from the SRS. We'll be progressively // collapsing this vector into smaller and smaller vectors until it is // of length 1. let mut g = self.g.clone(); // Perform the inner product argument, round by round. let mut rounds = Vec::with_capacity(self.k as usize); for k in (1..=self.k).rev() { let half = 1 << (k - 1); // half the length of `a`, `b`, `G` // Compute L, R // // TODO: If we modify multiexp to take "extra" bases, we could speed // this piece up a bit by combining the multiexps. let l = best_multiexp(&a[0..half], &g[half..]); let r = best_multiexp(&a[half..], &g[0..half]); let value_l = compute_inner_product(&a[0..half], &b[half..]); let value_r = compute_inner_product(&a[half..], &b[0..half]); let mut l_randomness = C::Scalar::random(); let r_randomness = C::Scalar::random(); let l = l + &best_multiexp(&[value_l, l_randomness], &[u, self.h]); let r = r + &best_multiexp(&[value_r, r_randomness], &[u, self.h]); let mut l = l.to_affine(); let r = r.to_affine(); let challenge = loop { // We'll fork the transcript and adjust our randomness // until the challenge is a square. let mut transcript = transcript.clone(); // We expect these to not be points at infinity due to the randomness. let (l_x, l_y) = l.get_xy().unwrap(); let (r_x, r_y) = r.get_xy().unwrap(); // Feed L and R into the cloned transcript... transcript.absorb(l_x); transcript.absorb(l_y); transcript.absorb(r_x); transcript.absorb(r_y); // ... and get the squared challenge. let challenge_sq_packed = transcript.squeeze().get_lower_128(); let challenge_sq: C::Scalar = get_challenge_scalar(Challenge(challenge_sq_packed)); // There might be no square root, in which case we'll fork the // transcript. let challenge = challenge_sq.deterministic_sqrt(); if let Some(challenge) = challenge { break challenge; } else { // Try again, with slightly different randomness l = (l + self.h).to_affine(); l_randomness += &C::Scalar::one(); } }; // Challenge is unlikely to be zero. let challenge_inv = challenge.invert().unwrap(); let challenge_sq_inv = challenge_inv.square(); let challenge_sq = challenge.square(); // Feed L and R into the real transcript let (l_x, l_y) = l.get_xy().unwrap(); let (r_x, r_y) = r.get_xy().unwrap(); transcript.absorb(l_x); transcript.absorb(l_y); transcript.absorb(r_x); transcript.absorb(r_y); // And obtain the challenge, even though we already have it, since // squeezing affects the transcript. { let challenge_sq_packed = transcript.squeeze().get_lower_128(); let challenge_sq_expected = get_challenge_scalar(Challenge(challenge_sq_packed)); assert_eq!(challenge_sq, challenge_sq_expected); } // Done with this round. rounds.push((l, r)); // Collapse `a` and `b`. // TODO: parallelize for i in 0..half { a[i] = (a[i] * &challenge) + &(a[i + half] * &challenge_inv); b[i] = (b[i] * &challenge_inv) + &(b[i + half] * &challenge); } a.truncate(half); b.truncate(half); // Collapse `G` parallel_generator_collapse(&mut g, challenge, challenge_inv); g.truncate(half); // Update randomness (the synthetic blinding factor at the end) blind += &(l_randomness * &challenge_sq); blind += &(r_randomness * &challenge_sq_inv); } // We have fully collapsed `a`, `b`, `G` assert_eq!(a.len(), 1); let a = a[0]; assert_eq!(b.len(), 1); let b = b[0]; assert_eq!(g.len(), 1); let g = g[0]; // Random nonces for the zero-knowledge opening let d = C::Scalar::random(); let s = C::Scalar::random(); let delta = best_multiexp(&[d, d * &b, s], &[g, u, self.h]).to_affine(); let (delta_x, delta_y) = delta.get_xy().unwrap(); // Feed delta into the transcript transcript.absorb(delta_x); transcript.absorb(delta_y); // Obtain the challenge c. let c_packed = transcript.squeeze().get_lower_128(); let c: C::Scalar = get_challenge_scalar(Challenge(c_packed)); // Compute z1 and z2 as described in the Halo paper. let z1 = a * &c + &d; let z2 = c * &blind + &s; Ok(OpeningProof { fork, rounds, delta, z1, z2, }) } /// Checks to see if an [`OpeningProof`] is valid given the current /// `transcript`, and a point `x` that the polynomial commitment `p` opens /// purportedly to the value `v`. pub fn verify_proof>( &self, proof: &OpeningProof, transcript: &mut H, x: C::Scalar, p: &C, v: C::Scalar, ) -> bool { // Check for well-formedness if proof.rounds.len() != self.k as usize { return false; } transcript.absorb(C::Base::from_u64(proof.fork as u64)); // Compute U let u = { let u_x = transcript.squeeze(); // y^2 = x^3 + B let u_y2 = u_x.square() * &u_x + &C::b(); let u_y = u_y2.deterministic_sqrt(); if u_y.is_none() { return false; } let u_y = u_y.unwrap(); C::from_xy(u_x, u_y).unwrap() }; let mut extra_scalars = Vec::with_capacity(proof.rounds.len() * 2 + 4 + self.n as usize); let mut extra_bases = Vec::with_capacity(proof.rounds.len() * 2 + 4 + self.n as usize); // Data about the challenges from each of the rounds. let mut challenges = Vec::with_capacity(proof.rounds.len()); let mut challenges_inv = Vec::with_capacity(proof.rounds.len()); let mut challenges_sq = Vec::with_capacity(proof.rounds.len()); let mut allinv = Field::one(); for round in &proof.rounds { // Feed L and R into the transcript. let l = round.0.get_xy(); let r = round.1.get_xy(); if bool::from(l.is_none() | r.is_none()) { return false; } let l = l.unwrap(); let r = r.unwrap(); transcript.absorb(l.0); transcript.absorb(l.1); transcript.absorb(r.0); transcript.absorb(r.1); let challenge_sq_packed = transcript.squeeze().get_lower_128(); let challenge_sq: C::Scalar = get_challenge_scalar(Challenge(challenge_sq_packed)); let challenge = challenge_sq.deterministic_sqrt(); if challenge.is_none() { // We didn't sample a square. return false; } let challenge = challenge.unwrap(); let challenge_inv = challenge.invert(); if bool::from(challenge_inv.is_none()) { // We sampled zero for some reason, unlikely to happen by // chance. return false; } let challenge_inv = challenge_inv.unwrap(); allinv *= challenge_inv; let challenge_sq_inv = challenge_inv.square(); extra_scalars.push(challenge_sq); extra_bases.push(round.0); extra_scalars.push(challenge_sq_inv); extra_bases.push(round.1); challenges.push(challenge); challenges_inv.push(challenge_inv); challenges_sq.push(challenge_sq); } let delta = proof.delta.get_xy(); if bool::from(delta.is_none()) { return false; } let delta = delta.unwrap(); // Feed delta into the transcript transcript.absorb(delta.0); transcript.absorb(delta.1); // Get the challenge `c` let c_packed = transcript.squeeze().get_lower_128(); let c: C::Scalar = get_challenge_scalar(Challenge(c_packed)); // Check // [c] P + [c * v] U + [c] sum(L_i * u_i^2) + [c] sum(R_i * u_i^-2) + delta - [z1] G - [z1 * b] U - [z2] H // = 0 for scalar in &mut extra_scalars { *scalar *= &c; } let b = compute_b(x, &challenges, &challenges_inv); let neg_z1 = -proof.z1; // [c] P extra_bases.push(*p); extra_scalars.push(c); // [c * v] U - [z1 * b] U extra_bases.push(u); extra_scalars.push((c * &v) + &(neg_z1 * &b)); // delta extra_bases.push(proof.delta); extra_scalars.push(Field::one()); // - [z2] H extra_bases.push(self.h); extra_scalars.push(-proof.z2); // - [z1] G extra_bases.extend(&self.g); let mut s = compute_s(&challenges_sq, allinv); // TODO: parallelize for s in &mut s { *s *= &neg_z1; } extra_scalars.extend(s); bool::from(best_multiexp(&extra_scalars, &extra_bases).is_zero()) } } fn compute_b(x: F, challenges: &[F], challenges_inv: &[F]) -> F { assert!(!challenges.is_empty()); assert_eq!(challenges.len(), challenges_inv.len()); if challenges.len() == 1 { *challenges_inv.last().unwrap() + *challenges.last().unwrap() * x } else { (*challenges_inv.last().unwrap() + *challenges.last().unwrap() * x) * compute_b( x.square(), &challenges[0..(challenges.len() - 1)], &challenges_inv[0..(challenges.len() - 1)], ) } } // TODO: parallelize fn compute_s(challenges_sq: &[F], allinv: F) -> Vec { let lg_n = challenges_sq.len(); let n = 1 << lg_n; let mut s = Vec::with_capacity(n); s.push(allinv); for i in 1..n { let lg_i = (32 - 1 - (i as u32).leading_zeros()) as usize; let k = 1 << lg_i; let u_lg_i_sq = challenges_sq[(lg_n - 1) - lg_i]; s.push(s[i - k] * u_lg_i_sq); } s } fn parallel_generator_collapse( g: &mut [C], challenge: C::Scalar, challenge_inv: C::Scalar, ) { let len = g.len() / 2; let (mut g_lo, g_hi) = g.split_at_mut(len); parallelize(&mut g_lo, |g_lo, start| { let g_hi = &g_hi[start..]; let mut tmp = Vec::with_capacity(g_lo.len()); for (g_lo, g_hi) in g_lo.iter().zip(g_hi.iter()) { // TODO: could use multiexp tmp.push(((*g_lo) * challenge_inv) + &((*g_hi) * challenge)); } C::Projective::batch_to_affine(&tmp, g_lo); }); } #[test] fn test_commit_lagrange() { const K: u32 = 6; use crate::arithmetic::{EpAffine, Fp, Fq}; use crate::transcript::DummyHash; let params = Params::::new::>(K); let a = (0..(1 << K)).map(|l| Fq::from(l)).collect::>(); let mut b = a.clone(); let mut alpha = Fq::ROOT_OF_UNITY; for _ in K..Fq::S { alpha = alpha.square(); } best_fft(&mut b, alpha, K); assert_eq!(params.commit(&a, alpha), params.commit_lagrange(&b, alpha)); } #[test] fn test_opening_proof() { const K: u32 = 6; use crate::arithmetic::{eval_polynomial, EpAffine, Fp, Fq}; use crate::transcript::DummyHash; let params = Params::::new::>(K); let px = (0..(1 << K)) .map(|l| Fq::from(l + 1) * Fq::ZETA) .collect::>(); let blind = Fq::random(); let p = params.commit(&px, blind).to_affine(); let mut transcript = DummyHash::init(Field::one()); let (p_x, p_y) = p.get_xy().unwrap(); transcript.absorb(p_x); transcript.absorb(p_y); let x_packed = transcript.squeeze().get_lower_128(); let x: Fq = get_challenge_scalar(Challenge(x_packed)); // Evaluate the polynomial let v = eval_polynomial(&px, x); transcript.absorb(Fp::from_bytes(&v.to_bytes()).unwrap()); // unlikely to fail since p ~ q loop { let mut transcript_dup = transcript.clone(); let opening_proof = params.create_proof(&mut transcript, &px, blind, x); if opening_proof.is_err() { transcript = transcript_dup; transcript.absorb(Field::one()); } else { let opening_proof = opening_proof.unwrap(); assert!(params.verify_proof(&opening_proof, &mut transcript_dup, x, &p, v)); break; } } }