Skip to main content

liminal_protocol/lifecycle/operations/
live_frontier.rs

1//! Move-only executable ownership for live lifecycle frontier transitions.
2//!
3//! Storage supplies canonical encoded row charges, but every participant,
4//! binding, cursor, retained row, and aggregate-claim transition is derived
5//! inside the protocol from an existing sealed operation commit.
6
7use alloc::{boxed::Box, vec, vec::Vec};
8
9use crate::{algebra::ResourceVector, wire::RecordAdmission};
10
11use super::super::{
12    AttachCommit, AttachTransition, BindingState, ClaimFrontiers, ClosureAccounting, ClosureDebt,
13    CommittedDetachTransition, DebtCompletion, DetachCell, DetachedCredentialRecovery, Event,
14    FencedAttachCommit, FrontierBinding, FrontierParticipant, IdentityState,
15    InitialEnrollmentFrontierCommit, LeaveCommitError, LeaveCommitParameters, LiveMember,
16    MarkerAckCommit, NonzeroParticipantAckCommit, ObserverProgressProjection, OrderLedger,
17    ParticipantAckCommit, PendingFinalization, PendingLeaveCommitParameters,
18    PrepareLeaveAuthorityError, RetainedCausalRecord, RetainedCausalRecordKind, SequenceLedger,
19    StoredEdge, VerifiedLeaveRequest,
20    claim_frontier::{FencedMarkerSourceRecord, LiveFrontierTransitionError},
21    commit_leave, commit_pending_leave,
22};
23use super::{
24    InitialEnrollmentOperationCommit, MarkerDeliveryProjection, MarkerDrainCommit,
25    RecordAdmissionPersistenceParts, RetainedRecordCharge, UnchangedRecordAdmission,
26};
27
28mod binding_fate_transition;
29mod ledger;
30mod state;
31pub(super) use binding_fate_transition::BindingFateOwnerPlan;
32use ledger::{
33    detach_order, detach_sequence, detached_attach_order, detached_attach_sequence,
34    enrollment_order, enrollment_sequence, superseding_attach_order, superseding_attach_sequence,
35};
36use state::{
37    accounting_after_fenced_attach, accounting_after_leave, accounting_after_marker_ack,
38    accounting_after_rows, retained_attached, retained_terminal,
39};
40
41/// Complete executable frontier, closure-accounting, and keyed-retention owner.
42///
43/// The owner is intentionally move-only. It is the only live mutation input and
44/// never exposes a constructor from independent frontier/accounting components.
45/// Frontier, closure, retained charges, and participant history therefore cannot
46/// be cloned or recombined from different owners:
47///
48/// ```compile_fail
49/// use liminal_protocol::lifecycle::LiveFrontierOwner;
50///
51/// fn clone_frontier(owner: &LiveFrontierOwner) -> LiveFrontierOwner {
52///     owner.clone()
53/// }
54/// ```
55///
56/// ```compile_fail
57/// use liminal_protocol::lifecycle::LiveFrontierOwner;
58///
59/// fn splice(left: &mut LiveFrontierOwner, right: LiveFrontierOwner) {
60///     left.frontiers = right.frontiers;
61///     left.closure_accounting = right.closure_accounting;
62///     left.retained_charges = right.retained_charges;
63/// }
64/// ```
65#[derive(Debug, PartialEq, Eq)]
66pub struct LiveFrontierOwner {
67    frontiers: ClaimFrontiers,
68    closure_accounting: ClosureAccounting,
69    retained_charges: Vec<RetainedRecordCharge>,
70    retained_record_limit: u64,
71}
72
73impl LiveFrontierOwner {
74    /// Acquires live ownership from the protocol's atomic initial-enrollment result.
75    #[must_use]
76    pub fn from_initial_enrollment<F>(
77        initial: InitialEnrollmentFrontierCommit<F>,
78        retained_record_limit: u64,
79    ) -> (InitialEnrollmentOperationCommit<F>, Self) {
80        let (operation, frontiers, closure_accounting, attached_charge) =
81            initial.into_conversation_parts();
82        let attached = operation.enrollment().attached;
83        let retained_charges = vec![RetainedRecordCharge::new(
84            attached.delivery_seq(),
85            attached.admission_order(),
86            attached_charge,
87        )];
88        (
89            operation,
90            Self {
91                frontiers,
92                closure_accounting,
93                retained_charges,
94                retained_record_limit,
95            },
96        )
97    }
98
99    #[cfg(any(test, feature = "test-support"))]
100    pub(in crate::lifecycle) const fn from_test_parts(
101        frontiers: ClaimFrontiers,
102        closure_accounting: ClosureAccounting,
103        retained_charges: Vec<RetainedRecordCharge>,
104        retained_record_limit: u64,
105    ) -> Self {
106        Self {
107            frontiers,
108            closure_accounting,
109            retained_charges,
110            retained_record_limit,
111        }
112    }
113
114    /// Test-support-only checked capacity extension applied after a real
115    /// terminal selector has already chosen Pending. It cannot affect that
116    /// disposition; the caller supplies the exact encoded finalizer charges.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if exact capacity arithmetic or reconstruction fails.
121    #[cfg(any(test, feature = "test-support"))]
122    pub fn with_pending_finalizer_test_capacity(
123        mut self,
124        finalizer_rows: u64,
125        finalizer_charge: ResourceVector,
126    ) -> Result<Self, &'static str> {
127        let retained_count = u64::try_from(self.retained_charges.len())
128            .map_err(|_| "retained record count exceeds u64")?;
129        self.retained_record_limit = retained_count
130            .checked_add(finalizer_rows)
131            .ok_or("pending finalizer retained-record capacity overflow")?;
132        let current = self.closure_accounting;
133        let configured = current.configured_cap();
134        let configured = ResourceVector::new(
135            configured
136                .entries
137                .checked_add(finalizer_charge.entries)
138                .ok_or("pending finalizer entry capacity overflow")?,
139            configured
140                .bytes
141                .checked_add(finalizer_charge.bytes)
142                .ok_or("pending finalizer byte capacity overflow")?,
143        );
144        self.closure_accounting = ClosureAccounting::try_new(
145            current.state(),
146            current.marker_capacity_credits(),
147            current.marker_anchors(),
148            current.edge_sequence_claims(),
149            current.edge_order_position_claims(),
150            current.edge_k_remaining(),
151            current.baseline(),
152            configured,
153            current.episode_churn_used(),
154            current.episode_churn_limit(),
155        )
156        .map_err(|_| "pending finalizer closure capacity extension refused")?;
157        Ok(self)
158    }
159
160    /// Borrows the coupled claim frontiers.
161    #[must_use]
162    pub const fn frontiers(&self) -> &ClaimFrontiers {
163        &self.frontiers
164    }
165
166    /// Returns complete current closure accounting.
167    #[must_use]
168    pub const fn closure_accounting(&self) -> ClosureAccounting {
169        self.closure_accounting
170    }
171
172    /// Borrows canonical keyed charges for the retained suffix.
173    #[must_use]
174    pub fn retained_charges(&self) -> &[RetainedRecordCharge] {
175        &self.retained_charges
176    }
177
178    /// Returns the signed retained causal-row cap.
179    #[must_use]
180    pub const fn retained_record_limit(&self) -> u64 {
181        self.retained_record_limit
182    }
183
184    /// Consumes the complete owner for `RecordAdmission`, Leave, or persistence.
185    #[must_use]
186    pub fn into_parts(
187        self,
188    ) -> (
189        ClaimFrontiers,
190        ClosureAccounting,
191        Vec<RetainedRecordCharge>,
192        u64,
193    ) {
194        (
195            self.frontiers,
196            self.closure_accounting,
197            self.retained_charges,
198            self.retained_record_limit,
199        )
200    }
201
202    /// Restores the exact owner returned by a non-committing admission and
203    /// recovers the same request for a same-lock retry.
204    #[must_use]
205    pub fn from_unchanged_record_admission<EF, V, LF>(
206        unchanged: UnchangedRecordAdmission<'_, EF, V, LF>,
207        retained_record_limit: u64,
208    ) -> (Self, RecordAdmission, ResourceVector) {
209        let (prestate, encoded_record_charge) = unchanged.into_parts();
210        let (request, frontiers, closure_accounting, retained_charges) =
211            prestate.into_live_owner_parts();
212        (
213            Self {
214                frontiers,
215                closure_accounting,
216                retained_charges,
217                retained_record_limit,
218            },
219            request,
220            encoded_record_charge,
221        )
222    }
223
224    /// Acquires the exact owner from the complete sealed successful
225    /// `RecordAdmission` persistence authority.
226    #[must_use]
227    pub fn from_record_admission_persistence(
228        persistence: RecordAdmissionPersistenceParts,
229        retained_record_limit: u64,
230    ) -> Self {
231        Self {
232            frontiers: persistence.frontiers,
233            closure_accounting: persistence.accounting,
234            retained_charges: persistence.retained_charges,
235            retained_record_limit,
236        }
237    }
238
239    /// Acquires the exact post-drain owner and durable marker successor.
240    #[must_use]
241    pub fn from_marker_drain(
242        commit: MarkerDrainCommit,
243        retained_record_limit: u64,
244    ) -> (Self, StoredEdge, MarkerDeliveryProjection) {
245        let (frontiers, closure_accounting, retained_charges, successor, projection) =
246            commit.into_parts();
247        (
248            Self {
249                frontiers,
250                closure_accounting,
251                retained_charges,
252                retained_record_limit,
253            },
254            successor,
255            projection,
256        )
257    }
258
259    pub(super) fn commit_binding_terminal_candidate(
260        self,
261        active_binding: super::super::ActiveBinding,
262        admission_order: super::super::AdmissionOrder,
263        delivery_seq: crate::wire::DeliverySeq,
264        charge: RetainedRecordCharge,
265    ) -> Result<Self, Box<(Self, LiveFrontierError)>> {
266        let mut active = self.frontiers.active_identities().participants().to_vec();
267        let Some(participant) = active
268            .iter_mut()
269            .find(|participant| participant.participant_index() == active_binding.participant_id)
270        else {
271            return Err(Box::new((self, LiveFrontierError::Authority)));
272        };
273        if participant.binding() != FrontierBinding::Bound(active_binding.binding_epoch) {
274            return Err(Box::new((self, LiveFrontierError::Authority)));
275        }
276        *participant = FrontierParticipant::new(
277            participant.participant_index(),
278            participant.cursor(),
279            FrontierBinding::Detached(active_binding.binding_epoch),
280        );
281        let row = RetainedCausalRecord {
282            delivery_seq,
283            admission_order,
284            kind: RetainedCausalRecordKind::BindingTerminal(super::super::BindingTerminalOwner {
285                participant_index: active_binding.participant_id,
286                binding_epoch: active_binding.binding_epoch,
287            }),
288        };
289        let Some(sequence) = detach_sequence(self.frontiers.sequence().ledger(), delivery_seq)
290        else {
291            return Err(Box::new((self, LiveFrontierError::Frontier)));
292        };
293        let Some(order) = detach_order(
294            self.frontiers.order().ledger(),
295            admission_order.transaction_order(),
296        ) else {
297            return Err(Box::new((self, LiveFrontierError::Frontier)));
298        };
299        match transition(self, (), active, &[row], vec![charge], sequence, order) {
300            Ok(committed) => {
301                let ((), owner) = committed.into_parts();
302                Ok(owner)
303            }
304            Err(failure) => {
305                let error = failure.error();
306                let ((), owner) = failure.into_parts();
307                Err(Box::new((owner, error)))
308            }
309        }
310    }
311
312    pub(super) fn pend_binding_terminal_candidate(
313        self,
314        active_binding: super::super::ActiveBinding,
315        admission_order: super::super::AdmissionOrder,
316        delivery_seq: crate::wire::DeliverySeq,
317    ) -> Result<Self, Box<(Self, LiveFrontierError)>> {
318        let Some(order) = detach_order(
319            self.frontiers.order().ledger(),
320            admission_order.transaction_order(),
321        ) else {
322            return Err(Box::new((self, LiveFrontierError::Frontier)));
323        };
324        let Self {
325            frontiers,
326            closure_accounting,
327            retained_charges,
328            retained_record_limit,
329        } = self;
330        match frontiers.apply_pending_binding_terminal(
331            active_binding.participant_id,
332            active_binding.binding_epoch,
333            delivery_seq,
334            admission_order,
335            order,
336        ) {
337            Ok(frontiers) => Ok(Self {
338                frontiers,
339                closure_accounting,
340                retained_charges,
341                retained_record_limit,
342            }),
343            Err(failure) => {
344                let (frontiers, error) = *failure;
345                Err(Box::new((
346                    Self {
347                        frontiers,
348                        closure_accounting,
349                        retained_charges,
350                        retained_record_limit,
351                    },
352                    map_frontier_error(error),
353                )))
354            }
355        }
356    }
357
358    /// Retains this move-only owner and the exact recovery while its durable
359    /// marker source row is read and validated.
360    ///
361    /// # Errors
362    /// Returns the unchanged owner and recovery when the fully restored frontier
363    /// does not contain their exact delivered marker occurrence.
364    pub fn retain_fenced_marker_source(
365        self,
366        recovery: DetachedCredentialRecovery,
367    ) -> Result<RetainedFencedMarkerSource, Box<FencedMarkerSourceRetentionRefused>> {
368        let Some(source) = self.frontiers.fenced_marker_source(recovery) else {
369            return Err(Box::new(FencedMarkerSourceRetentionRefused {
370                owner: self,
371                recovery,
372            }));
373        };
374        Ok(RetainedFencedMarkerSource {
375            owner: self,
376            recovery,
377            expectation: FencedMarkerSourceExpectation { source },
378        })
379    }
380
381    /// Consumes the complete owner and the exact descriptive inputs to mint one
382    /// fenced attach proof from the selected retained marker occurrence.
383    ///
384    /// This is the sole public production mint. The caller cannot supply a raw
385    /// marker token: the owner removes it from its fully validated frontiers.
386    /// Refusal returns this owner and every input unchanged after reinstalling
387    /// that same occurrence authority, allowing serial retry but no fork.
388    #[must_use]
389    pub fn mint_fenced_attach(
390        mut self,
391        marker_source_sequence: u64,
392        recovery: DetachedCredentialRecovery,
393        debt: ClosureDebt,
394        event: Event,
395        successor: DebtCompletion,
396    ) -> MintFencedAttachResult {
397        let Some(record) = self.frontiers.take_fenced_marker_record(recovery) else {
398            return MintFencedAttachResult::MintRefused(Box::new(MintFencedAttachRefused {
399                owner: self,
400                marker_source_sequence,
401                recovery,
402                debt,
403                event,
404                successor,
405                reason: FencedAttachMintRefusalReason::MarkerAuthority,
406            }));
407        };
408        match recovery.fenced_attach(record, debt, event, successor) {
409            Ok(proof) => MintFencedAttachResult::Minted(Box::new(MintedFencedAttach {
410                owner_without_marker_authority: self,
411                proof,
412            })),
413            Err(refusal) => {
414                self.frontiers
415                    .reinstall_fenced_marker_record((*refusal).into_record());
416                MintFencedAttachResult::MintRefused(Box::new(MintFencedAttachRefused {
417                    owner: self,
418                    marker_source_sequence,
419                    recovery,
420                    debt,
421                    event,
422                    successor,
423                    reason: FencedAttachMintRefusalReason::ProofInputs,
424                }))
425            }
426        }
427    }
428}
429
430/// Exact protocol-recomputed marker facts a durable source row must match.
431#[derive(Clone, Copy, Debug, PartialEq, Eq)]
432pub struct FencedMarkerSourceExpectation {
433    source: FencedMarkerSourceRecord,
434}
435
436impl FencedMarkerSourceExpectation {
437    /// Returns the conversation owning the durable marker source.
438    #[must_use]
439    pub const fn conversation_id(self) -> u64 {
440        self.source.conversation_id
441    }
442
443    /// Returns the marker's durable delivery sequence.
444    #[must_use]
445    pub const fn marker_delivery_seq(self) -> u64 {
446        self.source.delivery_seq
447    }
448
449    /// Returns the marker's immutable causal key.
450    #[must_use]
451    pub const fn admission_order(self) -> super::super::AdmissionOrder {
452        self.source.admission_order
453    }
454
455    /// Returns the permanent marker owner.
456    #[must_use]
457    pub const fn participant_id(self) -> u64 {
458        self.source.participant_id
459    }
460
461    /// Returns the marker's immutable provenance.
462    #[must_use]
463    pub const fn provenance(self) -> super::super::MarkerProvenance {
464        self.source.provenance
465    }
466
467    /// Returns the historically validated delivery target.
468    #[must_use]
469    pub const fn target_binding(self) -> FrontierBinding {
470        self.source.target_binding
471    }
472}
473
474/// Move-only owner/recovery pair held across one bounded durable source read.
475#[derive(Debug, PartialEq, Eq)]
476pub struct RetainedFencedMarkerSource {
477    owner: LiveFrontierOwner,
478    recovery: DetachedCredentialRecovery,
479    expectation: FencedMarkerSourceExpectation,
480}
481
482impl RetainedFencedMarkerSource {
483    /// Returns the protocol-recomputed facts the durable row must match.
484    #[must_use]
485    pub const fn expectation(&self) -> FencedMarkerSourceExpectation {
486        self.expectation
487    }
488
489    /// Returns the unchanged owner and recovery after source validation.
490    #[must_use]
491    pub fn into_parts(self) -> (LiveFrontierOwner, DetachedCredentialRecovery) {
492        (self.owner, self.recovery)
493    }
494}
495
496/// Refused source retention with the owner and recovery unchanged.
497#[derive(Debug, PartialEq, Eq)]
498pub struct FencedMarkerSourceRetentionRefused {
499    owner: LiveFrontierOwner,
500    recovery: DetachedCredentialRecovery,
501}
502
503impl FencedMarkerSourceRetentionRefused {
504    /// Returns the unchanged owner and recovery after retention refusal.
505    #[must_use]
506    pub fn into_parts(self) -> (LiveFrontierOwner, DetachedCredentialRecovery) {
507        (self.owner, self.recovery)
508    }
509}
510
511/// Exact reason the owner could not mint a fenced attach proof.
512#[derive(Clone, Copy, Debug, PartialEq, Eq)]
513pub enum FencedAttachMintRefusalReason {
514    /// The validated frontier did not own the selected delivered marker record,
515    /// or that record authority was already consumed.
516    MarkerAuthority,
517    /// Marker authority existed, but recovery/event/successor inputs disagreed.
518    ProofInputs,
519}
520
521/// Successful one-use fenced proof mint.
522#[derive(Debug, PartialEq, Eq)]
523pub struct MintedFencedAttach {
524    owner_without_marker_authority: LiveFrontierOwner,
525    proof: FencedAttachCommit,
526}
527
528impl MintedFencedAttach {
529    /// Consumes the result into the owner with spent marker authority and the
530    /// sole proof admitted to the downstream by-value chain.
531    #[must_use]
532    pub fn into_parts(self) -> (LiveFrontierOwner, FencedAttachCommit) {
533        (self.owner_without_marker_authority, self.proof)
534    }
535}
536
537/// Failed one-use fenced proof mint with unchanged retry authority and inputs.
538#[derive(Debug, PartialEq, Eq)]
539pub struct MintFencedAttachRefused {
540    owner: LiveFrontierOwner,
541    marker_source_sequence: u64,
542    recovery: DetachedCredentialRecovery,
543    debt: ClosureDebt,
544    event: Event,
545    successor: DebtCompletion,
546    reason: FencedAttachMintRefusalReason,
547}
548
549impl MintFencedAttachRefused {
550    /// Returns the typed refusal cause.
551    #[must_use]
552    pub const fn reason(&self) -> FencedAttachMintRefusalReason {
553        self.reason
554    }
555
556    /// Consumes the refusal into the unchanged owner and all original inputs.
557    #[must_use]
558    pub fn into_parts(
559        self,
560    ) -> (
561        LiveFrontierOwner,
562        u64,
563        DetachedCredentialRecovery,
564        ClosureDebt,
565        Event,
566        DebtCompletion,
567    ) {
568        (
569            self.owner,
570            self.marker_source_sequence,
571            self.recovery,
572            self.debt,
573            self.event,
574            self.successor,
575        )
576    }
577}
578
579/// Complete result of the sole production fenced proof mint.
580#[derive(Debug, PartialEq, Eq)]
581pub enum MintFencedAttachResult {
582    /// Exactly one marker authority was spent and one proof was minted.
583    Minted(Box<MintedFencedAttach>),
584    /// No proof was minted; the same authority and inputs are serially retryable.
585    MintRefused(Box<MintFencedAttachRefused>),
586}
587
588/// Complete move-only settled Leave result: tombstone and executable owner.
589#[derive(Debug, PartialEq, Eq)]
590pub struct LiveLeaveCommit<EF, V, LF> {
591    identity: IdentityState<EF, V, LF>,
592    owner: LiveFrontierOwner,
593}
594
595impl<EF, V, LF> LiveLeaveCommit<EF, V, LF> {
596    /// Projects permanent Leave's exact protocol-committed `Left` sequence.
597    #[must_use]
598    pub const fn observer_progress_projection(&self) -> Option<ObserverProgressProjection> {
599        let IdentityState::Retired(retired) = &self.identity else {
600            return None;
601        };
602        let committed = retired.committed_result();
603        Some(ObserverProgressProjection::new(
604            committed.conversation_id(),
605            committed.left_delivery_seq(),
606        ))
607    }
608
609    /// Consumes the atomic result into its inseparable tombstone and owner.
610    #[must_use]
611    pub fn into_parts(self) -> (IdentityState<EF, V, LF>, LiveFrontierOwner) {
612        (self.identity, self.owner)
613    }
614}
615
616/// Typed failure of the protocol-owned settled Leave live transition.
617#[derive(Clone, Debug, PartialEq, Eq)]
618pub enum LiveLeaveError {
619    /// Claim-frontier Leave authority could not be prepared.
620    Prepare(PrepareLeaveAuthorityError),
621    /// Membership retirement rejected inconsistent authority.
622    Commit(LeaveCommitError),
623    /// Canonical Left-row charge did not name the protocol-produced row.
624    RetainedCharge,
625    /// Resulting retained-row count exceeded the signed cap.
626    RetainedRecordLimit,
627    /// Resulting closure accounting exceeded configured capacity.
628    ClosureAccounting,
629    /// Leave did not produce a retired identity.
630    Identity,
631}
632
633/// Commits settled bound or detached Leave through one complete live owner.
634///
635/// # Errors
636///
637/// Returns [`LiveLeaveError`] when preparation or retirement authority is
638/// inconsistent, the caller's keyed Left charge does not match the committed
639/// row, or the resulting retention/closure accounting exceeds its authority.
640pub fn commit_settled_leave_frontier<EF, V, LF, D>(
641    owner: LiveFrontierOwner,
642    member: LiveMember<EF>,
643    binding: BindingState,
644    detach_cell: DetachCell<D>,
645    verified: VerifiedLeaveRequest<V, LF>,
646    left_delivery_seq: u64,
647    left_charge: RetainedRecordCharge,
648) -> Result<LiveLeaveCommit<EF, V, LF>, LiveLeaveError> {
649    let LiveFrontierOwner {
650        frontiers,
651        closure_accounting,
652        mut retained_charges,
653        retained_record_limit,
654    } = owner;
655    let retired_marker_charge =
656        retired_marker_charge(&frontiers, &retained_charges, member.participant_id())?;
657    let authority = frontiers
658        .prepare_settled_leave_authority(&member, binding)
659        .map_err(LiveLeaveError::Prepare)?;
660    let commit = commit_leave(
661        member,
662        binding,
663        detach_cell,
664        verified,
665        authority,
666        LeaveCommitParameters { left_delivery_seq },
667    )
668    .map_err(LiveLeaveError::Commit)?;
669    let (identity, frontiers) = commit.into_parts();
670    let IdentityState::Retired(retired) = &identity else {
671        return Err(LiveLeaveError::Identity);
672    };
673    if left_charge.delivery_seq() != retired.committed_result().left_delivery_seq()
674        || left_charge.admission_order() != retired.left_admission_order()
675        || left_charge.encoded_charge().entries != 1
676    {
677        return Err(LiveLeaveError::RetainedCharge);
678    }
679    retained_charges.push(left_charge);
680    retained_charges.sort_unstable_by_key(|charge| charge.delivery_seq());
681    let retained_len = u64::try_from(frontiers.retained_records().len())
682        .map_err(|_| LiveLeaveError::RetainedRecordLimit)?;
683    if retained_len > retained_record_limit
684        || retained_charges.len() != frontiers.retained_records().len()
685    {
686        return Err(LiveLeaveError::RetainedRecordLimit);
687    }
688    let closure_accounting =
689        accounting_after_leave(closure_accounting, &[left_charge], retired_marker_charge)
690            .ok_or(LiveLeaveError::ClosureAccounting)?;
691    Ok(LiveLeaveCommit {
692        identity,
693        owner: LiveFrontierOwner {
694            frontiers,
695            closure_accounting,
696            retained_charges,
697            retained_record_limit,
698        },
699    })
700}
701
702/// Commits a pending binding terminal immediately before Leave through one
703/// complete live owner.
704///
705/// # Errors
706///
707/// Returns [`LiveLeaveError`] when pending preparation or retirement authority
708/// is inconsistent, either caller charge does not match its protocol-produced
709/// row, or resulting retention/closure accounting exceeds its authority.
710pub fn commit_pending_leave_frontier<EF, V, LF, D>(
711    owner: LiveFrontierOwner,
712    member: LiveMember<EF>,
713    pending: PendingFinalization,
714    detach_cell: DetachCell<D>,
715    verified: VerifiedLeaveRequest<V, LF>,
716    parameters: PendingLeaveCommitParameters,
717    charges: [RetainedRecordCharge; 2],
718) -> Result<LiveLeaveCommit<EF, V, LF>, LiveLeaveError> {
719    let [terminal_charge, left_charge] = charges;
720    let terminal_delivery_seq = parameters.terminal_delivery_seq;
721    let LiveFrontierOwner {
722        frontiers,
723        closure_accounting,
724        mut retained_charges,
725        retained_record_limit,
726    } = owner;
727    let retired_marker_charge =
728        retired_marker_charge(&frontiers, &retained_charges, member.participant_id())?;
729    let authority = frontiers
730        .prepare_pending_leave_authority(&member, pending)
731        .map_err(LiveLeaveError::Prepare)?;
732    let commit = commit_pending_leave(
733        member,
734        pending,
735        detach_cell,
736        verified,
737        authority,
738        parameters,
739    )
740    .map_err(LiveLeaveError::Commit)?;
741    let (identity, frontiers) = commit.into_parts();
742    let IdentityState::Retired(retired) = &identity else {
743        return Err(LiveLeaveError::Identity);
744    };
745    if retired.committed_result().prior_terminal_delivery_seq() != Some(terminal_delivery_seq)
746        || terminal_charge.delivery_seq() != terminal_delivery_seq
747        || terminal_charge.admission_order() != pending.admission_order()
748        || terminal_charge.encoded_charge().entries != 1
749        || left_charge.delivery_seq() != retired.committed_result().left_delivery_seq()
750        || left_charge.admission_order() != retired.left_admission_order()
751        || left_charge.encoded_charge().entries != 1
752    {
753        return Err(LiveLeaveError::RetainedCharge);
754    }
755    retained_charges.extend([terminal_charge, left_charge]);
756    retained_charges.sort_unstable_by_key(|charge| charge.delivery_seq());
757    let retained_len = u64::try_from(frontiers.retained_records().len())
758        .map_err(|_| LiveLeaveError::RetainedRecordLimit)?;
759    if retained_len > retained_record_limit {
760        return Err(LiveLeaveError::RetainedRecordLimit);
761    }
762    if retained_charges.len() != frontiers.retained_records().len() {
763        return Err(LiveLeaveError::RetainedCharge);
764    }
765    let closure_accounting = accounting_after_leave(
766        closure_accounting,
767        &[terminal_charge, left_charge],
768        retired_marker_charge,
769    )
770    .ok_or(LiveLeaveError::ClosureAccounting)?;
771    Ok(LiveLeaveCommit {
772        identity,
773        owner: LiveFrontierOwner {
774            frontiers,
775            closure_accounting,
776            retained_charges,
777            retained_record_limit,
778        },
779    })
780}
781
782fn retired_marker_charge(
783    frontiers: &ClaimFrontiers,
784    retained_charges: &[RetainedRecordCharge],
785    participant_id: crate::wire::ParticipantId,
786) -> Result<Option<RetainedRecordCharge>, LiveLeaveError> {
787    let marker_sequence = frontiers
788        .retained_marker_records()
789        .iter()
790        .find_map(|record| {
791            matches!(
792                record.kind,
793                RetainedCausalRecordKind::CompactionMarker {
794                    participant_index,
795                    ..
796                } if participant_index == participant_id
797            )
798            .then_some(record.delivery_seq)
799        });
800    let Some(marker_sequence) = marker_sequence else {
801        return Ok(None);
802    };
803    retained_charges
804        .iter()
805        .copied()
806        .find(|charge| charge.delivery_seq() == marker_sequence)
807        .map(Some)
808        .ok_or(LiveLeaveError::RetainedCharge)
809}
810
811/// Exact charges for a credential attach's one or two retained rows.
812#[derive(Debug, PartialEq, Eq)]
813pub struct AttachFrontierCharges {
814    terminal: Option<RetainedRecordCharge>,
815    attached: RetainedRecordCharge,
816    seal: LiveTransitionInputSeal,
817}
818
819#[derive(Debug, PartialEq, Eq)]
820enum LiveTransitionInputSeal {
821    Validated,
822}
823
824impl AttachFrontierCharges {
825    /// Couples the canonical `Attached` charge with an optional terminal charge.
826    #[must_use]
827    pub const fn new(
828        terminal: Option<RetainedRecordCharge>,
829        attached: RetainedRecordCharge,
830    ) -> Self {
831        Self {
832            terminal,
833            attached,
834            seal: LiveTransitionInputSeal::Validated,
835        }
836    }
837
838    const fn into_parts(self) -> (Option<RetainedRecordCharge>, RetainedRecordCharge) {
839        let Self {
840            terminal,
841            attached,
842            seal,
843        } = self;
844        match seal {
845            LiveTransitionInputSeal::Validated => (terminal, attached),
846        }
847    }
848}
849
850/// A typed lifecycle commit paired with its complete post-transition owner.
851#[derive(Debug, PartialEq, Eq)]
852pub struct LiveFrontierCommit<T> {
853    operation: T,
854    owner: LiveFrontierOwner,
855}
856
857impl<T> LiveFrontierCommit<T> {
858    /// Borrows the exact typed lifecycle commit.
859    #[must_use]
860    pub const fn operation(&self) -> &T {
861        &self.operation
862    }
863
864    /// Borrows the complete post-transition owner.
865    #[must_use]
866    pub const fn owner(&self) -> &LiveFrontierOwner {
867        &self.owner
868    }
869
870    /// Consumes the atomic transition for durability publication.
871    #[must_use]
872    pub fn into_parts(self) -> (T, LiveFrontierOwner) {
873        (self.operation, self.owner)
874    }
875}
876
877/// Failed live transition retaining the unchanged complete owner and operation.
878#[derive(Debug, PartialEq, Eq)]
879pub struct LiveFrontierFailure<T> {
880    error: LiveFrontierError,
881    operation: T,
882    owner: LiveFrontierOwner,
883}
884
885impl<T> LiveFrontierFailure<T> {
886    /// Returns the exact typed transition failure.
887    #[must_use]
888    pub const fn error(&self) -> LiveFrontierError {
889        self.error
890    }
891
892    /// Recovers the unchanged owner and intact operation commit.
893    #[must_use]
894    pub fn into_parts(self) -> (T, LiveFrontierOwner) {
895        (self.operation, self.owner)
896    }
897}
898
899/// Failure selected while coupling a sealed lifecycle commit to live ownership.
900#[derive(Clone, Copy, Debug, PartialEq, Eq)]
901pub enum LiveFrontierError {
902    /// Commit and live owner name different authority.
903    Authority,
904    /// A mandatory immutable/recovery transition has precedence.
905    Precedence,
906    /// Canonical keyed row charges differ from the commit-derived retained rows.
907    RetainedCharge,
908    /// The retained causal-row cap would be exceeded.
909    RetainedRecordLimit,
910    /// Aggregate claim arithmetic or exact owner reconstruction failed.
911    Frontier,
912    /// Resulting closure accounting is invalid or outside its signed capacity.
913    ClosureAccounting,
914}
915
916/// Result of coupling any typed lifecycle commit to live frontier ownership.
917pub type LiveFrontierResult<T> = Result<LiveFrontierCommit<T>, Box<LiveFrontierFailure<T>>>;
918
919/// Applies a subsequent enrollment to the complete live owner.
920///
921/// # Errors
922///
923/// Returns a failure retaining the unchanged owner and intact enrollment commit.
924pub fn apply_enrollment_frontier<F>(
925    owner: LiveFrontierOwner,
926    operation: super::super::EnrollmentCommit<F>,
927    attached_charge: RetainedRecordCharge,
928) -> LiveFrontierResult<super::super::EnrollmentCommit<F>> {
929    let attached = operation.attached;
930    if attached.conversation_id() != owner.frontiers.conversation_id() {
931        return failure(owner, operation, LiveFrontierError::Authority);
932    }
933    let participant_id = attached.participant_id();
934    let mut active = owner.frontiers.active_identities().participants().to_vec();
935    if active
936        .iter()
937        .any(|participant| participant.participant_index() == participant_id)
938    {
939        return failure(owner, operation, LiveFrontierError::Authority);
940    }
941    active.push(FrontierParticipant::new(
942        participant_id,
943        operation.member.cursor(),
944        FrontierBinding::Bound(attached.binding_epoch()),
945    ));
946    active.sort_unstable_by_key(|participant| participant.participant_index());
947    let rows = [retained_attached(attached)];
948    let Some(sequence) =
949        enrollment_sequence(owner.frontiers.sequence().ledger(), attached.delivery_seq())
950    else {
951        return failure(owner, operation, LiveFrontierError::Frontier);
952    };
953    let Some(order) = enrollment_order(
954        owner.frontiers.order().ledger(),
955        attached.admission_order().transaction_order(),
956    ) else {
957        return failure(owner, operation, LiveFrontierError::Frontier);
958    };
959    transition(
960        owner,
961        operation,
962        active,
963        &rows,
964        vec![attached_charge],
965        sequence,
966        order,
967    )
968}
969
970/// Applies credential attach to the complete live owner.
971///
972/// # Errors
973///
974/// Returns a failure retaining the unchanged owner, intact attach commit, and
975/// exact reason the commit could not enter the frontier.
976pub fn apply_attach_frontier<F, V>(
977    owner: LiveFrontierOwner,
978    operation: AttachCommit<F, V>,
979    charges: AttachFrontierCharges,
980) -> LiveFrontierResult<AttachCommit<F, V>> {
981    let (terminal_charge, attached_charge) = charges.into_parts();
982    let attached = operation.attached;
983    if attached.conversation_id() != owner.frontiers.conversation_id() {
984        return failure(owner, operation, LiveFrontierError::Authority);
985    }
986    let mut active = owner.frontiers.active_identities().participants().to_vec();
987    let Some(participant) = active
988        .iter_mut()
989        .find(|participant| participant.participant_index() == attached.participant_id())
990    else {
991        return failure(owner, operation, LiveFrontierError::Authority);
992    };
993    *participant = FrontierParticipant::new(
994        participant.participant_index(),
995        operation.member.cursor(),
996        FrontierBinding::Bound(attached.binding_epoch()),
997    );
998    let current_sequence = owner.frontiers.sequence().ledger();
999    let current_order = owner.frontiers.order().ledger();
1000    let (rows, keyed_charges, sequence, order) = match operation.transition {
1001        AttachTransition::Detached => {
1002            if terminal_charge.is_some() {
1003                return failure(owner, operation, LiveFrontierError::RetainedCharge);
1004            }
1005            let Some(sequence) =
1006                detached_attach_sequence(current_sequence, attached.delivery_seq())
1007            else {
1008                return failure(owner, operation, LiveFrontierError::Frontier);
1009            };
1010            let Some(order) = detached_attach_order(
1011                current_order,
1012                attached.admission_order().transaction_order(),
1013            ) else {
1014                return failure(owner, operation, LiveFrontierError::Frontier);
1015            };
1016            (
1017                vec![retained_attached(attached)],
1018                vec![attached_charge],
1019                sequence,
1020                order,
1021            )
1022        }
1023        AttachTransition::Superseded { terminal } => {
1024            let Some(terminal_charge) = terminal_charge else {
1025                return failure(owner, operation, LiveFrontierError::RetainedCharge);
1026            };
1027            let rows = vec![
1028                retained_terminal(terminal.into()),
1029                retained_attached(attached),
1030            ];
1031            let Some(sequence) = superseding_attach_sequence(current_sequence, &rows) else {
1032                return failure(owner, operation, LiveFrontierError::Frontier);
1033            };
1034            let Some(order) = superseding_attach_order(
1035                current_order,
1036                attached.admission_order().transaction_order(),
1037            ) else {
1038                return failure(owner, operation, LiveFrontierError::Frontier);
1039            };
1040            (
1041                rows,
1042                vec![terminal_charge, attached_charge],
1043                sequence,
1044                order,
1045            )
1046        }
1047        AttachTransition::FencedRecovery {
1048            prior_binding_epoch,
1049            composed_terminal,
1050            next_closure_state,
1051        } => {
1052            return apply_fenced_attach_frontier(
1053                owner,
1054                operation,
1055                terminal_charge,
1056                attached_charge,
1057                prior_binding_epoch,
1058                composed_terminal,
1059                next_closure_state,
1060            );
1061        }
1062    };
1063    transition(
1064        owner,
1065        operation,
1066        active,
1067        &rows,
1068        keyed_charges,
1069        sequence,
1070        order,
1071    )
1072}
1073
1074/// Applies a committed detach terminal to the complete live owner.
1075///
1076/// # Errors
1077///
1078/// Returns a failure retaining the unchanged owner and intact detach commit.
1079pub fn apply_detach_frontier<EF, V>(
1080    owner: LiveFrontierOwner,
1081    operation: CommittedDetachTransition<EF, V>,
1082    terminal_charge: RetainedRecordCharge,
1083) -> LiveFrontierResult<CommittedDetachTransition<EF, V>> {
1084    let terminal = operation.terminal();
1085    if terminal.conversation_id() != owner.frontiers.conversation_id() {
1086        return failure(owner, operation, LiveFrontierError::Authority);
1087    }
1088    let mut active = owner.frontiers.active_identities().participants().to_vec();
1089    let Some(participant) = active
1090        .iter_mut()
1091        .find(|participant| participant.participant_index() == terminal.participant_id())
1092    else {
1093        return failure(owner, operation, LiveFrontierError::Authority);
1094    };
1095    *participant = FrontierParticipant::new(
1096        participant.participant_index(),
1097        operation.member().cursor(),
1098        FrontierBinding::Detached(terminal.binding_epoch()),
1099    );
1100    let row = retained_terminal(terminal.into());
1101    let Some(sequence) = detach_sequence(owner.frontiers.sequence().ledger(), row.delivery_seq)
1102    else {
1103        return failure(owner, operation, LiveFrontierError::Frontier);
1104    };
1105    let Some(order) = detach_order(
1106        owner.frontiers.order().ledger(),
1107        row.admission_order.transaction_order(),
1108    ) else {
1109        return failure(owner, operation, LiveFrontierError::Frontier);
1110    };
1111    transition(
1112        owner,
1113        operation,
1114        active,
1115        &[row],
1116        vec![terminal_charge],
1117        sequence,
1118        order,
1119    )
1120}
1121
1122/// Applies a zero-debt participant acknowledgement cursor transition.
1123///
1124/// # Errors
1125///
1126/// Returns a failure retaining the unchanged owner and intact ack commit.
1127pub fn apply_participant_ack_frontier(
1128    mut owner: LiveFrontierOwner,
1129    operation: ParticipantAckCommit,
1130) -> LiveFrontierResult<ParticipantAckCommit> {
1131    let request = operation.outcome().request();
1132    let Some(current) = owner
1133        .frontiers
1134        .active_identities()
1135        .participants()
1136        .iter()
1137        .find(|participant| participant.participant_index() == request.participant_id)
1138        .copied()
1139    else {
1140        return failure(owner, operation, LiveFrontierError::Authority);
1141    };
1142    let participant = FrontierParticipant::new(
1143        request.participant_id,
1144        request.through_seq,
1145        current.binding(),
1146    );
1147    owner.frontiers = match owner.frontiers.apply_live_identity(participant) {
1148        Ok(frontiers) => frontiers,
1149        Err(frontier_failure) => {
1150            let (frontiers, error) = *frontier_failure;
1151            owner.frontiers = frontiers;
1152            return failure(owner, operation, map_frontier_error(error));
1153        }
1154    };
1155    Ok(LiveFrontierCommit { operation, owner })
1156}
1157
1158/// Applies a nonzero-debt participant acknowledgement cursor transition.
1159///
1160/// The episode and member remain owned by the sealed aggregate commit; this
1161/// transition consumes the same exact acknowledged cursor into the coupled
1162/// claim-frontier participant rank.
1163///
1164/// # Errors
1165///
1166/// Returns a failure retaining the unchanged owner and intact aggregate commit.
1167pub fn apply_nonzero_participant_ack_frontier(
1168    mut owner: LiveFrontierOwner,
1169    operation: NonzeroParticipantAckCommit,
1170) -> LiveFrontierResult<NonzeroParticipantAckCommit> {
1171    let request = operation.outcome().request();
1172    let Some(current) = owner
1173        .frontiers
1174        .active_identities()
1175        .participants()
1176        .iter()
1177        .find(|participant| participant.participant_index() == request.participant_id)
1178        .copied()
1179    else {
1180        return failure(owner, operation, LiveFrontierError::Authority);
1181    };
1182    let participant = FrontierParticipant::new(
1183        request.participant_id,
1184        request.through_seq,
1185        current.binding(),
1186    );
1187    owner.frontiers = match owner.frontiers.apply_live_identity(participant) {
1188        Ok(frontiers) => frontiers,
1189        Err(frontier_failure) => {
1190            let (frontiers, error) = *frontier_failure;
1191            owner.frontiers = frontiers;
1192            return failure(owner, operation, map_frontier_error(error));
1193        }
1194    };
1195    Ok(LiveFrontierCommit { operation, owner })
1196}
1197
1198/// Applies a zero-debt marker acknowledgement cursor transition.
1199///
1200/// # Errors
1201///
1202/// Returns a failure retaining the unchanged owner and intact marker-ack commit.
1203pub fn apply_marker_ack_frontier(
1204    mut owner: LiveFrontierOwner,
1205    operation: MarkerAckCommit,
1206) -> LiveFrontierResult<MarkerAckCommit> {
1207    let request = operation.outcome().request();
1208    if !owner
1209        .frontiers
1210        .retained_marker_records()
1211        .iter()
1212        .any(|record| {
1213            record.delivery_seq == request.marker_delivery_seq
1214                && matches!(
1215                    record.kind,
1216                    RetainedCausalRecordKind::CompactionMarker { participant_index, .. }
1217                        if participant_index == request.participant_id
1218                )
1219        })
1220    {
1221        return failure(owner, operation, LiveFrontierError::Authority);
1222    }
1223    let Some(current) = owner
1224        .frontiers
1225        .active_identities()
1226        .participants()
1227        .iter()
1228        .find(|participant| participant.participant_index() == request.participant_id)
1229        .copied()
1230    else {
1231        return failure(owner, operation, LiveFrontierError::Authority);
1232    };
1233    let Some(accounting) = accounting_after_marker_ack(owner.closure_accounting) else {
1234        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
1235    };
1236    let participant = FrontierParticipant::new(
1237        request.participant_id,
1238        request.marker_delivery_seq,
1239        current.binding(),
1240    );
1241    owner.frontiers = match owner.frontiers.apply_live_identity(participant) {
1242        Ok(frontiers) => frontiers,
1243        Err(frontier_failure) => {
1244            let (frontiers, error) = *frontier_failure;
1245            owner.frontiers = frontiers;
1246            return failure(owner, operation, map_frontier_error(error));
1247        }
1248    };
1249    owner.closure_accounting = accounting;
1250    Ok(LiveFrontierCommit { operation, owner })
1251}
1252
1253fn apply_fenced_attach_frontier<F, V>(
1254    owner: LiveFrontierOwner,
1255    operation: AttachCommit<F, V>,
1256    terminal_charge: Option<RetainedRecordCharge>,
1257    attached_charge: RetainedRecordCharge,
1258    prior_binding_epoch: crate::wire::BindingEpoch,
1259    composed_terminal: Option<super::super::CommittedBindingTerminal>,
1260    next_closure_state: super::super::ClosureState,
1261) -> LiveFrontierResult<AttachCommit<F, V>> {
1262    let attached = operation.attached;
1263    let (rows, charges) = match (composed_terminal, terminal_charge) {
1264        (None, None) => (vec![retained_attached(attached)], vec![attached_charge]),
1265        (Some(terminal), Some(terminal_charge)) => (
1266            vec![retained_terminal(terminal), retained_attached(attached)],
1267            vec![terminal_charge, attached_charge],
1268        ),
1269        (None, Some(_)) | (Some(_), None) => {
1270            return failure(owner, operation, LiveFrontierError::RetainedCharge);
1271        }
1272    };
1273    let participant = FrontierParticipant::new(
1274        attached.participant_id(),
1275        operation.member.cursor(),
1276        FrontierBinding::Bound(attached.binding_epoch()),
1277    );
1278    fenced_attach_transition(
1279        owner,
1280        operation,
1281        participant,
1282        prior_binding_epoch,
1283        next_closure_state,
1284        &rows,
1285        charges,
1286    )
1287}
1288
1289fn fenced_attach_transition<T>(
1290    mut owner: LiveFrontierOwner,
1291    operation: T,
1292    participant: FrontierParticipant,
1293    prior_binding_epoch: crate::wire::BindingEpoch,
1294    next_closure_state: super::super::ClosureState,
1295    rows: &[RetainedCausalRecord],
1296    charges: Vec<RetainedRecordCharge>,
1297) -> LiveFrontierResult<T> {
1298    if rows.len() != charges.len()
1299        || rows.iter().zip(&charges).any(|(row, charge)| {
1300            row.delivery_seq != charge.delivery_seq()
1301                || row.admission_order != charge.admission_order()
1302                || charge.encoded_charge().entries != 1
1303        })
1304    {
1305        return failure(owner, operation, LiveFrontierError::RetainedCharge);
1306    }
1307    let resulting_len = owner
1308        .frontiers
1309        .retained_records()
1310        .len()
1311        .checked_add(rows.len());
1312    if resulting_len
1313        .and_then(|len| u64::try_from(len).ok())
1314        .is_none_or(|len| len > owner.retained_record_limit)
1315    {
1316        return failure(owner, operation, LiveFrontierError::RetainedRecordLimit);
1317    }
1318    let Some(accounting) =
1319        accounting_after_fenced_attach(owner.closure_accounting, &charges, next_closure_state)
1320    else {
1321        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
1322    };
1323    owner.frontiers =
1324        match owner
1325            .frontiers
1326            .apply_live_fenced_attach(participant, prior_binding_epoch, rows)
1327        {
1328            Ok(frontiers) => frontiers,
1329            Err(frontier_failure) => {
1330                let (frontiers, error) = *frontier_failure;
1331                owner.frontiers = frontiers;
1332                return failure(owner, operation, map_frontier_error(error));
1333            }
1334        };
1335    owner.retained_charges.extend(charges);
1336    owner
1337        .retained_charges
1338        .sort_unstable_by_key(|charge| charge.delivery_seq());
1339    owner.closure_accounting = accounting;
1340    Ok(LiveFrontierCommit { operation, owner })
1341}
1342
1343fn transition<T>(
1344    mut owner: LiveFrontierOwner,
1345    operation: T,
1346    active: Vec<FrontierParticipant>,
1347    rows: &[RetainedCausalRecord],
1348    charges: Vec<RetainedRecordCharge>,
1349    sequence: SequenceLedger,
1350    order: OrderLedger,
1351) -> LiveFrontierResult<T> {
1352    if rows.len() != charges.len()
1353        || rows.iter().zip(&charges).any(|(row, charge)| {
1354            row.delivery_seq != charge.delivery_seq()
1355                || row.admission_order != charge.admission_order()
1356                || charge.encoded_charge().entries != 1
1357        })
1358    {
1359        return failure(owner, operation, LiveFrontierError::RetainedCharge);
1360    }
1361    let resulting_len = owner
1362        .frontiers
1363        .retained_records()
1364        .len()
1365        .checked_add(rows.len());
1366    if resulting_len
1367        .and_then(|len| u64::try_from(len).ok())
1368        .is_none_or(|len| len > owner.retained_record_limit)
1369    {
1370        return failure(owner, operation, LiveFrontierError::RetainedRecordLimit);
1371    }
1372    let Some(accounting) = accounting_after_rows(owner.closure_accounting, &charges) else {
1373        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
1374    };
1375    owner.frontiers = match owner
1376        .frontiers
1377        .apply_live_transition(active, rows, sequence, order)
1378    {
1379        Ok(frontiers) => frontiers,
1380        Err(frontier_failure) => {
1381            let (frontiers, error) = *frontier_failure;
1382            owner.frontiers = frontiers;
1383            return failure(owner, operation, map_frontier_error(error));
1384        }
1385    };
1386    owner.retained_charges.extend(charges);
1387    owner.closure_accounting = accounting;
1388    Ok(LiveFrontierCommit { operation, owner })
1389}
1390
1391const fn map_frontier_error(error: LiveFrontierTransitionError) -> LiveFrontierError {
1392    match error {
1393        LiveFrontierTransitionError::Authority => LiveFrontierError::Authority,
1394        LiveFrontierTransitionError::Precedence => LiveFrontierError::Precedence,
1395        LiveFrontierTransitionError::RecordPosition
1396        | LiveFrontierTransitionError::Exhausted
1397        | LiveFrontierTransitionError::ResultingFrontier => LiveFrontierError::Frontier,
1398    }
1399}
1400
1401fn failure<T, U>(
1402    owner: LiveFrontierOwner,
1403    operation: T,
1404    error: LiveFrontierError,
1405) -> Result<U, Box<LiveFrontierFailure<T>>> {
1406    Err(Box::new(LiveFrontierFailure {
1407        error,
1408        operation,
1409        owner,
1410    }))
1411}