zebra_network/protocol/internal/
request.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
use std::{collections::HashSet, fmt};

use zebra_chain::{
    block,
    transaction::{UnminedTx, UnminedTxId},
};

use super::super::types::Nonce;

#[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary;

/// A network request, represented in internal format.
///
/// The network layer aims to abstract away the details of the Bitcoin wire
/// protocol into a clear request/response API. Each [`Request`] documents the
/// possible [`Response`s](super::Response) it can generate; it is fine (and
/// recommended!) to match on the expected responses and treat the others as
/// `unreachable!()`, since their return indicates a bug in the network code.
///
/// # Cancellations
///
/// The peer set handles cancelled requests (i.e., requests where the future
/// returned by `Service::call` is dropped before it resolves) on a best-effort
/// basis. Requests are routed to a particular peer connection, and then
/// translated into Zcash protocol messages and sent over the network. If a
/// request is cancelled after it is submitted but before it is processed by a
/// peer connection, no messages will be sent. Otherwise, if it is cancelled
/// while waiting for a response, the peer connection resets its state and makes
/// a best-effort attempt to ignore any messages responsive to the cancelled
/// request, subject to limitations in the underlying Zcash protocol.
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
pub enum Request {
    /// Requests additional peers from the server.
    ///
    /// # Response
    ///
    /// Returns [`Response::Peers`](super::Response::Peers).
    Peers,

    /// Heartbeats triggered on peer connection start.
    ///
    /// This is included as a bit of a hack, it should only be used
    /// internally for connection management. You should not expect to
    /// be firing or handling `Ping` requests or `Pong` responses.
    #[doc(hidden)]
    Ping(Nonce),

    /// Request block data by block hashes.
    ///
    /// This uses a `HashSet` rather than a `Vec` for two reasons. First, it
    /// automatically deduplicates the requested blocks. Second, the internal
    /// protocol translator needs to maintain a `HashSet` anyways, in order to
    /// keep track of which requested blocks have been received and when the
    /// request is ready. Rather than force the internals to always convert into
    /// a `HashSet`, we require the caller to pass one, so that if the caller
    /// didn't start with a `Vec` but with, e.g., an iterator, they can collect
    /// directly into a `HashSet` and save work.
    ///
    /// If this requests a recently-advertised block, the peer set will make a
    /// best-effort attempt to route the request to a peer that advertised the
    /// block. This routing is only used for request sets of size 1.
    /// Otherwise, it is routed using the normal load-balancing strategy.
    ///
    /// The list contains zero or more block hashes.
    ///
    /// # Returns
    ///
    /// Returns [`Response::Blocks`](super::Response::Blocks).
    BlocksByHash(HashSet<block::Hash>),

    /// Request transactions by their unmined transaction ID.
    ///
    /// v4 transactions use a legacy transaction ID, and
    /// v5 transactions use a witnessed transaction ID.
    ///
    /// This uses a `HashSet` for the same reason as [`Request::BlocksByHash`].
    ///
    /// If this requests a recently-advertised transaction, the peer set will
    /// make a best-effort attempt to route the request to a peer that advertised
    /// the transaction. This routing is only used for request sets of size 1.
    /// Otherwise, it is routed using the normal load-balancing strategy.
    ///
    /// The list contains zero or more unmined transaction IDs.
    ///
    /// # Returns
    ///
    /// Returns [`Response::Transactions`](super::Response::Transactions).
    TransactionsById(HashSet<UnminedTxId>),

    /// Request block hashes of subsequent blocks in the chain, given hashes of
    /// known blocks.
    ///
    /// The known blocks list contains zero or more block hashes.
    ///
    /// # Returns
    ///
    /// Returns
    /// [`Response::BlockHashes`](super::Response::BlockHashes).
    ///
    /// # Warning
    ///
    /// This is implemented by sending a `getblocks` message. Bitcoin nodes
    /// respond to `getblocks` with an `inv` message containing a list of the
    /// subsequent blocks. However, Bitcoin nodes *also* send `inv` messages
    /// unsolicited in order to gossip new blocks to their peers. These gossip
    /// messages can race with the response to a `getblocks` request, and there
    /// is no way for the network layer to distinguish them. For this reason, the
    /// response may occasionally contain a single hash of a new chain tip rather
    /// than a list of hashes of subsequent blocks. We believe that unsolicited
    /// `inv` messages will always have exactly one block hash.
    FindBlocks {
        /// Hashes of known blocks, ordered from highest height to lowest height.
        //
        // TODO: make this into an IndexMap - an ordered unique list of hashes (#2244)
        known_blocks: Vec<block::Hash>,
        /// Optionally, the last block hash to request.
        stop: Option<block::Hash>,
    },

