Skip to content

Commit e42cf87

Browse files
TomWambsgansclaude
andcommitted
Merge branch 'main' into whir-split-eq
Reconcile main's arena-allocator migration (#247, "remove global_allocator in zk-alloc") with the WHIR split-eq refactor on this branch. Conflicts resolved: - quotient_gkr/layers.rs: materialise_in_full keeps the branch's pub(super) visibility and adopts main's ArenaVec return type. - whir/utils.rs: keep the split-eq LSB-cols FFT-prep layout but write into an ArenaVec via par_fill (main's allocator) instead of par_map_collect's Vec; drop the now-unused log2_strict_usize import. - whir/open.rs: keep the branch's final_round / open_merkle_tree_at_challenges refactor and SVO sumcheck; drop main's combine_statement (deleted on this branch). Propagate main's ArenaVec migration through the new SVO helpers (SumcheckSingle.weights, lsb_fold, fold_by_tensor, build_post_svo_weights) to avoid Vec<->ArenaVec copies in the prover hot path. Auto-merges verified coherent: eq_mle.rs composes the branch's compute_eval_eq_base_batched rename/unpacked-output with main's ArenaVec buffers; commit.rs changes are in disjoint methods. cargo fmt / clippy clean; cargo testall (release) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 parents a2d1bd0 + 95a34a2 commit e42cf87

82 files changed

Lines changed: 1803 additions & 1129 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,6 @@ include_dir = "0.7"
8383

8484
[features]
8585
prox-gaps-conjecture = ["rec_aggregation/prox-gaps-conjecture"]
86-
# Build with the plain system allocator instead of zk-alloc (for comparison/debugging).
87-
standard-alloc = ["rec_aggregation/standard-alloc"]
8886

8987
[dependencies]
9088
clap.workspace = true
@@ -102,3 +100,4 @@ system-info.workspace = true
102100

103101
[profile.release]
104102
lto = "thin"
103+
codegen-units = 1

SECURITY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Security Policy
2+
3+
For now, leanVM is not used in production.
4+
Security reports are very much appreciated: please open an issue or submit a pull request.
5+
A bounty program is expected to launch soon.

crates/backend/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ tracing.workspace = true
1515
fiat-shamir = { path = "fiat-shamir", package = "mt-fiat-shamir" }
1616
koala-bear = { path = "koala-bear", package = "mt-koala-bear" }
1717
utils = { path = "utils", package = "utils" }
18+
zk-alloc.workspace = true

crates/backend/air/src/symbolic.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,15 @@ fn alloc_node<F: Field>(node: SymbolicNode<F>) -> u32 {
9292
})
9393
}
9494

95-
pub fn get_node<F: Field>(idx: u32) -> SymbolicNode<F> {
95+
/// # Safety
96+
/// `idx` must be an offset returned by `alloc_node::<F>` for the current (same `F`, uncleared) arena.
97+
pub unsafe fn get_node<F: Field>(idx: u32) -> SymbolicNode<F> {
9698
ARENA.with(|arena| {
9799
let bytes = arena.borrow();
100+
assert!(
101+
idx as usize + std::mem::size_of::<SymbolicNode<F>>() <= bytes.len(),
102+
"arena index out of bounds"
103+
);
98104
unsafe { std::ptr::read_unaligned(bytes.as_ptr().add(idx as usize) as *const SymbolicNode<F>) }
99105
})
100106
}

crates/backend/fiat-shamir/src/merkle_pruning.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ impl<Data: Clone, F: Clone> MerklePaths<Data, F> {
8383
}
8484
}
8585

