1use crate::abi::{CapabilityMask, InstanceId};
10use crate::runtime::Kernel;
11
12use super::wal::{
13 StepVerdict, TrustAnchor, Wal, WalError, WalHeader, WalRecordContent, WalWriter,
14};
15
16#[derive(Debug, Default, Clone, PartialEq, Eq)]
18pub struct ReplayReport {
19 pub submits_replayed: u32,
21 pub steps_replayed: u32,
23 pub total_effects_applied: u32,
25 pub total_effects_denied: u32,
27 pub final_chain_tip: [u8; 32],
31}
32
33#[derive(Debug, Clone)]
35#[non_exhaustive]
36pub enum ReplayError {
37 HeaderIncompatible(String),
39 KernelSemverMismatch {
41 expected: (u16, u16, u16),
43 got: (u16, u16, u16),
45 },
46 AbiVersionMismatch {
48 expected: (u16, u16),
50 got: (u16, u16),
52 },
53 PostcardVersionMismatch {
55 expected: u32,
57 got: u32,
59 },
60 Blake3VersionMismatch {
62 expected: u32,
64 got: u32,
66 },
67 WalCorrupted(WalError),
69 SubmitFailed(String),
72 StepUnderflow {
75 instance: InstanceId,
77 },
78 PoppedIdDivergence {
81 seq: u64,
83 },
84 VerdictDivergence {
87 seq: u64,
89 expected: StepVerdict,
91 got: StepVerdict,
93 },
94 StateDigestDivergence {
97 seq: u64,
99 },
100 ChainTipDivergence {
103 expected: [u8; 32],
105 measured: [u8; 32],
107 },
108 ManifestDigestMismatch {
113 expected: [u8; 32],
115 got: [u8; 32],
117 },
118}
119
120impl From<WalError> for ReplayError {
121 fn from(e: WalError) -> Self {
122 Self::WalCorrupted(e)
123 }
124}
125
126impl core::fmt::Display for ReplayError {
127 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
128 match self {
129 Self::HeaderIncompatible(m) => write!(f, "wal header incompatible: {}", m),
130 Self::KernelSemverMismatch { expected, got } => {
131 write!(
132 f,
133 "kernel semver mismatch: expected {:?}, got {:?}",
134 expected, got
135 )
136 }
137 Self::AbiVersionMismatch { expected, got } => {
138 write!(
139 f,
140 "abi version mismatch: expected {:?}, got {:?}",
141 expected, got
142 )
143 }
144 Self::PostcardVersionMismatch { expected, got } => {
145 write!(
146 f,
147 "postcard version mismatch: expected {}, got {}",
148 expected, got
149 )
150 }
151 Self::Blake3VersionMismatch { expected, got } => {
152 write!(f, "blake3 version mismatch: expected {}, got {}", expected, got)
153 }
154 Self::WalCorrupted(e) => write!(f, "wal corrupted: {}", e),
155 Self::SubmitFailed(m) => write!(f, "submit failed: {}", m),
156 Self::StepUnderflow { instance } => {
157 write!(f, "replay step underflow: no due action for {:?}", instance)
158 }
159 Self::PoppedIdDivergence { seq } => {
160 write!(f, "replay popped-id divergence at record {}", seq)
161 }
162 Self::VerdictDivergence { seq, expected, got } => write!(
163 f,
164 "replay verdict divergence at record {}: expected {:?}, got {:?}",
165 seq, expected, got
166 ),
167 Self::StateDigestDivergence { seq } => {
168 write!(f, "replay state-digest divergence at record {}", seq)
169 }
170 Self::ChainTipDivergence { expected, measured } => write!(
171 f,
172 "replay chain-tip divergence: expected {}, measured {}",
173 blake3::Hash::from(*expected).to_hex(),
174 blake3::Hash::from(*measured).to_hex()
175 ),
176 Self::ManifestDigestMismatch { expected, got } => write!(
177 f,
178 "replay manifest_digest mismatch: wal {}, kernel {}",
179 blake3::Hash::from(*expected).to_hex(),
180 blake3::Hash::from(*got).to_hex()
181 ),
182 }
183 }
184}
185
186impl std::error::Error for ReplayError {}
187
188fn check_header_gates(wal: &Wal) -> Result<(), ReplayError> {
192 if wal.header.magic != WalHeader::MAGIC {
193 return Err(ReplayError::HeaderIncompatible(
194 "magic mismatch (expected ARKHEWAL)".to_string(),
195 ));
196 }
197 if wal.header.kernel_semver.0 != WalHeader::CURRENT_KERNEL_SEMVER.0 {
198 return Err(ReplayError::KernelSemverMismatch {
199 expected: WalHeader::CURRENT_KERNEL_SEMVER,
200 got: wal.header.kernel_semver,
201 });
202 }
203 if wal.header.abi_version != WalHeader::ABI_VERSION {
204 return Err(ReplayError::AbiVersionMismatch {
205 expected: WalHeader::ABI_VERSION,
206 got: wal.header.abi_version,
207 });
208 }
209 if wal.header.postcard_version != WalHeader::POSTCARD_MAJOR {
213 return Err(ReplayError::PostcardVersionMismatch {
214 expected: WalHeader::POSTCARD_MAJOR,
215 got: wal.header.postcard_version,
216 });
217 }
218 if wal.header.blake3_version != WalHeader::BLAKE3_MAJOR {
219 return Err(ReplayError::Blake3VersionMismatch {
220 expected: WalHeader::BLAKE3_MAJOR,
221 got: wal.header.blake3_version,
222 });
223 }
224 Ok(())
225}
226
227fn replay_records(kernel: &mut Kernel, wal: &Wal) -> Result<ReplayReport, ReplayError> {
239 let mut report = ReplayReport::default();
240 let mut writer = WalWriter::rebuild_from_header(&wal.header);
244
245 for rec in &wal.records {
246 match &rec.content {
247 WalRecordContent::Submit {
248 seq: _,
249 instance,
250 principal,
251 actor,
252 caps_at_submit,
253 at,
254 action_type_code,
255 action_bytes,
256 allocated_id,
257 } => {
258 let caps = CapabilityMask::from_bits_retain(*caps_at_submit);
259 kernel
260 .submit_with_id(
261 *instance,
262 principal.clone(),
263 *actor,
264 caps,
265 *allocated_id,
266 *at,
267 *action_type_code,
268 action_bytes.clone(),
269 )
270 .map_err(|e| ReplayError::SubmitFailed(format!("{:?}", e)))?;
271 writer.append_submit(
272 *instance,
273 principal.clone(),
274 *actor,
275 *caps_at_submit,
276 *at,
277 *action_type_code,
278 action_bytes.clone(),
279 *allocated_id,
280 )?;
281 report.submits_replayed = report.submits_replayed.saturating_add(1);
282 }
283 WalRecordContent::Step {
284 seq,
285 instance,
286 popped_id,
287 now,
288 session_caps,
289 verdict,
290 post_state_digest,
291 } => {
292 let session = CapabilityMask::from_bits_retain(*session_caps);
293 let result = kernel
294 .process_instance_record(*instance, *now, session, true)
295 .ok_or(ReplayError::StepUnderflow {
296 instance: *instance,
297 })?;
298 if result.popped_id != *popped_id {
300 return Err(ReplayError::PoppedIdDivergence { seq: *seq });
301 }
302 if result.verdict != *verdict {
303 return Err(ReplayError::VerdictDivergence {
304 seq: *seq,
305 expected: *verdict,
306 got: result.verdict,
307 });
308 }
309 if result.digest != *post_state_digest {
310 return Err(ReplayError::StateDigestDivergence { seq: *seq });
311 }
312 writer.append_step(
313 *instance,
314 *popped_id,
315 *now,
316 *session_caps,
317 *verdict,
318 *post_state_digest,
319 )?;
320 report.steps_replayed = report.steps_replayed.saturating_add(1);
321 report.total_effects_applied = report
322 .total_effects_applied
323 .saturating_add(result.effects_applied);
324 report.total_effects_denied = report
325 .total_effects_denied
326 .saturating_add(result.effects_denied);
327 }
328 }
329 }
330
331 let measured = writer.chain_tip();
334 let sealed = wal.chain_tip();
335 if measured != sealed {
336 return Err(ReplayError::ChainTipDivergence {
337 expected: sealed,
338 measured,
339 });
340 }
341 report.final_chain_tip = measured;
342 Ok(report)
343}
344
345pub fn replay_into(kernel: &mut Kernel, wal: &Wal) -> Result<ReplayReport, ReplayError> {
356 check_header_gates(wal)?;
357 if wal.header.manifest_digest != kernel.manifest_digest() {
362 return Err(ReplayError::ManifestDigestMismatch {
363 expected: wal.header.manifest_digest,
364 got: kernel.manifest_digest(),
365 });
366 }
367 wal.verify_chain(wal.header.world_id)?;
368 replay_records(kernel, wal)
369}
370
371pub fn replay_into_verified(
378 kernel: &mut Kernel,
379 wal: &Wal,
380 world_id: [u8; 32],
381 anchor: &TrustAnchor,
382) -> Result<ReplayReport, ReplayError> {
383 check_header_gates(wal)?;
384 wal.verify_chain_anchored(world_id, anchor)?;
385 replay_records(kernel, wal)
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::abi::{EntityId, ExternalId, InstanceId, Principal, Tick, TypeCode};
392 use crate::persist::wal::{SkipReason, Wal, WalWriter};
393 use crate::runtime::Kernel;
394 use crate::state::traits::_sealed::Sealed;
395 use crate::state::{
396 Action, ActionCompute, ActionContext, ActionDeriv, InstanceConfig, Op,
397 };
398 use bytes::Bytes;
399 use serde::{Deserialize, Serialize};
400
401 fn world() -> [u8; 32] {
402 [11u8; 32]
403 }
404
405 fn priv_cfg() -> InstanceConfig {
409 InstanceConfig {
410 default_caps: CapabilityMask::SYSTEM,
411 max_entities: 1024,
412 max_scheduled: 1024,
413 max_inbox_per_route: 16,
414 memory_budget_bytes: 1 << 20,
415 ..Default::default()
416 }
417 }
418
419 #[derive(Serialize, Deserialize)]
423 struct SpawnAction {
424 id: u64,
425 }
426 impl Sealed for SpawnAction {}
427 impl ActionDeriv for SpawnAction {
428 const TYPE_CODE: TypeCode = TypeCode(100);
429 const SCHEMA_VERSION: u32 = 1;
430 }
431 impl ActionCompute for SpawnAction {
432 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
433 vec![Op::SpawnEntity {
434 id: EntityId::new(self.id).unwrap(),
435 owner: Principal::System,
436 }]
437 }
438 }
439
440 #[derive(Serialize, Deserialize)]
444 struct ScheduleChildAction {
445 child_id: u64,
446 at: u64,
447 }
448 impl Sealed for ScheduleChildAction {}
449 impl ActionDeriv for ScheduleChildAction {
450 const TYPE_CODE: TypeCode = TypeCode(200);
451 const SCHEMA_VERSION: u32 = 1;
452 }
453 impl ActionCompute for ScheduleChildAction {
454 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
455 let child = SpawnAction { id: self.child_id };
456 vec![Op::ScheduleAction {
457 at: Tick(self.at),
458 actor: None,
459 action_type_code: SpawnAction::TYPE_CODE,
460 action_bytes: Bytes::from(Action::canonical_bytes(&child)),
461 }]
462 }
463 }
464
465 #[derive(Serialize, Deserialize)]
467 struct SignalAction {
468 target: u64,
469 }
470 impl Sealed for SignalAction {}
471 impl ActionDeriv for SignalAction {
472 const TYPE_CODE: TypeCode = TypeCode(300);
473 const SCHEMA_VERSION: u32 = 1;
474 }
475 impl ActionCompute for SignalAction {
476 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
477 vec![Op::SendSignal {
478 target: InstanceId::new(self.target).unwrap(),
479 route: crate::abi::RouteId(1),
480 payload: Bytes::from_static(b"ping"),
481 }]
482 }
483 }
484
485 #[derive(Serialize, Deserialize)]
488 struct TwoSetAction {
489 a: u64,
490 b: u64,
491 }
492 impl Sealed for TwoSetAction {}
493 impl ActionDeriv for TwoSetAction {
494 const TYPE_CODE: TypeCode = TypeCode(400);
495 const SCHEMA_VERSION: u32 = 1;
496 }
497 impl ActionCompute for TwoSetAction {
498 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
499 let e = EntityId::new(1).unwrap();
500 vec![
501 Op::SpawnEntity {
502 id: e,
503 owner: Principal::System,
504 },
505 Op::SetComponent {
506 entity: e,
507 type_code: TypeCode(7),
508 bytes: Bytes::from(vec![0u8; self.a as usize]),
509 size: self.a,
510 },
511 Op::SetComponent {
512 entity: e,
513 type_code: TypeCode(8),
514 bytes: Bytes::from(vec![0u8; self.b as usize]),
515 size: self.b,
516 },
517 ]
518 }
519 }
520
521 #[derive(Serialize, Deserialize)]
524 struct ScheduleNeedsSystemAction;
525 impl Sealed for ScheduleNeedsSystemAction {}
526 impl ActionDeriv for ScheduleNeedsSystemAction {
527 const TYPE_CODE: TypeCode = TypeCode(500);
528 const SCHEMA_VERSION: u32 = 1;
529 }
530 impl ActionCompute for ScheduleNeedsSystemAction {
531 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
532 vec![Op::ScheduleAction {
533 at: Tick(9),
534 actor: None,
535 action_type_code: SpawnAction::TYPE_CODE,
536 action_bytes: Bytes::from_static(b""),
537 }]
538 }
539 }
540
541 #[test]
544 fn replay_empty_wal_succeeds() {
545 let w = WalWriter::new(world(), [0u8; 32]);
546 let wal = Wal::from_writer(w);
547 let mut kernel = Kernel::new();
548 let report = replay_into(&mut kernel, &wal).unwrap();
549 assert_eq!(report.submits_replayed, 0);
550 assert_eq!(report.steps_replayed, 0);
551 }
552
553 #[test]
554 fn replay_rejects_wrong_magic() {
555 let w = WalWriter::new(world(), [0u8; 32]);
556 let mut wal = Wal::from_writer(w);
557 wal.header.magic = *b"BADMAGIC";
558 let mut kernel = Kernel::new();
559 assert!(matches!(
560 replay_into(&mut kernel, &wal),
561 Err(ReplayError::HeaderIncompatible(_))
562 ));
563 }
564
565 #[test]
566 fn replay_rejects_kernel_semver_major_mismatch() {
567 let w = WalWriter::new(world(), [0u8; 32]);
568 let mut wal = Wal::from_writer(w);
569 wal.header.kernel_semver = (99, 0, 0);
570 let mut kernel = Kernel::new();
571 assert!(matches!(
572 replay_into(&mut kernel, &wal),
573 Err(ReplayError::KernelSemverMismatch { .. })
574 ));
575 }
576
577 #[test]
578 fn replay_rejects_postcard_version_mismatch() {
579 let w = WalWriter::new(world(), [0u8; 32]);
580 let mut wal = Wal::from_writer(w);
581 wal.header.postcard_version = 999;
582 let mut kernel = Kernel::new();
583 assert!(matches!(
584 replay_into(&mut kernel, &wal),
585 Err(ReplayError::PostcardVersionMismatch { .. })
586 ));
587 }
588
589 #[test]
590 fn replay_rejects_corrupted_chain() {
591 let mut w = WalWriter::new(world(), [0u8; 32]);
592 w.append_step(
593 InstanceId::new(1).unwrap(),
594 crate::state::ScheduledActionId::new(1).unwrap(),
595 Tick(0),
596 CapabilityMask::SYSTEM.bits(),
597 StepVerdict::Committed,
598 [9u8; 32],
599 )
600 .unwrap();
601 let mut wal = Wal::from_writer(w);
602 wal.records[0].this_chain_hash = [0xFFu8; 32];
603 let mut kernel = Kernel::new();
604 assert!(matches!(
605 replay_into(&mut kernel, &wal),
606 Err(ReplayError::WalCorrupted(_))
607 ));
608 }
609
610 #[test]
619 fn replay_internal_scheduling_is_bit_identical() {
620 let build = || -> Kernel {
621 let mut k = Kernel::new_with_wal(world(), [0u8; 32]);
622 k.register_action::<ScheduleChildAction>();
623 k.register_action::<SpawnAction>();
624 let inst = k.create_instance(priv_cfg());
625 let parent = ScheduleChildAction {
626 child_id: 77,
627 at: 1,
628 };
629 k.submit(
630 inst,
631 Principal::System,
632 None,
633 CapabilityMask::SYSTEM,
634 Tick(0),
635 ScheduleChildAction::TYPE_CODE,
636 Action::canonical_bytes(&parent),
637 )
638 .unwrap();
639 k.step(Tick(0), CapabilityMask::SYSTEM); k.step(Tick(1), CapabilityMask::SYSTEM); k
642 };
643 let k1 = build();
644 let original_tip = k1.wal_chain_tip().unwrap();
645 let wal = k1.export_wal().unwrap();
646 let submit_count = wal
648 .records
649 .iter()
650 .filter(|r| matches!(r.kind(), crate::persist::WalRecordKind::Submit))
651 .count();
652 let step_count = wal
653 .records
654 .iter()
655 .filter(|r| matches!(r.kind(), crate::persist::WalRecordKind::Step))
656 .count();
657 assert_eq!(submit_count, 1, "only the parent is an exogenous submit");
658 assert_eq!(step_count, 2, "two pops: parent then re-derived child");
659
660 let mut k2 = Kernel::new_with_wal(world(), [0u8; 32]);
661 k2.register_action::<ScheduleChildAction>();
662 k2.register_action::<SpawnAction>();
663 let _ = k2.create_instance(priv_cfg());
664 let report = replay_into(&mut k2, &wal).expect("bit-identical replay");
665 assert_eq!(report.submits_replayed, 1);
666 assert_eq!(report.steps_replayed, 2);
667 assert_eq!(report.final_chain_tip, original_tip);
668 }
669
670 #[test]
674 fn replay_history_with_authorize_denial() {
675 let build = || -> Kernel {
676 let mut k = Kernel::new_with_wal(world(), [0u8; 32]);
677 k.register_action::<ScheduleNeedsSystemAction>();
678 k.register_action::<SpawnAction>();
679 let inst = k.create_instance(InstanceConfig {
682 max_scheduled: 1024,
683 ..Default::default()
684 });
685 k.submit(
686 inst,
687 Principal::External(ExternalId(7)),
688 None,
689 CapabilityMask::empty(),
690 Tick(0),
691 ScheduleNeedsSystemAction::TYPE_CODE,
692 Vec::new(),
693 )
694 .unwrap();
695 k.step(Tick(0), CapabilityMask::SYSTEM);
696 k
697 };
698 let k1 = build();
699 let original_tip = k1.wal_chain_tip().unwrap();
700 let wal = k1.export_wal().unwrap();
701 assert!(matches!(
702 wal.records[1].content,
703 WalRecordContent::Step {
704 verdict: StepVerdict::AuthDenied,
705 ..
706 }
707 ));
708
709 let mut k2 = Kernel::new_with_wal(world(), [0u8; 32]);
710 k2.register_action::<ScheduleNeedsSystemAction>();
711 k2.register_action::<SpawnAction>();
712 let _ = k2.create_instance(InstanceConfig {
713 max_scheduled: 1024,
714 ..Default::default()
715 });
716 let report = replay_into(&mut k2, &wal).expect("replay ok");
717 assert_eq!(report.final_chain_tip, original_tip);
718 }
719
720 #[test]
723 fn replay_history_with_budget_partial() {
724 let build = || -> Kernel {
725 let mut k = Kernel::new_with_wal(world(), [0u8; 32]);
726 k.register_action::<TwoSetAction>();
727 let inst = k.create_instance(InstanceConfig {
730 memory_budget_bytes: 100,
731 max_entities: 1024,
732 ..Default::default()
733 });
734 let a = TwoSetAction { a: 40, b: 80 };
735 k.submit(
736 inst,
737 Principal::System,
738 None,
739 CapabilityMask::SYSTEM,
740 Tick(0),
741 TwoSetAction::TYPE_CODE,
742 Action::canonical_bytes(&a),
743 )
744 .unwrap();
745 k.step(Tick(0), CapabilityMask::SYSTEM);
746 k
747 };
748 let k1 = build();
749 let original_tip = k1.wal_chain_tip().unwrap();
750 let wal = k1.export_wal().unwrap();
751 assert!(matches!(
752 wal.records[1].content,
753 WalRecordContent::Step {
754 verdict: StepVerdict::BudgetPartial { denied: 1 },
755 ..
756 }
757 ));
758
759 let mut k2 = Kernel::new_with_wal(world(), [0u8; 32]);
760 k2.register_action::<TwoSetAction>();
761 let _ = k2.create_instance(InstanceConfig {
762 memory_budget_bytes: 100,
763 max_entities: 1024,
764 ..Default::default()
765 });
766 let report = replay_into(&mut k2, &wal).expect("replay ok");
767 assert_eq!(report.final_chain_tip, original_tip);
768 }
769
770 #[test]
773 fn replay_skipped_unregistered_and_deserfailed() {
774 let build = || -> Kernel {
775 let mut k = Kernel::new_with_wal(world(), [0u8; 32]);
776 k.register_action::<SpawnAction>();
778 let inst = k.create_instance(priv_cfg());
779 k.submit(
781 inst,
782 Principal::System,
783 None,
784 CapabilityMask::SYSTEM,
785 Tick(0),
786 TypeCode(200),
787 vec![1, 2, 3],
788 )
789 .unwrap();
790 k.submit(
792 inst,
793 Principal::System,
794 None,
795 CapabilityMask::SYSTEM,
796 Tick(1),
797 SpawnAction::TYPE_CODE,
798 Vec::new(),
799 )
800 .unwrap();
801 k.step(Tick(0), CapabilityMask::SYSTEM);
802 k.step(Tick(1), CapabilityMask::SYSTEM);
803 k
804 };
805 let k1 = build();
806 let original_tip = k1.wal_chain_tip().unwrap();
807 let wal = k1.export_wal().unwrap();
808 let verdicts: Vec<StepVerdict> = wal
809 .records
810 .iter()
811 .filter_map(|r| match &r.content {
812 WalRecordContent::Step { verdict, .. } => Some(*verdict),
813 WalRecordContent::Submit { .. } => None,
814 })
815 .collect();
816 assert!(verdicts.contains(&StepVerdict::Skipped {
817 reason: SkipReason::Unregistered
818 }));
819 assert!(verdicts.contains(&StepVerdict::Skipped {
820 reason: SkipReason::DeserFailed
821 }));
822
823 let mut k2 = Kernel::new_with_wal(world(), [0u8; 32]);
824 k2.register_action::<SpawnAction>();
825 let _ = k2.create_instance(priv_cfg());
826 let report = replay_into(&mut k2, &wal).expect("replay ok");
827 assert_eq!(report.final_chain_tip, original_tip);
828 }
829
830 #[test]
834 fn next_seq_tiebreak_survives_replay() {
835 #[derive(Serialize, Deserialize)]
836 struct ScheduleTwoSameTick;
837 impl Sealed for ScheduleTwoSameTick {}
838 impl ActionDeriv for ScheduleTwoSameTick {
839 const TYPE_CODE: TypeCode = TypeCode(600);
840 const SCHEMA_VERSION: u32 = 1;
841 }
842 impl ActionCompute for ScheduleTwoSameTick {
843 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
844 let mk = |id: u64| {
845 let child = SpawnAction { id };
846 Op::ScheduleAction {
847 at: Tick(5),
848 actor: None,
849 action_type_code: SpawnAction::TYPE_CODE,
850 action_bytes: Bytes::from(Action::canonical_bytes(&child)),
851 }
852 };
853 vec![mk(11), mk(22)]
854 }
855 }
856
857 let build = || -> Kernel {
858 let mut k = Kernel::new_with_wal(world(), [0u8; 32]);
859 k.register_action::<ScheduleTwoSameTick>();
860 k.register_action::<SpawnAction>();
861 let inst = k.create_instance(priv_cfg());
862 k.submit(
863 inst,
864 Principal::System,
865 None,
866 CapabilityMask::SYSTEM,
867 Tick(0),
868 TypeCode(600),
869 Vec::new(),
870 )
871 .unwrap();
872 k.step(Tick(0), CapabilityMask::SYSTEM);
873 k.step(Tick(5), CapabilityMask::SYSTEM); k.step(Tick(5), CapabilityMask::SYSTEM); k
876 };
877 let k1 = build();
878 let original_tip = k1.wal_chain_tip().unwrap();
879 let wal = k1.export_wal().unwrap();
880
881 let mut k2 = Kernel::new_with_wal(world(), [0u8; 32]);
882 k2.register_action::<ScheduleTwoSameTick>();
883 k2.register_action::<SpawnAction>();
884 let _ = k2.create_instance(priv_cfg());
885 let report = replay_into(&mut k2, &wal).expect("replay ok");
886 assert_eq!(report.final_chain_tip, original_tip);
887 }
888
889 #[test]
893 fn signal_routing_replays_bit_identical() {
894 let build = || -> Kernel {
895 let mut k = Kernel::new_with_wal(world(), [0u8; 32]);
896 k.register_action::<SignalAction>();
897 let a = k.create_instance(priv_cfg()); let _b = k.create_instance(priv_cfg()); let act = SignalAction { target: 2 };
900 k.submit(
901 a,
902 Principal::System,
903 None,
904 CapabilityMask::SYSTEM,
905 Tick(0),
906 SignalAction::TYPE_CODE,
907 Action::canonical_bytes(&act),
908 )
909 .unwrap();
910 k.step(Tick(0), CapabilityMask::SYSTEM);
911 k
912 };
913 let k1 = build();
914 let original_tip = k1.wal_chain_tip().unwrap();
915 let wal = k1.export_wal().unwrap();
916
917 let mut k2 = Kernel::new_with_wal(world(), [0u8; 32]);
918 k2.register_action::<SignalAction>();
919 let _ = k2.create_instance(priv_cfg());
920 let _ = k2.create_instance(priv_cfg());
921 let report = replay_into(&mut k2, &wal).expect("replay ok");
922 assert_eq!(report.final_chain_tip, original_tip);
923 }
924
925 #[test]
929 fn manifest_digest_validated_on_default_path() {
930 let mut k1 = Kernel::new_with_wal(world(), [0xAB; 32]);
931 k1.register_action::<SpawnAction>();
932 let inst = k1.create_instance(priv_cfg());
933 let a = SpawnAction { id: 1 };
934 k1.submit(
935 inst,
936 Principal::System,
937 None,
938 CapabilityMask::SYSTEM,
939 Tick(0),
940 SpawnAction::TYPE_CODE,
941 Action::canonical_bytes(&a),
942 )
943 .unwrap();
944 k1.step(Tick(0), CapabilityMask::SYSTEM);
945 let wal = k1.export_wal().unwrap();
946
947 let mut k2 = Kernel::new_with_wal(world(), [0xCD; 32]);
949 k2.register_action::<SpawnAction>();
950 let _ = k2.create_instance(priv_cfg());
951 assert!(matches!(
952 replay_into(&mut k2, &wal),
953 Err(ReplayError::ManifestDigestMismatch { .. })
954 ));
955
956 let mut k3 = Kernel::new_with_wal(world(), [0xAB; 32]);
958 k3.register_action::<SpawnAction>();
959 let _ = k3.create_instance(priv_cfg());
960 assert!(replay_into(&mut k3, &wal).is_ok());
961 }
962
963 #[test]
969 fn replay_measures_not_copies_tip() {
970 let mut k1 = Kernel::new_with_wal(world(), [0u8; 32]);
971 k1.register_action::<SpawnAction>();
972 let inst = k1.create_instance(priv_cfg());
973 let a = SpawnAction { id: 5 };
974 k1.submit(
975 inst,
976 Principal::System,
977 None,
978 CapabilityMask::SYSTEM,
979 Tick(0),
980 SpawnAction::TYPE_CODE,
981 Action::canonical_bytes(&a),
982 )
983 .unwrap();
984 k1.step(Tick(0), CapabilityMask::SYSTEM);
985 let wal = k1.export_wal().unwrap();
986
987 let mut k2 = Kernel::new_with_wal(world(), [0u8; 32]);
991 let _ = k2.create_instance(priv_cfg());
992 assert!(matches!(
993 replay_into(&mut k2, &wal),
994 Err(ReplayError::VerdictDivergence { .. })
995 ));
996 }
997
998 #[test]
1002 fn replay_into_kernel_missing_instance_is_graceful_error() {
1003 let mut k1 = Kernel::new_with_wal(world(), [0u8; 32]);
1004 k1.register_action::<SpawnAction>();
1005 let inst = k1.create_instance(priv_cfg());
1006 let a = SpawnAction { id: 1 };
1007 k1.submit(
1008 inst,
1009 Principal::System,
1010 None,
1011 CapabilityMask::SYSTEM,
1012 Tick(0),
1013 SpawnAction::TYPE_CODE,
1014 Action::canonical_bytes(&a),
1015 )
1016 .unwrap();
1017 k1.step(Tick(0), CapabilityMask::SYSTEM);
1018 let wal = k1.export_wal().unwrap();
1019
1020 let mut k2 = Kernel::new_with_wal(world(), [0u8; 32]);
1023 k2.register_action::<SpawnAction>();
1024 let result = replay_into(&mut k2, &wal);
1025 assert!(
1026 matches!(result, Err(ReplayError::SubmitFailed(_))),
1027 "missing instance must surface as a graceful ReplayError, got {result:?}"
1028 );
1029 }
1030}