    /// Request headers of subsequent blocks in the chain, given hashes of
    /// known blocks.
    ///
    /// The known blocks list contains zero or more block hashes.
    ///
    /// # Returns
    ///
    /// Returns
    /// [`Response::BlockHeaders`](super::Response::BlockHeaders).
    FindHeaders {
        /// Hashes of known blocks, ordered from highest height to lowest height.
        //
        // TODO: make this into an IndexMap - an ordered unique list of hashes (#2244)
        known_blocks: Vec<block::Hash>,
        /// Optionally, the last header to request.
        stop: Option<block::Hash>,
    },

    /// Push an unmined transaction to a remote peer, without advertising it to them first.
    ///
    /// This is implemented by sending an unsolicited `tx` message.
    ///
    /// # Returns
    ///
    /// Returns [`Response::Nil`](super::Response::Nil).
    PushTransaction(UnminedTx),

    /// Advertise a set of unmined transactions to all peers.
    ///
    /// Both Zebra and zcashd sometimes advertise multiple transactions at once.
    ///
    /// This is implemented by sending an `inv` message containing the unmined
    /// transaction IDs, allowing the remote peer to choose whether to download
    /// them. Remote peers who choose to download the transaction will generate a
    /// [`Request::TransactionsById`] against the "inbound" service passed to
    /// [`init`](crate::init).
    ///
    /// v4 transactions use a legacy transaction ID, and
    /// v5 transactions use a witnessed transaction ID.
    ///
    /// The list contains zero or more transaction IDs.
    ///
    /// The peer set routes this request specially, sending it to *half of*
    /// the available peers.
    ///
    /// # Returns
    ///
    /// Returns [`Response::Nil`](super::Response::Nil).
    AdvertiseTransactionIds(HashSet<UnminedTxId>),

    /// Advertise a block to all peers.
    ///
    /// This is implemented by sending an `inv` message containing the
    /// block hash, allowing the remote peer to choose whether to download
    /// it. Remote peers who choose to download the block will generate a
    /// [`Request::BlocksByHash`] against the "inbound" service passed to
    /// [`init`](crate::init).
    ///
    /// The peer set routes this request specially, sending it to *half of*
    /// the available peers.
    ///
    /// # Returns
    ///
    /// Returns [`Response::Nil`](super::Response::Nil).
    AdvertiseBlock(block::Hash),

    /// Request the contents of this node's mempool.
    ///
    /// # Returns
    ///
    /// Returns [`Response::TransactionIds`](super::Response::TransactionIds).
    MempoolTransactionIds,
}

impl fmt::Display for Request {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(&match self {
            Request::Peers => "Peers".to_string(),
            Request::Ping(_) => "Ping".to_string(),

            Request::BlocksByHash(hashes) => {
                format!("BlocksByHash({})", hashes.len())
            }
            Request::TransactionsById(ids) => format!("TransactionsById({})", ids.len()),

            Request::FindBlocks { known_blocks, stop } => format!(
                "FindBlocks {{ known_blocks: {}, stop: {} }}",
                known_blocks.len(),
                if stop.is_some() { "Some" } else { "None" },
            ),
            Request::FindHeaders { known_blocks, stop } => format!(
                "FindHeaders {{ known_blocks: {}, stop: {} }}",
                known_blocks.len(),
                if stop.is_some() { "Some" } else { "None" },
            ),

            Request::PushTransaction(_) => "PushTransaction".to_string(),
            Request::AdvertiseTransactionIds(ids) => {
                format!("AdvertiseTransactionIds({})", ids.len())
            }

            Request::AdvertiseBlock(_) => "AdvertiseBlock".to_string(),
            Request::MempoolTransactionIds => "MempoolTransactionIds".to_string(),
        })
    }
}

impl Request {
    /// Returns the Zebra internal request type as a string.
    pub fn command(&self) -> &'static str {
        match self {
            Request::Peers => "Peers",
            Request::Ping(_) => "Ping",

            Request::BlocksByHash(_) => "BlocksByHash",
            Request::TransactionsById(_) => "TransactionsById",

            Request::FindBlocks { .. } => "FindBlocks",
            Request::FindHeaders { .. } => "FindHeaders",

            Request::PushTransaction(_) => "PushTransaction",
            Request::AdvertiseTransactionIds(_) => "AdvertiseTransactionIds",

            Request::AdvertiseBlock(_) => "AdvertiseBlock",
            Request::MempoolTransactionIds => "MempoolTransactionIds",
        }
    }

    /// Returns true if the request is for block or transaction inventory downloads.
    pub fn is_inventory_download(&self) -> bool {
        matches!(
            self,
            Request::BlocksByHash(_) | Request::TransactionsById(_)
        )
    }

    /// Returns the block hash inventory downloads from the request, if any.
    pub fn block_hash_inventory(&self) -> HashSet<block::Hash> {
        if let Request::BlocksByHash(block_hashes) = self {
            block_hashes.clone()
        } else {
            HashSet::new()
        }
    }

    /// Returns the transaction ID inventory downloads from the request, if any.
    pub fn transaction_id_inventory(&self) -> HashSet<UnminedTxId> {
        if let Request::TransactionsById(transaction_ids) = self {
            transaction_ids.clone()
        } else {
            HashSet::new()
        }
    }
}