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