Skip to main content

deepstrike_core/runtime/kernel/wire/
restore.rs

1//! §12.2 · bounded-tail restore.
2//!
3//! One entry point, [`restore_operation`], running the spec's ladder in the spec's order:
4//!
5//! ```text
6//! load latest checkpoint
7//! → verify checkpoint version/digests/operation/genesis/covered head
8//! → restore logical state
9//! → replay and verify checkpoint tail
10//! → load committed records after through_step_seq
11//! → verify record chain and step digests
12//! → replay bounded post-checkpoint tail
13//! → expose pending effects or terminal
14//! ```
15//!
16//! Three properties are load-bearing, and each is a reaction to how the historical resume path
17//! failed:
18//!
19//! 1. **There is no second state machine.** With a checkpoint or without one, the replay goes
20//!    through the same `prepare`/`commit` fold every live transition goes through, and the driver
21//!    plans every replayed input exactly as it planned it the first time. The retired recovery path
22//!    had a separate replay mode, and the two drifted. Here, "restore from genesis" is literally
23//!    `restore_operation` with no
24//!    checkpoint — [`KernelTransaction::rebuild_from_records`] — and nothing else.
25//! 2. **The restore verifies itself.** After the logical state is installed and before a single
26//!    tail input is replayed, the restored runtime is re-projected and the projection is digested.
27//!    It must equal the checkpoint's `state_digest`. A hydration that forgets a field therefore
28//!    produces a refusal instead of a subtly incomplete runtime.
29//! 3. **The cost is the tail, not the run.** [`RestoreCost`] counts what was read, and the count is
30//!    bounded by the tail bounds plus whatever the journal holds above `through_step_seq` — never
31//!    by how long the operation has been running. That is the property §12 exists for, and
32//!    `long_run_restore_cost_is_bounded_by_the_tail` measures it rather than asserting it.
33
34use super::checkpoint::{
35    CanonicalInput, KernelCheckpoint, LogicalKernelState, LogicalStateProjection,
36};
37use super::config::ConfigDefaults;
38use super::driver::{CanonicalOperationDriver, PlannedStep};
39use super::effect::{Digest, KernelEffect};
40use super::fault::{KernelFault, KernelFaultCode};
41use super::record::{KernelRecord, NormalizedInput, canonical_bytes, canonical_digest};
42use super::terminal::KernelTerminal;
43use super::transaction::{InMemoryRecordIndex, KernelTransaction, RecordIndex};
44
45/// A canonical runtime: the transaction and the driver that plans for it.
46///
47/// The pair is what a restore produces, because neither half is a runtime on its own — the
48/// transaction decides *whether* an input is accepted and the driver decides *what* it means, and
49/// §12.2's "behaves identically to one that was never interrupted" is a claim about both.
50pub struct RestoredOperation<Index = InMemoryRecordIndex> {
51    pub transaction: KernelTransaction<PlannedStep, Index>,
52    pub driver: CanonicalOperationDriver,
53    pub cost: RestoreCost,
54}
55
56impl<Index: RecordIndex> std::fmt::Debug for RestoredOperation<Index> {
57    /// Names what a restore *is* — where it landed and what it cost — without printing a whole
58    /// semantic engine into a test failure.
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("RestoredOperation")
61            .field("head", &self.transaction.head())
62            .field("lifecycle", &self.transaction.lifecycle())
63            .field("cost", &self.cost)
64            .finish()
65    }
66}
67
68impl<Index: RecordIndex> RestoredOperation<Index> {
69    /// §12.2 line 8 · the effects the restored operation is still waiting on.
70    ///
71    /// Republished on purpose (adjudication §5g-1): a record that reached the journal is a fact,
72    /// and the effects its step planned may never have been handed to a host — the crash could have
73    /// landed between the append and the publish. Effects are idempotent by `effect_id`, so
74    /// re-exposing one the host already ran costs a duplicate resolution that DEC-1 answers with a
75    /// `Replayed`; *not* re-exposing one that was never run strands the operation forever.
76    pub fn pending_effects(&self) -> Vec<KernelEffect> {
77        self.transaction.pending_effects().cloned().collect()
78    }
79
80    /// §12.2 line 8 · the terminal, if this operation already ended.
81    pub fn terminal(&self) -> Option<&KernelTerminal> {
82        self.transaction.terminal()
83    }
84}
85
86/// What a restore actually read.
87///
88/// A deterministic counter rather than a timer: the claim "restore is bounded by the tail" is about
89/// how much *history* is touched, and a wall-clock benchmark would measure the machine instead. The
90/// three counters are the three sources a restore can read from, and `records_before_checkpoint` is
91/// the one that must stay zero whenever a checkpoint exists.
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
93pub struct RestoreCost {
94    /// Journal records read from below the checkpoint's base. Zero with a checkpoint; the whole
95    /// journal without one.
96    pub records_before_checkpoint: u64,
97    /// Canonical inputs replayed out of the checkpoint's own bounded tail — `(base, through]`.
98    pub tail_inputs_replayed: u64,
99    /// Journal records replayed from above `through_step_seq`.
100    pub records_after_checkpoint: u64,
101    /// Bytes read across all three, for the byte axis of the tail bound.
102    pub bytes_read: u64,
103}
104
105impl RestoreCost {
106    pub fn total_transitions(&self) -> u64 {
107        self.records_before_checkpoint + self.tail_inputs_replayed + self.records_after_checkpoint
108    }
109}
110
111/// §12.2 · restore one operation from its latest checkpoint plus the records above it.
112///
113/// `checkpoint = None` is the no-checkpoint arm of §12.2's last line: the fold starts at genesis and
114/// runs the *same* path. `records` are the committed records **after** `through_step_seq` when a
115/// checkpoint is supplied, and the whole journal when one is not.
116pub fn restore_operation<Index: RecordIndex>(
117    checkpoint: Option<&KernelCheckpoint>,
118    records: &[KernelRecord],
119    defaults: ConfigDefaults,
120    index: Index,
121) -> Result<RestoredOperation<Index>, KernelFault> {
122    let Some(checkpoint) = checkpoint else {
123        return restore_from_genesis(records, defaults, index);
124    };
125
126    // ----- line 2 · verify version/digests/operation/genesis/covered head -----
127    //
128    // The version halves are enforced by the decoder, which cannot even construct a checkpoint of a
129    // revision this kernel does not read; the digest and coverage halves are re-run here because a
130    // checkpoint handed over as a value (rather than decoded from bytes) has not passed them yet.
131    checkpoint.verify().map_err(|error| error.fault())?;
132
133    // ----- line 3 · restore logical state -----
134    let state = checkpoint.logical_state();
135    let mut driver =
136        CanonicalOperationDriver::restore_logical_state(&state.transition.resolved_config, state)?;
137    let mut transaction = KernelTransaction::restore_from_checkpoint(checkpoint, defaults, index)?;
138
139    // The restore verifies itself before it replays anything: if the state that came back does not
140    // hash to the state that was captured, the tail would be replayed onto a *different* history
141    // and every record it produced would be wrong for a reason no later check could localise.
142    verify_restored_state(&transaction, &driver, checkpoint)?;
143
144    let mut cost = RestoreCost::default();
145
146    // ----- line 4 · replay and verify the checkpoint tail -----
147    for entry in checkpoint.tail_inputs() {
148        cost.tail_inputs_replayed += 1;
149        cost.bytes_read += canonical_bytes(&entry.input)
150            .map(|bytes| bytes.len() as u64)
151            .unwrap_or(0);
152        replay_tail_entry(&mut transaction, &mut driver, entry)?;
153    }
154
155    // ----- lines 5–7 · the post-checkpoint records -----
156    replay_records(&mut transaction, &mut driver, records, &mut cost, false)?;
157
158    Ok(RestoredOperation {
159        transaction,
160        driver,
161        cost,
162    })
163}
164
165/// §12.2 last line · no checkpoint, so the fold starts at genesis — through the same code.
166fn restore_from_genesis<Index: RecordIndex>(
167    records: &[KernelRecord],
168    defaults: ConfigDefaults,
169    index: Index,
170) -> Result<RestoredOperation<Index>, KernelFault> {
171    let mut driver = CanonicalOperationDriver::new();
172    let transaction =
173        KernelTransaction::rebuild_from_records(records, defaults, index, |context| {
174            driver.fold(context)
175        })?;
176    let cost = RestoreCost {
177        records_before_checkpoint: records.len() as u64,
178        tail_inputs_replayed: 0,
179        records_after_checkpoint: 0,
180        bytes_read: records
181            .iter()
182            .map(|record| record.record_bytes().len() as u64)
183            .sum(),
184    };
185    Ok(RestoredOperation {
186        transaction,
187        driver,
188        cost,
189    })
190}
191
192/// The self-check of §12.2 line 3: re-project the restored runtime and compare digests.
193fn verify_restored_state<Index: RecordIndex>(
194    transaction: &KernelTransaction<PlannedStep, Index>,
195    driver: &CanonicalOperationDriver,
196    checkpoint: &KernelCheckpoint,
197) -> Result<(), KernelFault> {
198    let reprojected = project(transaction, driver)?;
199    let digest = state_digest(&reprojected)?;
200    if &digest != checkpoint.state_digest() {
201        return Err(KernelFault::new(
202            KernelFaultCode::CheckpointCorrupted,
203            format!(
204                "the restored logical state hashes to {digest}, but the checkpoint at step {} \
205                 captured {}; the restore would replay its tail onto a different history",
206                checkpoint.base_step_seq(),
207                checkpoint.state_digest()
208            ),
209        ));
210    }
211    Ok(())
212}
213
214/// Project the pair back into the DTO the checkpoint stores. The mirror of a candidate, minus the
215/// header — which is what makes the digest comparable.
216fn project<Index: RecordIndex>(
217    transaction: &KernelTransaction<PlannedStep, Index>,
218    driver: &CanonicalOperationDriver,
219) -> Result<LogicalKernelState, KernelFault> {
220    let LogicalStateProjection {
221        root_kind,
222        focus,
223        syscall,
224        scheduler,
225        context_vm,
226    } = driver.project_logical_state();
227    Ok(LogicalKernelState {
228        transition: transaction.transition_state_for_restore(root_kind, focus)?,
229        syscall,
230        scheduler,
231        context_vm,
232    })
233}
234
235fn state_digest(state: &LogicalKernelState) -> Result<Digest, KernelFault> {
236    canonical_bytes(state)
237        .map(|bytes| canonical_digest(bytes.as_slice()))
238        .map_err(|error| {
239            KernelFault::new(
240                KernelFaultCode::CheckpointCorrupted,
241                error.message().to_string(),
242            )
243        })
244}
245
246/// §12.2 line 4 · replay one bounded-tail input and verify it reproduces the record it produced.
247fn replay_tail_entry<Index: RecordIndex>(
248    transaction: &mut KernelTransaction<PlannedStep, Index>,
249    driver: &mut CanonicalOperationDriver,
250    entry: &CanonicalInput,
251) -> Result<(), KernelFault> {
252    replay_one(transaction, driver, &entry.input, &entry.record_digest)
253}
254
255/// §12.2 lines 5–7 · fold the committed records above the checkpoint, verifying the chain.
256fn replay_records<Index: RecordIndex>(
257    transaction: &mut KernelTransaction<PlannedStep, Index>,
258    driver: &mut CanonicalOperationDriver,
259    records: &[KernelRecord],
260    cost: &mut RestoreCost,
261    below_checkpoint: bool,
262) -> Result<(), KernelFault> {
263    for record in records {
264        if below_checkpoint {
265            cost.records_before_checkpoint += 1;
266        } else {
267            cost.records_after_checkpoint += 1;
268        }
269        cost.bytes_read += record.record_bytes().len() as u64;
270
271        let input = record.normalized_input().map_err(|error| {
272            KernelFault::new(
273                KernelFaultCode::RecordCorrupted,
274                error.message().to_string(),
275            )
276        })?;
277        replay_one(transaction, driver, &input, record.record_digest())?;
278    }
279    Ok(())
280}
281
282/// One replayed transition, through the transaction's own replay primitive.
283///
284/// Both halves of the ladder land here, which is what makes the tail and the journal the *same*
285/// fold: the only difference between them is where the expected record digest came from.
286fn replay_one<Index: RecordIndex>(
287    transaction: &mut KernelTransaction<PlannedStep, Index>,
288    driver: &mut CanonicalOperationDriver,
289    input: &NormalizedInput,
290    expected: &Digest,
291) -> Result<(), KernelFault> {
292    transaction.replay_committed(input, expected, &mut |context| driver.fold(context))?;
293    Ok(())
294}