Skip to main content

dig_peer_protocol/
introducer_wire.rs

1//! Introducer wire types for both Chia-standard and DIG-extension opcodes.
2//!
3//! ## Chia-standard (opcodes 63/64)
4//!
5//! [`RequestPeersIntroducer`] and [`RespondPeersIntroducer`] use `#[streamable(message)]`
6//! because opcodes 63/64 exist in stock `ProtocolMessageTypes`. These work with
7//! `Peer::request_infallible` directly.
8//!
9//! ## DIG-extension (opcodes 218/219)
10//!
11//! [`RegisterPeer`] and [`RegisterAck`] are DIG-specific (DSC-005). Since opcodes 218/219
12//! don't exist in stock `ProtocolMessageTypes`, these structs implement `Streamable`
13//! manually and provide `to_dig_message`/`from_dig_message` helpers for wire encoding
14//! via [`DigMessage`] instead of the `ChiaProtocolMessage` trait.
15
16use chia_protocol::{NodeType, TimestampedPeerInfo};
17use chia_streamable_macro::streamable;
18use chia_traits::Streamable;
19
20use crate::dig_message::DigMessage;
21use crate::dig_message_type::DigMessageType;
22
23// ---------------------------------------------------------------------------
24// Chia-standard introducer types (opcodes 63/64)
25// ---------------------------------------------------------------------------
26
27/// Empty introducer "get peers" request (protocol opcode **63**).
28#[streamable(message)]
29pub struct RequestPeersIntroducer {}
30
31/// Introducer peer list response (protocol opcode **64**).
32#[streamable(message)]
33pub struct RespondPeersIntroducer {
34    peer_list: Vec<TimestampedPeerInfo>,
35}
36
37// ---------------------------------------------------------------------------
38// DIG-extension introducer types (opcodes 218/219)
39// ---------------------------------------------------------------------------
40
41/// Registration request: advertise this node's P2P reachability to the introducer.
42///
43/// Opcode **218** (`DigMessageType::RegisterPeer`). Not in stock `ProtocolMessageTypes`.
44/// Use [`RegisterPeer::to_dig_message`] to encode for wire send.
45#[streamable]
46pub struct RegisterPeer {
47    /// Externally reachable IP or hostname.
48    ip: String,
49    /// P2P listening port.
50    port: u16,
51    /// Declared service role — gossip nodes register as `NodeType::FullNode`.
52    node_type: NodeType,
53}
54
55/// Introducer acknowledgement. `success == false` is a valid wire outcome (policy rejection).
56///
57/// Opcode **219** (`DigMessageType::RegisterAck`).
58#[streamable]
59pub struct RegisterAck {
60    success: bool,
61}
62
63impl RegisterPeer {
64    /// Encode as a [`DigMessage`] with opcode 218 and the given correlation `id`.
65    pub fn to_dig_message(&self, id: Option<u16>) -> Result<DigMessage, chia_traits::Error> {
66        let data = self.to_bytes()?;
67        Ok(DigMessage::new(
68            DigMessageType::RegisterPeer as u8,
69            id,
70            data.into(),
71        ))
72    }
73
74    /// Decode from a [`DigMessage`]. Returns `None` if opcode is not 218.
75    pub fn from_dig_message(msg: &DigMessage) -> Option<Result<Self, chia_traits::Error>> {
76        if msg.msg_type != DigMessageType::RegisterPeer as u8 {
77            return None;
78        }
79        Some(Self::from_bytes(&msg.data))
80    }
81}
82
83impl RegisterAck {
84    /// Encode as a [`DigMessage`] with opcode 219 and the given correlation `id`.
85    pub fn to_dig_message(&self, id: Option<u16>) -> Result<DigMessage, chia_traits::Error> {
86        let data = self.to_bytes()?;
87        Ok(DigMessage::new(
88            DigMessageType::RegisterAck as u8,
89            id,
90            data.into(),
91        ))
92    }
93
94    /// Decode from a [`DigMessage`]. Returns `None` if opcode is not 219.
95    pub fn from_dig_message(msg: &DigMessage) -> Option<Result<Self, chia_traits::Error>> {
96        if msg.msg_type != DigMessageType::RegisterAck as u8 {
97            return None;
98        }
99        Some(Self::from_bytes(&msg.data))
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn register_peer_round_trip() {
109        let rp = RegisterPeer::new("192.168.1.1".into(), 9444, NodeType::FullNode);
110        let msg = rp.to_dig_message(Some(42)).expect("encode");
111        assert_eq!(msg.msg_type, 218);
112        assert_eq!(msg.id, Some(42));
113
114        let decoded = RegisterPeer::from_dig_message(&msg)
115            .expect("correct opcode")
116            .expect("decode");
117        assert_eq!(decoded.ip, "192.168.1.1");
118        assert_eq!(decoded.port, 9444);
119        assert_eq!(decoded.node_type, NodeType::FullNode);
120    }
121
122    #[test]
123    fn register_ack_round_trip() {
124        let ack = RegisterAck::new(true);
125        let msg = ack.to_dig_message(None).expect("encode");
126        assert_eq!(msg.msg_type, 219);
127        assert_eq!(msg.id, None);
128
129        let decoded = RegisterAck::from_dig_message(&msg)
130            .expect("correct opcode")
131            .expect("decode");
132        assert!(decoded.success);
133    }
134
135    #[test]
136    fn wrong_opcode_returns_none() {
137        let msg = DigMessage::new(200, None, chia_protocol::Bytes::default());
138        assert!(RegisterPeer::from_dig_message(&msg).is_none());
139        assert!(RegisterAck::from_dig_message(&msg).is_none());
140    }
141
142    #[test]
143    fn request_peers_introducer_streamable() {
144        let req = RequestPeersIntroducer::new();
145        let bytes = req.to_bytes().expect("encode");
146        let _back = RequestPeersIntroducer::from_bytes(&bytes).expect("decode");
147    }
148
149    #[test]
150    fn respond_peers_introducer_streamable() {
151        let resp = RespondPeersIntroducer::new(vec![]);
152        let bytes = resp.to_bytes().expect("encode");
153        let back = RespondPeersIntroducer::from_bytes(&bytes).expect("decode");
154        assert!(back.peer_list.is_empty());
155    }
156
157    #[test]
158    fn respond_peers_introducer_with_populated_list_round_trips() {
159        // A non-empty peer_list exercises the Vec<TimestampedPeerInfo> serialization path.
160        // TimestampedPeerInfo fields are private to chia-protocol, so build via the
161        // generated `new()` and compare by derived equality rather than field access.
162        let peers = vec![
163            TimestampedPeerInfo::new("203.0.113.7".into(), 9444, 1_700_000_000),
164            TimestampedPeerInfo::new("198.51.100.42".into(), 18444, 1_700_000_500),
165        ];
166        let resp = RespondPeersIntroducer::new(peers.clone());
167        let bytes = resp.to_bytes().expect("encode");
168        let back = RespondPeersIntroducer::from_bytes(&bytes).expect("decode");
169        assert_eq!(back.peer_list.len(), 2);
170        assert_eq!(back.peer_list, peers);
171        // A different list must NOT compare equal (guards against a trivial all-pass).
172        let other = vec![TimestampedPeerInfo::new("10.0.0.1".into(), 1, 1)];
173        assert_ne!(back.peer_list, other);
174    }
175
176    #[test]
177    fn register_peer_from_dig_message_decode_error() {
178        // Correct opcode (218) but a corrupt/truncated body: the opcode check passes
179        // (Some(..)), so the inner Streamable::from_bytes returns Err — covering the
180        // decode-error arm rather than the wrong-opcode None arm.
181        let bad = DigMessage::new(
182            DigMessageType::RegisterPeer as u8,
183            None,
184            chia_protocol::Bytes::new(vec![0xFF]), // not a valid RegisterPeer encoding
185        );
186        let result = RegisterPeer::from_dig_message(&bad);
187        let inner = result.expect("opcode matched, so we get Some(..)");
188        assert!(inner.is_err(), "corrupt body must surface a decode Err");
189    }
190
191    #[test]
192    fn register_ack_from_dig_message_decode_error() {
193        // Same path for RegisterAck (opcode 219) with an empty body — a bool needs 1 byte.
194        let bad = DigMessage::new(
195            DigMessageType::RegisterAck as u8,
196            None,
197            chia_protocol::Bytes::default(),
198        );
199        let result = RegisterAck::from_dig_message(&bad);
200        let inner = result.expect("opcode matched, so we get Some(..)");
201        assert!(
202            inner.is_err(),
203            "empty body must surface a decode Err for a bool field"
204        );
205    }
206
207    #[test]
208    fn register_ack_false_round_trips() {
209        // success == false is an explicit valid wire outcome (policy rejection).
210        let ack = RegisterAck::new(false);
211        let msg = ack.to_dig_message(Some(9)).expect("encode");
212        assert_eq!(msg.msg_type, 219);
213        assert_eq!(msg.id, Some(9));
214        let decoded = RegisterAck::from_dig_message(&msg)
215            .expect("correct opcode")
216            .expect("decode");
217        assert!(!decoded.success);
218    }
219}