Skip to main content

dig_peer_protocol/
dig_message.rs

1//! [`DigMessage`] — wire-compatible `Message` with raw `u8` opcode support.
2//!
3//! ## Problem
4//!
5//! `chia_protocol::Message` stores `msg_type` as `ProtocolMessageTypes` (a closed `#[repr(u8)]`
6//! enum). `Message::from_bytes` rejects any opcode not in that enum, which means DIG extension
7//! opcodes (200–219) cannot be decoded without patching the upstream crate.
8//!
9//! ## Solution
10//!
11//! `DigMessage` uses the same wire layout (`u8 + Option<u16> + Bytes`) but stores `msg_type`
12//! as a plain `u8`. This allows encoding and decoding any opcode — Chia or DIG — without
13//! modifying upstream types. Conversion to/from `chia_protocol::Message` is provided for
14//! opcodes that the Chia enum recognizes.
15
16use chia_protocol::{Bytes, Message, ProtocolMessageTypes};
17use chia_traits::Streamable;
18
19/// Wire message with raw `u8` opcode — handles both Chia (0–107) and DIG (200–219) opcodes.
20///
21/// Same binary layout as `chia_protocol::Message`:
22/// ```text
23/// [u8 msg_type] [bool has_id] [u16 id (if has_id)] [u32 data_len] [u8... data]
24/// ```
25///
26/// Use [`DigMessage::to_bytes`] / [`DigMessage::from_bytes`] for wire serialization.
27/// Use [`DigMessage::try_into_chia_message`] to convert to stock `Message` when the opcode
28/// is a known Chia type.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct DigMessage {
31    /// Raw wire opcode — no enum restriction.
32    pub msg_type: u8,
33    /// Correlation ID for request/response pairing (same as `Message.id`).
34    pub id: Option<u16>,
35    /// Serialized payload body.
36    pub data: Bytes,
37}
38
39impl DigMessage {
40    /// Protocol-level ceiling on a single message's declared payload length (bytes).
41    ///
42    /// `from_bytes` rejects any `data_len` prefix above this value with `None` before
43    /// slicing/allocating the payload, so a peer cannot force a multi-gigabyte `Vec`
44    /// allocation via a lying (or genuine but oversized) length prefix. Mirrors
45    /// `chia-protocol`'s own message-size ceiling (16 MiB) — comfortably above any
46    /// legitimate DIG opcode payload (attestations, checkpoints, block-transaction
47    /// batches) while bounding worst-case per-message memory.
48    ///
49    /// **Callers MUST still enforce a per-frame size cap at the transport/framing layer
50    /// BEFORE buffering an incoming frame into a contiguous `&[u8]`** — this constant
51    /// only bounds what `from_bytes` will accept once a slice already exists; it cannot
52    /// stop a transport from reading an unbounded number of bytes off the wire first.
53    pub const MAX_MESSAGE_SIZE: usize = 16 * 1024 * 1024; // 16 MiB
54
55    /// Construct a new DIG message.
56    pub fn new(msg_type: u8, id: Option<u16>, data: Bytes) -> Self {
57        Self { msg_type, id, data }
58    }
59
60    /// Serialize to wire bytes (same format as `Message::to_bytes`).
61    ///
62    /// Layout: `[u8 msg_type] [u8 has_id (0/1)] [u16 id if present] [u32 data_len] [data...]`
63    pub fn to_bytes(&self) -> Vec<u8> {
64        let mut buf = Vec::with_capacity(1 + 1 + 2 + 4 + self.data.len());
65        buf.push(self.msg_type);
66        match self.id {
67            Some(id) => {
68                buf.push(1); // has_id = true
69                buf.extend_from_slice(&id.to_be_bytes());
70            }
71            None => {
72                buf.push(0); // has_id = false
73            }
74        }
75        buf.extend_from_slice(&(self.data.len() as u32).to_be_bytes());
76        buf.extend_from_slice(&self.data);
77        buf
78    }
79
80    /// Deserialize from wire bytes. Accepts any `msg_type` value — no enum validation.
81    ///
82    /// Returns `None` if the buffer is too short, the length prefix doesn't match, or
83    /// the declared `data_len` exceeds [`Self::MAX_MESSAGE_SIZE`].
84    ///
85    /// This is a leaf parser over an already-materialized `&[u8]`: the `MAX_MESSAGE_SIZE`
86    /// check bounds the allocation this function performs, but it cannot bound how many
87    /// bytes a transport read off the wire to build `bytes` in the first place. **Callers
88    /// MUST enforce a per-frame size cap at the transport/framing layer before buffering
89    /// an incoming frame**, so that a peer cannot force unbounded buffering ahead of ever
90    /// reaching this function.
91    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
92        if bytes.len() < 2 {
93            return None;
94        }
95        let msg_type = bytes[0];
96        let has_id = bytes[1];
97        let mut offset: usize = 2;
98
99        let id = if has_id != 0 {
100            let after_id = offset.checked_add(2)?;
101            if bytes.len() < after_id {
102                return None;
103            }
104            let id = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]);
105            offset = after_id;
106            Some(id)
107        } else {
108            None
109        };
110
111        let after_len = offset.checked_add(4)?;
112        if bytes.len() < after_len {
113            return None;
114        }
115        let data_len = u32::from_be_bytes([
116            bytes[offset],
117            bytes[offset + 1],
118            bytes[offset + 2],
119            bytes[offset + 3],
120        ]) as usize;
121        offset = after_len;
122
123        if data_len > Self::MAX_MESSAGE_SIZE {
124            return None;
125        }
126
127        // Checked (not `offset + data_len`) so a peer-controlled `data_len` can never
128        // wrap `usize` on a 32-bit target — overflow here returns None instead of
129        // either panicking (debug) or proceeding with a wrapped, corrupted range
130        // (release). MAX_MESSAGE_SIZE already rejects data_len this large in practice,
131        // but the arithmetic itself stays overflow-safe independent of that cap.
132        let end = offset.checked_add(data_len)?;
133        if bytes.len() < end {
134            return None;
135        }
136        let data = Bytes::new(bytes[offset..end].to_vec());
137
138        Some(Self { msg_type, id, data })
139    }
140
141    /// Deserialize from an OWNED byte buffer, moving the payload instead of copying it.
142    ///
143    /// Identical acceptance/rejection rules to [`Self::from_bytes`] (same length checks,
144    /// same [`Self::MAX_MESSAGE_SIZE`] cap, same `None` on any malformed input) — the
145    /// only difference is that when `buf` already owns its bytes (e.g. a transport that
146    /// read the frame directly into a `Vec<u8>`), the payload is moved into the result
147    /// via `Vec::drain` instead of being copied out of a borrowed `&[u8]` with `to_vec`.
148    /// Prefer this over `from_bytes` whenever the caller already has an owned `Vec<u8>`
149    /// and does not need it afterward.
150    pub fn from_bytes_owned(mut buf: Vec<u8>) -> Option<Self> {
151        if buf.len() < 2 {
152            return None;
153        }
154        let msg_type = buf[0];
155        let has_id = buf[1];
156        let mut offset: usize = 2;
157
158        let id = if has_id != 0 {
159            let after_id = offset.checked_add(2)?;
160            if buf.len() < after_id {
161                return None;
162            }
163            let id = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
164            offset = after_id;
165            Some(id)
166        } else {
167            None
168        };
169
170        let after_len = offset.checked_add(4)?;
171        if buf.len() < after_len {
172            return None;
173        }
174        let data_len = u32::from_be_bytes([
175            buf[offset],
176            buf[offset + 1],
177            buf[offset + 2],
178            buf[offset + 3],
179        ]) as usize;
180        offset = after_len;
181
182        if data_len > Self::MAX_MESSAGE_SIZE {
183            return None;
184        }
185
186        let end = offset.checked_add(data_len)?;
187        if buf.len() < end {
188            return None;
189        }
190
191        // Move just the payload range out of `buf` without copying: drain removes and
192        // yields the [offset..end) elements by value, leaving any trailing bytes (and
193        // the header bytes before offset) dropped rather than retained.
194        let data: Vec<u8> = buf.drain(offset..end).collect();
195
196        Some(Self {
197            msg_type,
198            id,
199            data: Bytes::new(data),
200        })
201    }
202
203    /// Convert from a stock `chia_protocol::Message` (lossless — opcode stored as u8).
204    ///
205    /// Clones `msg.data`. Prefer [`Self::from_chia_message_owned`] when an owned
206    /// `Message` is available and does not need to be kept afterward.
207    pub fn from_chia_message(msg: &Message) -> Self {
208        Self {
209            msg_type: msg.msg_type as u8,
210            id: msg.id,
211            data: msg.data.clone(),
212        }
213    }
214
215    /// Convert from an OWNED stock `chia_protocol::Message`, moving `data` instead of
216    /// cloning it (lossless — opcode stored as u8).
217    pub fn from_chia_message_owned(msg: Message) -> Self {
218        Self {
219            msg_type: msg.msg_type as u8,
220            id: msg.id,
221            data: msg.data,
222        }
223    }
224
225    /// Try to convert to a stock `chia_protocol::Message`, cloning `self.data`.
226    ///
227    /// Fails if `msg_type` is not a valid `ProtocolMessageTypes` discriminant
228    /// (i.e., DIG extension opcodes 200+ will fail here — use `DigMessage` directly
229    /// for those). Prefer [`Self::into_chia_message`] when `self` does not need to be
230    /// kept afterward.
231    pub fn try_into_chia_message(&self) -> Option<Message> {
232        // ProtocolMessageTypes implements Streamable; from_bytes on a single u8
233        let pmt = ProtocolMessageTypes::from_bytes(&[self.msg_type]).ok()?;
234        Some(Message {
235            msg_type: pmt,
236            id: self.id,
237            data: self.data.clone(),
238        })
239    }
240
241    /// Try to convert into a stock `chia_protocol::Message` BY VALUE, moving `self.data`
242    /// instead of cloning it.
243    ///
244    /// Same failure condition as [`Self::try_into_chia_message`]: `None` when `msg_type`
245    /// is not a valid `ProtocolMessageTypes` discriminant. On failure `self` is dropped
246    /// (its opcode was not a known Chia type, so there is nothing useful to hand back).
247    pub fn into_chia_message(self) -> Option<Message> {
248        let pmt = ProtocolMessageTypes::from_bytes(&[self.msg_type]).ok()?;
249        Some(Message {
250            msg_type: pmt,
251            id: self.id,
252            data: self.data,
253        })
254    }
255
256    /// Whether this message carries a DIG extension opcode (>= 200).
257    pub fn is_dig_extension(&self) -> bool {
258        self.msg_type >= 200
259    }
260
261    /// Whether this message carries a standard Chia opcode (< 200).
262    pub fn is_chia_standard(&self) -> bool {
263        self.msg_type < 200
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn round_trip_chia_opcode() {
273        // NewPeak = 20
274        let msg = DigMessage::new(20, Some(42), Bytes::new(vec![1, 2, 3]));
275        let wire = msg.to_bytes();
276        let decoded = DigMessage::from_bytes(&wire).expect("decode");
277        assert_eq!(decoded.msg_type, 20);
278        assert_eq!(decoded.id, Some(42));
279        assert_eq!(decoded.data.as_ref(), &[1, 2, 3]);
280    }
281
282    #[test]
283    fn round_trip_dig_opcode() {
284        // RegisterPeer = 218
285        let msg = DigMessage::new(218, None, Bytes::new(vec![0xAB]));
286        let wire = msg.to_bytes();
287        let decoded = DigMessage::from_bytes(&wire).expect("decode");
288        assert_eq!(decoded.msg_type, 218);
289        assert_eq!(decoded.id, None);
290        assert!(decoded.is_dig_extension());
291        assert!(!decoded.is_chia_standard());
292    }
293
294    #[test]
295    fn from_bytes_owned_avoids_copy_when_buffer_is_owned() {
296        // Given an owned Vec<u8> (e.g. a transport that already read the frame into its
297        // own buffer), from_bytes_owned must decode identically to from_bytes but move
298        // the payload bytes into the resulting DigMessage instead of copying them via
299        // to_vec() on a borrowed slice.
300        let msg = DigMessage::new(218, Some(7), Bytes::new(vec![9, 9, 9]));
301        let wire: Vec<u8> = msg.to_bytes();
302        let decoded = DigMessage::from_bytes_owned(wire).expect("decode");
303        assert_eq!(decoded, msg);
304    }
305
306    #[test]
307    fn from_bytes_owned_rejects_same_malformed_inputs_as_from_bytes() {
308        assert!(DigMessage::from_bytes_owned(vec![]).is_none());
309        assert!(DigMessage::from_bytes_owned(vec![20, 1]).is_none());
310
311        let mut wire = vec![1u8, 0];
312        wire.extend_from_slice(&((DigMessage::MAX_MESSAGE_SIZE as u32) + 1).to_be_bytes());
313        assert!(DigMessage::from_bytes_owned(wire).is_none());
314
315        let truncated = vec![20u8, 0, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD];
316        assert!(DigMessage::from_bytes_owned(truncated).is_none());
317    }
318
319    #[test]
320    fn from_bytes_owned_ignores_trailing_bytes_like_from_bytes() {
321        // Parity with from_bytes: trailing bytes beyond data_len are not an error and
322        // are simply not included in the decoded payload.
323        let mut wire = vec![20u8, 0, 0x00, 0x00, 0x00, 0x02, 0xAB, 0xCD];
324        wire.extend_from_slice(&[0xEE, 0xFF]); // trailing garbage
325        let decoded = DigMessage::from_bytes_owned(wire).expect("decode");
326        assert_eq!(decoded.data.as_ref(), &[0xAB, 0xCD]);
327    }
328
329    #[test]
330    fn into_chia_message_moves_data_without_cloning() {
331        // into_chia_message consumes self by value; the resulting Message's data must
332        // equal what try_into_chia_message (the cloning, borrowing variant) would have
333        // produced, proving the by-value path is behaviourally identical.
334        let dig = DigMessage::new(20, Some(3), Bytes::new(vec![1, 2, 3, 4]));
335        let expected_data = dig.data.clone();
336        let msg = dig.into_chia_message().expect("known opcode");
337        assert_eq!(msg.msg_type, ProtocolMessageTypes::NewPeak);
338        assert_eq!(msg.id, Some(3));
339        assert_eq!(msg.data, expected_data);
340    }
341
342    #[test]
343    fn into_chia_message_rejects_unknown_opcode() {
344        let dig = DigMessage::new(250, None, Bytes::default());
345        assert!(dig.into_chia_message().is_none());
346    }
347
348    #[test]
349    fn from_chia_message_owned_moves_data_without_cloning() {
350        let chia_msg = Message {
351            msg_type: ProtocolMessageTypes::NewPeak,
352            id: Some(11),
353            data: Bytes::new(vec![7, 7, 7]),
354        };
355        let expected_data = chia_msg.data.clone();
356        let dig = DigMessage::from_chia_message_owned(chia_msg);
357        assert_eq!(dig.msg_type, 20);
358        assert_eq!(dig.id, Some(11));
359        assert_eq!(dig.data, expected_data);
360    }
361
362    #[test]
363    fn from_chia_message() {
364        let chia_msg = Message {
365            msg_type: ProtocolMessageTypes::NewPeak,
366            id: Some(7),
367            data: Bytes::new(vec![0xFF]),
368        };
369        let dig = DigMessage::from_chia_message(&chia_msg);
370        assert_eq!(dig.msg_type, 20); // NewPeak = 20
371        assert_eq!(dig.id, Some(7));
372
373        // Round-trip back to Chia
374        let back = dig.try_into_chia_message().expect("known opcode");
375        assert_eq!(back.msg_type, ProtocolMessageTypes::NewPeak);
376    }
377
378    #[test]
379    fn unknown_opcode_cannot_convert_to_chia_message() {
380        // Use an opcode that is NOT in ProtocolMessageTypes (neither Chia nor vendored DIG).
381        let dig = DigMessage::new(250, None, Bytes::default());
382        assert!(dig.try_into_chia_message().is_none());
383    }
384
385    #[test]
386    fn empty_buffer_returns_none() {
387        assert!(DigMessage::from_bytes(&[]).is_none());
388        assert!(DigMessage::from_bytes(&[20]).is_none());
389    }
390
391    #[test]
392    fn truncated_id_prefix_returns_none() {
393        // has_id = 1 but the buffer ends before the 2-byte id can be read.
394        // [msg_type=20][has_id=1] then only ONE of the two id bytes present.
395        assert!(DigMessage::from_bytes(&[20, 1]).is_none());
396        assert!(DigMessage::from_bytes(&[20, 1, 0x00]).is_none());
397    }
398
399    #[test]
400    fn truncated_data_len_prefix_returns_none() {
401        // has_id = 0, so offset = 2, but fewer than 4 bytes remain for the u32 data_len.
402        assert!(DigMessage::from_bytes(&[20, 0]).is_none());
403        assert!(DigMessage::from_bytes(&[20, 0, 0x00, 0x00, 0x00]).is_none());
404
405        // With an id present (offset = 4), still short of the 4-byte data_len.
406        assert!(DigMessage::from_bytes(&[20, 1, 0x00, 0x2A, 0x00, 0x00]).is_none());
407    }
408
409    #[test]
410    fn truncated_data_returns_none() {
411        // Declares data_len = 4 but supplies only 2 payload bytes.
412        // [msg_type=20][has_id=0][data_len=00 00 00 04][data=AB CD] (missing 2 bytes)
413        let wire = [20u8, 0, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD];
414        assert!(DigMessage::from_bytes(&wire).is_none());
415
416        // Exact-fit boundary: data_len = 2 with exactly 2 payload bytes decodes.
417        let ok = [20u8, 0, 0x00, 0x00, 0x00, 0x02, 0xAB, 0xCD];
418        let decoded = DigMessage::from_bytes(&ok).expect("exact-length payload decodes");
419        assert_eq!(decoded.data.as_ref(), &[0xAB, 0xCD]);
420    }
421
422    #[test]
423    fn oversized_data_len_is_rejected() {
424        // data_len prefix declares more than MAX_MESSAGE_SIZE — must be rejected with
425        // None BEFORE any attempt to read/allocate the (possibly absent) payload bytes,
426        // regardless of how many bytes actually follow.
427        let over = (DigMessage::MAX_MESSAGE_SIZE as u32) + 1;
428        let mut wire = vec![20u8, 0]; // msg_type=20, has_id=0
429        wire.extend_from_slice(&over.to_be_bytes());
430        // No payload bytes supplied at all — if the cap check didn't fire first, this
431        // would already fail the length check, so also prove the cap fires even when
432        // enough bytes *are* present.
433        assert!(DigMessage::from_bytes(&wire).is_none());
434    }
435
436    #[test]
437    fn max_data_len_prefix_does_not_panic_and_is_rejected() {
438        // data_len = u32::MAX. On a 32-bit target this used to overflow `offset +
439        // data_len` (unchecked usize addition): a debug build would panic (a
440        // network-reachable crash), a release build would wrap and proceed into a
441        // corrupted slice range. The fix uses checked_add and must return None
442        // uniformly, on every target width, without panicking either way — this test
443        // must pass identically under `cargo test` (checked arithmetic, debug or
444        // release) regardless of pointer width. (Also caught by the MAX_MESSAGE_SIZE
445        // cap now, but the bounds-check arithmetic itself must be overflow-safe
446        // independent of that cap — see the direct offset-overflow test below.)
447        let mut wire = vec![20u8, 0]; // msg_type=20, has_id=0
448        wire.extend_from_slice(&u32::MAX.to_be_bytes());
449        assert!(DigMessage::from_bytes(&wire).is_none());
450    }
451
452    #[test]
453    fn offset_plus_data_len_overflow_is_checked_not_wrapping() {
454        // Regression test for the raw `offset + data_len` addition itself (line 98 in
455        // the pre-fix code / the bounds check below the MAX_MESSAGE_SIZE cap): it must
456        // use checked arithmetic and return None on overflow rather than silently
457        // wrapping (which on a 32-bit usize could previously turn a huge data_len into
458        // a small wrapped value that passed the length check and then panicked on the
459        // slice index instead). usize::MAX stands in for "large enough to overflow
460        // offset + data_len on the current target's pointer width" — on 64-bit this
461        // value is not reachable via a real u32 data_len, so this test exercises the
462        // checked_add call path directly rather than only the u32-parsed case above.
463        let bytes = [20u8, 0, 0, 0, 0, 0]; // msg_type, has_id=0, data_len=0
464                                           // Sanity: normal zero-length message still decodes fine (no regression).
465        assert!(DigMessage::from_bytes(&bytes).is_some());
466
467        // The u32::MAX case above is the reachable-from-the-wire overflow probe; assert
468        // its result is a clean None (not a panic) for every build profile.
469        let mut wire = vec![1u8, 0];
470        wire.extend_from_slice(&u32::MAX.to_be_bytes());
471        let result = std::panic::catch_unwind(|| DigMessage::from_bytes(&wire));
472        assert!(
473            result.is_ok(),
474            "from_bytes must not panic on data_len = u32::MAX"
475        );
476        assert_eq!(result.unwrap(), None);
477    }
478
479    #[test]
480    fn data_len_one_byte_over_cap_is_rejected_at_exact_boundary() {
481        // Prove the cap is enforced as `> MAX_MESSAGE_SIZE`, not some looser bound, by
482        // checking the smallest possible over-cap value (cap + 1) is rejected even
483        // though the length-prefix parsing itself succeeds (only the cap check fails).
484        let over_by_one = (DigMessage::MAX_MESSAGE_SIZE as u32) + 1;
485        let mut wire = vec![1u8, 0];
486        wire.extend_from_slice(&over_by_one.to_be_bytes());
487        assert!(DigMessage::from_bytes(&wire).is_none());
488    }
489
490    #[test]
491    fn zero_length_data_round_trip() {
492        // data_len = 0 is a valid wire message (e.g. RegisterPeersIntroducer body).
493        let msg = DigMessage::new(64, Some(1), Bytes::default());
494        let wire = msg.to_bytes();
495        let decoded = DigMessage::from_bytes(&wire).expect("zero-length decode");
496        assert_eq!(decoded, msg);
497        assert!(decoded.data.as_ref().is_empty());
498    }
499
500    #[test]
501    fn from_chia_message_preserves_no_id() {
502        // Exercise the id == None branch of from_chia_message + the boundary opcode 199/200.
503        let chia_msg = Message {
504            msg_type: ProtocolMessageTypes::Handshake,
505            id: None,
506            data: Bytes::default(),
507        };
508        let dig = DigMessage::from_chia_message(&chia_msg);
509        assert_eq!(dig.id, None);
510        assert!(dig.is_chia_standard());
511        assert!(!dig.is_dig_extension());
512    }
513
514    #[test]
515    fn dig_extension_boundary_at_200() {
516        // 199 is the last Chia-standard value; 200 is the first DIG extension value.
517        let below = DigMessage::new(199, None, Bytes::default());
518        assert!(below.is_chia_standard());
519        assert!(!below.is_dig_extension());
520
521        let at = DigMessage::new(200, None, Bytes::default());
522        assert!(at.is_dig_extension());
523        assert!(!at.is_chia_standard());
524    }
525}