Skip to main content

zakura_network/protocol/external/
message.rs

1//! Definitions of network messages.
2
3use std::{error::Error, fmt, sync::Arc};
4
5use chrono::{DateTime, Utc};
6
7use zakura_chain::{
8    block::{self, Block},
9    transaction::UnminedTx,
10};
11
12use crate::{meta_addr::MetaAddr, BoxError};
13
14use super::{addr::AddrInVersion, inv::InventoryHash, types::*};
15
16#[cfg(any(test, feature = "proptest-impl"))]
17use proptest_derive::Arbitrary;
18
19#[cfg(any(test, feature = "proptest-impl"))]
20use zakura_chain::serialization::arbitrary::datetime_full;
21
22/// A Bitcoin-like network message for the Zcash protocol.
23///
24/// The Zcash network protocol is mostly inherited from Bitcoin, and a list of
25/// Bitcoin network messages can be found [on the Bitcoin
26/// wiki][btc_wiki_protocol].
27///
28/// That page describes the wire format of the messages, while this enum stores
29/// an internal representation. The internal representation is unlinked from the
30/// wire format, and the translation between the two happens only during
31/// serialization and deserialization. For instance, Bitcoin identifies messages
32/// by a 12-byte ascii command string; we consider this a serialization detail
33/// and use the enum discriminant instead. (As a side benefit, this also means
34/// that we have a clearly-defined validation boundary for network messages
35/// during serialization).
36///
37/// [btc_wiki_protocol]: https://en.bitcoin.it/wiki/Protocol_documentation
38#[derive(Clone, Eq, PartialEq, Debug)]
39#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
40pub enum Message {
41    /// A `version` message.
42    ///
43    /// Note that although this is called `version` in Bitcoin, its role is really
44    /// analogous to a `ClientHello` message in TLS, used to begin a handshake, and
45    /// is distinct from a simple version number.
46    ///
47    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#version)
48    Version(VersionMessage),
49
50    /// A `verack` message.
51    ///
52    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#verack)
53    Verack,
54
55    /// A `ping` message.
56    ///
57    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#ping)
58    Ping(
59        /// A nonce unique to this [`Self::Ping`] message.
60        Nonce,
61    ),
62
63    /// A `pong` message.
64    ///
65    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#pong)
66    Pong(
67        /// The nonce from the [`Self::Ping`] message this was in response to.
68        Nonce,
69    ),
70
71    /// A `reject` message.
72    ///
73    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#reject)
74    Reject {
75        /// Type of message rejected.
76        // It's unclear if this is strictly limited to message command
77        // codes, so leaving it a String.
78        message: String,
79
80        /// RejectReason code relating to rejected message.
81        ccode: RejectReason,
82
83        /// Human-readable version of rejection reason.
84        reason: String,
85
86        /// Optional extra data provided for some errors.
87        // Currently, all errors which provide this field fill it with
88        // the TXID or block header hash of the object being rejected,
89        // so the field is 32 bytes.
90        data: Option<[u8; 32]>,
91    },
92
93    /// A bounded Zakura P2P v2 legacy upgrade prelude payload.
94    ///
95    /// This command is only valid after both peers advertised `NODE_P2P_V2` and
96    /// completed `version`/`verack`. The inner payload is decoded by the Zakura
97    /// upgrade layer, keeping the legacy codec command-aware but protocol-neutral.
98    P2pV2Upgrade(Vec<u8>),
99
100    /// A `getaddr` message.
101    ///
102    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getaddr)
103    GetAddr,
104
105    /// A sent or received `addr` message, or a received `addrv2` message.
106    ///
107    /// Currently, Zebra:
108    /// - sends and receives `addr` messages,
109    /// - parses received `addrv2` messages, ignoring some address types,
110    /// - but does not send `addrv2` messages.
111    ///
112    ///
113    /// The list contains `0..=MAX_META_ADDR` addresses.
114    ///
115    /// Because some address types are ignored, the deserialized vector can be empty,
116    /// even if the peer sent addresses. This is not an error.
117    ///
118    /// [addr Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#addr)
119    /// [addrv2 ZIP 155](https://zips.z.cash/zip-0155#specification)
120    Addr(Vec<MetaAddr>),
121
122    /// A `getblocks` message.
123    ///
124    /// `known_blocks` is a series of known block hashes spaced out along the
125    /// peer's best chain. The remote peer uses them to compute the intersection
126    /// of its best chain and determine the blocks following the intersection
127    /// point.
128    ///
129    /// The peer responds with an `inv` packet with the hashes of subsequent blocks.
130    /// If supplied, the `stop` parameter specifies the last header to request.
131    /// Otherwise, an inv packet with the maximum number (500) are sent.
132    ///
133    /// The known blocks list contains zero or more block hashes.
134    ///
135    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getheaders)
136    GetBlocks {
137        /// Hashes of known blocks, ordered from highest height to lowest height.
138        known_blocks: Vec<block::Hash>,
139        /// Optionally, the last header to request.
140        stop: Option<block::Hash>,
141    },
142
143    /// An `inv` message.
144    ///
145    /// Allows a node to advertise its knowledge of one or more
146    /// objects. It can be received unsolicited, or in reply to
147    /// `getblocks`.
148    ///
149    /// The list contains zero or more inventory hashes.
150    ///
151    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#inv)
152    /// [ZIP-239](https://zips.z.cash/zip-0239)
153    Inv(Vec<InventoryHash>),
154
155    /// A `getheaders` message.
156    ///
157    /// `known_blocks` is a series of known block hashes spaced out along the
158    /// peer's best chain. The remote peer uses them to compute the intersection
159    /// of its best chain and determine the blocks following the intersection
160    /// point.
161    ///
162    /// The peer responds with an `headers` packet with the headers of subsequent blocks.
163    /// If supplied, the `stop` parameter specifies the last header to request.
164    /// Otherwise, the maximum number of block headers (160) are sent.
165    ///
166    /// The known blocks list contains zero or more block hashes.
167    ///
168    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getheaders)
169    GetHeaders {
170        /// Hashes of known blocks, ordered from highest height to lowest height.
171        known_blocks: Vec<block::Hash>,
172        /// Optionally, the last header to request.
173        stop: Option<block::Hash>,
174    },
175
176    /// A `headers` message.
177    ///
178    /// Returns block headers in response to a getheaders packet.
179    ///
180    /// Each block header is accompanied by a transaction count.
181    ///
182    /// The list contains zero or more headers.
183    ///
184    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#headers)
185    Headers(Vec<block::CountedHeader>),
186
187    /// A `getdata` message.
188    ///
189    /// `getdata` is used in response to `inv`, to retrieve the
190    /// content of a specific object, and is usually sent after
191    /// receiving an `inv` packet, after filtering known elements.
192    ///
193    /// `zcashd` returns requested items in a single batch of messages.
194    /// Missing blocks are silently skipped. Missing transaction hashes are
195    /// included in a single `notfound` message following the transactions.
196    /// Other item or non-item messages can come before or after the batch.
197    ///
198    /// The list contains zero or more inventory hashes.
199    ///
200    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getdata)
201    /// [ZIP-239](https://zips.z.cash/zip-0239)
202    /// [zcashd code](https://github.com/zcash/zcash/blob/e7b425298f6d9a54810cb7183f00be547e4d9415/src/main.cpp#L5523)
203    GetData(Vec<InventoryHash>),
204
205    /// A `block` message.
206    ///
207    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#block)
208    Block(Arc<Block>),
209
210    /// A `tx` message.
211    ///
212    /// This message can be used to:
213    /// - send unmined transactions in response to `GetData` requests, and
214    /// - advertise unmined transactions for the mempool.
215    ///
216    /// Zebra chooses to advertise new transactions using `Inv(hash)` rather than `Tx(transaction)`.
217    ///
218    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#tx)
219    Tx(UnminedTx),
220
221    /// A `notfound` message.
222    ///
223    /// Zebra responds with this message when it doesn't have the requested blocks or transactions.
224    ///
225    /// When a peer requests a list of transaction hashes, `zcashd` returns:
226    ///   - a batch of messages containing found transactions, then
227    ///   - a `notfound` message containing a list of transaction hashes that
228    ///     aren't available in its mempool or state.
229    ///
230    /// But when a peer requests blocks or headers, any missing items are
231    /// silently skipped, without any `notfound` messages.
232    ///
233    /// The list contains zero or more inventory hashes.
234    ///
235    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#notfound)
236    /// [ZIP-239](https://zips.z.cash/zip-0239)
237    /// [zcashd code](https://github.com/zcash/zcash/blob/e7b425298f6d9a54810cb7183f00be547e4d9415/src/main.cpp#L5632)
238    // See note above on `Inventory`.
239    NotFound(Vec<InventoryHash>),
240
241    /// A `mempool` message.
242    ///
243    /// This was defined in [BIP35], which is included in Zcash.
244    ///
245    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#mempool)
246    ///
247    /// [BIP35]: https://github.com/bitcoin/bips/blob/master/bip-0035.mediawiki
248    Mempool,
249}
250
251/// The maximum size of the user agent string.
252///
253/// This is equivalent to `MAX_SUBVERSION_LENGTH` in `zcashd`:
254/// <https://github.com/zcash/zcash/blob/adfc7218435faa1c8985a727f997a795dcffa0c7/src/net.h#L56>
255pub const MAX_USER_AGENT_LENGTH: usize = 256;
256
257/// A `version` message.
258///
259/// Note that although this is called `version` in Bitcoin, its role is really
260/// analogous to a `ClientHello` message in TLS, used to begin a handshake, and
261/// is distinct from a simple version number.
262///
263/// This struct provides a type that is guaranteed to be a `version` message,
264/// and allows [`Message::Version`](Message) fields to be accessed directly.
265///
266/// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#version)
267#[derive(Clone, Eq, PartialEq, Debug)]
268#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
269pub struct VersionMessage {
270    /// The network version number supported by the sender.
271    pub version: Version,
272
273    /// The network services advertised by the sender.
274    pub services: PeerServices,
275
276    /// The time when the version message was sent.
277    ///
278    /// This is a 64-bit field. Zebra rejects out-of-range times as invalid.
279    ///
280    /// TODO: replace with a custom DateTime64 type (#2171)
281    #[cfg_attr(
282        any(test, feature = "proptest-impl"),
283        proptest(strategy = "datetime_full()")
284    )]
285    pub timestamp: DateTime<Utc>,
286
287    /// The network address of the node receiving this message, and its
288    /// advertised network services.
289    ///
290    /// Q: how does the handshake know the remote peer's services already?
291    pub address_recv: AddrInVersion,
292
293    /// The network address of the node sending this message, and its
294    /// advertised network services.
295    pub address_from: AddrInVersion,
296
297    /// Node random nonce, randomly generated every time a version
298    /// packet is sent. This nonce is used to detect connections
299    /// to self.
300    pub nonce: Nonce,
301
302    /// The Zcash user agent advertised by the sender.
303    pub user_agent: String,
304
305    /// The last block received by the emitting node.
306    pub start_height: block::Height,
307
308    /// Whether the remote peer should announce relayed
309    /// transactions or not, see [BIP 0037].
310    ///
311    /// Zebra does not implement the bloom filters in [BIP 0037].
312    /// Instead, it only relays:
313    /// - newly verified best chain block hashes and mempool transaction IDs,
314    /// - after it reaches the chain tip.
315    ///
316    /// [BIP 0037]: https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
317    pub relay: bool,
318}
319
320/// The maximum size of the rejection message.
321///
322/// This is equivalent to `COMMAND_SIZE` in zcashd:
323/// <https://github.com/zcash/zcash/blob/adfc7218435faa1c8985a727f997a795dcffa0c7/src/protocol.h#L33>
324/// <https://github.com/zcash/zcash/blob/c0fbeb809bf2303e30acef0d2b74db11e9177427/src/main.cpp#L7544>
325pub const MAX_REJECT_MESSAGE_LENGTH: usize = 12;
326
327/// The maximum size of the rejection reason.
328///
329/// This is equivalent to `MAX_REJECT_MESSAGE_LENGTH` in zcashd:
330/// <https://github.com/zcash/zcash/blob/adfc7218435faa1c8985a727f997a795dcffa0c7/src/main.h#L126>
331/// <https://github.com/zcash/zcash/blob/c0fbeb809bf2303e30acef0d2b74db11e9177427/src/main.cpp#L7544>
332pub const MAX_REJECT_REASON_LENGTH: usize = 111;
333
334impl From<VersionMessage> for Message {
335    fn from(version_message: VersionMessage) -> Self {
336        Message::Version(version_message)
337    }
338}
339
340impl TryFrom<Message> for VersionMessage {
341    type Error = BoxError;
342
343    fn try_from(message: Message) -> Result<Self, Self::Error> {
344        match message {
345            Message::Version(version_message) => Ok(version_message),
346            _ => Err(format!(
347                "{} message is not a version message: {message:?}",
348                message.command()
349            )
350            .into()),
351        }
352    }
353}
354
355// TODO: add tests for Error conversion and Reject message serialization
356// (Zebra does not currently send reject messages, and it ignores received reject messages.)
357impl<E> From<E> for Message
358where
359    E: Error,
360{
361    fn from(e: E) -> Self {
362        let message = e
363            .to_string()
364            .escape_default()
365            .take(MAX_REJECT_MESSAGE_LENGTH)
366            .collect();
367        let reason = e
368            .source()
369            .map(ToString::to_string)
370            .unwrap_or_default()
371            .escape_default()
372            .take(MAX_REJECT_REASON_LENGTH)
373            .collect();
374
375        Message::Reject {
376            message,
377
378            // The generic case, impls for specific error types should
379            // use specific varieties of `RejectReason`.
380            ccode: RejectReason::Other,
381
382            reason,
383
384            // The hash of the rejected block or transaction.
385            // We don't have that data here, so the caller needs to fill it in later.
386            data: None,
387        }
388    }
389}
390
391/// Reject Reason CCodes
392///
393/// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#reject)
394#[derive(Copy, Clone, Debug, Eq, PartialEq)]
395#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
396#[repr(u8)]
397#[allow(missing_docs)]
398pub enum RejectReason {
399    Malformed = 0x01,
400    Invalid = 0x10,
401    Obsolete = 0x11,
402    Duplicate = 0x12,
403    Nonstandard = 0x40,
404    Dust = 0x41,
405    InsufficientFee = 0x42,
406    Checkpoint = 0x43,
407    Other = 0x50,
408}
409
410impl fmt::Display for Message {
411    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
412        f.write_str(&match self {
413            Message::Version(VersionMessage {
414                version,
415                address_recv,
416                address_from,
417                user_agent,
418                ..
419            }) => format!(
420                "version {{ network: {}, recv: {},_from: {}, user_agent: {:?} }}",
421                version,
422                address_recv.addr(),
423                address_from.addr(),
424                user_agent,
425            ),
426            Message::Verack => "verack".to_string(),
427
428            Message::Ping(_) => "ping".to_string(),
429            Message::Pong(_) => "pong".to_string(),
430
431            Message::Reject {
432                message,
433                reason,
434                data,
435                ..
436            } => format!(
437                "reject {{ message: {:?}, reason: {:?}, data: {} }}",
438                message,
439                reason,
440                if data.is_some() { "Some" } else { "None" },
441            ),
442            Message::P2pV2Upgrade(payload) => {
443                format!("p2pv2up {{ bytes: {} }}", payload.len())
444            }
445
446            Message::GetAddr => "getaddr".to_string(),
447            Message::Addr(addrs) => format!("addr {{ addrs: {} }}", addrs.len()),
448
449            Message::GetBlocks { known_blocks, stop } => format!(
450                "getblocks {{ known_blocks: {}, stop: {} }}",
451                known_blocks.len(),
452                if stop.is_some() { "Some" } else { "None" },
453            ),
454            Message::Inv(invs) => format!("inv {{ invs: {} }}", invs.len()),
455
456            Message::GetHeaders { known_blocks, stop } => format!(
457                "getheaders {{ known_blocks: {}, stop: {} }}",
458                known_blocks.len(),
459                if stop.is_some() { "Some" } else { "None" },
460            ),
461            Message::Headers(headers) => format!("headers {{ headers: {} }}", headers.len()),
462
463            Message::GetData(invs) => format!("getdata {{ invs: {} }}", invs.len()),
464            Message::Block(block) => format!(
465                "block {{ height: {}, hash: {} }}",
466                block
467                    .coinbase_height()
468                    .as_ref()
469                    .map(|h| h.0.to_string())
470                    .unwrap_or_else(|| "None".into()),
471                block.hash(),
472            ),
473            Message::Tx(_) => "tx".to_string(),
474            Message::NotFound(invs) => format!("notfound {{ invs: {} }}", invs.len()),
475
476            Message::Mempool => "mempool".to_string(),
477        })
478    }
479}
480
481impl Message {
482    /// Returns the Zcash protocol message command as a string.
483    pub fn command(&self) -> &'static str {
484        match self {
485            Message::Version(_) => "version",
486            Message::Verack => "verack",
487            Message::Ping(_) => "ping",
488            Message::Pong(_) => "pong",
489            Message::Reject { .. } => "reject",
490            Message::P2pV2Upgrade(_) => "p2pv2up",
491            Message::GetAddr => "getaddr",
492            Message::Addr(_) => "addr",
493            Message::GetBlocks { .. } => "getblocks",
494            Message::Inv(_) => "inv",
495            Message::GetHeaders { .. } => "getheaders",
496            Message::Headers(_) => "headers",
497            Message::GetData(_) => "getdata",
498            Message::Block(_) => "block",
499            Message::Tx(_) => "tx",
500            Message::NotFound(_) => "notfound",
501            Message::Mempool => "mempool",
502        }
503    }
504}