1use alloc::collections::BTreeMap;
6use alloc::string::String;
7use alloc::vec::Vec;
8use hekate_core::errors;
9use hekate_math::{Flat, HardwareField, TowerField};
10
11use crate::FixedColumn;
12
13mod rank;
14
15pub use rank::{
16 OMEGA, OMEGA_ORDER, RankClock, RankTable, TableHeight, rank_clocks, validate_ordered_buses,
17 validate_ordered_emits,
18};
19
20pub type ChallengeLabel = &'static [u8];
23
24pub const EMIT_RANK_LABEL: ChallengeLabel = b"kappa_emit_rank";
26
27pub const MAX_ROW_INDEX_BYTES: usize = 8;
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub enum BusKind {
38 #[default]
39 Permutation,
40 Lookup,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
46pub enum Side {
47 Request,
48 Response,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
54pub enum Source {
55 Column(usize),
57
58 Columns(Vec<usize>),
61
62 RowIndexLeBytes(usize),
66
67 Const(u128),
69
70 RowIndexByte(usize),
72
73 PhaseColumn(usize),
77
78 EmitRank(Side),
81}
82
83#[derive(Clone, Debug)]
96pub struct PermutationCheckSpec {
97 pub kind: BusKind,
98
99 pub sources: Vec<(Source, ChallengeLabel)>,
102
103 pub selector: Option<usize>,
106
107 pub recv_selector: Option<usize>,
110
111 pub clock_waiver: Option<String>,
114}
115
116impl PermutationCheckSpec {
117 pub fn new(sources: Vec<(Source, ChallengeLabel)>, selector: Option<usize>) -> Self {
118 Self {
119 sources,
120 selector,
121 recv_selector: None,
122 kind: BusKind::Permutation,
123 clock_waiver: None,
124 }
125 }
126
127 pub fn new_lookup(sources: Vec<(Source, ChallengeLabel)>, selector: Option<usize>) -> Self {
131 Self {
132 sources,
133 selector,
134 recv_selector: None,
135 kind: BusKind::Lookup,
136 clock_waiver: None,
137 }
138 }
139
140 pub fn new_paired(
144 sources: Vec<(Source, ChallengeLabel)>,
145 s_send: usize,
146 s_recv: usize,
147 kind: BusKind,
148 ) -> Self {
149 Self {
150 sources,
151 selector: Some(s_send),
152 recv_selector: Some(s_recv),
153 kind,
154 clock_waiver: None,
155 }
156 }
157
158 pub fn with_clock_waiver(mut self, reason: impl Into<String>) -> Self {
162 self.clock_waiver = Some(reason.into());
163 self
164 }
165
166 pub fn num_sources(&self) -> usize {
167 self.sources.len()
168 }
169
170 pub fn has_selector(&self) -> bool {
171 self.selector.is_some()
172 }
173
174 pub fn has_paired(&self) -> bool {
175 self.recv_selector.is_some()
176 }
177
178 pub fn shift_column_indices(&mut self, offset: usize) {
181 for (source, _) in &mut self.sources {
182 match source {
183 Source::Column(idx) | Source::PhaseColumn(idx) => *idx += offset,
184 Source::Columns(indices) => {
185 for idx in indices {
186 *idx += offset;
187 }
188 }
189 _ => {}
190 }
191 }
192
193 if let Some(sel_idx) = &mut self.selector {
194 *sel_idx += offset;
195 }
196
197 if let Some(sel_idx) = &mut self.recv_selector {
198 *sel_idx += offset;
199 }
200 }
201
202 pub fn has_real_clock_source(&self) -> bool {
205 self.sources.iter().any(|(src, _)| {
206 matches!(
207 src,
208 Source::RowIndexLeBytes(_) | Source::RowIndexByte(_) | Source::EmitRank(_)
209 )
210 })
211 }
212
213 pub fn validate_clock_stitching(&self, _bus_id: &str) -> errors::Result<()> {
216 for (src, _) in &self.sources {
217 let width_ok = match src {
218 Source::RowIndexLeBytes(n) => *n >= 1 && *n <= MAX_ROW_INDEX_BYTES,
219 Source::RowIndexByte(k) => *k < MAX_ROW_INDEX_BYTES,
220 Source::Column(_)
221 | Source::Columns(_)
222 | Source::Const(_)
223 | Source::PhaseColumn(_)
224 | Source::EmitRank(_) => true,
225 };
226
227 if !width_ok {
228 return Err(errors::Error::Protocol {
229 protocol: "logup_bus",
230 message: "row-index clock wider than 8 bytes; the row-index \
231 MLE folds at most 8 and truncates the rest silently",
232 });
233 }
234 }
235
236 let waiver_status = self.clock_waiver.as_deref().map(WaiverStatus::classify);
237
238 match (self.kind, self.has_real_clock_source(), waiver_status) {
239 (BusKind::Lookup, _, Some(_)) => Err(errors::Error::Protocol {
240 protocol: "logup_bus",
241 message: "lookup bus carries a clock_waiver; waivers only apply \
242 to Permutation kind, drop the .with_clock_waiver(...) call",
243 }),
244 (BusKind::Permutation, true, Some(_)) => Err(errors::Error::Protocol {
245 protocol: "logup_bus",
246 message: "permutation bus carries both a clock source and a \
247 clock_waiver; pick one shape",
248 }),
249 (BusKind::Permutation, false, Some(WaiverStatus::Empty)) => {
250 Err(errors::Error::Protocol {
251 protocol: "logup_bus",
252 message: "permutation bus has an empty clock_waiver; provide a \
253 non-empty reason citing the load-bearing AIR constraint",
254 })
255 }
256 (BusKind::Permutation, false, Some(WaiverStatus::TooShort)) => {
257 Err(errors::Error::Protocol {
258 protocol: "logup_bus",
259 message: "permutation bus has an under-specified clock_waiver; \
260 the reason must be at least 32 chars and cite a file/line \
261 of the load-bearing AIR constraint",
262 })
263 }
264 (BusKind::Permutation, false, Some(WaiverStatus::MissingCitation)) => {
265 Err(errors::Error::Protocol {
266 protocol: "logup_bus",
267 message: "permutation bus clock_waiver lacks a 'see <path>' citation; \
268 waiver text must start with 'see ' followed by the file path \
269 of the load-bearing AIR constraint",
270 })
271 }
272 (BusKind::Permutation, false, None) => Err(errors::Error::Protocol {
273 protocol: "logup_bus",
274 message: "permutation bus lacks per-row clock stitching; give the \
275 Service an EmitRank slot, give a raw spec a RowIndexLeBytes \
276 or RowIndexByte source, or add a clock_waiver citing the AIR \
277 constraint that forces per-row uniqueness",
278 }),
279 _ => Ok(()),
280 }
281 }
282}
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq)]
286pub enum ServiceSlot {
287 Value(ChallengeLabel),
289
290 Const(ChallengeLabel, u128),
293
294 EmitRank,
297}
298
299#[derive(Clone, Debug, PartialEq, Eq)]
302pub struct Service {
303 pub bus_id: &'static str,
304 pub kind: BusKind,
305 pub slots: Vec<ServiceSlot>,
306}
307
308impl Service {
309 pub fn num_values(&self) -> usize {
310 self.slots
311 .iter()
312 .filter(|s| matches!(s, ServiceSlot::Value(_)))
313 .count()
314 }
315
316 pub fn request(
319 &self,
320 values: &[usize],
321 selector: usize,
322 ) -> errors::Result<PermutationCheckSpec> {
323 self.spec(values, Side::Request, selector)
324 }
325
326 pub fn respond(
329 &self,
330 values: &[usize],
331 selector: usize,
332 ) -> errors::Result<PermutationCheckSpec> {
333 self.spec(values, Side::Response, selector)
334 }
335
336 fn spec(
337 &self,
338 values: &[usize],
339 side: Side,
340 selector: usize,
341 ) -> errors::Result<PermutationCheckSpec> {
342 let rank_slots = self
343 .slots
344 .iter()
345 .filter(|s| matches!(s, ServiceSlot::EmitRank))
346 .count();
347
348 let clock_ok = matches!(
349 (self.kind, rank_slots),
350 (BusKind::Permutation, 1) | (BusKind::Lookup, 0)
351 );
352
353 if !clock_ok {
354 return Err(errors::Error::Protocol {
355 protocol: "service",
356 message: "Permutation schema needs exactly one EmitRank slot; \
357 Lookup schema must have none",
358 });
359 }
360
361 if values.len() != self.num_values() {
362 return Err(errors::Error::Protocol {
363 protocol: "service",
364 message: "value column count does not match the schema's Value slots",
365 });
366 }
367
368 let mut sources = Vec::with_capacity(self.slots.len());
369 let mut next_value = values.iter();
370
371 for slot in &self.slots {
372 match slot {
373 ServiceSlot::Value(label) => {
374 let col = next_value.next().ok_or(errors::Error::Protocol {
375 protocol: "service",
376 message: "value column count does not match the schema's Value slots",
377 })?;
378
379 sources.push((Source::Column(*col), *label));
380 }
381 ServiceSlot::Const(label, v) => {
382 sources.push((Source::Const(*v), *label));
383 }
384 ServiceSlot::EmitRank => {
385 sources.push((Source::EmitRank(side), EMIT_RANK_LABEL));
386 }
387 }
388 }
389
390 Ok(match self.kind {
391 BusKind::Permutation => PermutationCheckSpec::new(sources, Some(selector)),
392 BusKind::Lookup => PermutationCheckSpec::new_lookup(sources, Some(selector)),
393 })
394 }
395}
396
397#[derive(Clone, Copy, Debug, PartialEq, Eq)]
398enum WaiverStatus {
399 Empty,
400 TooShort,
401 MissingCitation,
402 Ok,
403}
404
405impl WaiverStatus {
406 const MIN_WAIVER_LEN: usize = 32;
407 const REQUIRED_PREFIX: &'static str = "see ";
408
409 fn classify(s: &str) -> Self {
410 if s.is_empty() {
411 Self::Empty
412 } else if s.len() < Self::MIN_WAIVER_LEN {
413 Self::TooShort
414 } else if !s.starts_with(Self::REQUIRED_PREFIX) {
415 Self::MissingCitation
416 } else {
417 Self::Ok
418 }
419 }
420}
421
422pub fn validate_fixed_selectors<F>(
426 specs: &[(String, PermutationCheckSpec)],
427 fixed: &[FixedColumn<F>],
428) -> errors::Result<()> {
429 for (_, spec) in specs {
430 if spec.selector.is_none() && spec.recv_selector.is_some() {
431 return Err(errors::Error::Protocol {
432 protocol: "logup_bus",
433 message: "paired bus has recv_selector without send selector",
434 });
435 }
436
437 let phases = spec.sources.iter().filter_map(|(src, _)| match src {
438 Source::PhaseColumn(col) => Some(*col),
439 _ => None,
440 });
441
442 for sel in [spec.selector, spec.recv_selector]
443 .into_iter()
444 .flatten()
445 .chain(phases)
446 {
447 let Some(fc) = fixed.iter().find(|fc| fc.col_idx == sel) else {
448 return Err(errors::Error::Protocol {
449 protocol: "logup_bus",
450 message: "bus selector or clock phase reads a witness column; \
451 every selector, recv_selector and PhaseColumn must be \
452 None or a member of the table's fixed_columns",
453 });
454 };
455
456 if !fc.shape.is_overlay() {
457 return Err(errors::Error::Protocol {
458 protocol: "logup_bus",
459 message: "bus selector or clock phase is pinned to a substituted \
460 shape; FirstRow/LastRow/Custom replace the committed \
461 column with a virtual poly the bus cannot read; pin \
462 with Cadence, Segments, Periodic, Sparse or Dense",
463 });
464 }
465 }
466 }
467
468 Ok(())
469}
470
471pub fn validate_bus_set<'a, I>(endpoints: I) -> errors::Result<()>
474where
475 I: IntoIterator<Item = (&'a str, &'a PermutationCheckSpec)>,
476{
477 let mut by_bus: BTreeMap<&'a str, Vec<&'a PermutationCheckSpec>> = BTreeMap::new();
478
479 for (bus_id, spec) in endpoints {
480 if !bus_id.bytes().all(|b| b.is_ascii_graphic()) {
481 return Err(errors::Error::Protocol {
482 protocol: "logup_bus",
483 message: "bus_id has a non-graphic byte; bus ids must be \
484 graphic ASCII (0x21..=0x7E). A space, zero-width, \
485 or homoglyph byte forges a visually identical \
486 second bus that balances on its own",
487 });
488 }
489
490 by_bus.entry(bus_id).or_default().push(spec);
491 }
492
493 for specs in by_bus.values() {
494 let any_lookup = specs.iter().any(|s| s.kind == BusKind::Lookup);
495 let any_perm = specs.iter().any(|s| s.kind == BusKind::Permutation);
496
497 if any_lookup && any_perm {
498 return Err(errors::Error::Protocol {
499 protocol: "logup_bus",
500 message: "bus_id has mixed BusKind across endpoints; \
501 all endpoints must agree on Permutation or Lookup",
502 });
503 }
504 }
505
506 Ok(())
507}
508
509pub fn accumulate_lookup_heights(
514 specs: &[(String, PermutationCheckSpec)],
515 table_rows: u64,
516 heights: &mut BTreeMap<String, u64>,
517) {
518 for (bus_id, spec) in specs {
519 if spec.kind == BusKind::Lookup {
520 let entry = heights.entry(bus_id.clone()).or_insert(0);
521 *entry = (*entry).max(table_rows);
522 }
523 }
524}
525
526pub fn eval_row_idx_le_mle<F>(num_bytes: usize, r_final: &[Flat<F>]) -> Flat<F>
529where
530 F: TowerField + HardwareField + From<u128>,
531{
532 let total_bits = (num_bytes.min(8) * 8).min(r_final.len());
533
534 let mut acc = Flat::from_raw(F::ZERO);
535 for (i, r) in r_final.iter().enumerate().take(total_bits) {
536 acc += F::from(1u128 << i).to_hardware() * *r;
537 }
538
539 acc
540}
541
542pub fn eval_row_idx_byte_mle<F>(byte_idx: usize, r_final: &[Flat<F>]) -> Flat<F>
545where
546 F: TowerField + HardwareField + From<u128>,
547{
548 let bit_start = byte_idx.saturating_mul(8);
549 if bit_start >= r_final.len() {
550 return Flat::from_raw(F::ZERO);
551 }
552
553 let end = (bit_start + 8).min(r_final.len());
554
555 let mut acc = Flat::from_raw(F::ZERO);
556 for (j, i) in (bit_start..end).enumerate() {
557 acc += F::from(1u128 << j).to_hardware() * r_final[i];
558 }
559
560 acc
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use crate::FixedShape;
567 use hekate_math::Block128;
568
569 fn test_service() -> Service {
570 Service {
571 bus_id: "svc",
572 kind: BusKind::Permutation,
573 slots: vec![
574 ServiceSlot::Value(b"k_a"),
575 ServiceSlot::Const(b"k_dom", 7),
576 ServiceSlot::EmitRank,
577 ServiceSlot::Value(b"k_dir"),
578 ],
579 }
580 }
581
582 #[test]
583 fn permutation_spec_creation() {
584 let sources = vec![
585 (Source::Column(0), b"kappa_0" as ChallengeLabel),
586 (Source::Column(1), b"kappa_1" as ChallengeLabel),
587 (Source::RowIndexLeBytes(4), b"kappa_clk" as ChallengeLabel),
588 ];
589
590 let spec = PermutationCheckSpec::new(sources, Some(2));
591
592 assert_eq!(spec.num_sources(), 3);
593 assert!(spec.has_selector());
594 assert_eq!(spec.selector, Some(2));
595 }
596
597 #[test]
598 fn source_variants() {
599 let col = Source::Column(5);
600 let cols = Source::Columns(vec![0, 1, 2, 3]);
601 let clock = Source::RowIndexLeBytes(4);
602 let constant = Source::Const(0x01);
603
604 assert_eq!(col, Source::Column(5));
605 assert_eq!(cols, Source::Columns(vec![0, 1, 2, 3]));
606 assert_eq!(clock, Source::RowIndexLeBytes(4));
607 assert_eq!(constant, Source::Const(0x01));
608 }
609
610 #[test]
611 fn new_paired_populates_both_selectors() {
612 let spec = PermutationCheckSpec::new_paired(
613 vec![(Source::Column(0), b"k_a" as ChallengeLabel)],
614 3,
615 5,
616 BusKind::Permutation,
617 );
618
619 assert!(spec.has_selector());
620 assert!(spec.has_paired());
621 assert_eq!(spec.selector, Some(3));
622 assert_eq!(spec.recv_selector, Some(5));
623 assert_eq!(spec.kind, BusKind::Permutation);
624 }
625
626 #[test]
627 fn new_defaults_recv_selector_none() {
628 let spec =
629 PermutationCheckSpec::new(vec![(Source::Column(0), b"k_a" as ChallengeLabel)], Some(1));
630
631 assert!(!spec.has_paired());
632 assert_eq!(spec.recv_selector, None);
633 }
634
635 #[test]
636 fn new_lookup_defaults_recv_selector_none() {
637 let spec = PermutationCheckSpec::new_lookup(
638 vec![(Source::Column(0), b"k_a" as ChallengeLabel)],
639 Some(1),
640 );
641
642 assert!(!spec.has_paired());
643 assert_eq!(spec.recv_selector, None);
644 }
645
646 #[test]
647 fn shift_column_indices_covers_recv_selector() {
648 let mut spec = PermutationCheckSpec::new_paired(
649 vec![
650 (Source::Column(0), b"k_a" as ChallengeLabel),
651 (Source::Columns(vec![1, 2]), b"k_b" as ChallengeLabel),
652 ],
653 3,
654 5,
655 BusKind::Lookup,
656 );
657
658 spec.shift_column_indices(10);
659
660 assert_eq!(spec.selector, Some(13));
661 assert_eq!(spec.recv_selector, Some(15));
662
663 match &spec.sources[0].0 {
664 Source::Column(idx) => assert_eq!(*idx, 10),
665 other => panic!("expected Column, got {other:?}"),
666 }
667
668 match &spec.sources[1].0 {
669 Source::Columns(idxs) => assert_eq!(idxs, &vec![11, 12]),
670 other => panic!("expected Columns, got {other:?}"),
671 }
672 }
673
674 #[test]
675 fn validate_bus_set_rejects_homoglyph_split_bus() {
676 let clockless =
677 PermutationCheckSpec::new(vec![(Source::Column(0), b"k" as ChallengeLabel)], Some(1));
678 let other = clockless.clone();
679
680 assert!(
683 validate_bus_set(vec![("ram_link", &clockless), ("ram_link\u{200b}", &other)]).is_err()
684 );
685 }
686
687 #[test]
688 fn validate_bus_set_accepts_ascii_bus_id() {
689 let spec =
690 PermutationCheckSpec::new(vec![(Source::Column(0), b"k" as ChallengeLabel)], Some(1));
691
692 assert!(validate_bus_set(vec![("ram_link", &spec)]).is_ok());
693 }
694
695 #[test]
696 fn service_endpoints_share_schema() {
697 let svc = test_service();
698
699 let req = svc.request(&[10, 11], 12).unwrap();
700 let resp = svc.respond(&[20, 21], 23).unwrap();
701
702 let req_labels: Vec<_> = req.sources.iter().map(|(_, l)| *l).collect();
703 let resp_labels: Vec<_> = resp.sources.iter().map(|(_, l)| *l).collect();
704
705 assert_eq!(req_labels, resp_labels);
706 assert_eq!(req_labels[2], EMIT_RANK_LABEL);
707
708 assert_eq!(req.sources[0].0, Source::Column(10));
709 assert_eq!(req.sources[1].0, Source::Const(7));
710 assert_eq!(req.sources[2].0, Source::EmitRank(Side::Request));
711 assert_eq!(req.sources[3].0, Source::Column(11));
712 assert_eq!(req.selector, Some(12));
713
714 assert_eq!(resp.sources[0].0, Source::Column(20));
715 assert_eq!(resp.sources[1].0, Source::Const(7));
716 assert_eq!(resp.sources[2].0, Source::EmitRank(Side::Response));
717 assert_eq!(resp.sources[3].0, Source::Column(21));
718 assert_eq!(resp.selector, Some(23));
719
720 req.validate_clock_stitching("svc").unwrap();
721 resp.validate_clock_stitching("svc").unwrap();
722
723 validate_bus_set(vec![("svc", &req), ("svc", &resp)]).unwrap();
724 }
725
726 #[test]
727 fn service_rejects_schema_mismatches() {
728 let svc = test_service();
729
730 assert!(svc.request(&[10], 12).is_err());
731 assert!(svc.respond(&[20, 21, 22], 23).is_err());
732
733 let lookup = Service {
734 bus_id: "rom",
735 kind: BusKind::Lookup,
736 slots: vec![ServiceSlot::Value(b"k_a"), ServiceSlot::EmitRank],
737 };
738
739 assert!(lookup.respond(&[3], 4).is_err());
740
741 let clockless = Service {
742 bus_id: "bad",
743 kind: BusKind::Permutation,
744 slots: vec![ServiceSlot::Value(b"k_a")],
745 };
746
747 assert!(clockless.request(&[1], 2).is_err());
748
749 let two_ranks = Service {
750 bus_id: "bad",
751 kind: BusKind::Permutation,
752 slots: vec![
753 ServiceSlot::EmitRank,
754 ServiceSlot::Value(b"k_a"),
755 ServiceSlot::EmitRank,
756 ],
757 };
758
759 assert!(two_ranks.request(&[1], 2).is_err());
760 }
761
762 #[test]
763 fn lookup_service_derives_lookup_specs() {
764 let svc = Service {
765 bus_id: "rom",
766 kind: BusKind::Lookup,
767 slots: vec![ServiceSlot::Value(b"k_a")],
768 };
769
770 let req = svc.request(&[1], 2).unwrap();
771 let resp = svc.respond(&[3], 4).unwrap();
772
773 assert_eq!(req.kind, BusKind::Lookup);
774 assert_eq!(resp.kind, BusKind::Lookup);
775 assert_eq!(req.sources.len(), 1);
776 }
777
778 #[test]
779 fn column_clock_needs_waiver_whatever_its_label() {
780 let spoofed = PermutationCheckSpec::new(
781 vec![
782 (Source::Column(0), b"k_a" as ChallengeLabel),
783 (Source::Column(1), EMIT_RANK_LABEL),
784 ],
785 Some(2),
786 );
787
788 assert!(spoofed.validate_clock_stitching("svc").is_err());
789
790 spoofed
791 .with_clock_waiver(
792 "see permutation/mod.rs: a body constraint forces per-row uniqueness",
793 )
794 .validate_clock_stitching("svc")
795 .unwrap();
796 }
797
798 #[test]
799 fn witness_clock_phase_is_rejected() {
800 let phased = PermutationCheckSpec::new(
801 vec![
802 (Source::Column(10), b"k_a" as ChallengeLabel),
803 (Source::RowIndexByte(0), b"k_clk_b0" as ChallengeLabel),
804 (Source::PhaseColumn(60), b"k_clk_b1" as ChallengeLabel),
805 ],
806 Some(12),
807 );
808 let specs = vec![(String::from("svc"), phased)];
809
810 let selector_only = vec![FixedColumn::<Block128> {
811 col_idx: 12,
812 shape: FixedShape::Periodic {
813 period: 1,
814 values: vec![Block128::ONE],
815 },
816 }];
817
818 assert!(validate_fixed_selectors(&specs, &selector_only).is_err());
819
820 let mut pinned = selector_only;
821 pinned.push(FixedColumn {
822 col_idx: 60,
823 shape: FixedShape::Periodic {
824 period: 2,
825 values: vec![Block128::ZERO, Block128::ONE],
826 },
827 });
828
829 validate_fixed_selectors(&specs, &pinned).unwrap();
830
831 let mut substituted = pinned;
832 substituted[1].shape = FixedShape::LastRow;
833
834 assert!(validate_fixed_selectors(&specs, &substituted).is_err());
835 }
836}