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
13pub type ChallengeLabel = &'static [u8];
16
17pub const REQUEST_IDX_LABEL: ChallengeLabel = b"kappa_request_idx";
19
20pub const REQUEST_IDX_BYTE_LABELS: [ChallengeLabel; 4] = [
23 b"kappa_request_idx_b0",
24 b"kappa_request_idx_b1",
25 b"kappa_request_idx_b2",
26 b"kappa_request_idx_b3",
27];
28
29pub const MAX_ROW_INDEX_BYTES: usize = 8;
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39pub enum BusKind {
40 #[default]
41 Permutation,
42 Lookup,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum Source {
49 Column(usize),
51
52 Columns(Vec<usize>),
55
56 RowIndexLeBytes(usize),
62
63 Const(u128),
65
66 RowIndexByte(usize),
68
69 PhaseColumn(usize),
73}
74
75#[derive(Clone, Debug)]
88pub struct PermutationCheckSpec {
89 pub kind: BusKind,
90
91 pub sources: Vec<(Source, ChallengeLabel)>,
94
95 pub selector: Option<usize>,
98
99 pub recv_selector: Option<usize>,
102
103 pub clock_waiver: Option<String>,
106}
107
108impl PermutationCheckSpec {
109 pub fn new(sources: Vec<(Source, ChallengeLabel)>, selector: Option<usize>) -> Self {
110 Self {
111 sources,
112 selector,
113 recv_selector: None,
114 kind: BusKind::Permutation,
115 clock_waiver: None,
116 }
117 }
118
119 pub fn new_lookup(sources: Vec<(Source, ChallengeLabel)>, selector: Option<usize>) -> Self {
123 Self {
124 sources,
125 selector,
126 recv_selector: None,
127 kind: BusKind::Lookup,
128 clock_waiver: None,
129 }
130 }
131
132 pub fn new_paired(
136 sources: Vec<(Source, ChallengeLabel)>,
137 s_send: usize,
138 s_recv: usize,
139 kind: BusKind,
140 ) -> Self {
141 Self {
142 sources,
143 selector: Some(s_send),
144 recv_selector: Some(s_recv),
145 kind,
146 clock_waiver: None,
147 }
148 }
149
150 pub fn with_clock_waiver(mut self, reason: impl Into<String>) -> Self {
154 self.clock_waiver = Some(reason.into());
155 self
156 }
157
158 pub fn num_sources(&self) -> usize {
159 self.sources.len()
160 }
161
162 pub fn has_selector(&self) -> bool {
163 self.selector.is_some()
164 }
165
166 pub fn has_paired(&self) -> bool {
167 self.recv_selector.is_some()
168 }
169
170 pub fn shift_column_indices(&mut self, offset: usize) {
173 for (source, _) in &mut self.sources {
174 match source {
175 Source::Column(idx) | Source::PhaseColumn(idx) => *idx += offset,
176 Source::Columns(indices) => {
177 for idx in indices {
178 *idx += offset;
179 }
180 }
181 _ => {}
182 }
183 }
184
185 if let Some(sel_idx) = &mut self.selector {
186 *sel_idx += offset;
187 }
188
189 if let Some(sel_idx) = &mut self.recv_selector {
190 *sel_idx += offset;
191 }
192 }
193
194 pub fn has_real_clock_source(&self) -> bool {
197 self.sources
198 .iter()
199 .any(|(src, _)| matches!(src, Source::RowIndexLeBytes(_) | Source::RowIndexByte(_)))
200 }
201
202 pub fn has_request_idx_column(&self) -> bool {
203 self.sources
204 .iter()
205 .any(|(src, label)| matches!(src, Source::Column(_)) && is_request_idx_label(label))
206 }
207
208 pub fn validate_clock_stitching(&self, _bus_id: &str) -> errors::Result<()> {
211 for (src, _) in &self.sources {
212 let width_ok = match src {
213 Source::RowIndexLeBytes(n) => *n >= 1 && *n <= MAX_ROW_INDEX_BYTES,
214 Source::RowIndexByte(k) => *k < MAX_ROW_INDEX_BYTES,
215 Source::Column(_)
216 | Source::Columns(_)
217 | Source::Const(_)
218 | Source::PhaseColumn(_) => true,
219 };
220
221 if !width_ok {
222 return Err(errors::Error::Protocol {
223 protocol: "logup_bus",
224 message: "row-index clock wider than 8 bytes; the row-index \
225 MLE folds at most 8 and truncates the rest silently",
226 });
227 }
228 }
229
230 let waiver_status = self.clock_waiver.as_deref().map(WaiverStatus::classify);
231 let has_clock_marker = self.has_real_clock_source() || self.has_request_idx_column();
232
233 match (self.kind, has_clock_marker, waiver_status) {
234 (BusKind::Lookup, _, Some(_)) => Err(errors::Error::Protocol {
235 protocol: "logup_bus",
236 message: "lookup bus carries a clock_waiver; waivers only apply \
237 to Permutation kind, drop the .with_clock_waiver(...) call",
238 }),
239 (BusKind::Permutation, true, Some(_)) => Err(errors::Error::Protocol {
240 protocol: "logup_bus",
241 message: "permutation bus carries both a clock source and a \
242 clock_waiver; pick one shape",
243 }),
244 (BusKind::Permutation, false, Some(WaiverStatus::Empty)) => {
245 Err(errors::Error::Protocol {
246 protocol: "logup_bus",
247 message: "permutation bus has an empty clock_waiver; provide a \
248 non-empty reason citing the load-bearing AIR constraint",
249 })
250 }
251 (BusKind::Permutation, false, Some(WaiverStatus::TooShort)) => {
252 Err(errors::Error::Protocol {
253 protocol: "logup_bus",
254 message: "permutation bus has an under-specified clock_waiver; \
255 the reason must be at least 32 chars and cite a file/line \
256 of the load-bearing AIR constraint",
257 })
258 }
259 (BusKind::Permutation, false, Some(WaiverStatus::MissingCitation)) => {
260 Err(errors::Error::Protocol {
261 protocol: "logup_bus",
262 message: "permutation bus clock_waiver lacks a 'see <path>' citation; \
263 waiver text must start with 'see ' followed by the file path \
264 of the load-bearing AIR constraint",
265 })
266 }
267 (BusKind::Permutation, false, None) => Err(errors::Error::Protocol {
268 protocol: "logup_bus",
269 message: "permutation bus lacks per-row clock stitching; give the \
270 Service a RequestIdx or RequestIdxBytes slot, or a \
271 clock_waiver citing the AIR constraint that forces \
272 per-row uniqueness",
273 }),
274 _ => Ok(()),
275 }
276 }
277}
278
279#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281pub enum ServiceSlot {
282 Value(ChallengeLabel),
284
285 Const(ChallengeLabel, u128),
288
289 RequestIdx { num_bytes: usize },
293
294 RequestIdxBytes { num_bytes: usize },
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 pub clock_waiver: Option<&'static str>,
310}
311
312impl Service {
313 pub fn num_values(&self) -> usize {
314 self.slots
315 .iter()
316 .filter(|s| matches!(s, ServiceSlot::Value(_)))
317 .count()
318 }
319
320 pub fn clock_columns(&self) -> usize {
323 self.slots
324 .iter()
325 .map(|s| match s {
326 ServiceSlot::RequestIdx { .. } => 1,
327 ServiceSlot::RequestIdxBytes { num_bytes } => *num_bytes,
328 _ => 0,
329 })
330 .sum()
331 }
332
333 pub fn request(
336 &self,
337 values: &[usize],
338 selector: usize,
339 ) -> errors::Result<PermutationCheckSpec> {
340 self.spec(values, None, None, selector)
341 }
342
343 pub fn request_phased(
348 &self,
349 values: &[usize],
350 phase_col: usize,
351 selector: usize,
352 ) -> errors::Result<PermutationCheckSpec> {
353 self.spec(values, None, Some(phase_col), selector)
354 }
355
356 pub fn respond(
360 &self,
361 values: &[usize],
362 clock_cols: &[usize],
363 selector: usize,
364 ) -> errors::Result<PermutationCheckSpec> {
365 self.spec(values, Some(clock_cols), None, selector)
366 }
367
368 fn spec(
369 &self,
370 values: &[usize],
371 respond_idx: Option<&[usize]>,
372 phase: Option<usize>,
373 selector: usize,
374 ) -> errors::Result<PermutationCheckSpec> {
375 let clock_slots = self
376 .slots
377 .iter()
378 .filter(|s| {
379 matches!(
380 s,
381 ServiceSlot::RequestIdx { .. } | ServiceSlot::RequestIdxBytes { .. }
382 )
383 })
384 .count();
385
386 let clock_ok = matches!(
387 (self.kind, clock_slots, self.clock_waiver),
388 (BusKind::Permutation, 1, None)
389 | (BusKind::Permutation, 0, Some(_))
390 | (BusKind::Lookup, 0, None)
391 );
392
393 if !clock_ok {
394 return Err(errors::Error::Protocol {
395 protocol: "service",
396 message: "Permutation schema needs exactly one clock slot or a \
397 clock_waiver, never both; Lookup schema must have neither",
398 });
399 }
400
401 if phase.is_some() {
402 let clock_bytes = self.slots.iter().find_map(|s| match s {
403 ServiceSlot::RequestIdxBytes { num_bytes } => Some(*num_bytes),
404 _ => None,
405 });
406
407 match clock_bytes {
408 None => {
409 return Err(errors::Error::Protocol {
410 protocol: "service",
411 message: "a phased clock needs a byte-split clock slot; \
412 a whole-word, waived or lookup schema has \
413 no top byte to replace",
414 });
415 }
416 Some(num_bytes) if num_bytes < 2 => {
417 return Err(errors::Error::Protocol {
418 protocol: "service",
419 message: "a phased clock needs at least two clock bytes",
420 });
421 }
422 Some(_) => {}
423 }
424 }
425
426 if let Some(cols) = respond_idx
427 && cols.len() != self.clock_columns()
428 {
429 return Err(errors::Error::Protocol {
430 protocol: "service",
431 message: "clock column count does not match the schema's clock slot",
432 });
433 }
434
435 if values.len() != self.num_values() {
436 return Err(errors::Error::Protocol {
437 protocol: "service",
438 message: "value column count does not match the schema's Value slots",
439 });
440 }
441
442 let mut sources = Vec::with_capacity(self.slots.len());
443 let mut next_value = values.iter();
444 let mut next_clock = respond_idx.unwrap_or(&[]).iter();
445
446 for slot in &self.slots {
447 match slot {
448 ServiceSlot::Value(label) => {
449 let col = next_value.next().ok_or(errors::Error::Protocol {
450 protocol: "service",
451 message: "value column count does not match the schema's Value slots",
452 })?;
453
454 sources.push((Source::Column(*col), *label));
455 }
456 ServiceSlot::Const(label, v) => {
457 sources.push((Source::Const(*v), *label));
458 }
459 ServiceSlot::RequestIdx { num_bytes } => {
460 if *num_bytes == 0 || *num_bytes > MAX_ROW_INDEX_BYTES {
461 return Err(errors::Error::Protocol {
462 protocol: "service",
463 message: "RequestIdx num_bytes must be 1..=8; a wider \
464 clock is silently truncated by the row-index MLE",
465 });
466 }
467
468 let source = match respond_idx {
469 None => Source::RowIndexLeBytes(*num_bytes),
470 Some(_) => {
471 Source::Column(*next_clock.next().ok_or(errors::Error::Protocol {
472 protocol: "service",
473 message: "RequestIdx binds exactly one committed \
474 clock column on the responder",
475 })?)
476 }
477 };
478
479 sources.push((source, REQUEST_IDX_LABEL));
480 }
481 ServiceSlot::RequestIdxBytes { num_bytes } => {
482 if *num_bytes == 0 || *num_bytes > REQUEST_IDX_BYTE_LABELS.len() {
483 return Err(errors::Error::Protocol {
484 protocol: "service",
485 message: "byte-split clock wider than the label family",
486 });
487 }
488
489 for (byte, label) in REQUEST_IDX_BYTE_LABELS.iter().enumerate().take(*num_bytes)
490 {
491 let top = byte + 1 == *num_bytes;
492 let source = match (respond_idx, phase) {
493 (Some(_), _) => Source::Column(*next_clock.next().ok_or(
494 errors::Error::Protocol {
495 protocol: "service",
496 message: "byte-split clock binds one committed \
497 column per byte on the responder",
498 },
499 )?),
500 (None, Some(col)) if top => Source::PhaseColumn(col),
501 (None, _) => Source::RowIndexByte(byte),
502 };
503
504 sources.push((source, *label));
505 }
506 }
507 }
508 }
509
510 let spec = match self.kind {
511 BusKind::Permutation => PermutationCheckSpec::new(sources, Some(selector)),
512 BusKind::Lookup => PermutationCheckSpec::new_lookup(sources, Some(selector)),
513 };
514
515 Ok(match self.clock_waiver {
516 Some(reason) => spec.with_clock_waiver(reason),
517 None => spec,
518 })
519 }
520}
521
522#[derive(Clone, Copy, Debug, PartialEq, Eq)]
523enum WaiverStatus {
524 Empty,
525 TooShort,
526 MissingCitation,
527 Ok,
528}
529
530impl WaiverStatus {
531 const MIN_WAIVER_LEN: usize = 32;
532 const REQUIRED_PREFIX: &'static str = "see ";
533
534 fn classify(s: &str) -> Self {
535 if s.is_empty() {
536 Self::Empty
537 } else if s.len() < Self::MIN_WAIVER_LEN {
538 Self::TooShort
539 } else if !s.starts_with(Self::REQUIRED_PREFIX) {
540 Self::MissingCitation
541 } else {
542 Self::Ok
543 }
544 }
545}
546
547pub fn validate_fixed_selectors<F>(
551 specs: &[(String, PermutationCheckSpec)],
552 fixed: &[FixedColumn<F>],
553) -> errors::Result<()> {
554 for (_, spec) in specs {
555 if spec.selector.is_none() && spec.recv_selector.is_some() {
556 return Err(errors::Error::Protocol {
557 protocol: "logup_bus",
558 message: "paired bus has recv_selector without send selector",
559 });
560 }
561
562 let phases = spec.sources.iter().filter_map(|(src, _)| match src {
563 Source::PhaseColumn(col) => Some(*col),
564 _ => None,
565 });
566
567 for sel in [spec.selector, spec.recv_selector]
568 .into_iter()
569 .flatten()
570 .chain(phases)
571 {
572 let Some(fc) = fixed.iter().find(|fc| fc.col_idx == sel) else {
573 return Err(errors::Error::Protocol {
574 protocol: "logup_bus",
575 message: "bus selector or clock phase reads a witness column; \
576 every selector, recv_selector and PhaseColumn must be \
577 None or a member of the table's fixed_columns",
578 });
579 };
580
581 if !fc.shape.is_overlay() {
582 return Err(errors::Error::Protocol {
583 protocol: "logup_bus",
584 message: "bus selector or clock phase is pinned to a substituted \
585 shape; FirstRow/LastRow/Custom replace the committed \
586 column with a virtual poly the bus cannot read; pin \
587 with Cadence, Segments, Periodic, Sparse or Dense",
588 });
589 }
590 }
591 }
592
593 Ok(())
594}
595
596pub fn validate_bus_set<'a, I>(endpoints: I) -> errors::Result<()>
602where
603 I: IntoIterator<Item = (&'a str, &'a PermutationCheckSpec)>,
604{
605 let mut by_bus: BTreeMap<&'a str, Vec<&'a PermutationCheckSpec>> = BTreeMap::new();
606
607 for (bus_id, spec) in endpoints {
608 if !bus_id.bytes().all(|b| b.is_ascii_graphic()) {
609 return Err(errors::Error::Protocol {
610 protocol: "logup_bus",
611 message: "bus_id has a non-graphic byte; bus ids must be \
612 graphic ASCII (0x21..=0x7E). A space, zero-width, \
613 or homoglyph byte forges a visually identical \
614 second bus that balances on its own",
615 });
616 }
617
618 by_bus.entry(bus_id).or_default().push(spec);
619 }
620
621 for (bus_id, specs) in &by_bus {
622 let any_lookup = specs.iter().any(|s| s.kind == BusKind::Lookup);
623 let any_perm = specs.iter().any(|s| s.kind == BusKind::Permutation);
624
625 if any_lookup && any_perm {
626 return Err(errors::Error::Protocol {
627 protocol: "logup_bus",
628 message: "bus_id has mixed BusKind across endpoints; \
629 all endpoints must agree on Permutation or Lookup",
630 });
631 }
632
633 if any_lookup {
634 continue;
635 }
636
637 if specs.len() < 2 {
638 continue;
639 }
640
641 let any_real_clock = specs.iter().any(|s| s.has_real_clock_source());
642 let all_waivered = specs.iter().all(|s| s.clock_waiver.is_some());
643
644 if !any_real_clock && !all_waivered {
645 let _ = bus_id;
646 return Err(errors::Error::Protocol {
647 protocol: "logup_bus",
648 message: "permutation bus_id has no endpoint owning a real \
649 Source::RowIndexLeBytes/RowIndexByte clock and not all \
650 endpoints declare a clock_waiver; label-only stitching \
651 is forgeable and admits char-2 parity collapse",
652 });
653 }
654 }
655
656 Ok(())
657}
658
659pub fn accumulate_lookup_heights(
664 specs: &[(String, PermutationCheckSpec)],
665 table_rows: u64,
666 heights: &mut BTreeMap<String, u64>,
667) {
668 for (bus_id, spec) in specs {
669 if spec.kind == BusKind::Lookup {
670 let entry = heights.entry(bus_id.clone()).or_insert(0);
671 *entry = (*entry).max(table_rows);
672 }
673 }
674}
675
676pub fn eval_row_idx_le_mle<F>(num_bytes: usize, r_final: &[Flat<F>]) -> Flat<F>
679where
680 F: TowerField + HardwareField + From<u128>,
681{
682 let total_bits = (num_bytes.min(8) * 8).min(r_final.len());
683 let mut acc = Flat::from_raw(F::ZERO);
684
685 for (i, r) in r_final.iter().enumerate().take(total_bits) {
686 acc += F::from(1u128 << i).to_hardware() * *r;
687 }
688
689 acc
690}
691
692pub fn eval_row_idx_byte_mle<F>(byte_idx: usize, r_final: &[Flat<F>]) -> Flat<F>
695where
696 F: TowerField + HardwareField + From<u128>,
697{
698 let bit_start = byte_idx.saturating_mul(8);
699 if bit_start >= r_final.len() {
700 return Flat::from_raw(F::ZERO);
701 }
702
703 let end = (bit_start + 8).min(r_final.len());
704 let mut acc = Flat::from_raw(F::ZERO);
705
706 for (j, i) in (bit_start..end).enumerate() {
707 acc += F::from(1u128 << j).to_hardware() * r_final[i];
708 }
709
710 acc
711}
712
713pub fn is_request_idx_label(label: ChallengeLabel) -> bool {
716 label == REQUEST_IDX_LABEL || REQUEST_IDX_BYTE_LABELS.contains(&label)
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722 use crate::FixedShape;
723 use hekate_math::Block128;
724
725 fn test_service() -> Service {
726 Service {
727 bus_id: "svc",
728 kind: BusKind::Permutation,
729 slots: vec![
730 ServiceSlot::Value(b"k_a"),
731 ServiceSlot::Const(b"k_dom", 7),
732 ServiceSlot::RequestIdx { num_bytes: 4 },
733 ServiceSlot::Value(b"k_dir"),
734 ],
735 clock_waiver: None,
736 }
737 }
738
739 fn byte_split_service() -> Service {
740 Service {
741 bus_id: "svc",
742 kind: BusKind::Permutation,
743 slots: vec![
744 ServiceSlot::Value(b"k_a"),
745 ServiceSlot::Const(b"k_dom", 7),
746 ServiceSlot::RequestIdxBytes { num_bytes: 4 },
747 ServiceSlot::Value(b"k_dir"),
748 ],
749 clock_waiver: None,
750 }
751 }
752
753 #[test]
754 fn permutation_spec_creation() {
755 let sources = vec![
756 (Source::Column(0), b"kappa_0" as ChallengeLabel),
757 (Source::Column(1), b"kappa_1" as ChallengeLabel),
758 (Source::RowIndexLeBytes(4), b"kappa_clk" as ChallengeLabel),
759 ];
760
761 let spec = PermutationCheckSpec::new(sources, Some(2));
762
763 assert_eq!(spec.num_sources(), 3);
764 assert!(spec.has_selector());
765 assert_eq!(spec.selector, Some(2));
766 }
767
768 #[test]
769 fn source_variants() {
770 let col = Source::Column(5);
771 let cols = Source::Columns(vec![0, 1, 2, 3]);
772 let clock = Source::RowIndexLeBytes(4);
773 let constant = Source::Const(0x01);
774
775 assert_eq!(col, Source::Column(5));
776 assert_eq!(cols, Source::Columns(vec![0, 1, 2, 3]));
777 assert_eq!(clock, Source::RowIndexLeBytes(4));
778 assert_eq!(constant, Source::Const(0x01));
779 }
780
781 #[test]
782 fn new_paired_populates_both_selectors() {
783 let spec = PermutationCheckSpec::new_paired(
784 vec![(Source::Column(0), b"k_a" as ChallengeLabel)],
785 3,
786 5,
787 BusKind::Permutation,
788 );
789
790 assert!(spec.has_selector());
791 assert!(spec.has_paired());
792 assert_eq!(spec.selector, Some(3));
793 assert_eq!(spec.recv_selector, Some(5));
794 assert_eq!(spec.kind, BusKind::Permutation);
795 }
796
797 #[test]
798 fn new_defaults_recv_selector_none() {
799 let spec =
800 PermutationCheckSpec::new(vec![(Source::Column(0), b"k_a" as ChallengeLabel)], Some(1));
801
802 assert!(!spec.has_paired());
803 assert_eq!(spec.recv_selector, None);
804 }
805
806 #[test]
807 fn new_lookup_defaults_recv_selector_none() {
808 let spec = PermutationCheckSpec::new_lookup(
809 vec![(Source::Column(0), b"k_a" as ChallengeLabel)],
810 Some(1),
811 );
812
813 assert!(!spec.has_paired());
814 assert_eq!(spec.recv_selector, None);
815 }
816
817 #[test]
818 fn shift_column_indices_covers_recv_selector() {
819 let mut spec = PermutationCheckSpec::new_paired(
820 vec![
821 (Source::Column(0), b"k_a" as ChallengeLabel),
822 (Source::Columns(vec![1, 2]), b"k_b" as ChallengeLabel),
823 ],
824 3,
825 5,
826 BusKind::Lookup,
827 );
828
829 spec.shift_column_indices(10);
830
831 assert_eq!(spec.selector, Some(13));
832 assert_eq!(spec.recv_selector, Some(15));
833
834 match &spec.sources[0].0 {
835 Source::Column(idx) => assert_eq!(*idx, 10),
836 other => panic!("expected Column, got {other:?}"),
837 }
838
839 match &spec.sources[1].0 {
840 Source::Columns(idxs) => assert_eq!(idxs, &vec![11, 12]),
841 other => panic!("expected Columns, got {other:?}"),
842 }
843 }
844
845 #[test]
846 fn validate_bus_set_rejects_homoglyph_split_bus() {
847 let clockless =
848 PermutationCheckSpec::new(vec![(Source::Column(0), b"k" as ChallengeLabel)], Some(1));
849 let other = clockless.clone();
850
851 assert!(validate_bus_set(vec![("ram_link", &clockless), ("ram_link", &other)]).is_err());
852
853 assert!(
857 validate_bus_set(vec![("ram_link", &clockless), ("ram_link\u{200b}", &other)]).is_err()
858 );
859 }
860
861 #[test]
862 fn validate_bus_set_accepts_ascii_bus_id() {
863 let spec =
864 PermutationCheckSpec::new(vec![(Source::Column(0), b"k" as ChallengeLabel)], Some(1));
865
866 assert!(validate_bus_set(vec![("ram_link", &spec)]).is_ok());
867 }
868
869 #[test]
870 fn service_endpoints_share_schema() {
871 let svc = test_service();
872
873 let req = svc.request(&[10, 11], 12).unwrap();
874 let resp = svc.respond(&[20, 21], &[22], 23).unwrap();
875
876 let req_labels: Vec<_> = req.sources.iter().map(|(_, l)| *l).collect();
877 let resp_labels: Vec<_> = resp.sources.iter().map(|(_, l)| *l).collect();
878
879 assert_eq!(req_labels, resp_labels);
880
881 assert_eq!(req.sources[0].0, Source::Column(10));
882 assert_eq!(req.sources[1].0, Source::Const(7));
883 assert_eq!(req.sources[2].0, Source::RowIndexLeBytes(4));
884 assert_eq!(req.sources[3].0, Source::Column(11));
885 assert_eq!(req.selector, Some(12));
886
887 assert_eq!(resp.sources[0].0, Source::Column(20));
888 assert_eq!(resp.sources[1].0, Source::Const(7));
889 assert_eq!(resp.sources[2].0, Source::Column(22));
890 assert_eq!(resp.sources[3].0, Source::Column(21));
891 assert_eq!(resp.selector, Some(23));
892
893 req.validate_clock_stitching("svc").unwrap();
894 resp.validate_clock_stitching("svc").unwrap();
895 }
896
897 #[test]
898 fn service_rejects_schema_mismatches() {
899 let svc = test_service();
900
901 assert!(svc.request(&[10], 12).is_err());
902 assert!(svc.respond(&[20, 21], &[], 23).is_err());
903
904 let lookup = Service {
905 bus_id: "rom",
906 kind: BusKind::Lookup,
907 slots: vec![ServiceSlot::Value(b"k_a")],
908 clock_waiver: None,
909 };
910
911 assert!(lookup.respond(&[3], &[9], 4).is_err());
912
913 let clockless = Service {
914 bus_id: "bad",
915 kind: BusKind::Permutation,
916 slots: vec![ServiceSlot::Value(b"k_a")],
917 clock_waiver: None,
918 };
919
920 assert!(clockless.request(&[1], 2).is_err());
921 }
922
923 #[test]
924 fn lookup_service_derives_lookup_specs() {
925 let svc = Service {
926 bus_id: "rom",
927 kind: BusKind::Lookup,
928 slots: vec![ServiceSlot::Value(b"k_a")],
929 clock_waiver: None,
930 };
931
932 let req = svc.request(&[1], 2).unwrap();
933 let resp = svc.respond(&[3], &[], 4).unwrap();
934
935 assert_eq!(req.kind, BusKind::Lookup);
936 assert_eq!(resp.kind, BusKind::Lookup);
937 assert_eq!(req.sources.len(), 1);
938 }
939
940 #[test]
941 fn request_phased_replaces_only_top_clock_byte() {
942 let svc = byte_split_service();
943
944 let phased = svc.request_phased(&[10, 11], 60, 12).unwrap();
945 let plain = svc.request(&[10, 11], 12).unwrap();
946
947 let labels = |spec: &PermutationCheckSpec| -> Vec<ChallengeLabel> {
948 spec.sources.iter().map(|(_, l)| *l).collect()
949 };
950
951 assert_eq!(labels(&phased), labels(&plain));
952
953 assert_eq!(phased.sources[2].0, Source::RowIndexByte(0));
954 assert_eq!(phased.sources[3].0, Source::RowIndexByte(1));
955 assert_eq!(phased.sources[4].0, Source::RowIndexByte(2));
956 assert_eq!(phased.sources[5].0, Source::PhaseColumn(60));
957 assert_eq!(plain.sources[5].0, Source::RowIndexByte(3));
958
959 assert!(phased.has_real_clock_source());
960
961 phased.validate_clock_stitching("svc").unwrap();
962
963 let responder = svc.respond(&[20, 21], &[40, 41, 42, 43], 23).unwrap();
964
965 validate_bus_set(vec![("svc", &phased), ("svc", &responder)]).unwrap();
966 }
967
968 #[test]
969 fn request_phased_rejects_whole_word_clock() {
970 assert!(test_service().request_phased(&[10, 11], 60, 12).is_err());
971 }
972
973 #[test]
974 fn request_phased_rejects_clockless_schema() {
975 let waived = Service {
976 bus_id: "svc",
977 kind: BusKind::Permutation,
978 slots: vec![ServiceSlot::Value(b"k_a")],
979 clock_waiver: Some("see permutation.rs: a body constraint forces per-row uniqueness"),
980 };
981
982 assert!(waived.request_phased(&[10], 60, 12).is_err());
983 assert!(waived.request(&[10], 12).is_ok());
984
985 let lookup = Service {
986 bus_id: "svc",
987 kind: BusKind::Lookup,
988 slots: vec![ServiceSlot::Value(b"k_a")],
989 clock_waiver: None,
990 };
991
992 assert!(lookup.request_phased(&[10], 60, 12).is_err());
993 assert!(lookup.request(&[10], 12).is_ok());
994 }
995
996 #[test]
997 fn request_phased_rejects_one_byte_clock() {
998 let svc = Service {
999 slots: vec![ServiceSlot::RequestIdxBytes { num_bytes: 1 }],
1000 ..byte_split_service()
1001 };
1002
1003 assert!(svc.request_phased(&[], 60, 12).is_err());
1004 assert!(svc.request(&[], 12).is_ok());
1005 }
1006
1007 #[test]
1008 fn witness_clock_phase_is_rejected() {
1009 let svc = byte_split_service();
1010 let phased = svc.request_phased(&[10, 11], 60, 12).unwrap();
1011 let specs = vec![(String::from("svc"), phased)];
1012
1013 let selector_only = vec![FixedColumn::<Block128> {
1014 col_idx: 12,
1015 shape: FixedShape::Periodic {
1016 period: 1,
1017 values: vec![Block128::ONE],
1018 },
1019 }];
1020
1021 assert!(validate_fixed_selectors(&specs, &selector_only).is_err());
1022
1023 let mut pinned = selector_only;
1024 pinned.push(FixedColumn {
1025 col_idx: 60,
1026 shape: FixedShape::Periodic {
1027 period: 2,
1028 values: vec![Block128::ZERO, Block128::ONE],
1029 },
1030 });
1031
1032 validate_fixed_selectors(&specs, &pinned).unwrap();
1033
1034 let mut substituted = pinned;
1035 substituted[1].shape = FixedShape::LastRow;
1036
1037 assert!(validate_fixed_selectors(&specs, &substituted).is_err());
1038 }
1039}