86+
const MAX_MERKLE_PATHS: usize = 1 << 10;
87+
8688
impl<Data: Clone, F: Clone> PrunedMerklePaths<Data, F> {
8789
pub fn restore(
8890
mut self,
@@ -98,6 +100,9 @@ impl<Data: Clone, F: Clone> PrunedMerklePaths<Data, F> {
98100
if h >= 32 {
99101
return None; // prevent DoS with huge tree height
100102
}
103+
if n > MAX_MERKLE_PATHS {
104+
return None; // prevent DoS with huge number of paths
105+
}
101106
if self.n_trailing_zeros > 1024 {
102107
return None; // prevent DoS with huge leaf data
103108
}
@@ -117,8 +122,8 @@ impl<Data: Clone, F: Clone> PrunedMerklePaths<Data, F> {
117122
};
118123
let skip = |i: usize| self.paths.get(i + 1).map(|p| lca_level(self.paths[i].0, p.0) - 1);
119124

120-
// Backward pass: compute subtree hashes needed to restore skipped siblings
121-
let mut subtree_hashes: Vec<Vec<[F; DIGEST_LEN_FE]>> = vec![vec![]; n];
125+
// Backward pass: each path donates one subtree hash (the sibling its predecessor omitted).
126+
let mut donated: Vec<Option<[F; DIGEST_LEN_FE]>> = vec![None; n];
122127

123128
for i in (0..n).rev() {
124129
let (leaf_idx, ref stored) = self.paths[i];
@@ -128,10 +133,12 @@ impl<Data: Clone, F: Clone> PrunedMerklePaths<Data, F> {
128133
let mut stored = stored.iter();
129134
let mut hash = hash_leaf(self.leaf_data.get(i)?);
130135

131-
subtree_hashes[i].push(hash.clone());
132136
for lvl in 0..levels(i) {
137+
if lvl + 1 == levels(i) {
138+
donated[i] = Some(hash.clone()); // top level kept: this is predecessor i-1's missing sibling
139+
}
133140
let sibling = if skip(i) == Some(lvl) {
134-
subtree_hashes.get(i + 1)?.get(lvl)?.clone()
141+
donated[i + 1].clone()? // contributed by successor path i+1
135142
} else {
136143
stored.next()?.clone()
137144
};
@@ -140,7 +147,6 @@ impl<Data: Clone, F: Clone> PrunedMerklePaths<Data, F> {
140147
} else {
141148
hash_combine(&sibling, &hash)
142149
};
143-
subtree_hashes[i].push(hash.clone());
144150
}
145151
if stored.next().is_some() {
146152
return None;
@@ -157,7 +163,7 @@ impl<Data: Clone, F: Clone> PrunedMerklePaths<Data, F> {
157163
let mut siblings = Vec::with_capacity(h);
158164
for lvl in 0..levels(i) {
159165
let sibling = if skip(i) == Some(lvl) {
160-
subtree_hashes.get(i + 1)?.get(lvl)?.clone()
166+
donated[i + 1].clone()? // contributed by successor path i+1
161167
} else {
162168
stored.next()?.clone()
163169
};

crates/backend/koala-bear/src/quintic_extension/packed_extension.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ impl<F: Field, PF: PackedField<Scalar = F>> From<QuinticExtensionField<F>> for P
4848
#[inline]
4949
fn from(x: QuinticExtensionField<F>) -> Self {
5050
Self {
51-
value: x.value.map(Into::into),
51+
value: array::from_fn(|i| x.value[i].into()),
5252
}
5353
}
5454
}
@@ -117,10 +117,11 @@ macro_rules! impl_packed_ext_scalar_ops {
117117
impl Mul<KoalaBear> for PackedQuinticExtensionField<KoalaBear, $pf> {
118118
type Output = Self;
119119
#[inline]
120-
fn mul(self, rhs: KoalaBear) -> Self {
121-
Self {
122-
value: self.value.map(|x| x * rhs),
120+
fn mul(mut self, rhs: KoalaBear) -> Self {
121+
for v in &mut self.value {
122+
*v *= rhs;
123123
}
124+
self
124125
}
125126
}
126127

@@ -281,10 +282,12 @@ where
281282
type Output = Self;
282283

283284
#[inline]
284-
fn neg(self) -> Self {
285-
Self {
286-
value: self.value.map(PF::neg),
285+
fn neg(mut self) -> Self {
286+
// Loop, not `self.value.map(..)`: avoids a thin-LTO de-inlined `Wrapped` closure.
287+
for v in &mut self.value {
288+
*v = -*v;
287289
}
290+
self
288291
}
289292
}
290293

@@ -478,7 +481,7 @@ where
478481

479482
#[inline(always)]
480483
fn mul(self, rhs: QuinticExtensionField<F>) -> Self {
481-
let b: [PF; 5] = rhs.value.map(|x| x.into());
484+
let b: [PF; 5] = array::from_fn(|i| rhs.value[i].into());
482485
Self {
483486
value: super::extension::quintic_mul(&self.value, &b, PF::dot_product::<5>),
484487
}
@@ -493,10 +496,11 @@ where
493496
type Output = Self;
494497

495498
#[inline]
496-
fn mul(self, rhs: PF) -> Self {
497-
Self {
498-
value: self.value.map(|x| x * rhs),
499+
fn mul(mut self, rhs: PF) -> Self {
500+
for v in &mut self.value {
501+
*v *= rhs;
499502
}
503+
self
500504
}
501505
}
502506

crates/backend/parallel/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ thread_local! {
5656

5757
/// Calling worker's id in `0..NUM_THREADS` (`0` off-pool).
5858
#[must_use]
59-
pub fn current_worker_id() -> usize {
59+
pub(crate) fn current_worker_id() -> usize {
6060
WORKER_ID.with(Cell::get)
6161
}
6262

crates/backend/poly/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ field = { path = "../field", package = "mt-field" }
88
utils = { path = "../utils", package = "utils" }
99
system-info.workspace = true
1010
parallel.workspace = true
11-
tracing.workspace = true
11+
zk-alloc.workspace = true
12+
1213
itertools.workspace = true
1314
rand.workspace = true
1415
serde.workspace = true

crates/backend/poly/src/eq_mle.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::{EFPacking, PF};
33
use ::utils::{iter_array_chunks_padded, log2_ceil_usize, log2_strict_usize};
44
use field::*;
55
use system_info::NUM_THREADS;
6+
use zk_alloc::ArenaVec;
67

78
const LOG_NUM_THREADS: usize = log2_ceil_usize(NUM_THREADS);
89
const LOG_BATCHED_TILE_SIZE: usize = 14;
@@ -59,26 +60,29 @@ fn par_eval_eq<In, Buf, Out>(
5960
/// defined on the boolean hypercube by: ∀ (x_1, ..., x_n) ∈ {0, 1}^n,
6061
/// P(x_1, ..., x_n) = Π_{i=1}^{n} (x_i.α_i + (1 - x_i).(1 - α_i))
6162
/// (often denoted as P(x) = eq(x, evals))
62-
pub fn eval_eq<F: ExtensionField<PF<F>>>(eval: &[F]) -> Vec<F> {
63+
/// Returns an arena-backed table (see [`ArenaVec`]). Every eq table is phase-local proof scratch
64+
/// (consumed within the proving phase that built it, or system-backed when the arena is inactive,
65+
/// e.g. in the verifier), so it never outlives a `begin_phase()` reset.
66+
pub fn eval_eq<F: ExtensionField<PF<F>>>(eval: &[F]) -> ArenaVec<F> {
6367
eval_eq_scaled(eval, F::ONE)
6468
}
6569

66-
pub fn eval_eq_scaled<F: ExtensionField<PF<F>>>(eval: &[F], scalar: F) -> Vec<F> {
70+
pub fn eval_eq_scaled<F: ExtensionField<PF<F>>>(eval: &[F], scalar: F) -> ArenaVec<F> {
6771
// Alloc memory without initializing it to zero.
68-
// This is safe because we overwrite it inside `eval_eq`.
69-
let mut out = unsafe { uninitialized_vec(1 << eval.len()) };
72+
// This is safe because we overwrite it inside `compute_eval_eq`.
73+
let mut out = unsafe { ArenaVec::uninitialized(1 << eval.len()) };
7074
compute_eval_eq::<PF<F>, F, false>(eval, &mut out, scalar);
7175
out
7276
}
7377

74-
pub fn eval_eq_packed<F: ExtensionField<PF<F>>>(eval: &[F]) -> Vec<EFPacking<F>> {
78+
pub fn eval_eq_packed<F: ExtensionField<PF<F>>>(eval: &[F]) -> ArenaVec<EFPacking<F>> {
7579
eval_eq_packed_scaled(eval, F::ONE)
7680
}
7781

78-
pub fn eval_eq_packed_scaled<F: ExtensionField<PF<F>>>(eval: &[F], scalar: F) -> Vec<EFPacking<F>> {
82+
pub fn eval_eq_packed_scaled<F: ExtensionField<PF<F>>>(eval: &[F], scalar: F) -> ArenaVec<EFPacking<F>> {
7983
// Alloc memory without initializing it to zero.
80-
// This is safe because we overwrite it inside `eval_eq`.
81-
let mut out = unsafe { uninitialized_vec(1 << (eval.len() - packing_log_width::<F>())) };
84+
// This is safe because we overwrite it inside `compute_eval_eq_packed`.
85+
let mut out = unsafe { ArenaVec::uninitialized(1 << (eval.len() - packing_log_width::<F>())) };
8286
compute_eval_eq_packed::<F, false>(eval, &mut out, scalar);
8387
out
8488
}
@@ -105,7 +109,7 @@ where
105109
let packed = &mut out[selector >> shift];
106110
let mut unpacked: Vec<EF> = unpack_extension(&[*packed]);
107111
compute_sparse_eval_eq::<EF>(selector & ((1 << shift) - 1), eval, &mut unpacked, scalar);
108-
*packed = pack_extension(&unpacked)[0];
112+
*packed = pack_extension::<_, Vec<_>>(&unpacked)[0];
109113
return;
110114
}
111115

@@ -180,7 +184,7 @@ where
180184
let (log_chunks, n_chunks) = parallel_split();
181185
if eval.len() <= log_packing_width + 1 + log_chunks {
182186
// Small case: evaluate unpacked, then pack lanes into `out`.
183-
let mut unpacked = EF::zero_vec(1 << eval.len());
187+
let mut unpacked = unsafe { ArenaVec::zeroed(1 << eval.len()) };
184188
eval_eq_basic::<_, _, _, false>(eval, &mut unpacked, scalar);
185189
out.iter_mut()
186190
.zip(unpacked.chunks_exact(packing_width))
@@ -275,7 +279,7 @@ pub fn compute_eval_eq_base_packed<F, EF, const INITIALIZED: bool>(
275279
let (log_chunks, n_chunks) = parallel_split();
276280
if eval.len() <= log_packing_width + 1 + log_chunks {
277281
// Small case: evaluate unpacked, then pack lanes into `out`.
278-
let mut unpacked = EF::zero_vec(1 << eval.len());
282+
let mut unpacked = unsafe { ArenaVec::zeroed(1 << eval.len()) };
279283
eval_eq_basic::<_, _, _, false>(eval, &mut unpacked, scalar);
280284
out.iter_mut()
281285
.zip(unpacked.chunks_exact(packing_width))
@@ -340,7 +344,7 @@ where
340344
.map(|(eval, &scalar)| {
341345
let middle = &eval[n_prefix_levels..n - log_packing_width];
342346
let eq_suffix = packed_eq_poly::<F, F>(&eval[n - log_packing_width..], F::ONE);
343-
let mut eq_prefix: Vec<EF> = unsafe { uninitialized_vec(1 << n_prefix_levels) };
347+
let mut eq_prefix: ArenaVec<EF> = unsafe { ArenaVec::uninitialized(1 << n_prefix_levels) };
344348
eval_eq_basic::<F, F, EF, false>(&eval[..n_prefix_levels], &mut eq_prefix, scalar);
345349
(eq_prefix, middle, eq_suffix)
346350
})
@@ -875,7 +879,7 @@ pub fn compute_eval_eq_packed_dual<EF>(
875879

876880
let (log_chunks, n_chunks) = parallel_split();
877881
if eval_a.len() <= log_packing_width + 1 + log_chunks {
878-
let mut output_no_packing = EF::zero_vec(1 << eval_a.len());
882+
let mut output_no_packing = unsafe { ArenaVec::zeroed(1 << eval_a.len()) };
879883
eval_eq_basic::<_, _, _, false>(eval_a, &mut output_no_packing, scalar_a);
880884
eval_eq_basic::<_, _, _, true>(eval_b, &mut output_no_packing, scalar_b);
881885
out.iter_mut()
@@ -1130,7 +1134,7 @@ fn packed_eq_poly<F: Field, EF: ExtensionField<F>>(eval: &[EF], scalar: EF) -> E
11301134
debug_assert_eq!(F::Packing::WIDTH, 1 << eval.len());
11311135

11321136
// We build up the evaluations of the equality polynomial in buffer.
1133-
let mut buffer = EF::zero_vec(1 << eval.len());
1137+
let mut buffer = unsafe { ArenaVec::zeroed(1 << eval.len()) };
11341138
buffer[0] = scalar;
11351139

11361140
fill_buffer(eval.iter().rev(), &mut buffer);

0 commit comments

Comments
 (0)