Skip to main content

ozzy_replication/
wire.rs

1//! Bounded native replica messages. No I/O or implicit consensus authority.
2//!
3//! The transport supplies an independently authenticated voter/session binding.
4//! Scope decoding checks the immutable configuration; the production cores still
5//! decide whether the message's view, history, role, and evidence are admissible.
6
7mod 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/// Authenticated peer and established session supplied by the transport adapter.
51///
52/// Construction checks membership and nonzero IDs, not cryptographic identity.
53/// Never construct this binding from untrusted packet fields or routing IDs alone.
54#[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    /// Bind one independently authenticated configured voter to its live session.
64    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    /// Require epoch-bound normal payloads bound to this locally issued receive epoch.
82    ///
83    /// Set only from the receiver's own current flow state, never incoming fields.
84    /// Rejects legacy PREPARE and stale `PREPARE_FLOW` before decoding/hashing bodies.
85    /// Control and installation-history messages retain their independent rules.
86    #[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/// Directional receive and command work bounds.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct WireLimits {
96    /// Maximum actual metadata and payload frame sizes.
97    pub envelope: EnvelopeLimits,
98    /// Maximum canonical operations in a prepare/history response, independent of records.
99    pub max_operations: usize,
100}
101
102impl WireLimits {
103    /// Conservative shared prepare/history profile. Round the largest fixed
104    /// metadata span up to 16 bytes, preserving room for control exchanges.
105    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    /// Bound a routed native message, including its 16-byte destination identity.
122    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/// Fixed-size replica control metadata. Receipt alone has no authority.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum Control {
142    /// Cumulative durable backup evidence plus its current session fence.
143    PrepareOk {
144        /// Matching contiguous prefix after successful disk synchronization.
145        ack: PrepareOk,
146    },
147    /// Cumulative retained-memory evidence with independent background persistence.
148    PrepareRetained {
149        /// Validated bytes retained until application and persistence complete.
150        ack: RetainedPrepareOk,
151    },
152    /// Primary's cumulative commit announcement or idle heartbeat.
153    Commit(Commit),
154    /// Volatile suspicion about this current view, not a durable promise or vote.
155    ExitView(Scope),
156    /// First election phase, emitted after the sender's durable view promise.
157    StartViewChange(StartViewChange),
158    /// Frozen full-WAL descriptor for the candidate's second-phase quorum.
159    DoViewChange(DoViewChange),
160    /// Primary's durably installed selected lineage, not a backup installation.
161    StartView(StartView),
162}
163
164impl Control {
165    /// Claimed prefix for routing/replay hints. This does not validate its evidence.
166    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    /// Exact group, immutable configuration digest, and active/proposed view.
175    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/// Decoded replica command, still subject to role/history checks in the core.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum ReplicaMessage<'a> {
202    /// Receiver-fenced hint to enter nonvoting recovery, never new authority.
203    HistoryRetired(HistoryRetired),
204    /// Checkpoint state bytes for a separately authorized nonvoting recovery.
205    Checkpoint(CheckpointMessage<'a>),
206    /// Fresh lost-state evidence, neither receipt credit nor a normal/election vote.
207    Recovery(RecoveryMessage),
208    /// Receipt/credit exchange or epoch-bound data; never durable quorum evidence.
209    Flow(FlowMessage<'a>),
210    /// Fixed-size quorum/control evidence from the bound sender.
211    Control(Control),
212    /// Hash-validated canonical bytes, still requiring application validation.
213    Prepare(PrepareBatch<'a>),
214    /// Correlated bounded request for an independently pinned source lineage.
215    FetchOps(FetchOps),
216    /// Hash-validated response bytes, not a normal prepare or commit announcement.
217    Ops(OpsBatch<'a>),
218}
219
220/// Envelope and valid prefix length in the caller's reusable metadata buffer.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub struct ControlEncoding {
223    /// First Ozzy frame, with exact lengths for this encoding.
224    pub header: [u8; 64],
225    /// Second frame length; the third frame must be present and empty.
226    pub metadata_bytes: usize,
227}
228
229/// Encode control metadata into caller-owned storage without allocating.
230///
231/// Invalid fields or insufficient capacity leave the output unchanged. Callers
232/// may send only messages actually authorized by the current consensus role.
233pub 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
301/// Read only the fixed group/configuration prefix of an implemented PEER
302/// command. This does not authenticate the sender or validate membership, view,
303/// operation hashes, or payloads. Dispatch uses it to select an actor, which must
304/// still call `decode` with an independently established `PeerBinding`.
305pub 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
346/// Decode a bounded packet and reject mismatched voter/session/configuration.
347///
348/// This does not validate a claim against local disk state or count a quorum.
349/// Old views remain recognizable so the consensus core can apply its fencing rules.
350pub 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/// Structural wire failure. No rejection changes replica or journal state.
600#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
601pub enum WireError {
602    /// Invalid receipt metadata or mismatched local receive incarnation.
603    #[error(transparent)]
604    Flow(#[from] FlowError),
605    /// Response does not match the live source, request, range, or response bound.
606    #[error("replica history response does not match the outstanding request")]
607    Transfer,
608    /// Invalid election view, frozen generation, or accepted/commit relationship.
609    #[error("invalid replica election history descriptor")]
610    History,
611    /// A batch/request is empty, too large, or exceeds an operation/frame work bound.
612    #[error("replica command exceeds operation or byte limits")]
613    Limit,
614    /// Unsupported canonical operation kind.
615    #[error("unsupported replica operation kind {0}")]
616    OperationKind(u16),
617    /// Canonical envelope does not continue the announced scope/hash chain.
618    #[error("replica canonical operation chain mismatch")]
619    Chain,
620    /// Body or envelope digest does not match the received canonical bytes.
621    #[error("replica canonical operation digest mismatch")]
622    Digest,
623    /// The ACK's evidence kind does not match the selected RAM/disk codec.
624    #[error("replica ACK does not carry the configured evidence")]
625    Evidence,
626    /// Invalid native envelope or directional frame bound.
627    #[error(transparent)]
628    Envelope(#[from] EnvelopeError),
629    /// Invalid configured voter supplied to the binding.
630    #[error(transparent)]
631    Configuration(#[from] ReplicationError),
632    /// Sender, embedded voter, or session does not match authenticated binding.
633    #[error("replica peer or session mismatch")]
634    Peer,
635    /// Group/configuration epoch/digest does not match the configured group.
636    #[error("replica configuration scope mismatch")]
637    Scope,
638    /// Invalid genesis/digest pairing or unrepresentable successor operation.
639    #[error("invalid replica log prefix")]
640    Prefix,
641    /// A control command cannot carry payload bytes.
642    #[error("unexpected replica payload")]
643    Payload,
644    /// Truncated fields or extra metadata.
645    #[error("replica metadata length mismatch")]
646    Length,
647    /// Output storage cannot hold the complete encoded message.
648    #[error("replica encoding buffer too small")]
649    Capacity,
650    /// Command correlation/response flag does not match its exchange family.
651    #[error("invalid replica command correlation")]
652    Correlation,
653    /// Opcode does not have an implemented replica codec.
654    #[error("unsupported replica command")]
655    UnsupportedCommand,
656}