use super::checkpoint::{
CanonicalInput, KernelCheckpoint, LogicalKernelState, LogicalStateProjection,
};
use super::config::ConfigDefaults;
use super::driver::{CanonicalOperationDriver, PlannedStep};
use super::effect::{Digest, KernelEffect};
use super::fault::{KernelFault, KernelFaultCode};
use super::record::{KernelRecord, NormalizedInput, canonical_bytes, canonical_digest};
use super::terminal::KernelTerminal;
use super::transaction::{InMemoryRecordIndex, KernelTransaction, RecordIndex};
pub struct RestoredOperation<Index = InMemoryRecordIndex> {
pub transaction: KernelTransaction<PlannedStep, Index>,
pub driver: CanonicalOperationDriver,
pub cost: RestoreCost,
}
impl<Index: RecordIndex> std::fmt::Debug for RestoredOperation<Index> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RestoredOperation")
.field("head", &self.transaction.head())
.field("lifecycle", &self.transaction.lifecycle())
.field("cost", &self.cost)
.finish()
}
}
impl<Index: RecordIndex> RestoredOperation<Index> {
pub fn pending_effects(&self) -> Vec<KernelEffect> {
self.transaction.pending_effects().cloned().collect()
}
pub fn terminal(&self) -> Option<&KernelTerminal> {
self.transaction.terminal()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RestoreCost {
pub records_before_checkpoint: u64,
pub tail_inputs_replayed: u64,
pub records_after_checkpoint: u64,
pub bytes_read: u64,
}
impl RestoreCost {
pub fn total_transitions(&self) -> u64 {
self.records_before_checkpoint + self.tail_inputs_replayed + self.records_after_checkpoint
}
}
pub fn restore_operation<Index: RecordIndex>(
checkpoint: Option<&KernelCheckpoint>,
records: &[KernelRecord],
defaults: ConfigDefaults,
index: Index,
) -> Result<RestoredOperation<Index>, KernelFault> {
let Some(checkpoint) = checkpoint else {
return restore_from_genesis(records, defaults, index);
};
checkpoint.verify().map_err(|error| error.fault())?;
let state = checkpoint.logical_state();
let mut driver =
CanonicalOperationDriver::restore_logical_state(&state.transition.resolved_config, state)?;
let mut transaction = KernelTransaction::restore_from_checkpoint(checkpoint, defaults, index)?;
verify_restored_state(&transaction, &driver, checkpoint)?;
let mut cost = RestoreCost::default();
for entry in checkpoint.tail_inputs() {
cost.tail_inputs_replayed += 1;
cost.bytes_read += canonical_bytes(&entry.input)
.map(|bytes| bytes.len() as u64)
.unwrap_or(0);
replay_tail_entry(&mut transaction, &mut driver, entry)?;
}
replay_records(&mut transaction, &mut driver, records, &mut cost, false)?;
Ok(RestoredOperation {
transaction,
driver,
cost,
})
}
fn restore_from_genesis<Index: RecordIndex>(
records: &[KernelRecord],
defaults: ConfigDefaults,
index: Index,
) -> Result<RestoredOperation<Index>, KernelFault> {
let mut driver = CanonicalOperationDriver::new();
let transaction =
KernelTransaction::rebuild_from_records(records, defaults, index, |context| {
driver.fold(context)
})?;
let cost = RestoreCost {
records_before_checkpoint: records.len() as u64,
tail_inputs_replayed: 0,
records_after_checkpoint: 0,
bytes_read: records
.iter()
.map(|record| record.record_bytes().len() as u64)
.sum(),
};
Ok(RestoredOperation {
transaction,
driver,
cost,
})
}
fn verify_restored_state<Index: RecordIndex>(
transaction: &KernelTransaction<PlannedStep, Index>,
driver: &CanonicalOperationDriver,
checkpoint: &KernelCheckpoint,
) -> Result<(), KernelFault> {
let reprojected = project(transaction, driver)?;
let digest = state_digest(&reprojected)?;
if &digest != checkpoint.state_digest() {
return Err(KernelFault::new(
KernelFaultCode::CheckpointCorrupted,
format!(
"the restored logical state hashes to {digest}, but the checkpoint at step {} \
captured {}; the restore would replay its tail onto a different history",
checkpoint.base_step_seq(),
checkpoint.state_digest()
),
));
}
Ok(())
}
fn project<Index: RecordIndex>(
transaction: &KernelTransaction<PlannedStep, Index>,
driver: &CanonicalOperationDriver,
) -> Result<LogicalKernelState, KernelFault> {
let LogicalStateProjection {
root_kind,
focus,
syscall,
scheduler,
context_vm,
} = driver.project_logical_state();
Ok(LogicalKernelState {
transition: transaction.transition_state_for_restore(root_kind, focus)?,
syscall,
scheduler,
context_vm,
})
}
fn state_digest(state: &LogicalKernelState) -> Result<Digest, KernelFault> {
canonical_bytes(state)
.map(|bytes| canonical_digest(bytes.as_slice()))
.map_err(|error| {
KernelFault::new(
KernelFaultCode::CheckpointCorrupted,
error.message().to_string(),
)
})
}
fn replay_tail_entry<Index: RecordIndex>(
transaction: &mut KernelTransaction<PlannedStep, Index>,
driver: &mut CanonicalOperationDriver,
entry: &CanonicalInput,
) -> Result<(), KernelFault> {
replay_one(transaction, driver, &entry.input, &entry.record_digest)
}
fn replay_records<Index: RecordIndex>(
transaction: &mut KernelTransaction<PlannedStep, Index>,
driver: &mut CanonicalOperationDriver,
records: &[KernelRecord],
cost: &mut RestoreCost,
below_checkpoint: bool,
) -> Result<(), KernelFault> {
for record in records {
if below_checkpoint {
cost.records_before_checkpoint += 1;
} else {
cost.records_after_checkpoint += 1;
}
cost.bytes_read += record.record_bytes().len() as u64;
let input = record.normalized_input().map_err(|error| {
KernelFault::new(
KernelFaultCode::RecordCorrupted,
error.message().to_string(),
)
})?;
replay_one(transaction, driver, &input, record.record_digest())?;
}
Ok(())
}
fn replay_one<Index: RecordIndex>(
transaction: &mut KernelTransaction<PlannedStep, Index>,
driver: &mut CanonicalOperationDriver,
input: &NormalizedInput,
expected: &Digest,
) -> Result<(), KernelFault> {
transaction.replay_committed(input, expected, &mut |context| driver.fold(context))?;
Ok(())
}