use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
use crate::{
AllocationHistory, AllocationLedger, AllocationPolicy, AllocationSlotDescriptor,
DiagnosticCheck, DiagnosticCode, DiagnosticDeclaration, DiagnosticExport, DiagnosticFailure,
DiagnosticMemorySize, DiagnosticRangeAuthority, DiagnosticRuntimeBinding, DiagnosticStableCell,
DiagnosticStableCellStatus, LedgerCommitError, LedgerPayloadEnvelopeError,
MemoryRuntimeDoctorReport, PolicyIdentity, RecoveredLedger, RuntimeBootstrapPolicy,
StableCellLedgerRecord,
physical::CommitStoreDiagnostic,
registry::{SealedDeclarationFingerprint, SealedDeclarationSnapshot},
slot::MEMORY_MANAGER_LEDGER_ID,
stable_cell::decode_stable_cell_ledger_record_from_memory,
};
use ic_stable_structures::Memory;
use std::{borrow::Cow, fmt::Display};
impl<M: Memory> MemoryRuntime<M> {
pub fn diagnostic_export(&self) -> Result<DiagnosticExport, RuntimeDiagnosticError> {
if !self.is_bootstrapped() {
return Err(RuntimeDiagnosticError::NotBootstrapped);
}
let record = self.ledger_record_from_memory()?;
let (recovered, commit_recovery) = record.store().recover_with_diagnostic();
let recovered = recovered?;
let ledger = recovered.ledger();
Ok(
DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
ledger,
ledger_anchor_descriptor(),
Some(commit_recovery),
self.memory_sizes(&recovered),
),
)
}
pub fn commit_recovery_diagnostic(
&self,
) -> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
let record = self.ledger_record_from_memory()?;
Ok(record.store().physical().diagnostic())
}
#[must_use]
pub fn doctor_report<P>(
&self,
declarations: &SealedDeclarationSnapshot,
policy: &P,
) -> MemoryRuntimeDoctorReport
where
P: RuntimeBootstrapPolicy,
P::Error: Display,
{
let stable_cell = self.stable_cell_diagnostic();
let (recovered, commit_recovery) = stable_cell
.record
.as_ref()
.map(|record| record.store().recover_with_diagnostic())
.map_or((None, None), |(recovered, diagnostic)| {
(Some(recovered), Some(diagnostic))
});
let recovered_for_export = recovered.as_ref().and_then(|result| result.as_ref().ok());
let ledger = recovered_for_export.map(|recovered| {
DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
recovered.ledger(),
ledger_anchor_descriptor(),
commit_recovery,
self.memory_sizes(recovered),
)
});
let diagnostic_declarations = declarations
.registered_declarations()
.iter()
.map(|registration| {
DiagnosticDeclaration::new(
registration.authority(),
registration.declaration().clone(),
)
})
.collect();
let registered_records = declarations
.registered_ranges()
.iter()
.map(|registration| registration.record().clone())
.collect();
let range_authority = DiagnosticRangeAuthority::new(
registered_records,
declarations.range_authority().clone(),
);
let tested_policy_identity = policy
.runtime_bootstrap_identity()
.map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
let tested_declaration_fingerprint = declarations.fingerprint();
let established_bootstrap_binding = self.established_bootstrap_binding();
let bootstrap_binding = diagnostic_bootstrap_binding(
&tested_policy_identity,
tested_declaration_fingerprint,
established_bootstrap_binding.as_ref(),
);
let validation = match &tested_policy_identity {
Ok(_) => diagnostic_validation(declarations, policy, recovered.as_ref()),
Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
};
MemoryRuntimeDoctorReport {
bootstrapped: self.is_bootstrapped(),
tested_policy_identity,
tested_declaration_fingerprint,
established_bootstrap_binding,
bootstrap_binding,
ledger_anchor: ledger_anchor_descriptor(),
stable_cell: stable_cell.diagnostic,
commit_recovery,
ledger,
registered_declarations: diagnostic_declarations,
range_authority,
validation,
}
}
fn memory_sizes<'a>(
&'a self,
recovered: &'a RecoveredLedger,
) -> impl Iterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)> + 'a {
recovered
.ledger()
.allocation_history()
.records()
.iter()
.map(move |record| {
let id = record
.slot()
.memory_manager_id()
.expect("recovered ledger slot");
(
record.slot().clone(),
DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
)
})
}
fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
match &self.lifecycle {
RuntimeLifecycle::Unbootstrapped => None,
RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
binding.policy_identity.clone(),
binding.source.fingerprint(),
)),
}
}
fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
if memory.size() == 0 {
return StableCellDiagnostic {
diagnostic: DiagnosticStableCell::new(
DiagnosticStableCellStatus::Empty,
memory_size,
),
record: Some(StableCellLedgerRecord::default()),
};
}
match decode_stable_cell_ledger_record_from_memory(&memory) {
Ok(record) => StableCellDiagnostic {
diagnostic: DiagnosticStableCell::new(
DiagnosticStableCellStatus::Readable,
memory_size,
),
record: Some(record),
},
Err(err) => StableCellDiagnostic {
diagnostic: DiagnosticStableCell::new(
DiagnosticStableCellStatus::Corrupt {
failure: DiagnosticFailure::new(
DiagnosticCode::StableCell,
err.to_string(),
),
},
memory_size,
),
record: None,
},
}
}
}
struct StableCellDiagnostic {
diagnostic: DiagnosticStableCell,
record: Option<StableCellLedgerRecord>,
}
const fn ledger_anchor_descriptor() -> AllocationSlotDescriptor {
AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
}
fn diagnostic_validation<P: AllocationPolicy>(
declarations: &SealedDeclarationSnapshot,
custom_policy: &P,
recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
) -> DiagnosticCheck
where
P::Error: Display,
{
let recovered = match diagnostic_validation_ledger(recovered) {
Ok(recovered) => recovered,
Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
};
let resolved = match declarations.resolve(recovered.ledger(), Vec::new()) {
Ok(resolved) => resolved,
Err(err) => {
return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
}
};
let policy = super::policy::RuntimeMemoryManagerPolicy {
declarations: &resolved,
custom_policy,
};
match crate::validate_allocations(&recovered, resolved.allocation_snapshot().clone(), &policy) {
Ok(_) => DiagnosticCheck::passed(),
Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
}
}
fn diagnostic_bootstrap_binding(
tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
tested_declaration_fingerprint: SealedDeclarationFingerprint,
established: Option<&DiagnosticRuntimeBinding>,
) -> DiagnosticCheck {
let tested_policy_identity = match tested_policy_identity {
Ok(identity) => identity,
Err(failure) => {
return DiagnosticCheck::not_run(failure.code, failure.message.clone());
}
};
let Some(established) = established else {
return DiagnosticCheck::not_run(
DiagnosticCode::RuntimeBinding,
"runtime has not completed bootstrap",
);
};
if &established.policy_identity == tested_policy_identity
&& established.declaration_fingerprint == tested_declaration_fingerprint
{
return DiagnosticCheck::passed();
}
DiagnosticCheck::failed(
DiagnosticCode::RuntimeBinding,
format!(
"tested policy/declaration binding differs from established runtime binding: \
tested_policy={tested_policy_identity:?}, \
tested_declarations={tested_declaration_fingerprint:?}, \
established={established:?}"
),
)
}
pub(super) fn diagnostic_validation_ledger(
recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
if let Some(Ok(recovered)) = recovered {
return Ok(Cow::Borrowed(recovered));
}
if let Some(Err(err)) = recovered {
if matches!(
err,
LedgerCommitError::Recovery(crate::CommitRecoveryError::NoValidGeneration)
) {
return diagnostic_genesis_recovered_ledger().map(Cow::Owned);
}
let code = if matches!(
err,
LedgerCommitError::PayloadEnvelope(
LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
)
) {
DiagnosticCode::UnsupportedFormat
} else {
DiagnosticCode::LedgerRecovery
};
return Err(DiagnosticFailure::new(
code,
format!("protected ledger recovery: {err}"),
));
}
Err(DiagnosticFailure::new(
DiagnosticCode::StableCell,
"stable-cell ledger record is not readable",
))
}
fn diagnostic_genesis_recovered_ledger() -> Result<crate::RecoveredLedger, DiagnosticFailure> {
AllocationLedger::new(0, AllocationHistory::default())
.map(crate::RecoveredLedger::from_trusted_ledger)
.map_err(|err| {
DiagnosticFailure::new(
DiagnosticCode::GenesisLedger,
format!("genesis ledger: {err}"),
)
})
}