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 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 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 #[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 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 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}