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