The Permutation Argument
1. Why this chapter exists
Any time you write region.constrain_equal(a, b) or
AssignedCell::copy_advice(...) you are using the permutation
argument. It is the mechanism that lets a circuit author treat the
table as a graph of cells with edges, rather than a flat collection
of rows. The argument is a routine generalization of Plonk's
copy-constraint argument, but the multi-column and chunking
machinery is non-obvious. If you ever change anything in
plonk/permutation/, you need to know what each polynomial in the
proof represents.
2. Definitions
Fix , .
Definition (Wire indexing). Suppose the columns enrolled in the permutation argument are (each a column of the table). The set of wires is the product of column indices and row labels, giving wires total.
Definition (Permutation ). A permutation
.
The cycles of are the equivalence classes of cells joined
by constrain_equal. The chunking required when is large is
described below.
Definition (Cycle-product identity). Define the wire identity . The permutation is consistent with the witness iff, for every pair of ,
where is a fixed coset shift (a quadratic non-residue) and is a polynomial encoding of . The argument is encoded by committing to a running product polynomial that realizes this identity.
Definition (Chunking). With many columns, the polynomial
would have unbounded degree. The argument splits the columns into
chunks of size at most and accumulates a
separate grand product per chunk. Chunk boundaries
introduce additional boundary identities, which is why the comment
block in
plonk/permutation.rs
lists "intermediate" and "final" boundary constraints separately.
3. The code
3.1 The argument struct
use super::circuit::{Any, Column};
use crate::{
arithmetic::CurveAffine,
poly::{Coeff, ExtendedLagrangeCoeff, LagrangeCoeff, Polynomial},
};
pub(crate) mod keygen;
pub(crate) mod prover;
pub(crate) mod verifier;
/// A permutation argument.
#[derive(Debug, Clone)]
pub(crate) struct Argument {
/// A sequence of columns involved in the argument.
columns: Vec<Column<Any>>,
}
impl Argument {
pub(crate) fn new() -> Self {
Argument { columns: vec![] }
}
/// Returns the minimum circuit degree required by the permutation argument.
/// The argument may use larger degree gates depending on the actual
/// circuit's degree and how many columns are involved in the permutation.
pub(crate) fn required_degree(&self) -> usize {
// degree 2:
// l_0(X) * (1 - z(X)) = 0
//
// We will fit as many polynomials p_i(X) as possible
// into the required degree of the circuit, so the
// following will not affect the required degree of
// this middleware.
//
// (1 - (l_last(X) + l_blind(X))) * (
// z(\omega X) \prod (p(X) + \beta s_i(X) + \gamma)
// - z(X) \prod (p(X) + \delta^i \beta X + \gamma)
// )
//
// On the first sets of columns, except the first
// set, we will do
//
// l_0(X) * (z(X) - z'(\omega^(last) X)) = 0
//
// where z'(X) is the permutation for the previous set
// of columns.
//
// On the final set of columns, we will do
//
// degree 3:
// l_last(X) * (z'(X)^2 - z'(X)) = 0
//
// which will allow the last value to be zero to
// ensure the argument is perfectly complete.
// There are constraints of degree 3 regardless of the
// number of columns involved.
3
}
pub(crate) fn add_column(&mut self, column: Column<Any>) {
if !self.columns.contains(&column) {
self.columns.push(column);
}
}
pub(crate) fn get_columns(&self) -> Vec<Column<Any>> {
self.columns.clone()
}
}
/// The verifying key for a single permutation argument.
#[derive(Clone, Debug)]
pub(crate) struct VerifyingKey<C: CurveAffine> {
commitments: Vec<C>,
}
/// The proving key for a single permutation argument.
#[derive(Clone, Debug)]
pub(crate) struct ProvingKey<C: CurveAffine> {
permutations: Vec<Polynomial<C::Scalar, LagrangeCoeff>>,
polys: Vec<Polynomial<C::Scalar, Coeff>>,
pub(super) cosets: Vec<Polynomial<C::Scalar, ExtendedLagrangeCoeff>>,
}
Argument carries the list of columns enrolled in the permutation;
required_degree records the four constraint patterns the verifier
checks (in the same style as the lookup chapter):
- ,
- a chunk-internal grand-product recurrence over ,
- a "chain" constraint for , linking chunk to the end of chunk ,
- , ensuring the last value is in .
3.2 Enrolling columns
The user-facing API:
/// Enable the ability to enforce equality over cells in this column
pub fn enable_equality<C: Into<Column<Any>>>(&mut self, column: C) {
let column = column.into();
self.query_any_index(column, Rotation::cur());
self.permutation.add_column(column);
}
/// Add a lookup argument for some input expressions and table columns.
///
/// `table_map` returns a map between input expressions and the table columns
/// they need to match.
meta.enable_equality(column) is the only entry point. It adds
the column to the permutation argument's column list, after which
Region::constrain_equal can target cells in that column. The
order of enable_equality calls matters: it determines the wire
indexing , which appears in the term of the
constraint and therefore in the verifying key. Reordering it breaks
verifying-key compatibility.
3.3 Keygen
use group::{
ff::{Field, PrimeField},
Curve,
};
use super::{Argument, ProvingKey, VerifyingKey};
use crate::{
arithmetic::CurveAffine,
plonk::{Any, Column, Error},
poly::{
commitment::{Blind, Params},
EvaluationDomain,
},
};
#[derive(Debug)]
pub(crate) struct Assembly {
columns: Vec<Column<Any>>,
pub(crate) mapping: Vec<Vec<(usize, usize)>>,
aux: Vec<Vec<(usize, usize)>>,
sizes: Vec<Vec<usize>>,
}
impl Assembly {
pub(crate) fn new(n: usize, p: &Argument) -> Self {
// Initialize the copy vector to keep track of copy constraints in all
// the permutation arguments.
let mut columns = vec![];
for i in 0..p.columns.len() {
// Computes [(i, 0), (i, 1), ..., (i, n - 1)]
columns.push((0..n).map(|j| (i, j)).collect());
}
// Before any equality constraints are applied, every cell in the permutation is
// in a 1-cycle; therefore mapping and aux are identical, because every cell is
// its own distinguished element.
Assembly {
columns: p.columns.clone(),
mapping: columns.clone(),
aux: columns,
sizes: vec![vec![1usize; n]; p.columns.len()],
}
}
pub(crate) fn copy(
&mut self,
left_column: Column<Any>,
left_row: usize,
right_column: Column<Any>,
right_row: usize,
) -> Result<(), Error> {
let left_column = self
.columns
.iter()
.position(|c| c == &left_column)
.ok_or(Error::ColumnNotInPermutation(left_column))?;
let right_column = self
.columns
.iter()
.position(|c| c == &right_column)
.ok_or(Error::ColumnNotInPermutation(right_column))?;
// Check bounds
if left_row >= self.mapping[left_column].len()
|| right_row >= self.mapping[right_column].len()
{
return Err(Error::BoundsFailure);
}
// See book/src/design/permutation.md for a description of this algorithm.
let mut left_cycle = self.aux[left_column][left_row];
let mut right_cycle = self.aux[right_column][right_row];
// If left and right are in the same cycle, do nothing.
if left_cycle == right_cycle {
return Ok(());
}
if self.sizes[left_cycle.0][left_cycle.1] < self.sizes[right_cycle.0][right_cycle.1] {
std::mem::swap(&mut left_cycle, &mut right_cycle);
}
// Merge the right cycle into the left one.
self.sizes[left_cycle.0][left_cycle.1] += self.sizes[right_cycle.0][right_cycle.1];
let mut i = right_cycle;
loop {
self.aux[i.0][i.1] = left_cycle;
i = self.mapping[i.0][i.1];
if i == right_cycle {
break;
}
}
let tmp = self.mapping[left_column][left_row];
self.mapping[left_column][left_row] = self.mapping[right_column][right_row];
self.mapping[right_column][right_row] = tmp;
Ok(())
}
pub(crate) fn build_vk<C: CurveAffine>(
self,
params: &Params<C>,
domain: &EvaluationDomain<C::Scalar>,
p: &Argument,
) -> VerifyingKey<C> {
// Compute [omega^0, omega^1, ..., omega^{params.n - 1}]
let mut omega_powers = Vec::with_capacity(params.n as usize);
{
let mut cur = C::Scalar::ONE;
for _ in 0..params.n {
omega_powers.push(cur);
cur *= &domain.get_omega();
}
}
// Compute [omega_powers * \delta^0, omega_powers * \delta^1, ..., omega_powers * \delta^m]
let mut deltaomega = Vec::with_capacity(p.columns.len());
{
let mut cur = C::Scalar::ONE;
for _ in 0..p.columns.len() {
let mut omega_powers = omega_powers.clone();
for o in &mut omega_powers {
*o *= &cur;
}
deltaomega.push(omega_powers);
cur *= &C::Scalar::DELTA;
}
}
// Pre-compute commitments for the URS.
let mut commitments = Vec::with_capacity(p.columns.len());
for i in 0..p.columns.len() {
// Computes the permutation polynomial based on the permutation
// description in the assembly.
let mut permutation_poly = domain.empty_lagrange();
for (j, p) in permutation_poly.iter_mut().enumerate() {
let (permuted_i, permuted_j) = self.mapping[i][j];
*p = deltaomega[permuted_i][permuted_j];
}
// Compute commitment to permutation polynomial
commitments.push(
params
.commit_lagrange(&permutation_poly, Blind::default())
.to_affine(),
);
}
VerifyingKey { commitments }
}
pub(crate) fn build_pk<C: CurveAffine>(
self,
params: &Params<C>,
domain: &EvaluationDomain<C::Scalar>,
p: &Argument,
) -> ProvingKey<C> {
// Compute [omega^0, omega^1, ..., omega^{params.n - 1}]
let mut omega_powers = Vec::with_capacity(params.n as usize);
{
let mut cur = C::Scalar::ONE;
for _ in 0..params.n {
omega_powers.push(cur);
cur *= &domain.get_omega();
}
}
// Compute [omega_powers * \delta^0, omega_powers * \delta^1, ..., omega_powers * \delta^m]
let mut deltaomega = Vec::with_capacity(p.columns.len());
{
let mut cur = C::Scalar::ONE;
for _ in 0..p.columns.len() {
let mut omega_powers = omega_powers.clone();
for o in &mut omega_powers {
*o *= &cur;
}
deltaomega.push(omega_powers);
cur *= &C::Scalar::DELTA;
}
}
// Compute permutation polynomials, convert to coset form.
let mut permutations = vec![];
let mut polys = vec![];
let mut cosets = vec![];
for i in 0..p.columns.len() {
// Computes the permutation polynomial based on the permutation
// description in the assembly.
let mut permutation_poly = domain.empty_lagrange();
for (j, p) in permutation_poly.iter_mut().enumerate() {
let (permuted_i, permuted_j) = self.mapping[i][j];
*p = deltaomega[permuted_i][permuted_j];
}
// Store permutation polynomial and precompute its coset evaluation
permutations.push(permutation_poly.clone());
let poly = domain.lagrange_to_coeff(permutation_poly);
polys.push(poly.clone());
cosets.push(domain.coeff_to_extended(poly));
}
ProvingKey {
permutations,
polys,
cosets,
}
}
}
The keygen step:
- Walks the equality-constraint graph produced by the layouter, computes the union-find of cycles.
- Encodes each cycle as a permutation on the -wire space.
- Builds the permutation polynomials in Lagrange basis, where holds the next wire identity in 's cycle starting from .
- Commits to each . The commitments become the permutation
VerifyingKey.
3.4 The prover
use group::{
ff::{BatchInvert, Field, PrimeField},
Curve,
};
use rand_core::RngCore;
use std::iter::{self, ExactSizeIterator};
use super::super::{circuit::Any, ChallengeBeta, ChallengeGamma, ChallengeX};
use super::{Argument, ProvingKey};
use crate::{
arithmetic::{eval_polynomial, parallelize, CurveAffine},
plonk::{self, Error},
poly::{
self,
commitment::{Blind, Params},
multiopen::ProverQuery,
Coeff, ExtendedLagrangeCoeff, LagrangeCoeff, Polynomial, Rotation,
},
transcript::{EncodedChallenge, TranscriptWrite},
};
pub struct CommittedSet<C: CurveAffine, Ev> {
permutation_product_poly: Polynomial<C::Scalar, Coeff>,
permutation_product_coset: poly::AstLeaf<Ev, ExtendedLagrangeCoeff>,
permutation_product_blind: Blind<C::Scalar>,
}
pub(crate) struct Committed<C: CurveAffine, Ev> {
sets: Vec<CommittedSet<C, Ev>>,
}
pub struct ConstructedSet<C: CurveAffine> {
permutation_product_poly: Polynomial<C::Scalar, Coeff>,
permutation_product_blind: Blind<C::Scalar>,
}
pub(crate) struct Constructed<C: CurveAffine> {
sets: Vec<ConstructedSet<C>>,
}
pub(crate) struct Evaluated<C: CurveAffine> {
constructed: Constructed<C>,
}
impl Argument {
#[allow(clippy::too_many_arguments)]
pub(in crate::plonk) fn commit<
C: CurveAffine,
E: EncodedChallenge<C>,
Ev: Copy + Send + Sync,
R: RngCore,
T: TranscriptWrite<C, E>,
>(
&self,
params: &Params<C>,
pk: &plonk::ProvingKey<C>,
pkey: &ProvingKey<C>,
advice: &[Polynomial<C::Scalar, LagrangeCoeff>],
fixed: &[Polynomial<C::Scalar, LagrangeCoeff>],
instance: &[Polynomial<C::Scalar, LagrangeCoeff>],
beta: ChallengeBeta<C>,
gamma: ChallengeGamma<C>,
evaluator: &mut poly::Evaluator<Ev, C::Scalar, ExtendedLagrangeCoeff>,
mut rng: R,
transcript: &mut T,
) -> Result<Committed<C, Ev>, Error> {
let domain = &pk.vk.domain;
// How many columns can be included in a single permutation polynomial?
// We need to multiply by z(X) and (1 - (l_last(X) + l_blind(X))). This
// will never underflow because of the requirement of at least a degree
// 3 circuit for the permutation argument.
assert!(pk.vk.cs_degree >= 3);
let chunk_len = pk.vk.cs_degree - 2;
let blinding_factors = pk.vk.cs.blinding_factors();
// Each column gets its own delta power.
let mut deltaomega = C::Scalar::ONE;
// Track the "last" value from the previous column set
let mut last_z = C::Scalar::ONE;
let mut sets = vec![];
for (columns, permutations) in self
.columns
.chunks(chunk_len)
.zip(pkey.permutations.chunks(chunk_len))
{
// Goal is to compute the products of fractions
//
// (p_j(\omega^i) + \delta^j \omega^i \beta + \gamma) /
// (p_j(\omega^i) + \beta s_j(\omega^i) + \gamma)
//
// where p_j(X) is the jth column in this permutation,
// and i is the ith row of the column.
let mut modified_values = vec![C::Scalar::ONE; params.n as usize];
// Iterate over each column of the permutation
for (&column, permuted_column_values) in columns.iter().zip(permutations.iter()) {
let values = match column.column_type() {
Any::Advice => advice,
Any::Fixed => fixed,
Any::Instance => instance,
};
parallelize(&mut modified_values, |modified_values, start| {
for ((modified_values, value), permuted_value) in modified_values
.iter_mut()
.zip(values[column.index()][start..].iter())
.zip(permuted_column_values[start..].iter())
{
*modified_values *= &(*beta * permuted_value + &*gamma + value);
}
});
}
// Invert to obtain the denominator for the permutation product polynomial
modified_values.batch_invert();
// Iterate over each column again, this time finishing the computation
// of the entire fraction by computing the numerators
for &column in columns.iter() {
let omega = domain.get_omega();
let values = match column.column_type() {
Any::Advice => advice,
Any::Fixed => fixed,
Any::Instance => instance,
};
parallelize(&mut modified_values, |modified_values, start| {
let mut deltaomega = deltaomega * &omega.pow_vartime([start as u64]);
for (modified_values, value) in modified_values
.iter_mut()
.zip(values[column.index()][start..].iter())
{
// Multiply by p_j(\omega^i) + \delta^j \omega^i \beta
*modified_values *= &(deltaomega * &*beta + &*gamma + value);
deltaomega *= ω
}
});
deltaomega *= &C::Scalar::DELTA;
}
// The modified_values vector is a vector of products of fractions
// of the form
//
// (p_j(\omega^i) + \delta^j \omega^i \beta + \gamma) /
// (p_j(\omega^i) + \beta s_j(\omega^i) + \gamma)
//
// where i is the index into modified_values, for the jth column in
// the permutation
// Compute the evaluations of the permutation product polynomial
// over our domain, starting with z[0] = 1
let mut z = vec![last_z];
for row in 1..(params.n as usize) {
let mut tmp = z[row - 1];
tmp *= &modified_values[row - 1];
z.push(tmp);
}
let mut z = domain.lagrange_from_vec(z);
// Set blinding factors
for z in &mut z[params.n as usize - blinding_factors..] {
*z = C::Scalar::random(&mut rng);
}
// Set new last_z
last_z = z[params.n as usize - (blinding_factors + 1)];
let blind = Blind(C::Scalar::random(&mut rng));
let permutation_product_commitment_projective = params.commit_lagrange(&z, blind);
let permutation_product_blind = blind;
let z = domain.lagrange_to_coeff(z);
let permutation_product_poly = z.clone();
let permutation_product_coset =
evaluator.register_poly(domain.coeff_to_extended(z.clone()));
let permutation_product_commitment =
permutation_product_commitment_projective.to_affine();
// Hash the permutation product commitment
transcript.write_point(permutation_product_commitment)?;
sets.push(CommittedSet {
permutation_product_poly,
permutation_product_coset,
permutation_product_blind,
});
}
Ok(Committed { sets })
}
}
impl<C: CurveAffine, Ev: Copy + Send + Sync> Committed<C, Ev> {
#[allow(clippy::too_many_arguments)]
pub(in crate::plonk) fn construct<'a>(
self,
pk: &'a plonk::ProvingKey<C>,
p: &'a Argument,
advice_cosets: &'a [poly::AstLeaf<Ev, ExtendedLagrangeCoeff>],
fixed_cosets: &'a [poly::AstLeaf<Ev, ExtendedLagrangeCoeff>],
instance_cosets: &'a [poly::AstLeaf<Ev, ExtendedLagrangeCoeff>],
permutation_cosets: &'a [poly::AstLeaf<Ev, ExtendedLagrangeCoeff>],
l0: poly::AstLeaf<Ev, ExtendedLagrangeCoeff>,
l_blind: poly::AstLeaf<Ev, ExtendedLagrangeCoeff>,
l_last: poly::AstLeaf<Ev, ExtendedLagrangeCoeff>,
beta: ChallengeBeta<C>,
gamma: ChallengeGamma<C>,
) -> (
Constructed<C>,
impl Iterator<Item = poly::Ast<Ev, C::Scalar, ExtendedLagrangeCoeff>> + 'a,
) {
let chunk_len = pk.vk.cs_degree - 2;
let blinding_factors = pk.vk.cs.blinding_factors();
let last_rotation = Rotation(-((blinding_factors + 1) as i32));
let constructed = Constructed {
sets: self
.sets
.iter()
.map(|set| ConstructedSet {
permutation_product_poly: set.permutation_product_poly.clone(),
permutation_product_blind: set.permutation_product_blind,
})
.collect(),
};
let expressions = iter::empty()
// Enforce only for the first set.
// l_0(X) * (1 - z_0(X)) = 0
.chain(
self.sets
.first()
.map(|first_set| (poly::Ast::one() - first_set.permutation_product_coset) * l0),
)
// Enforce only for the last set.
// l_last(X) * (z_l(X)^2 - z_l(X)) = 0
.chain(self.sets.last().map(|last_set| {
((poly::Ast::from(last_set.permutation_product_coset)
* last_set.permutation_product_coset)
- last_set.permutation_product_coset)
* l_last
}))
// Except for the first set, enforce.
// l_0(X) * (z_i(X) - z_{i-1}(\omega^(last) X)) = 0
.chain(
self.sets
.iter()
.skip(1)
.zip(self.sets.iter())
.map(|(set, last_set)| {
(poly::Ast::from(set.permutation_product_coset)
- last_set
.permutation_product_coset
.with_rotation(last_rotation))
* l0
})
.collect::<Vec<_>>(),
)
// And for all the sets we enforce:
// (1 - (l_last(X) + l_blind(X))) * (
// z_i(\omega X) \prod_j (p(X) + \beta s_j(X) + \gamma)
// - z_i(X) \prod_j (p(X) + \delta^j \beta X + \gamma)
// )
.chain(
self.sets
.into_iter()
.zip(p.columns.chunks(chunk_len))
.zip(permutation_cosets.chunks(chunk_len))
.enumerate()
.map(move |(chunk_index, ((set, columns), cosets))| {
let mut left = poly::Ast::<_, C::Scalar, _>::from(
set.permutation_product_coset
.with_rotation(Rotation::next()),
);
for (values, permutation) in columns
.iter()
.map(|&column| match column.column_type() {
Any::Advice => &advice_cosets[column.index()],
Any::Fixed => &fixed_cosets[column.index()],
Any::Instance => &instance_cosets[column.index()],
})
.zip(cosets.iter())
{
left *= poly::Ast::<_, C::Scalar, _>::from(*values)
+ (poly::Ast::ConstantTerm(*beta) * poly::Ast::from(*permutation))
+ poly::Ast::ConstantTerm(*gamma);
}
let mut right = poly::Ast::from(set.permutation_product_coset);
let mut current_delta = *beta
* &(C::Scalar::DELTA.pow_vartime([(chunk_index * chunk_len) as u64]));
for values in columns.iter().map(|&column| match column.column_type() {
Any::Advice => &advice_cosets[column.index()],
Any::Fixed => &fixed_cosets[column.index()],
Any::Instance => &instance_cosets[column.index()],
}) {
right *= poly::Ast::from(*values)
+ poly::Ast::LinearTerm(current_delta)
+ poly::Ast::ConstantTerm(*gamma);
current_delta *= &C::Scalar::DELTA;
}
(left - right) * (poly::Ast::one() - (poly::Ast::from(l_last) + l_blind))
}),
);
(constructed, expressions)
}
}
impl<C: CurveAffine> super::ProvingKey<C> {
pub(in crate::plonk) fn open(
&self,
x: ChallengeX<C>,
) -> impl Iterator<Item = ProverQuery<'_, C>> + Clone {
self.polys.iter().map(move |poly| ProverQuery {
point: *x,
poly,
blind: Blind::default(),
})
}
pub(in crate::plonk) fn evaluate<E: EncodedChallenge<C>, T: TranscriptWrite<C, E>>(
&self,
x: ChallengeX<C>,
transcript: &mut T,
) -> Result<(), Error> {
// Hash permutation evals
for eval in self.polys.iter().map(|poly| eval_polynomial(poly, *x)) {
transcript.write_scalar(eval)?;
}
Ok(())
}
}
impl<C: CurveAffine> Constructed<C> {
pub(in crate::plonk) fn evaluate<E: EncodedChallenge<C>, T: TranscriptWrite<C, E>>(
self,
pk: &plonk::ProvingKey<C>,
x: ChallengeX<C>,
transcript: &mut T,
) -> Result<Evaluated<C>, Error> {
let domain = &pk.vk.domain;
let blinding_factors = pk.vk.cs.blinding_factors();
{
let mut sets = self.sets.iter();
while let Some(set) = sets.next() {
let permutation_product_eval = eval_polynomial(&set.permutation_product_poly, *x);
let permutation_product_next_eval = eval_polynomial(
&set.permutation_product_poly,
domain.rotate_omega(*x, Rotation::next()),
);
// Hash permutation product evals
for eval in iter::empty()
.chain(Some(&permutation_product_eval))
.chain(Some(&permutation_product_next_eval))
{
transcript.write_scalar(*eval)?;
}
// If we have any remaining sets to process, evaluate this set at omega^u
// so we can constrain the last value of its running product to equal the
// first value of the next set's running product, chaining them together.
if sets.len() > 0 {
let permutation_product_last_eval = eval_polynomial(
&set.permutation_product_poly,
domain.rotate_omega(*x, Rotation(-((blinding_factors + 1) as i32))),
);
transcript.write_scalar(permutation_product_last_eval)?;
}
}
}
Ok(Evaluated { constructed: self })
}
}
impl<C: CurveAffine> Evaluated<C> {
pub(in crate::plonk) fn open<'a>(
&'a self,
pk: &'a plonk::ProvingKey<C>,
x: ChallengeX<C>,
) -> impl Iterator<Item = ProverQuery<'a, C>> + Clone {
let blinding_factors = pk.vk.cs.blinding_factors();
let x_next = pk.vk.domain.rotate_omega(*x, Rotation::next());
let x_last = pk
.vk
.domain
.rotate_omega(*x, Rotation(-((blinding_factors + 1) as i32)));
iter::empty()
.chain(self.constructed.sets.iter().flat_map(move |set| {
iter::empty()
// Open permutation product commitments at x and \omega x
.chain(Some(ProverQuery {
point: *x,
poly: &set.permutation_product_poly,
blind: set.permutation_product_blind,
}))
.chain(Some(ProverQuery {
point: x_next,
poly: &set.permutation_product_poly,
blind: set.permutation_product_blind,
}))
}))
// Open it at \omega^{last} x for all but the last set. This rotation is only
// sensical for the first row, but we only use this rotation in a constraint
// that is gated on l_0.
.chain(
self.constructed
.sets
.iter()
.rev()
.skip(1)
.flat_map(move |set| {
Some(ProverQuery {
point: x_last,
poly: &set.permutation_product_poly,
blind: set.permutation_product_blind,
})
}),
)
}
}
The prover (per chunk):
- Computes the numerator product and the denominator on the small domain.
- Inverts the denominator using a batch inversion.
- Walks the rows building as the running product.
- Commits to .
3.5 The verifier
use group::ff::{Field, PrimeField};
use std::iter;
use super::super::{circuit::Any, ChallengeBeta, ChallengeGamma, ChallengeX};
use super::{Argument, VerifyingKey};
use crate::{
arithmetic::CurveAffine,
plonk::{self, Error},
poly::{multiopen::VerifierQuery, Rotation},
transcript::{EncodedChallenge, TranscriptRead},
};
pub struct Committed<C: CurveAffine> {
permutation_product_commitments: Vec<C>,
}
pub struct EvaluatedSet<C: CurveAffine> {
permutation_product_commitment: C,
permutation_product_eval: C::Scalar,
permutation_product_next_eval: C::Scalar,
permutation_product_last_eval: Option<C::Scalar>,
}
pub struct CommonEvaluated<C: CurveAffine> {
permutation_evals: Vec<C::Scalar>,
}
pub struct Evaluated<C: CurveAffine> {
sets: Vec<EvaluatedSet<C>>,
}
impl Argument {
pub(crate) fn read_product_commitments<
C: CurveAffine,
E: EncodedChallenge<C>,
T: TranscriptRead<C, E>,
>(
&self,
vk: &plonk::VerifyingKey<C>,
transcript: &mut T,
) -> Result<Committed<C>, Error> {
let chunk_len = vk.cs_degree - 2;
let permutation_product_commitments = self
.columns
.chunks(chunk_len)
.map(|_| transcript.read_point())
.collect::<Result<Vec<_>, _>>()?;
Ok(Committed {
permutation_product_commitments,
})
}
}
impl<C: CurveAffine> VerifyingKey<C> {
pub(in crate::plonk) fn evaluate<E: EncodedChallenge<C>, T: TranscriptRead<C, E>>(
&self,
transcript: &mut T,
) -> Result<CommonEvaluated<C>, Error> {
let permutation_evals = self
.commitments
.iter()
.map(|_| transcript.read_scalar())
.collect::<Result<Vec<_>, _>>()?;
Ok(CommonEvaluated { permutation_evals })
}
}
impl<C: CurveAffine> Committed<C> {
pub(crate) fn evaluate<E: EncodedChallenge<C>, T: TranscriptRead<C, E>>(
self,
transcript: &mut T,
) -> Result<Evaluated<C>, Error> {
let mut sets = vec![];
let mut iter = self.permutation_product_commitments.into_iter();
while let Some(permutation_product_commitment) = iter.next() {
let permutation_product_eval = transcript.read_scalar()?;
let permutation_product_next_eval = transcript.read_scalar()?;
let permutation_product_last_eval = if iter.len() > 0 {
Some(transcript.read_scalar()?)
} else {
None
};
sets.push(EvaluatedSet {
permutation_product_commitment,
permutation_product_eval,
permutation_product_next_eval,
permutation_product_last_eval,
});
}
Ok(Evaluated { sets })
}
}
impl<C: CurveAffine> Evaluated<C> {
#[allow(clippy::too_many_arguments)]
pub(in crate::plonk) fn expressions<'a>(
&'a self,
vk: &'a plonk::VerifyingKey<C>,
p: &'a Argument,
common: &'a CommonEvaluated<C>,
advice_evals: &'a [C::Scalar],
fixed_evals: &'a [C::Scalar],
instance_evals: &'a [C::Scalar],
l_0: C::Scalar,
l_last: C::Scalar,
l_blind: C::Scalar,
beta: ChallengeBeta<C>,
gamma: ChallengeGamma<C>,
x: ChallengeX<C>,
) -> impl Iterator<Item = C::Scalar> + 'a {
let chunk_len = vk.cs_degree - 2;
iter::empty()
// Enforce only for the first set.
// l_0(X) * (1 - z_0(X)) = 0
.chain(
self.sets
.first()
.map(|first_set| l_0 * &(C::Scalar::ONE - &first_set.permutation_product_eval)),
)
// Enforce only for the last set.
// l_last(X) * (z_l(X)^2 - z_l(X)) = 0
.chain(self.sets.last().map(|last_set| {
(last_set.permutation_product_eval.square() - &last_set.permutation_product_eval)
* &l_last
}))
// Except for the first set, enforce.
// l_0(X) * (z_i(X) - z_{i-1}(\omega^(last) X)) = 0
.chain(
self.sets
.iter()
.skip(1)
.zip(self.sets.iter())
.map(|(set, last_set)| {
(
set.permutation_product_eval,
last_set.permutation_product_last_eval.unwrap(),
)
})
.map(move |(set, prev_last)| (set - &prev_last) * &l_0),
)
// And for all the sets we enforce:
// (1 - (l_last(X) + l_blind(X))) * (
// z_i(\omega X) \prod (p(X) + \beta s_i(X) + \gamma)
// - z_i(X) \prod (p(X) + \delta^i \beta X + \gamma)
// )
.chain(
self.sets
.iter()
.zip(p.columns.chunks(chunk_len))
.zip(common.permutation_evals.chunks(chunk_len))
.enumerate()
.map(move |(chunk_index, ((set, columns), permutation_evals))| {
let mut left = set.permutation_product_next_eval;
for (eval, permutation_eval) in columns
.iter()
.map(|&column| match column.column_type() {
Any::Advice => advice_evals[vk.cs.get_any_query_index(column)],
Any::Fixed => fixed_evals[vk.cs.get_any_query_index(column)],
Any::Instance => instance_evals[vk.cs.get_any_query_index(column)],
})
.zip(permutation_evals.iter())
{
left *= &(eval + &(*beta * permutation_eval) + &*gamma);
}
let mut right = set.permutation_product_eval;
let mut current_delta = (*beta * &*x)
* &(C::Scalar::DELTA.pow_vartime([(chunk_index * chunk_len) as u64]));
for eval in columns.iter().map(|&column| match column.column_type() {
Any::Advice => advice_evals[vk.cs.get_any_query_index(column)],
Any::Fixed => fixed_evals[vk.cs.get_any_query_index(column)],
Any::Instance => instance_evals[vk.cs.get_any_query_index(column)],
}) {
right *= &(eval + ¤t_delta + &*gamma);
current_delta *= &C::Scalar::DELTA;
}
(left - &right) * (C::Scalar::ONE - &(l_last + &l_blind))
}),
)
}
pub(in crate::plonk) fn queries<'r, 'params: 'r>(
&'r self,
vk: &'r plonk::VerifyingKey<C>,
x: ChallengeX<C>,
) -> impl Iterator<Item = VerifierQuery<'r, 'params, C>> + Clone {
let blinding_factors = vk.cs.blinding_factors();
let x_next = vk.domain.rotate_omega(*x, Rotation::next());
let x_last = vk
.domain
.rotate_omega(*x, Rotation(-((blinding_factors + 1) as i32)));
iter::empty()
.chain(self.sets.iter().flat_map(move |set| {
iter::empty()
// Open permutation product commitments at x and \omega^{-1} x
// Open permutation product commitments at x and \omega x
.chain(Some(VerifierQuery::new_commitment(
&set.permutation_product_commitment,
*x,
set.permutation_product_eval,
)))
.chain(Some(VerifierQuery::new_commitment(
&set.permutation_product_commitment,
x_next,
set.permutation_product_next_eval,
)))
}))
// Open it at \omega^{last} x for all but the last set
.chain(self.sets.iter().rev().skip(1).flat_map(move |set| {
Some(VerifierQuery::new_commitment(
&set.permutation_product_commitment,
x_last,
set.permutation_product_last_eval.unwrap(),
))
}))
}
}
impl<C: CurveAffine> CommonEvaluated<C> {
pub(in crate::plonk) fn queries<'r, 'params: 'r>(
&'r self,
vkey: &'r VerifyingKey<C>,
x: ChallengeX<C>,
) -> impl Iterator<Item = VerifierQuery<'r, 'params, C>> + Clone {
// Open permutation commitments for each permutation argument at x
vkey.commitments
.iter()
.zip(self.permutation_evals.iter())
.map(move |(commitment, &eval)| VerifierQuery::new_commitment(commitment, *x, eval))
}
}
The verifier expects to find one commitment per chunk in the transcript, queries each at and , then checks the four identities listed in 3.1 above. The check at that is what closes the loop, since the recurrence and boundary identities together imply exactly when the multiset equality holds.
4. Failure modes
- Forgetting
enable_equality. The most frequent contributor mistake. The error isError::ColumnNotInPermutation; the fix is to addmeta.enable_equality(col)in the configure block. - Changing the enrollment order. Adding a new
enable_equality(col)call between two existing ones shifts every later column's wire index by one, which changes the term in the proof identity and breaks every existing verifying key for that circuit. Add new columns at the end of the enrollment list. - Mixing chunking sizes. The chunk size is implied by the circuit's degree. If you increase the gate degree, the chunk size grows, the number of chunks shrinks, and the verifying key changes. This is one of the surprising consequences of "adding a high-degree custom gate".
- Inappropriate use of
assign_advice_from_constant. This path piggybacks on the permutation argument via theConstraintSystem::constantscolumns. If the constants column was not enrolled (the user forgot to add it viameta.enable_constant), the helper returnsNotEnoughColumnsForConstants.
5. Spec pointers
- PLONK paper, eprint 2019/953, Section 5.1: the original permutation argument.
- The Halo 2 Book "Permutation argument" chapter works through the multi-column chunking in full.
- Tornado-style permutation arguments are surveyed in Yao Sun's PLONK notes for a less terse exposition.
6. Exercises
- Open
halo2_proofs/src/plonk/permutation.rsand read the comment inrequired_degree. Identify each of the four constraint patterns by name. - Use
MockProverto verify a circuit, then manually mutate one advice cell after synthesis (you will need to use the dev tools' internal API). Confirm the failure surface isPermutation { column, location }and matches the cell you broke. - Re-read
halo2_proofs/src/plonk/permutation/keygen.rsand describe in two sentences how the cycles in the equality graph become the permutation . - Run
circuit-layoutand compute (by hand or withCircuitCost) how the permutation polynomial count changes when you add another advice column to thesimple-examplecircuit.
Answers in the code
- Exercise 1: boundary; chunk-internal recurrence; chunk-chaining constraint ; final boundary .