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