1use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
2use crate::{
3 AllocationHistory, AllocationLedger, AllocationPolicy, AllocationSlotDescriptor,
4 DiagnosticCheck, DiagnosticCode, DiagnosticDeclaration, DiagnosticExport, DiagnosticFailure,
5 DiagnosticMemorySize, DiagnosticMemorySizeOutcome, DiagnosticRangeAuthority,
6 DiagnosticRuntimeBinding, DiagnosticStableCell, DiagnosticStableCellStatus, LedgerCommitError,
7 LedgerPayloadEnvelopeError, MemoryRuntimeDoctorReport, PolicyIdentity, RuntimeBootstrapPolicy,
8 StableCellLedgerRecord,
9 physical::CommitStoreDiagnostic,
10 registry::{SealedDeclarationFingerprint, SealedDeclarationSnapshot},
11 slot::MEMORY_MANAGER_LEDGER_ID,
12 stable_cell::decode_stable_cell_ledger_record_from_memory,
13};
14use ic_stable_structures::Memory;
15use std::{borrow::Cow, fmt::Display};
16
17impl<M: Memory> MemoryRuntime<M> {
18 pub fn diagnostic_export(&self) -> Result<DiagnosticExport, RuntimeDiagnosticError> {
20 if !self.is_bootstrapped() {
21 return Err(RuntimeDiagnosticError::NotBootstrapped);
22 }
23 let record = self.ledger_record_from_memory()?;
24 let (recovered, commit_recovery) = record.store().recover_with_diagnostic();
25 let recovered = recovered?;
26 let ledger = recovered.ledger();
27 Ok(
28 DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
29 ledger,
30 ledger_anchor_descriptor(),
31 Some(commit_recovery),
32 self.memory_sizes(ledger)?,
33 ),
34 )
35 }
36
37 pub fn commit_recovery_diagnostic(
42 &self,
43 ) -> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
44 let record = self.ledger_record_from_memory()?;
45 Ok(record.store().physical().diagnostic())
46 }
47
48 #[must_use]
54 pub fn doctor_report<P>(
55 &self,
56 declarations: &SealedDeclarationSnapshot,
57 policy: &P,
58 ) -> MemoryRuntimeDoctorReport
59 where
60 P: RuntimeBootstrapPolicy,
61 P::Error: Display,
62 {
63 let stable_cell = self.stable_cell_diagnostic();
64 let (recovered, commit_recovery) = stable_cell
65 .record
66 .as_ref()
67 .map(|record| record.store().recover_with_diagnostic())
68 .map_or((None, None), |(recovered, diagnostic)| {
69 (Some(recovered), Some(diagnostic))
70 });
71 let recovered_for_export = recovered.as_ref().and_then(|result| result.as_ref().ok());
72 let ledger = recovered_for_export.map(|recovered| {
73 DiagnosticExport::from_ledger_with_commit_recovery_and_memory_size_outcomes(
74 recovered.ledger(),
75 ledger_anchor_descriptor(),
76 commit_recovery,
77 self.memory_size_outcomes(recovered.ledger()),
78 )
79 });
80 let diagnostic_declarations = declarations
81 .registered_declarations()
82 .iter()
83 .map(|registration| {
84 DiagnosticDeclaration::new(
85 registration.authority(),
86 registration.declaration().clone(),
87 )
88 })
89 .collect();
90 let registered_records = declarations
91 .registered_ranges()
92 .iter()
93 .map(|registration| registration.record().clone())
94 .collect();
95 let range_authority = DiagnosticRangeAuthority::new(
96 registered_records,
97 declarations.range_authority().clone(),
98 );
99 let tested_policy_identity = policy
100 .runtime_bootstrap_identity()
101 .map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
102 let tested_declaration_fingerprint = declarations.fingerprint();
103 let established_bootstrap_binding = self.established_bootstrap_binding();
104 let bootstrap_binding = diagnostic_bootstrap_binding(
105 &tested_policy_identity,
106 tested_declaration_fingerprint,
107 established_bootstrap_binding.as_ref(),
108 );
109 let validation = match &tested_policy_identity {
110 Ok(_) => diagnostic_validation(
111 declarations,
112 policy,
113 stable_cell.record.as_ref(),
114 recovered.as_ref(),
115 ),
116 Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
117 };
118
119 MemoryRuntimeDoctorReport {
120 bootstrapped: self.is_bootstrapped(),
121 tested_policy_identity,
122 tested_declaration_fingerprint,
123 established_bootstrap_binding,
124 bootstrap_binding,
125 ledger_anchor: ledger_anchor_descriptor(),
126 stable_cell: stable_cell.diagnostic,
127 commit_recovery,
128 ledger,
129 registered_declarations: diagnostic_declarations,
130 range_authority,
131 validation,
132 }
133 }
134
135 fn memory_sizes(
136 &self,
137 ledger: &AllocationLedger,
138 ) -> Result<Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)>, RuntimeDiagnosticError> {
139 ledger
140 .allocation_history()
141 .records()
142 .iter()
143 .map(|record| {
144 let id = record.slot().memory_manager_id()?;
145 Ok((
146 record.slot().clone(),
147 DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
148 ))
149 })
150 .collect()
151 }
152
153 pub(super) fn memory_size_outcomes(
154 &self,
155 ledger: &AllocationLedger,
156 ) -> Vec<(AllocationSlotDescriptor, DiagnosticMemorySizeOutcome)> {
157 ledger
158 .allocation_history()
159 .records()
160 .iter()
161 .map(|record| {
162 let outcome = match record.slot().memory_manager_id() {
163 Ok(id) => DiagnosticMemorySizeOutcome::Measured(
164 DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
165 ),
166 Err(err) => DiagnosticMemorySizeOutcome::Failed(DiagnosticFailure::new(
167 DiagnosticCode::MemorySize,
168 err.to_string(),
169 )),
170 };
171 (record.slot().clone(), outcome)
172 })
173 .collect()
174 }
175
176 fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
177 match &self.lifecycle {
178 RuntimeLifecycle::Unbootstrapped => None,
179 RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
180 binding.policy_identity.clone(),
181 binding.source.fingerprint(),
182 )),
183 }
184 }
185
186 fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
187 let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
188 let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
189 if memory.size() == 0 {
190 return StableCellDiagnostic {
191 diagnostic: DiagnosticStableCell::new(
192 DiagnosticStableCellStatus::Empty,
193 memory_size,
194 ),
195 record: Some(StableCellLedgerRecord::default()),
196 };
197 }
198
199 match decode_stable_cell_ledger_record_from_memory(&memory) {
200 Ok(record) => StableCellDiagnostic {
201 diagnostic: DiagnosticStableCell::new(
202 DiagnosticStableCellStatus::Readable,
203 memory_size,
204 ),
205 record: Some(record),
206 },
207 Err(err) => StableCellDiagnostic {
208 diagnostic: DiagnosticStableCell::new(
209 DiagnosticStableCellStatus::Corrupt {
210 failure: DiagnosticFailure::new(
211 DiagnosticCode::StableCell,
212 err.to_string(),
213 ),
214 },
215 memory_size,
216 ),
217 record: None,
218 },
219 }
220 }
221}
222
223struct StableCellDiagnostic {
224 diagnostic: DiagnosticStableCell,
225 record: Option<StableCellLedgerRecord>,
226}
227
228const fn ledger_anchor_descriptor() -> AllocationSlotDescriptor {
229 AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
230}
231
232fn diagnostic_validation<P: AllocationPolicy>(
233 declarations: &SealedDeclarationSnapshot,
234 custom_policy: &P,
235 stable_cell_record: Option<&StableCellLedgerRecord>,
236 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
237) -> DiagnosticCheck
238where
239 P::Error: Display,
240{
241 let recovered = match diagnostic_validation_ledger(stable_cell_record, recovered) {
242 Ok(recovered) => recovered,
243 Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
244 };
245 let resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
246 Ok(resolved) => resolved,
247 Err(err) => {
248 return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
249 }
250 };
251 let policy = super::policy::RuntimeMemoryManagerPolicy {
252 declarations: &resolved,
253 custom_policy,
254 };
255 match crate::validate_allocations(&recovered, resolved.allocation_snapshot().clone(), &policy) {
256 Ok(_) => DiagnosticCheck::passed(),
257 Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
258 }
259}
260
261fn diagnostic_bootstrap_binding(
262 tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
263 tested_declaration_fingerprint: SealedDeclarationFingerprint,
264 established: Option<&DiagnosticRuntimeBinding>,
265) -> DiagnosticCheck {
266 let tested_policy_identity = match tested_policy_identity {
267 Ok(identity) => identity,
268 Err(failure) => {
269 return DiagnosticCheck::not_run(failure.code, failure.message.clone());
270 }
271 };
272 let Some(established) = established else {
273 return DiagnosticCheck::not_run(
274 DiagnosticCode::RuntimeBinding,
275 "runtime has not completed bootstrap",
276 );
277 };
278 if &established.policy_identity == tested_policy_identity
279 && established.declaration_fingerprint == tested_declaration_fingerprint
280 {
281 return DiagnosticCheck::passed();
282 }
283 DiagnosticCheck::failed(
284 DiagnosticCode::RuntimeBinding,
285 format!(
286 "tested policy/declaration binding differs from established runtime binding: \
287 tested_policy={tested_policy_identity:?}, \
288 tested_declarations={tested_declaration_fingerprint:?}, \
289 established={established:?}"
290 ),
291 )
292}
293
294pub(super) fn diagnostic_validation_ledger<'recovery>(
295 stable_cell_record: Option<&StableCellLedgerRecord>,
296 recovered: Option<&'recovery Result<crate::RecoveredLedger, LedgerCommitError>>,
297) -> Result<Cow<'recovery, crate::RecoveredLedger>, DiagnosticFailure> {
298 if let Some(Ok(recovered)) = recovered {
299 return Ok(Cow::Borrowed(recovered));
300 }
301 if let Some(Err(err)) = recovered {
302 if stable_cell_record.is_some_and(|record| record.store().physical().is_uninitialized()) {
303 return diagnostic_genesis_recovered_ledger().map(Cow::Owned);
304 }
305 let code = if matches!(
306 err,
307 LedgerCommitError::PayloadEnvelope(
308 LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
309 )
310 ) {
311 DiagnosticCode::UnsupportedFormat
312 } else {
313 DiagnosticCode::LedgerRecovery
314 };
315 return Err(DiagnosticFailure::new(
316 code,
317 format!("protected ledger recovery: {err}"),
318 ));
319 }
320 if stable_cell_record.is_some() {
321 return diagnostic_genesis_recovered_ledger().map(Cow::Owned);
322 }
323 Err(DiagnosticFailure::new(
324 DiagnosticCode::StableCell,
325 "stable-cell ledger record is not readable",
326 ))
327}
328
329fn diagnostic_genesis_recovered_ledger() -> Result<crate::RecoveredLedger, DiagnosticFailure> {
330 AllocationLedger::new(0, AllocationHistory::default())
331 .map(|ledger| crate::RecoveredLedger::from_trusted_parts(ledger, 0))
332 .map_err(|err| {
333 DiagnosticFailure::new(
334 DiagnosticCode::GenesisLedger,
335 format!("genesis ledger: {err}"),
336 )
337 })
338}