Skip to main content

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 O(logn)O(\log n) 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 HH with n=2kn = 2^k and a cyclic group G\mathbb{G} of prime order qq.

Definition (Pedersen vector commitment). Given generators G0,,Gn1,WGG_0, \dots, G_{n-1}, W \in \mathbb{G} chosen by hashing to the curve (so no one knows discrete logs), a commitment to a vector aFqn\vec a \in \mathbb{F}_q^n with blinding rFqr \in \mathbb{F}_q is Com(a;r)=i=0n1aiGi+rWG.\mathsf{Com}(\vec a; r) = \sum_{i=0}^{n-1} a_i G_i + r W \in \mathbb{G}. This is computationally binding and perfectly hiding.

Definition (Pedersen polynomial commitment). For a polynomial p(X)=aiXip(X) = \sum a_i X^i, define Com(p;r)=Com(a;r)\mathsf{Com}(p; r) = \mathsf{Com}(\vec a; r). Halo 2's Params also carry a Lagrange-basis generator set L0,,Ln1L_0, \dots, L_{n-1} so the same commitment can be expressed in either basis without an extra FFT.

Definition (Inner product argument). A succinct argument that a,b=c\langle \vec a, \vec b \rangle = c for a publicly known b\vec b and a committed a\vec a. The argument is O(logn)O(\log n) rounds and sends 2logn+22 \log n + 2 group elements; verification time is also O(logn)O(\log n) after a single multi-scalar multiplication that can be deferred or amortized across many proofs.

Definition (IPA polynomial opening). Opening a polynomial commitment Com(p)\mathsf{Com}(p) at a point xx reduces to an inner product check: p(x)=a,bp(x) = \langle \vec a, \vec b \rangle with bi=xib_i = x^i. 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 GGG' \in \mathbb{G} plus claimed scalar cFqc' \in \mathbb{F}_q) such that the entire verification reduces to checking G=iciGiG' = \sum_i c'_i G_i. 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

halo2_proofs/src/poly/commitment.rs

/// 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 2k2^{k} uses a Params(k') for some kkk' \geq k.

The struct holds:

  • g: the nn generators in coefficient-basis form.
  • g_lagrange: the same nn generators rotated by the inverse FFT matrix, so that committing a Lagrange-basis polynomial is also one MSM.
  • w: the blinding generator WW.
  • 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 cc alongside a\vec a).

3.2 The commitment operation

Params::commit is the workhorse:

halo2_proofs/src/poly/commitment.rs

/// 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

halo2_proofs/src/poly/commitment.rs
/// 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 αp1+βp2\alpha p_1 + \beta p_2 is αr1+βr2\alpha r_1 + \beta r_2; the Blind newtype enforces that algebra at the type level.

3.4 The IPA prover

halo2_proofs/src/poly/commitment/prover.rs
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 GG' from (G0,,Gn1)(G_0, \dots, G_{n-1}) and the round challenges u0,,uk1u_0, \dots, u_{k-1}. 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

halo2_proofs/src/poly/commitment/verifier.rs
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 O(n)O(n) to O(logn)O(\log n) amortized.

3.6 The MSM accumulator

halo2_proofs/src/poly/commitment/msm.rs
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(&params);
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(&params);
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 (base,scalar)(\text{base}, \text{scalar}) pairs and runs the actual MSM only when forced. This is what enables BatchVerifier.

4. Failure modes

  • Reusing Params across k values. Params(k) is only valid for circuits up to size 2k2^k. Reusing a smaller Params for a larger circuit truncates the witness silently; reusing a larger Params works but wastes setup.
  • Forgetting to blind. Calling params.commit(values, Blind::default()) (i.e. with r=0r = 0) makes the commitment binding but not hiding. The IPA proof itself becomes non-zero-knowledge. Always pass a freshly random Blind for any witness polynomial.
  • Mismatched basis. commit and commit_lagrange use different generator sets. Passing a Lagrange-basis polynomial to commit (or vice versa) gives you a commitment to a different polynomial. The Polynomial<F, B> type tag prevents this at the boundary.
  • Forgetting Guard::use_challenges. A Guard that is dropped without being finalized silently passes verification. Either call use_challenges or combine it into a BatchVerifier.

5. Spec pointers

6. Exercises

  1. Open halo2_proofs/src/poly/commitment.rs and find the personalization string used by Params::new. What would change if you altered it?

  2. The IPA proof contains 2log2n+22 \log_2 n + 2 field / group elements. Count them in the prover code: trace one for round loop in halo2_proofs/src/poly/commitment/prover.rs and identify which lines write L and R to the transcript.

  3. Run the msm bench:

    cargo bench -p halo2_proofs --bench msm

    How does single-proof verification cost compare with the plonk bench's amortized batched cost?

  4. Implement a print_size helper that, given a proof byte slice, prints n_round_messages = (len - constant_part) / point_size. Use it on a proof produced by the simple-example.

Answers in the code

  • Exercise 1: the personalization is the byte string "Halo2-Parameters" passed to C::CurveExt::hash_to_curve. Changing it would shift every generator; all existing verifying keys would be invalidated.
  • Exercise 2: the L and R writes are in the inner loop of create_proof in halo2_proofs/src/poly/commitment/prover.rs.

7. Further reading