1use crate::abi::CapabilityMask;
10use crate::runtime::Kernel;
11
12use super::wal::{Wal, WalError, WalHeader};
13
14#[derive(Debug, Default, Clone, PartialEq, Eq)]
16pub struct ReplayReport {
17 pub records_replayed: u32,
19 pub total_effects_applied: u32,
21 pub total_effects_denied: u32,
23 pub final_chain_tip: [u8; 32],
26}
27
28#[derive(Debug, Clone)]
30#[non_exhaustive]
31pub enum ReplayError {
32 HeaderIncompatible(String),
34 KernelSemverMismatch {
36 expected: (u16, u16, u16),
38 got: (u16, u16, u16),
40 },
41 AbiVersionMismatch {
43 expected: (u16, u16),
45 got: (u16, u16),
47 },
48 WalCorrupted(WalError),
50 SubmitFailed(String),
53}
54
55impl From<WalError> for ReplayError {
56 fn from(e: WalError) -> Self {
57 Self::WalCorrupted(e)
58 }
59}
60
61impl core::fmt::Display for ReplayError {
62 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63 match self {
64 Self::HeaderIncompatible(m) => write!(f, "wal header incompatible: {}", m),
65 Self::KernelSemverMismatch { expected, got } => {
66 write!(
67 f,
68 "kernel semver mismatch: expected {:?}, got {:?}",
69 expected, got
70 )
71 }
72 Self::AbiVersionMismatch { expected, got } => {
73 write!(
74 f,
75 "abi version mismatch: expected {:?}, got {:?}",
76 expected, got
77 )
78 }
79 Self::WalCorrupted(e) => write!(f, "wal corrupted: {}", e),
80 Self::SubmitFailed(m) => write!(f, "submit failed: {}", m),
81 }
82 }
83}
84
85impl std::error::Error for ReplayError {}
86
87pub fn replay_into(kernel: &mut Kernel, wal: &Wal) -> Result<ReplayReport, ReplayError> {
92 if wal.header.magic != WalHeader::MAGIC {
93 return Err(ReplayError::HeaderIncompatible(
94 "magic mismatch (expected ARKHEWAL)".to_string(),
95 ));
96 }
97 if wal.header.kernel_semver.0 != WalHeader::CURRENT_KERNEL_SEMVER.0 {
98 return Err(ReplayError::KernelSemverMismatch {
99 expected: WalHeader::CURRENT_KERNEL_SEMVER,
100 got: wal.header.kernel_semver,
101 });
102 }
103 if wal.header.abi_version != WalHeader::ABI_VERSION {
104 return Err(ReplayError::AbiVersionMismatch {
105 expected: WalHeader::ABI_VERSION,
106 got: wal.header.abi_version,
107 });
108 }
109
110 wal.verify_chain(wal.header.world_id)?;
111
112 let mut report = ReplayReport::default();
113 for rec in &wal.records {
114 let caps = CapabilityMask::from_bits_truncate(rec.caps_bits);
115 let principal = match &rec.principal {
116 crate::abi::Principal::Unauthenticated => crate::abi::Principal::Unauthenticated,
117 crate::abi::Principal::External(e) => crate::abi::Principal::External(*e),
118 crate::abi::Principal::System => crate::abi::Principal::System,
119 };
120 kernel
121 .submit(
122 rec.instance,
123 principal,
124 None,
125 rec.at,
126 rec.action_type_code,
127 rec.action_bytes.clone(),
128 )
129 .map_err(|e| ReplayError::SubmitFailed(format!("{:?}", e)))?;
130 let step_report = kernel.step(rec.at, caps);
131 report.records_replayed = report.records_replayed.saturating_add(1);
132 report.total_effects_applied = report
133 .total_effects_applied
134 .saturating_add(step_report.effects_applied);
135 report.total_effects_denied = report
136 .total_effects_denied
137 .saturating_add(step_report.effects_denied);
138 }
139 report.final_chain_tip = wal.chain_tip();
140 Ok(report)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::abi::Tick;
147 use crate::persist::wal::{AuthDecisionAnnotation, Wal, WalWriter};
148
149 fn world() -> [u8; 32] {
150 [11u8; 32]
151 }
152
153 #[test]
154 fn replay_empty_wal_succeeds() {
155 let w = WalWriter::new(world(), [0u8; 32]);
156 let wal = Wal::from_writer(w);
157 let mut kernel = Kernel::new();
158 let report = replay_into(&mut kernel, &wal).unwrap();
159 assert_eq!(report.records_replayed, 0);
160 }
161
162 #[test]
163 fn replay_rejects_wrong_magic() {
164 let w = WalWriter::new(world(), [0u8; 32]);
165 let mut wal = Wal::from_writer(w);
166 wal.header.magic = *b"BADMAGIC";
167 let mut kernel = Kernel::new();
168 let result = replay_into(&mut kernel, &wal);
169 assert!(matches!(result, Err(ReplayError::HeaderIncompatible(_))));
170 }
171
172 #[test]
173 fn replay_rejects_kernel_semver_major_mismatch() {
174 let w = WalWriter::new(world(), [0u8; 32]);
175 let mut wal = Wal::from_writer(w);
176 wal.header.kernel_semver = (99, 0, 0);
177 let mut kernel = Kernel::new();
178 let result = replay_into(&mut kernel, &wal);
179 assert!(matches!(
180 result,
181 Err(ReplayError::KernelSemverMismatch { .. })
182 ));
183 }
184
185 #[test]
186 fn replay_rejects_corrupted_chain() {
187 let mut w = WalWriter::new(world(), [0u8; 32]);
188 w.append(
189 Tick(0),
190 crate::abi::InstanceId::new(1).unwrap(),
191 crate::abi::Principal::System,
192 crate::abi::TypeCode(100),
193 vec![],
194 0,
195 crate::runtime::stage::StepStage::default(),
196 AuthDecisionAnnotation::AllAuthorized,
197 )
198 .unwrap();
199 let mut wal = Wal::from_writer(w);
200 wal.records[0].this_chain_hash = [0xFFu8; 32];
201 let mut kernel = Kernel::new();
202 let result = replay_into(&mut kernel, &wal);
203 assert!(matches!(result, Err(ReplayError::WalCorrupted(_))));
204 }
205}