Skip to main content

arkhe_kernel/persist/
replay.rs

1//! WAL → Kernel replay (A1 D1-Total bit-identical reconstruction).
2//!
3//! Replay is the from-fresh-state path: the caller re-creates the
4//! instances referenced by the WAL with matching configs before calling
5//! `replay_into`. The snapshot path (`KernelSnapshot` plus
6//! `Kernel::from_snapshot`) is the alternative — restore from a
7//! point-in-time blob without re-running history.
8
9use crate::abi::CapabilityMask;
10use crate::runtime::Kernel;
11
12use super::wal::{Wal, WalError, WalHeader};
13
14/// Aggregated outcome of [`replay_into`].
15#[derive(Debug, Default, Clone, PartialEq, Eq)]
16pub struct ReplayReport {
17    /// Number of WAL records consumed.
18    pub records_replayed: u32,
19    /// Sum of `effects_applied` across all replayed steps.
20    pub total_effects_applied: u32,
21    /// Sum of `effects_denied` across all replayed steps.
22    pub total_effects_denied: u32,
23    /// Chain tip after the final replayed record (matches the
24    /// pre-replay export when the replay is bit-identical).
25    pub final_chain_tip: [u8; 32],
26}
27
28/// Failure modes for [`replay_into`].
29#[derive(Debug, Clone)]
30#[non_exhaustive]
31pub enum ReplayError {
32    /// WAL header magic doesn't match `WalHeader::MAGIC`.
33    HeaderIncompatible(String),
34    /// `kernel_semver` differs between WAL header and the running kernel.
35    KernelSemverMismatch {
36        /// Semver pinned in the WAL header.
37        expected: (u16, u16, u16),
38        /// Current running kernel semver.
39        got: (u16, u16, u16),
40    },
41    /// `abi_version` differs between WAL header and the running kernel.
42    AbiVersionMismatch {
43        /// ABI version pinned in the WAL header.
44        expected: (u16, u16),
45        /// Current running kernel ABI version.
46        got: (u16, u16),
47    },
48    /// Underlying WAL chain/signature verification failure.
49    WalCorrupted(WalError),
50    /// `Kernel::submit` failed during replay (carries the formatted
51    /// upstream error).
52    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
87/// Replay every record into `kernel`. The caller must already have
88/// created the instances referenced by the WAL; for the integrated
89/// path (no manual pre-creation), use `Kernel::from_snapshot` against
90/// a `KernelSnapshot` instead.
91pub 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}