You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by gk...@apache.org on 2026/08/11 08:33:40 UTC
(iggy) branch remove_namespace_field updated: address review comments
This is an automated email from the ASF dual-hosted git repository.
numinnex pushed a commit to branch remove_namespace_field
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/remove_namespace_field by this push:
new b1fea62dc address review comments
b1fea62dc is described below
commit b1fea62dc5205e469eba5e244c5997af2753db71
Author: Grzegorz Koszyk <g....@gmail.com>
AuthorDate: Tue Aug 11 10:33:34 2026 +0200
address review comments
---
core/binary_protocol/src/consensus/header.rs | 226 ++++++++++++++++-----------
core/binary_protocol/src/version.rs | 4 +-
core/consensus/src/client_table.rs | 7 +-
core/metadata/src/stm/snapshot.rs | 59 ++++++-
core/server-ng/src/dispatch.rs | 4 +-
core/server_common/src/consensus_message.rs | 104 +++++++++++-
6 files changed, 294 insertions(+), 110 deletions(-)
diff --git a/core/binary_protocol/src/consensus/header.rs b/core/binary_protocol/src/consensus/header.rs
index f52e38aba..4f8fffe95 100644
--- a/core/binary_protocol/src/consensus/header.rs
+++ b/core/binary_protocol/src/consensus/header.rs
@@ -315,9 +315,9 @@ const _: () = {
/// (it is derived: plane from `operation`, partition group from the body),
/// so this is where the derivation result lives for the internal hop.
///
-/// Layout: identical to [`RequestHeader`] with `group` claiming the first
-/// eight reserved bytes, so the in-place `transmute_header` rewrite stays a
-/// same-size copy.
+/// Layout: identical to [`RequestHeader`] with `group` claiming the LAST
+/// eight reserved bytes (the tail, bytes 248..256); the leading 52 reserved
+/// bytes keep their client-wire meaning, so promotion is a same-size copy.
#[repr(C)]
#[derive(Debug, Clone, Copy, CheckedBitPattern, NoUninit)]
pub struct RoutedRequestHeader {
@@ -368,7 +368,7 @@ impl Default for RoutedRequestHeader {
size: 0,
view: 0,
release: 0,
- command: Command2::Request,
+ command: Command2::Reserved,
replica: 0,
reserved_frame: [0; 66],
client: 0,
@@ -410,44 +410,64 @@ impl Default for RequestHeader {
}
}
-impl RoutedRequestHeader {
- /// Promote a client-wire [`RequestHeader`] to the server-internal routed
- /// shape, stamping the resolved consensus `group`. Every shared field is
- /// copied verbatim; this is the ONLY sanctioned crossing between the two
- /// layouts (transmute-based reads across them would alias `group` with
- /// reserved bytes).
- #[must_use]
- pub const fn from_request(header: &RequestHeader, group: u64) -> Self {
- Self {
- checksum: header.checksum,
- checksum_body: header.checksum_body,
- cluster: header.cluster,
- size: header.size,
- view: header.view,
- release: header.release,
- command: header.command,
- replica: header.replica,
- reserved_frame: header.reserved_frame,
- client: header.client,
- request_checksum: header.request_checksum,
- timestamp: header.timestamp,
- request: header.request,
- operation: header.operation,
- operation_padding: header.operation_padding,
- session: header.session,
- user_id: header.user_id,
- reserved: {
- let mut reserved = [0u8; 52];
- let mut i = 0;
- while i < 52 {
- reserved[i] = header.reserved[i];
- i += 1;
- }
- reserved
- },
- group,
+/// Field rules shared by the client-wire [`RequestHeader`] and the
+/// server-internal [`RoutedRequestHeader`]. The routed shape is decoded
+/// straight off the peer wire (`MessageBag`), so it must reject everything
+/// the client boundary rejects; validating only the command byte there
+/// would let a peer frame carry `client = 0` into the client table's hard
+/// assert, or `operation = Reserved` into a cached-register replay.
+fn validate_request_fields(
+ client: u128,
+ operation: Operation,
+ session: u64,
+ request: u64,
+) -> Result<(), ConsensusError> {
+ if client == 0 {
+ return Err(ConsensusError::InvalidField(
+ "request: client must be != 0".to_string(),
+ ));
+ }
+ // Reserved is the zero value, never a real client op
+ // (`is_client_allowed` rejects it). Refusing it here rather than after
+ // the dedup preflight matters: a bound client sending
+ // `Reserved, request = 0` used to pass validation, reach
+ // `request_preflight`, hit its own watermark and get its register
+ // reply replayed before the operation gate ever ran.
+ if operation == Operation::Reserved {
+ return Err(ConsensusError::InvalidField(
+ "operation must not be Reserved".to_string(),
+ ));
+ }
+ // Register: session must be 0, request must be 0.
+ // NonReplicated: sessionless by design (the `ClientTable` ignores
+ // these ops and the server routes/auth-gates them by transport id),
+ // so a pre-register client may legitimately send session 0 --
+ // ping must work before authentication.
+ // Other non-register ops: session must be > 0, request must be > 0.
+ if operation == Operation::Register {
+ if session != 0 {
+ return Err(ConsensusError::InvalidField(
+ "register: session must be 0".to_string(),
+ ));
+ }
+ if request != 0 {
+ return Err(ConsensusError::InvalidField(
+ "register: request must be 0".to_string(),
+ ));
+ }
+ } else if operation != Operation::NonReplicated {
+ if session == 0 {
+ return Err(ConsensusError::InvalidField(
+ "non-register: session must be > 0".to_string(),
+ ));
+ }
+ if request == 0 {
+ return Err(ConsensusError::InvalidField(
+ "non-register: request must be > 0".to_string(),
+ ));
}
}
+ Ok(())
}
impl ConsensusHeader for RoutedRequestHeader {
@@ -480,7 +500,7 @@ impl ConsensusHeader for RoutedRequestHeader {
found: self.command,
});
}
- Ok(())
+ validate_request_fields(self.client, self.operation, self.session, self.request)
}
}
@@ -512,52 +532,7 @@ impl ConsensusHeader for RequestHeader {
found: self.command,
});
}
- if self.client == 0 {
- return Err(ConsensusError::InvalidField(
- "request: client must be != 0".to_string(),
- ));
- }
- // Reserved is the zero value, never a real client op
- // (`is_client_allowed` rejects it). Refusing it here rather than after
- // the dedup preflight matters: a bound client sending
- // `Reserved, request = 0` used to pass validation, reach
- // `request_preflight`, hit its own watermark and get its register
- // reply replayed before the operation gate ever ran.
- if self.operation == Operation::Reserved {
- return Err(ConsensusError::InvalidField(
- "operation must not be Reserved".to_string(),
- ));
- }
- // Register: session must be 0, request must be 0.
- // NonReplicated: sessionless by design (the `ClientTable` ignores
- // these ops and the server routes/auth-gates them by transport id),
- // so a pre-register client may legitimately send session 0 --
- // ping must work before authentication.
- // Other non-register ops: session must be > 0, request must be > 0.
- if self.operation == Operation::Register {
- if self.session != 0 {
- return Err(ConsensusError::InvalidField(
- "register: session must be 0".to_string(),
- ));
- }
- if self.request != 0 {
- return Err(ConsensusError::InvalidField(
- "register: request must be 0".to_string(),
- ));
- }
- } else if self.operation != Operation::NonReplicated {
- if self.session == 0 {
- return Err(ConsensusError::InvalidField(
- "non-register: session must be > 0".to_string(),
- ));
- }
- if self.request == 0 {
- return Err(ConsensusError::InvalidField(
- "non-register: request must be > 0".to_string(),
- ));
- }
- }
- Ok(())
+ validate_request_fields(self.client, self.operation, self.session, self.request)
}
}
@@ -2268,8 +2243,8 @@ mod tests {
EvictionHeader, EvictionReason, GenericHeader, HEADER_SIZE, Operation, PrepareHeader,
PrepareOkHeader, RepairPrepareHeader, RepairRangeReplyHeader, ReplyHeader, RequestHeader,
RequestPreparesHeader, RequestStartViewHeader, RequestStateChunkHeader,
- RequestStateTransferHeader, StartViewChangeHeader, StartViewHeader, StateChunkHeader,
- StateTransferTargetHeader,
+ RequestStateTransferHeader, RoutedRequestHeader, StartViewChangeHeader, StartViewHeader,
+ StateChunkHeader, StateTransferTargetHeader,
};
use aligned_vec::{AVec, ConstAlign};
@@ -2533,17 +2508,88 @@ mod tests {
}
// `status` is carved from the reserved tail; the SDK reply funnel peeks it
- // at this offset before any body decode, so a layout drift must trip here.
+ // at this offset before any body decode, and four foreign SDKs hardcode
+ // it, so a layout drift must trip here.
#[test]
fn reply_header_status_offset_and_size_pinned() {
use std::mem::offset_of;
assert_eq!(size_of::<ReplyHeader>(), HEADER_SIZE);
+ assert_eq!(offset_of!(ReplyHeader, status), 216);
assert_eq!(
offset_of!(ReplyHeader, reserved) + size_of::<[u8; 36]>(),
HEADER_SIZE
);
}
+ // `group` claims the TAIL of the client header's reserved area; the
+ // leading 52 reserved bytes carry data (the non-replicated op code lives
+ // in `reserved[0..4]`), so a reshuffle of the carve must trip here rather
+ // than silently move the code range.
+ #[test]
+ fn routed_request_group_claims_reserved_tail() {
+ use std::mem::offset_of;
+ assert_eq!(
+ offset_of!(RoutedRequestHeader, reserved),
+ offset_of!(RequestHeader, reserved)
+ );
+ assert_eq!(
+ offset_of!(RoutedRequestHeader, group),
+ offset_of!(RequestHeader, reserved) + 52
+ );
+ assert_eq!(offset_of!(RoutedRequestHeader, group), 248);
+ assert_eq!(
+ offset_of!(RoutedRequestHeader, group) + size_of::<u64>(),
+ HEADER_SIZE
+ );
+ }
+
+ // The routed shape is decoded straight off the peer wire, so it must
+ // enforce the same field rules as the client boundary; a command-only
+ // validate lets a forged `client = 0` frame reach the client table's
+ // hard assert.
+ #[test]
+ fn routed_request_zero_client_rejected() {
+ let header = RoutedRequestHeader {
+ command: Command2::Request,
+ operation: Operation::SendMessages,
+ session: 10,
+ request: 1,
+ ..RoutedRequestHeader::default()
+ };
+ assert!(header.validate().is_err());
+ }
+
+ #[test]
+ fn routed_request_reserved_operation_rejected() {
+ let header = RoutedRequestHeader {
+ command: Command2::Request,
+ client: 0xCAFE,
+ session: 10,
+ request: 1,
+ ..RoutedRequestHeader::default()
+ };
+ assert!(header.validate().is_err());
+ }
+
+ #[test]
+ fn routed_request_default_fails_validate() {
+ assert!(RoutedRequestHeader::default().validate().is_err());
+ }
+
+ #[test]
+ fn routed_request_valid_fields_accepted() {
+ let header = RoutedRequestHeader {
+ command: Command2::Request,
+ operation: Operation::SendMessages,
+ client: 0xCAFE,
+ session: 10,
+ request: 1,
+ group: 7,
+ ..RoutedRequestHeader::default()
+ };
+ assert!(header.validate().is_ok());
+ }
+
// A nonzero status rides the reserved region, which reply `validate` does
// not inspect: a status-bearing reply stays valid so the SDK can peek it.
#[test]
diff --git a/core/binary_protocol/src/version.rs b/core/binary_protocol/src/version.rs
index 9e7916c41..670c460b0 100644
--- a/core/binary_protocol/src/version.rs
+++ b/core/binary_protocol/src/version.rs
@@ -63,8 +63,8 @@
//! `ClientVersionInfo` is the leading bytes of the login-register request
//! *body*, which itself rides inside a 256-byte VSR `RequestHeader` (see
//! `consensus::header`): `command` = `Command2::Request`, `operation` =
-//! `Operation::Register`, `namespace` = `METADATA_GROUP`,
-//! client id in `RequestHeader.client`. A foreign SDK emits that header,
+//! `Operation::Register`, client id in `RequestHeader.client`. The client
+//! sends no group; the server derives it. A foreign SDK emits that header,
//! then the body starting with this prefix, to reach the gate.
//!
//! ## Login gate
diff --git a/core/consensus/src/client_table.rs b/core/consensus/src/client_table.rs
index 4413b8643..31c494d9f 100644
--- a/core/consensus/src/client_table.rs
+++ b/core/consensus/src/client_table.rs
@@ -1019,7 +1019,12 @@ impl From<Truncated> for ClientTableWireError {
}
/// Format tag for [`ClientTable::encode`]; bump on layout change.
-pub const CLIENT_TABLE_MAGIC: [u8; 4] = *b"ICT1";
+///
+/// That includes any `ReplyHeader` layout move -- cached replies are embedded
+/// as raw wire bytes, so an artifact written under an older header layout must
+/// be refused, not silently misread. `ICT2`: `status` sits at offset 216 (the
+/// pre-`ICT2` layout carried a `namespace` word before it).
+pub const CLIENT_TABLE_MAGIC: [u8; 4] = *b"ICT2";
/// Per-entry fixed fields in the wire encoding: `client(u128) epoch(u64)
/// user_id(u32) watermark(u64) watermark_checksum(u128) ring_len(u8)`.
diff --git a/core/metadata/src/stm/snapshot.rs b/core/metadata/src/stm/snapshot.rs
index ca28a30a4..e9fadca17 100644
--- a/core/metadata/src/stm/snapshot.rs
+++ b/core/metadata/src/stm/snapshot.rs
@@ -53,6 +53,10 @@ pub enum SnapshotError {
commit_op: u64,
table_frontier: u64,
},
+ /// The snapshot was written under a different format version. Refuse it
+ /// rather than reinterpret embedded raw bytes (the client table's cached
+ /// replies are wire `ReplyHeader` frames) under the wrong layout.
+ UnsupportedVersion { found: u32, supported: u32 },
}
/// Stage at which snapshot persistence failed.
@@ -104,6 +108,13 @@ impl fmt::Display for SnapshotError {
commit_op {commit_op}, table frontier {table_frontier}"
)
}
+ Self::UnsupportedVersion { found, supported } => {
+ write!(
+ f,
+ "unsupported metadata snapshot version {found}; this build reads only \
+ version {supported}"
+ )
+ }
}
}
}
@@ -116,7 +127,8 @@ impl std::error::Error for SnapshotError {
Self::Io(e) | Self::Persist { source: e, .. } => Some(e),
Self::ChecksumMismatch { .. }
| Self::Truncated { .. }
- | Self::IncoherentManifest { .. } => None,
+ | Self::IncoherentManifest { .. }
+ | Self::UnsupportedVersion { .. } => None,
}
}
}
@@ -138,10 +150,17 @@ impl From<std::io::Error> for SnapshotError {
/// replicas with identical state must serialize identically. Regression guards:
/// `stream::tests::populated_streams_snapshot_reencode_is_byte_stable` and
/// `impls::metadata::tests::populated_snapshot_reencode_and_checksum_are_stable`.
+/// Current [`MetadataSnapshot::version`]. Bump whenever the serialized form
+/// changes meaning without changing shape -- in particular the client table's
+/// cached replies, which are embedded as raw `ReplyHeader` wire bytes msgpack
+/// cannot introspect. Version 2: `status` sits at reply-header offset 216
+/// (version 1 carried a `namespace` word before it).
+pub const METADATA_SNAPSHOT_VERSION: u32 = 2;
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetadataSnapshot {
- /// Snapshot format version for forward/backward compatibility.
- /// TODO(krishvishal): Properly handle versioning for snapshot. This is a placeholder for now.
+ /// Snapshot format version; [`MetadataSnapshot::decode`] refuses any other
+ /// value (see [`METADATA_SNAPSHOT_VERSION`]).
pub version: u32,
/// Timestamp when the snapshot was created (microseconds since epoch).
pub created_at: u64,
@@ -171,7 +190,7 @@ impl MetadataSnapshot {
#[must_use]
pub const fn new(sequence_number: u64) -> Self {
Self {
- version: 1,
+ version: METADATA_SNAPSHOT_VERSION,
// Deterministic placeholder. The real creation time is stamped by
// `Snapshot::create` from the consensus-injected clock (see
// `VsrConsensus::clock_realtime_micros`) so a replayed simulator
@@ -196,9 +215,18 @@ impl MetadataSnapshot {
/// Decode a snapshot from msgpack bytes.
///
/// # Errors
- /// Returns `SnapshotError::Deserialize` if msgpack deserialization fails.
+ /// Returns `SnapshotError::Deserialize` if msgpack deserialization fails,
+ /// or `SnapshotError::UnsupportedVersion` if the snapshot was written
+ /// under a different format version.
pub fn decode(bytes: &[u8]) -> Result<Self, SnapshotError> {
- rmp_serde::from_slice(bytes).map_err(SnapshotError::Deserialize)
+ let snapshot: Self = rmp_serde::from_slice(bytes).map_err(SnapshotError::Deserialize)?;
+ if snapshot.version != METADATA_SNAPSHOT_VERSION {
+ return Err(SnapshotError::UnsupportedVersion {
+ found: snapshot.version,
+ supported: METADATA_SNAPSHOT_VERSION,
+ });
+ }
+ Ok(snapshot)
}
}
@@ -440,6 +468,25 @@ mod tests {
assert!(decoded.client_table.is_none());
}
+ // The client table's cached replies are embedded as raw `ReplyHeader`
+ // wire bytes msgpack cannot introspect, so a snapshot from a different
+ // format version must be refused, never reinterpreted under the current
+ // header layout.
+ #[test]
+ fn decode_refuses_a_snapshot_from_another_format_version() {
+ let mut snapshot = MetadataSnapshot::new(42);
+ snapshot.version = METADATA_SNAPSHOT_VERSION - 1;
+
+ let encoded = snapshot.encode().unwrap();
+ assert!(matches!(
+ MetadataSnapshot::decode(&encoded),
+ Err(SnapshotError::UnsupportedVersion {
+ found,
+ supported: METADATA_SNAPSHOT_VERSION,
+ }) if found == METADATA_SNAPSHOT_VERSION - 1
+ ));
+ }
+
#[test]
fn roundtrip_with_data() {
let ts = IggyTimestamp::from(1_694_968_446_131_680_u64);
diff --git a/core/server-ng/src/dispatch.rs b/core/server-ng/src/dispatch.rs
index da40c303e..73769fe3f 100644
--- a/core/server-ng/src/dispatch.rs
+++ b/core/server-ng/src/dispatch.rs
@@ -867,9 +867,7 @@ async fn handle_client_request<B, MJ, S, SB>(
// client wire carries no group (it is derived -- plane from `operation`,
// partition target from the payload), so it starts unset here and the
// resolution sites below stamp it before anything routes on it.
- let request = request.transmute_header(|header, new_header: &mut RoutedRequestHeader| {
- *new_header = RoutedRequestHeader::from_request(&header, 0);
- });
+ let request = request.into_routed();
// The last point that still sees the body the CLIENT sent; every rewrite below
// substitutes server-chosen bytes and carries the stamp through unchanged.
diff --git a/core/server_common/src/consensus_message.rs b/core/server_common/src/consensus_message.rs
index 6180853f9..66f973a99 100644
--- a/core/server_common/src/consensus_message.rs
+++ b/core/server_common/src/consensus_message.rs
@@ -19,12 +19,15 @@ use crate::iobuf::{Frozen, Owned};
use iggy_binary_protocol::{
Command2, CommitHeader, ConsensusError, ConsensusHeader, DoViewChangeHeader, GenericHeader,
Operation, PrepareHeader, PrepareOkHeader, RepairPrepareHeader, RepairRangeReplyHeader,
- RequestPreparesHeader, RequestStartViewHeader, RequestStateChunkHeader,
+ RequestHeader, RequestPreparesHeader, RequestStartViewHeader, RequestStateChunkHeader,
RequestStateTransferHeader, RoutedRequestHeader, StartViewChangeHeader, StartViewHeader,
StateChunkHeader, StateTransferTargetHeader,
};
use smallvec::SmallVec;
-use std::{marker::PhantomData, mem::size_of};
+use std::{
+ marker::PhantomData,
+ mem::{offset_of, size_of},
+};
pub const MESSAGE_ALIGN: usize = 4096;
@@ -378,6 +381,31 @@ where
}
}
+impl Message<RequestHeader> {
+ /// Retype the client-wire request into the server-internal
+ /// [`RoutedRequestHeader`] shape in place, with `group` starting unset.
+ ///
+ /// The two layouts share every field offset (const-asserted where they
+ /// are declared) and `group` claims the client header's reserved tail,
+ /// so the promotion zeroes those eight bytes instead of rebuilding the
+ /// whole 256-byte header. This is the only sanctioned crossing between
+ /// the two layouts: transmute-based reads across them would alias
+ /// `group` with reserved bytes a client may have sent nonzero.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the retyped header fails [`RoutedRequestHeader`] validation;
+ /// unreachable when `self` already passed [`RequestHeader`] validation,
+ /// which enforces the same field rules.
+ #[must_use]
+ pub fn into_routed(self) -> Message<RoutedRequestHeader> {
+ let group_offset = offset_of!(RoutedRequestHeader, group);
+ let mut owned = self.into_owned();
+ owned.as_mut_slice()[group_offset..group_offset + size_of::<u64>()].fill(0);
+ Message::try_from(owned).expect("retyped request message must stay valid")
+ }
+}
+
impl<H> Message<H, ResponseBacking>
where
H: ConsensusHeader,
@@ -889,9 +917,7 @@ mod tests {
#[test]
fn try_as_typed_invalid_validation_returns_err() {
// `RequestHeader::validate` rejects operation=Register with non-zero
- // session. The client-wire header is the one carrying the field rules;
- // `RoutedRequestHeader` is the post-resolution shape and gates on the
- // command alone.
+ // session; the routed shape shares the same field rules.
let mut owned = header_bytes(Command2::Request, 256);
{
let buf = owned.as_mut_slice();
@@ -975,9 +1001,6 @@ mod tests {
#[test]
fn client_wire_decode_of_request_with_invalid_register_session_returns_err() {
// `RequestHeader::validate` rejects Register with non-zero session.
- // Not routed through `MessageBag`: that path decodes the already-routed
- // `RoutedRequestHeader` for replica-to-replica frames, and the field
- // rules are the client boundary's job.
let mut owned = header_bytes(Command2::Request, 256);
{
let buf = owned.as_mut_slice();
@@ -1079,6 +1102,71 @@ mod tests {
assert_eq!(prepared.header().command, Command2::Prepare);
}
+ // into_routed: in-place client-wire -> routed retype
+
+ // Promotion must carry the data-bearing reserved prefix verbatim (the
+ // non-replicated op code lives in `reserved[0..4]`) and unset only the
+ // `group` tail, whatever junk the client sent in those eight bytes.
+ #[test]
+ fn into_routed_keeps_reserved_prefix_and_unsets_group() {
+ const RESERVED_OFF: usize = std::mem::offset_of!(RequestHeader, reserved);
+
+ let mut owned = header_bytes(Command2::Request, 256);
+ {
+ let buf = owned.as_mut_slice();
+ for (index, byte) in buf[RESERVED_OFF..RESERVED_OFF + 60].iter_mut().enumerate() {
+ *byte = u8::try_from(index).expect("60 fits u8") + 1;
+ }
+ }
+ let request = Message::<RequestHeader>::try_from(owned).expect("valid client frame");
+ let client_header = *request.header();
+
+ let routed = request.into_routed();
+ let header = routed.header();
+ assert_eq!(
+ header.reserved[..],
+ client_header.reserved[..52],
+ "the reserved prefix carries data and must survive promotion"
+ );
+ assert_eq!(
+ header.group, 0,
+ "the client-sent reserved tail must not leak into `group`"
+ );
+ assert_eq!(header.client, client_header.client);
+ assert_eq!(header.operation, client_header.operation);
+ assert_eq!(header.session, client_header.session);
+ assert_eq!(header.request, client_header.request);
+ assert_eq!(header.user_id, client_header.user_id);
+ }
+
+ // A peer-wire `Command2::Request` decodes as `RoutedRequestHeader`, so its
+ // validate must enforce the client-boundary field rules: a forged
+ // `client = 0` frame would otherwise reach the client table's hard assert
+ // and abort the metadata primary, and a `Reserved` operation would replay
+ // that client's cached register reply.
+ #[test]
+ fn messagebag_dispatch_rejects_request_with_zero_client() {
+ let mut owned = header_bytes(Command2::Request, 256);
+ owned.as_mut_slice()[REQUEST_CLIENT_OFF..REQUEST_CLIENT_OFF + 16]
+ .copy_from_slice(&0u128.to_le_bytes());
+ let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
+ assert!(matches!(
+ MessageBag::try_from(generic),
+ Err(ConsensusError::InvalidField(_))
+ ));
+ }
+
+ #[test]
+ fn messagebag_dispatch_rejects_request_with_reserved_operation() {
+ let mut owned = header_bytes(Command2::Request, 256);
+ owned.as_mut_slice()[REQUEST_OPERATION_OFF] = Operation::Reserved as u8;
+ let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
+ assert!(matches!(
+ MessageBag::try_from(generic),
+ Err(ConsensusError::InvalidField(_))
+ ));
+ }
+
// ResponseBacking via SmallVec<Frozen>
#[test]