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