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        Ok(self.recovered_diagnostic_export(&recovered, Some(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<CommitStoreDiagnostic, 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        policy: &P,
50    ) -> MemoryRuntimeDoctorReport
51    where
52        P: RuntimeBootstrapPolicy,
53        P::Error: Display,
54    {
55        let stable_cell = self.stable_cell_diagnostic();
56        let (recovered, commit_recovery) = stable_cell
57            .record
58            .as_ref()
59            .map(|record| record.store().recover_with_diagnostic())
60            .map_or((None, None), |(recovered, diagnostic)| {
61                (Some(recovered), Some(diagnostic))
62            });
63        let recovered_for_export = recovered.as_ref().and_then(|result| result.as_ref().ok());
64        let ledger = recovered_for_export
65            .map(|recovered| self.recovered_diagnostic_export(recovered, commit_recovery));
66        let diagnostic_declarations = declarations
67            .registered_declarations()
68            .iter()
69            .map(|registration| {
70                DiagnosticDeclaration::new(
71                    registration.authority(),
72                    registration.declaration().clone(),
73                )
74            })
75            .collect();
76        let registered_records = declarations
77            .registered_ranges()
78            .iter()
79            .map(|registration| registration.record().clone())
80            .collect();
81        let range_authority = DiagnosticRangeAuthority::new(
82            registered_records,
83            declarations.range_authority().clone(),
84        );
85        let tested_policy_identity = policy
86            .runtime_bootstrap_identity()
87            .map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
88        let tested_declaration_fingerprint = declarations.fingerprint();
89        let established_bootstrap_binding = self.established_bootstrap_binding();
90        let bootstrap_binding = diagnostic_bootstrap_binding(
91            &tested_policy_identity,
92            tested_declaration_fingerprint,
93            established_bootstrap_binding.as_ref(),
94        );
95        let validation = match &tested_policy_identity {
96            Ok(_) => diagnostic_validation(declarations, policy, recovered.as_ref()),
97            Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
98        };
99
100        MemoryRuntimeDoctorReport {
101            bootstrapped: self.is_bootstrapped(),
102            tested_policy_identity,
103            tested_declaration_fingerprint,
104            established_bootstrap_binding,
105            bootstrap_binding,
106            ledger_anchor: ledger_anchor_descriptor(),
107            stable_cell: stable_cell.diagnostic,
108            commit_recovery,
109            ledger,
110            registered_declarations: diagnostic_declarations,
111            range_authority,
112            validation,
113        }
114    }
115
116    fn recovered_diagnostic_export(
117        &self,
118        recovered: &RecoveredLedger,
119        commit_recovery: Option<CommitStoreDiagnostic>,
120    ) -> DiagnosticExport {
121        let mut export =
122            DiagnosticExport::from_ledger(recovered.ledger(), ledger_anchor_descriptor());
123        export.commit_recovery = commit_recovery;
124        for record in &mut export.records {
125            let id = record
126                .allocation
127                .slot()
128                .memory_manager_id()
129                .expect("recovered ledger slot");
130            record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(
131                self.memory(id).size(),
132            ));
133        }
134        export
135    }
136
137    fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
138        match &self.lifecycle {
139            RuntimeLifecycle::Unbootstrapped => None,
140            RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
141                binding.policy_identity.clone(),
142                binding.source.fingerprint(),
143            )),
144        }
145    }
146
147    fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
148        let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
149        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
150        if memory.size() == 0 {
151            return StableCellDiagnostic {
152                diagnostic: DiagnosticStableCell::new(
153                    DiagnosticStableCellStatus::Empty,
154                    memory_size,
155                ),
156                record: Some(StableCellLedgerRecord::default()),
157            };
158        }
159
160        match decode_stable_cell_ledger_record_from_memory(&memory) {
161            Ok(record) => StableCellDiagnostic {
162                diagnostic: DiagnosticStableCell::new(
163                    DiagnosticStableCellStatus::Readable,
164                    memory_size,
165                ),
166                record: Some(record),
167            },
168            Err(err) => StableCellDiagnostic {
169                diagnostic: DiagnosticStableCell::new(
170                    DiagnosticStableCellStatus::Corrupt {
171                        failure: DiagnosticFailure::new(
172                            DiagnosticCode::StableCell,
173                            err.to_string(),
174                        ),
175                    },
176                    memory_size,
177                ),
178                record: None,
179            },
180        }
181    }
182}
183
184struct StableCellDiagnostic {
185    diagnostic: DiagnosticStableCell,
186    record: Option<StableCellLedgerRecord>,
187}
188
189const fn ledger_anchor_descriptor() -> AllocationSlotDescriptor {
190    AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
191}
192
193fn diagnostic_validation<P: AllocationPolicy>(
194    declarations: &SealedDeclarationSnapshot,
195    custom_policy: &P,
196    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
197) -> DiagnosticCheck
198where
199    P::Error: Display,
200{
201    let recovered = match diagnostic_validation_ledger(recovered) {
202        Ok(recovered) => recovered,
203        Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
204    };
205    let resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
206        Ok(resolved) => resolved,
207        Err(err) => {
208            return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
209        }
210    };
211    let policy = super::policy::RuntimeMemoryManagerPolicy {
212        declarations: &resolved,
213        custom_policy,
214    };
215    match crate::validation::check_allocations(&recovered, resolved.allocation_snapshot(), &policy)
216    {
217        Ok(()) => DiagnosticCheck::passed(),
218        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
219    }
220}
221
222fn diagnostic_bootstrap_binding(
223    tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
224    tested_declaration_fingerprint: SealedDeclarationFingerprint,
225    established: Option<&DiagnosticRuntimeBinding>,
226) -> DiagnosticCheck {
227    let tested_policy_identity = match tested_policy_identity {
228        Ok(identity) => identity,
229        Err(failure) => {
230            return DiagnosticCheck::not_run(failure.code, failure.message.clone());
231        }
232    };
233    let Some(established) = established else {
234        return DiagnosticCheck::not_run(
235            DiagnosticCode::RuntimeBinding,
236            "runtime has not completed bootstrap",
237        );
238    };
239    if &established.policy_identity == tested_policy_identity
240        && established.declaration_fingerprint == tested_declaration_fingerprint
241    {
242        return DiagnosticCheck::passed();
243    }
244    DiagnosticCheck::failed(
245        DiagnosticCode::RuntimeBinding,
246        format!(
247            "tested policy/declaration binding differs from established runtime binding: \
248             tested_policy={tested_policy_identity:?}, \
249             tested_declarations={tested_declaration_fingerprint:?}, \
250             established={established:?}"
251        ),
252    )
253}
254
255pub(super) fn diagnostic_validation_ledger(
256    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
257) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
258    if let Some(Ok(recovered)) = recovered {
259        return Ok(Cow::Borrowed(recovered));
260    }
261    if let Some(Err(err)) = recovered {
262        // Protected recovery returns NoValidGeneration only for two absent slots.
263        if matches!(
264            err,
265            LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
266        ) {
267            return Ok(Cow::Owned(RecoveredLedger::from_trusted_ledger(
268                AllocationLedger::empty_genesis(),
269            )));
270        }
271        let code = if matches!(
272            err,
273            LedgerCommitError::PayloadEnvelope(
274                LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
275            )
276        ) {
277            DiagnosticCode::UnsupportedFormat
278        } else {
279            DiagnosticCode::LedgerRecovery
280        };
281        return Err(DiagnosticFailure::new(
282            code,
283            format!("protected ledger recovery: {err}"),
284        ));
285    }
286    Err(DiagnosticFailure::new(
287        DiagnosticCode::StableCell,
288        "stable-cell ledger record is not readable",
289    ))
290}