1#![cfg_attr(not(test), warn(clippy::arithmetic_side_effects))]
8
9mod admission;
10mod election;
11pub use admission::ReceiveFence;
12mod flow;
13pub use flow::flow_state_route;
14mod compact;
15pub use compact::{COMPACT_STATE_BYTES, CompactState};
16mod prepare;
17mod publication;
18pub use publication::{
19 PUBLICATION_BYTES, PUBLICATION_TOPIC_BYTES, decode_publication, encode_publication,
20 publication_topic,
21};
22mod checkpoint;
23mod recovery;
24pub use checkpoint::{
25 CheckpointMessage, CheckpointRequest, HistoryFence, HistoryRetired, encode_checkpoint,
26 encode_history_retired,
27};
28mod fields;
29mod transfer;
30
31pub use flow::{FlowMessage, FlowProbe, FlowState, encode_flow_probe, encode_flow_state};
32pub use prepare::{
33 Operation, Operations, Prepare, PrepareBatch, PrepareEncoding, PrepareMetadata,
34 VerifiedOperation, encode_flow_prepare, encode_flow_prepare_metadata, encode_prepare,
35 encode_prepare_metadata, encode_prepare_unbound,
36};
37pub use recovery::{
38 RecoveryMessage, RecoveryRequest, RecoveryState, encode_recovery, encode_recovery_state,
39};
40pub use transfer::{FetchOps, OpsBatch, encode_fetch, encode_ops};
41
42use fields::{ReadFields, WriteFields, validate_election_view, validate_prefix, validate_scope};
43use ozzy_proto::layout::{LayoutError, Reader, Writer};
44use ozzy_proto::{Envelope, EnvelopeError, EnvelopeLimits, Opcode};
45use ozzy_proto::{LinkSessionId, NodeId};
46
47use crate::flow::{FlowError, ReceiveEpoch};
48use crate::{
49 Commit, Configuration, DoViewChange, Prefix, PrepareOk, ReplicationError, Scope, StartView,
50 StartViewChange,
51};
52
53const COMMON_BYTES: usize = 80;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct PeerBinding {
61 configuration: Configuration,
62 peer: NodeId,
63 session: LinkSessionId,
64 receive_epoch: Option<ReceiveEpoch>,
65}
66
67impl PeerBinding {
68 pub fn new(
70 configuration: Configuration,
71 peer: NodeId,
72 session: LinkSessionId,
73 ) -> Result<Self, WireError> {
74 configuration.voter_index(peer)?;
75 if session.as_bytes() == &[0; 16] {
76 return Err(WireError::Peer);
77 }
78 Ok(Self {
79 configuration,
80 peer,
81 session,
82 receive_epoch: None,
83 })
84 }
85
86 #[must_use]
92 pub const fn with_receive_epoch(mut self, epoch: ReceiveEpoch) -> Self {
93 self.receive_epoch = Some(epoch);
94 self
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct EncodeContext {
101 pub sender: NodeId,
103 pub session: LinkSessionId,
105 pub limits: WireLimits,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct WireLimits {
112 pub envelope: EnvelopeLimits,
114 pub max_operations: usize,
116}
117
118impl WireLimits {
119 pub fn for_transfer(max_operations: usize, max_body_bytes: usize) -> Option<Self> {
122 let fixed = transfer::FETCH_BYTES
123 .max(transfer::OPS_FIXED_BYTES)
124 .max(prepare::BATCH_BYTES + 16)
125 .next_multiple_of(16);
126 Some(Self {
127 envelope: EnvelopeLimits {
128 max_metadata_bytes: max_operations
129 .checked_mul(prepare::DESCRIPTOR_BYTES)?
130 .checked_add(fixed)?,
131 max_payload_bytes: max_body_bytes,
132 },
133 max_operations,
134 })
135 }
136
137 pub fn message_bytes(self) -> Option<usize> {
139 self.envelope
140 .max_metadata_bytes
141 .checked_add(self.envelope.max_payload_bytes)?
142 .checked_add(ozzy_proto::ENVELOPE_BYTES + 16)
143 }
144}
145
146impl Default for WireLimits {
147 fn default() -> Self {
148 Self {
149 envelope: EnvelopeLimits::default(),
150 max_operations: 64,
151 }
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum Control {
158 PrepareOk {
160 ack: PrepareOk,
162 },
163 Commit(Commit),
165 ExitView(Scope),
167 StartViewChange(StartViewChange),
169 DoViewChange(DoViewChange),
171 StartView(StartView),
173}
174
175impl Control {
176 pub const fn acknowledged(self) -> Option<Prefix> {
178 match self {
179 Self::PrepareOk { ack, .. } => Some(ack.durable),
180 _ => None,
181 }
182 }
183
184 pub const fn scope(self) -> Scope {
186 match self {
187 Self::PrepareOk { ack, .. } => ack.scope,
188 Self::Commit(message) => message.scope,
189 Self::ExitView(scope) => scope,
190 Self::StartViewChange(message) => message.scope,
191 Self::DoViewChange(message) => message.scope,
192 Self::StartView(message) => message.scope,
193 }
194 }
195
196 fn opcode(self) -> Opcode {
197 match self {
198 Self::PrepareOk { .. } => Opcode::PrepareOk,
199 Self::Commit(_) => Opcode::Commit,
200 Self::ExitView(_) => Opcode::ExitView,
201 Self::StartViewChange(_) => Opcode::StartViewChange,
202 Self::DoViewChange(_) => Opcode::DoViewChange,
203 Self::StartView(_) => Opcode::StartView,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum ReplicaMessage<'a> {
211 HistoryRetired(HistoryRetired),
213 Checkpoint(CheckpointMessage<'a>),
215 Recovery(RecoveryMessage),
217 Flow(FlowMessage<'a>),
219 Control(Control),
221 Prepare(PrepareBatch<'a>),
223 FetchOps(FetchOps),
225 Ops(OpsBatch<'a>),
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub struct ControlEncoding {
232 pub header: [u8; 64],
234 pub metadata_bytes: usize,
236}
237
238pub fn encode_control(
243 sender: NodeId,
244 session: LinkSessionId,
245 message: Control,
246 output: &mut [u8],
247) -> Result<ControlEncoding, WireError> {
248 let scope = message.scope();
249 validate_scope(scope)?;
250 let metadata_bytes = match message {
251 Control::PrepareOk { ack, .. } => {
252 validate_prefix(ack.durable)?;
253 COMMON_BYTES + 40
254 }
255 Control::Commit(message) => {
256 validate_prefix(message.committed)?;
257 COMMON_BYTES + 40
258 }
259 Control::StartViewChange(message) => {
260 validate_election_view(message.scope)?;
261 COMMON_BYTES
262 }
263 Control::ExitView(_) => COMMON_BYTES,
264 Control::DoViewChange(message) => {
265 election::validate_report(message)?;
266 COMMON_BYTES + 16 + 8 + 40 + 40
267 }
268 Control::StartView(message) => {
269 election::validate_start(message)?;
270 COMMON_BYTES + 16 + 40 + 40
271 }
272 };
273 if output.len() < metadata_bytes {
274 return Err(WireError::Capacity);
275 }
276 let header = Envelope {
277 opcode: message.opcode(),
278 response: false,
279 request_id: None,
280 sender,
281 session: Some(session),
282 }
283 .encode_header(metadata_bytes, 0, EnvelopeLimits::default())?;
284 let mut writer = Writer::with_len(output, metadata_bytes)?;
285 writer.scope(scope, sender)?;
286 match message {
287 Control::PrepareOk { ack } => writer.prefix(ack.durable)?,
288 Control::Commit(message) => writer.prefix(message.committed)?,
289 Control::StartViewChange(_) | Control::ExitView(_) => {}
290 Control::DoViewChange(message) => election::write_report(&mut writer, message)?,
291 Control::StartView(message) => election::write_start(&mut writer, message)?,
292 }
293 Ok(ControlEncoding {
294 header,
295 metadata_bytes,
296 })
297}
298
299pub fn route(packet: ozzy_proto::Packet<'_>, limits: EnvelopeLimits) -> Result<Scope, WireError> {
304 packet
305 .envelope
306 .validate_frames(packet.metadata.len(), packet.payload.len(), limits)?;
307 if !matches!(
308 packet.envelope.opcode,
309 Opcode::ReplicaOpen
310 | Opcode::ReplicaState
311 | Opcode::Prepare
312 | Opcode::PrepareFlow
313 | Opcode::PreparePub
314 | Opcode::PrepareOk
315 | Opcode::Commit
316 | Opcode::ExitView
317 | Opcode::StartViewChange
318 | Opcode::DoViewChange
319 | Opcode::StartView
320 | Opcode::Recovery
321 | Opcode::RecoveryState
322 | Opcode::FetchOps
323 | Opcode::Ops
324 | Opcode::HistoryRetired
325 | Opcode::SnapshotBegin
326 | Opcode::SnapshotChunk
327 ) {
328 return Err(WireError::UnsupportedCommand);
329 }
330 if !matches!(
331 packet.envelope.opcode,
332 Opcode::Prepare
333 | Opcode::PrepareFlow
334 | Opcode::PreparePub
335 | Opcode::Ops
336 | Opcode::SnapshotChunk
337 ) && !packet.payload.is_empty()
338 {
339 return Err(WireError::Payload);
340 }
341 Reader::new(packet.metadata).routing_scope(packet.envelope.sender)
342}
343
344pub fn decode<'a>(
349 frames: &[&'a [u8]],
350 binding: PeerBinding,
351 limits: WireLimits,
352) -> Result<ReplicaMessage<'a>, WireError> {
353 let packet = ozzy_proto::decode_packet(frames, limits.envelope)?;
354 if packet.envelope.sender != binding.peer || packet.envelope.session != Some(binding.session) {
355 return Err(WireError::Peer);
356 }
357 let mut reader = Reader::new(packet.metadata);
358 let incoming = Incoming {
359 scope: reader.scope(binding)?,
360 envelope: packet.envelope,
361 reader,
362 payload: packet.payload,
363 };
364 match packet.envelope.opcode {
365 Opcode::HistoryRetired => {
366 checkpoint::decode_retired(incoming).map(ReplicaMessage::HistoryRetired)
367 }
368 Opcode::SnapshotBegin | Opcode::SnapshotChunk => {
369 checkpoint::decode(incoming, binding, limits).map(ReplicaMessage::Checkpoint)
370 }
371 Opcode::Recovery => recovery::decode_request(incoming)
372 .map(|request| ReplicaMessage::Recovery(RecoveryMessage::Request(request))),
373 Opcode::RecoveryState => recovery::decode_state(incoming, binding)
374 .map(|state| ReplicaMessage::Recovery(RecoveryMessage::State(state))),
375 Opcode::ReplicaOpen => flow::decode_probe(incoming)
376 .map(|probe| ReplicaMessage::Flow(FlowMessage::Probe(probe))),
377 Opcode::ReplicaState => flow::decode_state(incoming)
378 .map(|state| ReplicaMessage::Flow(FlowMessage::State(state))),
379 Opcode::FetchOps => transfer::decode_fetch(incoming, binding).map(ReplicaMessage::FetchOps),
380 Opcode::Ops => transfer::decode_ops(incoming, limits).map(ReplicaMessage::Ops),
381 _ => decode_uncorrelated(incoming, binding, limits),
382 }
383}
384
385struct Incoming<'a> {
387 scope: Scope,
388 envelope: Envelope,
389 reader: Reader<'a>,
390 payload: &'a [u8],
391}
392
393fn decode_uncorrelated(
395 Incoming {
396 scope,
397 envelope,
398 mut reader,
399 payload,
400 }: Incoming<'_>,
401 binding: PeerBinding,
402 limits: WireLimits,
403) -> Result<ReplicaMessage<'_>, WireError> {
404 if envelope.response || envelope.request_id.is_some() {
405 return Err(WireError::Correlation);
406 }
407 match envelope.opcode {
408 Opcode::PrepareFlow => {
409 let epoch = ReceiveEpoch::new(u128::from_be_bytes(reader.array()?))?;
410 if binding.receive_epoch != Some(epoch) {
411 return Err(FlowError::Channel.into());
412 }
413 prepare::decode_prepare(scope, reader, payload, limits)
414 .map(|batch| ReplicaMessage::Flow(FlowMessage::Prepare { epoch, batch }))
415 }
416 Opcode::Prepare => {
417 if binding.receive_epoch.is_some() {
418 return Err(FlowError::Channel.into());
419 }
420 prepare::decode_prepare(scope, reader, payload, limits).map(ReplicaMessage::Prepare)
421 }
422 _ if !payload.is_empty() => Err(WireError::Payload),
423 opcode => decode_control(scope, opcode, reader).map(ReplicaMessage::Control),
424 }
425}
426
427fn decode_control(
428 scope: Scope,
429 opcode: Opcode,
430 mut reader: Reader<'_>,
431) -> Result<Control, WireError> {
432 let message = match opcode {
433 Opcode::PrepareOk => Control::PrepareOk {
434 ack: PrepareOk {
435 scope,
436 durable: reader.prefix()?,
437 },
438 },
439 Opcode::Commit => Control::Commit(Commit {
440 scope,
441 committed: reader.prefix()?,
442 }),
443 Opcode::StartViewChange => {
444 validate_election_view(scope)?;
445 Control::StartViewChange(StartViewChange { scope })
446 }
447 Opcode::ExitView => Control::ExitView(scope),
448 Opcode::DoViewChange => Control::DoViewChange(election::read_report(scope, &mut reader)?),
449 Opcode::StartView => Control::StartView(election::read_start(scope, &mut reader)?),
450 _ => return Err(WireError::UnsupportedCommand),
451 };
452 reader.finish()?;
453 Ok(message)
454}
455
456#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
458pub enum WireError {
459 #[error(transparent)]
461 Flow(#[from] FlowError),
462 #[error("replica history response does not match the outstanding request")]
464 Transfer,
465 #[error("invalid replica election history descriptor")]
467 History,
468 #[error("replica command exceeds operation or byte limits")]
470 Limit,
471 #[error("unsupported replica operation kind {0}")]
473 OperationKind(u16),
474 #[error("replica canonical operation chain mismatch")]
476 Chain,
477 #[error("replica canonical operation digest mismatch")]
479 Digest,
480 #[error(transparent)]
482 Envelope(#[from] EnvelopeError),
483 #[error(transparent)]
485 Configuration(#[from] ReplicationError),
486 #[error("replica peer or session mismatch")]
488 Peer,
489 #[error("replica configuration scope mismatch")]
491 Scope,
492 #[error("invalid replica log prefix")]
494 Prefix,
495 #[error("unexpected replica payload")]
497 Payload,
498 #[error("replica metadata length mismatch")]
500 Length,
501 #[error("replica encoding buffer too small")]
503 Capacity,
504 #[error("invalid replica command correlation")]
506 Correlation,
507 #[error("unsupported replica command")]
509 UnsupportedCommand,
510}
511
512impl From<LayoutError> for WireError {
513 fn from(error: LayoutError) -> Self {
514 match error {
515 LayoutError::Truncated { .. } | LayoutError::Trailing(_) => Self::Length,
516 LayoutError::Overflow { .. } => Self::Capacity,
517 }
518 }
519}