mod admission;
mod election;
pub use admission::ReceiveFence;
mod flow;
pub use flow::flow_state_route;
mod compact;
pub use compact::{COMPACT_STATE_BYTES, CompactState};
mod prepare;
mod publication;
pub use publication::{
PUBLICATION_BYTES, PUBLICATION_TOPIC_BYTES, PublicationPart, decode_publication,
encode_publication, encode_publication_group, publication_topic,
};
mod checkpoint;
mod recovery;
pub use checkpoint::{
CheckpointMessage, CheckpointRequest, HistoryFence, HistoryRetired, encode_checkpoint,
encode_history_retired,
};
mod transfer;
pub use flow::{FlowMessage, FlowProbe, FlowState, encode_flow_probe, encode_flow_state};
pub use prepare::{
Operation, Operations, Prepare, PrepareBatch, PrepareEncoding, PrepareMetadata,
VerifiedOperation, encode_flow_prepare, encode_flow_prepare_metadata, encode_prepare,
encode_prepare_metadata, encode_prepare_unbound,
};
pub use recovery::{
RecoveryMessage, RecoveryRequest, RecoveryState, encode_recovery, encode_recovery_state,
};
pub use transfer::{FetchOps, OpsBatch, encode_fetch, encode_ops};
use ozzy_proto::{Envelope, EnvelopeError, EnvelopeLimits, Opcode};
use ozzy_proto::{GroupId, LinkSessionId, NodeId};
use crate::flow::{FlowError, ReceiveEpoch};
use crate::{
Commit, Configuration, Digest, DoViewChange, OpNumber, Prefix, PrepareOk, QuorumPolicy,
ReplicationError, RetainedPrepareOk, Scope, StartView, StartViewChange,
};
const COMMON_BYTES: usize = 80;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PeerBinding {
configuration: Configuration,
peer: NodeId,
session: LinkSessionId,
receive_epoch: Option<ReceiveEpoch>,
}
impl PeerBinding {
pub fn new(
configuration: Configuration,
peer: NodeId,
session: LinkSessionId,
) -> Result<Self, WireError> {
configuration.voter_index(peer)?;
if session.as_bytes() == &[0; 16] {
return Err(WireError::Peer);
}
Ok(Self {
configuration,
peer,
session,
receive_epoch: None,
})
}
#[must_use]
pub const fn with_receive_epoch(mut self, epoch: ReceiveEpoch) -> Self {
self.receive_epoch = Some(epoch);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WireLimits {
pub envelope: EnvelopeLimits,
pub max_operations: usize,
}
impl WireLimits {
pub fn for_transfer(max_operations: usize, max_body_bytes: usize) -> Option<Self> {
let fixed = transfer::FETCH_BYTES
.max(transfer::OPS_FIXED_BYTES)
.max(prepare::BATCH_BYTES + 16)
.next_multiple_of(16);
Some(Self {
envelope: EnvelopeLimits {
max_metadata_bytes: max_operations
.checked_mul(prepare::DESCRIPTOR_BYTES)?
.checked_add(fixed)?,
max_payload_bytes: max_body_bytes,
},
max_operations,
})
}
pub fn message_bytes(self) -> Option<usize> {
self.envelope
.max_metadata_bytes
.checked_add(self.envelope.max_payload_bytes)?
.checked_add(ozzy_proto::ENVELOPE_BYTES + 16)
}
}
impl Default for WireLimits {
fn default() -> Self {
Self {
envelope: EnvelopeLimits::default(),
max_operations: 64,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Control {
PrepareOk {
ack: PrepareOk,
},
PrepareRetained {
ack: RetainedPrepareOk,
},
Commit(Commit),
ExitView(Scope),
StartViewChange(StartViewChange),
DoViewChange(DoViewChange),
StartView(StartView),
}
impl Control {
pub const fn acknowledged(self) -> Option<Prefix> {
match self {
Self::PrepareOk { ack, .. } => Some(ack.durable),
Self::PrepareRetained { ack, .. } => Some(ack.retained),
_ => None,
}
}
pub const fn scope(self) -> Scope {
match self {
Self::PrepareOk { ack, .. } => ack.scope,
Self::PrepareRetained { ack, .. } => ack.scope,
Self::Commit(message) => message.scope,
Self::ExitView(scope) => scope,
Self::StartViewChange(message) => message.scope,
Self::DoViewChange(message) => message.scope,
Self::StartView(message) => message.scope,
}
}
fn opcode(self) -> Opcode {
match self {
Self::PrepareOk { .. } | Self::PrepareRetained { .. } => Opcode::PrepareOk,
Self::Commit(_) => Opcode::Commit,
Self::ExitView(_) => Opcode::ExitView,
Self::StartViewChange(_) => Opcode::StartViewChange,
Self::DoViewChange(_) => Opcode::DoViewChange,
Self::StartView(_) => Opcode::StartView,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplicaMessage<'a> {
HistoryRetired(HistoryRetired),
Checkpoint(CheckpointMessage<'a>),
Recovery(RecoveryMessage),
Flow(FlowMessage<'a>),
Control(Control),
Prepare(PrepareBatch<'a>),
FetchOps(FetchOps),
Ops(OpsBatch<'a>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ControlEncoding {
pub header: [u8; 64],
pub metadata_bytes: usize,
}
pub fn encode_control(
sender: NodeId,
session: LinkSessionId,
message: Control,
output: &mut [u8],
) -> Result<ControlEncoding, WireError> {
let scope = message.scope();
validate_scope(scope)?;
let metadata_bytes = match message {
Control::PrepareOk { ack, .. } => {
validate_prefix(ack.durable)?;
COMMON_BYTES + 40 + 1
}
Control::PrepareRetained { ack, .. } => {
validate_prefix(ack.retained)?;
COMMON_BYTES + 40 + 1
}
Control::Commit(message) => {
validate_prefix(message.committed)?;
COMMON_BYTES + 40
}
Control::StartViewChange(message) => {
validate_election_view(message.scope)?;
COMMON_BYTES
}
Control::ExitView(_) => COMMON_BYTES,
Control::DoViewChange(message) => {
election::validate_report(message)?;
COMMON_BYTES + 16 + 8 + 40 + 40
}
Control::StartView(message) => {
election::validate_start(message)?;
COMMON_BYTES + 16 + 40 + 40
}
};
if output.len() < metadata_bytes {
return Err(WireError::Capacity);
}
let header = Envelope {
opcode: message.opcode(),
response: false,
request_id: None,
sender,
session: Some(session),
}
.encode_header(metadata_bytes, 0, EnvelopeLimits::default())?;
let mut writer = Writer::new(&mut output[..metadata_bytes]);
writer.scope(scope, sender);
match message {
Control::PrepareOk { ack } => {
writer.prefix(ack.durable);
writer.bytes(&[2]);
}
Control::PrepareRetained { ack } => {
writer.prefix(ack.retained);
writer.bytes(&[3]);
}
Control::Commit(message) => writer.prefix(message.committed),
Control::StartViewChange(_) | Control::ExitView(_) => {}
Control::DoViewChange(message) => election::write_report(&mut writer, message),
Control::StartView(message) => election::write_start(&mut writer, message),
}
Ok(ControlEncoding {
header,
metadata_bytes,
})
}
pub fn route(packet: ozzy_proto::Packet<'_>, limits: EnvelopeLimits) -> Result<Scope, WireError> {
packet
.envelope
.validate_frames(packet.metadata.len(), packet.payload.len(), limits)?;
if !matches!(
packet.envelope.opcode,
Opcode::ReplicaOpen
| Opcode::ReplicaState
| Opcode::Prepare
| Opcode::PrepareFlow
| Opcode::PreparePub
| Opcode::PrepareOk
| Opcode::Commit
| Opcode::ExitView
| Opcode::StartViewChange
| Opcode::DoViewChange
| Opcode::StartView
| Opcode::Recovery
| Opcode::RecoveryState
| Opcode::FetchOps
| Opcode::Ops
| Opcode::HistoryRetired
| Opcode::SnapshotBegin
| Opcode::SnapshotChunk
) {
return Err(WireError::UnsupportedCommand);
}
if !matches!(
packet.envelope.opcode,
Opcode::Prepare
| Opcode::PrepareFlow
| Opcode::PreparePub
| Opcode::Ops
| Opcode::SnapshotChunk
) && !packet.payload.is_empty()
{
return Err(WireError::Payload);
}
Reader::new(packet.metadata).routing_scope(packet.envelope.sender)
}
pub fn decode<'a>(
frames: &[&'a [u8]],
binding: PeerBinding,
limits: WireLimits,
) -> Result<ReplicaMessage<'a>, WireError> {
let packet = ozzy_proto::decode_packet(frames, limits.envelope)?;
if packet.envelope.sender != binding.peer || packet.envelope.session != Some(binding.session) {
return Err(WireError::Peer);
}
let mut reader = Reader::new(packet.metadata);
let scope = reader.scope(binding)?;
match packet.envelope.opcode {
Opcode::HistoryRetired => {
return checkpoint::decode_retired(scope, packet.envelope, reader, packet.payload)
.map(ReplicaMessage::HistoryRetired);
}
Opcode::SnapshotBegin | Opcode::SnapshotChunk => {
return checkpoint::decode(
scope,
packet.envelope,
reader,
packet.payload,
binding,
limits,
)
.map(ReplicaMessage::Checkpoint);
}
Opcode::Recovery => {
return recovery::decode_request(scope, packet.envelope, reader, packet.payload)
.map(|request| ReplicaMessage::Recovery(RecoveryMessage::Request(request)));
}
Opcode::RecoveryState => {
return recovery::decode_state(scope, packet.envelope, reader, packet.payload, binding)
.map(|state| ReplicaMessage::Recovery(RecoveryMessage::State(state)));
}
Opcode::ReplicaOpen => {
return flow::decode_probe(scope, packet.envelope, reader, packet.payload)
.map(|probe| ReplicaMessage::Flow(FlowMessage::Probe(probe)));
}
Opcode::ReplicaState => {
return flow::decode_state(scope, packet.envelope, reader, packet.payload)
.map(|state| ReplicaMessage::Flow(FlowMessage::State(state)));
}
_ => {}
}
if packet.envelope.opcode == Opcode::FetchOps {
return transfer::decode_fetch(scope, packet.envelope, reader, packet.payload, binding)
.map(ReplicaMessage::FetchOps);
}
if packet.envelope.opcode == Opcode::Ops {
return transfer::decode_ops(scope, packet.envelope, reader, packet.payload, limits)
.map(ReplicaMessage::Ops);
}
if packet.envelope.response || packet.envelope.request_id.is_some() {
return Err(WireError::Correlation);
}
if packet.envelope.opcode == Opcode::PrepareFlow {
let epoch = ReceiveEpoch::new(u128::from_be_bytes(reader.bytes()?))?;
if binding.receive_epoch != Some(epoch) {
return Err(FlowError::Channel.into());
}
return prepare::decode_prepare(scope, reader, packet.payload, limits)
.map(|batch| ReplicaMessage::Flow(FlowMessage::Prepare { epoch, batch }));
}
if packet.envelope.opcode == Opcode::Prepare {
if binding.receive_epoch.is_some() {
return Err(FlowError::Channel.into());
}
return prepare::decode_prepare(scope, reader, packet.payload, limits)
.map(ReplicaMessage::Prepare);
}
if !packet.payload.is_empty() {
return Err(WireError::Payload);
}
decode_control(scope, packet.envelope.opcode, reader, binding).map(ReplicaMessage::Control)
}
fn decode_control(
scope: Scope,
opcode: Opcode,
mut reader: Reader<'_>,
binding: PeerBinding,
) -> Result<Control, WireError> {
let message = match opcode {
Opcode::PrepareOk => {
let prefix = reader.prefix()?;
let evidence = reader.bytes::<1>()?[0];
match (binding.configuration.policy(), evidence) {
(QuorumPolicy::Durable, 2) => Control::PrepareOk {
ack: PrepareOk {
scope,
durable: prefix,
},
},
(QuorumPolicy::Replicated, 3) => Control::PrepareRetained {
ack: RetainedPrepareOk {
scope,
retained: prefix,
},
},
_ => return Err(WireError::Evidence),
}
}
Opcode::Commit => Control::Commit(Commit {
scope,
committed: reader.prefix()?,
}),
Opcode::StartViewChange => {
validate_election_view(scope)?;
Control::StartViewChange(StartViewChange { scope })
}
Opcode::ExitView => Control::ExitView(scope),
Opcode::DoViewChange => Control::DoViewChange(election::read_report(scope, &mut reader)?),
Opcode::StartView => Control::StartView(election::read_start(scope, &mut reader)?),
_ => return Err(WireError::UnsupportedCommand),
};
reader.finish()?;
Ok(message)
}
fn validate_scope(scope: Scope) -> Result<(), WireError> {
if scope.group_id.as_bytes() == &[0; 16] || scope.configuration_digest == Digest::ZERO {
return Err(WireError::Scope);
}
Ok(())
}
fn validate_election_view(scope: Scope) -> Result<(), WireError> {
if scope.view == 0 {
return Err(WireError::History);
}
Ok(())
}
fn validate_prefix(prefix: Prefix) -> Result<(), WireError> {
if (prefix.op.0 == 0) != (prefix.digest == Digest::ZERO) || prefix.op.0 == u64::MAX {
return Err(WireError::Prefix);
}
Ok(())
}
#[derive(Debug)]
struct Reader<'a> {
remaining: &'a [u8],
}
impl<'a> Reader<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { remaining: bytes }
}
fn bytes<const N: usize>(&mut self) -> Result<[u8; N], WireError> {
let (bytes, rest) = self
.remaining
.split_at_checked(N)
.ok_or(WireError::Length)?;
self.remaining = rest;
Ok(bytes.try_into().expect("exact field"))
}
fn u64(&mut self) -> Result<u64, WireError> {
Ok(u64::from_be_bytes(self.bytes()?))
}
fn u32(&mut self) -> Result<u32, WireError> {
Ok(u32::from_be_bytes(self.bytes()?))
}
fn digest(&mut self) -> Result<Digest, WireError> {
Ok(Digest::from_bytes(self.bytes()?))
}
fn prefix(&mut self) -> Result<Prefix, WireError> {
let value = Prefix {
op: OpNumber(self.u64()?),
digest: self.digest()?,
};
validate_prefix(value)?;
Ok(value)
}
fn routing_scope(&mut self, sender: NodeId) -> Result<Scope, WireError> {
let group_id = GroupId::from_bytes(self.bytes()?);
let configuration_epoch = self.u64()?;
let view = self.u64()?;
let voter = NodeId::from_bytes(self.bytes()?);
let configuration_digest = self.digest()?;
if voter != sender {
return Err(WireError::Peer);
}
let scope = Scope {
group_id,
configuration_epoch,
configuration_digest,
view,
};
validate_scope(scope)?;
if configuration_epoch == 0 {
return Err(WireError::Scope);
}
Ok(scope)
}
fn scope(&mut self, binding: PeerBinding) -> Result<Scope, WireError> {
let scope = self.routing_scope(binding.peer)?;
if scope
!= (Scope {
view: scope.view,
..binding.configuration.scope()
})
{
return Err(WireError::Scope);
}
Ok(scope)
}
fn finish(self) -> Result<(), WireError> {
if self.remaining.is_empty() {
Ok(())
} else {
Err(WireError::Length)
}
}
}
#[derive(Debug)]
struct Writer<'a> {
output: &'a mut [u8],
offset: usize,
}
impl<'a> Writer<'a> {
fn new(output: &'a mut [u8]) -> Self {
Self { output, offset: 0 }
}
fn bytes(&mut self, bytes: &[u8]) {
let end = self.offset + bytes.len();
self.output[self.offset..end].copy_from_slice(bytes);
self.offset = end;
}
fn u64(&mut self, value: u64) {
self.bytes(&value.to_be_bytes());
}
fn prefix(&mut self, prefix: Prefix) {
self.u64(prefix.op.0);
self.bytes(prefix.digest.as_bytes());
}
fn scope(&mut self, scope: Scope, voter: NodeId) {
self.bytes(scope.group_id.as_bytes());
self.u64(scope.configuration_epoch);
self.u64(scope.view);
self.bytes(voter.as_bytes());
self.bytes(scope.configuration_digest.as_bytes());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum WireError {
#[error(transparent)]
Flow(#[from] FlowError),
#[error("replica history response does not match the outstanding request")]
Transfer,
#[error("invalid replica election history descriptor")]
History,
#[error("replica command exceeds operation or byte limits")]
Limit,
#[error("unsupported replica operation kind {0}")]
OperationKind(u16),
#[error("replica canonical operation chain mismatch")]
Chain,
#[error("replica canonical operation digest mismatch")]
Digest,
#[error("replica ACK does not carry the configured evidence")]
Evidence,
#[error(transparent)]
Envelope(#[from] EnvelopeError),
#[error(transparent)]
Configuration(#[from] ReplicationError),
#[error("replica peer or session mismatch")]
Peer,
#[error("replica configuration scope mismatch")]
Scope,
#[error("invalid replica log prefix")]
Prefix,
#[error("unexpected replica payload")]
Payload,
#[error("replica metadata length mismatch")]
Length,
#[error("replica encoding buffer too small")]
Capacity,
#[error("invalid replica command correlation")]
Correlation,
#[error("unsupported replica command")]
UnsupportedCommand,
}