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