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