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::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 = record.store().recover()?;
25        let ledger = recovered.ledger();
26        Ok(
27            DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
28                ledger,
29                ledger_anchor_descriptor(),
30                Some(record.store().physical().diagnostic()),
31                self.memory_sizes(ledger)?,
32            ),
33        )
34    }
35
36    /// Diagnose protected commit recovery from this runtime's ledger memory.
37    ///
38    /// This operation is available before bootstrap when the stable-cell
39    /// envelope is readable or the ledger memory is empty.
40    pub fn commit_recovery_diagnostic(
41        &self,
42    ) -> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
43        let record = self.ledger_record_from_memory()?;
44        Ok(record.store().physical().diagnostic())
45    }
46
47    /// Build preflight and lifecycle diagnostics for this runtime.
48    #[must_use]
49    pub fn doctor_report<P>(
50        &self,
51        declarations: &SealedDeclarationSnapshot,
52        policy: &P,
53    ) -> MemoryRuntimeDoctorReport
54    where
55        P: RuntimeBootstrapPolicy,
56        P::Error: Display,
57    {
58        let stable_cell = self.stable_cell_diagnostic();
59        let commit_recovery = stable_cell
60            .record
61            .as_ref()
62            .map(|record| record.store().physical().diagnostic());
63        let recovered = stable_cell
64            .record
65            .as_ref()
66            .map(|record| record.store().recover());
67        let recovered_for_export = recovered.as_ref().and_then(|result| result.as_ref().ok());
68        let ledger = recovered_for_export.map(|recovered| {
69            DiagnosticExport::from_ledger_with_commit_recovery_and_memory_size_outcomes(
70                recovered.ledger(),
71                ledger_anchor_descriptor(),
72                commit_recovery,
73                self.memory_size_outcomes(recovered.ledger()),
74            )
75        });
76        let diagnostic_declarations = declarations
77            .registered_declarations()
78            .iter()
79            .map(|registration| {
80                DiagnosticDeclaration::new(
81                    registration.authority(),
82                    registration.declaration().clone(),
83                )
84            })
85            .collect();
86        let registered_records = declarations
87            .registered_ranges()
88            .iter()
89            .map(|registration| registration.record().clone())
90            .collect();
91        let range_authority = DiagnosticRangeAuthority::new(
92            registered_records,
93            Ok(declarations.range_authority().clone()),
94        );
95        let tested_policy_identity = policy
96            .runtime_bootstrap_identity()
97            .map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
98        let tested_declaration_fingerprint = declarations.fingerprint();
99        let established_bootstrap_binding = self.established_bootstrap_binding();
100        let bootstrap_binding = diagnostic_bootstrap_binding(
101            &tested_policy_identity,
102            tested_declaration_fingerprint,
103            established_bootstrap_binding.as_ref(),
104        );
105        let validation = match &tested_policy_identity {
106            Ok(_) => diagnostic_validation(
107                declarations,
108                policy,
109                stable_cell.record.as_ref(),
110                recovered.as_ref(),
111            ),
112            Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
113        };
114
115        MemoryRuntimeDoctorReport {
116            bootstrapped: self.is_bootstrapped(),
117            tested_policy_identity,
118            tested_declaration_fingerprint,
119            established_bootstrap_binding,
120            bootstrap_binding,
121            ledger_anchor: ledger_anchor_descriptor(),
122            stable_cell: stable_cell.diagnostic,
123            commit_recovery,
124            ledger,
125            registered_declarations: diagnostic_declarations,
126            range_authority,
127            validation,
128        }
129    }
130
131    fn memory_sizes(
132        &self,
133        ledger: &AllocationLedger,
134    ) -> Result<Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)>, RuntimeDiagnosticError> {
135        ledger
136            .allocation_history()
137            .records()
138            .iter()
139            .map(|record| {
140                let id = record.slot().memory_manager_id()?;
141                Ok((
142                    record.slot().clone(),
143                    DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
144                ))
145            })
146            .collect()
147    }
148
149    pub(super) fn memory_size_outcomes(
150        &self,
151        ledger: &AllocationLedger,
152    ) -> Vec<(AllocationSlotDescriptor, DiagnosticMemorySizeOutcome)> {
153        ledger
154            .allocation_history()
155            .records()
156            .iter()
157            .map(|record| {
158                let outcome = match record.slot().memory_manager_id() {
159                    Ok(id) => DiagnosticMemorySizeOutcome::Measured(
160                        DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
161                    ),
162                    Err(err) => DiagnosticMemorySizeOutcome::Failed(DiagnosticFailure::new(
163                        DiagnosticCode::MemorySize,
164                        err.to_string(),
165                    )),
166                };
167                (record.slot().clone(), outcome)
168            })
169            .collect()
170    }
171
172    fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
173        match &self.lifecycle {
174            RuntimeLifecycle::Unbootstrapped => None,
175            RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
176                binding.policy_identity.clone(),
177                binding.declarations.fingerprint(),
178            )),
179        }
180    }
181
182    fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
183        let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
184        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
185        if memory.size() == 0 {
186            return StableCellDiagnostic {
187                diagnostic: DiagnosticStableCell::new(
188                    DiagnosticStableCellStatus::Empty,
189                    memory_size,
190                ),
191                record: Some(StableCellLedgerRecord::default()),
192            };
193        }
194
195        match decode_stable_cell_ledger_record_from_memory(&memory) {
196            Ok(record) => StableCellDiagnostic {
197                diagnostic: DiagnosticStableCell::new(
198                    DiagnosticStableCellStatus::Readable,
199                    memory_size,
200                ),
201                record: Some(record),
202            },
203            Err(err) => StableCellDiagnostic {
204                diagnostic: DiagnosticStableCell::new(
205                    DiagnosticStableCellStatus::Corrupt {
206                        failure: DiagnosticFailure::new(
207                            DiagnosticCode::StableCell,
208                            err.to_string(),
209                        ),
210                    },
211                    memory_size,
212                ),
213                record: None,
214            },
215        }
216    }
217}
218
219struct StableCellDiagnostic {
220    diagnostic: DiagnosticStableCell,
221    record: Option<StableCellLedgerRecord>,
222}
223
224const fn ledger_anchor_descriptor() -> AllocationSlotDescriptor {
225    AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
226}
227
228fn diagnostic_validation<P: AllocationPolicy>(
229    declarations: &SealedDeclarationSnapshot,
230    custom_policy: &P,
231    stable_cell_record: Option<&StableCellLedgerRecord>,
232    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
233) -> DiagnosticCheck
234where
235    P::Error: Display,
236{
237    let recovered = match diagnostic_validation_ledger(stable_cell_record, recovered) {
238        Ok(recovered) => recovered,
239        Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
240    };
241    let policy = super::policy::RuntimeMemoryManagerPolicy {
242        declarations,
243        custom_policy,
244    };
245    match crate::validate_allocations(
246        &recovered,
247        declarations.allocation_snapshot().clone(),
248        &policy,
249    ) {
250        Ok(_) => DiagnosticCheck::passed(),
251        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
252    }
253}
254
255fn diagnostic_bootstrap_binding(
256    tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
257    tested_declaration_fingerprint: SealedDeclarationFingerprint,
258    established: Option<&DiagnosticRuntimeBinding>,
259) -> DiagnosticCheck {
260    let tested_policy_identity = match tested_policy_identity {
261        Ok(identity) => identity,
262        Err(failure) => {
263            return DiagnosticCheck::not_run(failure.code, failure.message.clone());
264        }
265    };
266    let Some(established) = established else {
267        return DiagnosticCheck::not_run(
268            DiagnosticCode::RuntimeBinding,
269            "runtime has not completed bootstrap",
270        );
271    };
272    if &established.policy_identity == tested_policy_identity
273        && established.declaration_fingerprint == tested_declaration_fingerprint
274    {
275        return DiagnosticCheck::passed();
276    }
277    DiagnosticCheck::failed(
278        DiagnosticCode::RuntimeBinding,
279        format!(
280            "tested policy/declaration binding differs from established runtime binding: \
281             tested_policy={tested_policy_identity:?}, \
282             tested_declarations={tested_declaration_fingerprint:?}, \
283             established={established:?}"
284        ),
285    )
286}
287
288pub(super) fn diagnostic_validation_ledger(
289    stable_cell_record: Option<&StableCellLedgerRecord>,
290    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
291) -> Result<crate::RecoveredLedger, DiagnosticFailure> {
292    if let Some(Ok(recovered)) = recovered {
293        return Ok(recovered.clone());
294    }
295    if let Some(Err(err)) = recovered {
296        if stable_cell_record.is_some_and(|record| record.store().physical().is_uninitialized()) {
297            return diagnostic_genesis_recovered_ledger();
298        }
299        let code = if matches!(
300            err,
301            LedgerCommitError::PayloadEnvelope(
302                LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
303            )
304        ) {
305            DiagnosticCode::UnsupportedFormat
306        } else {
307            DiagnosticCode::LedgerRecovery
308        };
309        return Err(DiagnosticFailure::new(
310            code,
311            format!("protected ledger recovery: {err}"),
312        ));
313    }
314    if stable_cell_record.is_some() {
315        return diagnostic_genesis_recovered_ledger();
316    }
317    Err(DiagnosticFailure::new(
318        DiagnosticCode::StableCell,
319        "stable-cell ledger record is not readable",
320    ))
321}
322
323fn diagnostic_genesis_recovered_ledger() -> Result<crate::RecoveredLedger, DiagnosticFailure> {
324    AllocationLedger::new(0, AllocationHistory::default())
325        .map(|ledger| crate::RecoveredLedger::from_trusted_parts(ledger, 0))
326        .map_err(|err| {
327            DiagnosticFailure::new(
328                DiagnosticCode::GenesisLedger,
329                format!("genesis ledger: {err}"),
330            )
331        })
332}