use crate::fleet_ensure::{
model::{ActualCycleConservation, EnsureAction, FleetEnsureStateRecord, FleetObservation},
view::terminal_source::TerminalSourceView,
};
use std::collections::{BTreeMap, BTreeSet};
pub(in crate::fleet_ensure) fn conservation(
source: &TerminalSourceView,
observation: &FleetObservation,
controlled_cycles: u128,
) -> Option<ActualCycleConservation> {
let mut funding = 0_u128;
let mut payments = 0_u128;
for action in &source.actions {
if let EnsureAction::Fund { amount, ledger, .. } = action {
if ledger != &source.reviewed_desired.desired().cycles_ledger {
return None;
}
funding = funding.checked_add(*amount)?;
payments = payments.checked_add(1)?;
}
}
let fees = super::super::cycle_bounds(source.reviewed_desired.desired())
.ok()?
.ledger_fee
.checked_mul(payments)?;
let debit = funding.checked_add(fees)?;
let bounds = &source.conservation;
if bounds.scheduled_transfer_cycles != 0
|| funding != bounds.maximum_new_funding_cycles
|| fees != bounds.maximum_unavoidable_fee_cycles
|| debit != bounds.maximum_operator_debit_cycles
|| source
.journal
.initial_operator_cycles
.checked_sub(observation.operator_cycles)?
!= debit
|| !domains_match(source, observation)
{
return None;
}
let accounted = source
.journal
.initial_controlled_cycles
.checked_add(funding)?;
let burn = accounted.saturating_sub(controlled_cycles);
if burn > bounds.maximum_execution_burn_cycles {
return None;
}
Some(ActualCycleConservation {
estate_funding_cycles: 0,
exact_estate_creation_fee_cycles: 0,
exact_unavoidable_fee_cycles: fees,
final_controlled_cycles: controlled_cycles,
observed_net_cycle_debit_cycles: burn,
observed_starting_cycles: source.journal.initial_controlled_cycles,
observed_net_cycle_credit_cycles: controlled_cycles.saturating_sub(accounted),
operator_debit_cycles: debit,
received_new_funding_cycles: funding,
})
}
fn domains_match(source: &TerminalSourceView, observation: &FleetObservation) -> bool {
let domains = &source.conservation.estate_funding_domains;
if domains.len() != observation.estate_funding_domains.len()
|| domains.len() != source.journal.initial_estate_funding_cycles_by_root.len()
{
return false;
}
let mut roots = BTreeSet::new();
domains.iter().all(|domain| {
let Some(observed) = observation.estate_funding_domains.get(&domain.root) else {
return false;
};
let Some(pool) = &observed.pool else {
return false;
};
let identities: BTreeSet<_> = pool.assets.iter().map(|asset| &asset.principal).collect();
let initial: BTreeSet<_> = domain.initial_pool_assets.iter().collect();
let no_creation = domain.required_creation_count == 0
&& domain.maximum_creation_debit_cycles == 0
&& domain.maximum_creation_fee_cycles == 0
&& domain.maximum_funding_cycles == 0
&& domain.pending_creation_count == 0
&& domain.pending_creation.is_none()
&& pool.pending_creation.is_none();
let authority_matches = domain.root_principal.is_some()
&& domain.cycles_ledger == observed.cycles_ledger
&& domain.root_principal == observed.root_principal;
let balance_matches = domain.available_cycles.is_some()
&& domain.available_cycles == observed.balance_cycles
&& source
.journal
.initial_estate_funding_cycles_by_root
.get(&domain.root)
.copied()
== domain.available_cycles;
let pool_matches = identities.len() == pool.assets.len()
&& initial.len() == domain.initial_pool_assets.len()
&& identities == initial;
no_creation
&& roots.insert(&domain.root)
&& authority_matches
&& balance_matches
&& pool_matches
})
}
pub(in crate::fleet_ensure) fn inventory_matches(
state: &FleetEnsureStateRecord,
entries: &[crate::registry::RegistryEntry],
) -> bool {
let names: BTreeMap<_, _> = state
.principals
.iter()
.map(|(name, principal)| (principal, name))
.collect();
let observed: BTreeSet<_> = entries.iter().map(|entry| &entry.pid).collect();
let complete = names.len() == state.principals.len()
&& names.len() == state.topology.len()
&& names.len() == entries.len()
&& observed.len() == entries.len();
if !complete {
return false;
}
entries.iter().all(|entry| {
let Some(topology) = names
.get(&entry.pid)
.and_then(|name| state.topology.get(*name))
else {
return false;
};
let parent = match &topology.parent {
Some(name) => match state.principals.get(name) {
Some(principal) => Some(principal.as_str()),
None => return false,
},
None => None,
};
let expected = (
&topology.module_hash,
&topology.protocol_binding,
&topology.role,
parent,
);
let observed = (
&entry.module_hash,
&entry.protocol_binding,
&entry.role,
entry.parent_pid.as_deref(),
);
expected == observed
})
}
pub(in crate::fleet_ensure) fn external_debit_conservation(
source: &TerminalSourceView,
observation: &FleetObservation,
controlled_cycles: u128,
state: &FleetEnsureStateRecord,
debit: &crate::fleet_ensure::model::RetirementWithdrawalRecord,
) -> Option<ActualCycleConservation> {
let desired = source.reviewed_desired.desired();
let external = debit.amount_cycles.checked_add(debit.fee_cycles)?;
let source_has_payment = source
.actions
.iter()
.any(|action| matches!(action, EnsureAction::Fund { .. }));
let binding_matches =
(&debit.ledger, &debit.operator) == (&desired.cycles_ledger, &desired.operator);
let outside_fleet = !contains_destination(state, observation, &debit.destination);
if !binding_matches
|| !outside_fleet
|| source_has_payment
|| source.conservation.maximum_operator_debit_cycles != 0
|| debit.timestamp_ns <= source.planned_at_time
|| debit.amount_cycles == 0
|| debit.fee_cycles == 0
{
return None;
}
let mut accounted = observation.clone();
accounted.operator_cycles = accounted.operator_cycles.checked_add(external)?;
conservation(source, &accounted, controlled_cycles)
}
fn contains_destination(
state: &FleetEnsureStateRecord,
observation: &FleetObservation,
destination: &str,
) -> bool {
let recorded = state
.principals
.values()
.chain(state.pending_principals.values())
.any(|id| id == destination)
|| state.retained_cycles_by_principal.contains_key(destination);
let observed = observation
.additional_controlled_cycles
.contains_key(destination)
|| observation
.canisters
.values()
.flatten()
.any(|live| live.principal == destination);
let estate = observation.estate_funding_domains.values().any(|domain| {
domain.root_principal.as_deref() == Some(destination)
|| domain.pool.as_ref().is_some_and(|pool| {
pool.assets
.iter()
.any(|asset| asset.principal == destination)
})
});
recorded || observed || estate
}