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 core::ops::Range;
9use hekate_core::config::{Config, FoldShape, MIN_PRODUCTION_BITS};
10use hekate_core::errors::Error;
11use hekate_core::poly::PolyVariant;
12use hekate_core::tensor::TensorProduct;
13use hekate_core::trace::{ColumnType, Trace, TraceColumn, TraceCompatibleField};
14use hekate_core::utils::{cheapest_split_vars, support_floor_vars};
15use hekate_math::{
16    Bit, Block8, Block16, Block32, Block64, Block128, Flat, HardwareField, TowerField,
17};
18
19use crate::linearized::{BITS, LinearMap};
20
21pub const RING_BLIND_BITS: usize = 128;
22
23/// Claim count from which [`ring_target`] switches
24/// to byte tables indexed by the claims: variable-time.
25const RING_TABLE_MIN_CLAIMS: usize = 300;
26
27/// Serializable expansion step descriptor.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum ExpansionEntry {
30    ExpandBits {
31        count: usize,
32        storage: ColumnType,
33    },
34    PassThrough {
35        count: usize,
36        storage: ColumnType,
37    },
38    ControlBits {
39        count: usize,
40    },
41    ReusePassThrough {
42        phy_col_start: usize,
43        count: usize,
44        storage: ColumnType,
45    },
46    ReuseExpandBits {
47        phy_col_start: usize,
48        count: usize,
49        storage: ColumnType,
50    },
51}
52
53/// Physical-to-virtual column mapping rule.
54#[derive(Clone, Copy, Debug)]
55enum EntryKind {
56    /// N physical columns to N ×
57    /// bit_width virtual Bit columns.
58    ExpandBits { count: usize, storage: ColumnType },
59
60    /// N physical columns to N virtual
61    /// columns of the same type.
62    PassThrough { count: usize, storage: ColumnType },
63
64    /// N physical Bit columns
65    /// to N virtual Bit columns.
66    ControlBits { count: usize },
67}
68
69impl EntryKind {
70    fn count(&self) -> usize {
71        match self {
72            Self::ExpandBits { count, .. }
73            | Self::PassThrough { count, .. }
74            | Self::ControlBits { count } => *count,
75        }
76    }
77
78    fn storage(&self) -> ColumnType {
79        match self {
80            Self::ExpandBits { storage, .. } | Self::PassThrough { storage, .. } => *storage,
81            Self::ControlBits { .. } => ColumnType::Bit,
82        }
83    }
84}
85
86/// Pre-computed expansion entry
87/// with frozen byte/column offsets.
88#[derive(Clone, Copy, Debug)]
89struct CompiledEntry {
90    /// Physical column index,
91    /// relative to `phy_start_idx`.
92    phy_col_start: usize,
93
94    /// Byte offset in the committed row.
95    byte_offset: usize,
96    kind: EntryKind,
97
98    /// True if this entry reuses physical
99    /// columns declared by a prior entry.
100    reuse: bool,
101}
102
103/// Declarative physical->virtual
104/// column expander for chiplets.
105///
106/// Built once per chiplet, generates
107/// `virtual_layout()`, `parse_row()`,
108/// and `expand_variants()` from the
109/// same packing specification.
110#[derive(Clone, Debug)]
111pub struct VirtualExpander {
112    entries: Vec<CompiledEntry>,
113    num_virtual: usize,
114    num_physical: usize,
115    physical_row_bytes: usize,
116    virtual_layout: Vec<ColumnType>,
117    error: Option<Error>,
118}
119
120impl VirtualExpander {
121    pub fn new() -> Self {
122        Self {
123            entries: Vec::new(),
124            num_virtual: 0,
125            num_physical: 0,
126            physical_row_bytes: 0,
127            virtual_layout: Vec::new(),
128            error: None,
129        }
130    }
131
132    /// Finalize the builder. Returns `Err` if any
133    /// builder step recorded a validation error.
134    pub fn build(self) -> Result<Self, Error> {
135        match self.error {
136            Some(e) => Err(e),
137            None => Ok(self),
138        }
139    }
140
141    /// N physical columns of `storage` type
142    /// to N × bit_width virtual Bit columns.
143    pub fn expand_bits(mut self, count: usize, storage: ColumnType) -> Self {
144        if self.error.is_some() {
145            return self;
146        }
147
148        let bits_per = match expand_bit_width(storage) {
149            Ok(v) => v,
150            Err(e) => {
151                self.error = Some(e);
152                return self;
153            }
154        };
155
156        let byte_offset = self.physical_row_bytes;
157        let phy_col_start = self.num_physical;
158
159        self.entries.push(CompiledEntry {
160            phy_col_start,
161            byte_offset,
162            kind: EntryKind::ExpandBits { count, storage },
163            reuse: false,
164        });
165
166        let virt_count = count * bits_per;
167        self.virtual_layout
168            .extend(repeat_n(ColumnType::Bit, virt_count));
169
170        self.num_virtual += virt_count;
171        self.num_physical += count;
172        self.physical_row_bytes += count * storage.byte_size();
173
174        self
175    }
176
177    /// N physical columns pass through
178    /// 1:1 as virtual columns.
179    pub fn pass_through(mut self, count: usize, storage: ColumnType) -> Self {
180        let byte_offset = self.physical_row_bytes;
181        let phy_col_start = self.num_physical;
182
183        self.entries.push(CompiledEntry {
184            phy_col_start,
185            byte_offset,
186            kind: EntryKind::PassThrough { count, storage },
187            reuse: false,
188        });
189
190        self.virtual_layout.extend(repeat_n(storage, count));
191
192        self.num_virtual += count;
193        self.num_physical += count;
194        self.physical_row_bytes += count * storage.byte_size();
195
196        self
197    }
198
199    /// N physical Bit columns pass through 1:1.
200    pub fn control_bits(mut self, count: usize) -> Self {
201        let byte_offset = self.physical_row_bytes;
202        let phy_col_start = self.num_physical;
203
204        self.entries.push(CompiledEntry {
205            phy_col_start,
206            byte_offset,
207            kind: EntryKind::ControlBits { count },
208            reuse: false,
209        });
210
211        self.virtual_layout.extend(repeat_n(ColumnType::Bit, count));
212
213        self.num_virtual += count;
214        self.num_physical += count;
215        self.physical_row_bytes += count;
216
217        self
218    }
219
220    /// Emit pass-through for columns already
221    /// declared by a prior fresh entry.
222    /// Does not advance the physical cursor.
223    pub fn reuse_pass_through(mut self, phy_col_start: usize, count: usize) -> Self {
224        if self.error.is_some() {
225            return self;
226        }
227
228        if phy_col_start + count > self.num_physical {
229            self.error = Some(Error::Protocol {
230                protocol: "virtual_expand",
231                message: "reuse_pass_through: range exceeds declared physical columns",
232            });
233            return self;
234        }
235
236        let (byte_offset, storage) = match self.find_phy_source(phy_col_start, count) {
237            Ok(v) => v,
238            Err(e) => {
239                self.error = Some(e);
240                return self;
241            }
242        };
243
244        self.entries.push(CompiledEntry {
245            phy_col_start,
246            byte_offset,
247            kind: EntryKind::PassThrough { count, storage },
248            reuse: true,
249        });
250
251        self.virtual_layout.extend(repeat_n(storage, count));
252
253        self.num_virtual += count;
254
255        self
256    }
257
258    /// Emit bit-expansion for columns already
259    /// declared by a prior fresh entry.
260    /// Does not advance the physical cursor.
261    pub fn reuse_expand_bits(mut self, phy_col_start: usize, count: usize) -> Self {
262        if self.error.is_some() {
263            return self;
264        }
265
266        if phy_col_start + count > self.num_physical {
267            self.error = Some(Error::Protocol {
268                protocol: "virtual_expand",
269                message: "reuse_expand_bits: range exceeds declared physical columns",
270            });
271            return self;
272        }
273
274        let (byte_offset, storage) = match self.find_phy_source(phy_col_start, count) {
275            Ok(v) => v,
276            Err(e) => {
277                self.error = Some(e);
278                return self;
279            }
280        };
281
282        let bits_per = match expand_bit_width(storage) {
283            Ok(v) => v,
284            Err(e) => {
285                self.error = Some(e);
286                return self;
287            }
288        };
289
290        self.entries.push(CompiledEntry {
291            phy_col_start,
292            byte_offset,
293            kind: EntryKind::ExpandBits { count, storage },
294            reuse: true,
295        });
296
297        let virt_count = count * bits_per;
298
299        self.virtual_layout
300            .extend(repeat_n(ColumnType::Bit, virt_count));
301
302        self.num_virtual += virt_count;
303
304        self
305    }
306
307    /// Append another expander's entries after this
308    /// one's, shifting its physical references onto
309    /// this expander's coordinate space.
310    pub fn append(mut self, other: &VirtualExpander) -> Self {
311        if self.error.is_some() {
312            return self;
313        }
314
315        if let Some(e) = &other.error {
316            self.error = Some(*e);
317            return self;
318        }
319
320        let phy_shift = self.num_physical;
321        let byte_shift = self.physical_row_bytes;
322
323        for entry in &other.entries {
324            self.entries.push(CompiledEntry {
325                phy_col_start: entry.phy_col_start + phy_shift,
326                byte_offset: entry.byte_offset + byte_shift,
327                kind: entry.kind,
328                reuse: entry.reuse,
329            });
330        }
331
332        self.virtual_layout.extend_from_slice(&other.virtual_layout);
333
334        self.num_virtual += other.num_virtual;
335        self.num_physical += other.num_physical;
336        self.physical_row_bytes += other.physical_row_bytes;
337
338        self
339    }
340
341    #[inline]
342    pub fn num_virtual_columns(&self) -> usize {
343        self.num_virtual
344    }
345
346    #[inline]
347    pub fn num_physical_columns(&self) -> usize {
348        self.num_physical
349    }
350
351    #[inline]
352    pub fn physical_row_bytes(&self) -> usize {
353        self.physical_row_bytes
354    }
355
356    #[inline]
357    pub fn virtual_layout(&self) -> &[ColumnType] {
358        &self.virtual_layout
359    }
360
361    /// Verifier-side:
362    /// parse committed physical row bytes
363    /// into virtual field elements.
364    pub fn parse_row<F: TraceCompatibleField>(
365        &self,
366        bytes: &[u8],
367        res: &mut Vec<Flat<F>>,
368    ) -> Result<(), Error> {
369        if bytes.len() != self.physical_row_bytes {
370            return Err(Error::Protocol {
371                protocol: "virtual_expand",
372                message: "parse_row: byte slice length mismatch",
373            });
374        }
375
376        res.reserve(self.num_virtual);
377
378        for entry in &self.entries {
379            let off = entry.byte_offset;
380            match entry.kind {
381                EntryKind::ExpandBits { count, storage } => {
382                    let bsz = storage.byte_size();
383                    let bits = expand_bit_width(storage)?;
384
385                    for i in 0..count {
386                        let start = off + i * bsz;
387                        for bit_idx in 0..bits {
388                            let bit = parse_tower_bit(storage, &bytes[start..start + bsz], bit_idx);
389                            res.push(Flat::from_raw(F::from(Bit::from(bit))));
390                        }
391                    }
392                }
393                EntryKind::PassThrough { count, storage } => {
394                    let bsz = storage.byte_size();
395                    for i in 0..count {
396                        let start = off + i * bsz;
397                        res.push(storage.parse_from_bytes(&bytes[start..start + bsz]));
398                    }
399                }
400                EntryKind::ControlBits { count } => {
401                    for i in 0..count {
402                        res.push(Flat::from_raw(F::from(Bit::from(bytes[off + i] & 1))));
403                    }
404                }
405            }
406        }
407
408        Ok(())
409    }
410
411    /// Prover-side:
412    /// expand physical `ColumnTrace`
413    /// into virtual `PolyVariant`s.
414    pub fn expand_variants<'a, F, T: Trace + ?Sized>(
415        &self,
416        trace: &'a T,
417        phy_start_idx: usize,
418    ) -> Result<Vec<PolyVariant<'a, F>>, Error>
419    where
420        F: TraceCompatibleField + 'static,
421    {
422        let columns = trace.columns();
423
424        let mut variants = Vec::with_capacity(self.num_virtual);
425        for entry in &self.entries {
426            let base = phy_start_idx + entry.phy_col_start;
427            match entry.kind {
428                EntryKind::ExpandBits { count, storage } => {
429                    let bits = expand_bit_width(storage)?;
430                    for i in 0..count {
431                        let col = columns.get(base + i).ok_or(Error::Protocol {
432                            protocol: "virtual_expand",
433                            message: "missing physical column for ExpandBits",
434                        })?;
435
436                        for bit_idx in 0..bits {
437                            variants.push(expand_packed_bit(col, storage, bit_idx)?);
438                        }
439                    }
440                }
441                EntryKind::PassThrough { count, storage } => {
442                    for i in 0..count {
443                        let col = columns.get(base + i).ok_or(Error::Protocol {
444                            protocol: "virtual_expand",
445                            message: "missing physical column for PassThrough",
446                        })?;
447
448                        variants.push(expand_pass_through(col, storage)?);
449                    }
450                }
451                EntryKind::ControlBits { count } => {
452                    for i in 0..count {
453                        let col = columns.get(base + i).ok_or(Error::Protocol {
454                            protocol: "virtual_expand",
455                            message: "missing physical column for ControlBits",
456                        })?;
457
458                        let data = col.as_bit_slice().ok_or(Error::Protocol {
459                            protocol: "virtual_expand",
460                            message: "control column must be Bit",
461                        })?;
462
463                        variants.push(PolyVariant::BitSlice(data));
464                    }
465                }
466            }
467        }
468
469        Ok(variants)
470    }
471
472    /// Wire-format serialization descriptor.
473    pub fn expansion_entries(&self) -> Vec<ExpansionEntry> {
474        self.entries
475            .iter()
476            .map(|e| match (e.kind, e.reuse) {
477                (EntryKind::PassThrough { count, storage }, true) => {
478                    ExpansionEntry::ReusePassThrough {
479                        phy_col_start: e.phy_col_start,
480                        count,
481                        storage,
482                    }
483                }
484                (EntryKind::ExpandBits { count, storage }, true) => {
485                    ExpansionEntry::ReuseExpandBits {
486                        phy_col_start: e.phy_col_start,
487                        count,
488                        storage,
489                    }
490                }
491                (EntryKind::ExpandBits { count, storage }, false) => {
492                    ExpansionEntry::ExpandBits { count, storage }
493                }
494                (EntryKind::PassThrough { count, storage }, false) => {
495                    ExpansionEntry::PassThrough { count, storage }
496                }
497                (EntryKind::ControlBits { count }, _) => ExpansionEntry::ControlBits { count },
498            })
499            .collect()
500    }
501
502    pub(crate) fn whole_column(&self, virt: usize) -> Option<usize> {
503        let mut base = 0usize;
504        for entry in &self.entries {
505            let width = match entry.kind {
506                EntryKind::ExpandBits { count, storage } => count * storage.byte_size() * 8,
507                EntryKind::PassThrough { count, .. } | EntryKind::ControlBits { count } => count,
508            };
509
510            if virt < base + width {
511                return match entry.kind {
512                    EntryKind::ExpandBits { .. } => None,
513                    EntryKind::PassThrough { .. } | EntryKind::ControlBits { .. } => {
514                        Some(entry.phy_col_start + (virt - base))
515                    }
516                };
517            }
518
519            base += width;
520        }
521
522        None
523    }
524
525    // Fresh entries have phy_col_start == running_phy;
526    // reuse entries point backward.
527    fn find_phy_source(
528        &self,
529        target_start: usize,
530        target_count: usize,
531    ) -> Result<(usize, ColumnType), Error> {
532        let mut running_phy = 0usize;
533        for entry in &self.entries {
534            if entry.phy_col_start != running_phy {
535                continue;
536            }
537
538            let entry_count = entry.kind.count();
539            let entry_end = running_phy + entry_count;
540
541            if target_start >= running_phy && target_start + target_count <= entry_end {
542                let storage = entry.kind.storage();
543                let offset_in_entry = target_start - running_phy;
544
545                return Ok((
546                    entry.byte_offset + offset_in_entry * storage.byte_size(),
547                    storage,
548                ));
549            }
550
551            running_phy = entry_end;
552        }
553
554        Err(Error::Protocol {
555            protocol: "virtual_expand",
556            message: "reuse: source columns not found in any single fresh entry",
557        })
558    }
559}
560
561impl Default for VirtualExpander {
562    fn default() -> Self {
563        Self::new()
564    }
565}
566
567/// Maps the claimed virtual evals and the committed
568/// columns onto ring-switch binding units, in claim
569/// order; a bit-expanded physical column is one
570/// Ring unit consuming its `bits` claims.
571#[derive(Clone)]
572pub struct RingSwitchPlan {
573    pub num_units: usize,
574    pub units: Vec<(bool, usize)>,
575    pub phys_rs: Vec<ColumnType>,
576    pub blind_cols: usize,
577    pub h_cols: usize,
578
579    phys_bit: Vec<Vec<usize>>,
580    phys_whole: Vec<Vec<usize>>,
581    h_whole: Vec<usize>,
582}
583
584impl RingSwitchPlan {
585    pub fn new(
586        layout: &[ColumnType],
587        entries: Option<&[ExpansionEntry]>,
588        num_blind: usize,
589        num_h: usize,
590    ) -> Result<Self, Error> {
591        let num_phys = layout.len();
592
593        let mut phys_bit = vec![Vec::new(); num_phys];
594        let mut phys_whole = vec![Vec::new(); num_phys];
595        let mut units: Vec<(bool, usize)> = Vec::new();
596
597        let bounds = |upper: usize| -> Result<(), Error> {
598            if upper > num_phys {
599                return Err(Error::Protocol {
600                    protocol: "ring_switch_plan",
601                    message: "expansion entry exceeds the physical column layout",
602                });
603            }
604
605            Ok(())
606        };
607
608        match entries {
609            Some(entries) => {
610                let mut running = 0usize;
611                for e in entries {
612                    match *e {
613                        ExpansionEntry::ExpandBits { count, storage } => {
614                            let bits = expand_bit_width(storage)?;
615
616                            bounds(running + count)?;
617
618                            for j in 0..count {
619                                phys_bit[running + j].push(units.len());
620                                units.push((true, bits));
621                            }
622
623                            running += count;
624                        }
625                        ExpansionEntry::PassThrough { count, .. }
626                        | ExpansionEntry::ControlBits { count } => {
627                            bounds(running + count)?;
628
629                            for j in 0..count {
630                                phys_whole[running + j].push(units.len());
631                                units.push((false, 1));
632                            }
633
634                            running += count;
635                        }
636                        ExpansionEntry::ReusePassThrough {
637                            phy_col_start,
638                            count,
639                            ..
640                        } => {
641                            bounds(phy_col_start + count)?;
642
643                            for j in 0..count {
644                                phys_whole[phy_col_start + j].push(units.len());
645                                units.push((false, 1));
646                            }
647                        }
648                        ExpansionEntry::ReuseExpandBits {
649                            phy_col_start,
650                            count,
651                            storage,
652                        } => {
653                            let bits = expand_bit_width(storage)?;
654
655                            bounds(phy_col_start + count)?;
656
657                            for j in 0..count {
658                                phys_bit[phy_col_start + j].push(units.len());
659                                units.push((true, bits));
660                            }
661                        }
662                    }
663                }
664
665                if running != num_phys {
666                    return Err(Error::Protocol {
667                        protocol: "ring_switch_plan",
668                        message: "expansion entries do not cover the physical column layout",
669                    });
670                }
671            }
672            None => {
673                for pw in phys_whole.iter_mut() {
674                    pw.push(units.len());
675                    units.push((false, 1));
676                }
677            }
678        }
679
680        let mut plan = Self {
681            num_units: units.len(),
682            units,
683            phys_rs: layout.iter().map(|ct| ct.rs_field()).collect(),
684            blind_cols: 0,
685            h_cols: 0,
686            phys_bit,
687            phys_whole,
688            h_whole: Vec::new(),
689        };
690
691        plan.append_tail(num_blind, num_h);
692
693        Ok(plan)
694    }
695
696    pub fn has_ring(&self) -> bool {
697        self.units.iter().any(|(is_ring, _)| *is_ring)
698    }
699
700    pub fn has_ring_blind(&self) -> bool {
701        self.blind_cols > 0 && self.has_ring()
702    }
703
704    pub fn whole_blinds(&self) -> usize {
705        self.blind_cols - usize::from(self.has_ring_blind())
706    }
707
708    pub fn total_claims(&self) -> usize {
709        self.units.iter().map(|(_, n)| n).sum()
710    }
711
712    pub fn total_claims_at(&self, num_vars: usize, split_vars: usize) -> usize {
713        let pack = 1usize << split_vars.saturating_sub(num_vars);
714
715        self.total_claims() + self.whole_blinds() * (pack - 1)
716    }
717
718    pub fn opened_row_bytes(&self) -> usize {
719        let trace: usize = self.phys_rs.iter().map(|ct| ct.byte_size()).sum();
720
721        trace + self.h_cols * ColumnType::B128.byte_size()
722    }
723
724    /// Committed columns in codeword slots at `split_vars`;
725    /// past `num_vars` a slot packs `2^(split - num_vars)` columns
726    /// of one class, zero blocks closing a class's last slot.
727    pub fn at_split(&self, num_vars: usize, split_vars: usize) -> SlotLayout {
728        let pack_vars = split_vars.saturating_sub(num_vars);
729        let pack = 1usize << pack_vars;
730
731        let plan = match pack {
732            1 => self.clone(),
733            _ => self.with_whole_blinds(self.whole_blinds() * pack),
734        };
735
736        let num_cols = plan.phys_rs.len();
737
738        let mut open: Vec<(SlotClass, usize)> = Vec::new();
739        let mut slot_rs = Vec::new();
740        let mut slot_roles = Vec::new();
741        let mut slot_fill: Vec<usize> = Vec::new();
742        let mut col_slot = Vec::with_capacity(num_cols);
743
744        for p in 0..num_cols {
745            let class = plan.slot_class(p);
746            let current = open.iter().position(|(c, _)| *c == class);
747
748            let slot = match current {
749                Some(i) if slot_fill[open[i].1] < pack => open[i].1,
750                _ => {
751                    let s = slot_rs.len();
752
753                    slot_rs.push(class.0);
754                    slot_roles.push((class.1, class.2));
755                    slot_fill.push(0);
756
757                    match current {
758                        Some(i) => open[i].1 = s,
759                        None => open.push((class, s)),
760                    }
761
762                    s
763                }
764            };
765
766            col_slot.push((slot, slot_fill[slot]));
767
768            slot_fill[slot] += 1;
769        }
770
771        let num_slots = slot_rs.len();
772        let h_slots = plan.h_cols.div_ceil(pack);
773
774        let mut slot_start = Vec::with_capacity(num_slots + 1);
775        slot_start.push(0);
776
777        for &fill in &slot_fill {
778            slot_start.push(slot_start[slot_start.len() - 1] + fill);
779        }
780
781        let mut slot_cols = vec![0; num_cols];
782        for (p, &(slot, block)) in col_slot.iter().enumerate() {
783            slot_cols[slot_start[slot] + block] = p;
784        }
785
786        let mut group_base = Vec::with_capacity(num_slots + h_slots);
787        let mut groups = 0;
788
789        for &(ring, whole) in &slot_roles {
790            group_base.push(groups);
791
792            groups += ring + whole;
793        }
794
795        for _ in 0..h_slots {
796            group_base.push(groups);
797
798            groups += 1;
799        }
800
801        let mut unit_home = vec![(0, 0, 0, false); plan.num_units];
802
803        for (p, &(slot, block)) in col_slot.iter().enumerate() {
804            let rings = plan.phys_bit[p].len();
805
806            for (i, &u) in plan.phys_bit[p].iter().enumerate() {
807                unit_home[u] = (slot, block, group_base[slot] + i, true);
808            }
809
810            for (i, &u) in plan.phys_whole[p].iter().enumerate() {
811                unit_home[u] = (slot, block, group_base[slot] + rings + i, false);
812            }
813        }
814
815        for (k, &u) in plan.h_whole.iter().enumerate() {
816            let slot = num_slots + k / pack;
817            unit_home[u] = (slot, k % pack, group_base[slot], false);
818        }
819
820        let mut group_sigma: Vec<Option<usize>> = vec![None; groups];
821        let mut sigma_slot = Vec::with_capacity(groups);
822        let mut unit_sigma = Vec::with_capacity(plan.num_units);
823        let mut unit_block = Vec::with_capacity(plan.num_units);
824
825        for &(slot, block, group, ring) in &unit_home {
826            let sigma = *group_sigma[group].get_or_insert_with(|| {
827                sigma_slot.push((slot, ring));
828
829                sigma_slot.len() - 1
830            });
831
832            unit_sigma.push(sigma);
833            unit_block.push(block);
834        }
835
836        SlotLayout {
837            plan,
838            num_vars,
839            split_vars,
840            slot_rs,
841            h_slots,
842            num_sigma: sigma_slot.len(),
843            slot_start,
844            slot_cols,
845            unit_sigma,
846            unit_block,
847            sigma_base: 0,
848            sigma_shift: sigma_slot.len(),
849            sigma_slot,
850        }
851    }
852
853    fn append_tail(&mut self, num_blind: usize, num_h: usize) {
854        let has_ring = self.has_ring();
855
856        for _ in 0..num_blind {
857            self.phys_bit.push(Vec::new());
858            self.phys_whole.push(vec![self.units.len()]);
859            self.phys_rs.push(ColumnType::B128);
860            self.units.push((false, 1));
861        }
862
863        self.blind_cols = num_blind;
864
865        // A uniform L-valued unit
866        if has_ring && num_blind > 0 {
867            self.phys_bit.push(vec![self.units.len()]);
868            self.phys_whole.push(Vec::new());
869            self.phys_rs.push(ColumnType::B128);
870            self.units.push((true, RING_BLIND_BITS));
871
872            self.blind_cols += 1;
873        }
874
875        for _ in 0..num_h {
876            self.h_whole.push(self.units.len());
877            self.units.push((false, 1));
878        }
879
880        self.h_cols = num_h;
881        self.num_units = self.units.len();
882    }
883
884    fn with_whole_blinds(&self, num_blind: usize) -> Self {
885        let data_cols = self.phys_rs.len() - self.blind_cols;
886        let data_units = self.num_units - self.blind_cols - self.h_cols;
887
888        let mut plan = Self {
889            num_units: data_units,
890            units: self.units[..data_units].to_vec(),
891            phys_rs: self.phys_rs[..data_cols].to_vec(),
892            blind_cols: 0,
893            h_cols: 0,
894            phys_bit: self.phys_bit[..data_cols].to_vec(),
895            phys_whole: self.phys_whole[..data_cols].to_vec(),
896            h_whole: Vec::new(),
897        };
898
899        plan.append_tail(num_blind, self.h_cols);
900
901        plan
902    }
903
904    fn slot_class(&self, col: usize) -> SlotClass {
905        (
906            self.phys_rs[col],
907            self.phys_bit[col].len(),
908            self.phys_whole[col].len(),
909            col >= self.phys_rs.len() - self.blind_cols,
910        )
911    }
912
913    fn census(&self) -> Census {
914        let mut data: Vec<(SlotClass, usize)> = Vec::new();
915        for col in 0..self.phys_rs.len() - self.blind_cols {
916            let class = self.slot_class(col);
917
918            match data.iter_mut().find(|(c, _)| *c == class) {
919                Some((_, count)) => *count += 1,
920                None => data.push((class, 1)),
921            }
922        }
923
924        Census {
925            data,
926            whole_blinds: self.whole_blinds(),
927            ring_blind: self.has_ring_blind(),
928            h_cols: self.h_cols,
929        }
930    }
931}
932
933type SlotClass = (ColumnType, usize, usize, bool);
934
935/// A [`RingSwitchPlan`] at one table height: committed columns
936/// in codeword slots, block `b` of a slot at message offset
937/// `b · 2^num_vars`, and one `eta` exponent per slot role.
938pub struct SlotLayout {
939    pub plan: RingSwitchPlan,
940    pub num_vars: usize,
941    pub split_vars: usize,
942    pub slot_rs: Vec<ColumnType>,
943    pub h_slots: usize,
944    pub num_sigma: usize,
945
946    slot_start: Vec<usize>,
947    slot_cols: Vec<usize>,
948    unit_sigma: Vec<usize>,
949    unit_block: Vec<usize>,
950    sigma_slot: Vec<(usize, bool)>,
951    sigma_base: usize,
952    sigma_shift: usize,
953}
954
955impl SlotLayout {
956    pub fn pack_vars(&self) -> usize {
957        self.split_vars.saturating_sub(self.num_vars)
958    }
959
960    pub fn pack(&self) -> usize {
961        1 << self.pack_vars()
962    }
963
964    pub fn grid_cols(&self) -> usize {
965        1 << self.split_vars
966    }
967
968    pub fn grid_rows(&self) -> usize {
969        1 << self.num_vars.saturating_sub(self.split_vars)
970    }
971
972    /// Plan columns of trace slot `slot`, in block order.
973    pub fn slot_columns(&self, slot: usize) -> &[usize] {
974        &self.slot_cols[self.slot_start[slot]..self.slot_start[slot + 1]]
975    }
976
977    pub fn h_slot_columns(&self, slot: usize) -> Range<usize> {
978        let pack = self.pack();
979
980        slot * pack..((slot + 1) * pack).min(self.plan.h_cols)
981    }
982
983    pub fn leaf_row_bytes(&self) -> usize {
984        self.slot_rs.iter().map(|ct| ct.byte_size()).sum()
985    }
986
987    pub fn h_leaf_row_bytes(&self) -> usize {
988        self.h_slots * ColumnType::B128.byte_size()
989    }
990
991    pub fn unit_block(&self, unit: usize) -> usize {
992        self.unit_block[unit]
993    }
994
995    /// PRF salt of blind column `blind`'s message,
996    /// past every slot's support salt.
997    pub fn blind_salt(&self, blind: usize) -> usize {
998        self.slot_rs.len() + blind
999    }
1000
1001    /// Per-unit claim weights `eta^σ(u) · eq(rho, b(u))`
1002    /// and the next-row factor `eta^#σ`. Errors unless
1003    /// `rho` holds one challenge per pack variable.
1004    pub fn unit_weights<F: HardwareField>(
1005        &self,
1006        eta: Flat<F>,
1007        rho: &[Flat<F>],
1008    ) -> Result<(Vec<Flat<F>>, Flat<F>), Error> {
1009        if rho.len() != self.pack_vars() {
1010            return Err(Error::Protocol {
1011                protocol: "ring_switch_plan",
1012                message: "block challenge count does not match the pack factor",
1013            });
1014        }
1015
1016        let (mut weights, shift) = self.sigma_weights(eta);
1017
1018        let eq = TensorProduct::new(rho.to_vec());
1019
1020        for (weight, &block) in weights.iter_mut().zip(&self.unit_block) {
1021            *weight *= eq.evaluate_at_index(block);
1022        }
1023
1024        Ok((weights, shift))
1025    }
1026
1027    pub fn sigma_weights<F: HardwareField>(&self, eta: Flat<F>) -> (Vec<Flat<F>>, Flat<F>) {
1028        let mut pows = Vec::with_capacity(self.num_sigma);
1029        let mut e = eta_pow(eta, self.sigma_base);
1030
1031        for _ in 0..self.num_sigma {
1032            pows.push(e);
1033
1034            e *= eta;
1035        }
1036
1037        let weights = self.unit_sigma.iter().map(|&sigma| pows[sigma]).collect();
1038
1039        (weights, eta_pow(eta, self.sigma_shift))
1040    }
1041
1042    /// Ring and whole fold coefficients per slot, trace slots
1043    /// then `h` slots, and the next-row factor `eta^#σ`.
1044    pub fn slot_coeffs<F: HardwareField>(
1045        &self,
1046        eta: Flat<F>,
1047    ) -> (Vec<Flat<F>>, Vec<Flat<F>>, Flat<F>) {
1048        let zero = Flat::from_raw(F::ZERO);
1049        let num_slots = self.slot_rs.len() + self.h_slots;
1050
1051        let mut coeff_bit = vec![zero; num_slots];
1052        let mut coeff_whole = vec![zero; num_slots];
1053
1054        let mut e = eta_pow(eta, self.sigma_base);
1055        for &(slot, ring) in &self.sigma_slot {
1056            match ring {
1057                true => coeff_bit[slot] += e,
1058                false => coeff_whole[slot] += e,
1059            }
1060
1061            e *= eta;
1062        }
1063
1064        (coeff_bit, coeff_whole, eta_pow(eta, self.sigma_shift))
1065    }
1066
1067    fn weights_b128<F>(
1068        &self,
1069        eta: F,
1070        rho: &[F],
1071    ) -> Result<(Vec<Flat<Block128>>, Flat<Block128>), Error>
1072    where
1073        F: HardwareField + Into<Block128>,
1074    {
1075        let flat = |x: F| Into::<Block128>::into(x).to_hardware();
1076        let rho: Vec<Flat<Block128>> = rho.iter().map(|&r| flat(r)).collect();
1077
1078        self.unit_weights(flat(eta), &rho)
1079    }
1080}
1081
1082pub struct PoolLayout {
1083    pub split_vars: usize,
1084    pub tables: Vec<SlotLayout>,
1085}
1086
1087impl PoolLayout {
1088    pub fn new(tables: &[(&RingSwitchPlan, usize)], field_bits: usize, config: &Config) -> Self {
1089        Self::at_split(tables, Self::split_vars_for(tables, field_bits, config))
1090    }
1091
1092    /// The split [`Self::new`] lays out, priced on
1093    /// census counts alone: it allocates no blind column.
1094    pub fn split_vars_for(
1095        tables: &[(&RingSwitchPlan, usize)],
1096        field_bits: usize,
1097        config: &Config,
1098    ) -> usize {
1099        let census: Vec<(Census, usize)> = tables
1100            .iter()
1101            .map(|&(plan, num_vars)| (plan.census(), num_vars))
1102            .collect();
1103
1104        let masters: usize = tables
1105            .iter()
1106            .map(|(plan, _)| 1 + usize::from(plan.has_ring()))
1107            .sum();
1108
1109        let max_vars = census.iter().map(|&(_, n)| n).max().unwrap_or(0);
1110
1111        let widest = census
1112            .iter()
1113            .map(|(c, n)| n + c.max_pack_vars())
1114            .fold(max_vars, usize::max);
1115
1116        let support_floor = support_floor_vars(config.ldt_support_size);
1117
1118        let row_bytes = |c: usize| -> usize {
1119            census
1120                .iter()
1121                .map(|(census, n)| {
1122                    (1usize << n.saturating_sub(c)) * census.row_bytes(1 << c.saturating_sub(*n))
1123                })
1124                .sum()
1125        };
1126
1127        let claim_bytes = |c: usize| -> usize {
1128            census
1129                .iter()
1130                .map(|(census, n)| census.blind_claim_bytes(1 << c.saturating_sub(*n)))
1131                .sum()
1132        };
1133
1134        let secure = |c: usize| {
1135            let units: usize = census
1136                .iter()
1137                .map(|(census, n)| census.num_sigma(1 << c.saturating_sub(*n)))
1138                .sum();
1139
1140            let shape = FoldShape {
1141                grid_cols: 1 << c,
1142                grid_rows: 1 << max_vars.saturating_sub(c),
1143                units: units + masters.saturating_sub(1),
1144            };
1145
1146            // Never config.min_security_bits:
1147            // unabsorbed, prover and verifier would diverge.
1148            c >= support_floor
1149                && config.estimated_security_bits(field_bits, shape) >= MIN_PRODUCTION_BITS
1150        };
1151
1152        let num_queries = config.num_queries;
1153
1154        cheapest_split_vars(1, widest, num_queries, row_bytes, claim_bytes, secure)
1155            .or_else(|| {
1156                cheapest_split_vars(1, widest, num_queries, row_bytes, claim_bytes, |_| true)
1157            })
1158            .unwrap_or(0)
1159    }
1160
1161    pub fn at_split(tables: &[(&RingSwitchPlan, usize)], split_vars: usize) -> Self {
1162        let mut layouts: Vec<SlotLayout> = tables
1163            .iter()
1164            .map(|&(plan, num_vars)| plan.at_split(num_vars, split_vars))
1165            .collect();
1166
1167        let sigma_shift = layouts.iter().map(|t| t.num_sigma).sum();
1168
1169        let mut sigma_base = 0;
1170        for layout in &mut layouts {
1171            layout.sigma_base = sigma_base;
1172            layout.sigma_shift = sigma_shift;
1173
1174            sigma_base += layout.num_sigma;
1175        }
1176
1177        Self {
1178            split_vars,
1179            tables: layouts,
1180        }
1181    }
1182
1183    pub fn num_vars(&self) -> usize {
1184        self.tables
1185            .iter()
1186            .map(|t| t.num_vars)
1187            .fold(self.split_vars, usize::max)
1188    }
1189
1190    pub fn grid_cols(&self) -> usize {
1191        1 << self.split_vars
1192    }
1193
1194    pub fn grid_rows(&self) -> usize {
1195        1 << (self.num_vars() - self.split_vars)
1196    }
1197
1198    pub fn num_sigma(&self) -> usize {
1199        self.tables.iter().map(|t| t.num_sigma).sum()
1200    }
1201
1202    pub fn num_masters(&self) -> usize {
1203        self.tables
1204            .iter()
1205            .map(|t| 1 + usize::from(t.plan.has_ring()))
1206            .sum()
1207    }
1208
1209    pub fn rho_vars(&self) -> usize {
1210        self.tables
1211            .iter()
1212            .map(SlotLayout::pack_vars)
1213            .max()
1214            .unwrap_or(0)
1215    }
1216
1217    pub fn fold_shape(&self) -> FoldShape {
1218        FoldShape {
1219            grid_cols: self.grid_cols(),
1220            grid_rows: self.grid_rows(),
1221            units: self.num_sigma() + self.num_masters().saturating_sub(1),
1222        }
1223    }
1224}
1225
1226struct Census {
1227    data: Vec<(SlotClass, usize)>,
1228    whole_blinds: usize,
1229    ring_blind: bool,
1230    h_cols: usize,
1231}
1232
1233impl Census {
1234    fn row_bytes(&self, pack: usize) -> usize {
1235        let data: usize = self
1236            .data
1237            .iter()
1238            .map(|&((rs, ..), count)| count.div_ceil(pack) * rs.byte_size())
1239            .sum();
1240
1241        let wide = self.whole_blinds + usize::from(self.ring_blind) + self.h_cols.div_ceil(pack);
1242
1243        data + wide * ColumnType::B128.byte_size()
1244    }
1245
1246    fn num_sigma(&self, pack: usize) -> usize {
1247        let data: usize = self
1248            .data
1249            .iter()
1250            .map(|&((_, ring, whole, _), count)| count.div_ceil(pack) * (ring + whole))
1251            .sum();
1252
1253        data + self.whole_blinds + usize::from(self.ring_blind) + self.h_cols.div_ceil(pack)
1254    }
1255
1256    fn blind_claim_bytes(&self, pack: usize) -> usize {
1257        2 * self.whole_blinds * (pack - 1) * ColumnType::B128.byte_size()
1258    }
1259
1260    fn max_pack_vars(&self) -> usize {
1261        let widest = self
1262            .data
1263            .iter()
1264            .map(|&(_, count)| count)
1265            .max()
1266            .unwrap_or(0)
1267            .max(self.h_cols);
1268
1269        match widest > 1 {
1270            true => (widest - 1).ilog2() as usize + 1,
1271            false => 0,
1272        }
1273    }
1274}
1275
1276fn expand_bit_width(storage: ColumnType) -> Result<usize, Error> {
1277    match storage {
1278        ColumnType::B8 => Ok(8),
1279        ColumnType::B16 => Ok(16),
1280        ColumnType::B32 => Ok(32),
1281        ColumnType::B64 => Ok(64),
1282        _ => Err(Error::Protocol {
1283            protocol: "virtual_expand",
1284            message: "ExpandBits requires B8/B16/B32/B64",
1285        }),
1286    }
1287}
1288
1289/// Tower-basis bit extraction from LE bytes.
1290fn parse_tower_bit(storage: ColumnType, bytes: &[u8], bit_idx: usize) -> u8 {
1291    match storage {
1292        ColumnType::B8 => Flat::from_raw(Block8(bytes[0])).tower_bit(bit_idx),
1293        ColumnType::B16 => {
1294            let mut arr = [0u8; 2];
1295            arr.copy_from_slice(bytes);
1296
1297            Flat::from_raw(Block16(u16::from_le_bytes(arr))).tower_bit(bit_idx)
1298        }
1299        ColumnType::B32 => {
1300            let mut arr = [0u8; 4];
1301            arr.copy_from_slice(bytes);
1302
1303            Flat::from_raw(Block32(u32::from_le_bytes(arr))).tower_bit(bit_idx)
1304        }
1305        ColumnType::B64 => {
1306            let mut arr = [0u8; 8];
1307            arr.copy_from_slice(bytes);
1308
1309            Flat::from_raw(Block64(u64::from_le_bytes(arr))).tower_bit(bit_idx)
1310        }
1311        _ => unreachable!(),
1312    }
1313}
1314
1315fn expand_packed_bit<F: TraceCompatibleField + 'static>(
1316    col: &'_ TraceColumn,
1317    storage: ColumnType,
1318    bit_idx: usize,
1319) -> Result<PolyVariant<'_, F>, Error> {
1320    match storage {
1321        ColumnType::B8 => {
1322            let data = col.as_b8_slice().ok_or(Error::Protocol {
1323                protocol: "virtual_expand",
1324                message: "ExpandBits B8: column type mismatch",
1325            })?;
1326
1327            Ok(PolyVariant::PackedBitB8 { data, bit_idx })
1328        }
1329        ColumnType::B16 => {
1330            let data = col.as_b16_slice().ok_or(Error::Protocol {
1331                protocol: "virtual_expand",
1332                message: "ExpandBits B16: column type mismatch",
1333            })?;
1334
1335            Ok(PolyVariant::PackedBitB16 { data, bit_idx })
1336        }
1337        ColumnType::B32 => {
1338            let data = col.as_b32_slice().ok_or(Error::Protocol {
1339                protocol: "virtual_expand",
1340                message: "ExpandBits B32: column type mismatch",
1341            })?;
1342
1343            Ok(PolyVariant::PackedBitB32 { data, bit_idx })
1344        }
1345        ColumnType::B64 => {
1346            let data = col.as_b64_slice().ok_or(Error::Protocol {
1347                protocol: "virtual_expand",
1348                message: "ExpandBits B64: column type mismatch",
1349            })?;
1350
1351            Ok(PolyVariant::PackedBitB64 { data, bit_idx })
1352        }
1353        _ => unreachable!(),
1354    }
1355}
1356
1357fn expand_pass_through<F: TraceCompatibleField + 'static>(
1358    col: &TraceColumn,
1359    storage: ColumnType,
1360) -> Result<PolyVariant<'_, F>, Error> {
1361    match storage {
1362        ColumnType::Bit => {
1363            let data = col.as_bit_slice().ok_or(Error::Protocol {
1364                protocol: "virtual_expand",
1365                message: "PassThrough Bit: column type mismatch",
1366            })?;
1367
1368            Ok(PolyVariant::BitSlice(data))
1369        }
1370        ColumnType::B8 => {
1371            let data = col.as_b8_slice().ok_or(Error::Protocol {
1372                protocol: "virtual_expand",
1373                message: "PassThrough B8: column type mismatch",
1374            })?;
1375
1376            Ok(PolyVariant::B8Slice(data))
1377        }
1378        ColumnType::B16 => {
1379            let data = col.as_b16_slice().ok_or(Error::Protocol {
1380                protocol: "virtual_expand",
1381                message: "PassThrough B16: column type mismatch",
1382            })?;
1383
1384            Ok(PolyVariant::B16Slice(data))
1385        }
1386        ColumnType::B32 => {
1387            let data = col.as_b32_slice().ok_or(Error::Protocol {
1388                protocol: "virtual_expand",
1389                message: "PassThrough B32: column type mismatch",
1390            })?;
1391
1392            Ok(PolyVariant::B32Slice(data))
1393        }
1394        ColumnType::B64 => {
1395            let data = col.as_b64_slice().ok_or(Error::Protocol {
1396                protocol: "virtual_expand",
1397                message: "PassThrough B64: column type mismatch",
1398            })?;
1399
1400            Ok(PolyVariant::B64Slice(data))
1401        }
1402        ColumnType::B128 => {
1403            let data = col.as_b128_slice().ok_or(Error::Protocol {
1404                protocol: "virtual_expand",
1405                message: "PassThrough B128: column type mismatch",
1406            })?;
1407
1408            Ok(PolyVariant::B128Slice(data))
1409        }
1410    }
1411}
1412
1413pub fn eq_tensor_b(r: &[Block128]) -> Vec<Block128> {
1414    let mut t = vec![Block128::ONE];
1415    for &ri in r {
1416        let len = t.len();
1417        let mut nt = Vec::with_capacity(len * 2);
1418
1419        for &v in &t {
1420            nt.push(v * (Block128::ONE + ri));
1421        }
1422
1423        for &v in &t {
1424            nt.push(v * ri);
1425        }
1426
1427        t = nt;
1428    }
1429
1430    t
1431}
1432
1433/// Σ_u eq(r'',u)·ŝ_u for one ring unit, ŝ_u = Σ_v bit_u(c_v)·2^v.
1434pub fn ring_batch_b(bit_claims: &[Block128], eq_mix: &[Block128]) -> Block128 {
1435    let mut acc = Block128::ZERO;
1436    for (u, &m) in eq_mix.iter().enumerate() {
1437        let mut shat = 0u128;
1438        for (v, cv) in bit_claims.iter().enumerate() {
1439            shat |= ((cv.0 >> u) & 1) << v;
1440        }
1441
1442        acc += m * Block128(shat);
1443    }
1444
1445    acc
1446}
1447
1448/// Reconstructs the sumcheck's initial claim from the claimed
1449/// virtual evals, in the tower basis. Under its layout weight `w`
1450/// a ring unit contributes `w·Σ_u eq(r'',u) ŝ_u`, a whole unit `w·c'`.
1451pub fn ring_target<F>(
1452    layout: &SlotLayout,
1453    claims: &[Flat<F>],
1454    eta_tower: F,
1455    rho: &[F],
1456    r_mix: &[Block128],
1457    shifted_claims: bool,
1458) -> Result<Block128, Error>
1459where
1460    F: HardwareField + Into<Block128>,
1461{
1462    let (weights, shift) = layout.weights_b128(eta_tower, rho)?;
1463
1464    let eq_mix = eq_tensor_b(r_mix);
1465    let plan = &layout.plan;
1466
1467    match claims.len() < RING_TABLE_MIN_CLAIMS {
1468        true => Ok(ring_target_transposed(
1469            plan,
1470            claims,
1471            &weights,
1472            shift,
1473            &eq_mix,
1474            shifted_claims,
1475        )),
1476        false => Ok(ring_target_tabulated(
1477            plan,
1478            claims,
1479            &weights,
1480            shift,
1481            &eq_mix,
1482            shifted_claims,
1483        )),
1484    }
1485}
1486
1487/// Per-claim weight `a_c` such that
1488/// `ring_target = Σ_c a_c · (ring ? φ_{r''}(c_c) : c_c)`,
1489/// with `φ_{r''}(x) = Σ_u eq(r'',u)·bit_u(x)`.
1490pub fn claim_weights<F>(
1491    layout: &SlotLayout,
1492    eta_tower: F,
1493    rho: &[F],
1494    shifted_claims: bool,
1495) -> Result<Vec<(bool, Block128)>, Error>
1496where
1497    F: HardwareField + Into<Block128>,
1498{
1499    let (unit_weights, shift) = layout.weights_b128(eta_tower, rho)?;
1500
1501    let plan = &layout.plan;
1502
1503    let shifts: &[Block128] = if shifted_claims {
1504        &[Block128::ONE, shift.to_tower()]
1505    } else {
1506        &[Block128::ONE]
1507    };
1508
1509    let mut weights = Vec::with_capacity(plan.total_claims() * shifts.len());
1510    for &shift_mul in shifts {
1511        for (&(is_ring, num_claims), unit_weight) in plan.units.iter().zip(&unit_weights) {
1512            let weight = unit_weight.to_tower() * shift_mul;
1513            for v in 0..num_claims {
1514                let basis = if is_ring {
1515                    Block128(1u128 << v)
1516                } else {
1517                    Block128::ONE
1518                };
1519
1520                weights.push((is_ring, weight * basis));
1521            }
1522        }
1523    }
1524
1525    Ok(weights)
1526}
1527
1528/// [`ring_target`] computed in the tower basis by
1529/// transposing each ring unit's claim bits into `ŝ_u`.
1530fn ring_target_transposed<F>(
1531    plan: &RingSwitchPlan,
1532    claims: &[Flat<F>],
1533    unit_weights: &[Flat<Block128>],
1534    shift: Flat<Block128>,
1535    eq_mix: &[Block128],
1536    shifted_claims: bool,
1537) -> Block128
1538where
1539    F: HardwareField + Into<Block128>,
1540{
1541    let claim_halves = if shifted_claims { 2 } else { 1 };
1542    let half = claims.len() / claim_halves;
1543
1544    let base = [(0usize, Block128::ONE)];
1545    let base_and_shift = [(0usize, Block128::ONE), (half, shift.to_tower())];
1546
1547    let offsets: &[(usize, Block128)] = if shifted_claims {
1548        &base_and_shift
1549    } else {
1550        &base
1551    };
1552
1553    let mut target = Block128::ZERO;
1554    for &(offset, shift_mul) in offsets {
1555        let half_claims = &claims[offset..offset + half];
1556
1557        let mut ci = 0usize;
1558        for (&(is_ring, num_claims), unit_weight) in plan.units.iter().zip(unit_weights) {
1559            let weight = unit_weight.to_tower() * shift_mul;
1560            if is_ring {
1561                let bits: Vec<Block128> = half_claims[ci..ci + num_claims]
1562                    .iter()
1563                    .map(|f| f.to_tower().into())
1564                    .collect();
1565
1566                target += weight * ring_batch_b(&bits, eq_mix);
1567            } else {
1568                let c: Block128 = half_claims[ci].to_tower().into();
1569                target += weight * c;
1570            }
1571
1572            ci += num_claims;
1573        }
1574    }
1575
1576    target
1577}
1578
1579/// [`ring_target`] computed in the flat basis, each
1580/// ring unit as `Σ_v 2^v · φ_{r''}(c_v)` with `φ_{r''}`
1581/// read from byte tables on the claims' raw bits.
1582fn ring_target_tabulated<F>(
1583    plan: &RingSwitchPlan,
1584    claims: &[Flat<F>],
1585    unit_weights: &[Flat<Block128>],
1586    shift: Flat<Block128>,
1587    eq_mix: &[Block128],
1588    shifted_claims: bool,
1589) -> Block128
1590where
1591    F: HardwareField + Into<Block128>,
1592{
1593    let zero = Flat::from_raw(Block128::ZERO);
1594    let one = Flat::from_raw(Block128::ONE);
1595
1596    let eq_flat: Vec<Flat<Block128>> = eq_mix.iter().map(|m| m.to_hardware()).collect();
1597
1598    let phi = LinearMap::new(|x: Flat<F>| {
1599        let tower: Block128 = x.to_tower().into();
1600
1601        let mut acc = zero;
1602        for (u, &m) in eq_flat.iter().enumerate() {
1603            if (tower.0 >> u) & 1 == 1 {
1604                acc += m;
1605            }
1606        }
1607
1608        acc
1609    });
1610
1611    let basis: Vec<Flat<Block128>> = (0..BITS)
1612        .map(|v| Block128(1u128 << v).to_hardware())
1613        .collect();
1614
1615    let claim_halves = if shifted_claims { 2 } else { 1 };
1616    let half = claims.len() / claim_halves;
1617
1618    let base = [(0usize, one)];
1619    let base_and_shift = [(0usize, one), (half, shift)];
1620
1621    let offsets: &[(usize, Flat<Block128>)] = if shifted_claims {
1622        &base_and_shift
1623    } else {
1624        &base
1625    };
1626
1627    let mut target = zero;
1628    for &(offset, shift_mul) in offsets {
1629        let half_claims = &claims[offset..offset + half];
1630
1631        let mut ci = 0usize;
1632        for (&(is_ring, num_claims), &unit_weight) in plan.units.iter().zip(unit_weights) {
1633            let unit_claims = &half_claims[ci..ci + num_claims];
1634            let value = if is_ring {
1635                let mut acc = zero;
1636                for (&c, &b) in unit_claims.iter().zip(&basis) {
1637                    acc += b * phi.apply(c);
1638                }
1639
1640                acc
1641            } else {
1642                Into::<Block128>::into(unit_claims[0].to_tower()).to_hardware()
1643            };
1644
1645            target += unit_weight * shift_mul * value;
1646            ci += num_claims;
1647        }
1648    }
1649
1650    target.to_tower()
1651}
1652
1653fn eta_pow<F: HardwareField>(eta: Flat<F>, exp: usize) -> Flat<F> {
1654    let mut acc = Flat::from_raw(F::ONE);
1655    for _ in 0..exp {
1656        acc *= eta;
1657    }
1658
1659    acc
1660}
1661
1662#[cfg(test)]
1663mod tests {
1664    use super::*;
1665    use hekate_core::trace::TraceBuilder;
1666    use hekate_math::{Block128, TowerField};
1667
1668    fn keccak_expander() -> VirtualExpander {
1669        VirtualExpander::new()
1670            .expand_bits(25, ColumnType::B64)
1671            .expand_bits(1, ColumnType::B64)
1672            .reuse_pass_through(0, 25)
1673            .control_bits(2)
1674            .build()
1675            .unwrap()
1676    }
1677
1678    fn keccak_physical_layout() -> Vec<ColumnType> {
1679        let mut layout = vec![ColumnType::B64; 26];
1680        layout.extend(repeat_n(ColumnType::Bit, 2));
1681
1682        layout
1683    }
1684
1685    fn plan_of(cols: usize) -> RingSwitchPlan {
1686        RingSwitchPlan::new(&vec![ColumnType::B128; cols], None, 1, 1).unwrap()
1687    }
1688
1689    fn mixed_plan(num_blind: usize, num_h: usize) -> RingSwitchPlan {
1690        let expander = VirtualExpander::new()
1691            .expand_bits(5, ColumnType::B32)
1692            .expand_bits(3, ColumnType::B64)
1693            .pass_through(3, ColumnType::B64)
1694            .reuse_pass_through(5, 3)
1695            .reuse_pass_through(8, 1)
1696            .control_bits(2)
1697            .pass_through(1, ColumnType::B128)
1698            .build()
1699            .unwrap();
1700
1701        let mut layout = vec![ColumnType::B32; 5];
1702        layout.extend(repeat_n(ColumnType::B64, 6));
1703        layout.extend(repeat_n(ColumnType::Bit, 2));
1704        layout.push(ColumnType::B128);
1705
1706        RingSwitchPlan::new(
1707            &layout,
1708            Some(&expander.expansion_entries()),
1709            num_blind,
1710            num_h,
1711        )
1712        .unwrap()
1713    }
1714
1715    fn xorshift(seed: u128) -> impl FnMut() -> Block128 {
1716        let mut state = seed;
1717        move || {
1718            state ^= state << 13;
1719            state ^= state >> 7;
1720            state ^= state << 17;
1721
1722            Block128(state)
1723        }
1724    }
1725
1726    fn narrow_plan(cols: usize, config: &Config) -> RingSwitchPlan {
1727        RingSwitchPlan::new(&vec![ColumnType::B32; cols], None, config.blind_units(), 0).unwrap()
1728    }
1729
1730    fn cheapest_split(
1731        tables: &[(&RingSwitchPlan, usize)],
1732        config: &Config,
1733        admissible: impl Fn(FoldShape) -> bool,
1734    ) -> Option<usize> {
1735        let hi = tables
1736            .iter()
1737            .map(|(plan, num_vars)| num_vars + plan.census().max_pack_vars())
1738            .max()
1739            .unwrap_or(0);
1740
1741        let row_bytes = |c: usize| {
1742            tables
1743                .iter()
1744                .map(|&(plan, num_vars)| {
1745                    let at = plan.at_split(num_vars, c);
1746
1747                    at.grid_rows() * (at.leaf_row_bytes() + at.h_leaf_row_bytes())
1748                })
1749                .sum()
1750        };
1751
1752        let claim_bytes = |c: usize| {
1753            tables
1754                .iter()
1755                .map(|&(plan, num_vars)| {
1756                    plan.census()
1757                        .blind_claim_bytes(1 << c.saturating_sub(num_vars))
1758                })
1759                .sum()
1760        };
1761
1762        cheapest_split_vars(1, hi, config.num_queries, row_bytes, claim_bytes, |c| {
1763            admissible(PoolLayout::at_split(tables, c).fold_shape())
1764        })
1765    }
1766
1767    #[test]
1768    fn ram_layout() {
1769        let e = VirtualExpander::new()
1770            .expand_bits(2, ColumnType::B32)
1771            .pass_through(13, ColumnType::B32)
1772            .pass_through(1, ColumnType::B128)
1773            .control_bits(4)
1774            .build()
1775            .unwrap();
1776
1777        assert_eq!(e.num_virtual_columns(), 82);
1778        assert_eq!(e.num_physical_columns(), 20);
1779        assert_eq!(e.physical_row_bytes(), 80);
1780
1781        let layout = e.virtual_layout();
1782
1783        assert_eq!(layout.len(), 82);
1784        assert!(layout[..64].iter().all(|&t| t == ColumnType::Bit));
1785        assert!(layout[64..77].iter().all(|&t| t == ColumnType::B32));
1786        assert_eq!(layout[77], ColumnType::B128);
1787        assert!(layout[78..82].iter().all(|&t| t == ColumnType::Bit));
1788    }
1789
1790    #[test]
1791    fn keccak_layout() {
1792        let e = VirtualExpander::new()
1793            .expand_bits(25, ColumnType::B64)
1794            .expand_bits(1, ColumnType::B64)
1795            .reuse_pass_through(0, 25)
1796            .control_bits(2)
1797            .build()
1798            .unwrap();
1799
1800        assert_eq!(e.num_virtual_columns(), 1691);
1801        assert_eq!(e.num_physical_columns(), 28);
1802        assert_eq!(e.physical_row_bytes(), 210);
1803
1804        let layout = e.virtual_layout();
1805
1806        assert_eq!(layout.len(), 1691);
1807        assert!(layout[..1600].iter().all(|&t| t == ColumnType::Bit));
1808        assert!(layout[1600..1664].iter().all(|&t| t == ColumnType::Bit));
1809        assert!(layout[1664..1689].iter().all(|&t| t == ColumnType::B64));
1810        assert!(layout[1689..1691].iter().all(|&t| t == ColumnType::Bit));
1811    }
1812
1813    #[test]
1814    fn ring_switch_plan_rejects_uncovered_columns() {
1815        let expander = keccak_expander();
1816        let entries = expander.expansion_entries();
1817
1818        let mut layout = keccak_physical_layout();
1819
1820        assert_eq!(expander.num_physical_columns(), layout.len());
1821        assert!(RingSwitchPlan::new(&layout, Some(&entries), 0, 0).is_ok());
1822
1823        layout.push(ColumnType::B64);
1824
1825        assert!(RingSwitchPlan::new(&layout, Some(&entries), 0, 0).is_err());
1826    }
1827
1828    #[test]
1829    fn ring_switch_plan_folds_every_committed_column() {
1830        let entries = keccak_expander().expansion_entries();
1831        let layout = keccak_physical_layout();
1832
1833        let plan = RingSwitchPlan::new(&layout, Some(&entries), 2, 0).unwrap();
1834
1835        let zero = Flat::from_raw(Block128::ZERO);
1836        let eta = Block128(0x2545F4914F6CDD1D_517CC1B727220A95).to_hardware();
1837
1838        for split in [4, 5, 7] {
1839            let at = plan.at_split(4, split);
1840            let (coeff_bit, coeff_whole, _) = at.slot_coeffs::<Block128>(eta);
1841
1842            for s in 0..at.slot_rs.len() {
1843                assert!(
1844                    coeff_bit[s] != zero || coeff_whole[s] != zero,
1845                    "split {split}: committed slot {s} enters no master fold"
1846                );
1847            }
1848        }
1849    }
1850
1851    #[test]
1852    fn h_units_trail_ring_blind() {
1853        let layout = [ColumnType::B32, ColumnType::B64];
1854        let expander = VirtualExpander::new()
1855            .expand_bits(1, ColumnType::B32)
1856            .pass_through(1, ColumnType::B64)
1857            .build()
1858            .unwrap();
1859        let entries = expander.expansion_entries();
1860
1861        let bare = RingSwitchPlan::new(&layout, Some(&entries), 1, 0).unwrap();
1862        let plan = RingSwitchPlan::new(&layout, Some(&entries), 1, 2).unwrap();
1863
1864        assert_eq!(plan.h_cols, 2);
1865        assert_eq!(plan.phys_rs, bare.phys_rs);
1866        assert_eq!(plan.total_claims(), bare.total_claims() + 2);
1867        assert_eq!(&plan.units[plan.num_units - 2..], &[(false, 1), (false, 1)]);
1868        assert_eq!(plan.units[plan.num_units - 3], (true, RING_BLIND_BITS));
1869        assert_eq!(plan.opened_row_bytes(), bare.opened_row_bytes() + 32);
1870
1871        let at = plan.at_split(3, 3);
1872
1873        assert_eq!(at.leaf_row_bytes(), bare.opened_row_bytes());
1874        assert_eq!(at.h_leaf_row_bytes(), 32);
1875
1876        let eta = Block128::from(0x9E37_79B9u128).to_hardware();
1877        let (coeff_bit, coeff_whole, eta_shift) = at.slot_coeffs::<Block128>(eta);
1878        let n = plan.phys_rs.len();
1879
1880        let mut first_h = Flat::from_raw(Block128::ONE);
1881        for _ in 0..plan.num_units - 2 {
1882            first_h *= eta;
1883        }
1884
1885        assert_eq!(coeff_whole.len(), n + 2);
1886        assert_eq!(coeff_bit[n..], [Flat::from_raw(Block128::ZERO); 2]);
1887        assert_eq!(coeff_whole[n], first_h);
1888        assert_eq!(coeff_whole[n + 1], first_h * eta);
1889        assert_eq!(eta_shift, first_h * eta * eta);
1890    }
1891
1892    #[test]
1893    fn blinded_ring_plan_ends_in_ring_blind_unit() {
1894        let entries = keccak_expander().expansion_entries();
1895        let layout = keccak_physical_layout();
1896
1897        let raw = RingSwitchPlan::new(&layout, Some(&entries), 0, 0).unwrap();
1898        let blinded = RingSwitchPlan::new(&layout, Some(&entries), 2, 0).unwrap();
1899        let whole_only = RingSwitchPlan::new(&[ColumnType::B32; 3], None, 2, 0).unwrap();
1900
1901        assert_eq!(raw.blind_cols, 0);
1902        assert_eq!(raw.phys_rs.len(), layout.len());
1903
1904        assert_eq!(blinded.blind_cols, 3);
1905        assert_eq!(blinded.whole_blinds(), 2);
1906        assert_eq!(blinded.phys_rs.len(), layout.len() + 3);
1907        assert_eq!(blinded.units.last(), Some(&(true, RING_BLIND_BITS)));
1908        assert_eq!(
1909            blinded.total_claims(),
1910            raw.total_claims() + 2 + RING_BLIND_BITS
1911        );
1912
1913        assert_eq!(whole_only.blind_cols, 2);
1914        assert_eq!(whole_only.whole_blinds(), 2);
1915        assert_eq!(whole_only.units.last(), Some(&(false, 1)));
1916
1917        let eta = Block128(0x2545F4914F6CDD1D_517CC1B727220A95).to_hardware();
1918        let at = blinded.at_split(4, 4);
1919        let (coeff_bit, coeff_whole, _) = at.slot_coeffs::<Block128>(eta);
1920        let last = blinded.phys_rs.len() - 1;
1921
1922        assert_ne!(coeff_bit[last], Flat::from_raw(Block128::ZERO));
1923        assert_eq!(coeff_whole[last], Flat::from_raw(Block128::ZERO));
1924    }
1925
1926    #[test]
1927    fn reuse_partial_range() {
1928        let e = VirtualExpander::new()
1929            .expand_bits(10, ColumnType::B32)
1930            .reuse_pass_through(3, 4)
1931            .build()
1932            .unwrap();
1933
1934        assert_eq!(e.num_virtual_columns(), 324);
1935        assert_eq!(e.num_physical_columns(), 10);
1936        assert_eq!(e.physical_row_bytes(), 40);
1937
1938        let layout = e.virtual_layout();
1939
1940        assert_eq!(layout[320..324].len(), 4);
1941        assert!(layout[320..324].iter().all(|&t| t == ColumnType::B32));
1942    }
1943
1944    #[test]
1945    fn reuse_exceeds_declared() {
1946        let result = VirtualExpander::new()
1947            .expand_bits(5, ColumnType::B32)
1948            .reuse_pass_through(3, 5)
1949            .build();
1950
1951        assert!(result.is_err());
1952    }
1953
1954    #[test]
1955    fn reuse_expand_bits_from_pass_through() {
1956        let e = VirtualExpander::new()
1957            .pass_through(4, ColumnType::B64)
1958            .reuse_expand_bits(0, 4)
1959            .build()
1960            .unwrap();
1961
1962        assert_eq!(e.num_physical_columns(), 4);
1963        assert_eq!(e.physical_row_bytes(), 32);
1964        assert_eq!(e.num_virtual_columns(), 4 + 256);
1965
1966        let layout = e.virtual_layout();
1967
1968        assert!(layout[0..4].iter().all(|&t| t == ColumnType::B64));
1969        assert!(layout[4..260].iter().all(|&t| t == ColumnType::Bit));
1970    }
1971
1972    #[test]
1973    fn reuse_expand_bits_exceeds_declared() {
1974        let result = VirtualExpander::new()
1975            .pass_through(4, ColumnType::B64)
1976            .reuse_expand_bits(2, 4)
1977            .build();
1978
1979        assert!(result.is_err());
1980    }
1981
1982    #[test]
1983    fn reuse_expand_bits_rejects_b128_source() {
1984        let result = VirtualExpander::new()
1985            .pass_through(1, ColumnType::B128)
1986            .reuse_expand_bits(0, 1)
1987            .build();
1988
1989        assert!(result.is_err());
1990    }
1991
1992    #[test]
1993    fn expand_rejects_bit() {
1994        let result = VirtualExpander::new()
1995            .expand_bits(1, ColumnType::Bit)
1996            .build();
1997
1998        assert!(result.is_err());
1999    }
2000
2001    #[test]
2002    fn expand_rejects_b128() {
2003        let result = VirtualExpander::new()
2004            .expand_bits(1, ColumnType::B128)
2005            .build();
2006
2007        assert!(result.is_err());
2008    }
2009
2010    #[test]
2011    fn empty_expander() {
2012        let e = VirtualExpander::new();
2013        assert_eq!(e.num_virtual_columns(), 0);
2014        assert_eq!(e.num_physical_columns(), 0);
2015        assert_eq!(e.physical_row_bytes(), 0);
2016        assert!(e.virtual_layout().is_empty());
2017    }
2018
2019    #[test]
2020    fn parse_row_b32_roundtrip() {
2021        let expander = VirtualExpander::new()
2022            .expand_bits(1, ColumnType::B32)
2023            .pass_through(1, ColumnType::B32)
2024            .control_bits(1)
2025            .build()
2026            .unwrap();
2027
2028        let val: u32 = 0xDEAD_BEEF;
2029        let pass_val: u32 = 0x1234_5678;
2030
2031        let mut bytes = Vec::new();
2032        bytes.extend_from_slice(&val.to_le_bytes());
2033        bytes.extend_from_slice(&pass_val.to_le_bytes());
2034        bytes.push(1);
2035
2036        let mut res: Vec<Flat<Block128>> = Vec::new();
2037        expander.parse_row(&bytes, &mut res).unwrap();
2038
2039        assert_eq!(res.len(), 34);
2040
2041        for (bit_idx, elem) in res.iter().enumerate().take(32) {
2042            let expected = Flat::from_raw(Block32(val)).tower_bit(bit_idx);
2043            let got = elem.tower_bit(0);
2044            assert_eq!(got, expected, "bit {bit_idx} mismatch");
2045        }
2046
2047        let pass = res[32];
2048        assert_eq!(
2049            pass,
2050            <Block128 as hekate_math::FlatPromote<Block32>>::promote_flat(Flat::from_raw(Block32(
2051                pass_val
2052            )))
2053        );
2054
2055        let ctrl = res[33].tower_bit(0);
2056        assert_eq!(ctrl, 1);
2057    }
2058
2059    #[test]
2060    fn expand_variants_b32() {
2061        let expander = VirtualExpander::new()
2062            .expand_bits(1, ColumnType::B32)
2063            .pass_through(1, ColumnType::B32)
2064            .control_bits(1)
2065            .build()
2066            .unwrap();
2067
2068        let layout = [ColumnType::B32, ColumnType::B32, ColumnType::Bit];
2069        let num_vars = 2;
2070
2071        let mut tb = TraceBuilder::new(&layout, num_vars).unwrap();
2072        tb.set_b32(0, 0, Block32(0xAAAA_BBBB)).unwrap();
2073        tb.set_b32(1, 0, Block32(0x1111_2222)).unwrap();
2074        tb.set_bit(2, 0, Bit::ONE).unwrap();
2075
2076        let trace = tb.build();
2077
2078        let variants: Vec<PolyVariant<'_, Block128>> = expander.expand_variants(&trace, 0).unwrap();
2079
2080        assert_eq!(variants.len(), 34);
2081
2082        for (i, v) in variants.iter().enumerate().take(32) {
2083            assert!(matches!(v, PolyVariant::PackedBitB32 { bit_idx, .. } if *bit_idx == i));
2084        }
2085
2086        assert!(matches!(variants[32], PolyVariant::B32Slice(_)));
2087        assert!(matches!(variants[33], PolyVariant::BitSlice(_)));
2088    }
2089
2090    #[test]
2091    fn claim_weights_reproduce_ring_target() {
2092        let layout = [
2093            ColumnType::B32,
2094            ColumnType::B32,
2095            ColumnType::B64,
2096            ColumnType::Bit,
2097        ];
2098
2099        let expander = VirtualExpander::new()
2100            .expand_bits(2, ColumnType::B32)
2101            .pass_through(1, ColumnType::B64)
2102            .control_bits(1)
2103            .build()
2104            .unwrap();
2105
2106        let entries = expander.expansion_entries();
2107        let plan = RingSwitchPlan::new(&layout, Some(&entries), 2, 0).unwrap();
2108
2109        let mut state = 0x9e37_79b9_7f4a_7c15_0123_4567_89ab_cdefu128;
2110        let mut next = || {
2111            state ^= state << 13;
2112            state ^= state >> 7;
2113            state ^= state << 17;
2114
2115            Block128(state)
2116        };
2117
2118        for split in [4, 5] {
2119            let at = plan.at_split(4, split);
2120
2121            let claims: Vec<Block128> = (0..2 * at.plan.total_claims()).map(|_| next()).collect();
2122            let eta = next();
2123            let rho: Vec<Block128> = (0..at.pack_vars()).map(|_| next()).collect();
2124            let r_mix: Vec<Block128> = (0..7).map(|_| next()).collect();
2125            let eq_mix = eq_tensor_b(&r_mix);
2126
2127            let flat: Vec<Flat<Block128>> = claims.iter().map(|c| c.to_hardware()).collect();
2128            let target = ring_target::<Block128>(&at, &flat, eta, &rho, &r_mix, true).unwrap();
2129
2130            let mut sum = Block128::ZERO;
2131            for ((is_ring, weight), claim) in claim_weights::<Block128>(&at, eta, &rho, true)
2132                .unwrap()
2133                .into_iter()
2134                .zip(&claims)
2135            {
2136                let value = match is_ring {
2137                    true => ring_batch_b(&[*claim], &eq_mix),
2138                    false => *claim,
2139                };
2140
2141                sum += weight * value;
2142            }
2143
2144            assert_eq!(sum, target, "split {split}");
2145        }
2146    }
2147
2148    /// Whenever a split passes `check_security`,
2149    /// the pool takes the cheapest split that passes.
2150    #[test]
2151    fn adaptive_split_clears_floor_whenever_any_split_can() {
2152        let zk = Config::prod();
2153        let base = Config {
2154            zero_knowledge: false,
2155            ..Config::prod()
2156        };
2157
2158        let wide: Vec<RingSwitchPlan> = [1, 4, 16, 64, 128].into_iter().map(plan_of).collect();
2159        let narrow_zk: Vec<RingSwitchPlan> = (1..=3).map(|cols| narrow_plan(cols, &zk)).collect();
2160        let narrow_base: Vec<RingSwitchPlan> =
2161            (1..=3).map(|cols| narrow_plan(cols, &base)).collect();
2162
2163        let mixed = mixed_plan(zk.blind_units(), 3);
2164
2165        let mut cases: Vec<(&Config, Vec<(&RingSwitchPlan, usize)>)> = Vec::new();
2166
2167        for plan in &wide {
2168            cases.extend((10..=30).map(|n| (&zk, vec![(plan, n)])));
2169        }
2170
2171        for (plan_zk, plan_base) in narrow_zk.iter().zip(&narrow_base) {
2172            cases.extend((1..=16).map(|n| (&zk, vec![(plan_zk, n)])));
2173            cases.extend((1..=16).map(|n| (&base, vec![(plan_base, n)])));
2174        }
2175
2176        for n in 4..=8 {
2177            cases.push((&zk, vec![(&mixed, 16), (&narrow_zk[2], n)]));
2178            cases.push((
2179                &zk,
2180                vec![(&wide[1], 12), (&narrow_zk[0], n), (&narrow_zk[1], n + 1)],
2181            ));
2182        }
2183
2184        let mut filtered = 0;
2185        for (config, tables) in &cases {
2186            let heights: Vec<usize> = tables.iter().map(|&(_, n)| n).collect();
2187            let chosen = PoolLayout::new(tables, 128, config).split_vars;
2188
2189            let passing = cheapest_split(tables, config, |shape| {
2190                config.check_security(128, shape).is_ok()
2191            });
2192
2193            if let Some(best) = passing {
2194                assert_eq!(chosen, best, "heights {heights:?}");
2195            }
2196
2197            if Some(chosen) != cheapest_split(tables, config, |_| true) {
2198                filtered += 1;
2199            }
2200        }
2201
2202        assert!(filtered > 0, "the security filter never fired");
2203    }
2204
2205    #[test]
2206    fn tabulated_ring_target_equals_transposed() {
2207        let layout = [
2208            ColumnType::B8,
2209            ColumnType::B16,
2210            ColumnType::B32,
2211            ColumnType::B64,
2212            ColumnType::B64,
2213            ColumnType::Bit,
2214        ];
2215
2216        let expander = VirtualExpander::new()
2217            .expand_bits(1, ColumnType::B8)
2218            .expand_bits(1, ColumnType::B16)
2219            .expand_bits(1, ColumnType::B32)
2220            .expand_bits(1, ColumnType::B64)
2221            .pass_through(1, ColumnType::B64)
2222            .control_bits(1)
2223            .build()
2224            .unwrap();
2225
2226        let entries = expander.expansion_entries();
2227
2228        let mut state = 0x2545_f491_4f6c_dd1d_9e37_79b9_7f4a_7c15u128;
2229
2230        let mut next = || {
2231            state ^= state << 13;
2232            state ^= state >> 7;
2233            state ^= state << 17;
2234
2235            Block128(state)
2236        };
2237
2238        let cases = [
2239            (0, 0, false, 4),
2240            (2, 1, true, 4),
2241            (1, 3, false, 4),
2242            (1, 3, true, 5),
2243            (2, 3, false, 6),
2244        ];
2245
2246        for (num_blind, num_h, shifted, split) in cases {
2247            let plan = RingSwitchPlan::new(&layout, Some(&entries), num_blind, num_h).unwrap();
2248            let at = plan.at_split(4, split);
2249
2250            let halves = if shifted { 2 } else { 1 };
2251            let claims: Vec<Flat<Block128>> = (0..halves * at.plan.total_claims())
2252                .map(|_| next().to_hardware())
2253                .collect();
2254
2255            let eta = next();
2256            let rho: Vec<Block128> = (0..at.pack_vars()).map(|_| next()).collect();
2257            let (weights, shift) = at.weights_b128(eta, &rho).unwrap();
2258
2259            let r_mix: Vec<Block128> = (0..7).map(|_| next()).collect();
2260            let eq_mix = eq_tensor_b(&r_mix);
2261
2262            assert_eq!(
2263                ring_target_tabulated(&at.plan, &claims, &weights, shift, &eq_mix, shifted),
2264                ring_target_transposed(&at.plan, &claims, &weights, shift, &eq_mix, shifted)
2265            );
2266        }
2267    }
2268
2269    #[test]
2270    fn unpacked_layout_keeps_eta_walk() {
2271        let plans = [
2272            mixed_plan(1, 2),
2273            mixed_plan(0, 0),
2274            RingSwitchPlan::new(
2275                &keccak_physical_layout(),
2276                Some(&keccak_expander().expansion_entries()),
2277                1,
2278                1,
2279            )
2280            .unwrap(),
2281            plan_of(7),
2282        ];
2283
2284        let eta = Block128(0x2545F4914F6CDD1D_517CC1B727220A95).to_hardware();
2285        let zero = Flat::from_raw(Block128::ZERO);
2286
2287        for plan in &plans {
2288            let at = plan.at_split(6, 4);
2289
2290            assert_eq!(at.plan.units, plan.units);
2291            assert_eq!(at.slot_rs, plan.phys_rs);
2292            assert_eq!(at.h_slots, plan.h_cols);
2293            assert_eq!(at.num_sigma, plan.num_units);
2294            assert_eq!(
2295                at.leaf_row_bytes() + at.h_leaf_row_bytes(),
2296                plan.opened_row_bytes()
2297            );
2298
2299            let mut pows = vec![Flat::from_raw(Block128::ONE)];
2300            for _ in 0..plan.num_units {
2301                pows.push(pows[pows.len() - 1] * eta);
2302            }
2303
2304            let (weights, shift) = at.unit_weights(eta, &[]).unwrap();
2305
2306            assert_eq!(weights, pows[..plan.num_units]);
2307            assert_eq!(shift, pows[plan.num_units]);
2308
2309            let (coeff_bit, coeff_whole, eta_shift) = at.slot_coeffs(eta);
2310            let num_phys = plan.phys_rs.len();
2311
2312            for p in 0..num_phys {
2313                let bit = plan.phys_bit[p].iter().fold(zero, |acc, &u| acc + pows[u]);
2314                let whole = plan.phys_whole[p]
2315                    .iter()
2316                    .fold(zero, |acc, &u| acc + pows[u]);
2317
2318                assert_eq!(coeff_bit[p], bit);
2319                assert_eq!(coeff_whole[p], whole);
2320            }
2321
2322            for (k, &u) in plan.h_whole.iter().enumerate() {
2323                assert_eq!(coeff_whole[num_phys + k], pows[u]);
2324            }
2325
2326            assert_eq!(eta_shift, shift);
2327        }
2328    }
2329
2330    #[test]
2331    fn packed_layout_groups_columns_by_class() {
2332        let plan = mixed_plan(1, 3);
2333
2334        for pack_vars in 1..=3 {
2335            let at = plan.at_split(5, 5 + pack_vars);
2336            let pack = 1 << pack_vars;
2337            let num_cols = at.plan.phys_rs.len();
2338
2339            assert_eq!(at.plan.whole_blinds(), pack);
2340            assert_eq!(at.h_slots, 3usize.div_ceil(pack));
2341
2342            let mut seen = vec![false; num_cols];
2343            let mut first = 0;
2344
2345            for s in 0..at.slot_rs.len() {
2346                let members = at.slot_columns(s);
2347
2348                assert!(!members.is_empty() && members.len() <= pack);
2349                assert!(members[0] >= first, "slots out of first-column order");
2350
2351                first = members[0];
2352
2353                for &p in members {
2354                    assert!(!seen[p]);
2355                    assert_eq!(at.plan.slot_class(p), at.plan.slot_class(members[0]));
2356                    assert_eq!(at.slot_rs[s], at.plan.phys_rs[p]);
2357
2358                    seen[p] = true;
2359                }
2360            }
2361
2362            assert!(seen.iter().all(|&s| s));
2363
2364            let data_cols = num_cols - at.plan.blind_cols;
2365            let blind_slots: Vec<usize> = (0..at.slot_rs.len())
2366                .filter(|&s| at.slot_columns(s)[0] >= data_cols)
2367                .collect();
2368
2369            let full = blind_slots
2370                .iter()
2371                .filter(|&&s| at.slot_columns(s).len() == pack)
2372                .count();
2373
2374            assert_eq!(
2375                blind_slots.len(),
2376                2,
2377                "one whole blind slot, one ring blind slot"
2378            );
2379            assert_eq!(full, 1);
2380            assert_eq!(at.blind_salt(0), at.slot_rs.len());
2381        }
2382    }
2383
2384    #[test]
2385    fn census_prices_built_layout() {
2386        let plans = [mixed_plan(1, 3), mixed_plan(0, 1), plan_of(13), plan_of(64)];
2387
2388        for plan in &plans {
2389            let census = plan.census();
2390
2391            for pack_vars in 0..=census.max_pack_vars() {
2392                let at = plan.at_split(9, 9 + pack_vars);
2393                let pack = 1 << pack_vars;
2394
2395                assert_eq!(
2396                    census.row_bytes(pack),
2397                    at.leaf_row_bytes() + at.h_leaf_row_bytes()
2398                );
2399                assert_eq!(census.num_sigma(pack), at.num_sigma);
2400            }
2401        }
2402    }
2403
2404    #[test]
2405    fn packed_unit_weights_factor_through_slot_coeffs() {
2406        let plan = mixed_plan(1, 3);
2407        let mut next = xorshift(0x85_5107);
2408
2409        for pack_vars in 0..=3 {
2410            let at = plan.at_split(4, 4 + pack_vars);
2411
2412            let eta = next().to_hardware();
2413            let rho: Vec<Flat<Block128>> = (0..pack_vars).map(|_| next().to_hardware()).collect();
2414
2415            let (weights, shift) = at.unit_weights(eta, &rho).unwrap();
2416            let (coeff_bit, coeff_whole, eta_shift) = at.slot_coeffs(eta);
2417
2418            assert_eq!(shift, eta_shift);
2419
2420            let eq = TensorProduct::new(rho.clone());
2421            let zero = Flat::from_raw(Block128::ZERO);
2422            let num_slots = at.slot_rs.len();
2423
2424            for s in 0..num_slots {
2425                for (block, &p) in at.slot_columns(s).iter().enumerate() {
2426                    let eq_b = eq.evaluate_at_index(block);
2427                    let bit = at.plan.phys_bit[p]
2428                        .iter()
2429                        .fold(zero, |acc, &u| acc + weights[u]);
2430                    let whole = at.plan.phys_whole[p]
2431                        .iter()
2432                        .fold(zero, |acc, &u| acc + weights[u]);
2433
2434                    assert_eq!(bit, coeff_bit[s] * eq_b, "slot {s} block {block}");
2435                    assert_eq!(whole, coeff_whole[s] * eq_b, "slot {s} block {block}");
2436                }
2437            }
2438
2439            for (k, &u) in at.plan.h_whole.iter().enumerate() {
2440                let slot = num_slots + k / at.pack();
2441                let eq_b = eq.evaluate_at_index(k % at.pack());
2442
2443                assert_eq!(weights[u], coeff_whole[slot] * eq_b);
2444            }
2445        }
2446    }
2447
2448    #[test]
2449    fn same_role_units_keep_distinct_exponents() {
2450        let plan = mixed_plan(1, 0);
2451        let eta = Block128(0x9E37_79B9_7F4A_7C15).to_hardware();
2452        let zero = Flat::from_raw(Block128::ZERO);
2453
2454        for pack_vars in 0..=2 {
2455            let at = plan.at_split(4, 4 + pack_vars);
2456            let doubled = &at.plan.phys_whole[8];
2457
2458            assert_eq!(doubled.len(), 2);
2459            assert_ne!(at.unit_sigma[doubled[0]], at.unit_sigma[doubled[1]]);
2460
2461            let (_, coeff_whole, _) = at.slot_coeffs(eta);
2462            let slot = (0..at.slot_rs.len())
2463                .find(|&s| at.slot_columns(s).contains(&8))
2464                .unwrap();
2465
2466            assert_ne!(coeff_whole[slot], zero);
2467        }
2468    }
2469
2470    #[test]
2471    fn at_split_packs_past_widest_class() {
2472        let plan = mixed_plan(1, 3);
2473        let widest = plan.census().max_pack_vars();
2474        let classes = plan.census().data.len();
2475
2476        let at = plan.at_split(6, 7 + widest);
2477
2478        assert_eq!(at.slot_rs.len(), classes + 2);
2479        assert_eq!(at.h_slots, 1);
2480        assert_eq!(at.plan.whole_blinds(), 2 << widest);
2481
2482        let single = plan_of(1).at_split(9, 10);
2483
2484        assert_eq!(single.slot_rs.len(), 2);
2485        assert_eq!(single.h_slots, 1);
2486    }
2487
2488    #[test]
2489    fn pool_of_one_table_keeps_its_weights() {
2490        let plan = mixed_plan(1, 3);
2491        let eta = xorshift(0x85_9001)().to_hardware();
2492
2493        for (num_vars, split) in [(6, 4), (6, 6), (4, 6)] {
2494            let alone = plan.at_split(num_vars, split);
2495            let pool = PoolLayout::at_split(&[(&plan, num_vars)], split);
2496
2497            assert!(pool.tables[0].sigma_weights(eta) == alone.sigma_weights(eta));
2498            assert!(pool.tables[0].slot_coeffs(eta) == alone.slot_coeffs(eta));
2499        }
2500    }
2501
2502    #[test]
2503    fn pool_exponents_run_across_tables() {
2504        let (wide, narrow) = (mixed_plan(1, 3), plan_of(3));
2505        let eta = xorshift(0x85_9002)().to_hardware();
2506
2507        let pool = PoolLayout::at_split(&[(&wide, 6), (&narrow, 4)], 5);
2508        let shift = eta_pow(eta, pool.num_sigma());
2509
2510        let cases = [
2511            (&pool.tables[0], wide.at_split(6, 5), 0),
2512            (
2513                &pool.tables[1],
2514                narrow.at_split(4, 5),
2515                pool.tables[0].num_sigma,
2516            ),
2517        ];
2518
2519        for (pooled, alone, base) in cases {
2520            let scale = eta_pow(eta, base);
2521
2522            let (weights, weight_shift) = pooled.sigma_weights(eta);
2523            let (alone_weights, _) = alone.sigma_weights(eta);
2524
2525            assert!(weight_shift == shift);
2526            assert!(
2527                weights
2528                    .iter()
2529                    .zip(&alone_weights)
2530                    .all(|(&w, &a)| w == scale * a)
2531            );
2532
2533            let (bit, whole, coeff_shift) = pooled.slot_coeffs(eta);
2534            let (alone_bit, alone_whole, _) = alone.slot_coeffs(eta);
2535
2536            assert!(coeff_shift == shift);
2537            assert!(bit.iter().zip(&alone_bit).all(|(&p, &a)| p == scale * a));
2538            assert!(
2539                whole
2540                    .iter()
2541                    .zip(&alone_whole)
2542                    .all(|(&p, &a)| p == scale * a)
2543            );
2544        }
2545    }
2546
2547    #[test]
2548    fn pool_shape_charges_line_walk() {
2549        let (wide, narrow) = (mixed_plan(1, 3), plan_of(3));
2550        let pool = PoolLayout::at_split(&[(&wide, 6), (&narrow, 4)], 5);
2551
2552        assert_eq!(pool.num_masters(), 3);
2553        assert_eq!(pool.num_vars(), 6);
2554        assert_eq!(pool.rho_vars(), 1);
2555        assert_eq!(pool.fold_shape().grid_rows, 2);
2556        assert_eq!(pool.fold_shape().units, pool.num_sigma() + 2);
2557    }
2558
2559    #[test]
2560    fn pool_packs_short_tables_over_full_width() {
2561        let config = Config::prod();
2562        let (tall, short) = (mixed_plan(1, 3), mixed_plan(1, 3));
2563
2564        let pool = PoolLayout::new(&[(&tall, 16), (&short, 8)], 128, &config);
2565        let split = pool.split_vars;
2566
2567        assert!(split > 8 && split <= 16);
2568        assert_eq!(pool.tables[0].grid_rows(), 1 << (16 - split));
2569        assert_eq!(pool.tables[1].pack(), 1 << (split - 8));
2570        assert_eq!(pool.tables[1].grid_rows(), 1);
2571        assert!(config.check_security(128, pool.fold_shape()).is_ok());
2572    }
2573
2574    #[test]
2575    fn census_prices_blind_claims_of_built_layout() {
2576        let plans = [
2577            mixed_plan(1, 3),
2578            mixed_plan(2, 1),
2579            mixed_plan(0, 1),
2580            plan_of(13),
2581        ];
2582
2583        for plan in &plans {
2584            let census = plan.census();
2585
2586            for pack_vars in 0..=4 {
2587                let at = plan.at_split(9, 9 + pack_vars);
2588                let added = at.plan.total_claims() - plan.total_claims();
2589
2590                assert_eq!(
2591                    census.blind_claim_bytes(1 << pack_vars),
2592                    2 * added * ColumnType::B128.byte_size()
2593                );
2594            }
2595        }
2596    }
2597
2598    #[test]
2599    fn total_claims_at_matches_packed_plan() {
2600        let plans = [
2601            mixed_plan(1, 3),
2602            mixed_plan(2, 1),
2603            mixed_plan(0, 1),
2604            plan_of(13),
2605        ];
2606
2607        for plan in &plans {
2608            for split_vars in 7..=13 {
2609                assert_eq!(
2610                    plan.total_claims_at(9, split_vars),
2611                    plan.at_split(9, split_vars).plan.total_claims(),
2612                    "split {split_vars}"
2613                );
2614            }
2615        }
2616    }
2617
2618    #[test]
2619    fn split_ignores_unabsorbed_min_security_bits() {
2620        let prod = Config::prod();
2621
2622        let lenient = Config {
2623            min_security_bits: 0,
2624            ..Config::prod()
2625        };
2626
2627        let strict = Config {
2628            min_security_bits: 128,
2629            ..Config::prod()
2630        };
2631
2632        let mut bound = 0;
2633        for cols in [1, 4, 16, 64, 128] {
2634            let plan = plan_of(cols);
2635
2636            for num_vars in 10..=30 {
2637                let tables = [(&plan, num_vars)];
2638                let chosen = PoolLayout::new(&tables, 128, &prod).split_vars;
2639
2640                assert_eq!(PoolLayout::new(&tables, 128, &lenient).split_vars, chosen);
2641                assert_eq!(PoolLayout::new(&tables, 128, &strict).split_vars, chosen);
2642
2643                if Some(chosen) != cheapest_split(&tables, &prod, |_| true) {
2644                    bound += 1;
2645                }
2646            }
2647        }
2648
2649        assert!(bound > 0, "no split was moved by the security filter");
2650    }
2651
2652    #[test]
2653    fn unit_weights_scale_with_units_not_pack() {
2654        let base = Config {
2655            zero_knowledge: false,
2656            ..Config::prod()
2657        };
2658
2659        let at = narrow_plan(3, &base).at_split(0, 40);
2660        let rho: Vec<Flat<Block128>> = (0..40u128).map(|i| Block128(i + 3).to_hardware()).collect();
2661
2662        let (weights, _) = at.unit_weights(Block128(5).to_hardware(), &rho).unwrap();
2663
2664        assert_eq!(weights.len(), at.plan.num_units);
2665    }
2666}