Skip to main content

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