Skip to content

Commit 790bdf0

Browse files
authored
Merge pull request #232 from WizardOfMenlo/recmo/zkwhirv3
zkWHIRv3 part 1: Single commitment base case
2 parents 7f0cb5e + f90a28c commit 790bdf0

29 files changed

Lines changed: 1444 additions & 617 deletions

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ parallel = ["dep:rayon"]
7474
rayon = ["dep:rayon"]
7575
asm = ["ark-ff/asm"]
7676
tracing = ["dep:tracing"]
77+
# Do not permute evaluation pointsin RS
78+
# This flag is to be removed after whir_zk supports it.
79+
rs_in_order = []
7780

7881
[[bench]]
7982
name = "expand_from_coeff"

benches/expand_from_coeff.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use divan::{black_box, AllocProfiler, Bencher};
2-
use whir::algebra::{fields::Field64, ntt};
2+
use whir::algebra::{fields::Field64, ntt, random_vector};
33

44
#[global_allocator]
55
static ALLOC: AllocProfiler = AllocProfiler::system();
@@ -10,7 +10,7 @@ static ALLOC: AllocProfiler = AllocProfiler::system();
1010
// - RS code expansion factors, and
1111
// - interleaved bloc size exponent of 2
1212
//
13-
const TEST_CASES: &[(u32, usize, usize)] = &[
13+
const TEST_CASES: &[(usize, usize, usize)] = &[
1414
(16, 2, 2),
1515
(18, 2, 2),
1616
(20, 2, 3),
@@ -21,20 +21,24 @@ const TEST_CASES: &[(u32, usize, usize)] = &[
2121
];
2222

2323
#[divan::bench(args = TEST_CASES)]
24-
fn interleaved_rs_encode(bencher: Bencher, case: &(u32, usize, usize)) {
24+
fn interleaved_rs_encode(bencher: Bencher, case: &(usize, usize, usize)) {
2525
bencher
2626
.with_inputs(|| {
2727
let (exp, expansion, coset_sz) = *case;
28-
29-
let size = 1 << exp;
30-
let coeffs: Vec<_> = (0..size).map(Field64::from).collect();
28+
let message_length = 1 << (exp - coset_sz);
29+
let num_messages = 1 << coset_sz;
30+
let mut rng = ark_std::rand::thread_rng();
31+
let coeffs: Vec<Vec<Field64>> = (0..num_messages)
32+
.map(|_| random_vector(&mut rng, message_length))
33+
.collect();
3134
(coeffs, expansion, coset_sz)
3235
})
33-
.bench_values(|(coeffs, expansion, coset_sz)| {
36+
.bench_values(|(coeffs, expansion, _coset_sz)| {
37+
let coeffs_refs = coeffs.iter().map(|v| v.as_slice()).collect::<Vec<_>>();
3438
black_box(ntt::interleaved_rs_encode(
35-
&[&coeffs],
36-
(coeffs.len() >> coset_sz) * expansion,
37-
1 << coset_sz,
39+
&coeffs_refs,
40+
&[],
41+
coeffs[0].len() * expansion,
3842
))
3943
});
4044
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 6fd50adb2eacc5a44429f8aeb8bae55b4a05ae2ac74432e184fa91729f375276 # shrinks to seed = 0, config = Config { commit: Config { embedding: Identity { field: FieldInfo { characteristic: [255, 255, 255, 255, 0, 0, 0, 1], extension_degree: 1 } }, num_vectors: 1, vector_size: 0, mask_length: 0, codeword_length: 1, interleaving_depth: 1, matrix_commit: Config { element_type: FieldInfo { characteristic: [255, 255, 255, 255, 0, 0, 0, 1], extension_degree: 1 }, num_cols: 1, leaf_hash_id: 09459020f451874a1b399819d079632cc0f9263b1486c423173c6e15d8e2d61d, merkle_tree: Config { num_leaves: 1, layers: [] } }, johnson_slack: 0.0, in_domain_samples: 0, out_domain_samples: 0, deduplicate_in_domain: false }, sumcheck: Config { field: FieldInfo { characteristic: [255, 255, 255, 255, 0, 0, 0, 1], extension_degree: 1 }, initial_size: 0, round_pow: Config { hash_id: 03e01749ebcc0477924254eb482066b864a8dd4d77252464ca6f5b6f5cc05b4c, threshold: 18446744073709551615 }, num_rounds: 0 }, masked: false }

src/algebra/fields.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ impl<F: Field> TypeInfo for F {
6565
#[derive(MontConfig)]
6666
#[modulus = "21888242871839275222246405745257275088548364400416034343698204186575808495617"]
6767
#[generator = "5"]
68+
#[small_subgroup_base = "3"]
69+
#[small_subgroup_power = "2"]
6870
pub struct BN254Config;
6971
pub type Field256 = Fp256<MontBackend<BN254Config, 4>>;
7072

@@ -83,6 +85,8 @@ pub type Field128 = Fp128<MontBackend<FrConfig128, 2>>;
8385
#[derive(MontConfig)]
8486
#[modulus = "18446744069414584321"]
8587
#[generator = "7"]
88+
#[small_subgroup_base = "15"]
89+
#[small_subgroup_power = "1"]
8690
pub struct FConfig64;
8791
pub type Field64 = Fp64<MontBackend<FConfig64, 1>>;
8892

src/algebra/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod ntt;
77
pub mod sumcheck;
88

99
use ark_ff::{AdditiveGroup, Field};
10+
use ark_std::rand::{distributions::Standard, prelude::Distribution, Rng};
1011
pub use multilinear::{eval_eq, mixed_multilinear_extend, multilinear_extend};
1112
pub use multilinear_point::MultilinearPoint;
1213
#[cfg(feature = "parallel")]
@@ -15,6 +16,15 @@ use rayon::prelude::*;
1516
use self::embedding::Embedding;
1617
#[cfg(feature = "parallel")]
1718
use crate::utils::workload_size;
19+
use crate::utils::zip_strict;
20+
21+
pub fn random_vector<R, F: Field>(rng: &mut R, length: usize) -> Vec<F>
22+
where
23+
R: Rng,
24+
Standard: Distribution<F>,
25+
{
26+
(0..length).map(|_| rng.gen()).collect::<Vec<F>>()
27+
}
1828

1929
pub fn geometric_sequence<F: Field>(base: F, length: usize) -> Vec<F> {
2030
let mut result = Vec::with_capacity(length);
@@ -51,6 +61,21 @@ pub fn lift<M: Embedding>(embedding: &M, source: &[M::Source]) -> Vec<M::Target>
5161
result
5262
}
5363

64+
pub fn scalar_mul<F: Field>(vector: &mut [F], weight: F) {
65+
for value in vector.iter_mut() {
66+
*value *= weight;
67+
}
68+
}
69+
70+
/// Scalar mul add into new vector.
71+
///
72+
/// Returns r such that r[i] = a[i] + c · b[i].
73+
pub fn scalar_mul_add_new<F: Field>(a: &[F], c: F, b: &[F]) -> Vec<F> {
74+
zip_strict(a.iter(), b.iter())
75+
.map(|(a, b)| *a + c * *b)
76+
.collect::<Vec<F>>()
77+
}
78+
5479
pub fn scalar_mul_add<F: Field>(accumulator: &mut [F], weight: F, vector: &[F]) {
5580
mixed_scalar_mul_add(
5681
&embedding::Identity::<F>::new(),

src/algebra/multilinear.rs

Lines changed: 173 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ark_ff::Field;
1+
use ark_ff::{AdditiveGroup, Field};
22

33
use crate::algebra::embedding::{Embedding, Identity};
44

@@ -8,75 +8,146 @@ pub fn multilinear_extend<F: Field>(evals: &[F], point: &[F]) -> F {
88
}
99

1010
/// Evaluate the multi-linear extension of `evals` in `point`.
11+
///
12+
/// Supports implicit zero-padding: when `evals.len() < 1 << point.len()`,
13+
/// the missing tail entries are treated as zeros.
14+
#[allow(clippy::too_many_lines)]
1115
pub fn mixed_multilinear_extend<M: Embedding>(
1216
embedding: &M,
1317
evals: &[M::Source],
1418
point: &[M::Target],
1519
) -> M::Target {
16-
assert_eq!(evals.len(), 1 << point.len());
17-
18-
// Helper to compute (a + (b - a) * c) efficiently with a, b in source field.
19-
let mixed = |a, b, c| embedding.mixed_add(embedding.mixed_mul(c, b - a), a);
20-
21-
match point {
22-
[] => embedding.map(evals[0]),
23-
[x] => mixed(evals[0], evals[1], *x),
24-
[x0, x1] => {
25-
let a0 = mixed(evals[0], evals[1], *x1);
26-
let a1 = mixed(evals[2], evals[3], *x1);
27-
a0 + (a1 - a0) * *x0
20+
#[inline]
21+
fn eval_exact<M: Embedding>(
22+
embedding: &M,
23+
evals: &[M::Source],
24+
point: &[M::Target],
25+
) -> M::Target {
26+
debug_assert_eq!(evals.len(), 1 << point.len());
27+
28+
// Helper to compute (a + (b - a) * c) efficiently with a, b in source field.
29+
let mixed = |a, b, c| embedding.mixed_add(embedding.mixed_mul(c, b - a), a);
30+
31+
match point {
32+
[] => embedding.map(evals[0]),
33+
[x] => mixed(evals[0], evals[1], *x),
34+
[x0, x1] => {
35+
let a0 = mixed(evals[0], evals[1], *x1);
36+
let a1 = mixed(evals[2], evals[3], *x1);
37+
a0 + (a1 - a0) * *x0
38+
}
39+
[x0, x1, x2] => {
40+
let a00 = mixed(evals[0], evals[1], *x2);
41+
let a01 = mixed(evals[2], evals[3], *x2);
42+
let a10 = mixed(evals[4], evals[5], *x2);
43+
let a11 = mixed(evals[6], evals[7], *x2);
44+
let a0 = a00 + (a01 - a00) * *x1;
45+
let a1 = a10 + (a11 - a10) * *x1;
46+
a0 + (a1 - a0) * *x0
47+
}
48+
[x0, x1, x2, x3] => {
49+
let a000 = mixed(evals[0], evals[1], *x3);
50+
let a001 = mixed(evals[2], evals[3], *x3);
51+
let a010 = mixed(evals[4], evals[5], *x3);
52+
let a011 = mixed(evals[6], evals[7], *x3);
53+
let a100 = mixed(evals[8], evals[9], *x3);
54+
let a101 = mixed(evals[10], evals[11], *x3);
55+
let a110 = mixed(evals[12], evals[13], *x3);
56+
let a111 = mixed(evals[14], evals[15], *x3);
57+
let a00 = a000 + (a001 - a000) * *x2;
58+
let a01 = a010 + (a011 - a010) * *x2;
59+
let a10 = a100 + (a101 - a100) * *x2;
60+
let a11 = a110 + (a111 - a110) * *x2;
61+
let a0 = a00 + (a01 - a00) * *x1;
62+
let a1 = a10 + (a11 - a10) * *x1;
63+
a0 + (a1 - a0) * *x0
64+
}
65+
[x, tail @ ..] => {
66+
let (f0, f1) = evals.split_at(evals.len() / 2);
67+
#[cfg(not(feature = "parallel"))]
68+
let (f0, f1) = (
69+
eval_exact(embedding, f0, tail),
70+
eval_exact(embedding, f1, tail),
71+
);
72+
73+
#[cfg(feature = "parallel")]
74+
let (f0, f1) = {
75+
use crate::utils::workload_size;
76+
if evals.len() > workload_size::<M::Source>() {
77+
rayon::join(
78+
|| eval_exact(embedding, f0, tail),
79+
|| eval_exact(embedding, f1, tail),
80+
)
81+
} else {
82+
(
83+
eval_exact(embedding, f0, tail),
84+
eval_exact(embedding, f1, tail),
85+
)
86+
}
87+
};
88+
89+
f0 + (f1 - f0) * *x
90+
}
2891
}
29-
[x0, x1, x2] => {
30-
let a00 = mixed(evals[0], evals[1], *x2);
31-
let a01 = mixed(evals[2], evals[3], *x2);
32-
let a10 = mixed(evals[4], evals[5], *x2);
33-
let a11 = mixed(evals[6], evals[7], *x2);
34-
let a0 = a00 + (a01 - a00) * *x1;
35-
let a1 = a10 + (a11 - a10) * *x1;
36-
a0 + (a1 - a0) * *x0
92+
}
93+
94+
#[inline]
95+
fn eval_partial<M: Embedding>(
96+
embedding: &M,
97+
evals: &[M::Source],
98+
point: &[M::Target],
99+
) -> M::Target {
100+
let size = 1 << point.len();
101+
debug_assert!(evals.len() <= size);
102+
if evals.is_empty() {
103+
return M::Target::ZERO;
37104
}
38-
[x0, x1, x2, x3] => {
39-
let a000 = mixed(evals[0], evals[1], *x3);
40-
let a001 = mixed(evals[2], evals[3], *x3);
41-
let a010 = mixed(evals[4], evals[5], *x3);
42-
let a011 = mixed(evals[6], evals[7], *x3);
43-
let a100 = mixed(evals[8], evals[9], *x3);
44-
let a101 = mixed(evals[10], evals[11], *x3);
45-
let a110 = mixed(evals[12], evals[13], *x3);
46-
let a111 = mixed(evals[14], evals[15], *x3);
47-
let a00 = a000 + (a001 - a000) * *x2;
48-
let a01 = a010 + (a011 - a010) * *x2;
49-
let a10 = a100 + (a101 - a100) * *x2;
50-
let a11 = a110 + (a111 - a110) * *x2;
51-
let a0 = a00 + (a01 - a00) * *x1;
52-
let a1 = a10 + (a11 - a10) * *x1;
53-
a0 + (a1 - a0) * *x0
105+
if evals.len() == size {
106+
return eval_exact(embedding, evals, point);
54107
}
55-
[x, tail @ ..] => {
56-
let (f0, f1) = evals.split_at(evals.len() / 2);
57-
#[cfg(not(feature = "parallel"))]
58-
let (f0, f1) = (
59-
mixed_multilinear_extend(embedding, f0, tail),
60-
mixed_multilinear_extend(embedding, f1, tail),
61-
);
62-
#[cfg(feature = "parallel")]
63-
let (f0, f1) = {
64-
use crate::utils::workload_size;
65-
if evals.len() > workload_size::<M::Source>() {
66-
rayon::join(
67-
|| mixed_multilinear_extend(embedding, f0, tail),
68-
|| mixed_multilinear_extend(embedding, f1, tail),
69-
)
70-
} else {
71-
(
72-
mixed_multilinear_extend(embedding, f0, tail),
73-
mixed_multilinear_extend(embedding, f1, tail),
74-
)
108+
109+
match point {
110+
[] => embedding.map(evals[0]),
111+
[x, tail @ ..] => {
112+
let half = size / 2;
113+
114+
// Only low half has data; high half is all implicit zeros.
115+
if evals.len() <= half {
116+
let f0 = eval_partial(embedding, evals, tail);
117+
return f0 * (M::Target::ONE - *x);
75118
}
76-
};
77-
f0 + (f1 - f0) * *x
119+
120+
// Low subtree is exact/full, high subtree is partial.
121+
let (low, high) = evals.split_at(half);
122+
123+
#[cfg(not(feature = "parallel"))]
124+
let (f0, f1) = (
125+
eval_exact(embedding, low, tail),
126+
eval_partial(embedding, high, tail),
127+
);
128+
129+
#[cfg(feature = "parallel")]
130+
let (f0, f1) = {
131+
use crate::utils::workload_size;
132+
if evals.len() > workload_size::<M::Source>() {
133+
rayon::join(
134+
|| eval_exact(embedding, low, tail),
135+
|| eval_partial(embedding, high, tail),
136+
)
137+
} else {
138+
(
139+
eval_exact(embedding, low, tail),
140+
eval_partial(embedding, high, tail),
141+
)
142+
}
143+
};
144+
145+
f0 + (f1 - f0) * *x
146+
}
78147
}
79148
}
149+
150+
eval_partial(embedding, evals, point)
80151
}
81152

82153
/// Accumulates a scaled evaluation of the equality function.
@@ -88,7 +159,7 @@ pub fn mixed_multilinear_extend<M: Embedding>(
88159
/// eq(X) = ∏ (1 - X_i + 2X_i z_i)
89160
/// ```
90161
///
91-
/// where `z_i` are the points.
162+
/// where `z_i` are the points.
92163
pub fn eval_eq<F: Field>(accumulator: &mut [F], point: &[F], scalar: F) {
93164
assert_eq!(accumulator.len(), 1 << point.len());
94165
if let [x0, xs @ ..] = point {
@@ -110,3 +181,45 @@ pub fn eval_eq<F: Field>(accumulator: &mut [F], point: &[F], scalar: F) {
110181
accumulator[0] += scalar;
111182
}
112183
}
184+
185+
#[cfg(test)]
186+
mod tests {
187+
use ark_std::rand::{rngs::StdRng, SeedableRng};
188+
use proptest::proptest;
189+
190+
use super::*;
191+
use crate::algebra::{random_vector, sumcheck::tests::zero_pad};
192+
193+
pub type F = crate::algebra::fields::Field64;
194+
195+
#[test]
196+
fn test_multilinear_zero_extend() {
197+
proptest!(|(seed:u64, length in 0_usize..(1 << 14))| {
198+
let mut rng = StdRng::seed_from_u64(seed);
199+
let vector: Vec<F> = random_vector(&mut rng, length);
200+
let extended_vector = zero_pad(&vector);
201+
let num_variables = length.next_power_of_two().trailing_zeros();
202+
let point = random_vector(&mut rng, num_variables as usize);
203+
assert_eq!(
204+
multilinear_extend(&vector, &point),
205+
multilinear_extend(&extended_vector, &point)
206+
);
207+
});
208+
}
209+
210+
#[test]
211+
fn test_multilinear_extra_variables() {
212+
proptest!(|(seed:u64, length in 0_usize..(1 << 10), excess_variables in 0_usize..3)| {
213+
let mut rng = StdRng::seed_from_u64(seed);
214+
let vector: Vec<F> = random_vector(&mut rng, length);
215+
let num_variables = length.next_power_of_two().trailing_zeros() as usize + excess_variables;
216+
let point = random_vector(&mut rng, num_variables);
217+
let mut extended_vector = vector.clone();
218+
extended_vector.resize(1 << num_variables, F::ZERO);
219+
assert_eq!(
220+
multilinear_extend(&vector, &point),
221+
multilinear_extend(&extended_vector, &point)
222+
);
223+
});
224+
}
225+
}

0 commit comments

Comments
 (0)