Skip to main content

ic_memory/runtime/
diagnostics.rs

1use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
2use crate::{
3    AllocationLedger, AllocationPolicy, DiagnosticCheck, DiagnosticCode, DiagnosticDeclaration,
4    DiagnosticExport, DiagnosticFailure, DiagnosticMemorySize, DiagnosticRangeAuthority,
5    DiagnosticRuntimeBinding, DiagnosticStableCell, DiagnosticStableCellStatus, LedgerCommitError,
6    LedgerPayloadEnvelopeError, MemoryRuntimeDoctorReport, PolicyIdentity, RecoveredLedger,
7    RuntimeBootstrapPolicy, StableCellLedgerRecord,
8    physical::CommitStoreDiagnostic,
9    registry::{SealedDeclarationFingerprint, SealedDeclarationSnapshot},
10    slot::MEMORY_MANAGER_LEDGER_ID,
11    stable_cell::decode_stable_cell_ledger_record_from_memory,
12};
13use ic_stable_structures::Memory;
14use std::{borrow::Cow, fmt::Display};
15
16impl<M: Memory> MemoryRuntime<M> {
17    /// Export this runtime's recovered ledger and live virtual-memory sizes.
18    pub fn diagnostic_export(&self) -> Result<DiagnosticExport, RuntimeDiagnosticError> {
19        if !self.is_bootstrapped() {
20            return Err(RuntimeDiagnosticError::NotBootstrapped);
21        }
22        let (recovered, commit_recovery) = self
23            .ledger_record_from_memory()?
24            .store()
25            .recover_with_diagnostic();
26        let recovered = recovered?;
27        Ok(self.recovered_diagnostic_export(Cow::Owned(recovered), commit_recovery))
28    }
29
30    /// Diagnose protected commit recovery from this runtime's ledger memory.
31    ///
32    /// This operation is available before bootstrap when the stable-cell
33    /// envelope is readable or the ledger memory is empty.
34    pub fn commit_recovery_diagnostic(
35        &self,
36    ) -> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
37        let record = self.ledger_record_from_memory()?;
38        Ok(record.store().physical().diagnostic())
39    }
40
41    /// Build preflight and lifecycle diagnostics for this runtime.
42    ///
43    /// Validation checks the supplied declarations and allocation policy only.
44    /// It does not execute `prepare_bootstrap`, predict its completed set, or
45    /// certify consumer admission. Diagnostics never replay preparation.
46    #[must_use]
47    pub fn doctor_report<P>(
48        &self,
49        declarations: &SealedDeclarationSnapshot,
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 slots before projecting the report or invoking custom policy.
59        let recovery = stable_cell
60            .record
61            .map(|record| record.store().recover_with_diagnostic());
62        let ledger = recovery.as_ref().and_then(|(recovered, diagnostic)| {
63            recovered.as_ref().ok().map(|recovered| {
64                self.recovered_diagnostic_export(Cow::Borrowed(recovered), *diagnostic)
65            })
66        });
67        let diagnostic_declarations = declarations
68            .registered_declarations()
69            .iter()
70            .map(|registration| {
71                DiagnosticDeclaration::new(
72                    registration.authority(),
73                    registration.declaration().clone(),
74                )
75            })
76            .collect();
77        let registered_records = declarations
78            .registered_ranges()
79            .iter()
80            .map(|registration| registration.record().clone())
81            .collect();
82        let range_authority = DiagnosticRangeAuthority::new(
83            registered_records,
84            declarations.range_authority().clone(),
85        );
86        let tested_policy_identity = policy
87            .runtime_bootstrap_identity()
88            .map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
89        let tested_declaration_fingerprint = declarations.fingerprint();
90        let established_bootstrap_binding = self.established_bootstrap_binding();
91        let bootstrap_binding = diagnostic_bootstrap_binding(
92            &tested_policy_identity,
93            tested_declaration_fingerprint,
94            established_bootstrap_binding.as_ref(),
95        );
96        let validation = match &tested_policy_identity {
97            Ok(_) => diagnostic_validation(
98                declarations,
99                policy,
100                recovery.as_ref().map(|(recovered, _)| recovered),
101            ),
102            Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
103        };
104
105        MemoryRuntimeDoctorReport {
106            bootstrapped: self.is_bootstrapped(),
107            tested_policy_identity,
108            tested_declaration_fingerprint,
109            established_bootstrap_binding,
110            bootstrap_binding,
111            ledger_anchor: crate::slot::LEDGER_SLOT,
112            stable_cell: stable_cell.diagnostic,
113            commit_recovery: recovery.as_ref().map(|(_, diagnostic)| *diagnostic),
114            ledger,
115            registered_declarations: diagnostic_declarations,
116            range_authority,
117            validation,
118        }
119    }
120
121    fn recovered_diagnostic_export(
122        &self,
123        recovered: Cow<'_, RecoveredLedger>,
124        commit_recovery: CommitStoreDiagnostic,
125    ) -> DiagnosticExport {
126        let anchor = crate::slot::LEDGER_SLOT;
127        let mut export = match recovered {
128            Cow::Borrowed(recovered) => DiagnosticExport::from_ledger(recovered.ledger(), anchor),
129            Cow::Owned(recovered) => {
130                DiagnosticExport::from_owned_ledger(recovered.into_ledger(), anchor)
131            }
132        };
133        export.commit_recovery = Some(commit_recovery);
134        for record in &mut export.records {
135            let id = record.allocation.slot().id();
136            record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(
137                self.memory_size_pages(id),
138            ));
139        }
140        export
141    }
142
143    fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
144        match &self.lifecycle {
145            RuntimeLifecycle::Unbootstrapped => None,
146            RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
147                binding.policy_identity.clone(),
148                binding.source.fingerprint(),
149            )),
150        }
151    }
152
153    fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
154        let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
155        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
156        match decode_stable_cell_ledger_record_from_memory(&memory) {
157            Ok(record) => StableCellDiagnostic {
158                diagnostic: DiagnosticStableCell::new(
159                    if memory_size.wasm_pages == 0 {
160                        DiagnosticStableCellStatus::Empty
161                    } else {
162                        DiagnosticStableCellStatus::Readable
163                    },
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
189fn diagnostic_validation<P: AllocationPolicy>(
190    declarations: &SealedDeclarationSnapshot,
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 resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
202        Ok(resolved) => resolved,
203        Err(err) => {
204            return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
205        }
206    };
207    let policy = super::policy::RuntimeMemoryManagerPolicy {
208        declarations: &resolved,
209        custom_policy,
210    };
211    match crate::validation::check_allocations(&recovered, resolved.allocation_snapshot(), &policy)
212    {
213        Ok(()) => DiagnosticCheck::passed(),
214        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
215    }
216}
217
218fn diagnostic_bootstrap_binding(
219    tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
220    tested_declaration_fingerprint: SealedDeclarationFingerprint,
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    {
238        return DiagnosticCheck::passed();
239    }
240    DiagnosticCheck::failed(
241        DiagnosticCode::RuntimeBinding,
242        format!(
243            "tested policy/declaration binding differs from established runtime binding: \
244             tested_policy={tested_policy_identity:?}, \
245             tested_declarations={tested_declaration_fingerprint:?}, \
246             established={established:?}"
247        ),
248    )
249}
250
251pub(super) fn diagnostic_validation_ledger(
252    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
253) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
254    if let Some(Ok(recovered)) = recovered {
255        return Ok(Cow::Borrowed(recovered));
256    }
257    if let Some(Err(err)) = recovered {
258        // Protected recovery returns NoValidGeneration only for two absent slots.
259        if matches!(
260            err,
261            LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
262        ) {
263            return Ok(Cow::Owned(RecoveredLedger::from_trusted_ledger(
264                AllocationLedger::empty_genesis(),
265            )));
266        }
267        let code = if matches!(
268            err,
269            LedgerCommitError::PayloadEnvelope(
270                LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
271            )
272        ) {
273            DiagnosticCode::UnsupportedFormat
274        } else {
275            DiagnosticCode::LedgerRecovery
276        };
277        return Err(DiagnosticFailure::new(
278            code,
279            format!("protected ledger recovery: {err}"),
280        ));
281    }
282    Err(DiagnosticFailure::new(
283        DiagnosticCode::StableCell,
284        "stable-cell ledger record is not readable",
285    ))
286}