Aztec团队Gabizon和Williamson 2020年论文 plookup: A simplified polynomial protocol for lookup tables。
代码实现参见:
本文主要关注的是 https://github.com/kevaundray/plookup
库代码,由于其中scipr/zexe
库已重构为arkworks-rs
系列库,本人已fork修改为可兼容最新的arkworks-rs系列库,具体见:
代码基本结构为:
src/kzg10.rs
目录,详细实现也可参看https://github.com/arkworks-rs/algebra/src/poly/
。相比于 Dusk network的Plonk代码解析,arkworks-rs/algebra/src/poly/
中增加了blinding属性,引入了random polynomial。src/multiset
目录。src/lookup
目录。multiset 定义为:
/// A MultiSet is a variation of a set, where we allow duplicate members
/// This can be emulated in Rust by using vectors
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct MultiSet(pub Vec);
其中:【令 d = n + 1 d=n+1 d=n+1】
/// Performs a element-wise insertion into the second multiset
/// Example: f {1,2,3,1} t : {3,1,2,3} => 结果为$(3,3,1,1,1,2,2,3)$
/// We now take each element from f and find the element in `t` then insert the element from `f` into right next to it's duplicate
/// We are assuming that `f` is contained in `t`
pub fn concatenate_and_sort(&self, t: &MultiSet) -> MultiSet {
assert!(self.is_subset_of(t));
let mut result = t.clone();
for element in self.0.iter() {
let index = result.position(element);
result.0.insert(index, *element);
}
result
}
/// Splits a multiset into halves as specified by the paper
/// If s = [1,2,3,4,5,6,7], we can deduce n using |s| = 2 * n + 1 = 7
/// n is therefore 3
/// We split s into two MultiSets of size n+1 each
/// s_0 = [1,2,3,4] ,|s_0| = n+1 = 4
/// s_1 = [4,5,6,7] , |s_1| = n+1 = 4
/// Notice that the last element of the first half equals the first element in the second half
/// This is specified in the paper
pub fn halve(&self) -> (MultiSet, MultiSet) {
let length = self.0.len();
let first_half = MultiSet::from_slice(&self.0[0..=length / 2]);
let second_half = MultiSet::from_slice(&self.0[length / 2..]);
(first_half, second_half)
}
/// Treats each element in the multiset as evaluation points
/// Computes IFFT of the set of evaluation points
/// and returns the coefficients as a Polynomial data structure
pub fn to_polynomial(&self, domain: &GeneralEvaluationDomain) -> Polynomial {
Polynomial::from_coefficients_vec(domain.ifft(&self.0))
}
// Computes the n'th lagrange poly for a particular domain
// Easiest way is to compute the evaluation points, which will be zero at every position except for n
// Then IFFT to get the coefficient form
// Note: n=0 is the first lagrange polynomial and n = domain.size() -1 is the last lagrange polynomial
pub fn compute_n_lagrange_poly(domain: &GeneralEvaluationDomain, n: usize) -> DensePoly {
assert!(n <= domain.size() - 1);
let mut evaluations = compute_n_lagrange_evaluations(domain.size(), n);
domain.ifft_in_place(&mut evaluations);
DensePoly::from_coefficients_vec(evaluations)
}
fn compute_n_lagrange_evaluations(domain_size: usize, n: usize) -> Vec {
let mut lagrange_evaluations = vec![Fr::zero(); domain_size];
lagrange_evaluations[n] = Fr::one();
lagrange_evaluations
}
test_manually_compute_z
,做了如下说明: // This test manually computes the values of the accumulator Z(x)
// Notice that the test will fail if:
// - You add a value to 'f' that is not in 't'
// - 't' is unordered
// Now notice that the test will pass if:
// - (1) len(t) != len(f) + 1
// - (2) len(t) is not a power of two
// The reason why the tests pass for these values is :
// (1) We made this restriction, so that it would be easier to check that h_1 and h_2 are continuous. This should not affect the outcome of Z(X) if h_1 and h_2 when merged
// indeed form 's'
// (2) This is a restriction that is placed upon the protocol due to using roots of unity. It would not affect the computation of Z(X)
】
/// Computes the values for Z(X)
pub fn compute_accumulator_values(
f: &MultiSet,
t: &MultiSet,
h_1: &MultiSet,
h_2: &MultiSet,
beta: Fr,
gamma: Fr,
) -> Vec {
let n = f.len();
// F(beta, gamma)
let mut numerator: Vec = Vec::with_capacity(n + 1);
// G(beta, gamma)
let mut denominator: Vec = Vec::with_capacity(n + 1);
// Z evaluated at the first root of unity is 1
numerator.push(Fr::one());
denominator.push(Fr::one());
let beta_one = Fr::one() + beta;
// Compute values for Z(X)
for i in 0..n {
let f_i = beta_one * compute_f_i(i, f, t, beta, gamma);
let g_i = compute_g_i(i, h_1, h_2, beta, gamma);
let last_numerator = *numerator.last().unwrap();
let last_denominator = *denominator.last().unwrap();
numerator.push(f_i * last_numerator);
denominator.push(g_i * last_denominator);
}
// Check that Z(g^{n+1}) = 1
let last_numerator = *numerator.last().unwrap();
let last_denominator = *denominator.last().unwrap();
assert_eq!(last_numerator / last_denominator, Fr::one());
// Combine numerator and denominator
assert_eq!(numerator.len(), denominator.len());
assert_eq!(numerator.len(), n + 1);
let mut evaluations = Vec::with_capacity(numerator.len());
for (n, d) in numerator.into_iter().zip(denominator) {
evaluations.push(n / d)
}
evaluations
}
// The quotient polynomial will encode the four checks for the multiset equality argument
// These checks are:
// 1) Z(X) evaluated at the first root of unity is 1
// 2) Z(X) is correct accumulated. z_(Xg) * g(X) = (1+beta)^n * z(X) * f(X)
// 3) The last element of h_1(x) is equal to the first element of h_2(x)
// 4) Z(x) evaluated at the last root of unity is 1
//
// We can denote check 1 and check 4 as point checks because they are checking the evaluation of Z(x) at a specific point
// We can denote check 3 as an interval check because it checks whether h_1 and h_2 combined form 's' without any gaps. See paper for more details on 's'
// We can denote check 2 as the term check
//
// Notice that the term check equation will determine the degree of the quotient polynomial
// We can compute it by adding the degrees of Z(x), f(x) and t(x).
// deg(Z(x)) = n because it has n + 1 elements
// deg(f(x)) = n although it has n elements, we must zero pad to ensure that f(x) evaluated on the n+1'th element is zero
// deg(t(x)) = n because we define it to have n + 1 elements.
// Summing the degrees gives us n + n + n = 3n
// However, similar to [GWC19](PLONK) we must divide by the vanishing polynomial
// So the degree of the quotient polynomial Q(x) is 3n - n = 2n
// Significance: Adding this protocol into PLONK will not "blow up" the degree of the quotient polynomial
// Where "blow up" denotes increasing the overall degree past 4n for standard plonk
pub fn compute(
domain: &GeneralEvaluationDomain,
z_poly: &DensePoly,
f_poly: &DensePoly,
t_poly: &DensePoly,
h_1_poly: &DensePoly,
h_2_poly: &DensePoly,
beta: Fr,
gamma: Fr,
) -> (DensePoly, DensePoly) {
// 1. Compute Point check polynomial
let point_check = compute_point_checks(z_poly, domain);
//2. Compute interval check polynomial
let interval_check = compute_interval_check(h_1_poly, h_2_poly, domain);
//3. Compute term check polynomial
let term_check = compute_term_check(
domain, z_poly, f_poly, t_poly, h_1_poly, h_2_poly, beta, gamma,
);
// Compute quotient polynomial
let sum = &(&interval_check + &point_check) + &term_check;
sum.divide_by_vanishing_poly(*domain).unwrap()
}
L n + 1 ( x ) ( h 1 ( x ) − h 2 ( g ⋅ x ) ) = 0 L_{n+1}(\mathbf{x})(h_1(\mathbf{x})-h_2(\mathbf{g}\cdot\mathbf{x}))=0 Ln+1(x)(h1(x)−h2(g⋅x))=0【因为之前有约定 h 1 ( 1 ) = h 2 ( g ) = s n + 1 h_1(1)=h_2(\mathbf{g})=s_{n+1} h1(1)=h2(g)=sn+1】表示为:【由domain_n
,提升至了 domain_2n
域。】
// Compute [L_n(x)](h_1(x) - h_2(x * g))
let i_evals: Vec<_> = (0..domain_2n.size())
.into_iter()
.map(|i| {
let ln_i = ln_2n_evals[i];
let h_1_i = h_1_evals[i];
let h_2_i_next = h_2_evals[i + 2];
ln_i * (h_1_i - h_2_i_next)
})
.collect();
( x − g n + 1 ) Z ( x ) ( 1 + β ) ⋅ ( γ + f ( x ) ) ( γ ( 1 + β ) + t ( x ) + β t ( g ⋅ x ) ) = ( x − g n + 1 ) Z ( g ⋅ x ) ( γ ( 1 + β ) + h 2 ( x ) + β h 2 ( g ⋅ x ) ) ( γ ( 1 + β ) + h 1 ( x ) + β h 1 ( g ⋅ x ) ) (\mathbf{x}-\mathbf{g}^{n+1})Z(\mathbf{x})(1+\beta)\cdot(\gamma+f(\mathbf{x}))(\gamma(1+\beta)+t(\mathbf{x})+\beta t(\mathbf{g}\cdot \mathbf{x}))=(\mathbf{x}-\mathbf{g}^{n+1})Z(\mathbf{g}\cdot\mathbf{x})(\gamma(1+\beta)+h_2(\mathbf{x})+\beta h_2(\mathbf{g}\cdot\mathbf{x}))(\gamma(1+\beta)+h_1(\mathbf{x})+\beta h_1(\mathbf{g}\cdot\mathbf{x})) (x−gn+1)Z(x)(1+β)⋅(γ+f(x))(γ(1+β)+t(x)+βt(g⋅x))=(x−gn+1)Z(g⋅x)(γ(1+β)+h2(x)+βh2(g⋅x))(γ(1+β)+h1(x)+βh1(g⋅x)) 表示为:【由domain_n
,提升至了domain_4n
域。】
pub fn compute_term_check(
domain: &GeneralEvaluationDomain,
z_poly: &DensePoly,
f_poly: &DensePoly,
t_poly: &DensePoly,
h_1_poly: &DensePoly,
h_2_poly: &DensePoly,
beta: Fr,
gamma: Fr,
) -> DensePoly {
// The equation for this is quite big. Similar to PLONK, we can split the point check into two.
// The first part will compute the grand product Z(X) term
// The second part will compute the grand product Z(Xg) term
// First Part
let part_a = compute_term_check_a(domain, z_poly, f_poly, t_poly, beta, gamma);
// Second part
let part_b = compute_term_check_b(domain, z_poly, h_1_poly, h_2_poly, beta, gamma);
&part_a - &part_b
}
// Evaluations store the evaluations of different polynomial.
// `t` denotes that the polynomial was evaluated at t(z) for some random evaluation challenge `z`
// `t_omega` denotes the polynomial was evaluated at t(z * omega) where omega is the group generator
// In the FFT context, the normal terminology is that t(z*omega) means to evaluate a polynomial at the next root of unity from `z`.
pub struct Evaluations {
pub f: Fr,
pub t: Fr,
pub t_omega: Fr,
pub h_1: Fr,
pub h_1_omega: Fr,
pub h_2: Fr,
pub h_2_omega: Fr,
pub z: Fr,
pub z_omega: Fr,
}
// Commitments of different polynomials
pub struct Commitments {
pub f: Commitment,
pub q: Commitment,
pub h_1: Commitment,
pub h_2: Commitment,
pub z: Commitment,
}
// In the best case, this protocol requires 3 extra G1 elements (Commitment)
// These are: h_1_commit,h_2_commit, f_commit
//
// The commitment to the accumulator and the quotient polynomial would ideally be joined into the existing ones in PLONK
//
// We would require 7 extra Scalar elements (Evaluations)
// These are: h_1_eval, h_1_omega_eval, h_2_eval, h_2_omega_eval, f_eval, t_eval, t_omega_eval
//
// We would ideally be able to combine the accumulator, Z(X) with the permutation accumulator in plonk, and the quotient polynomial with the quotient polynomial in PLONK
// Which would save us 2 evaluation points: z_eval and z_omega_eval
// q_eval which is the quotient evaluation is usually created from the prover messages
//
// Lastly, the Witness commitments can also be batched with the PLONK opening Proof.
pub struct EqualityProof {
pub aggregate_witness_comm: Commitment,
pub shifted_aggregate_witness_comm: Commitment,
pub evaluations: Evaluations,
pub commitments: Commitments,
}
在src\lookup
目录下,简单实现了2-fan-in 1-out 的加法门和异或门证明。
BIT_RANGE=16
,通过preprocess
生成的lookuptable为 ( 0 & 0 , 0 & 1 , ⋯ , 15 & 15 ) (0\&0,0\&1,\cdots,15\&15) (0&0,0&1,⋯,15&15) 或 ( 0 ∧ 0 , 0 ∧ 1 , ⋯ , 15 ∧ 15 ) (0\wedge0,0\wedge1,\cdots,15\wedge15) (0∧0,0∧1,⋯,15∧15) 。/// Construct a Generic lookup table over a bi-variate function
pub struct Generic(HashMap<(Fr, Fr), Fr>); //以两个输入组合为Key,以输出结果为value
// 其中的输入 索引表 $t_1,t_2$, 输出索引表$t_3$均为public info。
pub struct PreProcessedTable {
pub n: usize,
pub t_1: (MultiSet, Commitment, DensePoly),
pub t_2: (MultiSet, Commitment, DensePoly),
pub t_3: (MultiSet, Commitment, DensePoly),
}
// Aggregates the table and witness values into one multiset
// and pads the witness to be the correct size
//
// Aggregate our table values into one multiset
let merged_table = MultiSet::aggregate(
vec![
&preprocessed_table.t_1.0,
&preprocessed_table.t_2.0,
&preprocessed_table.t_3.0,
],
alpha,
);
// Aggregate witness values into one multiset
let mut merged_witness = MultiSet::aggregate(vec![f_1, f_2, f_3], alpha);
// Pad merged Witness to be one less than `n`
assert!(merged_witness.len() < preprocessed_table.n);
let pad_by = preprocessed_table.n - 1 - merged_witness.len();
merged_witness.extend(pad_by, merged_witness.last());
pub struct LookUp {
table: T,
// This is the set of values which we want to prove is a subset of the
// table values. This may or may not be equal to the whole witness.
left_wires: MultiSet,
right_wires: MultiSet,
output_wires: MultiSet,
}
pub struct LookUpProof {
pub multiset_equality_proof: EqualityProof,
}
src/lookup/lookup.rs
中的test_proof
。