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 /// # Returns
146 ///
147 /// Returns [`Response::Nil`](super::Response::Nil).
148 PushTransaction(UnminedTx),
149
150 /// Advertise a set of unmined transactions to all peers.
151 ///
152 /// Both Zebra and zcashd sometimes advertise multiple transactions at once.
153 ///
154 /// This is implemented by sending an `inv` message containing the unmined
155 /// transaction IDs, allowing the remote peer to choose whether to download
156 /// them. Remote peers who choose to download the transaction will generate a
157 /// [`Request::TransactionsById`] against the "inbound" service passed to
158 /// [`init`](crate::init).
159 ///
160 /// v4 transactions use a legacy transaction ID, and
161 /// v5 transactions use a witnessed transaction ID.
162 ///
163 /// The list contains zero or more transaction IDs.
164 ///
165 /// The peer set routes this request specially, sending it to *half of*
166 /// the available peers.
167 ///
168 /// # Returns
169 ///
170 /// Returns [`Response::Nil`](super::Response::Nil).
171 AdvertiseTransactionIds(HashSet<UnminedTxId>),
172
173 /// Advertise a block to all peers.
174 ///
175 /// This is implemented by sending an `inv` message containing the
176 /// block hash, allowing the remote peer to choose whether to download
177 /// it. Remote peers who choose to download the block will generate a
178 /// [`Request::BlocksByHash`] against the "inbound" service passed to
179 /// [`init`](crate::init).
180 ///
181 /// The peer set routes this request specially, sending it to *a fraction of*
182 /// the available peers. See [`number_of_peers_to_broadcast()`](crate::PeerSet::number_of_peers_to_broadcast)
183 /// for more details.
184 ///
185 /// The second field is the address of the peer that sent us this `inv`:
186 /// `Some(addr)` when the advertisement was relayed from a remote peer,
187 /// and `None` when Zebra originates the advertisement itself (for
188 /// example from the sync gossip task). Consumers use the address to
189 /// apply per-peer policies such as the inbound download per-IP cap.
190 ///
191 /// # Returns
192 ///
193 /// Returns [`Response::Nil`](super::Response::Nil).
194 AdvertiseBlock(block::Hash, Option<PeerSocketAddr>),
195
196 /// Advertise a block to all ready peers. This is equivalent to
197 /// [`Request::AdvertiseBlock`] except that the peer set will route
198 /// this request to all available ready peers. Used by the gossip task
199 /// to broadcast mined blocks to all ready peers.
200 AdvertiseBlockToAll(block::Hash),
201
202 /// Request the contents of this node's mempool.
203 ///
204 /// # Returns
205 ///
206 /// Returns [`Response::TransactionIds`](super::Response::TransactionIds).
207 MempoolTransactionIds,
208}
209
210impl fmt::Display for Request {
211 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212 f.write_str(&match self {
213 Request::Peers => "Peers".to_string(),
214 Request::Ping(_) => "Ping".to_string(),
215
216 Request::BlocksByHash(hashes) => {
217 format!("BlocksByHash({})", hashes.len())
218 }
219 Request::TransactionsById(ids) => format!("TransactionsById({})", ids.len()),
220
221 Request::FindBlocks { known_blocks, stop } => format!(
222 "FindBlocks {{ known_blocks: {}, stop: {} }}",
223 known_blocks.len(),
224 if stop.is_some() { "Some" } else { "None" },
225 ),
226 Request::FindHeaders { known_blocks, stop } => format!(
227 "FindHeaders {{ known_blocks: {}, stop: {} }}",
228 known_blocks.len(),
229 if stop.is_some() { "Some" } else { "None" },
230 ),
231
232 Request::PushTransaction(_) => "PushTransaction".to_string(),
233 Request::AdvertiseTransactionIds(ids) => {
234 format!("AdvertiseTransactionIds({})", ids.len())
235 }
236
237 Request::AdvertiseBlock(_, _) => "AdvertiseBlock".to_string(),
238 Request::AdvertiseBlockToAll(_) => "AdvertiseBlockToAll".to_string(),
239 Request::MempoolTransactionIds => "MempoolTransactionIds".to_string(),
240 })
241 }
242}
243
244impl Request {
245 /// Returns the Zebra internal request type as a string.
246 pub fn command(&self) -> &'static str {
247 match self {
248 Request::Peers => "Peers",
249 Request::Ping(_) => "Ping",
250
251 Request::BlocksByHash(_) => "BlocksByHash",
252 Request::TransactionsById(_) => "TransactionsById",
253
254 Request::FindBlocks { .. } => "FindBlocks",
255 Request::FindHeaders { .. } => "FindHeaders",
256
257 Request::PushTransaction(_) => "PushTransaction",
258 Request::AdvertiseTransactionIds(_) => "AdvertiseTransactionIds",
259
260 Request::AdvertiseBlock(_, _) | Request::AdvertiseBlockToAll(_) => "AdvertiseBlock",
261 Request::MempoolTransactionIds => "MempoolTransactionIds",
262 }
263 }
264
265 /// Returns true if the request is for block or transaction inventory downloads.
266 pub fn is_inventory_download(&self) -> bool {
267 matches!(
268 self,
269 Request::BlocksByHash(_) | Request::TransactionsById(_)
270 )
271 }
272
273 /// Returns the block hash inventory downloads from the request, if any.
274 pub fn block_hash_inventory(&self) -> HashSet<block::Hash> {
275 if let Request::BlocksByHash(block_hashes) = self {
276 block_hashes.clone()
277 } else {
278 HashSet::new()
279 }
280 }
281
282 /// Returns the transaction ID inventory downloads from the request, if any.
283 pub fn transaction_id_inventory(&self) -> HashSet<UnminedTxId> {
284 if let Request::TransactionsById(transaction_ids) = self {
285 transaction_ids.clone()
286 } else {
287 HashSet::new()
288 }
289 }
290}