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::{TrustAnchor, 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    /// `postcard_version` differs between WAL header and the running build.
49    PostcardVersionMismatch {
50        /// Postcard major pinned in the WAL header.
51        expected: u32,
52        /// Current running postcard major.
53        got: u32,
54    },
55    /// `blake3_version` differs between WAL header and the running build.
56    Blake3VersionMismatch {
57        /// BLAKE3 major pinned in the WAL header.
58        expected: u32,
59        /// Current running BLAKE3 major.
60        got: u32,
61    },
62    /// Underlying WAL chain/signature verification failure.
63    WalCorrupted(WalError),
64    /// `Kernel::submit` failed during replay (carries the formatted
65    /// upstream error).
66    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
111/// Header-compatibility gates shared by [`replay_into`] and
112/// [`replay_into_verified`] (A14): magic, kernel semver major, ABI
113/// version, postcard / BLAKE3 major. A mismatch is a structural error.
114fn 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    // postcard / BLAKE3 major versions are wire-format determinants (A14
133    // header pinning): a mismatch means the bytes were produced under a
134    // different codec/hash generation and must NOT be silently accepted.
135    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
150/// Replay the (already chain-verified) records into `kernel`.
151fn replay_records(kernel: &mut Kernel, wal: &Wal) -> Result<ReplayReport, ReplayError> {
152    let mut report = ReplayReport::default();
153    for rec in &wal.records {
154        // Preserve the EXACT recorded bits (including L2-defined high bits,
155        // which `caps.rs` documents as legitimate) rather than truncating
156        // to kernel-known bits. Truncation made the write side (full u64)
157        // and replay side disagree, so a re-recorded `caps_bits` diverged
158        // and broke A1 bit-identical chain reconstruction.
159        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
188/// Replay every record into `kernel` (integrity-only). The caller must
189/// already have created the instances referenced by the WAL; for the
190/// integrated path (no manual pre-creation), use `Kernel::from_snapshot`
191/// against a `KernelSnapshot` instead.
192///
193/// This verifies the chain's internal self-consistency but TRUSTS the
194/// WAL's provenance — it derives the verification policy/keys and the
195/// chain `world_id` from the (potentially attacker-controlled) header. For
196/// an untrusted WAL (tampered log / peer snapshot), use
197/// [`replay_into_verified`] with a [`TrustAnchor`].
198pub 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
204/// Replay every record into `kernel`, authenticating the WAL against a
205/// caller-supplied [`TrustAnchor`] and a caller-supplied `world_id` (NOT
206/// read from the untrusted header). Rejects a tier downgrade, a
207/// verifying-key substitution, a manifest mismatch, and a tail truncation
208/// before any record is applied. Use this for WAL bytes from an untrusted
209/// source.
210pub 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}