1use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
2use crate::{
3 DiagnosticCheck, DiagnosticCode, DiagnosticExport, DiagnosticFailure, DiagnosticMemorySize,
4 DiagnosticRuntimeBinding, DiagnosticStableCell, DiagnosticStableCellStatus, LedgerCommitError,
5 LedgerPayloadEnvelopeError, MemoryRuntimeDoctorReport, PolicyIdentity, RecoveredLedger,
6 RuntimeBootstrapPolicy, StableCellLedgerRecord,
7 physical::CommitSnapshotDiagnostic,
8 requests::{SealedDeclarationFingerprint, SealedDeclarationSnapshot},
9 slot::MEMORY_MANAGER_LEDGER_ID,
10 stable_cell::decode_stable_cell_ledger_record_from_memory,
11};
12use ic_stable_structures::{Memory, memory_manager::MemoryId};
13use std::{borrow::Cow, fmt::Display};
14
15impl<M: Memory> MemoryRuntime<M> {
16 pub fn diagnostic_export(&self) -> Result<DiagnosticExport, RuntimeDiagnosticError> {
18 if !self.is_bootstrapped() {
19 return Err(RuntimeDiagnosticError::NotBootstrapped);
20 }
21 let (recovered, commit_recovery) = self
22 .ledger_record_from_memory()?
23 .store()
24 .recover_with_diagnostic();
25 let recovered = recovered?;
26 Ok(self.recovered_diagnostic_export(recovered, commit_recovery))
27 }
28
29 pub fn commit_recovery_diagnostic(
34 &self,
35 ) -> Result<CommitSnapshotDiagnostic, RuntimeDiagnosticError> {
36 let record = self.ledger_record_from_memory()?;
37 Ok(record.store().physical().diagnostic())
38 }
39
40 #[must_use]
46 pub fn doctor_report<P>(
47 &self,
48 declarations: &SealedDeclarationSnapshot,
49 pool: &crate::MemoryAllocationPool,
50 policy: &P,
51 ) -> MemoryRuntimeDoctorReport
52 where
53 P: RuntimeBootstrapPolicy,
54 P::Error: Display,
55 {
56 let stable_cell = self.stable_cell_diagnostic();
57 let recovery = stable_cell
60 .record
61 .map(|record| record.store().recover_with_diagnostic());
62 let mut observed_pages = [0; crate::constants::MAX_ALLOCATIONS];
65 if let Some((Ok(recovered), _)) = &recovery {
66 for (record, pages) in recovered.ledger().records().iter().zip(&mut observed_pages) {
67 *pages = self.memory_size_pages(record.slot().id());
68 }
69 }
70 let tested_policy_identity = policy
71 .runtime_bootstrap_identity()
72 .map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
73 let tested_declaration_fingerprint = declarations.fingerprint();
74 let established_bootstrap_binding = self.established_bootstrap_binding();
75 let bootstrap_binding = diagnostic_bootstrap_binding(
76 &tested_policy_identity,
77 tested_declaration_fingerprint,
78 pool,
79 established_bootstrap_binding.as_ref(),
80 );
81 let validation = match &tested_policy_identity {
82 Ok(_) => diagnostic_validation(
83 self,
84 declarations,
85 pool,
86 policy,
87 recovery.as_ref().map(|(recovered, _)| recovered),
88 ),
89 Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
90 };
91 let commit_recovery = recovery.as_ref().map(|(_, diagnostic)| *diagnostic);
92 let ledger = recovery.and_then(|(recovered, diagnostic)| {
93 recovered.ok().map(|recovered| {
94 let mut export = DiagnosticExport::from_owned_ledger(
95 recovered.into_ledger(),
96 crate::slot::LEDGER_SLOT,
97 );
98 export.commit_recovery = Some(diagnostic);
99 for (record, pages) in export.records.iter_mut().zip(&observed_pages) {
100 record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(*pages));
101 }
102 export
103 })
104 });
105
106 MemoryRuntimeDoctorReport {
107 bootstrapped: self.is_bootstrapped(),
108 tested_policy_identity,
109 tested_declaration_fingerprint,
110 established_bootstrap_binding,
111 bootstrap_binding,
112 ledger_anchor: crate::slot::LEDGER_SLOT,
113 stable_cell: stable_cell.diagnostic,
114 commit_recovery,
115 ledger,
116 requests: declarations.requests().to_vec(),
117 allocation_pool: pool.clone(),
118 validation,
119 }
120 }
121
122 fn recovered_diagnostic_export(
123 &self,
124 recovered: RecoveredLedger,
125 commit_recovery: CommitSnapshotDiagnostic,
126 ) -> DiagnosticExport {
127 let mut export =
128 DiagnosticExport::from_owned_ledger(recovered.into_ledger(), crate::slot::LEDGER_SLOT);
129 export.commit_recovery = Some(commit_recovery);
130 for record in &mut export.records {
131 record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(
132 self.memory_size_pages(record.allocation.slot().id()),
133 ));
134 }
135 export
136 }
137
138 fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
139 match &self.lifecycle {
140 RuntimeLifecycle::Unbootstrapped => None,
141 RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
142 binding.policy_identity.clone(),
143 binding.source.fingerprint(),
144 binding.pool.clone(),
145 )),
146 }
147 }
148
149 fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
150 let memory = self
151 .memory_manager
152 .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
153 let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
154 match decode_stable_cell_ledger_record_from_memory(&memory) {
155 Ok(record) => StableCellDiagnostic {
156 diagnostic: DiagnosticStableCell::new(
157 if memory_size.wasm_pages == 0 {
158 DiagnosticStableCellStatus::Empty
159 } else {
160 DiagnosticStableCellStatus::Readable
161 },
162 memory_size,
163 ),
164 record: Some(record),
165 },
166 Err(err) => StableCellDiagnostic {
167 diagnostic: DiagnosticStableCell::new(
168 DiagnosticStableCellStatus::Corrupt {
169 failure: DiagnosticFailure::new(
170 DiagnosticCode::StableCell,
171 err.to_string(),
172 ),
173 },
174 memory_size,
175 ),
176 record: None,
177 },
178 }
179 }
180}
181
182struct StableCellDiagnostic {
183 diagnostic: DiagnosticStableCell,
184 record: Option<StableCellLedgerRecord>,
185}
186
187fn diagnostic_validation<M: Memory, P: RuntimeBootstrapPolicy>(
188 runtime: &MemoryRuntime<M>,
189 declarations: &SealedDeclarationSnapshot,
190 pool: &crate::MemoryAllocationPool,
191 custom_policy: &P,
192 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
193) -> DiagnosticCheck
194where
195 P::Error: Display,
196{
197 let recovered = match diagnostic_validation_ledger(recovered) {
198 Ok(recovered) => recovered,
199 Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
200 };
201 let occupied = recovered.ledger().occupied_slots();
202 if let Err(error) = runtime.validate_pool_custody(&occupied, pool) {
203 return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, error.to_string());
204 }
205 let resolved = match declarations.resolve(recovered.ledger(), &occupied, Vec::new(), pool) {
206 Ok(resolved) => resolved,
207 Err(err) => {
208 return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
209 }
210 };
211 match crate::validation::check_allocations(&recovered, &resolved, custom_policy) {
212 Ok(()) => DiagnosticCheck::passed(),
213 Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
214 }
215}
216
217fn diagnostic_bootstrap_binding(
218 tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
219 tested_declaration_fingerprint: SealedDeclarationFingerprint,
220 pool: &crate::MemoryAllocationPool,
221 established: Option<&DiagnosticRuntimeBinding>,
222) -> DiagnosticCheck {
223 let tested_policy_identity = match tested_policy_identity {
224 Ok(identity) => identity,
225 Err(failure) => {
226 return DiagnosticCheck::not_run(failure.code, failure.message.clone());
227 }
228 };
229 let Some(established) = established else {
230 return DiagnosticCheck::not_run(
231 DiagnosticCode::RuntimeBinding,
232 "runtime has not completed bootstrap",
233 );
234 };
235 if &established.policy_identity == tested_policy_identity
236 && established.declaration_fingerprint == tested_declaration_fingerprint
237 && &established.allocation_pool == pool
238 {
239 return DiagnosticCheck::passed();
240 }
241 DiagnosticCheck::failed(
242 DiagnosticCode::RuntimeBinding,
243 "tested policy/declaration/pool binding differs from established runtime binding",
244 )
245}
246
247pub(super) fn diagnostic_validation_ledger(
248 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
249) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
250 if let Some(Ok(recovered)) = recovered {
251 return Ok(Cow::Borrowed(recovered));
252 }
253 if let Some(Err(err)) = recovered {
254 if let Some(genesis) = RecoveredLedger::genesis_for_absent_snapshot(err) {
255 return Ok(Cow::Owned(genesis));
256 }
257 let code = if matches!(
258 err,
259 LedgerCommitError::PayloadEnvelope(
260 LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
261 )
262 ) {
263 DiagnosticCode::UnsupportedFormat
264 } else {
265 DiagnosticCode::LedgerRecovery
266 };
267 return Err(DiagnosticFailure::new(
268 code,
269 format!("protected ledger recovery: {err}"),
270 ));
271 }
272 Err(DiagnosticFailure::new(
273 DiagnosticCode::StableCell,
274 "stable-cell ledger record is not readable",
275 ))
276}