1use std::{
13 any::{type_name, Any},
14 iter::{self, zip},
15 sync::Arc,
16};
17
18use getset::{CopyGetters, Getters};
19use openvm_circuit_primitives::var_range::{
20 SharedVariableRangeCheckerChip, VariableRangeCheckerAir,
21};
22use openvm_instructions::{PhantomDiscriminant, VmOpcode};
23use openvm_stark_backend::{
24 config::{StarkGenericConfig, Val},
25 engine::StarkEngine,
26 interaction::BusIndex,
27 keygen::types::MultiStarkProvingKey,
28 prover::{
29 cpu::CpuBackend,
30 hal::ProverBackend,
31 types::{AirProvingContext, ProvingContext},
32 },
33 rap::AnyRap,
34 AirRef, AnyChip, Chip,
35};
36use rustc_hash::FxHashMap;
37use tracing::info_span;
38
39use super::{GenerationError, PhantomSubExecutor, SystemConfig};
40use crate::{
41 arch::Arena,
42 system::{
43 memory::{BOUNDARY_AIR_OFFSET, MERKLE_AIR_OFFSET},
44 phantom::PhantomExecutor,
45 SystemAirInventory, SystemChipComplex, SystemRecords,
46 },
47};
48
49pub const PROGRAM_AIR_ID: usize = 0;
51pub const PROGRAM_CACHED_TRACE_INDEX: usize = 0;
53pub const CONNECTOR_AIR_ID: usize = 1;
54pub const PUBLIC_VALUES_AIR_ID: usize = 2;
57pub const BOUNDARY_AIR_ID: usize = PUBLIC_VALUES_AIR_ID + 1 + BOUNDARY_AIR_OFFSET;
59pub const MERKLE_AIR_ID: usize = CONNECTOR_AIR_ID + 1 + MERKLE_AIR_OFFSET;
62
63pub type ExecutorId = u32;
64
65pub trait VmExecutionExtension<F> {
70 type Executor: AnyEnum;
72
73 fn extend_execution(
74 &self,
75 inventory: &mut ExecutorInventoryBuilder<F, Self::Executor>,
76 ) -> Result<(), ExecutorInventoryError>;
77}
78
79pub trait VmCircuitExtension<SC: StarkGenericConfig> {
81 fn extend_circuit(&self, inventory: &mut AirInventory<SC>) -> Result<(), AirInventoryError>;
82}
83
84pub trait VmProverExtension<E, RA, EXT>
91where
92 E: StarkEngine,
93 EXT: VmExecutionExtension<Val<E::SC>> + VmCircuitExtension<E::SC>,
94{
95 fn extend_prover(
102 &self,
103 extension: &EXT,
104 inventory: &mut ChipInventory<E::SC, RA, E::PB>,
105 ) -> Result<(), ChipInventoryError>;
106}
107
108pub struct ExecutorInventory<E> {
111 config: SystemConfig,
112 pub instruction_lookup: FxHashMap<VmOpcode, ExecutorId>,
116 pub executors: Vec<E>,
117 ext_start: Vec<usize>,
119}
120
121pub struct ExecutorInventoryBuilder<'a, F, E> {
127 old_executors: Vec<&'a dyn AnyEnum>,
131 new_inventory: ExecutorInventory<E>,
132 phantom_executors: FxHashMap<PhantomDiscriminant, Arc<dyn PhantomSubExecutor<F>>>,
133}
134
135#[derive(Clone, Getters, CopyGetters)]
136pub struct AirInventory<SC: StarkGenericConfig> {
137 #[get = "pub"]
138 config: SystemConfig,
139 #[get = "pub"]
141 system: SystemAirInventory<SC>,
142 #[get = "pub"]
148 ext_airs: Vec<AirRef<SC>>,
149 ext_start: Vec<usize>,
151
152 bus_idx_mgr: BusIndexManager,
153}
154
155#[derive(Clone, Copy, Debug, Default)]
156pub struct BusIndexManager {
157 bus_idx_max: BusIndex,
159}
160
161#[derive(Getters)]
164pub struct ChipInventory<SC, RA, PB>
165where
166 SC: StarkGenericConfig,
167 PB: ProverBackend,
168{
169 #[get = "pub"]
171 airs: AirInventory<SC>,
172 #[get = "pub"]
174 chips: Vec<Box<dyn AnyChip<RA, PB>>>,
175
176 cur_num_exts: usize,
179 pub executor_idx_to_insertion_idx: Vec<usize>,
186}
187
188#[derive(Getters)]
192pub struct VmChipComplex<SC, RA, PB, SCC>
193where
194 SC: StarkGenericConfig,
195 PB: ProverBackend,
196{
197 pub system: SCC,
199 pub inventory: ChipInventory<SC, RA, PB>,
200}
201
202impl<E> ExecutorInventory<E> {
205 #[allow(clippy::new_without_default)]
207 pub fn new(config: SystemConfig) -> Self {
208 Self {
209 config,
210 instruction_lookup: Default::default(),
211 executors: Default::default(),
212 ext_start: vec![0],
213 }
214 }
215
216 pub fn add_executor(
220 &mut self,
221 executor: impl Into<E>,
222 opcodes: impl IntoIterator<Item = VmOpcode>,
223 ) -> Result<(), ExecutorInventoryError> {
224 let opcodes: Vec<_> = opcodes.into_iter().collect();
225 for opcode in &opcodes {
226 if let Some(id) = self.instruction_lookup.get(opcode) {
227 return Err(ExecutorInventoryError::ExecutorExists {
228 opcode: *opcode,
229 id: *id,
230 });
231 }
232 }
233 let id = self.executors.len();
234 self.executors.push(executor.into());
235 for opcode in opcodes {
236 self.instruction_lookup
237 .insert(opcode, id.try_into().unwrap());
238 }
239 Ok(())
240 }
241
242 pub fn extend<F, E3, EXT>(
245 self,
246 other: &EXT,
247 ) -> Result<ExecutorInventory<E3>, ExecutorInventoryError>
248 where
249 F: 'static,
250 E: Into<E3> + AnyEnum,
251 E3: AnyEnum,
252 EXT: VmExecutionExtension<F>,
253 EXT::Executor: Into<E3>,
254 {
255 let mut builder: ExecutorInventoryBuilder<F, EXT::Executor> = self.builder();
256 other.extend_execution(&mut builder)?;
257 let other_inventory = builder.new_inventory;
258 let other_phantom_executors = builder.phantom_executors;
259 let mut inventory_ext = self.transmute();
260 inventory_ext.append(other_inventory.transmute())?;
261 let phantom_chip: &mut PhantomExecutor<F> = inventory_ext
262 .find_executor_mut()
263 .next()
264 .expect("system always has phantom chip");
265 let phantom_executors = &mut phantom_chip.phantom_executors;
266 for (discriminant, sub_executor) in other_phantom_executors {
267 if phantom_executors
268 .insert(discriminant, sub_executor)
269 .is_some()
270 {
271 return Err(ExecutorInventoryError::PhantomSubExecutorExists { discriminant });
272 }
273 }
274
275 Ok(inventory_ext)
276 }
277
278 pub fn builder<F, E2>(&self) -> ExecutorInventoryBuilder<'_, F, E2>
279 where
280 F: 'static,
281 E: AnyEnum,
282 {
283 let old_executors = self.executors.iter().map(|e| e as &dyn AnyEnum).collect();
284 ExecutorInventoryBuilder {
285 old_executors,
286 new_inventory: ExecutorInventory::new(self.config.clone()),
287 phantom_executors: Default::default(),
288 }
289 }
290
291 pub fn transmute<E2>(self) -> ExecutorInventory<E2>
292 where
293 E: Into<E2>,
294 {
295 ExecutorInventory {
296 config: self.config,
297 instruction_lookup: self.instruction_lookup,
298 executors: self.executors.into_iter().map(|e| e.into()).collect(),
299 ext_start: self.ext_start,
300 }
301 }
302
303 fn append(&mut self, mut other: ExecutorInventory<E>) -> Result<(), ExecutorInventoryError> {
306 let num_executors = self.executors.len();
307 for (opcode, mut id) in other.instruction_lookup.into_iter() {
308 id = id.checked_add(num_executors.try_into().unwrap()).unwrap();
309 if let Some(old_id) = self.instruction_lookup.insert(opcode, id) {
310 return Err(ExecutorInventoryError::ExecutorExists { opcode, id: old_id });
311 }
312 }
313 for id in &mut other.ext_start {
314 *id = id.checked_add(num_executors).unwrap();
315 }
316 self.executors.append(&mut other.executors);
317 self.ext_start.append(&mut other.ext_start);
318 Ok(())
319 }
320
321 pub fn get_executor(&self, opcode: VmOpcode) -> Option<&E> {
322 let id = self.instruction_lookup.get(&opcode)?;
323 self.executors.get(*id as usize)
324 }
325
326 pub fn get_mut_executor(&mut self, opcode: &VmOpcode) -> Option<&mut E> {
327 let id = self.instruction_lookup.get(opcode)?;
328 self.executors.get_mut(*id as usize)
329 }
330
331 pub fn executors(&self) -> &[E] {
332 &self.executors
333 }
334
335 pub fn find_executor<EX: 'static>(&self) -> impl Iterator<Item = &'_ EX>
336 where
337 E: AnyEnum,
338 {
339 self.executors
340 .iter()
341 .filter_map(|e| e.as_any_kind().downcast_ref())
342 }
343
344 pub fn find_executor_mut<EX: 'static>(&mut self) -> impl Iterator<Item = &'_ mut EX>
345 where
346 E: AnyEnum,
347 {
348 self.executors
349 .iter_mut()
350 .filter_map(|e| e.as_any_kind_mut().downcast_mut())
351 }
352
353 pub fn config(&self) -> &SystemConfig {
355 &self.config
356 }
357}
358
359impl<F, E> ExecutorInventoryBuilder<'_, F, E> {
360 pub fn add_executor(
361 &mut self,
362 executor: impl Into<E>,
363 opcodes: impl IntoIterator<Item = VmOpcode>,
364 ) -> Result<(), ExecutorInventoryError> {
365 self.new_inventory.add_executor(executor, opcodes)
366 }
367
368 pub fn add_phantom_sub_executor<PE>(
369 &mut self,
370 phantom_sub: PE,
371 discriminant: PhantomDiscriminant,
372 ) -> Result<(), ExecutorInventoryError>
373 where
374 E: AnyEnum,
375 F: 'static,
376 PE: PhantomSubExecutor<F> + 'static,
377 {
378 let existing = self
379 .phantom_executors
380 .insert(discriminant, Arc::new(phantom_sub));
381 if existing.is_some() {
382 return Err(ExecutorInventoryError::PhantomSubExecutorExists { discriminant });
383 }
384 Ok(())
385 }
386
387 pub fn find_executor<EX: 'static>(&self) -> impl Iterator<Item = &'_ EX>
388 where
389 E: AnyEnum,
390 {
391 self.old_executors
392 .iter()
393 .filter_map(|e| e.as_any_kind().downcast_ref())
394 }
395
396 pub fn pointer_max_bits(&self) -> usize {
398 self.new_inventory.config().memory_config.pointer_max_bits
399 }
400}
401
402impl<SC: StarkGenericConfig> AirInventory<SC> {
403 pub(crate) fn new(
405 config: SystemConfig,
406 system: SystemAirInventory<SC>,
407 bus_idx_mgr: BusIndexManager,
408 ) -> Self {
409 Self {
410 config,
411 system,
412 ext_start: Vec::new(),
413 ext_airs: Vec::new(),
414 bus_idx_mgr,
415 }
416 }
417
418 pub fn start_new_extension(&mut self) {
420 self.ext_start.push(self.ext_airs.len());
421 }
422
423 pub fn new_bus_idx(&mut self) -> BusIndex {
424 self.bus_idx_mgr.new_bus_idx()
425 }
426
427 pub fn find_air<A: 'static>(&self) -> impl Iterator<Item = &'_ A> {
432 self.ext_airs
433 .iter()
434 .filter_map(|air| air.as_any().downcast_ref())
435 }
436
437 pub fn add_air<A: AnyRap<SC> + 'static>(&mut self, air: A) {
438 self.add_air_ref(Arc::new(air));
439 }
440
441 pub fn add_air_ref(&mut self, air: AirRef<SC>) {
442 self.ext_airs.push(air);
443 }
444
445 pub fn range_checker(&self) -> &VariableRangeCheckerAir {
446 self.find_air()
447 .next()
448 .expect("system always has range checker AIR")
449 }
450
451 pub fn into_airs(self) -> impl Iterator<Item = AirRef<SC>> {
456 self.system
457 .into_airs()
458 .into_iter()
459 .chain(self.ext_airs.into_iter().rev())
460 }
461
462 pub fn num_airs(&self) -> usize {
464 self.config.num_airs() + self.ext_airs.len()
465 }
466
467 pub fn keygen<E: StarkEngine<SC = SC>>(self, engine: &E) -> MultiStarkProvingKey<SC> {
469 let mut builder = engine.keygen_builder();
470 for air in self.into_airs() {
471 builder.add_air(air);
472 }
473 builder.generate_pk()
474 }
475
476 pub fn pointer_max_bits(&self) -> usize {
478 self.config.memory_config.pointer_max_bits
479 }
480}
481
482impl BusIndexManager {
483 pub fn new() -> Self {
484 Self { bus_idx_max: 0 }
485 }
486
487 pub fn new_bus_idx(&mut self) -> BusIndex {
488 let idx = self.bus_idx_max;
489 self.bus_idx_max = self.bus_idx_max.checked_add(1).unwrap();
490 idx
491 }
492}
493
494impl<SC, RA, PB> ChipInventory<SC, RA, PB>
495where
496 SC: StarkGenericConfig,
497 PB: ProverBackend,
498{
499 pub fn new(airs: AirInventory<SC>) -> Self {
500 Self {
501 airs,
502 chips: Vec::new(),
503 cur_num_exts: 0,
504 executor_idx_to_insertion_idx: Vec::new(),
505 }
506 }
507
508 pub fn config(&self) -> &SystemConfig {
509 &self.airs.config
510 }
511
512 pub fn start_new_extension(&mut self) -> Result<(), ChipInventoryError> {
514 if self.cur_num_exts >= self.airs.ext_start.len() {
515 return Err(ChipInventoryError::MissingCircuitExtension(
516 self.airs.ext_start.len(),
517 ));
518 }
519 if self.chips.len() != self.airs.ext_start[self.cur_num_exts] {
520 return Err(ChipInventoryError::MissingChip {
521 actual: self.chips.len(),
522 expected: self.airs.ext_start[self.cur_num_exts],
523 });
524 }
525
526 self.cur_num_exts += 1;
527 Ok(())
528 }
529
530 pub fn next_air<A: 'static>(&self) -> Result<&A, ChipInventoryError> {
533 let cur_idx = self.chips.len();
534 self.airs
535 .ext_airs
536 .get(cur_idx)
537 .and_then(|air| air.as_any().downcast_ref())
538 .ok_or_else(|| ChipInventoryError::AirNotFound {
539 name: type_name::<A>().to_string(),
540 })
541 }
542
543 pub fn find_chip<C: 'static>(&self) -> impl Iterator<Item = &'_ C> {
548 self.chips.iter().filter_map(|c| c.as_any().downcast_ref())
549 }
550
551 pub fn add_periphery_chip<C: Chip<RA, PB> + 'static>(&mut self, chip: C) {
554 self.chips.push(Box::new(chip));
555 }
556
557 pub fn add_executor_chip<C: Chip<RA, PB> + 'static>(&mut self, chip: C) {
561 tracing::debug!("add_executor_chip: {}", type_name::<C>());
562 self.executor_idx_to_insertion_idx.push(self.chips.len());
563 self.chips.push(Box::new(chip));
564 }
565
566 pub fn executor_idx_to_air_idx(&self) -> Vec<usize> {
571 let num_airs = self.airs.num_airs();
572 assert_eq!(
573 num_airs,
574 self.config().num_airs() + self.chips.len(),
575 "Number of chips does not match number of AIRs"
576 );
577 self.executor_idx_to_insertion_idx
580 .iter()
581 .map(|insertion_idx| {
582 num_airs
583 .checked_sub(insertion_idx.checked_add(1).unwrap())
584 .unwrap_or_else(|| {
585 panic!(
586 "Attempt to subtract num_airs={num_airs} by {}",
587 insertion_idx + 1
588 )
589 })
590 })
591 .collect()
592 }
593
594 pub fn timestamp_max_bits(&self) -> usize {
595 self.airs.config().memory_config.timestamp_max_bits
596 }
597}
598
599impl<SC, RA> ChipInventory<SC, RA, CpuBackend<SC>>
601where
602 SC: StarkGenericConfig,
603{
604 pub fn range_checker(&self) -> Result<&SharedVariableRangeCheckerChip, ChipInventoryError> {
605 self.find_chip::<SharedVariableRangeCheckerChip>()
606 .next()
607 .ok_or_else(|| ChipInventoryError::ChipNotFound {
608 name: "VariableRangeCheckerChip".to_string(),
609 })
610 }
611}
612
613#[derive(thiserror::Error, Debug)]
616pub enum ExecutorInventoryError {
617 #[error("Opcode {opcode} already owned by executor id {id}")]
618 ExecutorExists { opcode: VmOpcode, id: ExecutorId },
619 #[error("Phantom discriminant {} already has sub-executor", .discriminant.0)]
620 PhantomSubExecutorExists { discriminant: PhantomDiscriminant },
621}
622
623#[derive(thiserror::Error, Debug)]
624pub enum AirInventoryError {
625 #[error("AIR {name} not found")]
626 AirNotFound { name: String },
627}
628
629#[derive(thiserror::Error, Debug)]
630pub enum ChipInventoryError {
631 #[error("Air {name} not found")]
632 AirNotFound { name: String },
633 #[error("Chip {name} not found")]
634 ChipNotFound { name: String },
635 #[error("Adding prover extension without execution extension. Number of execution extensions is {0}")]
636 MissingExecutionExtension(usize),
637 #[error(
638 "Adding prover extension without circuit extension. Number of circuit extensions is {0}"
639 )]
640 MissingCircuitExtension(usize),
641 #[error("Missing chip. Number of chips is {actual}, expected number is {expected}")]
642 MissingChip { actual: usize, expected: usize },
643 #[error("Missing executor chip. Number of executors with associated chips is {actual}, expected number is {expected}")]
644 MissingExecutor { actual: usize, expected: usize },
645}
646
647impl<SC, RA, PB, SCC> VmChipComplex<SC, RA, PB, SCC>
650where
651 SC: StarkGenericConfig,
652 RA: Arena,
653 PB: ProverBackend,
654 SCC: SystemChipComplex<RA, PB>,
655{
656 pub fn system_config(&self) -> &SystemConfig {
657 self.inventory.config()
658 }
659
660 pub(crate) fn generate_proving_ctx(
664 &mut self,
665 system_records: SystemRecords<PB::Val>,
666 record_arenas: Vec<RA>,
667 ) -> Result<ProvingContext<PB>, GenerationError> {
669 let num_sys_airs = self.system_config().num_airs();
677 let num_airs = num_sys_airs + self.inventory.chips.len();
678 if num_airs != record_arenas.len() {
679 return Err(GenerationError::UnexpectedNumArenas {
680 actual: record_arenas.len(),
681 expected: num_airs,
682 });
683 }
684 let mut _record_arenas = record_arenas;
685 let record_arenas = _record_arenas.split_off(num_sys_airs);
686 let sys_record_arenas = _record_arenas;
687
688 let ctx_without_empties: Vec<(usize, AirProvingContext<_>)> = iter::empty()
697 .chain(info_span!("system_trace_gen").in_scope(|| {
698 self.system
699 .generate_proving_ctx(system_records, sys_record_arenas)
700 }))
701 .chain(
702 zip(self.inventory.chips.iter().enumerate().rev(), record_arenas).map(
703 |((insertion_idx, chip), records)| {
704 let _span = (!records.is_empty()).then(|| {
706 let air_name = self.inventory.airs.ext_airs[insertion_idx].name();
707 info_span!("single_trace_gen", air = air_name).entered()
708 });
709 chip.generate_proving_ctx(records)
710 },
711 ),
712 )
713 .enumerate()
714 .filter(|(_air_id, ctx)| {
715 (!ctx.cached_mains.is_empty() || ctx.common_main.is_some())
716 && ctx.main_trace_height() > 0
717 })
718 .collect();
719
720 Ok(ProvingContext {
721 per_air: ctx_without_empties,
722 })
723 }
724}
725
726impl<F, EXT: VmExecutionExtension<F>> VmExecutionExtension<F> for Option<EXT> {
729 type Executor = EXT::Executor;
730
731 fn extend_execution(
732 &self,
733 inventory: &mut ExecutorInventoryBuilder<F, Self::Executor>,
734 ) -> Result<(), ExecutorInventoryError> {
735 if let Some(extension) = self {
736 extension.extend_execution(inventory)
737 } else {
738 Ok(())
739 }
740 }
741}
742
743impl<SC: StarkGenericConfig, EXT: VmCircuitExtension<SC>> VmCircuitExtension<SC> for Option<EXT> {
744 fn extend_circuit(&self, inventory: &mut AirInventory<SC>) -> Result<(), AirInventoryError> {
745 if let Some(extension) = self {
746 extension.extend_circuit(inventory)
747 } else {
748 Ok(())
749 }
750 }
751}
752
753pub trait AnyEnum {
755 fn as_any_kind(&self) -> &dyn Any;
757
758 fn as_any_kind_mut(&mut self) -> &mut dyn Any;
760}
761
762impl AnyEnum for () {
763 fn as_any_kind(&self) -> &dyn Any {
764 self
765 }
766 fn as_any_kind_mut(&mut self) -> &mut dyn Any {
767 self
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use openvm_circuit_derive::AnyEnum;
774 use openvm_stark_sdk::config::baby_bear_poseidon2::BabyBearPoseidon2Config;
775
776 use super::*;
777 use crate::{arch::VmCircuitConfig, system::memory::interface::MemoryInterfaceAirs};
778
779 #[allow(dead_code)]
780 #[derive(Copy, Clone)]
781 enum EnumA {
782 A(u8),
783 B(u32),
784 }
785
786 enum EnumB {
787 C(u64),
788 D(EnumA),
789 }
790
791 #[derive(AnyEnum)]
792 enum EnumC {
793 C(u64),
794 #[any_enum]
795 D(EnumA),
796 }
797
798 impl AnyEnum for EnumA {
799 fn as_any_kind(&self) -> &dyn Any {
800 match self {
801 EnumA::A(a) => a,
802 EnumA::B(b) => b,
803 }
804 }
805
806 fn as_any_kind_mut(&mut self) -> &mut dyn Any {
807 match self {
808 EnumA::A(a) => a,
809 EnumA::B(b) => b,
810 }
811 }
812 }
813
814 impl AnyEnum for EnumB {
815 fn as_any_kind(&self) -> &dyn Any {
816 match self {
817 EnumB::C(c) => c,
818 EnumB::D(d) => d.as_any_kind(),
819 }
820 }
821
822 fn as_any_kind_mut(&mut self) -> &mut dyn Any {
823 match self {
824 EnumB::C(c) => c,
825 EnumB::D(d) => d.as_any_kind_mut(),
826 }
827 }
828 }
829
830 #[test]
831 fn test_any_enum_downcast() {
832 let a = EnumA::A(1);
833 assert_eq!(a.as_any_kind().downcast_ref::<u8>(), Some(&1));
834 let b = EnumB::D(a);
835 assert!(b.as_any_kind().downcast_ref::<u64>().is_none());
836 assert!(b.as_any_kind().downcast_ref::<EnumA>().is_none());
837 assert_eq!(b.as_any_kind().downcast_ref::<u8>(), Some(&1));
838 let c = EnumB::C(3);
839 assert_eq!(c.as_any_kind().downcast_ref::<u64>(), Some(&3));
840 let d = EnumC::D(a);
841 assert!(d.as_any_kind().downcast_ref::<u64>().is_none());
842 assert!(d.as_any_kind().downcast_ref::<EnumA>().is_none());
843 assert_eq!(d.as_any_kind().downcast_ref::<u8>(), Some(&1));
844 let e = EnumC::C(3);
845 assert_eq!(e.as_any_kind().downcast_ref::<u64>(), Some(&3));
846 }
847
848 #[test]
849 fn test_system_bus_indices() {
850 let config = SystemConfig::default();
851 let inventory: AirInventory<BabyBearPoseidon2Config> = config.create_airs().unwrap();
852 let system = inventory.system();
853 let port = system.port();
854 assert_eq!(port.execution_bus.index(), 0);
855 assert_eq!(port.memory_bridge.memory_bus().index(), 1);
856 assert_eq!(port.program_bus.index(), 2);
857 assert_eq!(port.memory_bridge.range_bus().index(), 3);
858 match &system.memory.interface {
859 MemoryInterfaceAirs::Persistent { boundary, .. } => {
860 assert_eq!(boundary.merkle_bus.index, 4);
861 assert_eq!(boundary.compression_bus.index, 5);
862 }
863 _ => unreachable!(),
864 };
865 }
866}