liminal_server/server/participant/production/
marker_source.rs1use std::sync::Arc;
4
5use liminal::durability::DurableStore;
6use liminal_protocol::lifecycle::{
7 FencedMarkerSourceExpectation, LiveFrontierOwner, MarkerSequenceOwner,
8 RetainedFencedMarkerSource,
9};
10
11use super::log::{
12 DecodedStoredOperation, OperationLog, StoredMarkerDrain, StoredOperationV2, StoredOperationV3,
13};
14use super::restore_checkpoint_locator::checkpoint_conversation_route;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
18pub enum MarkerSourceRefusalReason {
19 #[error("durable marker source could not be read or decoded")]
21 DurableRead,
22 #[error("marker source sequence does not exist")]
24 Missing,
25 #[error("marker source sequence does not select MarkerDrained")]
27 WrongOperation,
28 #[error("durable marker body differs from the frontier-validated record")]
30 MarkerBody,
31 #[error("durable marker delivery sequence differs from the frontier-validated record")]
33 DeliverySequence,
34 #[error("durable marker transaction order differs from the frontier-validated record")]
36 TransactionOrder,
37 #[error("durable marker candidate phase differs from the frontier-validated record")]
39 CandidatePhase,
40 #[error("durable marker participant differs from the frontier-validated record")]
42 Participant,
43 #[error("durable marker retained charge differs from its measured canonical bytes")]
45 RetainedCharge,
46}
47
48#[derive(Debug)]
50pub struct MarkerSourceRefused {
51 retained: RetainedFencedMarkerSource,
52 marker_source_sequence: u64,
53 reason: MarkerSourceRefusalReason,
54}
55
56impl MarkerSourceRefused {
57 #[must_use]
59 pub const fn reason(&self) -> MarkerSourceRefusalReason {
60 self.reason
61 }
62
63 #[must_use]
65 pub fn into_parts(self) -> (RetainedFencedMarkerSource, u64) {
66 (self.retained, self.marker_source_sequence)
67 }
68}
69
70#[derive(Debug)]
72pub struct ValidatedFencedMarkerInputs {
73 retained: RetainedFencedMarkerSource,
74 marker_source_sequence: u64,
75}
76
77impl ValidatedFencedMarkerInputs {
78 #[must_use]
80 pub fn into_parts(
81 self,
82 ) -> (
83 LiveFrontierOwner,
84 liminal_protocol::lifecycle::DetachedCredentialRecovery,
85 u64,
86 ) {
87 let (owner, recovery) = self.retained.into_parts();
88 (owner, recovery, self.marker_source_sequence)
89 }
90}
91
92pub async fn validate_marker_source(
99 store: Arc<dyn DurableStore>,
100 retained: RetainedFencedMarkerSource,
101 marker_source_sequence: u64,
102) -> Result<ValidatedFencedMarkerInputs, Box<MarkerSourceRefused>> {
103 let expectation = retained.expectation();
104 let log = OperationLog::new(store, expectation.conversation_id());
105 let result = read_and_validate(&log, marker_source_sequence, expectation).await;
106 match result {
107 Ok(()) => Ok(ValidatedFencedMarkerInputs {
108 retained,
109 marker_source_sequence,
110 }),
111 Err(reason) => Err(Box::new(MarkerSourceRefused {
112 retained,
113 marker_source_sequence,
114 reason,
115 })),
116 }
117}
118
119async fn read_and_validate(
120 log: &OperationLog,
121 marker_source_sequence: u64,
122 expectation: FencedMarkerSourceExpectation,
123) -> Result<(), MarkerSourceRefusalReason> {
124 let route = checkpoint_conversation_route(expectation.conversation_id());
128 let operation = log
129 .read_committed_at(&route, marker_source_sequence)
130 .await
131 .map_err(|_| MarkerSourceRefusalReason::DurableRead)?
132 .ok_or(MarkerSourceRefusalReason::Missing)?
133 .operation;
134 let row = match operation {
135 DecodedStoredOperation::V2(StoredOperationV2::MarkerDrained { row })
136 | DecodedStoredOperation::V3(StoredOperationV3::MarkerDrained { row }) => row,
137 DecodedStoredOperation::V2(_) | DecodedStoredOperation::V3(_) => {
138 return Err(MarkerSourceRefusalReason::WrongOperation);
139 }
140 };
141 validate_row(&row, expectation)
142}
143
144fn validate_row(
145 row: &StoredMarkerDrain,
146 expectation: FencedMarkerSourceExpectation,
147) -> Result<(), MarkerSourceRefusalReason> {
148 let canonical_marker = canonical_marker_bytes(expectation);
149 if row.marker != canonical_marker {
150 return Err(MarkerSourceRefusalReason::MarkerBody);
151 }
152 let charge = row.retained_charge;
153 let order = expectation.admission_order();
154 if charge.delivery_seq != expectation.marker_delivery_seq() {
155 return Err(MarkerSourceRefusalReason::DeliverySequence);
156 }
157 if charge.transaction_order != order.transaction_order() {
158 return Err(MarkerSourceRefusalReason::TransactionOrder);
159 }
160 if charge.candidate_phase != order.candidate_phase() as u8 {
161 return Err(MarkerSourceRefusalReason::CandidatePhase);
162 }
163 if charge.participant_id != expectation.participant_id() {
164 return Err(MarkerSourceRefusalReason::Participant);
165 }
166 let marker_bytes = u64::try_from(canonical_marker.len())
167 .map_err(|_| MarkerSourceRefusalReason::RetainedCharge)?;
168 if charge.charge.entries != 1 || charge.charge.bytes != marker_bytes {
169 return Err(MarkerSourceRefusalReason::RetainedCharge);
170 }
171 Ok(())
172}
173
174pub(super) fn canonical_marker_bytes(expectation: FencedMarkerSourceExpectation) -> Vec<u8> {
175 format!(
176 "MarkerCandidateAuthority {{ delivery_seq: {:?}, admission_order: {:?}, target_binding: {:?}, provenance: {:?}, current_owner: {:?} }}",
177 expectation.marker_delivery_seq(),
178 expectation.admission_order(),
179 expectation.target_binding(),
180 expectation.provenance(),
181 MarkerSequenceOwner::Marker,
182 )
183 .into_bytes()
184}