1use alloc::vec::Vec;
10
11use crate::algebra::{
12 BaselineError, MandatoryCapacity, ResourceDimension, ResourceVector, WideResourceVector,
13 mandatory_capacity, retained_baseline, zero_debt_admission,
14};
15use crate::wire::{BindingEpoch, ClosureCheckedEnvelope, Generation, ParticipantIndex};
16
17use super::admission::{
18 OrderAdmissionError, OrderClaims, OrderHigh, OrderLedger, ResultingOrderClaims,
19 ResultingSequenceState, SequenceAdmissionError, SequenceClaims, SequenceLedger,
20};
21use super::{
22 ClosureAccounting, ClosureAccountingError, ClosureDebt, ClosureState, ObserverProjection,
23 RemainingClosureDecision, RequiredCapacityPlan, StoredEdge, check_remaining_closure,
24};
25
26const MAX_CHURN_LIMIT: u64 = u32::MAX as u64;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct InitialEnrollmentClosureInput {
32 accounting: ClosureAccounting,
33 identity_slots: u64,
34 mandatory_bound: ResourceVector,
35 recovery_claim: ResourceVector,
36 marker_max: ResourceVector,
37 attached_charge: ResourceVector,
38 participant_index: ParticipantIndex,
39 binding_epoch: BindingEpoch,
40 order: OrderLedger,
41 sequence: SequenceLedger,
42 physical_floor: u128,
43 observer_progress: u64,
44}
45
46impl InitialEnrollmentClosureInput {
47 #[allow(clippy::too_many_arguments)]
53 #[must_use]
54 pub const fn new(
55 accounting: ClosureAccounting,
56 identity_slots: u64,
57 mandatory_bound: ResourceVector,
58 recovery_claim: ResourceVector,
59 marker_max: ResourceVector,
60 attached_charge: ResourceVector,
61 participant_index: ParticipantIndex,
62 binding_epoch: BindingEpoch,
63 order: OrderLedger,
64 sequence: SequenceLedger,
65 physical_floor: u128,
66 observer_progress: u64,
67 ) -> Self {
68 Self {
69 accounting,
70 identity_slots,
71 mandatory_bound,
72 recovery_claim,
73 marker_max,
74 attached_charge,
75 participant_index,
76 binding_epoch,
77 order,
78 sequence,
79 physical_floor,
80 observer_progress,
81 }
82 }
83
84 #[must_use]
86 pub const fn with_binding_epoch(mut self, binding_epoch: BindingEpoch) -> Self {
87 self.binding_epoch = binding_epoch;
88 self
89 }
90
91 #[must_use]
93 pub const fn with_attached_charge(mut self, attached_charge: ResourceVector) -> Self {
94 self.attached_charge = attached_charge;
95 self
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub enum RecoveryQuartetStatus {
102 None,
104 Endowed,
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct PlannedEnrollmentMarker {
115 participant_index: ParticipantIndex,
116 planned_delivery_seq: u64,
117}
118
119impl PlannedEnrollmentMarker {
120 #[must_use]
122 pub const fn participant_index(self) -> ParticipantIndex {
123 self.participant_index
124 }
125
126 #[must_use]
128 pub const fn planned_delivery_seq(self) -> u64 {
129 self.planned_delivery_seq
130 }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
135pub struct InitialEnrollmentClosureProjection {
136 current_accounting: ClosureAccounting,
137 resulting_accounting: ClosureAccounting,
138 resulting_retained_charge: ResourceVector,
139 resulting_floor: u128,
140 resulting_baseline: WideResourceVector,
141 remaining_recovery_claim: ResourceVector,
142 capacity: MandatoryCapacity,
143 required_capacity: RequiredCapacityPlan,
144 recovery_quartet: RecoveryQuartetStatus,
145 new_markers: Vec<PlannedEnrollmentMarker>,
146 order: OrderLedger,
147 sequence: SequenceLedger,
148 participant_index: ParticipantIndex,
149 identity_slots: u64,
150 binding_epoch: BindingEpoch,
151}
152
153impl InitialEnrollmentClosureProjection {
154 #[must_use]
156 pub const fn resulting_closure_accounting(&self) -> ClosureAccounting {
157 self.resulting_accounting
158 }
159
160 #[must_use]
162 pub const fn resulting_closure_state(&self) -> ClosureState {
163 self.resulting_accounting.state()
164 }
165
166 #[must_use]
168 pub const fn resulting_retained_charge(&self) -> ResourceVector {
169 self.resulting_retained_charge
170 }
171
172 #[must_use]
174 pub const fn resulting_floor(&self) -> u128 {
175 self.resulting_floor
176 }
177
178 #[must_use]
180 pub const fn resulting_baseline(&self) -> WideResourceVector {
181 self.resulting_baseline
182 }
183
184 #[must_use]
186 pub const fn debt(&self) -> WideResourceVector {
187 self.capacity.debt
188 }
189
190 #[must_use]
193 pub const fn remaining_recovery_claim(&self) -> ResourceVector {
194 self.remaining_recovery_claim
195 }
196
197 #[must_use]
199 pub const fn recovery_quartet(&self) -> RecoveryQuartetStatus {
200 self.recovery_quartet
201 }
202
203 #[must_use]
205 pub fn new_marker_candidates(&self) -> &[PlannedEnrollmentMarker] {
206 &self.new_markers
207 }
208
209 #[must_use]
212 pub const fn required_capacity(&self) -> RequiredCapacityPlan {
213 self.required_capacity
214 }
215
216 #[must_use]
218 pub const fn participant_index(&self) -> ParticipantIndex {
219 self.participant_index
220 }
221
222 #[must_use]
224 pub const fn identity_slots(&self) -> u64 {
225 self.identity_slots
226 }
227
228 #[must_use]
230 pub const fn binding_epoch(&self) -> BindingEpoch {
231 self.binding_epoch
232 }
233
234 #[must_use]
236 pub const fn current_order(&self) -> OrderLedger {
237 self.order
238 }
239
240 #[must_use]
242 pub const fn observer_progress(&self) -> u64 {
243 0
244 }
245
246 pub(crate) fn plan_order(&self) -> Result<ResultingOrderClaims, OrderAdmissionError> {
254 self.order.plan_enrollment_with_recovery_quartet(matches!(
255 self.recovery_quartet,
256 RecoveryQuartetStatus::Endowed
257 ))
258 }
259
260 pub(crate) fn plan_sequence(&self) -> Result<ResultingSequenceState, SequenceAdmissionError> {
267 let marker_count = u64::try_from(self.new_markers.len()).map_err(|_| {
268 SequenceAdmissionError::MarkerClaimOverflow {
269 markers: self.sequence.claims().markers(),
270 new_markers: u64::MAX,
271 }
272 })?;
273 self.sequence.plan_enrollment_with_recovery_quartet(
274 marker_count,
275 matches!(self.recovery_quartet, RecoveryQuartetStatus::Endowed),
276 )
277 }
278
279 #[must_use]
284 pub fn remaining_closure_decision(
285 &self,
286 request: &ClosureCheckedEnvelope,
287 ) -> RemainingClosureDecision {
288 check_remaining_closure(
289 request,
290 self.current_accounting,
291 false,
292 0,
293 self.required_capacity,
294 )
295 }
296}
297
298#[derive(Clone, Debug, PartialEq, Eq)]
300pub enum InitialEnrollmentClosureError {
301 ClosureNotClear,
303 ClearOwnsEdgeResources,
305 ClearChurnNotReset {
307 used: u64,
309 },
310 ChurnLimit {
312 configured: u64,
314 },
315 ZeroIdentitySlots,
317 RecoveryClaimDiffersFromMandatoryBound,
319 ParticipantIndexOutsideIdentityLimit {
321 participant_index: ParticipantIndex,
323 identity_slots: u64,
325 },
326 InitialParticipantIndexNotZero {
328 participant_index: ParticipantIndex,
330 },
331 BindingGeneration {
333 generation: Generation,
335 },
336 NonemptyOrderLedger,
338 NonemptySequenceLedger,
340 InitialFloorOrObserver {
342 physical_floor: u128,
344 observer_progress: u64,
346 },
347 NonemptyMarkerState {
349 credits: u64,
351 anchors: u64,
353 },
354 BaselineMismatch {
356 derived: WideResourceVector,
358 durable: WideResourceVector,
360 },
361 StartupEnvelope {
363 dimension: ResourceDimension,
365 },
366 AttachedChargeExceedsMandatoryBound {
368 dimension: ResourceDimension,
370 },
371 AttachedEntryCharge {
373 actual: u64,
375 },
376 RetainedChargeOverflow {
378 dimension: ResourceDimension,
380 },
381 MandatoryCapacity,
383 ResultingAccounting(ClosureAccountingError),
385 Baseline(BaselineError),
387}
388
389pub fn project_initial_enrollment_closure(
402 input: InitialEnrollmentClosureInput,
403) -> Result<InitialEnrollmentClosureProjection, InitialEnrollmentClosureError> {
404 validate_initial_input(&input)?;
405
406 let initial_baseline = retained_baseline(
407 ResourceVector::default(),
408 input.identity_slots,
409 0,
410 input.marker_max,
411 )
412 .map_err(InitialEnrollmentClosureError::Baseline)?;
413 let resulting_retained_charge = input.attached_charge;
414 let resulting_baseline = retained_baseline(
415 resulting_retained_charge,
416 input.identity_slots,
417 0,
418 input.marker_max,
419 )
420 .map_err(InitialEnrollmentClosureError::Baseline)?;
421 let capacity = mandatory_capacity(
422 resulting_baseline,
423 input.mandatory_bound,
424 input.recovery_claim,
425 input.accounting.configured_cap(),
426 );
427 if !capacity.is_legal() {
428 return Err(InitialEnrollmentClosureError::MandatoryCapacity);
429 }
430
431 let (state, remaining_recovery_claim, edge_sequence_claims, edge_order_claims) =
432 ClosureDebt::new(capacity.debt).map_or_else(
433 || (ClosureState::Clear, ResourceVector::default(), 0, 0),
434 |debt| {
435 (
436 ClosureState::Owed {
437 debt,
438 edge: StoredEdge::ObserverProjection(ObserverProjection::new(1)),
439 },
440 input.recovery_claim,
441 0,
442 0,
443 )
444 },
445 );
446
447 let resulting_accounting = ClosureAccounting::try_new(
448 state,
449 0,
450 0,
451 edge_sequence_claims,
452 edge_order_claims,
453 remaining_recovery_claim,
454 resulting_baseline,
455 input.accounting.configured_cap(),
456 0,
457 input.accounting.episode_churn_limit(),
458 )
459 .map_err(InitialEnrollmentClosureError::ResultingAccounting)?;
460
461 let immediate_required = if capacity.debt.is_zero() {
462 checked_sum(
463 resulting_baseline,
464 input.mandatory_bound,
465 input.recovery_claim,
466 )?
467 } else {
468 checked_sum(
469 resulting_baseline,
470 ResourceVector::default(),
471 input.recovery_claim,
472 )?
473 };
474 let clear_successor_required = checked_sum(
475 initial_baseline,
476 input.mandatory_bound,
477 input.recovery_claim,
478 )?;
479 let required_capacity =
480 RequiredCapacityPlan::from_successors(&[immediate_required, clear_successor_required])
481 .map_err(|_| InitialEnrollmentClosureError::MandatoryCapacity)?;
482
483 Ok(InitialEnrollmentClosureProjection {
484 current_accounting: input.accounting,
485 resulting_accounting,
486 resulting_retained_charge,
487 resulting_floor: 1,
488 resulting_baseline,
489 remaining_recovery_claim,
490 capacity,
491 required_capacity,
492 recovery_quartet: RecoveryQuartetStatus::None,
493 new_markers: Vec::new(),
494 order: input.order,
495 sequence: input.sequence,
496 participant_index: input.participant_index,
497 identity_slots: input.identity_slots,
498 binding_epoch: input.binding_epoch,
499 })
500}
501
502fn validate_initial_input(
503 input: &InitialEnrollmentClosureInput,
504) -> Result<(), InitialEnrollmentClosureError> {
505 if input.accounting.state() != ClosureState::Clear {
506 return Err(InitialEnrollmentClosureError::ClosureNotClear);
507 }
508 if input.accounting.edge_sequence_claims() != 0
509 || input.accounting.edge_order_position_claims() != 0
510 || input.accounting.edge_k_remaining() != ResourceVector::default()
511 {
512 return Err(InitialEnrollmentClosureError::ClearOwnsEdgeResources);
513 }
514 if input.accounting.episode_churn_used() != 0 {
515 return Err(InitialEnrollmentClosureError::ClearChurnNotReset {
516 used: input.accounting.episode_churn_used(),
517 });
518 }
519 let churn_limit = input.accounting.episode_churn_limit();
520 if !(2..=MAX_CHURN_LIMIT).contains(&churn_limit) {
521 return Err(InitialEnrollmentClosureError::ChurnLimit {
522 configured: churn_limit,
523 });
524 }
525 if input.identity_slots == 0 {
526 return Err(InitialEnrollmentClosureError::ZeroIdentitySlots);
527 }
528 if input.mandatory_bound != input.recovery_claim {
529 return Err(InitialEnrollmentClosureError::RecoveryClaimDiffersFromMandatoryBound);
530 }
531 if input.participant_index >= input.identity_slots {
532 return Err(
533 InitialEnrollmentClosureError::ParticipantIndexOutsideIdentityLimit {
534 participant_index: input.participant_index,
535 identity_slots: input.identity_slots,
536 },
537 );
538 }
539 if input.participant_index != 0 {
540 return Err(
541 InitialEnrollmentClosureError::InitialParticipantIndexNotZero {
542 participant_index: input.participant_index,
543 },
544 );
545 }
546 if input.binding_epoch.capability_generation != Generation::ONE {
547 return Err(InitialEnrollmentClosureError::BindingGeneration {
548 generation: input.binding_epoch.capability_generation,
549 });
550 }
551 validate_initial_ledgers_and_floor(input)?;
552 validate_initial_capacity(input)
553}
554
555fn validate_initial_ledgers_and_floor(
556 input: &InitialEnrollmentClosureInput,
557) -> Result<(), InitialEnrollmentClosureError> {
558 if input.order.high() != OrderHigh::Empty || input.order.claims() != OrderClaims::default() {
559 return Err(InitialEnrollmentClosureError::NonemptyOrderLedger);
560 }
561 if input.sequence.high_watermark() != 0 || input.sequence.claims() != SequenceClaims::default()
562 {
563 return Err(InitialEnrollmentClosureError::NonemptySequenceLedger);
564 }
565 if input.physical_floor != 1 || input.observer_progress != 0 {
566 return Err(InitialEnrollmentClosureError::InitialFloorOrObserver {
567 physical_floor: input.physical_floor,
568 observer_progress: input.observer_progress,
569 });
570 }
571 if input.accounting.marker_capacity_credits() != 0 || input.accounting.marker_anchors() != 0 {
572 return Err(InitialEnrollmentClosureError::NonemptyMarkerState {
573 credits: input.accounting.marker_capacity_credits(),
574 anchors: input.accounting.marker_anchors(),
575 });
576 }
577 Ok(())
578}
579
580fn validate_initial_capacity(
581 input: &InitialEnrollmentClosureInput,
582) -> Result<(), InitialEnrollmentClosureError> {
583 let derived_baseline = retained_baseline(
584 ResourceVector::default(),
585 input.identity_slots,
586 0,
587 input.marker_max,
588 )
589 .map_err(InitialEnrollmentClosureError::Baseline)?;
590 if derived_baseline != input.accounting.baseline() {
591 return Err(InitialEnrollmentClosureError::BaselineMismatch {
592 derived: derived_baseline,
593 durable: input.accounting.baseline(),
594 });
595 }
596 if let Some(dimension) = startup_envelope_failure(
597 derived_baseline,
598 input.mandatory_bound,
599 input.recovery_claim,
600 input.accounting.configured_cap(),
601 ) {
602 return Err(InitialEnrollmentClosureError::StartupEnvelope { dimension });
603 }
604 if input.attached_charge.entries != 1 {
605 return Err(InitialEnrollmentClosureError::AttachedEntryCharge {
606 actual: input.attached_charge.entries,
607 });
608 }
609 if input.attached_charge.entries > input.mandatory_bound.entries {
610 return Err(
611 InitialEnrollmentClosureError::AttachedChargeExceedsMandatoryBound {
612 dimension: ResourceDimension::Entries,
613 },
614 );
615 }
616 if input.attached_charge.bytes > input.mandatory_bound.bytes {
617 return Err(
618 InitialEnrollmentClosureError::AttachedChargeExceedsMandatoryBound {
619 dimension: ResourceDimension::Bytes,
620 },
621 );
622 }
623 Ok(())
624}
625
626const fn startup_envelope_failure(
627 baseline: WideResourceVector,
628 mandatory_bound: ResourceVector,
629 recovery_claim: ResourceVector,
630 configured_cap: ResourceVector,
631) -> Option<ResourceDimension> {
632 if zero_debt_admission(baseline, mandatory_bound, recovery_claim, configured_cap) {
633 None
634 } else {
635 crate::algebra::zero_debt_capacity_failure(
636 baseline,
637 mandatory_bound,
638 recovery_claim,
639 configured_cap,
640 )
641 }
642}
643
644fn checked_sum(
645 baseline: WideResourceVector,
646 middle: ResourceVector,
647 last: ResourceVector,
648) -> Result<WideResourceVector, InitialEnrollmentClosureError> {
649 let Some(entries) = baseline
650 .entries
651 .checked_add(u128::from(middle.entries))
652 .and_then(|value| value.checked_add(u128::from(last.entries)))
653 else {
654 return Err(InitialEnrollmentClosureError::RetainedChargeOverflow {
655 dimension: ResourceDimension::Entries,
656 });
657 };
658 let Some(bytes) = baseline
659 .bytes
660 .checked_add(u128::from(middle.bytes))
661 .and_then(|value| value.checked_add(u128::from(last.bytes)))
662 else {
663 return Err(InitialEnrollmentClosureError::RetainedChargeOverflow {
664 dimension: ResourceDimension::Bytes,
665 });
666 };
667 Ok(WideResourceVector::new(entries, bytes))
668}