1use crate::abi::CapabilityMask;
10use crate::runtime::Kernel;
11
12use super::wal::{TrustAnchor, 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 PostcardVersionMismatch {
50 expected: u32,
52 got: u32,
54 },
55 Blake3VersionMismatch {
57 expected: u32,
59 got: u32,
61 },
62 WalCorrupted(WalError),
64 SubmitFailed(String),
67}
68
69impl From<WalError> for ReplayError {
70 fn from(e: WalError) -> Self {
71 Self::WalCorrupted(e)
72 }
73}
74
75impl core::fmt::Display for ReplayError {
76 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
77 match self {
78 Self::HeaderIncompatible(m) => write!(f, "wal header incompatible: {}", m),
79 Self::KernelSemverMismatch { expected, got } => {
80 write!(
81 f,
82 "kernel semver mismatch: expected {:?}, got {:?}",
83 expected, got
84 )
85 }
86 Self::AbiVersionMismatch { expected, got } => {
87 write!(
88 f,
89 "abi version mismatch: expected {:?}, got {:?}",
90 expected, got
91 )
92 }
93 Self::PostcardVersionMismatch { expected, got } => {
94 write!(
95 f,
96 "postcard version mismatch: expected {}, got {}",
97 expected, got
98 )
99 }
100 Self::Blake3VersionMismatch { expected, got } => {
101 write!(f, "blake3 version mismatch: expected {}, got {}", expected, got)
102 }
103 Self::WalCorrupted(e) => write!(f, "wal corrupted: {}", e),
104 Self::SubmitFailed(m) => write!(f, "submit failed: {}", m),
105 }
106 }
107}
108
109impl std::error::Error for ReplayError {}
110
111fn check_header_gates(wal: &Wal) -> Result<(), ReplayError> {
115 if wal.header.magic != WalHeader::MAGIC {
116 return Err(ReplayError::HeaderIncompatible(
117 "magic mismatch (expected ARKHEWAL)".to_string(),
118 ));
119 }
120 if wal.header.kernel_semver.0 != WalHeader::CURRENT_KERNEL_SEMVER.0 {
121 return Err(ReplayError::KernelSemverMismatch {
122 expected: WalHeader::CURRENT_KERNEL_SEMVER,
123 got: wal.header.kernel_semver,
124 });
125 }
126 if wal.header.abi_version != WalHeader::ABI_VERSION {
127 return Err(ReplayError::AbiVersionMismatch {
128 expected: WalHeader::ABI_VERSION,
129 got: wal.header.abi_version,
130 });
131 }
132 if wal.header.postcard_version != WalHeader::POSTCARD_MAJOR {
136 return Err(ReplayError::PostcardVersionMismatch {
137 expected: WalHeader::POSTCARD_MAJOR,
138 got: wal.header.postcard_version,
139 });
140 }
141 if wal.header.blake3_version != WalHeader::BLAKE3_MAJOR {
142 return Err(ReplayError::Blake3VersionMismatch {
143 expected: WalHeader::BLAKE3_MAJOR,
144 got: wal.header.blake3_version,
145 });
146 }
147 Ok(())
148}
149
150fn replay_records(kernel: &mut Kernel, wal: &Wal) -> Result<ReplayReport, ReplayError> {
152 let mut report = ReplayReport::default();
153 for rec in &wal.records {
154 let caps = CapabilityMask::from_bits_retain(rec.caps_bits);
160 let principal = match &rec.principal {
161 crate::abi::Principal::Unauthenticated => crate::abi::Principal::Unauthenticated,
162 crate::abi::Principal::External(e) => crate::abi::Principal::External(*e),
163 crate::abi::Principal::System => crate::abi::Principal::System,
164 };
165 kernel
166 .submit(
167 rec.instance,
168 principal,
169 rec.actor,
170 rec.at,
171 rec.action_type_code,
172 rec.action_bytes.clone(),
173 )
174 .map_err(|e| ReplayError::SubmitFailed(format!("{:?}", e)))?;
175 let step_report = kernel.step(rec.at, caps);
176 report.records_replayed = report.records_replayed.saturating_add(1);
177 report.total_effects_applied = report
178 .total_effects_applied
179 .saturating_add(step_report.effects_applied);
180 report.total_effects_denied = report
181 .total_effects_denied
182 .saturating_add(step_report.effects_denied);
183 }
184 report.final_chain_tip = wal.chain_tip();
185 Ok(report)
186}
187
188pub fn replay_into(kernel: &mut Kernel, wal: &Wal) -> Result<ReplayReport, ReplayError> {
199 check_header_gates(wal)?;
200 wal.verify_chain(wal.header.world_id)?;
201 replay_records(kernel, wal)
202}
203
204pub fn replay_into_verified(
211 kernel: &mut Kernel,
212 wal: &Wal,
213 world_id: [u8; 32],
214 anchor: &TrustAnchor,
215) -> Result<ReplayReport, ReplayError> {
216 check_header_gates(wal)?;
217 wal.verify_chain_anchored(world_id, anchor)?;
218 replay_records(kernel, wal)
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::abi::Tick;
225 use crate::persist::wal::{AuthDecisionAnnotation, Wal, WalWriter};
226
227 fn world() -> [u8; 32] {
228 [11u8; 32]
229 }
230
231 #[test]
232 fn replay_empty_wal_succeeds() {
233 let w = WalWriter::new(world(), [0u8; 32]);
234 let wal = Wal::from_writer(w);
235 let mut kernel = Kernel::new();
236 let report = replay_into(&mut kernel, &wal).unwrap();
237 assert_eq!(report.records_replayed, 0);
238 }
239
240 #[test]
241 fn replay_rejects_wrong_magic() {
242 let w = WalWriter::new(world(), [0u8; 32]);
243 let mut wal = Wal::from_writer(w);
244 wal.header.magic = *b"BADMAGIC";
245 let mut kernel = Kernel::new();
246 let result = replay_into(&mut kernel, &wal);
247 assert!(matches!(result, Err(ReplayError::HeaderIncompatible(_))));
248 }
249
250 #[test]
251 fn replay_rejects_kernel_semver_major_mismatch() {
252 let w = WalWriter::new(world(), [0u8; 32]);
253 let mut wal = Wal::from_writer(w);
254 wal.header.kernel_semver = (99, 0, 0);
255 let mut kernel = Kernel::new();
256 let result = replay_into(&mut kernel, &wal);
257 assert!(matches!(
258 result,
259 Err(ReplayError::KernelSemverMismatch { .. })
260 ));
261 }
262
263 #[test]
264 fn replay_rejects_postcard_version_mismatch() {
265 let w = WalWriter::new(world(), [0u8; 32]);
266 let mut wal = Wal::from_writer(w);
267 wal.header.postcard_version = 999;
268 let mut kernel = Kernel::new();
269 assert!(matches!(
270 replay_into(&mut kernel, &wal),
271 Err(ReplayError::PostcardVersionMismatch { .. })
272 ));
273 }
274
275 #[test]
276 fn replay_rejects_corrupted_chain() {
277 let mut w = WalWriter::new(world(), [0u8; 32]);
278 w.append(
279 Tick(0),
280 crate::abi::InstanceId::new(1).unwrap(),
281 crate::abi::Principal::System,
282 None,
283 crate::abi::TypeCode(100),
284 vec![],
285 0,
286 crate::runtime::stage::StepStage::default(),
287 AuthDecisionAnnotation::AllAuthorized,
288 )
289 .unwrap();
290 let mut wal = Wal::from_writer(w);
291 wal.records[0].this_chain_hash = [0xFFu8; 32];
292 let mut kernel = Kernel::new();
293 let result = replay_into(&mut kernel, &wal);
294 assert!(matches!(result, Err(ReplayError::WalCorrupted(_))));
295 }
296}