Skip to main content

ic_memory/runtime/
diagnostics.rs

1use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
2use crate::{
3    AllocationLedger, AllocationPolicy, DiagnosticCheck, DiagnosticCode, DiagnosticExport,
4    DiagnosticFailure, DiagnosticMemorySize, DiagnosticRuntimeBinding, DiagnosticStableCell,
5    DiagnosticStableCellStatus, LedgerCommitError, LedgerPayloadEnvelopeError,
6    MemoryRuntimeDoctorReport, PolicyIdentity, RecoveredLedger, RuntimeBootstrapPolicy,
7    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        pool: &crate::MemoryAllocationPool,
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 tested_policy_identity = policy
69            .runtime_bootstrap_identity()
70            .map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
71        let tested_declaration_fingerprint = declarations.fingerprint();
72        let established_bootstrap_binding = self.established_bootstrap_binding();
73        let bootstrap_binding = diagnostic_bootstrap_binding(
74            &tested_policy_identity,
75            tested_declaration_fingerprint,
76            pool,
77            established_bootstrap_binding.as_ref(),
78        );
79        let validation = match &tested_policy_identity {
80            Ok(_) => diagnostic_validation(
81                self,
82                declarations,
83                pool,
84                policy,
85                recovery.as_ref().map(|(recovered, _)| recovered),
86            ),
87            Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
88        };
89
90        MemoryRuntimeDoctorReport {
91            bootstrapped: self.is_bootstrapped(),
92            tested_policy_identity,
93            tested_declaration_fingerprint,
94            established_bootstrap_binding,
95            bootstrap_binding,
96            ledger_anchor: crate::slot::LEDGER_SLOT,
97            stable_cell: stable_cell.diagnostic,
98            commit_recovery: recovery.as_ref().map(|(_, diagnostic)| *diagnostic),
99            ledger,
100            requests: declarations.requests().to_vec(),
101            allocation_pool: pool.clone(),
102            validation,
103        }
104    }
105
106    fn recovered_diagnostic_export(
107        &self,
108        recovered: Cow<'_, RecoveredLedger>,
109        commit_recovery: CommitStoreDiagnostic,
110    ) -> DiagnosticExport {
111        let anchor = crate::slot::LEDGER_SLOT;
112        let mut export = match recovered {
113            Cow::Borrowed(recovered) => DiagnosticExport::from_ledger(recovered.ledger(), anchor),
114            Cow::Owned(recovered) => {
115                DiagnosticExport::from_owned_ledger(recovered.into_ledger(), anchor)
116            }
117        };
118        export.commit_recovery = Some(commit_recovery);
119        for record in &mut export.records {
120            let id = record.allocation.slot().id();
121            record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(
122                self.memory_size_pages(id),
123            ));
124        }
125        export
126    }
127
128    fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
129        match &self.lifecycle {
130            RuntimeLifecycle::Unbootstrapped => None,
131            RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
132                binding.policy_identity.clone(),
133                binding.source.fingerprint(),
134                binding.pool.clone(),
135            )),
136        }
137    }
138
139    fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
140        let memory = self
141            .memory_manager
142            .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
143        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
144        match decode_stable_cell_ledger_record_from_memory(&memory) {
145            Ok(record) => StableCellDiagnostic {
146                diagnostic: DiagnosticStableCell::new(
147                    if memory_size.wasm_pages == 0 {
148                        DiagnosticStableCellStatus::Empty
149                    } else {
150                        DiagnosticStableCellStatus::Readable
151                    },
152                    memory_size,
153                ),
154                record: Some(record),
155            },
156            Err(err) => StableCellDiagnostic {
157                diagnostic: DiagnosticStableCell::new(
158                    DiagnosticStableCellStatus::Corrupt {
159                        failure: DiagnosticFailure::new(
160                            DiagnosticCode::StableCell,
161                            err.to_string(),
162                        ),
163                    },
164                    memory_size,
165                ),
166                record: None,
167            },
168        }
169    }
170}
171
172struct StableCellDiagnostic {
173    diagnostic: DiagnosticStableCell,
174    record: Option<StableCellLedgerRecord>,
175}
176
177fn diagnostic_validation<M: Memory, P: AllocationPolicy>(
178    runtime: &MemoryRuntime<M>,
179    declarations: &SealedDeclarationSnapshot,
180    pool: &crate::MemoryAllocationPool,
181    custom_policy: &P,
182    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
183) -> DiagnosticCheck
184where
185    P::Error: Display,
186{
187    let recovered = match diagnostic_validation_ledger(recovered) {
188        Ok(recovered) => recovered,
189        Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
190    };
191    if let Err(error) = runtime.validate_pool_custody(recovered.ledger(), pool) {
192        return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, error.to_string());
193    }
194    let resolved = match declarations.resolve(recovered.ledger(), Vec::new(), pool) {
195        Ok(resolved) => resolved,
196        Err(err) => {
197            return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
198        }
199    };
200    let policy = super::policy::RuntimeMemoryManagerPolicy { custom_policy };
201    match crate::validation::check_allocations(&recovered, &resolved, &policy) {
202        Ok(()) => DiagnosticCheck::passed(),
203        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
204    }
205}
206
207fn diagnostic_bootstrap_binding(
208    tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
209    tested_declaration_fingerprint: SealedDeclarationFingerprint,
210    pool: &crate::MemoryAllocationPool,
211    established: Option<&DiagnosticRuntimeBinding>,
212) -> DiagnosticCheck {
213    let tested_policy_identity = match tested_policy_identity {
214        Ok(identity) => identity,
215        Err(failure) => {
216            return DiagnosticCheck::not_run(failure.code, failure.message.clone());
217        }
218    };
219    let Some(established) = established else {
220        return DiagnosticCheck::not_run(
221            DiagnosticCode::RuntimeBinding,
222            "runtime has not completed bootstrap",
223        );
224    };
225    if &established.policy_identity == tested_policy_identity
226        && established.declaration_fingerprint == tested_declaration_fingerprint
227        && &established.allocation_pool == pool
228    {
229        return DiagnosticCheck::passed();
230    }
231    DiagnosticCheck::failed(
232        DiagnosticCode::RuntimeBinding,
233        format!(
234            "tested policy/declaration/pool binding differs from established runtime binding: \
235             tested_policy={tested_policy_identity:?}, \
236             tested_declarations={tested_declaration_fingerprint:?}, \
237             tested_pool={pool:?}, \
238             established={established:?}"
239        ),
240    )
241}
242
243pub(super) fn diagnostic_validation_ledger(
244    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
245) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
246    if let Some(Ok(recovered)) = recovered {
247        return Ok(Cow::Borrowed(recovered));
248    }
249    if let Some(Err(err)) = recovered {
250        // Protected recovery returns NoValidGeneration only for two absent slots.
251        if matches!(
252            err,
253            LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
254        ) {
255            return Ok(Cow::Owned(RecoveredLedger::from_trusted_ledger(
256                AllocationLedger::empty_genesis(),
257            )));
258        }
259        let code = if matches!(
260            err,
261            LedgerCommitError::PayloadEnvelope(
262                LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
263            )
264        ) {
265            DiagnosticCode::UnsupportedFormat
266        } else {
267            DiagnosticCode::LedgerRecovery
268        };
269        return Err(DiagnosticFailure::new(
270            code,
271            format!("protected ledger recovery: {err}"),
272        ));
273    }
274    Err(DiagnosticFailure::new(
275        DiagnosticCode::StableCell,
276        "stable-cell ledger record is not readable",
277    ))
278}