Skip to main content

arkhe_kernel/persist/
snapshot.rs

1//! `KernelSnapshot` — serializable point-in-time capture of kernel state.
2//!
3//! Snapshot is **independent** from the WAL: WAL stores history (full
4//! replay from a fresh state), snapshot stores a single state at tick
5//! N. Hybrid recovery (snapshot at N + WAL from N+1) is the future
6//! integration; they ship as orthogonal mechanisms.
7//!
8//! What snapshot includes:
9//! - All instances (entities, components, scheduler, ledger, id_counters,
10//!   inflight_refs, wall_remainder, local_tick).
11//! - Kernel-level `next_instance_id` counter.
12//!
13//! What snapshot **excludes**:
14//! - `Box<dyn KernelObserver>` (not Serialize).
15//! - `ActionRegistry` (fn pointers, not Serialize).
16//! - Attached `WalWriter` (independent persistence layer).
17//!
18//! After `Kernel::from_snapshot(...)`, the caller must re-register every
19//! Action that was active when the snapshot was taken, and re-attach
20//! observers/WAL as needed.
21//!
22//! Determinism (A1): identical kernel state produces identical snapshot
23//! bytes — postcard-canonical encoding + BTreeMap iteration (A5).
24
25use std::collections::BTreeMap;
26
27use serde::{Deserialize, Serialize};
28
29use crate::abi::InstanceId;
30use crate::state::instance::InstanceSnapshot;
31
32/// Opaque point-in-time snapshot of kernel state.
33///
34/// Pub struct, pub(crate) fields — external callers see the type at the
35/// API boundary and can hold a value, but cannot inspect its internals
36/// (use [`serialize`](Self::serialize) / [`deserialize`](Self::deserialize)
37/// for round-trip persistence). Fed to
38/// [`Kernel::from_snapshot`](crate::Kernel::from_snapshot) for restore.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct KernelSnapshot {
41    pub(crate) instances: BTreeMap<InstanceId, InstanceSnapshot>,
42    pub(crate) next_instance_id: u64,
43}
44
45impl KernelSnapshot {
46    /// Encode to canonical postcard bytes.
47    pub fn serialize(&self) -> Result<Vec<u8>, SnapshotError> {
48        postcard::to_allocvec(self).map_err(|e| SnapshotError::SerializeFailed(format!("{}", e)))
49    }
50
51    /// Decode from canonical postcard bytes.
52    ///
53    /// After decoding, structural invariants are validated (currently the
54    /// scheduler three-table index consistency of every captured instance)
55    /// so a corrupt or tampered snapshot is rejected here rather than
56    /// triggering a latent panic during a later replay/step. This trusts
57    /// the *provenance* of the bytes — for an untrusted source, gate them
58    /// through [`deserialize_verified`](Self::deserialize_verified) first.
59    pub fn deserialize(bytes: &[u8]) -> Result<Self, SnapshotError> {
60        let snap: Self = postcard::from_bytes(bytes)
61            .map_err(|e| SnapshotError::DeserializeFailed(format!("{}", e)))?;
62        snap.validate_structure()?;
63        Ok(snap)
64    }
65
66    /// Decode from canonical postcard bytes, first verifying their
67    /// integrity against a caller-supplied BLAKE3 digest obtained
68    /// out-of-band (recorded when the snapshot was taken, or distributed
69    /// over a trusted channel). Use this for snapshots from an untrusted
70    /// source: it closes the gap whereby [`deserialize`](Self::deserialize)
71    /// alone trusts whatever bytes it is handed. The kernel supplies the
72    /// *mechanism* (digest comparison); the caller owns the *policy* of
73    /// which digest to trust.
74    pub fn deserialize_verified(
75        bytes: &[u8],
76        expected_digest: &[u8; 32],
77    ) -> Result<Self, SnapshotError> {
78        if blake3::hash(bytes).as_bytes() != expected_digest {
79            return Err(SnapshotError::IntegrityMismatch);
80        }
81        Self::deserialize(bytes)
82    }
83
84    /// BLAKE3 digest of a snapshot's canonical bytes — the value a caller
85    /// records out-of-band to later verify integrity via
86    /// [`deserialize_verified`](Self::deserialize_verified).
87    pub fn digest(bytes: &[u8]) -> [u8; 32] {
88        *blake3::hash(bytes).as_bytes()
89    }
90
91    /// Validate structural invariants of every captured instance (scheduler
92    /// three-table index consistency). Rejects a corrupt/tampered snapshot
93    /// before it can drive a latent panic during replay/step.
94    fn validate_structure(&self) -> Result<(), SnapshotError> {
95        for (id, inst) in &self.instances {
96            inst.scheduler
97                .validate()
98                .map_err(|reason| SnapshotError::Inconsistent(format!("instance {:?}: {}", id, reason)))?;
99        }
100        Ok(())
101    }
102
103    /// Number of instances captured.
104    pub fn instance_count(&self) -> usize {
105        self.instances.len()
106    }
107
108    /// Iterate captured instance ids in canonical (`InstanceId` ascending) order.
109    pub fn instance_ids(&self) -> impl Iterator<Item = InstanceId> + '_ {
110        self.instances.keys().copied()
111    }
112
113    /// Crate-internal constructor used by `Kernel::snapshot`.
114    #[doc(hidden)]
115    pub fn __construct(
116        instances: BTreeMap<InstanceId, InstanceSnapshot>,
117        next_instance_id: u64,
118    ) -> Self {
119        Self {
120            instances,
121            next_instance_id,
122        }
123    }
124
125    /// Crate-internal destructor used by `Kernel::from_snapshot`.
126    #[doc(hidden)]
127    pub fn __into_parts(self) -> (BTreeMap<InstanceId, InstanceSnapshot>, u64) {
128        (self.instances, self.next_instance_id)
129    }
130}
131
132/// Snapshot postcard round-trip failures.
133#[non_exhaustive]
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum SnapshotError {
136    /// Postcard refused to encode the snapshot.
137    SerializeFailed(String),
138    /// Postcard refused to decode the bytes.
139    DeserializeFailed(String),
140    /// The bytes decoded, but a structural invariant (e.g. scheduler index
141    /// consistency) did not hold — the snapshot is corrupt or tampered.
142    Inconsistent(String),
143    /// The bytes' BLAKE3 digest did not match the caller-supplied expected
144    /// digest — the integrity/authenticity check failed.
145    IntegrityMismatch,
146}
147
148impl core::fmt::Display for SnapshotError {
149    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
150        match self {
151            Self::SerializeFailed(m) => write!(f, "snapshot serialize failed: {}", m),
152            Self::DeserializeFailed(m) => write!(f, "snapshot deserialize failed: {}", m),
153            Self::Inconsistent(m) => write!(f, "snapshot structural validation failed: {}", m),
154            Self::IntegrityMismatch => write!(f, "snapshot integrity digest mismatch"),
155        }
156    }
157}
158
159impl std::error::Error for SnapshotError {}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::abi::{CapabilityMask, EntityId, Principal, Tick, TypeCode};
165    use crate::state::traits::_sealed::Sealed;
166    use crate::state::{ActionCompute, ActionContext, ActionDeriv, InstanceConfig, Op};
167    use crate::Kernel;
168    use bytes::Bytes;
169    use serde::{Deserialize as De, Serialize as Ser};
170
171    /// Test action: spawn entity `id` and attach a 4-byte component
172    /// under TypeCode(7).
173    #[derive(Ser, De)]
174    struct SpawnSetAction {
175        id: u64,
176    }
177    impl Sealed for SpawnSetAction {}
178    impl ActionDeriv for SpawnSetAction {
179        const TYPE_CODE: TypeCode = TypeCode(900);
180        const SCHEMA_VERSION: u32 = 1;
181    }
182    impl ActionCompute for SpawnSetAction {
183        fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
184            let entity = EntityId::new(self.id).unwrap();
185            vec![
186                Op::SpawnEntity {
187                    id: entity,
188                    owner: Principal::System,
189                },
190                Op::SetComponent {
191                    entity,
192                    type_code: TypeCode(7),
193                    bytes: Bytes::from(vec![0xCDu8; 4]),
194                    size: 4,
195                },
196            ]
197        }
198    }
199
200    fn submit(k: &mut Kernel, inst: InstanceId, id: u64) {
201        use crate::state::Action;
202        let bytes = Action::canonical_bytes(&SpawnSetAction { id });
203        k.submit(
204            inst,
205            Principal::System,
206            None,
207            Tick(0),
208            SpawnSetAction::TYPE_CODE,
209            bytes,
210        )
211        .unwrap();
212    }
213
214    fn boot_with_state(actions: &[u64]) -> (Kernel, InstanceId) {
215        let mut k = Kernel::new();
216        k.register_action::<SpawnSetAction>();
217        let inst = k.create_instance(InstanceConfig::default());
218        for id in actions {
219            submit(&mut k, inst, *id);
220            let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
221        }
222        (k, inst)
223    }
224
225    #[test]
226    fn snapshot_empty_kernel_serdes_roundtrip() {
227        let k = Kernel::new();
228        let snap = k.snapshot();
229        assert_eq!(snap.instance_count(), 0);
230        let bytes = snap.serialize().unwrap();
231        let back = KernelSnapshot::deserialize(&bytes).unwrap();
232        assert_eq!(back.instance_count(), 0);
233    }
234
235    #[test]
236    fn snapshot_captures_instance_state() {
237        let (k, inst) = boot_with_state(&[1, 2, 3]);
238        let snap = k.snapshot();
239        assert_eq!(snap.instance_count(), 1);
240        let ids: Vec<InstanceId> = snap.instance_ids().collect();
241        assert_eq!(ids, vec![inst]);
242    }
243
244    #[test]
245    fn snapshot_preserves_entities_and_components() {
246        let (k1, inst) = boot_with_state(&[1, 2]);
247        let snap = k1.snapshot();
248        let bytes = snap.serialize().unwrap();
249        let snap2 = KernelSnapshot::deserialize(&bytes).unwrap();
250        let mut k2 = Kernel::from_snapshot(snap2);
251        // Caller MUST re-register the action types they care about; for
252        // the read assertions below this isn't required.
253
254        let v1 = k1.instance_view(inst).unwrap();
255        let v2 = k2.instance_view(inst).unwrap();
256        assert_eq!(v1.entity_count(), v2.entity_count());
257        assert_eq!(v1.component_count(), v2.component_count());
258        assert_eq!(
259            v1.component(EntityId::new(1).unwrap(), TypeCode(7)),
260            v2.component(EntityId::new(1).unwrap(), TypeCode(7)),
261        );
262        assert_eq!(
263            v1.component(EntityId::new(2).unwrap(), TypeCode(7)),
264            v2.component(EntityId::new(2).unwrap(), TypeCode(7)),
265        );
266        // After from_snapshot, k2 is mutable; verify we can re-register the
267        // action and submit on the restored kernel without panic.
268        k2.register_action::<SpawnSetAction>();
269        submit(&mut k2, inst, 3);
270        let _ = k2.step(Tick(0), CapabilityMask::SYSTEM);
271        assert_eq!(k2.instance_view(inst).unwrap().entity_count(), 3);
272    }
273
274    #[test]
275    fn snapshot_preserves_id_counters() {
276        // create_instance increments next_instance_id; verify the round
277        // trip produces a kernel that issues the same next id.
278        let mut k1 = Kernel::new();
279        let _ = k1.create_instance(InstanceConfig::default());
280        let _ = k1.create_instance(InstanceConfig::default());
281        let _ = k1.create_instance(InstanceConfig::default()); // next_instance_id = 3
282        let snap = k1.snapshot();
283        let bytes = snap.serialize().unwrap();
284        let snap2 = KernelSnapshot::deserialize(&bytes).unwrap();
285        let mut k2 = Kernel::from_snapshot(snap2);
286        // Both kernels' next create_instance returns id=4.
287        let next1 = k1.create_instance(InstanceConfig::default());
288        let next2 = k2.create_instance(InstanceConfig::default());
289        assert_eq!(next1, next2);
290        assert_eq!(next1.get(), 4);
291    }
292
293    #[test]
294    fn snapshot_preserves_local_tick_and_wall_remainder() {
295        // Round-trip a kernel that has progressed; the restored kernel's
296        // view exposes the same local_tick. (apply_stage doesn't auto-
297        // advance local_tick — both readings should be 0.)
298        let (k1, inst) = boot_with_state(&[1]);
299        let snap = k1.snapshot();
300        let bytes = snap.serialize().unwrap();
301        let k2 = Kernel::from_snapshot(KernelSnapshot::deserialize(&bytes).unwrap());
302        assert_eq!(
303            k1.instance_view(inst).unwrap().local_tick(),
304            k2.instance_view(inst).unwrap().local_tick(),
305        );
306    }
307
308    #[test]
309    fn snapshot_deserialize_fresh_kernel_no_observers_no_registry() {
310        // After from_snapshot, observer count is 0 and action registry is
311        // empty. Submitting an unknown TypeCode silently skips (per
312        // existing kernel semantics).
313        let (k1, inst) = boot_with_state(&[1]);
314        let snap = k1.snapshot();
315        let mut k2 =
316            Kernel::from_snapshot(KernelSnapshot::deserialize(&snap.serialize().unwrap()).unwrap());
317        assert_eq!(k2.stats().observer_count, 0);
318        // Action registry empty: submit + step skips the action (no
319        // deserializer registered).
320        k2.submit(
321            inst,
322            Principal::System,
323            None,
324            Tick(0),
325            TypeCode(900),
326            vec![1u8],
327        )
328        .unwrap();
329        let report = k2.step(Tick(0), CapabilityMask::SYSTEM);
330        assert_eq!(report.actions_executed, 1);
331        assert_eq!(report.effects_applied, 0);
332    }
333
334    #[test]
335    fn snapshot_deserialize_verified_checks_digest() {
336        let (k, _) = boot_with_state(&[1, 2]);
337        let bytes = k.snapshot().serialize().unwrap();
338        let digest = KernelSnapshot::digest(&bytes);
339        // Correct digest → ok.
340        assert!(KernelSnapshot::deserialize_verified(&bytes, &digest).is_ok());
341        // Tampered digest → IntegrityMismatch (no decode attempted).
342        let mut bad = digest;
343        bad[0] ^= 0xFF;
344        assert!(matches!(
345            KernelSnapshot::deserialize_verified(&bytes, &bad),
346            Err(SnapshotError::IntegrityMismatch),
347        ));
348        // Tampered bytes under the original digest → IntegrityMismatch.
349        let mut bad_bytes = bytes.clone();
350        bad_bytes[0] ^= 0xFF;
351        assert!(matches!(
352            KernelSnapshot::deserialize_verified(&bad_bytes, &digest),
353            Err(SnapshotError::IntegrityMismatch),
354        ));
355    }
356
357    #[test]
358    fn snapshot_deterministic_same_state_same_bytes() {
359        // A1 D1 extension: identical kernel state ⇒ identical snapshot bytes.
360        // Build two independent kernels through the same sequence of
361        // operations and compare the postcard outputs byte-for-byte.
362        let (k1, _) = boot_with_state(&[1, 2, 3]);
363        let (k2, _) = boot_with_state(&[1, 2, 3]);
364        let b1 = k1.snapshot().serialize().unwrap();
365        let b2 = k2.snapshot().serialize().unwrap();
366        assert_eq!(
367            b1, b2,
368            "identical state must produce identical snapshot bytes"
369        );
370    }
371}