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