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, memory_manager::MemoryId};
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
155            .memory_manager
156            .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
157        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
158        match decode_stable_cell_ledger_record_from_memory(&memory) {
159            Ok(record) => StableCellDiagnostic {
160                diagnostic: DiagnosticStableCell::new(
161                    if memory_size.wasm_pages == 0 {
162                        DiagnosticStableCellStatus::Empty
163                    } else {
164                        DiagnosticStableCellStatus::Readable
165                    },
166                    memory_size,
167                ),
168                record: Some(record),
169            },
170            Err(err) => StableCellDiagnostic {
171                diagnostic: DiagnosticStableCell::new(
172                    DiagnosticStableCellStatus::Corrupt {
173                        failure: DiagnosticFailure::new(
174                            DiagnosticCode::StableCell,
175                            err.to_string(),
176                        ),
177                    },
178                    memory_size,
179                ),
180                record: None,
181            },
182        }
183    }
184}
185
186struct StableCellDiagnostic {
187    diagnostic: DiagnosticStableCell,
188    record: Option<StableCellLedgerRecord>,
189}
190
191fn diagnostic_validation<P: AllocationPolicy>(
192    declarations: &SealedDeclarationSnapshot,
193    custom_policy: &P,
194    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
195) -> DiagnosticCheck
196where
197    P::Error: Display,
198{
199    let recovered = match diagnostic_validation_ledger(recovered) {
200        Ok(recovered) => recovered,
201        Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
202    };
203    let resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
204        Ok(resolved) => resolved,
205        Err(err) => {
206            return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
207        }
208    };
209    let policy = super::policy::RuntimeMemoryManagerPolicy {
210        declarations: &resolved,
211        custom_policy,
212    };
213    match crate::validation::check_allocations(&recovered, resolved.allocation_snapshot(), &policy)
214    {
215        Ok(()) => DiagnosticCheck::passed(),
216        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
217    }
218}
219
220fn diagnostic_bootstrap_binding(
221    tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
222    tested_declaration_fingerprint: SealedDeclarationFingerprint,
223    established: Option<&DiagnosticRuntimeBinding>,
224) -> DiagnosticCheck {
225    let tested_policy_identity = match tested_policy_identity {
226        Ok(identity) => identity,
227        Err(failure) => {
228            return DiagnosticCheck::not_run(failure.code, failure.message.clone());
229        }
230    };
231    let Some(established) = established else {
232        return DiagnosticCheck::not_run(
233            DiagnosticCode::RuntimeBinding,
234            "runtime has not completed bootstrap",
235        );
236    };
237    if &established.policy_identity == tested_policy_identity
238        && established.declaration_fingerprint == tested_declaration_fingerprint
239    {
240        return DiagnosticCheck::passed();
241    }
242    DiagnosticCheck::failed(
243        DiagnosticCode::RuntimeBinding,
244        format!(
245            "tested policy/declaration binding differs from established runtime binding: \
246             tested_policy={tested_policy_identity:?}, \
247             tested_declarations={tested_declaration_fingerprint:?}, \
248             established={established:?}"
249        ),
250    )
251}
252
253pub(super) fn diagnostic_validation_ledger(
254    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
255) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
256    if let Some(Ok(recovered)) = recovered {
257        return Ok(Cow::Borrowed(recovered));
258    }
259    if let Some(Err(err)) = recovered {
260        // Protected recovery returns NoValidGeneration only for two absent slots.
261        if matches!(
262            err,
263            LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
264        ) {
265            return Ok(Cow::Owned(RecoveredLedger::from_trusted_ledger(
266                AllocationLedger::empty_genesis(),
267            )));
268        }
269        let code = if matches!(
270            err,
271            LedgerCommitError::PayloadEnvelope(
272                LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
273            )
274        ) {
275            DiagnosticCode::UnsupportedFormat
276        } else {
277            DiagnosticCode::LedgerRecovery
278        };
279        return Err(DiagnosticFailure::new(
280            code,
281            format!("protected ledger recovery: {err}"),
282        ));
283    }
284    Err(DiagnosticFailure::new(
285        DiagnosticCode::StableCell,
286        "stable-cell ledger record is not readable",
287    ))
288}