1mod admission;
8mod election;
9pub use admission::ReceiveFence;
10mod flow;
11pub use flow::flow_state_route;
12mod compact;
13pub use compact::{COMPACT_STATE_BYTES, CompactState};
14mod prepare;
15mod publication;
16pub use publication::{
17 PUBLICATION_BYTES, PUBLICATION_TOPIC_BYTES, PublicationPart, decode_publication,
18 encode_publication, encode_publication_group, publication_topic,
19};
20mod checkpoint;
21mod recovery;
22pub use checkpoint::{
23 CheckpointMessage, CheckpointRequest, HistoryFence, HistoryRetired, encode_checkpoint,
24 encode_history_retired,
25};
26mod transfer;
27
28pub use flow::{FlowMessage, FlowProbe, FlowState, encode_flow_probe, encode_flow_state};
29pub use prepare::{
30 Operation, Operations, Prepare, PrepareBatch, PrepareEncoding, PrepareMetadata,
31 VerifiedOperation, encode_flow_prepare, encode_flow_prepare_metadata, encode_prepare,
32 encode_prepare_metadata, encode_prepare_unbound,
33};
34pub use recovery::{
35 RecoveryMessage, RecoveryRequest, RecoveryState, encode_recovery, encode_recovery_state,
36};
37pub use transfer::{FetchOps, OpsBatch, encode_fetch, encode_ops};
38
39use ozzy_proto::{Envelope, EnvelopeError, EnvelopeLimits, Opcode};
40use ozzy_proto::{GroupId, LinkSessionId, NodeId};
41
42use crate::flow::{FlowError, ReceiveEpoch};
43use crate::{
44 Commit, Configuration, Digest, DoViewChange, OpNumber, Prefix, PrepareOk, QuorumPolicy,
45 ReplicationError, RetainedPrepareOk, Scope, StartView, StartViewChange,
46};
47
48const COMMON_BYTES: usize = 80;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct PeerBinding {
56 configuration: Configuration,
57 peer: NodeId,
58 session: LinkSessionId,
59 receive_epoch: Option<ReceiveEpoch>,
60}
61
62impl PeerBinding {
63 pub fn new(
65 configuration: Configuration,
66 peer: NodeId,
67 session: LinkSessionId,
68 ) -> Result<Self, WireError> {
69 configuration.voter_index(peer)?;
70 if session.as_bytes() == &[0; 16] {
71 return Err(WireError::Peer);
72 }
73 Ok(Self {
74 configuration,
75 peer,
76 session,
77 receive_epoch: None,
78 })
79 }
80
81 #[must_use]
87 pub const fn with_receive_epoch(mut self, epoch: ReceiveEpoch) -> Self {
88 self.receive_epoch = Some(epoch);
89 self
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct WireLimits {
96 pub envelope: EnvelopeLimits,
98 pub max_operations: usize,
100}
101
102impl WireLimits {
103 pub fn for_transfer(max_operations: usize, max_body_bytes: usize) -> Option<Self> {
106 let fixed = transfer::FETCH_BYTES
107 .max(transfer::OPS_FIXED_BYTES)
108 .max(prepare::BATCH_BYTES + 16)
109 .next_multiple_of(16);
110 Some(Self {
111 envelope: EnvelopeLimits {
112 max_metadata_bytes: max_operations
113 .checked_mul(prepare::DESCRIPTOR_BYTES)?
114 .checked_add(fixed)?,
115 max_payload_bytes: max_body_bytes,
116 },
117 max_operations,
118 })
119 }
120
121 pub fn message_bytes(self) -> Option<usize> {
123 self.envelope
124 .max_metadata_bytes
125 .checked_add(self.envelope.max_payload_bytes)?
126 .checked_add(ozzy_proto::ENVELOPE_BYTES + 16)
127 }
128}
129
130impl Default for WireLimits {
131 fn default() -> Self {
132 Self {
133 envelope: EnvelopeLimits::default(),
134 max_operations: 64,
135 }
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum Control {
142 PrepareOk {
144 ack: PrepareOk,
146 },
147 PrepareRetained {
149 ack: RetainedPrepareOk,
151 },
152 Commit(Commit),
154 ExitView(Scope),
156 StartViewChange(StartViewChange),
158 DoViewChange(DoViewChange),
160 StartView(StartView),
162}
163
164impl Control {
165 pub const fn acknowledged(self) -> Option<Prefix> {
167 match self {
168 Self::PrepareOk { ack, .. } => Some(ack.durable),
169 Self::PrepareRetained { ack, .. } => Some(ack.retained),
170 _ => None,
171 }
172 }
173
174 pub const fn scope(self) -> Scope {
176 match self {
177 Self::PrepareOk { ack, .. } => ack.scope,
178 Self::PrepareRetained { ack, .. } => ack.scope,
179 Self::Commit(message) => message.scope,
180 Self::ExitView(scope) => scope,
181 Self::StartViewChange(message) => message.scope,
182 Self::DoViewChange(message) => message.scope,
183 Self::StartView(message) => message.scope,
184 }
185 }
186
187 fn opcode(self) -> Opcode {
188 match self {
189 Self::PrepareOk { .. } | Self::PrepareRetained { .. } => Opcode::PrepareOk,
190 Self::Commit(_) => Opcode::Commit,
191 Self::ExitView(_) => Opcode::ExitView,
192 Self::StartViewChange(_) => Opcode::StartViewChange,
193 Self::DoViewChange(_) => Opcode::DoViewChange,
194 Self::StartView(_) => Opcode::StartView,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum ReplicaMessage<'a> {
202 HistoryRetired(HistoryRetired),
204 Checkpoint(CheckpointMessage<'a>),
206 Recovery(RecoveryMessage),
208 Flow(FlowMessage<'a>),
210 Control(Control),
212 Prepare(PrepareBatch<'a>),
214 FetchOps(FetchOps),
216 Ops(OpsBatch<'a>),
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub struct ControlEncoding {
223 pub header: [u8; 64],
225 pub metadata_bytes: usize,
227}
228
229pub fn encode_control(
234 sender: NodeId,
235 session: LinkSessionId,
236 message: Control,
237 output: &mut [u8],
238) -> Result<ControlEncoding, WireError> {
239 let scope = message.scope();
240 validate_scope(scope)?;
241 let metadata_bytes = match message {
242 Control::PrepareOk { ack, .. } => {
243 validate_prefix(ack.durable)?;
244 COMMON_BYTES + 40 + 1
245 }
246 Control::PrepareRetained { ack, .. } => {
247 validate_prefix(ack.retained)?;
248 COMMON_BYTES + 40 + 1
249 }
250 Control::Commit(message) => {
251 validate_prefix(message.committed)?;
252 COMMON_BYTES + 40
253 }
254 Control::StartViewChange(message) => {
255 validate_election_view(message.scope)?;
256 COMMON_BYTES
257 }
258 Control::ExitView(_) => COMMON_BYTES,
259 Control::DoViewChange(message) => {
260 election::validate_report(message)?;
261 COMMON_BYTES + 16 + 8 + 40 + 40
262 }
263 Control::StartView(message) => {
264 election::validate_start(message)?;
265 COMMON_BYTES + 16 + 40 + 40
266 }
267 };
268 if output.len() < metadata_bytes {
269 return Err(WireError::Capacity);
270 }
271 let header = Envelope {
272 opcode: message.opcode(),
273 response: false,
274 request_id: None,
275 sender,
276 session: Some(session),
277 }
278 .encode_header(metadata_bytes, 0, EnvelopeLimits::default())?;
279 let mut writer = Writer::new(&mut output[..metadata_bytes]);
280 writer.scope(scope, sender);
281 match message {
282 Control::PrepareOk { ack } => {
283 writer.prefix(ack.durable);
284 writer.bytes(&[2]);
285 }
286 Control::PrepareRetained { ack } => {
287 writer.prefix(ack.retained);
288 writer.bytes(&[3]);
289 }
290 Control::Commit(message) => writer.prefix(message.committed),
291 Control::StartViewChange(_) | Control::ExitView(_) => {}
292 Control::DoViewChange(message) => election::write_report(&mut writer, message),
293 Control::StartView(message) => election::write_start(&mut writer, message),
294 }
295 Ok(ControlEncoding {
296 header,
297 metadata_bytes,
298 })
299}
300
301pub fn route(packet: ozzy_proto::Packet<'_>, limits: EnvelopeLimits) -> Result<Scope, WireError> {
306 packet
307 .envelope
308 .validate_frames(packet.metadata.len(), packet.payload.len(), limits)?;
309 if !matches!(
310 packet.envelope.opcode,
311 Opcode::ReplicaOpen
312 | Opcode::ReplicaState
313 | Opcode::Prepare
314 | Opcode::PrepareFlow
315 | Opcode::PreparePub
316 | Opcode::PrepareOk
317 | Opcode::Commit
318 | Opcode::ExitView
319 | Opcode::StartViewChange
320 | Opcode::DoViewChange
321 | Opcode::StartView
322 | Opcode::Recovery
323 | Opcode::RecoveryState
324 | Opcode::FetchOps
325 | Opcode::Ops
326 | Opcode::HistoryRetired
327 | Opcode::SnapshotBegin
328 | Opcode::SnapshotChunk
329 ) {
330 return Err(WireError::UnsupportedCommand);
331 }
332 if !matches!(
333 packet.envelope.opcode,
334 Opcode::Prepare
335 | Opcode::PrepareFlow
336 | Opcode::PreparePub
337 | Opcode::Ops
338 | Opcode::SnapshotChunk
339 ) && !packet.payload.is_empty()
340 {
341 return Err(WireError::Payload);
342 }
343 Reader::new(packet.metadata).routing_scope(packet.envelope.sender)
344}
345
346pub fn decode<'a>(
351 frames: &[&'a [u8]],
352 binding: PeerBinding,
353 limits: WireLimits,
354) -> Result<ReplicaMessage<'a>, WireError> {
355 let packet = ozzy_proto::decode_packet(frames, limits.envelope)?;
356 if packet.envelope.sender != binding.peer || packet.envelope.session != Some(binding.session) {
357 return Err(WireError::Peer);
358 }
359 let mut reader = Reader::new(packet.metadata);
360 let scope = reader.scope(binding)?;
361 match packet.envelope.opcode {
362 Opcode::HistoryRetired => {
363 return checkpoint::decode_retired(scope, packet.envelope, reader, packet.payload)
364 .map(ReplicaMessage::HistoryRetired);
365 }
366 Opcode::SnapshotBegin | Opcode::SnapshotChunk => {
367 return checkpoint::decode(
368 scope,
369 packet.envelope,
370 reader,
371 packet.payload,
372 binding,
373 limits,
374 )
375 .map(ReplicaMessage::Checkpoint);
376 }
377 Opcode::Recovery => {
378 return recovery::decode_request(scope, packet.envelope, reader, packet.payload)
379 .map(|request| ReplicaMessage::Recovery(RecoveryMessage::Request(request)));
380 }
381 Opcode::RecoveryState => {
382 return recovery::decode_state(scope, packet.envelope, reader, packet.payload, binding)
383 .map(|state| ReplicaMessage::Recovery(RecoveryMessage::State(state)));
384 }
385 Opcode::ReplicaOpen => {
386 return flow::decode_probe(scope, packet.envelope, reader, packet.payload)
387 .map(|probe| ReplicaMessage::Flow(FlowMessage::Probe(probe)));
388 }
389 Opcode::ReplicaState => {
390 return flow::decode_state(scope, packet.envelope, reader, packet.payload)
391 .map(|state| ReplicaMessage::Flow(FlowMessage::State(state)));
392 }
393 _ => {}
394 }
395 if packet.envelope.opcode == Opcode::FetchOps {
396 return transfer::decode_fetch(scope, packet.envelope, reader, packet.payload, binding)
397 .map(ReplicaMessage::FetchOps);
398 }
399 if packet.envelope.opcode == Opcode::Ops {
400 return transfer::decode_ops(scope, packet.envelope, reader, packet.payload, limits)
401 .map(ReplicaMessage::Ops);
402 }
403 if packet.envelope.response || packet.envelope.request_id.is_some() {
404 return Err(WireError::Correlation);
405 }
406 if packet.envelope.opcode == Opcode::PrepareFlow {
407 let epoch = ReceiveEpoch::new(u128::from_be_bytes(reader.bytes()?))?;
408 if binding.receive_epoch != Some(epoch) {
409 return Err(FlowError::Channel.into());
410 }
411 return prepare::decode_prepare(scope, reader, packet.payload, limits)
412 .map(|batch| ReplicaMessage::Flow(FlowMessage::Prepare { epoch, batch }));
413 }
414 if packet.envelope.opcode == Opcode::Prepare {
415 if binding.receive_epoch.is_some() {
416 return Err(FlowError::Channel.into());
417 }
418 return prepare::decode_prepare(scope, reader, packet.payload, limits)
419 .map(ReplicaMessage::Prepare);
420 }
421 if !packet.payload.is_empty() {
422 return Err(WireError::Payload);
423 }
424 decode_control(scope, packet.envelope.opcode, reader, binding).map(ReplicaMessage::Control)
425}
426
427fn decode_control(
428 scope: Scope,
429 opcode: Opcode,
430 mut reader: Reader<'_>,
431 binding: PeerBinding,
432) -> Result<Control, WireError> {
433 let message = match opcode {
434 Opcode::PrepareOk => {
435 let prefix = reader.prefix()?;
436 let evidence = reader.bytes::<1>()?[0];
437 match (binding.configuration.policy(), evidence) {
438 (QuorumPolicy::Durable, 2) => Control::PrepareOk {
439 ack: PrepareOk {
440 scope,
441 durable: prefix,
442 },
443 },
444 (QuorumPolicy::Replicated, 3) => Control::PrepareRetained {
445 ack: RetainedPrepareOk {
446 scope,
447 retained: prefix,
448 },
449 },
450 _ => return Err(WireError::Evidence),
451 }
452 }
453 Opcode::Commit => Control::Commit(Commit {
454 scope,
455 committed: reader.prefix()?,
456 }),
457 Opcode::StartViewChange => {
458 validate_election_view(scope)?;
459 Control::StartViewChange(StartViewChange { scope })
460 }
461 Opcode::ExitView => Control::ExitView(scope),
462 Opcode::DoViewChange => Control::DoViewChange(election::read_report(scope, &mut reader)?),
463 Opcode::StartView => Control::StartView(election::read_start(scope, &mut reader)?),
464 _ => return Err(WireError::UnsupportedCommand),
465 };
466 reader.finish()?;
467 Ok(message)
468}
469
470fn validate_scope(scope: Scope) -> Result<(), WireError> {
471 if scope.group_id.as_bytes() == &[0; 16] || scope.configuration_digest == Digest::ZERO {
472 return Err(WireError::Scope);
473 }
474 Ok(())
475}
476
477fn validate_election_view(scope: Scope) -> Result<(), WireError> {
478 if scope.view == 0 {
479 return Err(WireError::History);
480 }
481 Ok(())
482}
483
484fn validate_prefix(prefix: Prefix) -> Result<(), WireError> {
485 if (prefix.op.0 == 0) != (prefix.digest == Digest::ZERO) || prefix.op.0 == u64::MAX {
486 return Err(WireError::Prefix);
487 }
488 Ok(())
489}
490
491#[derive(Debug)]
492struct Reader<'a> {
493 remaining: &'a [u8],
494}
495
496impl<'a> Reader<'a> {
497 fn new(bytes: &'a [u8]) -> Self {
498 Self { remaining: bytes }
499 }
500 fn bytes<const N: usize>(&mut self) -> Result<[u8; N], WireError> {
501 let (bytes, rest) = self
502 .remaining
503 .split_at_checked(N)
504 .ok_or(WireError::Length)?;
505 self.remaining = rest;
506 Ok(bytes.try_into().expect("exact field"))
507 }
508 fn u64(&mut self) -> Result<u64, WireError> {
509 Ok(u64::from_be_bytes(self.bytes()?))
510 }
511 fn u32(&mut self) -> Result<u32, WireError> {
512 Ok(u32::from_be_bytes(self.bytes()?))
513 }
514 fn digest(&mut self) -> Result<Digest, WireError> {
515 Ok(Digest::from_bytes(self.bytes()?))
516 }
517 fn prefix(&mut self) -> Result<Prefix, WireError> {
518 let value = Prefix {
519 op: OpNumber(self.u64()?),
520 digest: self.digest()?,
521 };
522 validate_prefix(value)?;
523 Ok(value)
524 }
525 fn routing_scope(&mut self, sender: NodeId) -> Result<Scope, WireError> {
526 let group_id = GroupId::from_bytes(self.bytes()?);
527 let configuration_epoch = self.u64()?;
528 let view = self.u64()?;
529 let voter = NodeId::from_bytes(self.bytes()?);
530 let configuration_digest = self.digest()?;
531 if voter != sender {
532 return Err(WireError::Peer);
533 }
534 let scope = Scope {
535 group_id,
536 configuration_epoch,
537 configuration_digest,
538 view,
539 };
540 validate_scope(scope)?;
541 if configuration_epoch == 0 {
542 return Err(WireError::Scope);
543 }
544 Ok(scope)
545 }
546
547 fn scope(&mut self, binding: PeerBinding) -> Result<Scope, WireError> {
548 let scope = self.routing_scope(binding.peer)?;
549 if scope
550 != (Scope {
551 view: scope.view,
552 ..binding.configuration.scope()
553 })
554 {
555 return Err(WireError::Scope);
556 }
557 Ok(scope)
558 }
559 fn finish(self) -> Result<(), WireError> {
560 if self.remaining.is_empty() {
561 Ok(())
562 } else {
563 Err(WireError::Length)
564 }
565 }
566}
567
568#[derive(Debug)]
569struct Writer<'a> {
570 output: &'a mut [u8],
571 offset: usize,
572}
573
574impl<'a> Writer<'a> {
575 fn new(output: &'a mut [u8]) -> Self {
576 Self { output, offset: 0 }
577 }
578 fn bytes(&mut self, bytes: &[u8]) {
579 let end = self.offset + bytes.len();
580 self.output[self.offset..end].copy_from_slice(bytes);
581 self.offset = end;
582 }
583 fn u64(&mut self, value: u64) {
584 self.bytes(&value.to_be_bytes());
585 }
586 fn prefix(&mut self, prefix: Prefix) {
587 self.u64(prefix.op.0);
588 self.bytes(prefix.digest.as_bytes());
589 }
590 fn scope(&mut self, scope: Scope, voter: NodeId) {
591 self.bytes(scope.group_id.as_bytes());
592 self.u64(scope.configuration_epoch);
593 self.u64(scope.view);
594 self.bytes(voter.as_bytes());
595 self.bytes(scope.configuration_digest.as_bytes());
596 }
597}
598
599#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
601pub enum WireError {
602 #[error(transparent)]
604 Flow(#[from] FlowError),
605 #[error("replica history response does not match the outstanding request")]
607 Transfer,
608 #[error("invalid replica election history descriptor")]
610 History,
611 #[error("replica command exceeds operation or byte limits")]
613 Limit,
614 #[error("unsupported replica operation kind {0}")]
616 OperationKind(u16),
617 #[error("replica canonical operation chain mismatch")]
619 Chain,
620 #[error("replica canonical operation digest mismatch")]
622 Digest,
623 #[error("replica ACK does not carry the configured evidence")]
625 Evidence,
626 #[error(transparent)]
628 Envelope(#[from] EnvelopeError),
629 #[error(transparent)]
631 Configuration(#[from] ReplicationError),
632 #[error("replica peer or session mismatch")]
634 Peer,
635 #[error("replica configuration scope mismatch")]
637 Scope,
638 #[error("invalid replica log prefix")]
640 Prefix,
641 #[error("unexpected replica payload")]
643 Payload,
644 #[error("replica metadata length mismatch")]
646 Length,
647 #[error("replica encoding buffer too small")]
649 Capacity,
650 #[error("invalid replica command correlation")]
652 Correlation,
653 #[error("unsupported replica command")]
655 UnsupportedCommand,
656}