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 *half of*
181 /// the available peers.
182 ///
183 /// # Returns
184 ///
185 /// Returns [`Response::Nil`](super::Response::Nil).
186 AdvertiseBlock(block::Hash),
187
188 /// Request the contents of this node's mempool.
189 ///
190 /// # Returns
191 ///
192 /// Returns [`Response::TransactionIds`](super::Response::TransactionIds).
193 MempoolTransactionIds,
194}
195
196impl fmt::Display for Request {
197 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
198 f.write_str(&match self {
199 Request::Peers => "Peers".to_string(),
200 Request::Ping(_) => "Ping".to_string(),
201
202 Request::BlocksByHash(hashes) => {
203 format!("BlocksByHash({})", hashes.len())
204 }
205 Request::TransactionsById(ids) => format!("TransactionsById({})", ids.len()),
206
207 Request::FindBlocks { known_blocks, stop } => format!(
208 "FindBlocks {{ known_blocks: {}, stop: {} }}",
209 known_blocks.len(),
210 if stop.is_some() { "Some" } else { "None" },
211 ),
212 Request::FindHeaders { known_blocks, stop } => format!(
213 "FindHeaders {{ known_blocks: {}, stop: {} }}",
214 known_blocks.len(),
215 if stop.is_some() { "Some" } else { "None" },
216 ),
217
218 Request::PushTransaction(_) => "PushTransaction".to_string(),
219 Request::AdvertiseTransactionIds(ids) => {
220 format!("AdvertiseTransactionIds({})", ids.len())
221 }
222
223 Request::AdvertiseBlock(_) => "AdvertiseBlock".to_string(),
224 Request::MempoolTransactionIds => "MempoolTransactionIds".to_string(),
225 })
226 }
227}
228
229impl Request {
230 /// Returns the Zebra internal request type as a string.
231 pub fn command(&self) -> &'static str {
232 match self {
233 Request::Peers => "Peers",
234 Request::Ping(_) => "Ping",
235
236 Request::BlocksByHash(_) => "BlocksByHash",
237 Request::TransactionsById(_) => "TransactionsById",
238
239 Request::FindBlocks { .. } => "FindBlocks",
240 Request::FindHeaders { .. } => "FindHeaders",
241
242 Request::PushTransaction(_) => "PushTransaction",
243 Request::AdvertiseTransactionIds(_) => "AdvertiseTransactionIds",
244
245 Request::AdvertiseBlock(_) => "AdvertiseBlock",
246 Request::MempoolTransactionIds => "MempoolTransactionIds",
247 }
248 }
249
250 /// Returns true if the request is for block or transaction inventory downloads.
251 pub fn is_inventory_download(&self) -> bool {
252 matches!(
253 self,
254 Request::BlocksByHash(_) | Request::TransactionsById(_)
255 )
256 }
257
258 /// Returns the block hash inventory downloads from the request, if any.
259 pub fn block_hash_inventory(&self) -> HashSet<block::Hash> {
260 if let Request::BlocksByHash(block_hashes) = self {
261 block_hashes.clone()
262 } else {
263 HashSet::new()
264 }
265 }
266
267 /// Returns the transaction ID inventory downloads from the request, if any.
268 pub fn transaction_id_inventory(&self) -> HashSet<UnminedTxId> {
269 if let Request::TransactionsById(transaction_ids) = self {
270 transaction_ids.clone()
271 } else {
272 HashSet::new()
273 }
274 }
275}