Skip to main content

ic_memory/runtime/
diagnostics.rs

1use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
2use crate::{
3    DiagnosticCheck, DiagnosticCode, DiagnosticExport, DiagnosticFailure, DiagnosticMemorySize,
4    DiagnosticRuntimeBinding, DiagnosticStableCell, DiagnosticStableCellStatus, LedgerCommitError,
5    LedgerCommitStore, MemoryRuntimeDoctorReport, PolicyIdentity, RecoveredLedger,
6    RuntimeBootstrapPolicy, physical::CommitSnapshotDiagnostic,
7    requests::SealedDeclarationSnapshot, slot::MEMORY_MANAGER_LEDGER_ID,
8    stable_cell::decode_stable_cell_commit_store_from_memory,
9};
10use ic_stable_structures::{Memory, memory_manager::MemoryId};
11use std::{borrow::Cow, fmt::Display};
12
13impl<M: Memory> MemoryRuntime<M> {
14    /// Export this runtime's recovered ledger and live virtual-memory sizes.
15    pub fn diagnostic_export(&self) -> Result<DiagnosticExport, RuntimeDiagnosticError> {
16        if !self.is_bootstrapped() {
17            return Err(RuntimeDiagnosticError::NotBootstrapped);
18        }
19        let recovered = self.ledger_record_from_memory()?.recover()?;
20        Ok(self.recovered_diagnostic_export(recovered))
21    }
22
23    /// Diagnose protected commit recovery from this runtime's ledger memory.
24    ///
25    /// This operation is available before bootstrap when the stable-cell
26    /// envelope is readable or the ledger memory is empty.
27    pub fn commit_recovery_diagnostic(
28        &self,
29    ) -> Result<CommitSnapshotDiagnostic, RuntimeDiagnosticError> {
30        let record = self.ledger_record_from_memory()?;
31        Ok(record.diagnostic())
32    }
33
34    /// Build preflight and lifecycle diagnostics for this runtime.
35    ///
36    /// Validation checks the supplied declarations and allocation policy only.
37    /// It does not execute `prepare_bootstrap`, predict its completed set, or
38    /// certify consumer admission. Diagnostics never replay preparation.
39    #[must_use]
40    pub fn doctor_report<P>(
41        &self,
42        declarations: &SealedDeclarationSnapshot,
43        pool: &crate::MemoryAllocationPool,
44        policy: &P,
45    ) -> MemoryRuntimeDoctorReport
46    where
47        P: RuntimeBootstrapPolicy,
48        P::Error: Display,
49    {
50        let stable_cell = self.stable_cell_diagnostic();
51        // Recovery owns its ledger and diagnostic evidence. Release the decoded
52        // physical snapshot before observing memory or invoking custom policy.
53        let recovery = stable_cell
54            .record
55            .map(|record| record.recover_with_diagnostic());
56        // Keep the original observation boundary before policy callbacks, without
57        // copying the recovered keys/records merely to retain those observations.
58        let mut observed_pages = [0; crate::constants::MAX_ALLOCATIONS];
59        if let Some((Ok(recovered), _)) = &recovery {
60            for (record, pages) in recovered.ledger().records().iter().zip(&mut observed_pages) {
61                *pages = self.memory_size_pages(record.slot().id());
62            }
63        }
64        let tested_policy_identity = policy.runtime_bootstrap_identity();
65        let established_bootstrap_binding = self.established_bootstrap_binding();
66        let bootstrap_binding = diagnostic_bootstrap_binding(
67            &tested_policy_identity,
68            declarations,
69            pool,
70            &self.lifecycle,
71        );
72        let validation = diagnostic_validation(
73            self,
74            declarations,
75            pool,
76            policy,
77            recovery.as_ref().map(|(recovered, _)| recovered),
78        );
79        let commit_recovery = recovery.as_ref().map(|(_, diagnostic)| *diagnostic);
80        let ledger = recovery.and_then(|(recovered, _)| {
81            recovered.ok().map(|recovered| {
82                let mut export = DiagnosticExport::from_owned_ledger(recovered.into_ledger());
83                for (record, pages) in export.records.iter_mut().zip(&observed_pages) {
84                    record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(*pages));
85                }
86                export
87            })
88        });
89
90        MemoryRuntimeDoctorReport {
91            bootstrapped: self.is_bootstrapped(),
92            tested_policy_identity,
93            established_bootstrap_binding,
94            bootstrap_binding,
95            stable_cell: stable_cell.diagnostic,
96            commit_recovery,
97            ledger,
98            requests: declarations.requests().to_vec(),
99            allocation_pool: pool.clone(),
100            validation,
101        }
102    }
103
104    fn recovered_diagnostic_export(&self, recovered: RecoveredLedger) -> DiagnosticExport {
105        let mut export = DiagnosticExport::from_owned_ledger(recovered.into_ledger());
106        for record in &mut export.records {
107            record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(
108                self.memory_size_pages(record.allocation.slot().id()),
109            ));
110        }
111        export
112    }
113
114    fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
115        match &self.lifecycle {
116            RuntimeLifecycle::Unbootstrapped => None,
117            RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
118                binding.policy_identity,
119                binding.pool.clone(),
120            )),
121        }
122    }
123
124    fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
125        let memory = self
126            .memory_manager
127            .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
128        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
129        match decode_stable_cell_commit_store_from_memory(&memory) {
130            Ok(record) => StableCellDiagnostic {
131                diagnostic: DiagnosticStableCell::new(
132                    if memory_size.wasm_pages == 0 {
133                        DiagnosticStableCellStatus::Empty
134                    } else {
135                        DiagnosticStableCellStatus::Readable
136                    },
137                    memory_size,
138                ),
139                record: Some(record),
140            },
141            Err(err) => StableCellDiagnostic {
142                diagnostic: DiagnosticStableCell::new(
143                    DiagnosticStableCellStatus::Corrupt {
144                        failure: DiagnosticFailure::new(
145                            DiagnosticCode::StableCell,
146                            err.to_string(),
147                        ),
148                    },
149                    memory_size,
150                ),
151                record: None,
152            },
153        }
154    }
155}
156
157struct StableCellDiagnostic {
158    diagnostic: DiagnosticStableCell,
159    record: Option<LedgerCommitStore>,
160}
161
162fn diagnostic_validation<M: Memory, P: RuntimeBootstrapPolicy>(
163    runtime: &MemoryRuntime<M>,
164    declarations: &SealedDeclarationSnapshot,
165    pool: &crate::MemoryAllocationPool,
166    custom_policy: &P,
167    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
168) -> DiagnosticCheck
169where
170    P::Error: Display,
171{
172    let recovered = match diagnostic_validation_ledger(recovered) {
173        Ok(recovered) => recovered,
174        Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
175    };
176    let occupied = recovered.ledger().occupied_slots();
177    if let Err(error) = runtime.validate_pool_custody(&occupied, pool) {
178        return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, error.to_string());
179    }
180    let resolved = match declarations.resolve(&recovered, &occupied, Vec::new(), pool) {
181        Ok(resolved) => resolved,
182        Err(err) => {
183            return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
184        }
185    };
186    match crate::validation::check_allocations(&recovered, &resolved, custom_policy) {
187        Ok(()) => DiagnosticCheck::passed(),
188        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
189    }
190}
191
192fn diagnostic_bootstrap_binding(
193    tested_policy_identity: &PolicyIdentity,
194    declarations: &SealedDeclarationSnapshot,
195    pool: &crate::MemoryAllocationPool,
196    lifecycle: &RuntimeLifecycle,
197) -> DiagnosticCheck {
198    let RuntimeLifecycle::Bootstrapped { binding, .. } = lifecycle else {
199        return DiagnosticCheck::not_run(
200            DiagnosticCode::RuntimeBinding,
201            "runtime has not completed bootstrap",
202        );
203    };
204    match binding.validate::<std::convert::Infallible>(declarations, pool, tested_policy_identity) {
205        Ok(()) => DiagnosticCheck::passed(),
206        Err(_) => DiagnosticCheck::failed(
207            DiagnosticCode::RuntimeBinding,
208            "tested policy/declaration/pool binding differs from established runtime binding",
209        ),
210    }
211}
212
213pub(super) fn diagnostic_validation_ledger(
214    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
215) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
216    if let Some(Ok(recovered)) = recovered {
217        return Ok(Cow::Borrowed(recovered));
218    }
219    if let Some(Err(err)) = recovered {
220        if let Some(genesis) = RecoveredLedger::genesis_for_absent_snapshot(err) {
221            return Ok(Cow::Owned(genesis));
222        }
223        let code = if matches!(
224            err,
225            LedgerCommitError::Recovery(crate::CommitRecoveryError::UnsupportedFormat { .. })
226        ) {
227            DiagnosticCode::UnsupportedFormat
228        } else {
229            DiagnosticCode::LedgerRecovery
230        };
231        return Err(DiagnosticFailure::new(
232            code,
233            format!("protected ledger recovery: {err}"),
234        ));
235    }
236    Err(DiagnosticFailure::new(
237        DiagnosticCode::StableCell,
238        "stable-cell ledger record is not readable",
239    ))
240}