1. 引言
Aztec团队Gabizon和Williamson 2020年论文 plookup: A simplified polynomial protocol for lookup tables。
代码实现参见:
- https://github.com/kevaundray/plookup 【Rust语言实现】
- https://github.com/neidis/plonk-plookup 【C/C++实现】
本文主要关注的是 https://github.com/kevaundray/plookup
库代码,由于其中scipr/zexe
库已重构为arkworks-rs
系列库,本人已fork修改为可兼容最新的arkworks-rs系列库,具体见:
- https://github.com/3for/plookup
代码基本结构为:
- kzg10 polynomial commitment scheme:具体见
src/kzg10.rs
目录,详细实现也可参看https://github.com/arkworks-rs/algebra/src/poly/
。相比于 Dusk network的Plonk代码解析,arkworks-rs/algebra/src/poly/
中增加了blinding属性,引入了random polynomial。 - multiset 定义及证明:具体见
src/multiset
目录。 - lookup table 定义及证明:具体见
src/lookup
目录。
2. multiset 证明
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<Fr>);
其中:【令 d = n + 1 d=n+1 d=n+1】
- 将 s = ( f , t ) ∈ F 2 n + 1 s=(f,t)\in\mathbb{F}^{2n+1} s=(f,t)∈F2n+1 sort by t t t 的实现为:【若 f = ( 1 , 2 ) , t = ( 2 , 1 , 2 ) f=(1,2), t=(2,1,2) f=(1,2),t=(2,1,2),则 s = ( 2 , 2 , 1 , 1 , 2 ) s=(2,2,1,1,2) s=(2,2,1,1,2)。】
/// 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
}
- 将 s s s 平分,以便于多项式 h 1 h_1 h1表示前半部分,多项式 h 2 h_2 h2表示后半部分。由于 s s s的元素个数 2 n + 1 2n+1 2n+1为奇数,因此第 n + 1 n+1 n+1个元素为 h 1 h_1 h1的最后一个元素,为 h 2 h_2 h2的第一个元素。
/// 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)
}
- 将FFT点值表示法 转换为 系数表示法:【将multiset中的元素看成是evaluations,转换为多项式系数表示。】
/// 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<Fr>) -> Polynomial<Fr> {
Polynomial::from_coefficients_vec(domain.ifft(&self.0))
}
- Lagrange base 表示为:对 i ∈ [ n + 1 ] i\in[n+1] i∈[n+1],有 L i ( g i ) = 1 L_i(\mathbf{g}^i)=1 Li(gi)=1;对任意的 j ≠ i j\neq i j=i,有 L i ( g j ) = 0 L_i(\mathbf{g}^j)=0 Li(gj)=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<Fr>, n: usize) -> DensePoly<Fr> {
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<Fr> {
let mut lagrange_evaluations = vec![Fr::zero(); domain_size];
lagrange_evaluations[n] = Fr::one();
lagrange_evaluations
}
- “单个多项式
f
∈
F
<
n
[
X
]
f\in\mathbb{F}_{<n}[X]
f∈F<n[X]的lookup table 关系证明” 中的
Prover 生成的 proof 对应为:
【相应的测试用例为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<Fr> {
let n = f.len();
// F(beta, gamma)
let mut numerator: Vec<Fr> = Vec::with_capacity(n + 1);
// G(beta, gamma)
let mut denominator: Vec<Fr> = 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
}
- “单个多项式
f
∈
F
<
n
[
X
]
f\in\mathbb{F}_{<n}[X]
f∈F<n[X]的lookup table 关系证明” 中的
Verifier的验证操作描述如下:【将其中的 6.b)验证称为 term check。】
// 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<Fr>,
z_poly: &DensePoly<Fr>,
f_poly: &DensePoly<Fr>,
t_poly: &DensePoly<Fr>,
h_1_poly: &DensePoly<Fr>,
h_2_poly: &DensePoly<Fr>,
beta: Fr,
gamma: Fr,
) -> (DensePoly<Fr>, DensePoly<Fr>) {
// 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<Fr>,
z_poly: &DensePoly<Fr>,
f_poly: &DensePoly<Fr>,
t_poly: &DensePoly<Fr>,
h_1_poly: &DensePoly<Fr>,
h_2_poly: &DensePoly<Fr>,
beta: Fr,
gamma: Fr,
) -> DensePoly<Fr> {
// 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
}
- multiset equality 证明:
// 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<Bls12_381>,
pub q: Commitment<Bls12_381>,
pub h_1: Commitment<Bls12_381>,
pub h_2: Commitment<Bls12_381>,
pub z: Commitment<Bls12_381>,
}
// 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<Bls12_381>,
pub shifted_aggregate_witness_comm: Commitment<Bls12_381>,
pub evaluations: Evaluations,
pub commitments: Commitments,
}
3. circuit lookup table证明
在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<Bls12_381>, DensePoly<Fr>),
pub t_2: (MultiSet, Commitment<Bls12_381>, DensePoly<Fr>),
pub t_3: (MultiSet, Commitment<Bls12_381>, DensePoly<Fr>),
}
- 扩展为多个多项式 f 1 , ⋯ , f w ∈ F < n [ X ] f_1,\cdots,f_w\in\mathbb{F}_{<n}[X] f1,⋯,fw∈F<n[X] 一一对应 多个 lookup tables 关系证明 相应实现为:【借助challenge来aggregate】
// 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());
- witness 为:
pub struct LookUp<T: LookUpTable> {
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,
}
- proof组成为:
pub struct LookUpProof {
pub multiset_equality_proof: EqualityProof,
}
- 具体测试用例参见
src/lookup/lookup.rs
中的test_proof
。