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