1use crate::core::{
4 CloseReason, CoreError, ErrorCategory, InterlaceObservation, InterlaceResourceLimits,
5 InterlaceSide, MachineError, PeerProofAssurance, validate_expected_peer_by,
6};
7use crate::iltp::{PortText, StreamEncoding, StreamOffers};
8use crate::records::{BySecret, ByValue, Record, RecordHash};
9use std::{
10 collections::{HashMap, HashSet},
11 sync::Arc,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ParticipantPosition {
16 First,
17 Second,
18}
19impl ParticipantPosition {
20 pub fn other(self) -> Self {
21 if self == Self::First {
22 Self::Second
23 } else {
24 Self::First
25 }
26 }
27}
28
29impl InterlaceSide {
30 #[doc(hidden)]
34 pub fn participant_position(
35 self,
36 local: Option<(InterlaceSide, ParticipantPosition)>,
37 ) -> ParticipantPosition {
38 match local {
39 Some((local_side, local_position)) if self == local_side => local_position,
40 Some((_, local_position)) => local_position.other(),
41 None if self == InterlaceSide::Side0 => ParticipantPosition::First,
42 None => ParticipantPosition::Second,
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum InterlaceEvent {
50 RecordSent {
51 position: ParticipantPosition,
52 record_hash: RecordHash,
53 },
54 RecordSaved {
55 position: ParticipantPosition,
56 record_hash: RecordHash,
57 },
58 FixedPoint,
59 Closed {
60 reason: CloseReason,
61 },
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum OpenEvent {
68 RecordSent {
69 position: ParticipantPosition,
70 record_hash: RecordHash,
71 },
72 RecordSaved {
73 position: ParticipantPosition,
74 record_hash: RecordHash,
75 record: Record,
76 },
77 FixedPoint,
78 Closed {
79 reason: CloseReason,
80 },
81}
82
83impl OpenEvent {
84 #[doc(hidden)]
85 pub fn from_observation(
86 observation: InterlaceObservation,
87 local: Option<(InterlaceSide, ParticipantPosition)>,
88 ) -> Self {
89 match observation {
90 InterlaceObservation::RecordSent {
91 side, record_hash, ..
92 } => Self::RecordSent {
93 position: side.participant_position(local),
94 record_hash,
95 },
96 InterlaceObservation::RecordSaved { side, record, .. } => Self::RecordSaved {
97 position: side.participant_position(local),
98 record_hash: record.hash(),
99 record,
100 },
101 }
102 }
103}
104
105impl From<OpenEvent> for InterlaceEvent {
106 fn from(event: OpenEvent) -> Self {
107 match event {
108 OpenEvent::RecordSent {
109 position,
110 record_hash,
111 } => Self::RecordSent {
112 position,
113 record_hash,
114 },
115 OpenEvent::RecordSaved {
116 position,
117 record_hash,
118 ..
119 } => Self::RecordSaved {
120 position,
121 record_hash,
122 },
123 OpenEvent::FixedPoint => Self::FixedPoint,
124 OpenEvent::Closed { reason } => Self::Closed { reason },
125 }
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum TimeoutPhase {
131 Prefacing,
132 Side0Proof,
133 Side1Proof,
134 Side0Operand,
135 Side1Operand,
136 Advertising,
137 Requesting,
138 Answering,
139 Fencing,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct Error {
144 detail: Arc<ErrorDetail>,
145 position: Option<ParticipantPosition>,
146 additional_errors: Vec<Error>,
147}
148
149#[derive(Debug, PartialEq, Eq)]
150struct ErrorDetail {
151 category: ErrorCategory,
152 record_hash: Option<RecordHash>,
153 message: String,
154 phase: Option<TimeoutPhase>,
155}
156
157impl Error {
158 fn new(
159 category: ErrorCategory,
160 position: Option<ParticipantPosition>,
161 record_hash: Option<RecordHash>,
162 message: impl Into<String>,
163 ) -> Self {
164 Self {
165 detail: Arc::new(ErrorDetail {
166 category,
167 record_hash,
168 message: message.into(),
169 phase: None,
170 }),
171 position,
172 additional_errors: Vec::new(),
173 }
174 }
175
176 pub fn with_cleanup(self, cleanup: Self) -> Self {
179 Self::combine([self, cleanup]).expect("two failures")
180 }
181
182 #[doc(hidden)]
183 pub fn combine(errors: impl IntoIterator<Item = Self>) -> Option<Self> {
184 let mut errors = errors.into_iter().peekable();
185 let first = errors.next()?;
186 if errors.peek().is_none() {
187 return Some(first);
188 }
189 let mut positions = HashMap::new();
190 let mut combined: Vec<Self> = Vec::new();
191 for mut group in std::iter::once(first).chain(errors) {
192 let additional = std::mem::take(&mut group.additional_errors);
193 for error in std::iter::once(group).chain(additional) {
195 let identity = Arc::as_ptr(&error.detail);
196 if let Some(&index) = positions.get(&identity) {
197 let retained: &mut Self = &mut combined[index];
198 retained.position = retained.position.or(error.position);
199 } else {
200 positions.insert(identity, combined.len());
201 combined.push(error);
202 }
203 }
204 }
205 let mut combined = combined.into_iter();
206 let mut first = combined.next()?;
207 first.additional_errors = combined.collect();
208 Some(first)
209 }
210
211 #[doc(hidden)]
212 pub fn with_position(mut self, position: ParticipantPosition) -> Self {
213 self.position.get_or_insert(position);
214 for error in &mut self.additional_errors {
215 error.position.get_or_insert(position);
216 }
217 self
218 }
219
220 pub fn additional_errors(&self) -> &[Self] {
223 &self.additional_errors
224 }
225
226 #[doc(hidden)]
227 pub fn peer_protocol(message: impl Into<String>) -> Self {
228 Self::new(ErrorCategory::PeerProtocol, None, None, message)
229 }
230
231 pub fn policy(message: impl Into<String>) -> Self {
232 Self::new(ErrorCategory::Policy, None, None, message)
233 }
234
235 #[doc(hidden)]
236 pub fn transport(message: impl Into<String>) -> Self {
237 Self::new(ErrorCategory::Transport, None, None, message)
238 }
239
240 #[doc(hidden)]
241 pub fn integration(message: impl Into<String>) -> Self {
242 Self::new(ErrorCategory::Integration, None, None, message)
243 }
244
245 #[doc(hidden)]
246 pub fn detailed(
247 category: ErrorCategory,
248 position: Option<ParticipantPosition>,
249 record_hash: Option<RecordHash>,
250 message: impl Into<String>,
251 ) -> Self {
252 Self::new(category, position, record_hash, message)
253 }
254
255 #[doc(hidden)]
256 pub fn store(
257 position: Option<ParticipantPosition>,
258 record_hash: Option<RecordHash>,
259 message: impl Into<String>,
260 ) -> Self {
261 Self::new(ErrorCategory::Store, position, record_hash, message)
262 }
263
264 #[doc(hidden)]
267 pub fn from_machine(
268 error: MachineError,
269 local: Option<(InterlaceSide, ParticipantPosition)>,
270 ) -> Self {
271 Self::new(
272 error.category,
273 error.side.map(|side| side.participant_position(local)),
274 error.record_hash,
275 error.detail,
276 )
277 }
278
279 #[doc(hidden)]
280 pub fn timeout(position: Option<ParticipantPosition>, phase: TimeoutPhase) -> Self {
281 Self {
282 detail: Arc::new(ErrorDetail {
283 category: ErrorCategory::Timeout,
284 record_hash: None,
285 message: format!("interlace phase timed out in {phase:?}"),
286 phase: Some(phase),
287 }),
288 position,
289 additional_errors: Vec::new(),
290 }
291 }
292
293 pub fn category(&self) -> ErrorCategory {
294 self.detail.category
295 }
296 pub fn position(&self) -> Option<ParticipantPosition> {
297 self.position
298 }
299 pub fn record_hash(&self) -> Option<RecordHash> {
300 self.detail.record_hash
301 }
302 pub fn message(&self) -> &str {
303 &self.detail.message
304 }
305 pub fn timeout_phase(&self) -> Option<TimeoutPhase> {
306 self.detail.phase
307 }
308}
309
310impl core::fmt::Display for Error {
311 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312 write!(f, "{:?}: {}", self.category(), self.message())?;
313 for error in &self.additional_errors {
314 write!(f, "; additional: {error}")?;
315 }
316 Ok(())
317 }
318}
319#[cfg(test)]
320#[path = "runtime_values_tests.rs"]
321mod tests;
322
323impl std::error::Error for Error {
324 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
325 self.additional_errors.first().map(|error| error as _)
326 }
327}
328impl From<CoreError> for Error {
329 fn from(value: CoreError) -> Self {
330 Self::new(value.category(), None, None, value.detail())
331 }
332}
333impl From<crate::logic::PreparationError> for Error {
334 fn from(value: crate::logic::PreparationError) -> Self {
335 let category = if value.is_limit_exceeded() {
336 ErrorCategory::LimitExceeded
337 } else {
338 ErrorCategory::Policy
339 };
340 Self::new(category, None, None, value.to_string())
341 }
342}
343impl From<std::io::Error> for Error {
344 fn from(value: std::io::Error) -> Self {
345 Self::transport(value.to_string())
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub enum Terminal {
351 Complete,
352 StoppedByLimit,
353 Failed(Error),
354}
355
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct BoundedInterlaceOutcome {
358 pub terminal: Terminal,
359 pub observations: Vec<InterlaceEvent>,
360 pub peer_by: Option<ByValue>,
363 pub outgoing_encoding: Option<StreamEncoding>,
366 pub incoming_encoding: Option<StreamEncoding>,
368}
369
370#[doc(hidden)]
373#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
374pub struct PeerDiagnostics {
375 pub peer_by: Option<ByValue>,
376 pub outgoing_encoding: Option<StreamEncoding>,
377 pub incoming_encoding: Option<StreamEncoding>,
378}
379
380#[doc(hidden)]
383pub fn bounded_terminal(error: Error) -> Terminal {
384 if error.category() == ErrorCategory::LimitExceeded && error.additional_errors().is_empty() {
385 Terminal::StoppedByLimit
386 } else {
387 Terminal::Failed(error)
388 }
389}
390
391#[doc(hidden)]
393pub fn settled_terminal(
394 primary: Option<Error>,
395 cleanup: impl IntoIterator<Item = Error>,
396) -> Terminal {
397 match (primary, Error::combine(cleanup)) {
398 (Some(primary), Some(cleanup)) => bounded_terminal(primary.with_cleanup(cleanup)),
399 (None, Some(cleanup)) => Terminal::Failed(cleanup),
400 (Some(primary), None) => bounded_terminal(primary),
401 (None, None) => Terminal::Complete,
402 }
403}
404
405#[doc(hidden)]
408pub fn finish_bounded(
409 (mut observations, primary): (Vec<InterlaceEvent>, Option<Error>),
410 cleanup: impl IntoIterator<Item = Error>,
411 peer: PeerDiagnostics,
412) -> BoundedInterlaceOutcome {
413 let terminal = settled_terminal(primary, cleanup);
414 if !matches!(terminal, Terminal::Complete) {
415 observations.retain(|event| !matches!(event, InterlaceEvent::FixedPoint));
416 }
417 BoundedInterlaceOutcome {
418 terminal,
419 observations,
420 peer_by: peer.peer_by,
421 outgoing_encoding: peer.outgoing_encoding,
422 incoming_encoding: peer.incoming_encoding,
423 }
424}
425
426#[doc(hidden)]
429pub fn store_outcome(
430 outcome: BoundedInterlaceOutcome,
431 supplied: impl IntoIterator<Item = RecordHash>,
432) -> StoreOutcome {
433 let complete = matches!(outcome.terminal, Terminal::Complete);
434 let sent = outcome
435 .observations
436 .iter()
437 .filter_map(|event| match event {
438 InterlaceEvent::RecordSent {
439 position: ParticipantPosition::First,
440 record_hash,
441 } => Some(*record_hash),
442 _ => None,
443 })
444 .collect::<HashSet<_>>();
445 let mut seen = HashSet::new();
446 let records = supplied
447 .into_iter()
448 .filter(|hash| seen.insert(*hash))
449 .map(|record_hash| {
450 let provider_sent = sent.contains(&record_hash);
451 StoreRecordResult {
452 record_hash,
453 disposition: if !complete {
454 StoreRecordDisposition::Unsettled
455 } else if provider_sent {
456 StoreRecordDisposition::Saved
457 } else {
458 StoreRecordDisposition::Skipped
459 },
460 provider_sent,
461 target_saved: complete && provider_sent,
462 }
463 })
464 .collect();
465 StoreOutcome {
466 terminal: outcome.terminal,
467 observations: outcome.observations,
468 records,
469 }
470}
471
472#[derive(Debug, Clone, PartialEq, Eq, Default)]
473pub struct InterlaceOperationOptions {
474 pub limits: InterlaceResourceLimits,
475}
476impl InterlaceOperationOptions {
477 pub fn new() -> Self {
478 Self::default()
479 }
480 pub fn with_limits(mut self, limits: InterlaceResourceLimits) -> Self {
481 self.limits = limits;
482 self
483 }
484}
485
486#[derive(Debug, Clone, PartialEq, Eq)]
487pub struct ListOutcome {
488 pub terminal: Terminal,
489 pub observations: Vec<InterlaceEvent>,
490 pub records: Vec<Record>,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq)]
494pub struct GetOutcome {
495 pub terminal: Terminal,
496 pub observations: Vec<InterlaceEvent>,
497 pub record: Option<Record>,
498}
499
500#[derive(Debug, Clone, PartialEq, Eq)]
501pub enum StoreRecordDisposition {
502 Saved,
503 Skipped,
504 Unsettled,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq)]
508pub struct StoreRecordResult {
509 pub record_hash: RecordHash,
510 pub disposition: StoreRecordDisposition,
511 pub provider_sent: bool,
512 pub target_saved: bool,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq)]
516pub struct StoreOutcome {
517 pub terminal: Terminal,
518 pub observations: Vec<InterlaceEvent>,
519 pub records: Vec<StoreRecordResult>,
520}
521
522#[derive(Debug, Clone, PartialEq, Eq)]
523pub enum OpenBoundary {
524 FixedPoint,
525 Closed(CloseReason),
526}
527
528#[derive(Debug, Clone, PartialEq, Eq)]
529pub struct OpenBoundaryOutcome {
530 pub boundary: OpenBoundary,
531 pub observations: Vec<OpenEvent>,
532}
533
534#[derive(Debug, Clone)]
535pub struct ConnectedPeerOptions {
536 by_secret: Option<BySecret>,
537 peer_proof: PeerProofAssurance,
538 expected_peer_by: Option<ByValue>,
539 stream_encodings: Vec<StreamEncoding>,
540 port: Option<PortText>,
541}
542impl Default for ConnectedPeerOptions {
543 fn default() -> Self {
544 Self {
545 by_secret: None,
546 peer_proof: PeerProofAssurance::default(),
547 expected_peer_by: None,
548 stream_encodings: StreamEncoding::COMPILED.to_vec(),
549 port: None,
550 }
551 }
552}
553impl ConnectedPeerOptions {
554 pub fn new() -> Self {
555 Self::default()
556 }
557 pub fn with_by_secret(mut self, secret: BySecret) -> Self {
558 self.by_secret = Some(secret);
559 self
560 }
561 pub fn with_peer_proof(mut self, value: PeerProofAssurance) -> Self {
562 self.peer_proof = value;
563 self
564 }
565 pub fn with_expected_peer_by(mut self, value: ByValue) -> Self {
566 self.expected_peer_by = Some(value);
567 self
568 }
569 pub fn with_stream_encodings(
574 mut self,
575 encodings: impl IntoIterator<Item = StreamEncoding>,
576 ) -> Self {
577 self.stream_encodings = encodings.into_iter().collect();
578 self
579 }
580 pub fn with_port(mut self, port: PortText) -> Self {
582 self.port = Some(port);
583 self
584 }
585 #[doc(hidden)]
586 pub fn validate(&self) -> Result<(), CoreError> {
587 validate_expected_peer_by(self.peer_proof, self.expected_peer_by)?;
588 StreamOffers::new(&self.stream_encodings).map_err(CoreError::InvalidConfig)?;
589 if matches!(self.peer_proof, PeerProofAssurance::ChannelBound(_))
590 && self.by_secret.is_none()
591 {
592 return Err(CoreError::InvalidConfig(
593 "channel-bound peer proof requires a local By-secret",
594 ));
595 }
596 Ok(())
597 }
598 #[doc(hidden)]
599 pub fn into_parts(
600 self,
601 ) -> (
602 Option<BySecret>,
603 PeerProofAssurance,
604 Option<ByValue>,
605 Vec<StreamEncoding>,
606 Option<PortText>,
607 ) {
608 (
609 self.by_secret,
610 self.peer_proof,
611 self.expected_peer_by,
612 self.stream_encodings,
613 self.port,
614 )
615 }
616}