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