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 (recovered, commit_recovery) = self
24 .ledger_record_from_memory()?
25 .store()
26 .recover_with_diagnostic();
27 let recovered = recovered?;
28 Ok(self.recovered_diagnostic_export(Cow::Owned(recovered), commit_recovery))
29 }
30
31 pub fn commit_recovery_diagnostic(
36 &self,
37 ) -> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
38 let record = self.ledger_record_from_memory()?;
39 Ok(record.store().physical().diagnostic())
40 }
41
42 #[must_use]
48 pub fn doctor_report<P>(
49 &self,
50 declarations: &SealedDeclarationSnapshot,
51 policy: &P,
52 ) -> MemoryRuntimeDoctorReport
53 where
54 P: RuntimeBootstrapPolicy,
55 P::Error: Display,
56 {
57 let stable_cell = self.stable_cell_diagnostic();
58 let recovery = stable_cell
61 .record
62 .map(|record| record.store().recover_with_diagnostic());
63 let ledger = recovery.as_ref().and_then(|(recovered, diagnostic)| {
64 recovered.as_ref().ok().map(|recovered| {
65 self.recovered_diagnostic_export(Cow::Borrowed(recovered), *diagnostic)
66 })
67 });
68 let diagnostic_declarations = declarations
69 .registered_declarations()
70 .iter()
71 .map(|registration| {
72 DiagnosticDeclaration::new(
73 registration.authority(),
74 registration.declaration().clone(),
75 )
76 })
77 .collect();
78 let registered_records = declarations
79 .registered_ranges()
80 .iter()
81 .map(|registration| registration.record().clone())
82 .collect();
83 let range_authority = DiagnosticRangeAuthority::new(
84 registered_records,
85 declarations.range_authority().clone(),
86 );
87 let tested_policy_identity = policy
88 .runtime_bootstrap_identity()
89 .map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
90 let tested_declaration_fingerprint = declarations.fingerprint();
91 let established_bootstrap_binding = self.established_bootstrap_binding();
92 let bootstrap_binding = diagnostic_bootstrap_binding(
93 &tested_policy_identity,
94 tested_declaration_fingerprint,
95 established_bootstrap_binding.as_ref(),
96 );
97 let validation = match &tested_policy_identity {
98 Ok(_) => diagnostic_validation(
99 declarations,
100 policy,
101 recovery.as_ref().map(|(recovered, _)| recovered),
102 ),
103 Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
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: ledger_anchor_descriptor(),
113 stable_cell: stable_cell.diagnostic,
114 commit_recovery: recovery.as_ref().map(|(_, diagnostic)| *diagnostic),
115 ledger,
116 registered_declarations: diagnostic_declarations,
117 range_authority,
118 validation,
119 }
120 }
121
122 fn recovered_diagnostic_export(
123 &self,
124 recovered: Cow<'_, RecoveredLedger>,
125 commit_recovery: CommitStoreDiagnostic,
126 ) -> DiagnosticExport {
127 let anchor = ledger_anchor_descriptor();
128 let mut export = match recovered {
129 Cow::Borrowed(recovered) => DiagnosticExport::from_ledger(recovered.ledger(), anchor),
130 Cow::Owned(recovered) => {
131 DiagnosticExport::from_owned_ledger(recovered.into_ledger(), anchor)
132 }
133 };
134 export.commit_recovery = Some(commit_recovery);
135 for record in &mut export.records {
136 let id = record
137 .allocation
138 .slot()
139 .memory_manager_id()
140 .expect("recovered ledger slot");
141 record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(
142 self.memory(id).size(),
143 ));
144 }
145 export
146 }
147
148 fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
149 match &self.lifecycle {
150 RuntimeLifecycle::Unbootstrapped => None,
151 RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
152 binding.policy_identity.clone(),
153 binding.source.fingerprint(),
154 )),
155 }
156 }
157
158 fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
159 let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
160 let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
161 match decode_stable_cell_ledger_record_from_memory(&memory) {
162 Ok(record) => StableCellDiagnostic {
163 diagnostic: DiagnosticStableCell::new(
164 if memory_size.wasm_pages == 0 {
165 DiagnosticStableCellStatus::Empty
166 } else {
167 DiagnosticStableCellStatus::Readable
168 },
169 memory_size,
170 ),
171 record: Some(record),
172 },
173 Err(err) => StableCellDiagnostic {
174 diagnostic: DiagnosticStableCell::new(
175 DiagnosticStableCellStatus::Corrupt {
176 failure: DiagnosticFailure::new(
177 DiagnosticCode::StableCell,
178 err.to_string(),
179 ),
180 },
181 memory_size,
182 ),
183 record: None,
184 },
185 }
186 }
187}
188
189struct StableCellDiagnostic {
190 diagnostic: DiagnosticStableCell,
191 record: Option<StableCellLedgerRecord>,
192}
193
194const fn ledger_anchor_descriptor() -> AllocationSlotDescriptor {
195 AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
196}
197
198fn diagnostic_validation<P: AllocationPolicy>(
199 declarations: &SealedDeclarationSnapshot,
200 custom_policy: &P,
201 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
202) -> DiagnosticCheck
203where
204 P::Error: Display,
205{
206 let recovered = match diagnostic_validation_ledger(recovered) {
207 Ok(recovered) => recovered,
208 Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
209 };
210 let resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
211 Ok(resolved) => resolved,
212 Err(err) => {
213 return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
214 }
215 };
216 let policy = super::policy::RuntimeMemoryManagerPolicy {
217 declarations: &resolved,
218 custom_policy,
219 };
220 match crate::validation::check_allocations(&recovered, resolved.allocation_snapshot(), &policy)
221 {
222 Ok(()) => DiagnosticCheck::passed(),
223 Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
224 }
225}
226
227fn diagnostic_bootstrap_binding(
228 tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
229 tested_declaration_fingerprint: SealedDeclarationFingerprint,
230 established: Option<&DiagnosticRuntimeBinding>,
231) -> DiagnosticCheck {
232 let tested_policy_identity = match tested_policy_identity {
233 Ok(identity) => identity,
234 Err(failure) => {
235 return DiagnosticCheck::not_run(failure.code, failure.message.clone());
236 }
237 };
238 let Some(established) = established else {
239 return DiagnosticCheck::not_run(
240 DiagnosticCode::RuntimeBinding,
241 "runtime has not completed bootstrap",
242 );
243 };
244 if &established.policy_identity == tested_policy_identity
245 && established.declaration_fingerprint == tested_declaration_fingerprint
246 {
247 return DiagnosticCheck::passed();
248 }
249 DiagnosticCheck::failed(
250 DiagnosticCode::RuntimeBinding,
251 format!(
252 "tested policy/declaration binding differs from established runtime binding: \
253 tested_policy={tested_policy_identity:?}, \
254 tested_declarations={tested_declaration_fingerprint:?}, \
255 established={established:?}"
256 ),
257 )
258}
259
260pub(super) fn diagnostic_validation_ledger(
261 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
262) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
263 if let Some(Ok(recovered)) = recovered {
264 return Ok(Cow::Borrowed(recovered));
265 }
266 if let Some(Err(err)) = recovered {
267 if matches!(
269 err,
270 LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
271 ) {
272 return Ok(Cow::Owned(RecoveredLedger::from_trusted_ledger(
273 AllocationLedger::empty_genesis(),
274 )));
275 }
276 let code = if matches!(
277 err,
278 LedgerCommitError::PayloadEnvelope(
279 LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
280 )
281 ) {
282 DiagnosticCode::UnsupportedFormat
283 } else {
284 DiagnosticCode::LedgerRecovery
285 };
286 return Err(DiagnosticFailure::new(
287 code,
288 format!("protected ledger recovery: {err}"),
289 ));
290 }
291 Err(DiagnosticFailure::new(
292 DiagnosticCode::StableCell,
293 "stable-cell ledger record is not readable",
294 ))
295}