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}