zebra_network/protocol/internal/
request.rs

1use std::{collections::HashSet, fmt};
2
3use zebra_chain::{
4    block,
5    transaction::{UnminedTx, UnminedTxId},
6};
7
8use super::super::types::Nonce;
9
10#[cfg(any(test, feature = "proptest-impl"))]
11use proptest_derive::Arbitrary;
12
13/// A network request, represented in internal format.
14///
15/// The network layer aims to abstract away the details of the Bitcoin wire
16/// protocol into a clear request/response API. Each [`Request`] documents the
17/// possible [`Response`s](super::Response) it can generate; it is fine (and
18/// recommended!) to match on the expected responses and treat the others as
19/// `unreachable!()`, since their return indicates a bug in the network code.
20///
21/// # Cancellations
22///
23/// The peer set handles cancelled requests (i.e., requests where the future
24/// returned by `Service::call` is dropped before it resolves) on a best-effort
25/// basis. Requests are routed to a particular peer connection, and then
26/// translated into Zcash protocol messages and sent over the network. If a
27/// request is cancelled after it is submitted but before it is processed by a
28/// peer connection, no messages will be sent. Otherwise, if it is cancelled
29/// while waiting for a response, the peer connection resets its state and makes
30/// a best-effort attempt to ignore any messages responsive to the cancelled
31/// request, subject to limitations in the underlying Zcash protocol.
32#[derive(Clone, Debug, Eq, PartialEq)]
33#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
34pub enum Request {
35    /// Requests additional peers from the server.
36    ///
37    /// # Response
38    ///
39    /// Returns [`Response::Peers`](super::Response::Peers).
40    Peers,
41
42    /// Heartbeats triggered on peer connection start.
43    ///
44    /// This is included as a bit of a hack, it should only be used
45    /// internally for connection management. You should not expect to
46    /// be firing or handling `Ping` requests or `Pong` responses.
47    #[doc(hidden)]
48    Ping(Nonce),
49
50    /// Request block data by block hashes.
51    ///
52    /// This uses a `HashSet` rather than a `Vec` for two reasons. First, it
53    /// automatically deduplicates the requested blocks. Second, the internal
54    /// protocol translator needs to maintain a `HashSet` anyways, in order to
55    /// keep track of which requested blocks have been received and when the
56    /// request is ready. Rather than force the internals to always convert into
57    /// a `HashSet`, we require the caller to pass one, so that if the caller
58    /// didn't start with a `Vec` but with, e.g., an iterator, they can collect
59    /// directly into a `HashSet` and save work.
60    ///
61    /// If this requests a recently-advertised block, the peer set will make a
62    /// best-effort attempt to route the request to a peer that advertised the
63    /// block. This routing is only used for request sets of size 1.
64    /// Otherwise, it is routed using the normal load-balancing strategy.
65    ///
66    /// The list contains zero or more block hashes.
67    ///
68    /// # Returns
69    ///
70    /// Returns [`Response::Blocks`](super::Response::Blocks).
71    BlocksByHash(HashSet<block::Hash>),
72
73    /// Request transactions by their unmined transaction ID.
74    ///
75    /// v4 transactions use a legacy transaction ID, and
76    /// v5 transactions use a witnessed transaction ID.
77    ///
78    /// This uses a `HashSet` for the same reason as [`Request::BlocksByHash`].
79    ///
80    /// If this requests a recently-advertised transaction, the peer set will
81    /// make a best-effort attempt to route the request to a peer that advertised
82    /// the transaction. This routing is only used for request sets of size 1.
83    /// Otherwise, it is routed using the normal load-balancing strategy.
84    ///
85    /// The list contains zero or more unmined transaction IDs.
86    ///
87    /// # Returns
88    ///
89    /// Returns [`Response::Transactions`](super::Response::Transactions).
90    TransactionsById(HashSet<UnminedTxId>),
91
92    /// Request block hashes of subsequent blocks in the chain, given hashes of
93    /// known blocks.
94    ///
95    /// The known blocks list contains zero or more block hashes.
96    ///
97    /// # Returns
98    ///
99    /// Returns
100    /// [`Response::BlockHashes`](super::Response::BlockHashes).
101    ///
102    /// # Warning
103    ///
104    /// This is implemented by sending a `getblocks` message. Bitcoin nodes
105    /// respond to `getblocks` with an `inv` message containing a list of the
106    /// subsequent blocks. However, Bitcoin nodes *also* send `inv` messages
107    /// unsolicited in order to gossip new blocks to their peers. These gossip
108    /// messages can race with the response to a `getblocks` request, and there
109    /// is no way for the network layer to distinguish them. For this reason, the
110    /// response may occasionally contain a single hash of a new chain tip rather
111    /// than a list of hashes of subsequent blocks. We believe that unsolicited
112    /// `inv` messages will always have exactly one block hash.
113    FindBlocks {
114        /// Hashes of known blocks, ordered from highest height to lowest height.
115        //
116        // TODO: make this into an IndexMap - an ordered unique list of hashes (#2244)
117        known_blocks: Vec<block::Hash>,
118        /// Optionally, the last block hash to request.
119        stop: Option<block::Hash>,
120    },
121
122    /// Request headers of subsequent blocks in the chain, given hashes of
123    /// known blocks.
124    ///
125    /// The known blocks list contains zero or more block hashes.
126    ///
127    /// # Returns
128    ///
129    /// Returns
130    /// [`Response::BlockHeaders`](super::Response::BlockHeaders).
131    FindHeaders {
132        /// Hashes of known blocks, ordered from highest height to lowest height.
133        //
134        // TODO: make this into an IndexMap - an ordered unique list of hashes (#2244)
135        known_blocks: Vec<block::Hash>,
136        /// Optionally, the last header to request.
137        stop: Option<block::Hash>,
138    },
139
140    /// Push an unmined transaction to a remote peer, without advertising it to them first.
141    ///
142    /// This is implemented by sending an unsolicited `tx` message.
143    ///
144    /// # Returns
145    ///
146    /// Returns [`Response::Nil`](super::Response::Nil).
147    PushTransaction(UnminedTx),
148
149    /// Advertise a set of unmined transactions to all peers.
150    ///
151    /// Both Zebra and zcashd sometimes advertise multiple transactions at once.
152    ///
153    /// This is implemented by sending an `inv` message containing the unmined
154    /// transaction IDs, allowing the remote peer to choose whether to download
155    /// them. Remote peers who choose to download the transaction will generate a
156    /// [`Request::TransactionsById`] against the "inbound" service passed to
157    /// [`init`](crate::init).
158    ///
159    /// v4 transactions use a legacy transaction ID, and
160    /// v5 transactions use a witnessed transaction ID.
161    ///
162    /// The list contains zero or more transaction IDs.
163    ///
164    /// The peer set routes this request specially, sending it to *half of*
165    /// the available peers.
166    ///
167    /// # Returns
168    ///
169    /// Returns [`Response::Nil`](super::Response::Nil).
170    AdvertiseTransactionIds(HashSet<UnminedTxId>),
171
172    /// Advertise a block to all peers.
173    ///
174    /// This is implemented by sending an `inv` message containing the
175    /// block hash, allowing the remote peer to choose whether to download
176    /// it. Remote peers who choose to download the block will generate a
177    /// [`Request::BlocksByHash`] against the "inbound" service passed to
178    /// [`init`](crate::init).
179    ///
180    /// The peer set routes this request specially, sending it to *a fraction of*
181    /// the available peers. See [`number_of_peers_to_broadcast()`](crate::PeerSet::number_of_peers_to_broadcast)
182    /// for more details.
183    ///
184    /// # Returns
185    ///
186    /// Returns [`Response::Nil`](super::Response::Nil).
187    AdvertiseBlock(block::Hash),
188
189    /// Advertise a block to all ready peers. This is equivalent to
190    /// [`Request::AdvertiseBlock`] except that the peer set will route
191    /// this request to all available ready peers. Used by the gossip task
192    /// to broadcast mined blocks to all ready peers.
193    AdvertiseBlockToAll(block::Hash),
194
195    /// Request the contents of this node's mempool.
196    ///
197    /// # Returns
198    ///
199    /// Returns [`Response::TransactionIds`](super::Response::TransactionIds).
200    MempoolTransactionIds,
201}
202
203impl fmt::Display for Request {
204    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
205        f.write_str(&match self {
206            Request::Peers => "Peers".to_string(),
207            Request::Ping(_) => "Ping".to_string(),
208
209            Request::BlocksByHash(hashes) => {
210                format!("BlocksByHash({})", hashes.len())
211            }
212            Request::TransactionsById(ids) => format!("TransactionsById({})", ids.len()),
213
214            Request::FindBlocks { known_blocks, stop } => format!(
215                "FindBlocks {{ known_blocks: {}, stop: {} }}",
216                known_blocks.len(),
217                if stop.is_some() { "Some" } else { "None" },
218            ),
219            Request::FindHeaders { known_blocks, stop } => format!(
220                "FindHeaders {{ known_blocks: {}, stop: {} }}",
221                known_blocks.len(),
222                if stop.is_some() { "Some" } else { "None" },
223            ),
224
225            Request::PushTransaction(_) => "PushTransaction".to_string(),
226            Request::AdvertiseTransactionIds(ids) => {
227                format!("AdvertiseTransactionIds({})", ids.len())
228            }
229
230            Request::AdvertiseBlock(_) => "AdvertiseBlock".to_string(),
231            Request::AdvertiseBlockToAll(_) => "AdvertiseBlockToAll".to_string(),
232            Request::MempoolTransactionIds => "MempoolTransactionIds".to_string(),
233        })
234    }
235}
236
237impl Request {
238    /// Returns the Zebra internal request type as a string.
239    pub fn command(&self) -> &'static str {
240        match self {
241            Request::Peers => "Peers",
242            Request::Ping(_) => "Ping",
243
244            Request::BlocksByHash(_) => "BlocksByHash",
245            Request::TransactionsById(_) => "TransactionsById",
246
247            Request::FindBlocks { .. } => "FindBlocks",
248            Request::FindHeaders { .. } => "FindHeaders",
249
250            Request::PushTransaction(_) => "PushTransaction",
251            Request::AdvertiseTransactionIds(_) => "AdvertiseTransactionIds",
252
253            Request::AdvertiseBlock(_) | Request::AdvertiseBlockToAll(_) => "AdvertiseBlock",
254            Request::MempoolTransactionIds => "MempoolTransactionIds",
255        }
256    }
257
258    /// Returns true if the request is for block or transaction inventory downloads.
259    pub fn is_inventory_download(&self) -> bool {
260        matches!(
261            self,
262            Request::BlocksByHash(_) | Request::TransactionsById(_)
263        )
264    }
265
266    /// Returns the block hash inventory downloads from the request, if any.
267    pub fn block_hash_inventory(&self) -> HashSet<block::Hash> {
268        if let Request::BlocksByHash(block_hashes) = self {
269            block_hashes.clone()
270        } else {
271            HashSet::new()
272        }
273    }
274
275    /// Returns the transaction ID inventory downloads from the request, if any.
276    pub fn transaction_id_inventory(&self) -> HashSet<UnminedTxId> {
277        if let Request::TransactionsById(transaction_ids) = self {
278            transaction_ids.clone()
279        } else {
280            HashSet::new()
281        }
282    }
283}