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