Skip to main content

ic_memory/runtime/
diagnostics.rs

1use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
2use crate::{
3    AllocationHistory, AllocationLedger, AllocationPolicy, AllocationSlotDescriptor,
4    DiagnosticCheck, DiagnosticCode, DiagnosticDeclaration, DiagnosticExport, DiagnosticFailure,
5    DiagnosticMemorySize, DiagnosticMemorySizeOutcome, DiagnosticRangeAuthority,
6    DiagnosticRuntimeBinding, DiagnosticStableCell, DiagnosticStableCellStatus, LedgerCommitError,
7    LedgerPayloadEnvelopeError, MemoryRuntimeDoctorReport, PolicyIdentity, 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(ledger)?,
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_size_outcomes(
74                recovered.ledger(),
75                ledger_anchor_descriptor(),
76                commit_recovery,
77                self.memory_size_outcomes(recovered.ledger()),
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(
111                declarations,
112                policy,
113                stable_cell.record.as_ref(),
114                recovered.as_ref(),
115            ),
116            Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
117        };
118
119        MemoryRuntimeDoctorReport {
120            bootstrapped: self.is_bootstrapped(),
121            tested_policy_identity,
122            tested_declaration_fingerprint,
123            established_bootstrap_binding,
124            bootstrap_binding,
125            ledger_anchor: ledger_anchor_descriptor(),
126            stable_cell: stable_cell.diagnostic,
127            commit_recovery,
128            ledger,
129            registered_declarations: diagnostic_declarations,
130            range_authority,
131            validation,
132        }
133    }
134
135    fn memory_sizes(
136        &self,
137        ledger: &AllocationLedger,
138    ) -> Result<Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)>, RuntimeDiagnosticError> {
139        ledger
140            .allocation_history()
141            .records()
142            .iter()
143            .map(|record| {
144                let id = record.slot().memory_manager_id()?;
145                Ok((
146                    record.slot().clone(),
147                    DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
148                ))
149            })
150            .collect()
151    }
152
153    pub(super) fn memory_size_outcomes(
154        &self,
155        ledger: &AllocationLedger,
156    ) -> Vec<(AllocationSlotDescriptor, DiagnosticMemorySizeOutcome)> {
157        ledger
158            .allocation_history()
159            .records()
160            .iter()
161            .map(|record| {
162                let outcome = match record.slot().memory_manager_id() {
163                    Ok(id) => DiagnosticMemorySizeOutcome::Measured(
164                        DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
165                    ),
166                    Err(err) => DiagnosticMemorySizeOutcome::Failed(DiagnosticFailure::new(
167                        DiagnosticCode::MemorySize,
168                        err.to_string(),
169                    )),
170                };
171                (record.slot().clone(), outcome)
172            })
173            .collect()
174    }
175
176    fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
177        match &self.lifecycle {
178            RuntimeLifecycle::Unbootstrapped => None,
179            RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
180                binding.policy_identity.clone(),
181                binding.source.fingerprint(),
182            )),
183        }
184    }
185
186    fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
187        let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
188        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
189        if memory.size() == 0 {
190            return StableCellDiagnostic {
191                diagnostic: DiagnosticStableCell::new(
192                    DiagnosticStableCellStatus::Empty,
193                    memory_size,
194                ),
195                record: Some(StableCellLedgerRecord::default()),
196            };
197        }
198
199        match decode_stable_cell_ledger_record_from_memory(&memory) {
200            Ok(record) => StableCellDiagnostic {
201                diagnostic: DiagnosticStableCell::new(
202                    DiagnosticStableCellStatus::Readable,
203                    memory_size,
204                ),
205                record: Some(record),
206            },
207            Err(err) => StableCellDiagnostic {
208                diagnostic: DiagnosticStableCell::new(
209                    DiagnosticStableCellStatus::Corrupt {
210                        failure: DiagnosticFailure::new(
211                            DiagnosticCode::StableCell,
212                            err.to_string(),
213                        ),
214                    },
215                    memory_size,
216                ),
217                record: None,
218            },
219        }
220    }
221}
222
223struct StableCellDiagnostic {
224    diagnostic: DiagnosticStableCell,
225    record: Option<StableCellLedgerRecord>,
226}
227
228const fn ledger_anchor_descriptor() -> AllocationSlotDescriptor {
229    AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
230}
231
232fn diagnostic_validation<P: AllocationPolicy>(
233    declarations: &SealedDeclarationSnapshot,
234    custom_policy: &P,
235    stable_cell_record: Option<&StableCellLedgerRecord>,
236    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
237) -> DiagnosticCheck
238where
239    P::Error: Display,
240{
241    let recovered = match diagnostic_validation_ledger(stable_cell_record, recovered) {
242        Ok(recovered) => recovered,
243        Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
244    };
245    let resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
246        Ok(resolved) => resolved,
247        Err(err) => {
248            return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
249        }
250    };
251    let policy = super::policy::RuntimeMemoryManagerPolicy {
252        declarations: &resolved,
253        custom_policy,
254    };
255    match crate::validate_allocations(&recovered, resolved.allocation_snapshot().clone(), &policy) {
256        Ok(_) => DiagnosticCheck::passed(),
257        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
258    }
259}
260
261fn diagnostic_bootstrap_binding(
262    tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
263    tested_declaration_fingerprint: SealedDeclarationFingerprint,
264    established: Option<&DiagnosticRuntimeBinding>,
265) -> DiagnosticCheck {
266    let tested_policy_identity = match tested_policy_identity {
267        Ok(identity) => identity,
268        Err(failure) => {
269            return DiagnosticCheck::not_run(failure.code, failure.message.clone());
270        }
271    };
272    let Some(established) = established else {
273        return DiagnosticCheck::not_run(
274            DiagnosticCode::RuntimeBinding,
275            "runtime has not completed bootstrap",
276        );
277    };
278    if &established.policy_identity == tested_policy_identity
279        && established.declaration_fingerprint == tested_declaration_fingerprint
280    {
281        return DiagnosticCheck::passed();
282    }
283    DiagnosticCheck::failed(
284        DiagnosticCode::RuntimeBinding,
285        format!(
286            "tested policy/declaration binding differs from established runtime binding: \
287             tested_policy={tested_policy_identity:?}, \
288             tested_declarations={tested_declaration_fingerprint:?}, \
289             established={established:?}"
290        ),
291    )
292}
293
294pub(super) fn diagnostic_validation_ledger<'recovery>(
295    stable_cell_record: Option<&StableCellLedgerRecord>,
296    recovered: Option<&'recovery Result<crate::RecoveredLedger, LedgerCommitError>>,
297) -> Result<Cow<'recovery, crate::RecoveredLedger>, DiagnosticFailure> {
298    if let Some(Ok(recovered)) = recovered {
299        return Ok(Cow::Borrowed(recovered));
300    }
301    if let Some(Err(err)) = recovered {
302        if stable_cell_record.is_some_and(|record| record.store().physical().is_uninitialized()) {
303            return diagnostic_genesis_recovered_ledger().map(Cow::Owned);
304        }
305        let code = if matches!(
306            err,
307            LedgerCommitError::PayloadEnvelope(
308                LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
309            )
310        ) {
311            DiagnosticCode::UnsupportedFormat
312        } else {
313            DiagnosticCode::LedgerRecovery
314        };
315        return Err(DiagnosticFailure::new(
316            code,
317            format!("protected ledger recovery: {err}"),
318        ));
319    }
320    if stable_cell_record.is_some() {
321        return diagnostic_genesis_recovered_ledger().map(Cow::Owned);
322    }
323    Err(DiagnosticFailure::new(
324        DiagnosticCode::StableCell,
325        "stable-cell ledger record is not readable",
326    ))
327}
328
329fn diagnostic_genesis_recovered_ledger() -> Result<crate::RecoveredLedger, DiagnosticFailure> {
330    AllocationLedger::new(0, AllocationHistory::default())
331        .map(|ledger| crate::RecoveredLedger::from_trusted_parts(ledger, 0))
332        .map_err(|err| {
333            DiagnosticFailure::new(
334                DiagnosticCode::GenesisLedger,
335                format!("genesis ledger: {err}"),
336            )
337        })
338}