use alloc::boxed::Box;
use crate::{
algebra::ResourceVector,
lifecycle::{ActiveBinding, AdmissionOrder},
wire::{BindingEpoch, ConversationId, DeliverySeq, ParticipantId, TransactionOrder},
};
use super::{LiveFrontierError, LiveFrontierOwner, RetainedRecordCharge};
use crate::lifecycle::{CommittedBindingTerminalPosition, PendingBindingTerminalPosition};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BindingTerminalEncoding {
ParticipantLifecycleV3CanonicalJson,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BindingTerminalCauseClass {
Detached,
Died,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CandidateTerminalKey {
active_binding: ActiveBinding,
cause_class: BindingTerminalCauseClass,
admission_order: AdmissionOrder,
delivery_seq: DeliverySeq,
}
impl CandidateTerminalKey {
#[must_use]
pub const fn conversation_id(self) -> ConversationId {
self.active_binding.conversation_id
}
#[must_use]
pub const fn participant_id(self) -> ParticipantId {
self.active_binding.participant_id
}
#[must_use]
pub const fn binding_epoch(self) -> BindingEpoch {
self.active_binding.binding_epoch
}
#[must_use]
pub const fn cause_class(self) -> BindingTerminalCauseClass {
self.cause_class
}
#[must_use]
pub const fn admission_order(self) -> AdmissionOrder {
self.admission_order
}
#[must_use]
pub const fn delivery_seq(self) -> DeliverySeq {
self.delivery_seq
}
#[must_use]
pub const fn bind_v3_charge(
self,
encoded_charge: ResourceVector,
) -> BindingTerminalCandidateCharge {
BindingTerminalCandidateCharge {
conversation_id: self.conversation_id(),
participant_id: self.participant_id(),
binding_epoch: self.binding_epoch(),
admission_order: self.admission_order,
delivery_seq: self.delivery_seq,
encoding: BindingTerminalEncoding::ParticipantLifecycleV3CanonicalJson,
charge: RetainedRecordCharge::new(
self.delivery_seq,
self.admission_order,
encoded_charge,
),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BindingTerminalCandidateCharge {
conversation_id: ConversationId,
participant_id: ParticipantId,
binding_epoch: BindingEpoch,
admission_order: AdmissionOrder,
delivery_seq: DeliverySeq,
encoding: BindingTerminalEncoding,
charge: RetainedRecordCharge,
}
impl BindingTerminalCandidateCharge {
#[must_use]
pub const fn retained_charge(self) -> RetainedRecordCharge {
self.charge
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BindingTerminalPrepareError {
Authority,
TransactionOrder,
DeliverySequence,
ObserverProgress,
}
#[derive(Debug, PartialEq, Eq)]
pub struct BindingTerminalPrepareRefused {
owner: LiveFrontierOwner,
error: BindingTerminalPrepareError,
}
impl BindingTerminalPrepareRefused {
#[must_use]
pub const fn error(&self) -> BindingTerminalPrepareError {
self.error
}
#[must_use]
pub fn into_owner(self) -> LiveFrontierOwner {
self.owner
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct PreparedBindingTerminal {
owner: LiveFrontierOwner,
key: CandidateTerminalKey,
hard_observer_progress: DeliverySeq,
}
impl PreparedBindingTerminal {
#[must_use]
pub const fn candidate_key(&self) -> CandidateTerminalKey {
self.key
}
#[must_use]
pub const fn hard_observer_progress(&self) -> DeliverySeq {
self.hard_observer_progress
}
#[must_use]
pub fn admit(self, candidate: BindingTerminalCandidateCharge) -> BindingTerminalAdmission {
if candidate.conversation_id != self.key.conversation_id()
|| candidate.participant_id != self.key.participant_id()
|| candidate.binding_epoch != self.key.binding_epoch()
|| candidate.admission_order != self.key.admission_order()
|| candidate.delivery_seq != self.key.delivery_seq()
|| candidate.encoding != BindingTerminalEncoding::ParticipantLifecycleV3CanonicalJson
|| candidate.charge.delivery_seq() != self.key.delivery_seq()
|| candidate.charge.admission_order() != self.key.admission_order()
|| candidate.charge.encoded_charge().entries != 1
{
return admit_refusal(self.owner, BindingTerminalAdmitError::CandidateCharge);
}
let retained_len = u64::try_from(self.owner.retained_charges().len());
let has_capacity = retained_len
.ok()
.and_then(|len| len.checked_add(1))
.is_some_and(|len| len <= self.owner.retained_record_limit());
if has_capacity {
return match self.owner.commit_binding_terminal_candidate(
self.key.active_binding,
self.key.admission_order,
self.key.delivery_seq,
candidate.charge,
) {
Ok(owner) => BindingTerminalAdmission::Commit(BindingTerminalCommit {
owner,
position: CommittedBindingTerminalPosition::new(
self.key.admission_order.transaction_order(),
self.key.delivery_seq,
),
}),
Err(failure) => {
let (owner, error) = *failure;
admit_refusal(owner, map_live_frontier_error(error))
}
};
}
if self.hard_observer_progress < self.key.delivery_seq {
return match self.owner.pend_binding_terminal_candidate(
self.key.active_binding,
self.key.admission_order,
self.key.delivery_seq,
) {
Ok(owner) => BindingTerminalAdmission::Pending(BindingTerminalPending {
owner,
position: PendingBindingTerminalPosition::new(
self.key.admission_order.transaction_order(),
),
blocked_at_observer: self.hard_observer_progress,
}),
Err(failure) => {
let (owner, error) = *failure;
admit_refusal(owner, map_live_frontier_error(error))
}
};
}
admit_refusal(self.owner, BindingTerminalAdmitError::RetainedRecordLimit)
}
#[must_use]
pub fn into_owner(self) -> LiveFrontierOwner {
self.owner
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct BindingTerminalCommit {
owner: LiveFrontierOwner,
position: CommittedBindingTerminalPosition,
}
impl BindingTerminalCommit {
#[must_use]
pub fn into_parts(self) -> (LiveFrontierOwner, CommittedBindingTerminalPosition) {
(self.owner, self.position)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct BindingTerminalPending {
owner: LiveFrontierOwner,
position: PendingBindingTerminalPosition,
blocked_at_observer: DeliverySeq,
}
impl BindingTerminalPending {
#[must_use]
pub const fn blocked_at_observer(&self) -> DeliverySeq {
self.blocked_at_observer
}
#[must_use]
pub fn into_parts(self) -> (LiveFrontierOwner, PendingBindingTerminalPosition) {
(self.owner, self.position)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BindingTerminalAdmitError {
CandidateCharge,
RetainedRecordLimit,
Authority,
Precedence,
Frontier,
ClosureAccounting,
}
#[derive(Debug, PartialEq, Eq)]
pub struct BindingTerminalAdmitRefused {
owner: LiveFrontierOwner,
error: BindingTerminalAdmitError,
}
impl BindingTerminalAdmitRefused {
#[must_use]
pub const fn error(&self) -> BindingTerminalAdmitError {
self.error
}
#[must_use]
pub fn into_owner(self) -> LiveFrontierOwner {
self.owner
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum BindingTerminalAdmission {
Commit(BindingTerminalCommit),
Pending(BindingTerminalPending),
Refused(Box<BindingTerminalAdmitRefused>),
}
fn admit_refusal(
owner: LiveFrontierOwner,
error: BindingTerminalAdmitError,
) -> BindingTerminalAdmission {
BindingTerminalAdmission::Refused(Box::new(BindingTerminalAdmitRefused { owner, error }))
}
const fn map_live_frontier_error(error: LiveFrontierError) -> BindingTerminalAdmitError {
match error {
LiveFrontierError::Authority => BindingTerminalAdmitError::Authority,
LiveFrontierError::Precedence => BindingTerminalAdmitError::Precedence,
LiveFrontierError::RetainedCharge => BindingTerminalAdmitError::CandidateCharge,
LiveFrontierError::RetainedRecordLimit => BindingTerminalAdmitError::RetainedRecordLimit,
LiveFrontierError::Frontier => BindingTerminalAdmitError::Frontier,
LiveFrontierError::ClosureAccounting => BindingTerminalAdmitError::ClosureAccounting,
}
}
impl LiveFrontierOwner {
pub fn prepare_binding_terminal(
self,
active_binding: ActiveBinding,
cause_class: BindingTerminalCauseClass,
next_transaction_order: TransactionOrder,
next_delivery_sequence: DeliverySeq,
hard_observer_progress: DeliverySeq,
) -> Result<PreparedBindingTerminal, Box<BindingTerminalPrepareRefused>> {
let authority_matches = active_binding.conversation_id
== self.frontiers().conversation_id()
&& self
.frontiers()
.active_identities()
.participants()
.iter()
.any(|participant| {
participant.participant_index() == active_binding.participant_id
&& participant.binding()
== crate::lifecycle::FrontierBinding::Bound(
active_binding.binding_epoch,
)
});
if !authority_matches {
return prepare_refusal(self, BindingTerminalPrepareError::Authority);
}
let expected_order = match self.frontiers().order().ledger().high() {
crate::lifecycle::OrderHigh::Empty => Some(0),
crate::lifecycle::OrderHigh::Allocated(high) => high.checked_add(1),
};
if expected_order != Some(next_transaction_order) {
return prepare_refusal(self, BindingTerminalPrepareError::TransactionOrder);
}
if self
.frontiers()
.sequence()
.ledger()
.high_watermark()
.checked_add(1)
!= Some(next_delivery_sequence)
{
return prepare_refusal(self, BindingTerminalPrepareError::DeliverySequence);
}
if hard_observer_progress > self.frontiers().sequence().ledger().high_watermark() {
return prepare_refusal(self, BindingTerminalPrepareError::ObserverProgress);
}
Ok(PreparedBindingTerminal {
owner: self,
key: CandidateTerminalKey {
active_binding,
cause_class,
admission_order: AdmissionOrder::binding_terminal(
next_transaction_order,
active_binding.participant_id,
),
delivery_seq: next_delivery_sequence,
},
hard_observer_progress,
})
}
}
fn prepare_refusal(
owner: LiveFrontierOwner,
error: BindingTerminalPrepareError,
) -> Result<PreparedBindingTerminal, Box<BindingTerminalPrepareRefused>> {
Err(Box::new(BindingTerminalPrepareRefused { owner, error }))
}