use chia_protocol::{Bytes32, Bytes48, Bytes96};
use chia_streamable_macro::Streamable;
use chia_traits::Streamable as StreamableTrait;
use crate::constants::{ENVELOPE_VERSION, MAX_ENVELOPE_BYTES};
use crate::error::{MessageError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Streamable)]
pub struct MessageType(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InteractionShape {
OneShot = 0,
Request = 1,
Response = 2,
StreamFrame = 3,
}
pub const FLAG_SHAPE_MASK: u8 = 0b0000_0011;
pub const FLAG_SEALED: u8 = 0b0000_0100;
impl InteractionShape {
#[must_use]
pub fn from_flags(flags: u8) -> Option<Self> {
match flags & FLAG_SHAPE_MASK {
0 => Some(Self::OneShot),
1 => Some(Self::Request),
2 => Some(Self::Response),
3 => Some(Self::StreamFrame),
_ => None,
}
}
#[must_use]
pub fn as_bits(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Streamable)]
pub struct StreamHeader {
pub frame: u8,
pub seq: u64,
pub window: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamFrame {
Open = 0,
OpenAck = 1,
Data = 2,
Credit = 3,
Close = 4,
CloseAck = 5,
Reset = 6,
}
impl StreamFrame {
#[must_use]
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(Self::Open),
1 => Some(Self::OpenAck),
2 => Some(Self::Data),
3 => Some(Self::Credit),
4 => Some(Self::Close),
5 => Some(Self::CloseAck),
6 => Some(Self::Reset),
_ => None,
}
}
#[must_use]
pub fn as_u8(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, PartialEq, Eq, Streamable)]
pub struct SealedPayload {
pub kem_enc: Bytes48,
pub ciphertext: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Streamable)]
pub struct InnerMessage {
pub message_type: u32,
pub correlation_id: Bytes32,
pub compression: u8,
pub uncompressed_len: u32,
pub counter: u64,
pub timestamp_ms: u64,
pub expires_at: u64,
pub payload: Vec<u8>,
pub sender_sig: Bytes96,
}
#[derive(Debug, Clone, PartialEq, Eq, Streamable)]
pub struct DigMessageEnvelope {
pub version: u8,
pub message_type: u32,
pub flags: u8,
pub correlation_id: Bytes32,
pub sender: Bytes32,
pub recipient: Bytes32,
pub sender_epoch: u32,
pub stream: Option<StreamHeader>,
pub sealed: SealedPayload,
}
impl DigMessageEnvelope {
pub fn header_bytes(&self) -> Result<Vec<u8>> {
let mut out = Vec::new();
let codec = |e: chia_traits::Error| MessageError::Codec(e.to_string());
self.version.stream(&mut out).map_err(codec)?;
self.message_type.stream(&mut out).map_err(codec)?;
self.flags.stream(&mut out).map_err(codec)?;
self.correlation_id.stream(&mut out).map_err(codec)?;
self.sender.stream(&mut out).map_err(codec)?;
self.recipient.stream(&mut out).map_err(codec)?;
self.sender_epoch.stream(&mut out).map_err(codec)?;
self.stream.stream(&mut out).map_err(codec)?;
Ok(out)
}
}
pub fn encode_envelope(envelope: &DigMessageEnvelope) -> Result<Vec<u8>> {
let bytes = envelope
.to_bytes()
.map_err(|e| MessageError::Codec(e.to_string()))?;
if bytes.len() > MAX_ENVELOPE_BYTES {
return Err(MessageError::EnvelopeTooLarge {
size: bytes.len(),
max: MAX_ENVELOPE_BYTES,
});
}
Ok(bytes)
}
pub fn decode_envelope(bytes: &[u8]) -> Result<DigMessageEnvelope> {
if bytes.len() > MAX_ENVELOPE_BYTES {
return Err(MessageError::EnvelopeTooLarge {
size: bytes.len(),
max: MAX_ENVELOPE_BYTES,
});
}
let envelope = DigMessageEnvelope::from_bytes(bytes)
.map_err(|e| MessageError::Truncated(e.to_string()))?;
if envelope.version > ENVELOPE_VERSION {
return Err(MessageError::UnsupportedVersion(envelope.version));
}
Ok(envelope)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compression::COMPRESSION_NONE;
fn sample(shape: InteractionShape, stream: Option<StreamHeader>) -> DigMessageEnvelope {
DigMessageEnvelope {
version: ENVELOPE_VERSION,
message_type: 0x0000_0201,
flags: shape.as_bits() | FLAG_SEALED,
correlation_id: Bytes32::new([1u8; 32]),
sender: Bytes32::new([2u8; 32]),
recipient: Bytes32::new([3u8; 32]),
sender_epoch: 7,
stream,
sealed: SealedPayload {
kem_enc: Bytes48::new([4u8; 48]),
ciphertext: vec![9, 8, 7, 6, 5],
},
}
}
#[test]
fn envelope_round_trips_for_every_shape() {
let cases = [
(InteractionShape::OneShot, None),
(InteractionShape::Request, None),
(InteractionShape::Response, None),
(
InteractionShape::StreamFrame,
Some(StreamHeader {
frame: StreamFrame::Data as u8,
seq: 42,
window: 8,
}),
),
];
for (shape, stream) in cases {
let env = sample(shape, stream);
let bytes = encode_envelope(&env).unwrap();
let decoded = decode_envelope(&bytes).unwrap();
assert_eq!(env, decoded, "{shape:?} must round-trip");
}
}
#[test]
fn encoding_is_byte_deterministic() {
let env = sample(InteractionShape::OneShot, None);
assert_eq!(
encode_envelope(&env).unwrap(),
encode_envelope(&env).unwrap()
);
}
#[test]
fn inner_message_round_trips() {
let inner = InnerMessage {
message_type: 0x0000_0201,
correlation_id: Bytes32::new([1u8; 32]),
compression: COMPRESSION_NONE,
uncompressed_len: 5,
counter: 11,
timestamp_ms: 1_700_000_000_000,
expires_at: 0,
payload: vec![1, 2, 3, 4, 5],
sender_sig: Bytes96::new([0u8; 96]),
};
let bytes = inner.to_bytes().unwrap();
assert_eq!(InnerMessage::from_bytes(&bytes).unwrap(), inner);
}
#[test]
fn unknown_newer_version_is_rejected() {
let mut env = sample(InteractionShape::OneShot, None);
env.version = ENVELOPE_VERSION + 1;
let bytes = env.to_bytes().unwrap();
assert_eq!(
decode_envelope(&bytes).unwrap_err(),
MessageError::UnsupportedVersion(ENVELOPE_VERSION + 1)
);
}
#[test]
fn oversized_frame_is_rejected_before_decoding() {
let bytes = vec![0u8; MAX_ENVELOPE_BYTES + 1];
assert_eq!(
decode_envelope(&bytes).unwrap_err(),
MessageError::EnvelopeTooLarge {
size: MAX_ENVELOPE_BYTES + 1,
max: MAX_ENVELOPE_BYTES
}
);
}
#[test]
fn oversized_envelope_encode_is_rejected() {
let mut env = sample(InteractionShape::OneShot, None);
env.sealed.ciphertext = vec![0u8; MAX_ENVELOPE_BYTES + 1];
assert!(matches!(
encode_envelope(&env).unwrap_err(),
MessageError::EnvelopeTooLarge { .. }
));
}
#[test]
fn truncated_frame_is_rejected() {
let env = sample(InteractionShape::OneShot, None);
let bytes = encode_envelope(&env).unwrap();
let err = decode_envelope(&bytes[..bytes.len() / 2]).unwrap_err();
assert!(matches!(err, MessageError::Truncated(_)));
}
#[test]
fn header_bytes_excludes_the_sealed_region() {
let env = sample(InteractionShape::OneShot, None);
let header = env.header_bytes().unwrap();
let full = env.to_bytes().unwrap();
assert!(full.starts_with(&header));
assert!(header.len() < full.len());
}
#[test]
fn flags_shape_helpers_round_trip() {
for shape in [
InteractionShape::OneShot,
InteractionShape::Request,
InteractionShape::Response,
InteractionShape::StreamFrame,
] {
let flags = shape.as_bits() | FLAG_SEALED;
assert_eq!(InteractionShape::from_flags(flags), Some(shape));
assert_eq!(flags & FLAG_SEALED, FLAG_SEALED);
}
}
#[test]
fn message_type_round_trips() {
let mt = MessageType(0x1000_0000);
assert_eq!(
MessageType::from_bytes(&mt.to_bytes().unwrap()).unwrap(),
mt
);
}
}