use alloc::vec::Vec;
use crate::algebra::{
BaselineError, MandatoryCapacity, ResourceDimension, ResourceVector, WideResourceVector,
mandatory_capacity, retained_baseline, zero_debt_admission,
};
use crate::wire::{BindingEpoch, ClosureCheckedEnvelope, Generation, ParticipantIndex};
use super::admission::{
OrderAdmissionError, OrderClaims, OrderHigh, OrderLedger, ResultingOrderClaims,
ResultingSequenceState, SequenceAdmissionError, SequenceClaims, SequenceLedger,
};
use super::{
ClosureAccounting, ClosureAccountingError, ClosureDebt, ClosureState, ObserverProjection,
RemainingClosureDecision, RequiredCapacityPlan, StoredEdge, check_remaining_closure,
};
const MAX_CHURN_LIMIT: u64 = u32::MAX as u64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InitialEnrollmentClosureInput {
accounting: ClosureAccounting,
identity_slots: u64,
mandatory_bound: ResourceVector,
recovery_claim: ResourceVector,
marker_max: ResourceVector,
attached_charge: ResourceVector,
participant_index: ParticipantIndex,
binding_epoch: BindingEpoch,
order: OrderLedger,
sequence: SequenceLedger,
physical_floor: u128,
observer_progress: u64,
}
impl InitialEnrollmentClosureInput {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub const fn new(
accounting: ClosureAccounting,
identity_slots: u64,
mandatory_bound: ResourceVector,
recovery_claim: ResourceVector,
marker_max: ResourceVector,
attached_charge: ResourceVector,
participant_index: ParticipantIndex,
binding_epoch: BindingEpoch,
order: OrderLedger,
sequence: SequenceLedger,
physical_floor: u128,
observer_progress: u64,
) -> Self {
Self {
accounting,
identity_slots,
mandatory_bound,
recovery_claim,
marker_max,
attached_charge,
participant_index,
binding_epoch,
order,
sequence,
physical_floor,
observer_progress,
}
}
#[must_use]
pub const fn with_binding_epoch(mut self, binding_epoch: BindingEpoch) -> Self {
self.binding_epoch = binding_epoch;
self
}
#[must_use]
pub const fn with_attached_charge(mut self, attached_charge: ResourceVector) -> Self {
self.attached_charge = attached_charge;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RecoveryQuartetStatus {
None,
Endowed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlannedEnrollmentMarker {
participant_index: ParticipantIndex,
planned_delivery_seq: u64,
}
impl PlannedEnrollmentMarker {
#[must_use]
pub const fn participant_index(self) -> ParticipantIndex {
self.participant_index
}
#[must_use]
pub const fn planned_delivery_seq(self) -> u64 {
self.planned_delivery_seq
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InitialEnrollmentClosureProjection {
current_accounting: ClosureAccounting,
resulting_accounting: ClosureAccounting,
resulting_retained_charge: ResourceVector,
resulting_floor: u128,
resulting_baseline: WideResourceVector,
remaining_recovery_claim: ResourceVector,
capacity: MandatoryCapacity,
required_capacity: RequiredCapacityPlan,
recovery_quartet: RecoveryQuartetStatus,
new_markers: Vec<PlannedEnrollmentMarker>,
order: OrderLedger,
sequence: SequenceLedger,
participant_index: ParticipantIndex,
identity_slots: u64,
binding_epoch: BindingEpoch,
}
impl InitialEnrollmentClosureProjection {
#[must_use]
pub const fn resulting_closure_accounting(&self) -> ClosureAccounting {
self.resulting_accounting
}
#[must_use]
pub const fn resulting_closure_state(&self) -> ClosureState {
self.resulting_accounting.state()
}
#[must_use]
pub const fn resulting_retained_charge(&self) -> ResourceVector {
self.resulting_retained_charge
}
#[must_use]
pub const fn resulting_floor(&self) -> u128 {
self.resulting_floor
}
#[must_use]
pub const fn resulting_baseline(&self) -> WideResourceVector {
self.resulting_baseline
}
#[must_use]
pub const fn debt(&self) -> WideResourceVector {
self.capacity.debt
}
#[must_use]
pub const fn remaining_recovery_claim(&self) -> ResourceVector {
self.remaining_recovery_claim
}
#[must_use]
pub const fn recovery_quartet(&self) -> RecoveryQuartetStatus {
self.recovery_quartet
}
#[must_use]
pub fn new_marker_candidates(&self) -> &[PlannedEnrollmentMarker] {
&self.new_markers
}
#[must_use]
pub const fn required_capacity(&self) -> RequiredCapacityPlan {
self.required_capacity
}
#[must_use]
pub const fn participant_index(&self) -> ParticipantIndex {
self.participant_index
}
#[must_use]
pub const fn identity_slots(&self) -> u64 {
self.identity_slots
}
#[must_use]
pub const fn binding_epoch(&self) -> BindingEpoch {
self.binding_epoch
}
#[must_use]
pub const fn current_order(&self) -> OrderLedger {
self.order
}
#[must_use]
pub const fn observer_progress(&self) -> u64 {
0
}
pub(crate) fn plan_order(&self) -> Result<ResultingOrderClaims, OrderAdmissionError> {
self.order.plan_enrollment_with_recovery_quartet(matches!(
self.recovery_quartet,
RecoveryQuartetStatus::Endowed
))
}
pub(crate) fn plan_sequence(&self) -> Result<ResultingSequenceState, SequenceAdmissionError> {
let marker_count = u64::try_from(self.new_markers.len()).map_err(|_| {
SequenceAdmissionError::MarkerClaimOverflow {
markers: self.sequence.claims().markers(),
new_markers: u64::MAX,
}
})?;
self.sequence.plan_enrollment_with_recovery_quartet(
marker_count,
matches!(self.recovery_quartet, RecoveryQuartetStatus::Endowed),
)
}
#[must_use]
pub fn remaining_closure_decision(
&self,
request: &ClosureCheckedEnvelope,
) -> RemainingClosureDecision {
check_remaining_closure(
request,
self.current_accounting,
false,
0,
self.required_capacity,
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InitialEnrollmentClosureError {
ClosureNotClear,
ClearOwnsEdgeResources,
ClearChurnNotReset {
used: u64,
},
ChurnLimit {
configured: u64,
},
ZeroIdentitySlots,
RecoveryClaimDiffersFromMandatoryBound,
ParticipantIndexOutsideIdentityLimit {
participant_index: ParticipantIndex,
identity_slots: u64,
},
InitialParticipantIndexNotZero {
participant_index: ParticipantIndex,
},
BindingGeneration {
generation: Generation,
},
NonemptyOrderLedger,
NonemptySequenceLedger,
InitialFloorOrObserver {
physical_floor: u128,
observer_progress: u64,
},
NonemptyMarkerState {
credits: u64,
anchors: u64,
},
BaselineMismatch {
derived: WideResourceVector,
durable: WideResourceVector,
},
StartupEnvelope {
dimension: ResourceDimension,
},
AttachedChargeExceedsMandatoryBound {
dimension: ResourceDimension,
},
AttachedEntryCharge {
actual: u64,
},
RetainedChargeOverflow {
dimension: ResourceDimension,
},
MandatoryCapacity,
ResultingAccounting(ClosureAccountingError),
Baseline(BaselineError),
}
pub fn project_initial_enrollment_closure(
input: InitialEnrollmentClosureInput,
) -> Result<InitialEnrollmentClosureProjection, InitialEnrollmentClosureError> {
validate_initial_input(&input)?;
let initial_baseline = retained_baseline(
ResourceVector::default(),
input.identity_slots,
0,
input.marker_max,
)
.map_err(InitialEnrollmentClosureError::Baseline)?;
let resulting_retained_charge = input.attached_charge;
let resulting_baseline = retained_baseline(
resulting_retained_charge,
input.identity_slots,
0,
input.marker_max,
)
.map_err(InitialEnrollmentClosureError::Baseline)?;
let capacity = mandatory_capacity(
resulting_baseline,
input.mandatory_bound,
input.recovery_claim,
input.accounting.configured_cap(),
);
if !capacity.is_legal() {
return Err(InitialEnrollmentClosureError::MandatoryCapacity);
}
let (state, remaining_recovery_claim, edge_sequence_claims, edge_order_claims) =
ClosureDebt::new(capacity.debt).map_or_else(
|| (ClosureState::Clear, ResourceVector::default(), 0, 0),
|debt| {
(
ClosureState::Owed {
debt,
edge: StoredEdge::ObserverProjection(ObserverProjection::new(1)),
},
input.recovery_claim,
0,
0,
)
},
);
let resulting_accounting = ClosureAccounting::try_new(
state,
0,
0,
edge_sequence_claims,
edge_order_claims,
remaining_recovery_claim,
resulting_baseline,
input.accounting.configured_cap(),
0,
input.accounting.episode_churn_limit(),
)
.map_err(InitialEnrollmentClosureError::ResultingAccounting)?;
let immediate_required = if capacity.debt.is_zero() {
checked_sum(
resulting_baseline,
input.mandatory_bound,
input.recovery_claim,
)?
} else {
checked_sum(
resulting_baseline,
ResourceVector::default(),
input.recovery_claim,
)?
};
let clear_successor_required = checked_sum(
initial_baseline,
input.mandatory_bound,
input.recovery_claim,
)?;
let required_capacity =
RequiredCapacityPlan::from_successors(&[immediate_required, clear_successor_required])
.map_err(|_| InitialEnrollmentClosureError::MandatoryCapacity)?;
Ok(InitialEnrollmentClosureProjection {
current_accounting: input.accounting,
resulting_accounting,
resulting_retained_charge,
resulting_floor: 1,
resulting_baseline,
remaining_recovery_claim,
capacity,
required_capacity,
recovery_quartet: RecoveryQuartetStatus::None,
new_markers: Vec::new(),
order: input.order,
sequence: input.sequence,
participant_index: input.participant_index,
identity_slots: input.identity_slots,
binding_epoch: input.binding_epoch,
})
}
fn validate_initial_input(
input: &InitialEnrollmentClosureInput,
) -> Result<(), InitialEnrollmentClosureError> {
if input.accounting.state() != ClosureState::Clear {
return Err(InitialEnrollmentClosureError::ClosureNotClear);
}
if input.accounting.edge_sequence_claims() != 0
|| input.accounting.edge_order_position_claims() != 0
|| input.accounting.edge_k_remaining() != ResourceVector::default()
{
return Err(InitialEnrollmentClosureError::ClearOwnsEdgeResources);
}
if input.accounting.episode_churn_used() != 0 {
return Err(InitialEnrollmentClosureError::ClearChurnNotReset {
used: input.accounting.episode_churn_used(),
});
}
let churn_limit = input.accounting.episode_churn_limit();
if !(2..=MAX_CHURN_LIMIT).contains(&churn_limit) {
return Err(InitialEnrollmentClosureError::ChurnLimit {
configured: churn_limit,
});
}
if input.identity_slots == 0 {
return Err(InitialEnrollmentClosureError::ZeroIdentitySlots);
}
if input.mandatory_bound != input.recovery_claim {
return Err(InitialEnrollmentClosureError::RecoveryClaimDiffersFromMandatoryBound);
}
if input.participant_index >= input.identity_slots {
return Err(
InitialEnrollmentClosureError::ParticipantIndexOutsideIdentityLimit {
participant_index: input.participant_index,
identity_slots: input.identity_slots,
},
);
}
if input.participant_index != 0 {
return Err(
InitialEnrollmentClosureError::InitialParticipantIndexNotZero {
participant_index: input.participant_index,
},
);
}
if input.binding_epoch.capability_generation != Generation::ONE {
return Err(InitialEnrollmentClosureError::BindingGeneration {
generation: input.binding_epoch.capability_generation,
});
}
validate_initial_ledgers_and_floor(input)?;
validate_initial_capacity(input)
}
fn validate_initial_ledgers_and_floor(
input: &InitialEnrollmentClosureInput,
) -> Result<(), InitialEnrollmentClosureError> {
if input.order.high() != OrderHigh::Empty || input.order.claims() != OrderClaims::default() {
return Err(InitialEnrollmentClosureError::NonemptyOrderLedger);
}
if input.sequence.high_watermark() != 0 || input.sequence.claims() != SequenceClaims::default()
{
return Err(InitialEnrollmentClosureError::NonemptySequenceLedger);
}
if input.physical_floor != 1 || input.observer_progress != 0 {
return Err(InitialEnrollmentClosureError::InitialFloorOrObserver {
physical_floor: input.physical_floor,
observer_progress: input.observer_progress,
});
}
if input.accounting.marker_capacity_credits() != 0 || input.accounting.marker_anchors() != 0 {
return Err(InitialEnrollmentClosureError::NonemptyMarkerState {
credits: input.accounting.marker_capacity_credits(),
anchors: input.accounting.marker_anchors(),
});
}
Ok(())
}
fn validate_initial_capacity(
input: &InitialEnrollmentClosureInput,
) -> Result<(), InitialEnrollmentClosureError> {
let derived_baseline = retained_baseline(
ResourceVector::default(),
input.identity_slots,
0,
input.marker_max,
)
.map_err(InitialEnrollmentClosureError::Baseline)?;
if derived_baseline != input.accounting.baseline() {
return Err(InitialEnrollmentClosureError::BaselineMismatch {
derived: derived_baseline,
durable: input.accounting.baseline(),
});
}
if let Some(dimension) = startup_envelope_failure(
derived_baseline,
input.mandatory_bound,
input.recovery_claim,
input.accounting.configured_cap(),
) {
return Err(InitialEnrollmentClosureError::StartupEnvelope { dimension });
}
if input.attached_charge.entries != 1 {
return Err(InitialEnrollmentClosureError::AttachedEntryCharge {
actual: input.attached_charge.entries,
});
}
if input.attached_charge.entries > input.mandatory_bound.entries {
return Err(
InitialEnrollmentClosureError::AttachedChargeExceedsMandatoryBound {
dimension: ResourceDimension::Entries,
},
);
}
if input.attached_charge.bytes > input.mandatory_bound.bytes {
return Err(
InitialEnrollmentClosureError::AttachedChargeExceedsMandatoryBound {
dimension: ResourceDimension::Bytes,
},
);
}
Ok(())
}
const fn startup_envelope_failure(
baseline: WideResourceVector,
mandatory_bound: ResourceVector,
recovery_claim: ResourceVector,
configured_cap: ResourceVector,
) -> Option<ResourceDimension> {
if zero_debt_admission(baseline, mandatory_bound, recovery_claim, configured_cap) {
None
} else {
crate::algebra::zero_debt_capacity_failure(
baseline,
mandatory_bound,
recovery_claim,
configured_cap,
)
}
}
fn checked_sum(
baseline: WideResourceVector,
middle: ResourceVector,
last: ResourceVector,
) -> Result<WideResourceVector, InitialEnrollmentClosureError> {
let Some(entries) = baseline
.entries
.checked_add(u128::from(middle.entries))
.and_then(|value| value.checked_add(u128::from(last.entries)))
else {
return Err(InitialEnrollmentClosureError::RetainedChargeOverflow {
dimension: ResourceDimension::Entries,
});
};
let Some(bytes) = baseline
.bytes
.checked_add(u128::from(middle.bytes))
.and_then(|value| value.checked_add(u128::from(last.bytes)))
else {
return Err(InitialEnrollmentClosureError::RetainedChargeOverflow {
dimension: ResourceDimension::Bytes,
});
};
Ok(WideResourceVector::new(entries, bytes))
}