zebra_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 zebra_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 zebra_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 `getaddr` message.
94    ///
95    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getaddr)
96    GetAddr,
97
98    /// A sent or received `addr` message, or a received `addrv2` message.
99    ///
100    /// Currently, Zebra:
101    /// - sends and receives `addr` messages,
102    /// - parses received `addrv2` messages, ignoring some address types,
103    /// - but does not send `addrv2` messages.
104    ///
105    ///
106    /// The list contains `0..=MAX_META_ADDR` addresses.
107    ///
108    /// Because some address types are ignored, the deserialized vector can be empty,
109    /// even if the peer sent addresses. This is not an error.
110    ///
111    /// [addr Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#addr)
112    /// [addrv2 ZIP 155](https://zips.z.cash/zip-0155#specification)
113    Addr(Vec<MetaAddr>),
114
115    /// A `getblocks` message.
116    ///
117    /// `known_blocks` is a series of known block hashes spaced out along the
118    /// peer's best chain. The remote peer uses them to compute the intersection
119    /// of its best chain and determine the blocks following the intersection
120    /// point.
121    ///
122    /// The peer responds with an `inv` packet with the hashes of subsequent blocks.
123    /// If supplied, the `stop` parameter specifies the last header to request.
124    /// Otherwise, an inv packet with the maximum number (500) are sent.
125    ///
126    /// The known blocks list contains zero or more block hashes.
127    ///
128    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getheaders)
129    GetBlocks {
130        /// Hashes of known blocks, ordered from highest height to lowest height.
131        known_blocks: Vec<block::Hash>,
132        /// Optionally, the last header to request.
133        stop: Option<block::Hash>,
134    },
135
136    /// An `inv` message.
137    ///
138    /// Allows a node to advertise its knowledge of one or more
139    /// objects. It can be received unsolicited, or in reply to
140    /// `getblocks`.
141    ///
142    /// The list contains zero or more inventory hashes.
143    ///
144    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#inv)
145    /// [ZIP-239](https://zips.z.cash/zip-0239)
146    Inv(Vec<InventoryHash>),
147
148    /// A `getheaders` message.
149    ///
150    /// `known_blocks` is a series of known block hashes spaced out along the
151    /// peer's best chain. The remote peer uses them to compute the intersection
152    /// of its best chain and determine the blocks following the intersection
153    /// point.
154    ///
155    /// The peer responds with an `headers` packet with the headers of subsequent blocks.
156    /// If supplied, the `stop` parameter specifies the last header to request.
157    /// Otherwise, the maximum number of block headers (160) are sent.
158    ///
159    /// The known blocks list contains zero or more block hashes.
160    ///
161    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getheaders)
162    GetHeaders {
163        /// Hashes of known blocks, ordered from highest height to lowest height.
164        known_blocks: Vec<block::Hash>,
165        /// Optionally, the last header to request.
166        stop: Option<block::Hash>,
167    },
168
169    /// A `headers` message.
170    ///
171    /// Returns block headers in response to a getheaders packet.
172    ///
173    /// Each block header is accompanied by a transaction count.
174    ///
175    /// The list contains zero or more headers.
176    ///
177    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#headers)
178    Headers(Vec<block::CountedHeader>),
179
180    /// A `getdata` message.
181    ///
182    /// `getdata` is used in response to `inv`, to retrieve the
183    /// content of a specific object, and is usually sent after
184    /// receiving an `inv` packet, after filtering known elements.
185    ///
186    /// `zcashd` returns requested items in a single batch of messages.
187    /// Missing blocks are silently skipped. Missing transaction hashes are
188    /// included in a single `notfound` message following the transactions.
189    /// Other item or non-item messages can come before or after the batch.
190    ///
191    /// The list contains zero or more inventory hashes.
192    ///
193    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getdata)
194    /// [ZIP-239](https://zips.z.cash/zip-0239)
195    /// [zcashd code](https://github.com/zcash/zcash/blob/e7b425298f6d9a54810cb7183f00be547e4d9415/src/main.cpp#L5523)
196    GetData(Vec<InventoryHash>),
197
198    /// A `block` message.
199    ///
200    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#block)
201    Block(Arc<Block>),
202
203    /// A `tx` message.
204    ///
205    /// This message can be used to:
206    /// - send unmined transactions in response to `GetData` requests, and
207    /// - advertise unmined transactions for the mempool.
208    ///
209    /// Zebra chooses to advertise new transactions using `Inv(hash)` rather than `Tx(transaction)`.
210    ///
211    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#tx)
212    Tx(UnminedTx),
213
214    /// A `notfound` message.
215    ///
216    /// Zebra responds with this message when it doesn't have the requested blocks or transactions.
217    ///
218    /// When a peer requests a list of transaction hashes, `zcashd` returns:
219    ///   - a batch of messages containing found transactions, then
220    ///   - a `notfound` message containing a list of transaction hashes that
221    ///     aren't available in its mempool or state.
222    ///
223    /// But when a peer requests blocks or headers, any missing items are
224    /// silently skipped, without any `notfound` messages.
225    ///
226    /// The list contains zero or more inventory hashes.
227    ///
228    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#notfound)
229    /// [ZIP-239](https://zips.z.cash/zip-0239)
230    /// [zcashd code](https://github.com/zcash/zcash/blob/e7b425298f6d9a54810cb7183f00be547e4d9415/src/main.cpp#L5632)
231    // See note above on `Inventory`.
232    NotFound(Vec<InventoryHash>),
233
234    /// A `mempool` message.
235    ///
236    /// This was defined in [BIP35], which is included in Zcash.
237    ///
238    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#mempool)
239    ///
240    /// [BIP35]: https://github.com/bitcoin/bips/blob/master/bip-0035.mediawiki
241    Mempool,
242
243    /// A `filterload` message.
244    ///
245    /// This was defined in [BIP37], which is included in Zcash.
246    ///
247    /// Zebra currently ignores this message.
248    ///
249    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#filterload.2C_filteradd.2C_filterclear.2C_merkleblock)
250    ///
251    /// [BIP37]: https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
252    FilterLoad {
253        /// The filter itself is simply a bit field of arbitrary
254        /// byte-aligned size. The maximum size is 36,000 bytes.
255        filter: Filter,
256
257        /// The number of hash functions to use in this filter. The
258        /// maximum value allowed in this field is 50.
259        hash_functions_count: u32,
260
261        /// A random value to add to the seed value in the hash
262        /// function used by the bloom filter.
263        tweak: Tweak,
264
265        /// A set of flags that control how matched items are added to the filter.
266        flags: u8,
267    },
268
269    /// A `filteradd` message.
270    ///
271    /// This was defined in [BIP37], which is included in Zcash.
272    ///
273    /// Zebra currently ignores this message.
274    ///
275    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#filterload.2C_filteradd.2C_filterclear.2C_merkleblock)
276    ///
277    /// [BIP37]: https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
278    FilterAdd {
279        /// The data element to add to the current filter.
280        // The data field must be smaller than or equal to 520 bytes
281        // in size (the maximum size of any potentially matched
282        // object).
283        //
284        // A Vec instead of [u8; 520] because of needed traits.
285        data: Vec<u8>,
286    },
287
288    /// A `filterclear` message.
289    ///
290    /// This was defined in [BIP37], which is included in Zcash.
291    ///
292    /// Zebra currently ignores this message.
293    ///
294    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#filterload.2C_filteradd.2C_filterclear.2C_merkleblock)
295    ///
296    /// [BIP37]: https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
297    FilterClear,
298}
299
300/// The maximum size of the user agent string.
301///
302/// This is equivalent to `MAX_SUBVERSION_LENGTH` in `zcashd`:
303/// <https://github.com/zcash/zcash/blob/adfc7218435faa1c8985a727f997a795dcffa0c7/src/net.h#L56>
304pub const MAX_USER_AGENT_LENGTH: usize = 256;
305
306/// A `version` message.
307///
308/// Note that although this is called `version` in Bitcoin, its role is really
309/// analogous to a `ClientHello` message in TLS, used to begin a handshake, and
310/// is distinct from a simple version number.
311///
312/// This struct provides a type that is guaranteed to be a `version` message,
313/// and allows [`Message::Version`](Message) fields to be accessed directly.
314///
315/// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#version)
316#[derive(Clone, Eq, PartialEq, Debug)]
317#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
318pub struct VersionMessage {
319    /// The network version number supported by the sender.
320    pub version: Version,
321
322    /// The network services advertised by the sender.
323    pub services: PeerServices,
324
325    /// The time when the version message was sent.
326    ///
327    /// This is a 64-bit field. Zebra rejects out-of-range times as invalid.
328    ///
329    /// TODO: replace with a custom DateTime64 type (#2171)
330    #[cfg_attr(
331        any(test, feature = "proptest-impl"),
332        proptest(strategy = "datetime_full()")
333    )]
334    pub timestamp: DateTime<Utc>,
335
336    /// The network address of the node receiving this message, and its
337    /// advertised network services.
338    ///
339    /// Q: how does the handshake know the remote peer's services already?
340    pub address_recv: AddrInVersion,
341
342    /// The network address of the node sending this message, and its
343    /// advertised network services.
344    pub address_from: AddrInVersion,
345
346    /// Node random nonce, randomly generated every time a version
347    /// packet is sent. This nonce is used to detect connections
348    /// to self.
349    pub nonce: Nonce,
350
351    /// The Zcash user agent advertised by the sender.
352    pub user_agent: String,
353
354    /// The last block received by the emitting node.
355    pub start_height: block::Height,
356
357    /// Whether the remote peer should announce relayed
358    /// transactions or not, see [BIP 0037].
359    ///
360    /// Zebra does not implement the bloom filters in [BIP 0037].
361    /// Instead, it only relays:
362    /// - newly verified best chain block hashes and mempool transaction IDs,
363    /// - after it reaches the chain tip.
364    ///
365    /// [BIP 0037]: https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
366    pub relay: bool,
367}
368
369/// The maximum size of the rejection message.
370///
371/// This is equivalent to `COMMAND_SIZE` in zcashd:
372/// <https://github.com/zcash/zcash/blob/adfc7218435faa1c8985a727f997a795dcffa0c7/src/protocol.h#L33>
373/// <https://github.com/zcash/zcash/blob/c0fbeb809bf2303e30acef0d2b74db11e9177427/src/main.cpp#L7544>
374pub const MAX_REJECT_MESSAGE_LENGTH: usize = 12;
375
376/// The maximum size of the rejection reason.
377///
378/// This is equivalent to `MAX_REJECT_MESSAGE_LENGTH` in zcashd:
379/// <https://github.com/zcash/zcash/blob/adfc7218435faa1c8985a727f997a795dcffa0c7/src/main.h#L126>
380/// <https://github.com/zcash/zcash/blob/c0fbeb809bf2303e30acef0d2b74db11e9177427/src/main.cpp#L7544>
381pub const MAX_REJECT_REASON_LENGTH: usize = 111;
382
383impl From<VersionMessage> for Message {
384    fn from(version_message: VersionMessage) -> Self {
385        Message::Version(version_message)
386    }
387}
388
389impl TryFrom<Message> for VersionMessage {
390    type Error = BoxError;
391
392    fn try_from(message: Message) -> Result<Self, Self::Error> {
393        match message {
394            Message::Version(version_message) => Ok(version_message),
395            _ => Err(format!(
396                "{} message is not a version message: {message:?}",
397                message.command()
398            )
399            .into()),
400        }
401    }
402}
403
404// TODO: add tests for Error conversion and Reject message serialization
405// (Zebra does not currently send reject messages, and it ignores received reject messages.)
406impl<E> From<E> for Message
407where
408    E: Error,
409{
410    fn from(e: E) -> Self {
411        let message = e
412            .to_string()
413            .escape_default()
414            .take(MAX_REJECT_MESSAGE_LENGTH)
415            .collect();
416        let reason = e
417            .source()
418            .map(ToString::to_string)
419            .unwrap_or_default()
420            .escape_default()
421            .take(MAX_REJECT_REASON_LENGTH)
422            .collect();
423
424        Message::Reject {
425            message,
426
427            // The generic case, impls for specific error types should
428            // use specific varieties of `RejectReason`.
429            ccode: RejectReason::Other,
430
431            reason,
432
433            // The hash of the rejected block or transaction.
434            // We don't have that data here, so the caller needs to fill it in later.
435            data: None,
436        }
437    }
438}
439
440/// Reject Reason CCodes
441///
442/// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#reject)
443#[derive(Copy, Clone, Debug, Eq, PartialEq)]
444#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
445#[repr(u8)]
446#[allow(missing_docs)]
447pub enum RejectReason {
448    Malformed = 0x01,
449    Invalid = 0x10,
450    Obsolete = 0x11,
451    Duplicate = 0x12,
452    Nonstandard = 0x40,
453    Dust = 0x41,
454    InsufficientFee = 0x42,
455    Checkpoint = 0x43,
456    Other = 0x50,
457}
458
459impl fmt::Display for Message {
460    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
461        f.write_str(&match self {
462            Message::Version(VersionMessage {
463                version,
464                address_recv,
465                address_from,
466                user_agent,
467                ..
468            }) => format!(
469                "version {{ network: {}, recv: {},_from: {}, user_agent: {:?} }}",
470                version,
471                address_recv.addr(),
472                address_from.addr(),
473                user_agent,
474            ),
475            Message::Verack => "verack".to_string(),
476
477            Message::Ping(_) => "ping".to_string(),
478            Message::Pong(_) => "pong".to_string(),
479
480            Message::Reject {
481                message,
482                reason,
483                data,
484                ..
485            } => format!(
486                "reject {{ message: {:?}, reason: {:?}, data: {} }}",
487                message,
488                reason,
489                if data.is_some() { "Some" } else { "None" },
490            ),
491
492            Message::GetAddr => "getaddr".to_string(),
493            Message::Addr(addrs) => format!("addr {{ addrs: {} }}", addrs.len()),
494
495            Message::GetBlocks { known_blocks, stop } => format!(
496                "getblocks {{ known_blocks: {}, stop: {} }}",
497                known_blocks.len(),
498                if stop.is_some() { "Some" } else { "None" },
499            ),
500            Message::Inv(invs) => format!("inv {{ invs: {} }}", invs.len()),
501
502            Message::GetHeaders { known_blocks, stop } => format!(
503                "getheaders {{ known_blocks: {}, stop: {} }}",
504                known_blocks.len(),
505                if stop.is_some() { "Some" } else { "None" },
506            ),
507            Message::Headers(headers) => format!("headers {{ headers: {} }}", headers.len()),
508
509            Message::GetData(invs) => format!("getdata {{ invs: {} }}", invs.len()),
510            Message::Block(block) => format!(
511                "block {{ height: {}, hash: {} }}",
512                block
513                    .coinbase_height()
514                    .as_ref()
515                    .map(|h| h.0.to_string())
516                    .unwrap_or_else(|| "None".into()),
517                block.hash(),
518            ),
519            Message::Tx(_) => "tx".to_string(),
520            Message::NotFound(invs) => format!("notfound {{ invs: {} }}", invs.len()),
521
522            Message::Mempool => "mempool".to_string(),
523
524            Message::FilterLoad { .. } => "filterload".to_string(),
525            Message::FilterAdd { .. } => "filteradd".to_string(),
526            Message::FilterClear => "filterclear".to_string(),
527        })
528    }
529}
530
531impl Message {
532    /// Returns the Zcash protocol message command as a string.
533    pub fn command(&self) -> &'static str {
534        match self {
535            Message::Version(_) => "version",
536            Message::Verack => "verack",
537            Message::Ping(_) => "ping",
538            Message::Pong(_) => "pong",
539            Message::Reject { .. } => "reject",
540            Message::GetAddr => "getaddr",
541            Message::Addr(_) => "addr",
542            Message::GetBlocks { .. } => "getblocks",
543            Message::Inv(_) => "inv",
544            Message::GetHeaders { .. } => "getheaders",
545            Message::Headers(_) => "headers",
546            Message::GetData(_) => "getdata",
547            Message::Block(_) => "block",
548            Message::Tx(_) => "tx",
549            Message::NotFound(_) => "notfound",
550            Message::Mempool => "mempool",
551            Message::FilterLoad { .. } => "filterload",
552            Message::FilterAdd { .. } => "filteradd",
553            Message::FilterClear => "filterclear",
554        }
555    }
556}