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, memory_manager::MemoryId};
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
155 .memory_manager
156 .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
157 let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
158 match decode_stable_cell_ledger_record_from_memory(&memory) {
159 Ok(record) => StableCellDiagnostic {
160 diagnostic: DiagnosticStableCell::new(
161 if memory_size.wasm_pages == 0 {
162 DiagnosticStableCellStatus::Empty
163 } else {
164 DiagnosticStableCellStatus::Readable
165 },
166 memory_size,
167 ),
168 record: Some(record),
169 },
170 Err(err) => StableCellDiagnostic {
171 diagnostic: DiagnosticStableCell::new(
172 DiagnosticStableCellStatus::Corrupt {
173 failure: DiagnosticFailure::new(
174 DiagnosticCode::StableCell,
175 err.to_string(),
176 ),
177 },
178 memory_size,
179 ),
180 record: None,
181 },
182 }
183 }
184}
185
186struct StableCellDiagnostic {
187 diagnostic: DiagnosticStableCell,
188 record: Option<StableCellLedgerRecord>,
189}
190
191fn diagnostic_validation<P: AllocationPolicy>(
192 declarations: &SealedDeclarationSnapshot,
193 custom_policy: &P,
194 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
195) -> DiagnosticCheck
196where
197 P::Error: Display,
198{
199 let recovered = match diagnostic_validation_ledger(recovered) {
200 Ok(recovered) => recovered,
201 Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
202 };
203 let resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
204 Ok(resolved) => resolved,
205 Err(err) => {
206 return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
207 }
208 };
209 let policy = super::policy::RuntimeMemoryManagerPolicy {
210 declarations: &resolved,
211 custom_policy,
212 };
213 match crate::validation::check_allocations(&recovered, resolved.allocation_snapshot(), &policy)
214 {
215 Ok(()) => DiagnosticCheck::passed(),
216 Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
217 }
218}
219
220fn diagnostic_bootstrap_binding(
221 tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
222 tested_declaration_fingerprint: SealedDeclarationFingerprint,
223 established: Option<&DiagnosticRuntimeBinding>,
224) -> DiagnosticCheck {
225 let tested_policy_identity = match tested_policy_identity {
226 Ok(identity) => identity,
227 Err(failure) => {
228 return DiagnosticCheck::not_run(failure.code, failure.message.clone());
229 }
230 };
231 let Some(established) = established else {
232 return DiagnosticCheck::not_run(
233 DiagnosticCode::RuntimeBinding,
234 "runtime has not completed bootstrap",
235 );
236 };
237 if &established.policy_identity == tested_policy_identity
238 && established.declaration_fingerprint == tested_declaration_fingerprint
239 {
240 return DiagnosticCheck::passed();
241 }
242 DiagnosticCheck::failed(
243 DiagnosticCode::RuntimeBinding,
244 format!(
245 "tested policy/declaration binding differs from established runtime binding: \
246 tested_policy={tested_policy_identity:?}, \
247 tested_declarations={tested_declaration_fingerprint:?}, \
248 established={established:?}"
249 ),
250 )
251}
252
253pub(super) fn diagnostic_validation_ledger(
254 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
255) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
256 if let Some(Ok(recovered)) = recovered {
257 return Ok(Cow::Borrowed(recovered));
258 }
259 if let Some(Err(err)) = recovered {
260 if matches!(
262 err,
263 LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
264 ) {
265 return Ok(Cow::Owned(RecoveredLedger::from_trusted_ledger(
266 AllocationLedger::empty_genesis(),
267 )));
268 }
269 let code = if matches!(
270 err,
271 LedgerCommitError::PayloadEnvelope(
272 LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
273 )
274 ) {
275 DiagnosticCode::UnsupportedFormat
276 } else {
277 DiagnosticCode::LedgerRecovery
278 };
279 return Err(DiagnosticFailure::new(
280 code,
281 format!("protected ledger recovery: {err}"),
282 ));
283 }
284 Err(DiagnosticFailure::new(
285 DiagnosticCode::StableCell,
286 "stable-cell ledger record is not readable",
287 ))
288}