1use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
2use crate::{
3 AllocationLedger, AllocationPolicy, AllocationSlotDescriptor, DiagnosticCheck, DiagnosticCode,
4 DiagnosticDeclaration, DiagnosticExport, DiagnosticFailure, DiagnosticMemorySize,
5 DiagnosticRangeAuthority, DiagnosticRuntimeBinding, DiagnosticStableCell,
6 DiagnosticStableCellStatus, LedgerCommitError, LedgerPayloadEnvelopeError,
7 MemoryRuntimeDoctorReport, PolicyIdentity, RecoveredLedger, 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(&recovered),
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_sizes(
74 recovered.ledger(),
75 ledger_anchor_descriptor(),
76 commit_recovery,
77 self.memory_sizes(recovered),
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(declarations, policy, recovered.as_ref()),
111 Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
112 };
113
114 MemoryRuntimeDoctorReport {
115 bootstrapped: self.is_bootstrapped(),
116 tested_policy_identity,
117 tested_declaration_fingerprint,
118 established_bootstrap_binding,
119 bootstrap_binding,
120 ledger_anchor: ledger_anchor_descriptor(),
121 stable_cell: stable_cell.diagnostic,
122 commit_recovery,
123 ledger,
124 registered_declarations: diagnostic_declarations,
125 range_authority,
126 validation,
127 }
128 }
129
130 fn memory_sizes<'a>(
131 &'a self,
132 recovered: &'a RecoveredLedger,
133 ) -> impl Iterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)> + 'a {
134 recovered
135 .ledger()
136 .allocation_history()
137 .records()
138 .iter()
139 .map(move |record| {
140 let id = record
141 .slot()
142 .memory_manager_id()
143 .expect("recovered ledger slot");
144 (
145 record.slot().clone(),
146 DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
147 )
148 })
149 }
150
151 fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
152 match &self.lifecycle {
153 RuntimeLifecycle::Unbootstrapped => None,
154 RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
155 binding.policy_identity.clone(),
156 binding.source.fingerprint(),
157 )),
158 }
159 }
160
161 fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
162 let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
163 let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
164 if memory.size() == 0 {
165 return StableCellDiagnostic {
166 diagnostic: DiagnosticStableCell::new(
167 DiagnosticStableCellStatus::Empty,
168 memory_size,
169 ),
170 record: Some(StableCellLedgerRecord::default()),
171 };
172 }
173
174 match decode_stable_cell_ledger_record_from_memory(&memory) {
175 Ok(record) => StableCellDiagnostic {
176 diagnostic: DiagnosticStableCell::new(
177 DiagnosticStableCellStatus::Readable,
178 memory_size,
179 ),
180 record: Some(record),
181 },
182 Err(err) => StableCellDiagnostic {
183 diagnostic: DiagnosticStableCell::new(
184 DiagnosticStableCellStatus::Corrupt {
185 failure: DiagnosticFailure::new(
186 DiagnosticCode::StableCell,
187 err.to_string(),
188 ),
189 },
190 memory_size,
191 ),
192 record: None,
193 },
194 }
195 }
196}
197
198struct StableCellDiagnostic {
199 diagnostic: DiagnosticStableCell,
200 record: Option<StableCellLedgerRecord>,
201}
202
203const fn ledger_anchor_descriptor() -> AllocationSlotDescriptor {
204 AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
205}
206
207fn diagnostic_validation<P: AllocationPolicy>(
208 declarations: &SealedDeclarationSnapshot,
209 custom_policy: &P,
210 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
211) -> DiagnosticCheck
212where
213 P::Error: Display,
214{
215 let recovered = match diagnostic_validation_ledger(recovered) {
216 Ok(recovered) => recovered,
217 Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
218 };
219 let resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
220 Ok(resolved) => resolved,
221 Err(err) => {
222 return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
223 }
224 };
225 let policy = super::policy::RuntimeMemoryManagerPolicy {
226 declarations: &resolved,
227 custom_policy,
228 };
229 match crate::validate_allocations(&recovered, resolved.allocation_snapshot().clone(), &policy) {
230 Ok(_) => DiagnosticCheck::passed(),
231 Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
232 }
233}
234
235fn diagnostic_bootstrap_binding(
236 tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
237 tested_declaration_fingerprint: SealedDeclarationFingerprint,
238 established: Option<&DiagnosticRuntimeBinding>,
239) -> DiagnosticCheck {
240 let tested_policy_identity = match tested_policy_identity {
241 Ok(identity) => identity,
242 Err(failure) => {
243 return DiagnosticCheck::not_run(failure.code, failure.message.clone());
244 }
245 };
246 let Some(established) = established else {
247 return DiagnosticCheck::not_run(
248 DiagnosticCode::RuntimeBinding,
249 "runtime has not completed bootstrap",
250 );
251 };
252 if &established.policy_identity == tested_policy_identity
253 && established.declaration_fingerprint == tested_declaration_fingerprint
254 {
255 return DiagnosticCheck::passed();
256 }
257 DiagnosticCheck::failed(
258 DiagnosticCode::RuntimeBinding,
259 format!(
260 "tested policy/declaration binding differs from established runtime binding: \
261 tested_policy={tested_policy_identity:?}, \
262 tested_declarations={tested_declaration_fingerprint:?}, \
263 established={established:?}"
264 ),
265 )
266}
267
268pub(super) fn diagnostic_validation_ledger(
269 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
270) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
271 if let Some(Ok(recovered)) = recovered {
272 return Ok(Cow::Borrowed(recovered));
273 }
274 if let Some(Err(err)) = recovered {
275 if matches!(
277 err,
278 LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
279 ) {
280 return Ok(Cow::Owned(RecoveredLedger::from_trusted_ledger(
281 AllocationLedger::empty_genesis(),
282 )));
283 }
284 let code = if matches!(
285 err,
286 LedgerCommitError::PayloadEnvelope(
287 LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
288 )
289 ) {
290 DiagnosticCode::UnsupportedFormat
291 } else {
292 DiagnosticCode::LedgerRecovery
293 };
294 return Err(DiagnosticFailure::new(
295 code,
296 format!("protected ledger recovery: {err}"),
297 ));
298 }
299 Err(DiagnosticFailure::new(
300 DiagnosticCode::StableCell,
301 "stable-cell ledger record is not readable",
302 ))
303}