Skip to main content

zakura_network/protocol/internal/
request.rs

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