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 /// A `filterload` message.
251 ///
252 /// This was defined in [BIP37], which is included in Zcash.
253 ///
254 /// Zebra currently ignores this message.
255 ///
256 /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#filterload.2C_filteradd.2C_filterclear.2C_merkleblock)
257 ///
258 /// [BIP37]: https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
259 FilterLoad {
260 /// The filter itself is simply a bit field of arbitrary
261 /// byte-aligned size. The maximum size is 36,000 bytes.
262 filter: Filter,
263
264 /// The number of hash functions to use in this filter. The
265 /// maximum value allowed in this field is 50.
266 hash_functions_count: u32,
267
268 /// A random value to add to the seed value in the hash
269 /// function used by the bloom filter.
270 tweak: Tweak,
271
272 /// A set of flags that control how matched items are added to the filter.
273 flags: u8,
274 },
275
276 /// A `filteradd` message.
277 ///
278 /// This was defined in [BIP37], which is included in Zcash.
279 ///
280 /// Zebra currently ignores this message.
281 ///
282 /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#filterload.2C_filteradd.2C_filterclear.2C_merkleblock)
283 ///
284 /// [BIP37]: https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
285 FilterAdd {
286 /// The data element to add to the current filter.
287 // The data field must be smaller than or equal to 520 bytes
288 // in size (the maximum size of any potentially matched
289 // object).
290 //
291 // A Vec instead of [u8; 520] because of needed traits.
292 data: Vec<u8>,
293 },
294
295 /// A `filterclear` message.
296 ///
297 /// This was defined in [BIP37], which is included in Zcash.
298 ///
299 /// Zebra currently ignores this message.
300 ///
301 /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#filterload.2C_filteradd.2C_filterclear.2C_merkleblock)
302 ///
303 /// [BIP37]: https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
304 FilterClear,
305}
306
307/// The maximum size of the user agent string.
308///
309/// This is equivalent to `MAX_SUBVERSION_LENGTH` in `zcashd`:
310/// <https://github.com/zcash/zcash/blob/adfc7218435faa1c8985a727f997a795dcffa0c7/src/net.h#L56>
311pub const MAX_USER_AGENT_LENGTH: usize = 256;
312
313/// A `version` message.
314///
315/// Note that although this is called `version` in Bitcoin, its role is really
316/// analogous to a `ClientHello` message in TLS, used to begin a handshake, and
317/// is distinct from a simple version number.
318///
319/// This struct provides a type that is guaranteed to be a `version` message,
320/// and allows [`Message::Version`](Message) fields to be accessed directly.
321///
322/// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#version)
323#[derive(Clone, Eq, PartialEq, Debug)]
324#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
325pub struct VersionMessage {
326 /// The network version number supported by the sender.
327 pub version: Version,
328
329 /// The network services advertised by the sender.
330 pub services: PeerServices,
331
332 /// The time when the version message was sent.
333 ///
334 /// This is a 64-bit field. Zebra rejects out-of-range times as invalid.
335 ///
336 /// TODO: replace with a custom DateTime64 type (#2171)
337 #[cfg_attr(
338 any(test, feature = "proptest-impl"),
339 proptest(strategy = "datetime_full()")
340 )]
341 pub timestamp: DateTime<Utc>,
342
343 /// The network address of the node receiving this message, and its
344 /// advertised network services.
345 ///
346 /// Q: how does the handshake know the remote peer's services already?
347 pub address_recv: AddrInVersion,
348
349 /// The network address of the node sending this message, and its
350 /// advertised network services.
351 pub address_from: AddrInVersion,
352
353 /// Node random nonce, randomly generated every time a version
354 /// packet is sent. This nonce is used to detect connections
355 /// to self.
356 pub nonce: Nonce,
357
358 /// The Zcash user agent advertised by the sender.
359 pub user_agent: String,
360
361 /// The last block received by the emitting node.
362 pub start_height: block::Height,
363
364 /// Whether the remote peer should announce relayed
365 /// transactions or not, see [BIP 0037].
366 ///
367 /// Zebra does not implement the bloom filters in [BIP 0037].
368 /// Instead, it only relays:
369 /// - newly verified best chain block hashes and mempool transaction IDs,
370 /// - after it reaches the chain tip.
371 ///
372 /// [BIP 0037]: https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
373 pub relay: bool,
374}
375
376/// The maximum size of the rejection message.
377///
378/// This is equivalent to `COMMAND_SIZE` in zcashd:
379/// <https://github.com/zcash/zcash/blob/adfc7218435faa1c8985a727f997a795dcffa0c7/src/protocol.h#L33>
380/// <https://github.com/zcash/zcash/blob/c0fbeb809bf2303e30acef0d2b74db11e9177427/src/main.cpp#L7544>
381pub const MAX_REJECT_MESSAGE_LENGTH: usize = 12;
382
383/// The maximum size of the rejection reason.
384///
385/// This is equivalent to `MAX_REJECT_MESSAGE_LENGTH` in zcashd:
386/// <https://github.com/zcash/zcash/blob/adfc7218435faa1c8985a727f997a795dcffa0c7/src/main.h#L126>
387/// <https://github.com/zcash/zcash/blob/c0fbeb809bf2303e30acef0d2b74db11e9177427/src/main.cpp#L7544>
388pub const MAX_REJECT_REASON_LENGTH: usize = 111;
389
390impl From<VersionMessage> for Message {
391 fn from(version_message: VersionMessage) -> Self {
392 Message::Version(version_message)
393 }
394}
395
396impl TryFrom<Message> for VersionMessage {
397 type Error = BoxError;
398
399 fn try_from(message: Message) -> Result<Self, Self::Error> {
400 match message {
401 Message::Version(version_message) => Ok(version_message),
402 _ => Err(format!(
403 "{} message is not a version message: {message:?}",
404 message.command()
405 )
406 .into()),
407 }
408 }
409}
410
411// TODO: add tests for Error conversion and Reject message serialization
412// (Zebra does not currently send reject messages, and it ignores received reject messages.)
413impl<E> From<E> for Message
414where
415 E: Error,
416{
417 fn from(e: E) -> Self {
418 let message = e
419 .to_string()
420 .escape_default()
421 .take(MAX_REJECT_MESSAGE_LENGTH)
422 .collect();
423 let reason = e
424 .source()
425 .map(ToString::to_string)
426 .unwrap_or_default()
427 .escape_default()
428 .take(MAX_REJECT_REASON_LENGTH)
429 .collect();
430
431 Message::Reject {
432 message,
433
434 // The generic case, impls for specific error types should
435 // use specific varieties of `RejectReason`.
436 ccode: RejectReason::Other,
437
438 reason,
439
440 // The hash of the rejected block or transaction.
441 // We don't have that data here, so the caller needs to fill it in later.
442 data: None,
443 }
444 }
445}
446
447/// Reject Reason CCodes
448///
449/// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#reject)
450#[derive(Copy, Clone, Debug, Eq, PartialEq)]
451#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
452#[repr(u8)]
453#[allow(missing_docs)]
454pub enum RejectReason {
455 Malformed = 0x01,
456 Invalid = 0x10,
457 Obsolete = 0x11,
458 Duplicate = 0x12,
459 Nonstandard = 0x40,
460 Dust = 0x41,
461 InsufficientFee = 0x42,
462 Checkpoint = 0x43,
463 Other = 0x50,
464}
465
466impl fmt::Display for Message {
467 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
468 f.write_str(&match self {
469 Message::Version(VersionMessage {
470 version,
471 address_recv,
472 address_from,
473 user_agent,
474 ..
475 }) => format!(
476 "version {{ network: {}, recv: {},_from: {}, user_agent: {:?} }}",
477 version,
478 address_recv.addr(),
479 address_from.addr(),
480 user_agent,
481 ),
482 Message::Verack => "verack".to_string(),
483
484 Message::Ping(_) => "ping".to_string(),
485 Message::Pong(_) => "pong".to_string(),
486
487 Message::Reject {
488 message,
489 reason,
490 data,
491 ..
492 } => format!(
493 "reject {{ message: {:?}, reason: {:?}, data: {} }}",
494 message,
495 reason,
496 if data.is_some() { "Some" } else { "None" },
497 ),
498 Message::P2pV2Upgrade(payload) => {
499 format!("p2pv2up {{ bytes: {} }}", payload.len())
500 }
501
502 Message::GetAddr => "getaddr".to_string(),
503 Message::Addr(addrs) => format!("addr {{ addrs: {} }}", addrs.len()),
504
505 Message::GetBlocks { known_blocks, stop } => format!(
506 "getblocks {{ known_blocks: {}, stop: {} }}",
507 known_blocks.len(),
508 if stop.is_some() { "Some" } else { "None" },
509 ),
510 Message::Inv(invs) => format!("inv {{ invs: {} }}", invs.len()),
511
512 Message::GetHeaders { known_blocks, stop } => format!(
513 "getheaders {{ known_blocks: {}, stop: {} }}",
514 known_blocks.len(),
515 if stop.is_some() { "Some" } else { "None" },
516 ),
517 Message::Headers(headers) => format!("headers {{ headers: {} }}", headers.len()),
518
519 Message::GetData(invs) => format!("getdata {{ invs: {} }}", invs.len()),
520 Message::Block(block) => format!(
521 "block {{ height: {}, hash: {} }}",
522 block
523 .coinbase_height()
524 .as_ref()
525 .map(|h| h.0.to_string())
526 .unwrap_or_else(|| "None".into()),
527 block.hash(),
528 ),
529 Message::Tx(_) => "tx".to_string(),
530 Message::NotFound(invs) => format!("notfound {{ invs: {} }}", invs.len()),
531
532 Message::Mempool => "mempool".to_string(),
533
534 Message::FilterLoad { .. } => "filterload".to_string(),
535 Message::FilterAdd { .. } => "filteradd".to_string(),
536 Message::FilterClear => "filterclear".to_string(),
537 })
538 }
539}
540
541impl Message {
542 /// Returns the Zcash protocol message command as a string.
543 pub fn command(&self) -> &'static str {
544 match self {
545 Message::Version(_) => "version",
546 Message::Verack => "verack",
547 Message::Ping(_) => "ping",
548 Message::Pong(_) => "pong",
549 Message::Reject { .. } => "reject",
550 Message::P2pV2Upgrade(_) => "p2pv2up",
551 Message::GetAddr => "getaddr",
552 Message::Addr(_) => "addr",
553 Message::GetBlocks { .. } => "getblocks",
554 Message::Inv(_) => "inv",
555 Message::GetHeaders { .. } => "getheaders",
556 Message::Headers(_) => "headers",
557 Message::GetData(_) => "getdata",
558 Message::Block(_) => "block",
559 Message::Tx(_) => "tx",
560 Message::NotFound(_) => "notfound",
561 Message::Mempool => "mempool",
562 Message::FilterLoad { .. } => "filterload",
563 Message::FilterAdd { .. } => "filteradd",
564 Message::FilterClear => "filterclear",
565 }
566 }
567}