use bytes::{BufMut, Bytes}; use commonware_codec::{ Buf, BufsMut, EncodeSize, Error, FixedSize, MaxEncodeSize, Read, ReadExt, Write, varint::MAX_U32_VARINT_SIZE, }; use commonware_utils::Span; /// Number of bytes a request adds to the key it carries: an 8-byte request ID and a 1-byte tag. /// An error carries no key and is exactly this size. pub const REQUEST_OVERHEAD: usize = u64::SIZE + u8::SIZE; /// Largest number of bytes a response adds to the value it carries: the request ID, the tag, and /// a value-length varint of at most 5 bytes. pub const MAX_RESPONSE_OVERHEAD: usize = REQUEST_OVERHEAD + MAX_U32_VARINT_SIZE; /// Returns an upper bound on the encoded size of any message for keys of type `Key` and values of /// at most `value` bytes. /// /// # Panics /// /// Panics if `value` exceeds `u32::MAX`. pub fn max_message_size(value: usize) -> usize { // A response's value-length varint encodes at most `u32::MAX`. u32::try_from(value).expect("value must fit the u32 length prefix"); // An error is a request without a key, so a request is always at least as wide. let request = Key::MAX_ENCODE_SIZE + REQUEST_OVERHEAD; let response = value + MAX_RESPONSE_OVERHEAD; request.max(response) } /// Represents a message sent between peers. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Message { /// Unique identifier for the message. /// Responses should have the same ID as the request they are responding to. pub id: u64, /// Payload is the data being sent. pub payload: Payload, } impl Write for Message { fn write(&self, buf: &mut impl BufMut) { buf.put_u64(self.id); self.payload.write(buf); } fn write_bufs(&self, buf: &mut impl BufsMut) { self.id.write(buf); self.payload.write_bufs(buf); } } impl EncodeSize for Message { fn encode_size(&self) -> usize { self.id.encode_size() + self.payload.encode_size() } fn encode_inline_size(&self) -> usize { self.id.encode_size() + self.payload.encode_inline_size() } } impl Read for Message { type Cfg = (); fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let id = u64::read(buf)?; let payload = Payload::read(buf)?; Ok(Self { id, payload }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Message where Key: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let id = u.arbitrary::()?; let payload = u.arbitrary::>()?; Ok(Self { id, payload }) } } /// Represents the contents of a message sent between peers. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Payload { // Request is a request for a response. Request(Key), // Response is a response to a request. Response(Bytes), // A response that indicates an unspecified error. // // This allows the requester to handle the error more quickly than timing out. Error, } impl Write for Payload { fn write(&self, buf: &mut impl BufMut) { match self { Self::Request(key) => { buf.put_u8(0); key.write(buf); } Self::Response(data) => { buf.put_u8(1); data.write(buf); } Self::Error => { buf.put_u8(2); } } } fn write_bufs(&self, buf: &mut impl BufsMut) { match self { Self::Request(key) => { buf.put_u8(0); key.write(buf); } Self::Response(data) => { buf.put_u8(1); data.write_bufs(buf); } Self::Error => { buf.put_u8(2); } } } } impl EncodeSize for Payload { fn encode_size(&self) -> usize { 1 + match self { Self::Request(key) => key.encode_size(), Self::Response(data) => data.encode_size(), Self::Error => 0, } } fn encode_inline_size(&self) -> usize { 1 + match self { Self::Request(key) => key.encode_size(), Self::Response(data) => data.encode_inline_size(), Self::Error => 0, } } } impl Read for Payload { type Cfg = (); fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let payload_type = u8::read(buf)?; match payload_type { 0 => { let key = Key::read(buf)?; Ok(Self::Request(key)) } 1 => { // The maximum length of a message is already bounded by the P2P connection. // Since the Bytes type will not allocate more memory than the buffer size, // we can safely read the bytes with no limit. If an attacker encodes the length of // the bytes with a value greater than the buffer size, the read will fail without // allocating more memory. let data = Bytes::read_cfg(buf, &(..).into())?; Ok(Self::Response(data)) } 2 => Ok(Self::Error), _ => Err(Error::Invalid("Payload", "Invalid payload type")), } } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Payload where Key: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let choice = u.int_in_range(0..=2)?; match choice { 0 => { let key = u.arbitrary::()?; Ok(Self::Request(key)) } 1 => { let size = u.int_in_range(0..=1024)?; let bytes = u.bytes(size)?; Ok(Self::Response(Bytes::from(bytes.to_vec()))) } 2 => Ok(Self::Error), _ => unreachable!(), } } } #[cfg(test)] mod tests { use super::*; use crate::p2p::mocks::Key as MockKey; use bytes::Buf as _; use commonware_codec::{DecodeExt, Encode}; use commonware_runtime::iobuf::EncodeExt; use commonware_utils::Widen; #[test] fn test_codec_request() { let key = MockKey(123u8); let payload = Payload::Request(key); let original = Message { id: 1234, payload }; let encoded = original.encode(); let decoded = Message::decode(encoded).unwrap(); assert_eq!(original, decoded); } #[test] fn test_codec_response() { let payload = Payload::::Response(Bytes::from("Hello, world!")); let original = Message { id: 4321, payload }; let encoded = original.encode(); let decoded = Message::decode(encoded).unwrap(); assert_eq!(original, decoded); } #[test] fn test_message_overhead() { let size = |payload: Payload| { Message { id: u64::MAX, payload, } .encode() .len() }; // A request adds exactly its overhead to the key, and an error is exactly that overhead. assert_eq!(size(Payload::Request(u8::MAX)), u8::SIZE + REQUEST_OVERHEAD); assert_eq!(size(Payload::Error), REQUEST_OVERHEAD); // Responses on both sides of each varint length boundary stay within the overhead. for len in [0, 127, 128, 16_383, 16_384] { assert!( size(Payload::Response(Bytes::from(vec![0; len]))) - len <= MAX_RESPONSE_OVERHEAD ); } // A response carrying a `u32::MAX`-byte value adds exactly the overhead. Measure the // envelope of an empty response and add the largest length prefix, so the test never // allocates that value. let envelope = size(Payload::Response(Bytes::new())) - Bytes::new().encode_size(); assert_eq!( envelope + Widen::::widen(u32::MAX).encode_size(), MAX_RESPONSE_OVERHEAD ); } #[test] fn test_max_message_size() { // The response is the widest message unless the key is wider than the value plus the // largest length prefix. assert_eq!(max_message_size::(0), MAX_RESPONSE_OVERHEAD); assert_eq!(max_message_size::(100), 100 + MAX_RESPONSE_OVERHEAD); assert_eq!(max_message_size::<[u8; 64]>(0), 64 + REQUEST_OVERHEAD); let largest: usize = Widen::widen(u32::MAX); assert_eq!( max_message_size::(largest), largest + MAX_RESPONSE_OVERHEAD ); } #[test] #[should_panic(expected = "value must fit the u32 length prefix")] fn test_max_message_size_beyond_prefix() { let largest: usize = Widen::widen(u32::MAX); max_message_size::(largest + 1); } #[test] fn test_codec_error() { let payload = Payload::::Error; let original = Message { id: 255, payload }; let encoded = original.encode(); let decoded = Message::decode(encoded).unwrap(); assert_eq!(original, decoded); } #[test] fn test_message_encode_iobufs_matches_encode() { let msg = Message { id: 42, payload: Payload::::Response(Bytes::from("hello world")), }; let encoded = msg.encode(); let mut encoded_bufs = msg.encode_iobufs(); let mut encoded_bufs_bytes = vec![0u8; encoded_bufs.remaining()]; encoded_bufs.copy_to_slice(&mut encoded_bufs_bytes); assert_eq!(encoded_bufs_bytes, encoded.as_ref()); } #[test] fn test_payload_response_encode_iobufs_matches_encode() { let payload = Payload::::Response(Bytes::from("response data")); let encoded = payload.encode(); let mut encoded_bufs = payload.encode_iobufs(); let mut encoded_bufs_bytes = vec![0u8; encoded_bufs.remaining()]; encoded_bufs.copy_to_slice(&mut encoded_bufs_bytes); assert_eq!(encoded_bufs_bytes, encoded.as_ref()); } #[cfg(feature = "arbitrary")] mod conformance { use super::*; use commonware_codec::conformance::CodecConformance; commonware_conformance::conformance_tests! { CodecConformance>, CodecConformance>, } } }