1use 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
23const RING_TABLE_MIN_CLAIMS: usize = 300;
26
27#[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#[derive(Clone, Copy, Debug)]
55enum EntryKind {
56 ExpandBits { count: usize, storage: ColumnType },
59
60 PassThrough { count: usize, storage: ColumnType },
63
64 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#[derive(Clone, Copy, Debug)]
89struct CompiledEntry {
90 phy_col_start: usize,
93
94 byte_offset: usize,
96 kind: EntryKind,
97
98 reuse: bool,
101}
102
103#[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 pub fn build(self) -> Result<Self, Error> {
135 match self.error {
136 Some(e) => Err(e),
137 None => Ok(self),
138 }
139 }
140
141 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 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 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 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 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 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 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 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 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 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#[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 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 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
935pub 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 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 pub fn blind_salt(&self, blind: usize) -> usize {
998 self.slot_rs.len() + blind
999 }
1000
1001 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 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 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 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
1289fn 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
1433pub 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
1448pub 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
1487pub 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
1528fn 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
1579fn 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 #[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}