Skip to main content

hekate_program/
expander.rs

1// SPDX-FileCopyrightText: 2026 Andrei Kochergin <andrei@oumuamua.dev>
2// SPDX-FileCopyrightText: 2026 Oumuamua Labs <info@oumuamua.dev>
3// SPDX-License-Identifier: AGPL-3.0-only
4
5use alloc::vec;
6use alloc::vec::Vec;
7use core::iter::repeat_n;
8use hekate_core::config::{Config, FoldShape, MIN_PRODUCTION_BITS};
9use hekate_core::errors::Error;
10use hekate_core::poly::PolyVariant;
11use hekate_core::trace::{ColumnType, Trace, TraceColumn, TraceCompatibleField};
12use hekate_core::utils::{compute_split_vars, support_floor_vars};
13use hekate_math::{
14    Bit, Block8, Block16, Block32, Block64, Block128, Flat, HardwareField, TowerField,
15};
16
17pub const RING_BLIND_BITS: usize = 128;
18
19/// Serializable expansion step descriptor.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum ExpansionEntry {
22    ExpandBits {
23        count: usize,
24        storage: ColumnType,
25    },
26    PassThrough {
27        count: usize,
28        storage: ColumnType,
29    },
30    ControlBits {
31        count: usize,
32    },
33    ReusePassThrough {
34        phy_col_start: usize,
35        count: usize,
36        storage: ColumnType,
37    },
38    ReuseExpandBits {
39        phy_col_start: usize,
40        count: usize,
41        storage: ColumnType,
42    },
43}
44
45/// Physical-to-virtual column mapping rule.
46#[derive(Clone, Copy, Debug)]
47enum EntryKind {
48    /// N physical columns to N ×
49    /// bit_width virtual Bit columns.
50    ExpandBits { count: usize, storage: ColumnType },
51
52    /// N physical columns to N virtual
53    /// columns of the same type.
54    PassThrough { count: usize, storage: ColumnType },
55
56    /// N physical Bit columns
57    /// to N virtual Bit columns.
58    ControlBits { count: usize },
59}
60
61impl EntryKind {
62    fn count(&self) -> usize {
63        match self {
64            Self::ExpandBits { count, .. }
65            | Self::PassThrough { count, .. }
66            | Self::ControlBits { count } => *count,
67        }
68    }
69
70    fn storage(&self) -> ColumnType {
71        match self {
72            Self::ExpandBits { storage, .. } | Self::PassThrough { storage, .. } => *storage,
73            Self::ControlBits { .. } => ColumnType::Bit,
74        }
75    }
76}
77
78/// Pre-computed expansion entry
79/// with frozen byte/column offsets.
80#[derive(Clone, Copy, Debug)]
81struct CompiledEntry {
82    /// Physical column index,
83    /// relative to `phy_start_idx`.
84    phy_col_start: usize,
85
86    /// Byte offset in the committed row.
87    byte_offset: usize,
88    kind: EntryKind,
89
90    /// True if this entry reuses physical
91    /// columns declared by a prior entry.
92    reuse: bool,
93}
94
95/// Declarative physical->virtual
96/// column expander for chiplets.
97///
98/// Built once per chiplet, generates
99/// `virtual_layout()`, `parse_row()`,
100/// and `expand_variants()` from the
101/// same packing specification.
102#[derive(Clone, Debug)]
103pub struct VirtualExpander {
104    entries: Vec<CompiledEntry>,
105    num_virtual: usize,
106    num_physical: usize,
107    physical_row_bytes: usize,
108    virtual_layout: Vec<ColumnType>,
109    error: Option<Error>,
110}
111
112impl VirtualExpander {
113    pub fn new() -> Self {
114        Self {
115            entries: Vec::new(),
116            num_virtual: 0,
117            num_physical: 0,
118            physical_row_bytes: 0,
119            virtual_layout: Vec::new(),
120            error: None,
121        }
122    }
123
124    /// Finalize the builder. Returns `Err` if any
125    /// builder step recorded a validation error.
126    pub fn build(self) -> Result<Self, Error> {
127        match self.error {
128            Some(e) => Err(e),
129            None => Ok(self),
130        }
131    }
132
133    /// N physical columns of `storage` type
134    /// to N × bit_width virtual Bit columns.
135    pub fn expand_bits(mut self, count: usize, storage: ColumnType) -> Self {
136        if self.error.is_some() {
137            return self;
138        }
139
140        let bits_per = match expand_bit_width(storage) {
141            Ok(v) => v,
142            Err(e) => {
143                self.error = Some(e);
144                return self;
145            }
146        };
147
148        let byte_offset = self.physical_row_bytes;
149        let phy_col_start = self.num_physical;
150
151        self.entries.push(CompiledEntry {
152            phy_col_start,
153            byte_offset,
154            kind: EntryKind::ExpandBits { count, storage },
155            reuse: false,
156        });
157
158        let virt_count = count * bits_per;
159        self.virtual_layout
160            .extend(repeat_n(ColumnType::Bit, virt_count));
161
162        self.num_virtual += virt_count;
163        self.num_physical += count;
164        self.physical_row_bytes += count * storage.byte_size();
165
166        self
167    }
168
169    /// N physical columns pass through
170    /// 1:1 as virtual columns.
171    pub fn pass_through(mut self, count: usize, storage: ColumnType) -> Self {
172        let byte_offset = self.physical_row_bytes;
173        let phy_col_start = self.num_physical;
174
175        self.entries.push(CompiledEntry {
176            phy_col_start,
177            byte_offset,
178            kind: EntryKind::PassThrough { count, storage },
179            reuse: false,
180        });
181
182        self.virtual_layout.extend(repeat_n(storage, count));
183
184        self.num_virtual += count;
185        self.num_physical += count;
186        self.physical_row_bytes += count * storage.byte_size();
187
188        self
189    }
190
191    /// N physical Bit columns pass through 1:1.
192    pub fn control_bits(mut self, count: usize) -> Self {
193        let byte_offset = self.physical_row_bytes;
194        let phy_col_start = self.num_physical;
195
196        self.entries.push(CompiledEntry {
197            phy_col_start,
198            byte_offset,
199            kind: EntryKind::ControlBits { count },
200            reuse: false,
201        });
202
203        self.virtual_layout.extend(repeat_n(ColumnType::Bit, count));
204
205        self.num_virtual += count;
206        self.num_physical += count;
207        self.physical_row_bytes += count;
208
209        self
210    }
211
212    /// Emit pass-through for columns already
213    /// declared by a prior fresh entry.
214    /// Does not advance the physical cursor.
215    pub fn reuse_pass_through(mut self, phy_col_start: usize, count: usize) -> Self {
216        if self.error.is_some() {
217            return self;
218        }
219
220        if phy_col_start + count > self.num_physical {
221            self.error = Some(Error::Protocol {
222                protocol: "virtual_expand",
223                message: "reuse_pass_through: range exceeds declared physical columns",
224            });
225            return self;
226        }
227
228        let (byte_offset, storage) = match self.find_phy_source(phy_col_start, count) {
229            Ok(v) => v,
230            Err(e) => {
231                self.error = Some(e);
232                return self;
233            }
234        };
235
236        self.entries.push(CompiledEntry {
237            phy_col_start,
238            byte_offset,
239            kind: EntryKind::PassThrough { count, storage },
240            reuse: true,
241        });
242
243        self.virtual_layout.extend(repeat_n(storage, count));
244
245        self.num_virtual += count;
246
247        self
248    }
249
250    /// Emit bit-expansion for columns already
251    /// declared by a prior fresh entry.
252    /// Does not advance the physical cursor.
253    pub fn reuse_expand_bits(mut self, phy_col_start: usize, count: usize) -> Self {
254        if self.error.is_some() {
255            return self;
256        }
257
258        if phy_col_start + count > self.num_physical {
259            self.error = Some(Error::Protocol {
260                protocol: "virtual_expand",
261                message: "reuse_expand_bits: range exceeds declared physical columns",
262            });
263            return self;
264        }
265
266        let (byte_offset, storage) = match self.find_phy_source(phy_col_start, count) {
267            Ok(v) => v,
268            Err(e) => {
269                self.error = Some(e);
270                return self;
271            }
272        };
273
274        let bits_per = match expand_bit_width(storage) {
275            Ok(v) => v,
276            Err(e) => {
277                self.error = Some(e);
278                return self;
279            }
280        };
281
282        self.entries.push(CompiledEntry {
283            phy_col_start,
284            byte_offset,
285            kind: EntryKind::ExpandBits { count, storage },
286            reuse: true,
287        });
288
289        let virt_count = count * bits_per;
290        self.virtual_layout
291            .extend(repeat_n(ColumnType::Bit, virt_count));
292
293        self.num_virtual += virt_count;
294
295        self
296    }
297
298    /// Append another expander's entries after this
299    /// one's, shifting its physical references onto
300    /// this expander's coordinate space.
301    pub fn append(mut self, other: &VirtualExpander) -> Self {
302        if self.error.is_some() {
303            return self;
304        }
305
306        if let Some(e) = &other.error {
307            self.error = Some(*e);
308            return self;
309        }
310
311        let phy_shift = self.num_physical;
312        let byte_shift = self.physical_row_bytes;
313
314        for entry in &other.entries {
315            self.entries.push(CompiledEntry {
316                phy_col_start: entry.phy_col_start + phy_shift,
317                byte_offset: entry.byte_offset + byte_shift,
318                kind: entry.kind,
319                reuse: entry.reuse,
320            });
321        }
322
323        self.virtual_layout.extend_from_slice(&other.virtual_layout);
324
325        self.num_virtual += other.num_virtual;
326        self.num_physical += other.num_physical;
327        self.physical_row_bytes += other.physical_row_bytes;
328
329        self
330    }
331
332    #[inline]
333    pub fn num_virtual_columns(&self) -> usize {
334        self.num_virtual
335    }
336
337    #[inline]
338    pub fn num_physical_columns(&self) -> usize {
339        self.num_physical
340    }
341
342    #[inline]
343    pub fn physical_row_bytes(&self) -> usize {
344        self.physical_row_bytes
345    }
346
347    #[inline]
348    pub fn virtual_layout(&self) -> &[ColumnType] {
349        &self.virtual_layout
350    }
351
352    /// Verifier-side:
353    /// parse committed physical row bytes
354    /// into virtual field elements.
355    pub fn parse_row<F: TraceCompatibleField>(
356        &self,
357        bytes: &[u8],
358        res: &mut Vec<Flat<F>>,
359    ) -> Result<(), Error> {
360        if bytes.len() != self.physical_row_bytes {
361            return Err(Error::Protocol {
362                protocol: "virtual_expand",
363                message: "parse_row: byte slice length mismatch",
364            });
365        }
366
367        res.reserve(self.num_virtual);
368
369        for entry in &self.entries {
370            let off = entry.byte_offset;
371            match entry.kind {
372                EntryKind::ExpandBits { count, storage } => {
373                    let bsz = storage.byte_size();
374                    let bits = expand_bit_width(storage)?;
375
376                    for i in 0..count {
377                        let start = off + i * bsz;
378                        for bit_idx in 0..bits {
379                            let bit = parse_tower_bit(storage, &bytes[start..start + bsz], bit_idx);
380                            res.push(Flat::from_raw(F::from(Bit::from(bit))));
381                        }
382                    }
383                }
384                EntryKind::PassThrough { count, storage } => {
385                    let bsz = storage.byte_size();
386                    for i in 0..count {
387                        let start = off + i * bsz;
388                        res.push(storage.parse_from_bytes(&bytes[start..start + bsz]));
389                    }
390                }
391                EntryKind::ControlBits { count } => {
392                    for i in 0..count {
393                        res.push(Flat::from_raw(F::from(Bit::from(bytes[off + i] & 1))));
394                    }
395                }
396            }
397        }
398
399        Ok(())
400    }
401
402    /// Prover-side:
403    /// expand physical `ColumnTrace`
404    /// into virtual `PolyVariant`s.
405    pub fn expand_variants<'a, F, T: Trace + ?Sized>(
406        &self,
407        trace: &'a T,
408        phy_start_idx: usize,
409    ) -> Result<Vec<PolyVariant<'a, F>>, Error>
410    where
411        F: TraceCompatibleField + 'static,
412    {
413        let columns = trace.columns();
414
415        let mut variants = Vec::with_capacity(self.num_virtual);
416        for entry in &self.entries {
417            let base = phy_start_idx + entry.phy_col_start;
418            match entry.kind {
419                EntryKind::ExpandBits { count, storage } => {
420                    let bits = expand_bit_width(storage)?;
421                    for i in 0..count {
422                        let col = columns.get(base + i).ok_or(Error::Protocol {
423                            protocol: "virtual_expand",
424                            message: "missing physical column for ExpandBits",
425                        })?;
426
427                        for bit_idx in 0..bits {
428                            variants.push(expand_packed_bit(col, storage, bit_idx)?);
429                        }
430                    }
431                }
432                EntryKind::PassThrough { count, storage } => {
433                    for i in 0..count {
434                        let col = columns.get(base + i).ok_or(Error::Protocol {
435                            protocol: "virtual_expand",
436                            message: "missing physical column for PassThrough",
437                        })?;
438
439                        variants.push(expand_pass_through(col, storage)?);
440                    }
441                }
442                EntryKind::ControlBits { count } => {
443                    for i in 0..count {
444                        let col = columns.get(base + i).ok_or(Error::Protocol {
445                            protocol: "virtual_expand",
446                            message: "missing physical column for ControlBits",
447                        })?;
448                        let data = col.as_bit_slice().ok_or(Error::Protocol {
449                            protocol: "virtual_expand",
450                            message: "control column must be Bit",
451                        })?;
452
453                        variants.push(PolyVariant::BitSlice(data));
454                    }
455                }
456            }
457        }
458
459        Ok(variants)
460    }
461
462    /// Wire-format serialization descriptor.
463    pub fn expansion_entries(&self) -> Vec<ExpansionEntry> {
464        self.entries
465            .iter()
466            .map(|e| match (e.kind, e.reuse) {
467                (EntryKind::PassThrough { count, storage }, true) => {
468                    ExpansionEntry::ReusePassThrough {
469                        phy_col_start: e.phy_col_start,
470                        count,
471                        storage,
472                    }
473                }
474                (EntryKind::ExpandBits { count, storage }, true) => {
475                    ExpansionEntry::ReuseExpandBits {
476                        phy_col_start: e.phy_col_start,
477                        count,
478                        storage,
479                    }
480                }
481                (EntryKind::ExpandBits { count, storage }, false) => {
482                    ExpansionEntry::ExpandBits { count, storage }
483                }
484                (EntryKind::PassThrough { count, storage }, false) => {
485                    ExpansionEntry::PassThrough { count, storage }
486                }
487                (EntryKind::ControlBits { count }, _) => ExpansionEntry::ControlBits { count },
488            })
489            .collect()
490    }
491
492    pub(crate) fn whole_column(&self, virt: usize) -> Option<usize> {
493        let mut base = 0usize;
494        for entry in &self.entries {
495            let width = match entry.kind {
496                EntryKind::ExpandBits { count, storage } => count * storage.byte_size() * 8,
497                EntryKind::PassThrough { count, .. } | EntryKind::ControlBits { count } => count,
498            };
499
500            if virt < base + width {
501                return match entry.kind {
502                    EntryKind::ExpandBits { .. } => None,
503                    EntryKind::PassThrough { .. } | EntryKind::ControlBits { .. } => {
504                        Some(entry.phy_col_start + (virt - base))
505                    }
506                };
507            }
508
509            base += width;
510        }
511
512        None
513    }
514
515    // Fresh entries have phy_col_start == running_phy;
516    // reuse entries point backward.
517    fn find_phy_source(
518        &self,
519        target_start: usize,
520        target_count: usize,
521    ) -> Result<(usize, ColumnType), Error> {
522        let mut running_phy = 0usize;
523        for entry in &self.entries {
524            if entry.phy_col_start != running_phy {
525                continue;
526            }
527
528            let entry_count = entry.kind.count();
529            let entry_end = running_phy + entry_count;
530
531            if target_start >= running_phy && target_start + target_count <= entry_end {
532                let storage = entry.kind.storage();
533                let offset_in_entry = target_start - running_phy;
534
535                return Ok((
536                    entry.byte_offset + offset_in_entry * storage.byte_size(),
537                    storage,
538                ));
539            }
540
541            running_phy = entry_end;
542        }
543
544        Err(Error::Protocol {
545            protocol: "virtual_expand",
546            message: "reuse: source columns not found in any single fresh entry",
547        })
548    }
549}
550
551impl Default for VirtualExpander {
552    fn default() -> Self {
553        Self::new()
554    }
555}
556
557/// Maps the claimed virtual evals and the committed columns
558/// onto ring-switch binding units. `eta^k` runs once per
559/// unit (in claim order); a bit-expanded physical column
560/// is one Ring unit consuming its `bits` claims.
561pub struct RingSwitchPlan {
562    pub num_units: usize,
563    pub units: Vec<(bool, usize)>,
564    pub phys_rs: Vec<ColumnType>,
565    pub blind_slots: usize,
566    pub h_cols: usize,
567
568    phys_bit: Vec<Vec<usize>>,
569    phys_whole: Vec<Vec<usize>>,
570    h_whole: Vec<usize>,
571}
572
573impl RingSwitchPlan {
574    pub fn new(
575        layout: &[ColumnType],
576        entries: Option<&[ExpansionEntry]>,
577        num_blind: usize,
578        num_h: usize,
579    ) -> Result<Self, Error> {
580        let num_phys = layout.len();
581        let total = num_phys + num_blind;
582
583        let mut phys_bit = vec![Vec::new(); total];
584        let mut phys_whole = vec![Vec::new(); total];
585        let mut units: Vec<(bool, usize)> = Vec::new();
586
587        let mut phys_rs: Vec<ColumnType> = layout.iter().map(|ct| ct.rs_field()).collect();
588        phys_rs.extend((0..num_blind).map(|_| ColumnType::B128));
589
590        let bounds = |upper: usize| -> Result<(), Error> {
591            if upper > num_phys {
592                return Err(Error::Protocol {
593                    protocol: "ring_switch_plan",
594                    message: "expansion entry exceeds the physical column layout",
595                });
596            }
597
598            Ok(())
599        };
600
601        match entries {
602            Some(entries) => {
603                let mut running = 0usize;
604                for e in entries {
605                    match *e {
606                        ExpansionEntry::ExpandBits { count, storage } => {
607                            let bits = expand_bit_width(storage)?;
608
609                            bounds(running + count)?;
610
611                            for j in 0..count {
612                                phys_bit[running + j].push(units.len());
613                                units.push((true, bits));
614                            }
615
616                            running += count;
617                        }
618                        ExpansionEntry::PassThrough { count, .. }
619                        | ExpansionEntry::ControlBits { count } => {
620                            bounds(running + count)?;
621
622                            for j in 0..count {
623                                phys_whole[running + j].push(units.len());
624                                units.push((false, 1));
625                            }
626
627                            running += count;
628                        }
629                        ExpansionEntry::ReusePassThrough {
630                            phy_col_start,
631                            count,
632                            ..
633                        } => {
634                            bounds(phy_col_start + count)?;
635
636                            for j in 0..count {
637                                phys_whole[phy_col_start + j].push(units.len());
638                                units.push((false, 1));
639                            }
640                        }
641                        ExpansionEntry::ReuseExpandBits {
642                            phy_col_start,
643                            count,
644                            storage,
645                        } => {
646                            let bits = expand_bit_width(storage)?;
647
648                            bounds(phy_col_start + count)?;
649
650                            for j in 0..count {
651                                phys_bit[phy_col_start + j].push(units.len());
652                                units.push((true, bits));
653                            }
654                        }
655                    }
656                }
657
658                if running != num_phys {
659                    return Err(Error::Protocol {
660                        protocol: "ring_switch_plan",
661                        message: "expansion entries do not cover the physical column layout",
662                    });
663                }
664            }
665            None => {
666                for pw in phys_whole.iter_mut().take(num_phys) {
667                    pw.push(units.len());
668                    units.push((false, 1));
669                }
670            }
671        }
672
673        let has_ring = units.iter().any(|(is_ring, _)| *is_ring);
674
675        for b in 0..num_blind {
676            phys_whole[num_phys + b].push(units.len());
677            units.push((false, 1));
678        }
679
680        let mut blind_slots = num_blind;
681
682        // A uniform L-valued unit
683        if has_ring && num_blind > 0 {
684            phys_bit.push(vec![units.len()]);
685            phys_whole.push(Vec::new());
686            phys_rs.push(ColumnType::B128);
687            units.push((true, RING_BLIND_BITS));
688            blind_slots += 1;
689        }
690
691        let mut h_whole = Vec::with_capacity(num_h);
692        for _ in 0..num_h {
693            h_whole.push(units.len());
694            units.push((false, 1));
695        }
696
697        let num_units = units.len();
698
699        Ok(Self {
700            units,
701            phys_bit,
702            phys_whole,
703            h_whole,
704            phys_rs,
705            blind_slots,
706            h_cols: num_h,
707            num_units,
708        })
709    }
710
711    pub fn has_ring(&self) -> bool {
712        self.units.iter().any(|(is_ring, _)| *is_ring)
713    }
714
715    pub fn has_ring_blind(&self) -> bool {
716        self.blind_slots > 0 && self.has_ring()
717    }
718
719    pub fn total_claims(&self) -> usize {
720        self.units.iter().map(|(_, n)| n).sum()
721    }
722
723    pub fn leaf_row_bytes(&self) -> usize {
724        self.phys_rs.iter().map(|ct| ct.byte_size()).sum()
725    }
726
727    pub fn h_leaf_row_bytes(&self) -> usize {
728        self.h_cols * ColumnType::B128.byte_size()
729    }
730
731    pub fn opened_row_bytes(&self) -> usize {
732        self.leaf_row_bytes() + self.h_leaf_row_bytes()
733    }
734
735    /// `log2(grid_cols)` for this table: the proof-size optimum,
736    /// stepped down, narrowing the grid to buy proximity bits,
737    /// to the widest split that clears `MIN_PRODUCTION_BITS`.
738    pub fn split_vars(&self, num_vars: usize, field_bits: usize, config: &Config) -> usize {
739        let split = compute_split_vars(
740            num_vars,
741            config.num_queries,
742            config.ldt_support_size,
743            self.opened_row_bytes(),
744        );
745
746        let floor = support_floor_vars(config.ldt_support_size)
747            .clamp(1, num_vars.max(1))
748            .min(split);
749
750        let bits = |c: usize| {
751            config.estimated_security_bits(
752                field_bits,
753                FoldShape {
754                    grid_cols: 1 << c,
755                    grid_rows: 1 << (num_vars - c),
756                    units: self.num_units,
757                },
758            )
759        };
760
761        // Never config.min_security_bits: unabsorbed, leaving the
762        // two sides free to derive different grids for one proof.
763        (floor..=split)
764            .rev()
765            .find(|&c| bits(c) >= MIN_PRODUCTION_BITS)
766            .unwrap_or(split)
767    }
768
769    /// Per committed column, trace leaf then h leaf:
770    /// its base `eta` coefficients in the ring and whole
771    /// masters, plus `eta^U` (the next-row shift multiplier).
772    pub fn column_coeffs<F>(&self, eta: Flat<F>) -> (Vec<Flat<F>>, Vec<Flat<F>>, Flat<F>)
773    where
774        F: HardwareField,
775    {
776        let mut eta_pows = Vec::with_capacity(self.num_units + 1);
777        let mut e = Flat::from_raw(F::ONE);
778
779        for _ in 0..=self.num_units {
780            eta_pows.push(e);
781            e *= eta;
782        }
783
784        let num_phys = self.phys_rs.len();
785        let total = num_phys + self.h_cols;
786
787        let mut coeff_bit = vec![Flat::from_raw(F::ZERO); total];
788        let mut coeff_whole = vec![Flat::from_raw(F::ZERO); total];
789
790        for p in 0..num_phys {
791            for &u in &self.phys_bit[p] {
792                coeff_bit[p] += eta_pows[u];
793            }
794
795            for &u in &self.phys_whole[p] {
796                coeff_whole[p] += eta_pows[u];
797            }
798        }
799
800        for (p, &u) in self.h_whole.iter().enumerate() {
801            coeff_whole[num_phys + p] += eta_pows[u];
802        }
803
804        (coeff_bit, coeff_whole, eta_pows[self.num_units])
805    }
806}
807
808fn expand_bit_width(storage: ColumnType) -> Result<usize, Error> {
809    match storage {
810        ColumnType::B8 => Ok(8),
811        ColumnType::B16 => Ok(16),
812        ColumnType::B32 => Ok(32),
813        ColumnType::B64 => Ok(64),
814        _ => Err(Error::Protocol {
815            protocol: "virtual_expand",
816            message: "ExpandBits requires B8/B16/B32/B64",
817        }),
818    }
819}
820
821/// Tower-basis bit extraction from LE bytes.
822fn parse_tower_bit(storage: ColumnType, bytes: &[u8], bit_idx: usize) -> u8 {
823    match storage {
824        ColumnType::B8 => Flat::from_raw(Block8(bytes[0])).tower_bit(bit_idx),
825        ColumnType::B16 => {
826            let mut arr = [0u8; 2];
827            arr.copy_from_slice(bytes);
828
829            Flat::from_raw(Block16(u16::from_le_bytes(arr))).tower_bit(bit_idx)
830        }
831        ColumnType::B32 => {
832            let mut arr = [0u8; 4];
833            arr.copy_from_slice(bytes);
834
835            Flat::from_raw(Block32(u32::from_le_bytes(arr))).tower_bit(bit_idx)
836        }
837        ColumnType::B64 => {
838            let mut arr = [0u8; 8];
839            arr.copy_from_slice(bytes);
840
841            Flat::from_raw(Block64(u64::from_le_bytes(arr))).tower_bit(bit_idx)
842        }
843        _ => unreachable!(),
844    }
845}
846
847fn expand_packed_bit<F: TraceCompatibleField + 'static>(
848    col: &'_ TraceColumn,
849    storage: ColumnType,
850    bit_idx: usize,
851) -> Result<PolyVariant<'_, F>, Error> {
852    match storage {
853        ColumnType::B8 => {
854            let data = col.as_b8_slice().ok_or(Error::Protocol {
855                protocol: "virtual_expand",
856                message: "ExpandBits B8: column type mismatch",
857            })?;
858
859            Ok(PolyVariant::PackedBitB8 { data, bit_idx })
860        }
861        ColumnType::B16 => {
862            let data = col.as_b16_slice().ok_or(Error::Protocol {
863                protocol: "virtual_expand",
864                message: "ExpandBits B16: column type mismatch",
865            })?;
866
867            Ok(PolyVariant::PackedBitB16 { data, bit_idx })
868        }
869        ColumnType::B32 => {
870            let data = col.as_b32_slice().ok_or(Error::Protocol {
871                protocol: "virtual_expand",
872                message: "ExpandBits B32: column type mismatch",
873            })?;
874
875            Ok(PolyVariant::PackedBitB32 { data, bit_idx })
876        }
877        ColumnType::B64 => {
878            let data = col.as_b64_slice().ok_or(Error::Protocol {
879                protocol: "virtual_expand",
880                message: "ExpandBits B64: column type mismatch",
881            })?;
882
883            Ok(PolyVariant::PackedBitB64 { data, bit_idx })
884        }
885        _ => unreachable!(),
886    }
887}
888
889fn expand_pass_through<F: TraceCompatibleField + 'static>(
890    col: &TraceColumn,
891    storage: ColumnType,
892) -> Result<PolyVariant<'_, F>, Error> {
893    match storage {
894        ColumnType::Bit => {
895            let data = col.as_bit_slice().ok_or(Error::Protocol {
896                protocol: "virtual_expand",
897                message: "PassThrough Bit: column type mismatch",
898            })?;
899
900            Ok(PolyVariant::BitSlice(data))
901        }
902        ColumnType::B8 => {
903            let data = col.as_b8_slice().ok_or(Error::Protocol {
904                protocol: "virtual_expand",
905                message: "PassThrough B8: column type mismatch",
906            })?;
907
908            Ok(PolyVariant::B8Slice(data))
909        }
910        ColumnType::B16 => {
911            let data = col.as_b16_slice().ok_or(Error::Protocol {
912                protocol: "virtual_expand",
913                message: "PassThrough B16: column type mismatch",
914            })?;
915
916            Ok(PolyVariant::B16Slice(data))
917        }
918        ColumnType::B32 => {
919            let data = col.as_b32_slice().ok_or(Error::Protocol {
920                protocol: "virtual_expand",
921                message: "PassThrough B32: column type mismatch",
922            })?;
923
924            Ok(PolyVariant::B32Slice(data))
925        }
926        ColumnType::B64 => {
927            let data = col.as_b64_slice().ok_or(Error::Protocol {
928                protocol: "virtual_expand",
929                message: "PassThrough B64: column type mismatch",
930            })?;
931
932            Ok(PolyVariant::B64Slice(data))
933        }
934        ColumnType::B128 => {
935            let data = col.as_b128_slice().ok_or(Error::Protocol {
936                protocol: "virtual_expand",
937                message: "PassThrough B128: column type mismatch",
938            })?;
939
940            Ok(PolyVariant::B128Slice(data))
941        }
942    }
943}
944
945pub fn eq_tensor_b(r: &[Block128]) -> Vec<Block128> {
946    let mut t = vec![Block128::ONE];
947    for &ri in r {
948        let len = t.len();
949        let mut nt = Vec::with_capacity(len * 2);
950
951        for &v in &t {
952            nt.push(v * (Block128::ONE + ri));
953        }
954
955        for &v in &t {
956            nt.push(v * ri);
957        }
958
959        t = nt;
960    }
961
962    t
963}
964
965/// Σ_u eq(r'',u)·ŝ_u for one ring unit, ŝ_u = Σ_v bit_u(c_v)·2^v.
966pub fn ring_batch_b(bit_claims: &[Block128], eq_mix: &[Block128]) -> Block128 {
967    let mut acc = Block128::ZERO;
968    for (u, &m) in eq_mix.iter().enumerate() {
969        let mut shat = 0u128;
970        for (v, cv) in bit_claims.iter().enumerate() {
971            shat |= ((cv.0 >> u) & 1) << v;
972        }
973
974        acc += m * Block128(shat);
975    }
976
977    acc
978}
979
980/// Reconstructs the sumcheck's initial claim from the claimed
981/// virtual evals, in the tower basis. Ring units contribute
982/// `eta·Σ_u eq(r'',u) ŝ_u`; whole units contribute `eta·c'`.
983pub fn ring_target<F>(
984    plan: &RingSwitchPlan,
985    claims: &[Flat<F>],
986    eta_tower: F,
987    r_mix: &[Block128],
988    shifted_claims: bool,
989) -> Block128
990where
991    F: HardwareField + Into<Block128>,
992{
993    let claim_halves = if shifted_claims { 2 } else { 1 };
994    let half = claims.len() / claim_halves;
995    let eq_mix = eq_tensor_b(r_mix);
996    let eta: Block128 = eta_tower.into();
997
998    let mut eta_pows = Vec::with_capacity(plan.num_units + 1);
999    let mut e = Block128::ONE;
1000
1001    for _ in 0..=plan.num_units {
1002        eta_pows.push(e);
1003        e *= eta;
1004    }
1005
1006    let eta_shift = eta_pows[plan.num_units];
1007
1008    let base = [(0usize, Block128::ONE)];
1009    let base_and_shift = [(0usize, Block128::ONE), (half, eta_shift)];
1010    let offsets: &[(usize, Block128)] = if shifted_claims {
1011        &base_and_shift
1012    } else {
1013        &base
1014    };
1015
1016    let mut target = Block128::ZERO;
1017    for &(offset, shift_mul) in offsets {
1018        let half_claims = &claims[offset..offset + half];
1019
1020        let mut ci = 0usize;
1021        for (unit_idx, &(is_ring, num_claims)) in plan.units.iter().enumerate() {
1022            let weight = eta_pows[unit_idx] * shift_mul;
1023            if is_ring {
1024                let bits: Vec<Block128> = half_claims[ci..ci + num_claims]
1025                    .iter()
1026                    .map(|f| f.to_tower().into())
1027                    .collect();
1028
1029                target += weight * ring_batch_b(&bits, &eq_mix);
1030            } else {
1031                let c: Block128 = half_claims[ci].to_tower().into();
1032                target += weight * c;
1033            }
1034
1035            ci += num_claims;
1036        }
1037    }
1038
1039    target
1040}
1041
1042/// Per-claim weight `a_c` such that
1043/// `ring_target = Σ_c a_c · (ring ? φ_{r''}(c_c) : c_c)`,
1044/// with `φ_{r''}(x) = Σ_u eq(r'',u)·bit_u(x)`.
1045pub fn claim_weights<F>(
1046    plan: &RingSwitchPlan,
1047    eta_tower: F,
1048    shifted_claims: bool,
1049) -> Vec<(bool, Block128)>
1050where
1051    F: HardwareField + Into<Block128>,
1052{
1053    let eta: Block128 = eta_tower.into();
1054
1055    let mut eta_pows = Vec::with_capacity(plan.num_units + 1);
1056    let mut e = Block128::ONE;
1057
1058    for _ in 0..=plan.num_units {
1059        eta_pows.push(e);
1060        e *= eta;
1061    }
1062
1063    let shifts: &[Block128] = if shifted_claims {
1064        &[Block128::ONE, eta_pows[plan.num_units]]
1065    } else {
1066        &[Block128::ONE]
1067    };
1068
1069    let mut weights = Vec::with_capacity(plan.total_claims() * shifts.len());
1070    for &shift_mul in shifts {
1071        for (unit_idx, &(is_ring, num_claims)) in plan.units.iter().enumerate() {
1072            let weight = eta_pows[unit_idx] * shift_mul;
1073            for v in 0..num_claims {
1074                let basis = if is_ring {
1075                    Block128(1u128 << v)
1076                } else {
1077                    Block128::ONE
1078                };
1079
1080                weights.push((is_ring, weight * basis));
1081            }
1082        }
1083    }
1084
1085    weights
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090    use super::*;
1091    use hekate_core::trace::TraceBuilder;
1092    use hekate_math::{Block128, TowerField};
1093
1094    fn keccak_expander() -> VirtualExpander {
1095        VirtualExpander::new()
1096            .expand_bits(25, ColumnType::B64)
1097            .expand_bits(1, ColumnType::B64)
1098            .reuse_pass_through(0, 25)
1099            .control_bits(2)
1100            .build()
1101            .unwrap()
1102    }
1103
1104    fn keccak_physical_layout() -> Vec<ColumnType> {
1105        let mut layout = vec![ColumnType::B64; 26];
1106        layout.extend(repeat_n(ColumnType::Bit, 2));
1107
1108        layout
1109    }
1110
1111    fn plan_of(cols: usize) -> RingSwitchPlan {
1112        RingSwitchPlan::new(&vec![ColumnType::B128; cols], None, 1, 1).unwrap()
1113    }
1114
1115    #[test]
1116    fn ram_layout() {
1117        let e = VirtualExpander::new()
1118            .expand_bits(2, ColumnType::B32)
1119            .pass_through(13, ColumnType::B32)
1120            .pass_through(1, ColumnType::B128)
1121            .control_bits(4)
1122            .build()
1123            .unwrap();
1124
1125        assert_eq!(e.num_virtual_columns(), 82);
1126        assert_eq!(e.num_physical_columns(), 20);
1127        assert_eq!(e.physical_row_bytes(), 80);
1128
1129        let layout = e.virtual_layout();
1130
1131        assert_eq!(layout.len(), 82);
1132        assert!(layout[..64].iter().all(|&t| t == ColumnType::Bit));
1133        assert!(layout[64..77].iter().all(|&t| t == ColumnType::B32));
1134        assert_eq!(layout[77], ColumnType::B128);
1135        assert!(layout[78..82].iter().all(|&t| t == ColumnType::Bit));
1136    }
1137
1138    #[test]
1139    fn keccak_layout() {
1140        let e = VirtualExpander::new()
1141            .expand_bits(25, ColumnType::B64)
1142            .expand_bits(1, ColumnType::B64)
1143            .reuse_pass_through(0, 25)
1144            .control_bits(2)
1145            .build()
1146            .unwrap();
1147
1148        assert_eq!(e.num_virtual_columns(), 1691);
1149        assert_eq!(e.num_physical_columns(), 28);
1150        assert_eq!(e.physical_row_bytes(), 210);
1151
1152        let layout = e.virtual_layout();
1153
1154        assert_eq!(layout.len(), 1691);
1155        assert!(layout[..1600].iter().all(|&t| t == ColumnType::Bit));
1156        assert!(layout[1600..1664].iter().all(|&t| t == ColumnType::Bit));
1157        assert!(layout[1664..1689].iter().all(|&t| t == ColumnType::B64));
1158        assert!(layout[1689..1691].iter().all(|&t| t == ColumnType::Bit));
1159    }
1160
1161    #[test]
1162    fn ring_switch_plan_rejects_uncovered_columns() {
1163        let expander = keccak_expander();
1164        let entries = expander.expansion_entries();
1165
1166        let mut layout = keccak_physical_layout();
1167
1168        assert_eq!(expander.num_physical_columns(), layout.len());
1169        assert!(RingSwitchPlan::new(&layout, Some(&entries), 0, 0).is_ok());
1170
1171        layout.push(ColumnType::B64);
1172
1173        assert!(RingSwitchPlan::new(&layout, Some(&entries), 0, 0).is_err());
1174    }
1175
1176    #[test]
1177    fn ring_switch_plan_folds_every_committed_column() {
1178        let entries = keccak_expander().expansion_entries();
1179        let layout = keccak_physical_layout();
1180
1181        let plan = RingSwitchPlan::new(&layout, Some(&entries), 2, 0).unwrap();
1182
1183        let zero = Flat::from_raw(Block128::ZERO);
1184        let eta = Block128(0x2545F4914F6CDD1D_517CC1B727220A95).to_hardware();
1185        let (coeff_bit, coeff_whole, _) = plan.column_coeffs::<Block128>(eta);
1186
1187        for p in 0..plan.phys_rs.len() {
1188            assert!(
1189                coeff_bit[p] != zero || coeff_whole[p] != zero,
1190                "committed column {p} enters no master fold"
1191            );
1192        }
1193    }
1194
1195    #[test]
1196    fn h_units_trail_ring_blind() {
1197        let layout = [ColumnType::B32, ColumnType::B64];
1198        let expander = VirtualExpander::new()
1199            .expand_bits(1, ColumnType::B32)
1200            .pass_through(1, ColumnType::B64)
1201            .build()
1202            .unwrap();
1203        let entries = expander.expansion_entries();
1204
1205        let bare = RingSwitchPlan::new(&layout, Some(&entries), 1, 0).unwrap();
1206        let plan = RingSwitchPlan::new(&layout, Some(&entries), 1, 2).unwrap();
1207
1208        assert_eq!(plan.h_cols, 2);
1209        assert_eq!(plan.phys_rs, bare.phys_rs);
1210        assert_eq!(plan.total_claims(), bare.total_claims() + 2);
1211        assert_eq!(&plan.units[plan.num_units - 2..], &[(false, 1), (false, 1)]);
1212        assert_eq!(plan.units[plan.num_units - 3], (true, RING_BLIND_BITS));
1213        assert_eq!(plan.leaf_row_bytes(), bare.opened_row_bytes());
1214        assert_eq!(plan.opened_row_bytes(), bare.opened_row_bytes() + 32);
1215
1216        let eta = Block128::from(0x9E37_79B9u128).to_hardware();
1217        let (coeff_bit, coeff_whole, eta_shift) = plan.column_coeffs::<Block128>(eta);
1218        let n = plan.phys_rs.len();
1219
1220        let mut first_h = Flat::from_raw(Block128::ONE);
1221        for _ in 0..plan.num_units - 2 {
1222            first_h *= eta;
1223        }
1224
1225        assert_eq!(coeff_whole.len(), n + 2);
1226        assert_eq!(coeff_bit[n..], [Flat::from_raw(Block128::ZERO); 2]);
1227        assert_eq!(coeff_whole[n], first_h);
1228        assert_eq!(coeff_whole[n + 1], first_h * eta);
1229        assert_eq!(eta_shift, first_h * eta * eta);
1230    }
1231
1232    #[test]
1233    fn blinded_ring_plan_ends_in_ring_blind_unit() {
1234        let entries = keccak_expander().expansion_entries();
1235        let layout = keccak_physical_layout();
1236
1237        let raw = RingSwitchPlan::new(&layout, Some(&entries), 0, 0).unwrap();
1238        let blinded = RingSwitchPlan::new(&layout, Some(&entries), 2, 0).unwrap();
1239        let whole_only = RingSwitchPlan::new(&[ColumnType::B32; 3], None, 2, 0).unwrap();
1240
1241        assert_eq!(raw.blind_slots, 0);
1242        assert_eq!(raw.phys_rs.len(), layout.len());
1243
1244        assert_eq!(blinded.blind_slots, 3);
1245        assert_eq!(blinded.phys_rs.len(), layout.len() + 3);
1246        assert_eq!(blinded.units.last(), Some(&(true, RING_BLIND_BITS)));
1247        assert_eq!(
1248            blinded.total_claims(),
1249            raw.total_claims() + 2 + RING_BLIND_BITS
1250        );
1251
1252        assert_eq!(whole_only.blind_slots, 2);
1253        assert_eq!(whole_only.units.last(), Some(&(false, 1)));
1254
1255        let eta = Block128(0x2545F4914F6CDD1D_517CC1B727220A95).to_hardware();
1256        let (coeff_bit, coeff_whole, _) = blinded.column_coeffs::<Block128>(eta);
1257        let last = blinded.phys_rs.len() - 1;
1258
1259        assert_ne!(coeff_bit[last], Flat::from_raw(Block128::ZERO));
1260        assert_eq!(coeff_whole[last], Flat::from_raw(Block128::ZERO));
1261    }
1262
1263    #[test]
1264    fn reuse_partial_range() {
1265        let e = VirtualExpander::new()
1266            .expand_bits(10, ColumnType::B32)
1267            .reuse_pass_through(3, 4)
1268            .build()
1269            .unwrap();
1270
1271        assert_eq!(e.num_virtual_columns(), 324);
1272        assert_eq!(e.num_physical_columns(), 10);
1273        assert_eq!(e.physical_row_bytes(), 40);
1274
1275        let layout = e.virtual_layout();
1276
1277        assert_eq!(layout[320..324].len(), 4);
1278        assert!(layout[320..324].iter().all(|&t| t == ColumnType::B32));
1279    }
1280
1281    #[test]
1282    fn reuse_exceeds_declared() {
1283        let result = VirtualExpander::new()
1284            .expand_bits(5, ColumnType::B32)
1285            .reuse_pass_through(3, 5)
1286            .build();
1287
1288        assert!(result.is_err());
1289    }
1290
1291    #[test]
1292    fn reuse_expand_bits_from_pass_through() {
1293        let e = VirtualExpander::new()
1294            .pass_through(4, ColumnType::B64)
1295            .reuse_expand_bits(0, 4)
1296            .build()
1297            .unwrap();
1298
1299        assert_eq!(e.num_physical_columns(), 4);
1300        assert_eq!(e.physical_row_bytes(), 32);
1301        assert_eq!(e.num_virtual_columns(), 4 + 256);
1302
1303        let layout = e.virtual_layout();
1304
1305        assert!(layout[0..4].iter().all(|&t| t == ColumnType::B64));
1306        assert!(layout[4..260].iter().all(|&t| t == ColumnType::Bit));
1307    }
1308
1309    #[test]
1310    fn reuse_expand_bits_exceeds_declared() {
1311        let result = VirtualExpander::new()
1312            .pass_through(4, ColumnType::B64)
1313            .reuse_expand_bits(2, 4)
1314            .build();
1315
1316        assert!(result.is_err());
1317    }
1318
1319    #[test]
1320    fn reuse_expand_bits_rejects_b128_source() {
1321        let result = VirtualExpander::new()
1322            .pass_through(1, ColumnType::B128)
1323            .reuse_expand_bits(0, 1)
1324            .build();
1325
1326        assert!(result.is_err());
1327    }
1328
1329    #[test]
1330    fn expand_rejects_bit() {
1331        let result = VirtualExpander::new()
1332            .expand_bits(1, ColumnType::Bit)
1333            .build();
1334
1335        assert!(result.is_err());
1336    }
1337
1338    #[test]
1339    fn expand_rejects_b128() {
1340        let result = VirtualExpander::new()
1341            .expand_bits(1, ColumnType::B128)
1342            .build();
1343
1344        assert!(result.is_err());
1345    }
1346
1347    #[test]
1348    fn empty_expander() {
1349        let e = VirtualExpander::new();
1350        assert_eq!(e.num_virtual_columns(), 0);
1351        assert_eq!(e.num_physical_columns(), 0);
1352        assert_eq!(e.physical_row_bytes(), 0);
1353        assert!(e.virtual_layout().is_empty());
1354    }
1355
1356    #[test]
1357    fn parse_row_b32_roundtrip() {
1358        let expander = VirtualExpander::new()
1359            .expand_bits(1, ColumnType::B32)
1360            .pass_through(1, ColumnType::B32)
1361            .control_bits(1)
1362            .build()
1363            .unwrap();
1364
1365        let val: u32 = 0xDEAD_BEEF;
1366        let pass_val: u32 = 0x1234_5678;
1367
1368        let mut bytes = Vec::new();
1369        bytes.extend_from_slice(&val.to_le_bytes());
1370        bytes.extend_from_slice(&pass_val.to_le_bytes());
1371        bytes.push(1);
1372
1373        let mut res: Vec<Flat<Block128>> = Vec::new();
1374        expander.parse_row(&bytes, &mut res).unwrap();
1375
1376        assert_eq!(res.len(), 34);
1377
1378        for (bit_idx, elem) in res.iter().enumerate().take(32) {
1379            let expected = Flat::from_raw(Block32(val)).tower_bit(bit_idx);
1380            let got = elem.tower_bit(0);
1381            assert_eq!(got, expected, "bit {bit_idx} mismatch");
1382        }
1383
1384        let pass = res[32];
1385        assert_eq!(
1386            pass,
1387            <Block128 as hekate_math::FlatPromote<Block32>>::promote_flat(Flat::from_raw(Block32(
1388                pass_val
1389            )))
1390        );
1391
1392        let ctrl = res[33].tower_bit(0);
1393        assert_eq!(ctrl, 1);
1394    }
1395
1396    #[test]
1397    fn expand_variants_b32() {
1398        let expander = VirtualExpander::new()
1399            .expand_bits(1, ColumnType::B32)
1400            .pass_through(1, ColumnType::B32)
1401            .control_bits(1)
1402            .build()
1403            .unwrap();
1404
1405        let layout = [ColumnType::B32, ColumnType::B32, ColumnType::Bit];
1406        let num_vars = 2;
1407
1408        let mut tb = TraceBuilder::new(&layout, num_vars).unwrap();
1409        tb.set_b32(0, 0, Block32(0xAAAA_BBBB)).unwrap();
1410        tb.set_b32(1, 0, Block32(0x1111_2222)).unwrap();
1411        tb.set_bit(2, 0, Bit::ONE).unwrap();
1412
1413        let trace = tb.build();
1414
1415        let variants: Vec<PolyVariant<'_, Block128>> = expander.expand_variants(&trace, 0).unwrap();
1416
1417        assert_eq!(variants.len(), 34);
1418
1419        for (i, v) in variants.iter().enumerate().take(32) {
1420            assert!(matches!(v, PolyVariant::PackedBitB32 { bit_idx, .. } if *bit_idx == i));
1421        }
1422
1423        assert!(matches!(variants[32], PolyVariant::B32Slice(_)));
1424        assert!(matches!(variants[33], PolyVariant::BitSlice(_)));
1425    }
1426
1427    #[test]
1428    fn claim_weights_reproduce_ring_target() {
1429        let layout = [
1430            ColumnType::B32,
1431            ColumnType::B32,
1432            ColumnType::B64,
1433            ColumnType::Bit,
1434        ];
1435
1436        let expander = VirtualExpander::new()
1437            .expand_bits(2, ColumnType::B32)
1438            .pass_through(1, ColumnType::B64)
1439            .control_bits(1)
1440            .build()
1441            .unwrap();
1442
1443        let entries = expander.expansion_entries();
1444        let plan = RingSwitchPlan::new(&layout, Some(&entries), 2, 0).unwrap();
1445
1446        let mut state = 0x9e37_79b9_7f4a_7c15_0123_4567_89ab_cdefu128;
1447        let mut next = || {
1448            state ^= state << 13;
1449            state ^= state >> 7;
1450            state ^= state << 17;
1451
1452            Block128(state)
1453        };
1454
1455        let claims: Vec<Block128> = (0..2 * plan.total_claims()).map(|_| next()).collect();
1456        let eta = next();
1457        let r_mix: Vec<Block128> = (0..7).map(|_| next()).collect();
1458        let eq_mix = eq_tensor_b(&r_mix);
1459
1460        let flat: Vec<Flat<Block128>> = claims.iter().map(|c| c.to_hardware()).collect();
1461        let target = ring_target::<Block128>(&plan, &flat, eta, &r_mix, true);
1462
1463        let mut sum = Block128::ZERO;
1464        for ((is_ring, weight), claim) in claim_weights::<Block128>(&plan, eta, true)
1465            .into_iter()
1466            .zip(&claims)
1467        {
1468            let value = match is_ring {
1469                true => ring_batch_b(&[*claim], &eq_mix),
1470                false => *claim,
1471            };
1472
1473            sum += weight * value;
1474        }
1475
1476        assert_eq!(sum, target);
1477    }
1478
1479    /// The scan is only correct if it finds the best split, not
1480    /// merely a better one: `bits` is not unimodal in the split,
1481    /// and a plateau must not end the search on a local peak.
1482    #[test]
1483    fn adaptive_split_clears_floor_whenever_any_split_can() {
1484        let config = Config::prod();
1485
1486        let mut stepped_down = 0;
1487        for cols in [1usize, 4, 16, 64, 128] {
1488            let plan = plan_of(cols);
1489            for num_vars in 10usize..=30 {
1490                let chosen = plan.split_vars(num_vars, 128, &config);
1491                let bits = |c: usize| {
1492                    config.estimated_security_bits(
1493                        128,
1494                        FoldShape {
1495                            grid_cols: 1 << c,
1496                            grid_rows: 1 << (num_vars - c),
1497                            units: plan.num_units,
1498                        },
1499                    )
1500                };
1501
1502                let optimal = compute_split_vars(
1503                    num_vars,
1504                    config.num_queries,
1505                    config.ldt_support_size,
1506                    plan.opened_row_bytes(),
1507                );
1508
1509                assert!(chosen <= optimal, "cols={cols} n={num_vars}: stepped up");
1510                assert!(
1511                    chosen >= support_floor_vars(config.ldt_support_size).min(optimal),
1512                    "cols={cols} n={num_vars}: below the support floor"
1513                );
1514
1515                if chosen < optimal {
1516                    stepped_down += 1;
1517                }
1518
1519                let best = (1..=optimal).map(bits).max().unwrap();
1520
1521                if best >= MIN_PRODUCTION_BITS {
1522                    assert!(
1523                        bits(chosen) >= MIN_PRODUCTION_BITS,
1524                        "cols={cols} n={num_vars}: chose {chosen} at {} bits, \
1525                         but some split reaches {best}",
1526                        bits(chosen),
1527                    );
1528                }
1529            }
1530        }
1531
1532        assert!(stepped_down > 0, "the adaptive walk never fired");
1533    }
1534}