Polynomial Commitments and the IPA
1. Why this chapter exists
A PLONKish proof is, structurally, a list of polynomials together with their evaluations at a few challenge points. The polynomial commitment scheme is what makes the proof short: instead of sending the polynomials, the prover sends group elements plus the evaluations, and the verifier checks the evaluations against the commitments. halo2 uses the inner product argument (IPA) from the Halo paper, which is trustless and recursion-friendly. Two practical reasons this chapter matters: (a) every other proof component eventually feeds into the IPA, so knowing its interface is non-negotiable; (b) the IPA's verifier cost is the dominant verification cost for small circuits.
2. Definitions
Fix the small domain with and a cyclic group of prime order .
Definition (Pedersen vector commitment). Given generators chosen by hashing to the curve (so no one knows discrete logs), a commitment to a vector with blinding is This is computationally binding and perfectly hiding.
Definition (Pedersen polynomial commitment). For a polynomial
, define
. Halo 2's Params
also carry a Lagrange-basis generator set
so the same commitment can be expressed in
either basis without an extra FFT.
Definition (Inner product argument). A succinct argument that for a publicly known and a committed . The argument is rounds and sends group elements; verification time is also after a single multi-scalar multiplication that can be deferred or amortized across many proofs.
Definition (IPA polynomial opening). Opening a polynomial commitment at a point reduces to an inner product check: with . The IPA then certifies the inner product.
Definition (Accumulator). The verifier of the IPA does not finalize the multi-scalar check immediately; it can produce an accumulator (commitment plus claimed scalar ) such that the entire verification reduces to checking . Halo's recursion uses this: the inner circuit "verifies" the previous proof by absorbing the accumulator and the outer verifier checks the combined accumulator at the end. This is the trick that lets halo2 do trustless recursion.
3. The code
3.1 The setup struct Params
/// These are the public parameters for the polynomial commitment scheme.
#[derive(Clone, Debug)]
pub struct Params<C: CurveAffine> {
pub(crate) k: u32,
pub(crate) n: u64,
pub(crate) g: Vec<C>,
pub(crate) g_lagrange: Vec<C>,
pub(crate) w: C,
pub(crate) u: C,
}
impl<C: CurveAffine> Params<C> {
/// 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);
// In src/arithmetic/fields.rs we ensure that usize is at least 32 bits.
let n: u64 = 1 << k;
Params::new(k) deterministically derives the generators by
hashing-to-curve with the personalization "Halo2-Parameters".
Two consequences:
- There is no trusted setup; anyone can rederive
Params(k)and confirm they match by recomputing the hash. - The setup is per-
k: a circuit of degree uses aParams(k')for some .
The struct holds:
g: the generators in coefficient-basis form.g_lagrange: the same generators rotated by the inverse FFT matrix, so that committing a Lagrange-basis polynomial is also one MSM.w: the blinding generator .u: the generator used to absorb the inner product (introduced by Halo's IPA variant; this is the "challenge group element" that lets the prover commit to alongside ).
3.2 The commitment operation
Params::commit is the workhorse:
/// 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: &Polynomial<C::Scalar, Coeff>, r: Blind<C::Scalar>) -> C::Curve {
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.0);
tmp_bases.extend(self.g.iter());
tmp_bases.push(self.w);
best_multiexp::<C>(&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: &Polynomial<C::Scalar, LagrangeCoeff>,
r: Blind<C::Scalar>,
) -> C::Curve {
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.0);
tmp_bases.extend(self.g_lagrange.iter());
tmp_bases.push(self.w);
best_multiexp::<C>(&tmp_scalars, &tmp_bases)
}
/// Generates an empty multiscalar multiplication struct using the
/// appropriate params.
pub fn empty_msm(&self) -> MSM<C> {
MSM::new(self)
}
/// Getter for g generators
pub fn get_g(&self) -> Vec<C> {
self.g.clone()
}
/// Get the circuit size parameter k
pub fn k(&self) -> u32 {
self.k
It is just best_multiexp(values, g) plus a blinding term
r * w. The Lagrange variant uses g_lagrange instead of g.
3.3 The Blind newtype
/// Wrapper type around a blinding factor.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct Blind<F>(pub F);
impl<F: Field> Default for Blind<F> {
fn default() -> Self {
Blind(F::ONE)
}
}
impl<F: Field> Add for Blind<F> {
type Output = Self;
fn add(self, rhs: Blind<F>) -> Self {
Blind(self.0 + rhs.0)
}
}
impl<F: Field> Mul for Blind<F> {
type Output = Self;
fn mul(self, rhs: Blind<F>) -> Self {
Blind(self.0 * rhs.0)
}
}
impl<F: Field> AddAssign for Blind<F> {
fn add_assign(&mut self, rhs: Blind<F>) {
self.0 += rhs.0;
}
}
impl<F: Field> MulAssign for Blind<F> {
fn mul_assign(&mut self, rhs: Blind<F>) {
self.0 *= rhs.0;
}
}
impl<F: Field> AddAssign<F> for Blind<F> {
fn add_assign(&mut self, rhs: F) {
self.0 += rhs;
}
}
impl<F: Field> MulAssign<F> for Blind<F> {
fn mul_assign(&mut self, rhs: F) {
self.0 *= rhs;
}
Blind<F> is a thin wrapper that overloads +, *, etc. so that
the prover can do "blinding arithmetic" with the same expressions
it uses for the underlying polynomials. The Pedersen blinding for
is ; the
Blind newtype enforces that algebra at the type level.
3.4 The IPA prover
use ff::Field;
use rand_core::RngCore;
use super::super::{Coeff, Polynomial};
use super::{Blind, Params};
use crate::arithmetic::{
best_multiexp, compute_inner_product, eval_polynomial, parallelize, CurveAffine,
};
use crate::transcript::{EncodedChallenge, TranscriptWrite};
use group::Curve;
use std::io;
/// 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 URS, if you want to
/// be rigorous.
pub fn create_proof<
C: CurveAffine,
E: EncodedChallenge<C>,
R: RngCore,
T: TranscriptWrite<C, E>,
>(
params: &Params<C>,
mut rng: R,
transcript: &mut T,
p_poly: &Polynomial<C::Scalar, Coeff>,
p_blind: Blind<C::Scalar>,
x_3: C::Scalar,
) -> io::Result<()> {
// We're limited to polynomials of degree n - 1.
assert_eq!(p_poly.len(), params.n as usize);
// Sample a random polynomial (of same degree) that has a root at x_3, first
// by setting all coefficients to random values.
let mut s_poly = (*p_poly).clone();
for coeff in s_poly.iter_mut() {
*coeff = C::Scalar::random(&mut rng);
}
// Evaluate the random polynomial at x_3
let s_at_x3 = eval_polynomial(&s_poly[..], x_3);
// Subtract constant coefficient to get a random polynomial with a root at x_3
s_poly[0] -= &s_at_x3;
// And sample a random blind
let s_poly_blind = Blind(C::Scalar::random(&mut rng));
// Write a commitment to the random polynomial to the transcript
let s_poly_commitment = params.commit(&s_poly, s_poly_blind).to_affine();
transcript.write_point(s_poly_commitment)?;
// Challenge that will ensure that the prover cannot change P but can only
// witness a random polynomial commitment that agrees with P at x_3, with high
// probability.
let xi = *transcript.squeeze_challenge_scalar::<()>();
// Challenge that ensures that the prover did not interfere with the U term
// in their commitments.
let z = *transcript.squeeze_challenge_scalar::<()>();
// We'll be opening `P' = P - [v] G_0 + [ξ] S` to ensure it has a root at
// zero.
let mut p_prime_poly = s_poly * xi + p_poly;
let v = eval_polynomial(&p_prime_poly, x_3);
p_prime_poly[0] -= &v;
let p_prime_blind = s_poly_blind * Blind(xi) + p_blind;
// This accumulates the synthetic blinding factor `f` starting
// with the blinding factor for `P'`.
let mut f = p_prime_blind.0;
// Initialize the vector `p_prime` as the coefficients of the polynomial.
let mut p_prime = p_prime_poly.values;
assert_eq!(p_prime.len(), params.n as usize);
// Initialize the vector `b` as the powers of `x_3`. The inner product of
// `p_prime` and `b` is the evaluation of the polynomial at `x_3`.
let mut b = Vec::with_capacity(1 << params.k);
{
let mut cur = C::Scalar::ONE;
for _ in 0..(1 << params.k) {
b.push(cur);
cur *= &x_3;
}
}
// Initialize the vector `G'` from the URS. We'll be progressively collapsing
// this vector into smaller and smaller vectors until it is of length 1.
let mut g_prime = params.g.clone();
// Perform the inner product argument, round by round.
for j in 0..params.k {
let half = 1 << (params.k - j - 1); // half the length of `p_prime`, `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_j = best_multiexp(&p_prime[half..], &g_prime[0..half]);
let r_j = best_multiexp(&p_prime[0..half], &g_prime[half..]);
let value_l_j = compute_inner_product(&p_prime[half..], &b[0..half]);
let value_r_j = compute_inner_product(&p_prime[0..half], &b[half..]);
let l_j_randomness = C::Scalar::random(&mut rng);
let r_j_randomness = C::Scalar::random(&mut rng);
let l_j = l_j + &best_multiexp(&[value_l_j * &z, l_j_randomness], &[params.u, params.w]);
let r_j = r_j + &best_multiexp(&[value_r_j * &z, r_j_randomness], &[params.u, params.w]);
let l_j = l_j.to_affine();
let r_j = r_j.to_affine();
// Feed L and R into the real transcript
transcript.write_point(l_j)?;
transcript.write_point(r_j)?;
let u_j = *transcript.squeeze_challenge_scalar::<()>();
let u_j_inv = u_j.invert().unwrap(); // TODO, bubble this up
// Collapse `p_prime` and `b`.
// TODO: parallelize
#[allow(clippy::assign_op_pattern)]
for i in 0..half {
p_prime[i] = p_prime[i] + &(p_prime[i + half] * &u_j_inv);
b[i] = b[i] + &(b[i + half] * &u_j);
}
p_prime.truncate(half);
b.truncate(half);
// Collapse `G'`
parallel_generator_collapse(&mut g_prime, u_j);
g_prime.truncate(half);
// Update randomness (the synthetic blinding factor at the end)
f += &(l_j_randomness * &u_j_inv);
f += &(r_j_randomness * &u_j);
}
// We have fully collapsed `p_prime`, `b`, `G'`
assert_eq!(p_prime.len(), 1);
let c = p_prime[0];
transcript.write_scalar(c)?;
transcript.write_scalar(f)?;
Ok(())
}
fn parallel_generator_collapse<C: CurveAffine>(g: &mut [C], challenge: C::Scalar) {
let len = g.len() / 2;
let (g_lo, g_hi) = g.split_at_mut(len);
parallelize(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()) {
tmp.push(g_lo.to_curve() + &(*g_hi * challenge));
}
C::Curve::batch_normalize(&tmp, g_lo);
});
}
The protocol, in pseudocode:
inputs: a (length n), b (length n) with b_i = x^i, blinding r, commitment P = Com(a; r)
for round = 0..log n:
split a, b, G into left/right halves (a_l, a_r), (b_l, b_r), (G_l, G_r)
L = <a_l, G_r> + <a_l, b_r> * U + blinding_l * W
R = <a_r, G_l> + <a_r, b_l> * U + blinding_r * W
write L, R to transcript
squeeze challenge u from transcript
a' = a_l + u^{-1} a_r; b' = b_l + u * b_r; G' = G_l + u * G_r
a, b, G = a', b', G'
output: final scalar a (of length 1) and final blinding
The verifier reconstructs from and the round challenges . The Halo paper's trick is that this reconstruction is itself an MSM with structured scalars, so the verifier can either compute it directly or defer it into an accumulator.
3.5 The IPA verifier
use group::{
ff::{BatchInvert, Field},
Curve,
};
use super::super::Error;
use super::{Params, MSM};
use crate::transcript::{EncodedChallenge, TranscriptRead};
use crate::arithmetic::{best_multiexp, CurveAffine};
/// A guard returned by the verifier
#[derive(Debug, Clone)]
pub struct Guard<'a, C: CurveAffine, E: EncodedChallenge<C>> {
msm: MSM<'a, C>,
neg_c: C::Scalar,
u: Vec<C::Scalar>,
u_packed: Vec<E>,
}
/// An accumulator instance consisting of an evaluation claim and a proof.
#[derive(Debug, Clone)]
pub struct Accumulator<C: CurveAffine, E: EncodedChallenge<C>> {
/// The claimed output of the linear-time polycommit opening protocol
pub g: C,
/// A vector of challenges u_0, ..., u_{k - 1} sampled by the verifier, to
/// be used in computing G'_0.
pub u_packed: Vec<E>,
}
impl<'a, C: CurveAffine, E: EncodedChallenge<C>> Guard<'a, C, E> {
/// Lets caller supply the challenges and obtain an MSM with updated
/// scalars and points.
pub fn use_challenges(mut self) -> MSM<'a, C> {
let s = compute_s(&self.u, self.neg_c);
self.msm.add_to_g_scalars(&s);
self.msm
}
/// Lets caller supply the purported G point and simply appends
/// [-c] G to return an updated MSM.
pub fn use_g(mut self, g: C) -> (MSM<'a, C>, Accumulator<C, E>) {
self.msm.append_term(self.neg_c, g);
let accumulator = Accumulator {
g,
u_packed: self.u_packed,
};
(self.msm, accumulator)
}
/// Computes G = ⟨s, params.g⟩
pub fn compute_g(&self) -> C {
let s = compute_s(&self.u, C::Scalar::ONE);
best_multiexp(&s, &self.msm.params.g).to_affine()
}
}
/// Checks to see if the proof represented within `transcript` is valid, and a
/// point `x` that the polynomial commitment `P` opens purportedly to the value
/// `v`. The provided `msm` should evaluate to the commitment `P` being opened.
pub fn verify_proof<'a, C: CurveAffine, E: EncodedChallenge<C>, T: TranscriptRead<C, E>>(
params: &'a Params<C>,
mut msm: MSM<'a, C>,
transcript: &mut T,
x: C::Scalar,
v: C::Scalar,
) -> Result<Guard<'a, C, E>, Error> {
let k = params.k as usize;
// P' = P - [v] G_0 + [ξ] S
msm.add_constant_term(-v); // add [-v] G_0
let s_poly_commitment = transcript.read_point().map_err(|_| Error::OpeningError)?;
let xi = *transcript.squeeze_challenge_scalar::<()>();
msm.append_term(xi, s_poly_commitment);
let z = *transcript.squeeze_challenge_scalar::<()>();
let mut rounds = vec![];
for _ in 0..k {
// Read L and R from the proof and write them to the transcript
let l = transcript.read_point().map_err(|_| Error::OpeningError)?;
let r = transcript.read_point().map_err(|_| Error::OpeningError)?;
let u_j_packed = transcript.squeeze_challenge();
let u_j = *u_j_packed.as_challenge_scalar::<()>();
rounds.push((l, r, u_j, /* to be inverted */ u_j, u_j_packed));
}
rounds
.iter_mut()
.map(|&mut (_, _, _, ref mut u_j, _)| u_j)
.batch_invert();
// This is the left-hand side of the verifier equation.
// P' + \sum([u_j^{-1}] L_j) + \sum([u_j] R_j)
let mut u = Vec::with_capacity(k);
let mut u_packed: Vec<E> = Vec::with_capacity(k);
for (l, r, u_j, u_j_inv, u_j_packed) in rounds {
msm.append_term(u_j_inv, l);
msm.append_term(u_j, r);
u.push(u_j);
u_packed.push(u_j_packed);
}
// Our goal is to check that the left hand side of the verifier
// equation
// P' + \sum([u_j^{-1}] L_j) + \sum([u_j] R_j)
// equals (given b = \mathbf{b}_0, and the prover's values c, f),
// the right-hand side
// = [c] (G'_0 + [b * z] U) + [f] W
// Subtracting the right-hand side from both sides we get
// P' + \sum([u_j^{-1}] L_j) + \sum([u_j] R_j)
// + [-c] G'_0 + [-cbz] U + [-f] W
// = 0
//
// Note that the guard returned from this function does not include
// the [-c]G'_0 term.
let c = transcript.read_scalar().map_err(|_| Error::SamplingError)?;
let neg_c = -c;
let f = transcript.read_scalar().map_err(|_| Error::SamplingError)?;
let b = compute_b(x, &u);
msm.add_to_u_scalar(neg_c * &b * &z);
msm.add_to_w_scalar(-f);
let guard = Guard {
msm,
neg_c,
u,
u_packed,
};
Ok(guard)
}
/// Computes $\prod\limits_{i=0}^{k-1} (1 + u_{k - 1 - i} x^{2^i})$.
fn compute_b<F: Field>(x: F, u: &[F]) -> F {
let mut tmp = F::ONE;
let mut cur = x;
for u_j in u.iter().rev() {
tmp *= F::ONE + &(*u_j * &cur);
cur *= cur;
}
tmp
}
/// Computes the coefficients of $g(X) = \prod\limits_{i=0}^{k-1} (1 + u_{k - 1 - i} X^{2^i})$.
fn compute_s<F: Field>(u: &[F], init: F) -> Vec<F> {
assert!(!u.is_empty());
let mut v = vec![F::ZERO; 1 << u.len()];
v[0] = init;
for (len, u_j) in u.iter().rev().enumerate().map(|(i, u_j)| (1 << i, u_j)) {
let (left, right) = v.split_at_mut(len);
let right = &mut right[0..len];
right.copy_from_slice(left);
for v in right {
*v *= u_j;
}
}
v
}
The verifier returns a Guard rather than a boolean. The Guard
holds the pending checks; calling Guard::use_challenges runs the
final MSM. The BatchVerifier aggregates many Guards into one
MSM, dropping per-proof cost from to amortized.
3.6 The MSM accumulator
use super::Params;
use crate::arithmetic::{best_multiexp, CurveAffine};
use ff::Field;
use group::Group;
use std::collections::BTreeMap;
/// A multiscalar multiplication in the polynomial commitment scheme
#[derive(Debug, Clone)]
pub struct MSM<'a, C: CurveAffine> {
pub(crate) params: &'a Params<C>,
g_scalars: Option<Vec<C::Scalar>>,
w_scalar: Option<C::Scalar>,
u_scalar: Option<C::Scalar>,
// x-coordinate -> (scalar, y-coordinate)
other: BTreeMap<C::Base, (C::Scalar, C::Base)>,
}
impl<'a, C: CurveAffine> MSM<'a, C> {
/// Create a new, empty MSM using the provided parameters.
pub fn new(params: &'a Params<C>) -> Self {
let g_scalars = None;
let w_scalar = None;
let u_scalar = None;
let other = BTreeMap::new();
MSM {
params,
g_scalars,
w_scalar,
u_scalar,
other,
}
}
/// Add another multiexp into this one
pub fn add_msm(&mut self, other: &Self) {
for (x, (scalar, y)) in other.other.iter() {
self.other
.entry(*x)
.and_modify(|(our_scalar, our_y)| {
if our_y == y {
*our_scalar += *scalar;
} else {
assert!(*our_y == -*y);
*our_scalar -= *scalar;
}
})
.or_insert((*scalar, *y));
}
if let Some(g_scalars) = &other.g_scalars {
self.add_to_g_scalars(g_scalars);
}
if let Some(w_scalar) = &other.w_scalar {
self.add_to_w_scalar(*w_scalar);
}
if let Some(u_scalar) = &other.u_scalar {
self.add_to_u_scalar(*u_scalar);
}
}
/// Add arbitrary term (the scalar and the point)
pub fn append_term(&mut self, scalar: C::Scalar, point: C) {
if !bool::from(point.is_identity()) {
let xy = point.coordinates().unwrap();
let x = *xy.x();
let y = *xy.y();
self.other
.entry(x)
.and_modify(|(our_scalar, our_y)| {
if *our_y == y {
*our_scalar += scalar;
} else {
assert!(*our_y == -y);
*our_scalar -= scalar;
}
})
.or_insert((scalar, y));
}
}
/// Add a value to the first entry of `g_scalars`.
pub fn add_constant_term(&mut self, constant: C::Scalar) {
if let Some(g_scalars) = self.g_scalars.as_mut() {
g_scalars[0] += &constant;
} else {
let mut g_scalars = vec![C::Scalar::ZERO; self.params.n as usize];
g_scalars[0] += &constant;
self.g_scalars = Some(g_scalars);
}
}
/// Add a vector of scalars to `g_scalars`. This function will panic if the
/// caller provides a slice of scalars that is not of length `params.n`.
pub fn add_to_g_scalars(&mut self, scalars: &[C::Scalar]) {
assert_eq!(scalars.len(), self.params.n as usize);
if let Some(g_scalars) = &mut self.g_scalars {
for (g_scalar, scalar) in g_scalars.iter_mut().zip(scalars.iter()) {
*g_scalar += scalar;
}
} else {
self.g_scalars = Some(scalars.to_vec());
}
}
/// Add to `w_scalar`
pub fn add_to_w_scalar(&mut self, scalar: C::Scalar) {
self.w_scalar = self.w_scalar.map_or(Some(scalar), |a| Some(a + &scalar));
}
/// Add to `u_scalar`
pub fn add_to_u_scalar(&mut self, scalar: C::Scalar) {
self.u_scalar = self.u_scalar.map_or(Some(scalar), |a| Some(a + &scalar));
}
/// Scale all scalars in the MSM by some scaling factor
pub fn scale(&mut self, factor: C::Scalar) {
if let Some(g_scalars) = &mut self.g_scalars {
for g_scalar in g_scalars {
*g_scalar *= &factor;
}
}
for other in self.other.values_mut() {
other.0 *= factor;
}
self.w_scalar = self.w_scalar.map(|a| a * &factor);
self.u_scalar = self.u_scalar.map(|a| a * &factor);
}
/// Perform multiexp and check that it results in zero
pub fn eval(self) -> bool {
let len = self.g_scalars.as_ref().map(|v| v.len()).unwrap_or(0)
+ self.w_scalar.map(|_| 1).unwrap_or(0)
+ self.u_scalar.map(|_| 1).unwrap_or(0)
+ self.other.len();
let mut scalars: Vec<C::Scalar> = Vec::with_capacity(len);
let mut bases: Vec<C> = Vec::with_capacity(len);
scalars.extend(self.other.values().map(|(scalar, _)| scalar));
bases.extend(
self.other
.iter()
.map(|(x, (_, y))| C::from_xy(*x, *y).unwrap()),
);
if let Some(w_scalar) = self.w_scalar {
scalars.push(w_scalar);
bases.push(self.params.w);
}
if let Some(u_scalar) = self.u_scalar {
scalars.push(u_scalar);
bases.push(self.params.u);
}
if let Some(g_scalars) = &self.g_scalars {
scalars.extend(g_scalars);
bases.extend(self.params.g.iter());
}
assert_eq!(scalars.len(), len);
bool::from(best_multiexp(&scalars, &bases).is_identity())
}
}
#[cfg(test)]
mod tests {
use crate::poly::commitment::{Params, MSM};
use group::Curve;
use pasta_curves::{arithmetic::CurveAffine, EpAffine, Fp, Fq};
#[test]
fn msm_arithmetic() {
let base = EpAffine::from_xy(-Fp::one(), Fp::from(2)).unwrap();
let base_viol = (base + base).to_affine();
let params = Params::new(4);
let mut a: MSM<EpAffine> = MSM::new(¶ms);
a.append_term(Fq::one(), base);
// a = [1] P
assert!(!a.clone().eval());
a.append_term(Fq::one(), base);
// a = [1+1] P
assert!(!a.clone().eval());
a.append_term(-Fq::one(), base_viol);
// a = [1+1] P + [-1] 2P
assert!(a.clone().eval());
let b = a.clone();
// Append a point that is the negation of an existing one.
a.append_term(Fq::from(4), -base);
// a = [1+1-4] P + [-1] 2P
assert!(!a.clone().eval());
a.append_term(Fq::from(2), base_viol);
// a = [1+1-4] P + [-1+2] 2P
assert!(a.clone().eval());
// Add two MSMs with common bases.
a.scale(Fq::from(3));
a.add_msm(&b);
// a = [3*(1+1)+(1+1-4)] P + [3*(-1)+(-1+2)] 2P
assert!(a.clone().eval());
let mut c: MSM<EpAffine> = MSM::new(¶ms);
c.append_term(Fq::from(2), base);
c.append_term(Fq::one(), -base_viol);
// c = [2] P + [1] (-2P)
assert!(c.clone().eval());
// Add two MSMs with bases that differ only in sign.
a.add_msm(&c);
assert!(a.eval());
}
}
MSM is a "deferred MSM" type: the verifier records
pairs and runs the actual MSM only
when forced. This is what enables BatchVerifier.
4. Failure modes
- Reusing
Paramsacrosskvalues.Params(k)is only valid for circuits up to size . Reusing a smallerParamsfor a larger circuit truncates the witness silently; reusing a largerParamsworks but wastes setup. - Forgetting to blind. Calling
params.commit(values, Blind::default())(i.e. with ) makes the commitment binding but not hiding. The IPA proof itself becomes non-zero-knowledge. Always pass a freshly randomBlindfor any witness polynomial. - Mismatched basis.
commitandcommit_lagrangeuse different generator sets. Passing a Lagrange-basis polynomial tocommit(or vice versa) gives you a commitment to a different polynomial. ThePolynomial<F, B>type tag prevents this at the boundary. - Forgetting
Guard::use_challenges. AGuardthat is dropped without being finalized silently passes verification. Either calluse_challengesor combine it into aBatchVerifier.
5. Spec pointers
- Halo paper, eprint 2019/1021: the original construction of the IPA with the accumulator trick.
- Bowe and Hopwood, "Faster batch forgery identification": batch verification of IPA.
- Bunz et al., "Bulletproofs": the inner-product argument's lineage and the recursive halving trick.
6. Exercises
-
Open
halo2_proofs/src/poly/commitment.rsand find the personalization string used byParams::new. What would change if you altered it? -
The IPA proof contains field / group elements. Count them in the prover code: trace one
for roundloop inhalo2_proofs/src/poly/commitment/prover.rsand identify which lines writeLandRto the transcript. -
Run the
msmbench:cargo bench -p halo2_proofs --bench msmHow does single-proof verification cost compare with the
plonkbench's amortized batched cost? -
Implement a
print_sizehelper that, given a proof byte slice, printsn_round_messages = (len - constant_part) / point_size. Use it on a proof produced by thesimple-example.
Answers in the code
- Exercise 1: the personalization is the byte string
"Halo2-Parameters"passed toC::CurveExt::hash_to_curve. Changing it would shift every generator; all existing verifying keys would be invalidated. - Exercise 2: the
LandRwrites are in the inner loop ofcreate_proofinhalo2_proofs/src/poly/commitment/prover.rs.
7. Further reading
- The Halo 2 Book "Inner product argument" chapter goes through the math in linear order, matching the variable names used in the code.
- The Plonk2 / halo2 talk at Zcon3 covers the recursion-friendly aspect of this commitment scheme.