Skip to main content

ic_memory/runtime/
diagnostics.rs

1use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
2use crate::{
3    AllocationLedger, AllocationPolicy, AllocationSlotDescriptor, DiagnosticCheck, DiagnosticCode,
4    DiagnosticDeclaration, DiagnosticExport, DiagnosticFailure, DiagnosticMemorySize,
5    DiagnosticRangeAuthority, DiagnosticRuntimeBinding, DiagnosticStableCell,
6    DiagnosticStableCellStatus, LedgerCommitError, LedgerPayloadEnvelopeError,
7    MemoryRuntimeDoctorReport, PolicyIdentity, RecoveredLedger, RuntimeBootstrapPolicy,
8    StableCellLedgerRecord,
9    physical::CommitStoreDiagnostic,
10    registry::{SealedDeclarationFingerprint, SealedDeclarationSnapshot},
11    slot::MEMORY_MANAGER_LEDGER_ID,
12    stable_cell::decode_stable_cell_ledger_record_from_memory,
13};
14use ic_stable_structures::Memory;
15use std::{borrow::Cow, fmt::Display};
16
17impl<M: Memory> MemoryRuntime<M> {
18    /// Export this runtime's recovered ledger and live virtual-memory sizes.
19    pub fn diagnostic_export(&self) -> Result<DiagnosticExport, RuntimeDiagnosticError> {
20        if !self.is_bootstrapped() {
21            return Err(RuntimeDiagnosticError::NotBootstrapped);
22        }
23        let record = self.ledger_record_from_memory()?;
24        let (recovered, commit_recovery) = record.store().recover_with_diagnostic();
25        let recovered = recovered?;
26        let ledger = recovered.ledger();
27        Ok(
28            DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
29                ledger,
30                ledger_anchor_descriptor(),
31                Some(commit_recovery),
32                self.memory_sizes(&recovered),
33            ),
34        )
35    }
36
37    /// Diagnose protected commit recovery from this runtime's ledger memory.
38    ///
39    /// This operation is available before bootstrap when the stable-cell
40    /// envelope is readable or the ledger memory is empty.
41    pub fn commit_recovery_diagnostic(
42        &self,
43    ) -> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
44        let record = self.ledger_record_from_memory()?;
45        Ok(record.store().physical().diagnostic())
46    }
47
48    /// Build preflight and lifecycle diagnostics for this runtime.
49    ///
50    /// Validation checks the supplied declarations and allocation policy only.
51    /// It does not execute `prepare_bootstrap`, predict its completed set, or
52    /// certify consumer admission. Diagnostics never replay preparation.
53    #[must_use]
54    pub fn doctor_report<P>(
55        &self,
56        declarations: &SealedDeclarationSnapshot,
57        policy: &P,
58    ) -> MemoryRuntimeDoctorReport
59    where
60        P: RuntimeBootstrapPolicy,
61        P::Error: Display,
62    {
63        let stable_cell = self.stable_cell_diagnostic();
64        let (recovered, commit_recovery) = stable_cell
65            .record
66            .as_ref()
67            .map(|record| record.store().recover_with_diagnostic())
68            .map_or((None, None), |(recovered, diagnostic)| {
69                (Some(recovered), Some(diagnostic))
70            });
71        let recovered_for_export = recovered.as_ref().and_then(|result| result.as_ref().ok());
72        let ledger = recovered_for_export.map(|recovered| {
73            DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
74                recovered.ledger(),
75                ledger_anchor_descriptor(),
76                commit_recovery,
77                self.memory_sizes(recovered),
78            )
79        });
80        let diagnostic_declarations = declarations
81            .registered_declarations()
82            .iter()
83            .map(|registration| {
84                DiagnosticDeclaration::new(
85                    registration.authority(),
86                    registration.declaration().clone(),
87                )
88            })
89            .collect();
90        let registered_records = declarations
91            .registered_ranges()
92            .iter()
93            .map(|registration| registration.record().clone())
94            .collect();
95        let range_authority = DiagnosticRangeAuthority::new(
96            registered_records,
97            declarations.range_authority().clone(),
98        );
99        let tested_policy_identity = policy
100            .runtime_bootstrap_identity()
101            .map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
102        let tested_declaration_fingerprint = declarations.fingerprint();
103        let established_bootstrap_binding = self.established_bootstrap_binding();
104        let bootstrap_binding = diagnostic_bootstrap_binding(
105            &tested_policy_identity,
106            tested_declaration_fingerprint,
107            established_bootstrap_binding.as_ref(),
108        );
109        let validation = match &tested_policy_identity {
110            Ok(_) => diagnostic_validation(declarations, policy, recovered.as_ref()),
111            Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
112        };
113
114        MemoryRuntimeDoctorReport {
115            bootstrapped: self.is_bootstrapped(),
116            tested_policy_identity,
117            tested_declaration_fingerprint,
118            established_bootstrap_binding,
119            bootstrap_binding,
120            ledger_anchor: ledger_anchor_descriptor(),
121            stable_cell: stable_cell.diagnostic,
122            commit_recovery,
123            ledger,
124            registered_declarations: diagnostic_declarations,
125            range_authority,
126            validation,
127        }
128    }
129
130    fn memory_sizes<'a>(
131        &'a self,
132        recovered: &'a RecoveredLedger,
133    ) -> impl Iterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)> + 'a {
134        recovered
135            .ledger()
136            .allocation_history()
137            .records()
138            .iter()
139            .map(move |record| {
140                let id = record
141                    .slot()
142                    .memory_manager_id()
143                    .expect("recovered ledger slot");
144                (
145                    record.slot().clone(),
146                    DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
147                )
148            })
149    }
150
151    fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
152        match &self.lifecycle {
153            RuntimeLifecycle::Unbootstrapped => None,
154            RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
155                binding.policy_identity.clone(),
156                binding.source.fingerprint(),
157            )),
158        }
159    }
160
161    fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
162        let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
163        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
164        if memory.size() == 0 {
165            return StableCellDiagnostic {
166                diagnostic: DiagnosticStableCell::new(
167                    DiagnosticStableCellStatus::Empty,
168                    memory_size,
169                ),
170                record: Some(StableCellLedgerRecord::default()),
171            };
172        }
173
174        match decode_stable_cell_ledger_record_from_memory(&memory) {
175            Ok(record) => StableCellDiagnostic {
176                diagnostic: DiagnosticStableCell::new(
177                    DiagnosticStableCellStatus::Readable,
178                    memory_size,
179                ),
180                record: Some(record),
181            },
182            Err(err) => StableCellDiagnostic {
183                diagnostic: DiagnosticStableCell::new(
184                    DiagnosticStableCellStatus::Corrupt {
185                        failure: DiagnosticFailure::new(
186                            DiagnosticCode::StableCell,
187                            err.to_string(),
188                        ),
189                    },
190                    memory_size,
191                ),
192                record: None,
193            },
194        }
195    }
196}
197
198struct StableCellDiagnostic {
199    diagnostic: DiagnosticStableCell,
200    record: Option<StableCellLedgerRecord>,
201}
202
203const fn ledger_anchor_descriptor() -> AllocationSlotDescriptor {
204    AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
205}
206
207fn diagnostic_validation<P: AllocationPolicy>(
208    declarations: &SealedDeclarationSnapshot,
209    custom_policy: &P,
210    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
211) -> DiagnosticCheck
212where
213    P::Error: Display,
214{
215    let recovered = match diagnostic_validation_ledger(recovered) {
216        Ok(recovered) => recovered,
217        Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
218    };
219    let resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
220        Ok(resolved) => resolved,
221        Err(err) => {
222            return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
223        }
224    };
225    let policy = super::policy::RuntimeMemoryManagerPolicy {
226        declarations: &resolved,
227        custom_policy,
228    };
229    match crate::validation::check_allocations(&recovered, resolved.allocation_snapshot(), &policy)
230    {
231        Ok(()) => DiagnosticCheck::passed(),
232        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
233    }
234}
235
236fn diagnostic_bootstrap_binding(
237    tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
238    tested_declaration_fingerprint: SealedDeclarationFingerprint,
239    established: Option<&DiagnosticRuntimeBinding>,
240) -> DiagnosticCheck {
241    let tested_policy_identity = match tested_policy_identity {
242        Ok(identity) => identity,
243        Err(failure) => {
244            return DiagnosticCheck::not_run(failure.code, failure.message.clone());
245        }
246    };
247    let Some(established) = established else {
248        return DiagnosticCheck::not_run(
249            DiagnosticCode::RuntimeBinding,
250            "runtime has not completed bootstrap",
251        );
252    };
253    if &established.policy_identity == tested_policy_identity
254        && established.declaration_fingerprint == tested_declaration_fingerprint
255    {
256        return DiagnosticCheck::passed();
257    }
258    DiagnosticCheck::failed(
259        DiagnosticCode::RuntimeBinding,
260        format!(
261            "tested policy/declaration binding differs from established runtime binding: \
262             tested_policy={tested_policy_identity:?}, \
263             tested_declarations={tested_declaration_fingerprint:?}, \
264             established={established:?}"
265        ),
266    )
267}
268
269pub(super) fn diagnostic_validation_ledger(
270    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
271) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
272    if let Some(Ok(recovered)) = recovered {
273        return Ok(Cow::Borrowed(recovered));
274    }
275    if let Some(Err(err)) = recovered {
276        // Protected recovery returns NoValidGeneration only for two absent slots.
277        if matches!(
278            err,
279            LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
280        ) {
281            return Ok(Cow::Owned(RecoveredLedger::from_trusted_ledger(
282                AllocationLedger::empty_genesis(),
283            )));
284        }
285        let code = if matches!(
286            err,
287            LedgerCommitError::PayloadEnvelope(
288                LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
289            )
290        ) {
291            DiagnosticCode::UnsupportedFormat
292        } else {
293            DiagnosticCode::LedgerRecovery
294        };
295        return Err(DiagnosticFailure::new(
296            code,
297            format!("protected ledger recovery: {err}"),
298        ));
299    }
300    Err(DiagnosticFailure::new(
301        DiagnosticCode::StableCell,
302        "stable-cell ledger record is not readable",
303    ))
304}