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