zebrad/components/inbound.rs
1//! The inbound service handles requests from Zebra's peers.
2//!
3//! It downloads and verifies gossiped blocks and mempool transactions,
4//! when Zebra is close to the chain tip.
5//!
6//! It also responds to peer requests for blocks, transactions, and peer addresses.
7
8use std::{
9 collections::HashSet,
10 future::Future,
11 pin::Pin,
12 sync::Arc,
13 task::{Context, Poll},
14 time::Duration,
15};
16
17use futures::{
18 future::{FutureExt, TryFutureExt},
19 stream::Stream,
20};
21use tokio::sync::oneshot::{self, error::TryRecvError};
22use tower::{buffer::Buffer, timeout::Timeout, util::BoxService, Service, ServiceExt};
23
24use zebra_network::{self as zn, PeerSocketAddr};
25use zebra_state::{self as zs};
26
27use zebra_chain::{
28 block::{self, Block},
29 serialization::ZcashSerialize,
30 transaction::UnminedTxId,
31};
32use zebra_consensus::{router::RouterError, VerifyBlockError};
33use zebra_network::{AddressBook, InventoryResponse};
34use zebra_node_services::mempool;
35
36use crate::BoxError;
37
38// Re-use the syncer timeouts for consistency.
39use super::sync::{BLOCK_DOWNLOAD_TIMEOUT, BLOCK_VERIFY_TIMEOUT};
40
41use InventoryResponse::*;
42
43mod cached_peer_addr_response;
44pub(crate) mod downloads;
45
46use cached_peer_addr_response::CachedPeerAddrResponse;
47
48#[cfg(test)]
49mod tests;
50
51use downloads::Downloads as BlockDownloads;
52
53/// The maximum amount of time an inbound service response can take.
54///
55/// If the response takes longer than this time, it will be cancelled,
56/// and the peer might be disconnected.
57pub const MAX_INBOUND_RESPONSE_TIME: Duration = Duration::from_secs(5);
58
59/// The number of bytes the [`Inbound`] service will queue in response to a single block or
60/// transaction request, before ignoring any additional block or transaction IDs in that request.
61///
62/// This is the same as `zcashd`'s default send buffer limit:
63/// <https://github.com/zcash/zcash/blob/829dd94f9d253bb705f9e194f13cb8ca8e545e1e/src/net.h#L84>
64/// as used in `ProcessGetData()`:
65/// <https://github.com/zcash/zcash/blob/829dd94f9d253bb705f9e194f13cb8ca8e545e1e/src/main.cpp#L6410-L6412>
66pub const GETDATA_SENT_BYTES_LIMIT: usize = 1_000_000;
67
68/// The maximum number of blocks the [`Inbound`] service will queue in response to a block request,
69/// before ignoring any additional block IDs in that request.
70///
71/// This is the same as `zcashd`'s request limit:
72/// <https://github.com/zcash/zcash/blob/829dd94f9d253bb705f9e194f13cb8ca8e545e1e/src/main.h#L108>
73///
74/// (Zebra's request limit is one block in transit per peer, because it fans out block requests to
75/// many peers instead of just a few peers.)
76pub const GETDATA_MAX_BLOCK_COUNT: usize = 16;
77
78type BlockDownloadPeerSet =
79 Buffer<BoxService<zn::Request, zn::Response, zn::BoxError>, zn::Request>;
80type State = Buffer<BoxService<zs::Request, zs::Response, zs::BoxError>, zs::Request>;
81type Mempool = Buffer<BoxService<mempool::Request, mempool::Response, BoxError>, mempool::Request>;
82type SemanticBlockVerifier = Buffer<
83 BoxService<zebra_consensus::Request, block::Hash, RouterError>,
84 zebra_consensus::Request,
85>;
86type GossipedBlockDownloads =
87 BlockDownloads<Timeout<BlockDownloadPeerSet>, Timeout<SemanticBlockVerifier>, State>;
88
89/// The services used by the [`Inbound`] service.
90pub struct InboundSetupData {
91 /// A shared list of peer addresses.
92 pub address_book: Arc<std::sync::Mutex<AddressBook>>,
93
94 /// A service that can be used to download gossiped blocks.
95 pub block_download_peer_set: BlockDownloadPeerSet,
96
97 /// A service that verifies downloaded blocks.
98 ///
99 /// Given to `Inbound.block_downloads` after the required services are set up.
100 pub block_verifier: SemanticBlockVerifier,
101
102 /// A service that manages transactions in the memory pool.
103 pub mempool: Mempool,
104
105 /// A service that manages cached blockchain state.
106 pub state: State,
107
108 /// Allows efficient access to the best tip of the blockchain.
109 pub latest_chain_tip: zs::LatestChainTip,
110
111 /// A channel to send misbehavior reports to the [`AddressBook`].
112 pub misbehavior_sender: tokio::sync::mpsc::Sender<(PeerSocketAddr, u32)>,
113}
114
115/// Tracks the internal state of the [`Inbound`] service during setup.
116#[allow(clippy::large_enum_variant)]
117pub enum Setup {
118 /// Waiting for service setup to complete.
119 ///
120 /// All requests are ignored.
121 Pending {
122 // Configuration
123 //
124 /// The configured full verification concurrency limit.
125 full_verify_concurrency_limit: usize,
126
127 // Services
128 //
129 /// A oneshot channel used to receive required services,
130 /// after they are set up.
131 setup: oneshot::Receiver<InboundSetupData>,
132 },
133
134 /// Setup is complete.
135 ///
136 /// All requests are answered.
137 Initialized {
138 // Services
139 //
140 /// An owned partial list of peer addresses used as a `GetAddr` response, and
141 /// a shared list of peer addresses used to periodically refresh the partial list.
142 ///
143 /// Refreshed from the address book in `poll_ready` method
144 /// after [`CACHED_ADDRS_REFRESH_INTERVAL`](cached_peer_addr_response::CACHED_ADDRS_REFRESH_INTERVAL).
145 cached_peer_addr_response: CachedPeerAddrResponse,
146
147 /// A `futures::Stream` that downloads and verifies gossiped blocks.
148 block_downloads: Pin<Box<GossipedBlockDownloads>>,
149
150 /// A service that manages transactions in the memory pool.
151 mempool: Mempool,
152
153 /// A service that manages cached blockchain state.
154 state: State,
155
156 /// A channel to send misbehavior reports to the [`AddressBook`].
157 misbehavior_sender: tokio::sync::mpsc::Sender<(PeerSocketAddr, u32)>,
158 },
159
160 /// Temporary state used in the inbound service's internal initialization code.
161 ///
162 /// If this state occurs outside the service initialization code, the service panics.
163 FailedInit,
164
165 /// Setup failed, because the setup channel permanently failed.
166 /// The service keeps returning readiness errors for every request.
167 FailedRecv {
168 /// The original channel error.
169 error: SharedRecvError,
170 },
171}
172
173/// A wrapper around `Arc<TryRecvError>` that implements `Error`.
174#[derive(thiserror::Error, Debug, Clone)]
175#[error(transparent)]
176pub struct SharedRecvError(Arc<TryRecvError>);
177
178impl From<TryRecvError> for SharedRecvError {
179 fn from(source: TryRecvError) -> Self {
180 Self(Arc::new(source))
181 }
182}
183
184/// Uses the node state to respond to inbound peer requests.
185///
186/// This service, wrapped in appropriate middleware, is passed to
187/// `zebra_network::init` to respond to inbound peer requests.
188///
189/// The `Inbound` service is responsible for:
190///
191/// - supplying network data like peer addresses to other nodes;
192/// - supplying chain data like blocks to other nodes;
193/// - supplying mempool transactions to other nodes;
194/// - receiving gossiped transactions; and
195/// - receiving gossiped blocks.
196///
197/// Because the `Inbound` service is responsible for participating in the gossip
198/// protocols used for transaction and block diffusion, there is a potential
199/// overlap with the `ChainSync` and `Mempool` components.
200///
201/// The division of responsibility is that:
202///
203/// The `ChainSync` and `Mempool` components are *internally driven*,
204/// periodically polling the network to check for new blocks or transactions.
205///
206/// The `Inbound` service is *externally driven*, responding to block gossip
207/// by attempting to download and validate advertised blocks.
208///
209/// Gossiped transactions are forwarded to the mempool downloader,
210/// which unifies polled and gossiped transactions into a single download list.
211pub struct Inbound {
212 /// Provides service dependencies, if they are available.
213 ///
214 /// Some services are unavailable until Zebra has completed setup.
215 setup: Setup,
216}
217
218impl Inbound {
219 /// Create a new inbound service.
220 ///
221 /// Dependent services are sent via the `setup` channel after initialization.
222 pub fn new(
223 full_verify_concurrency_limit: usize,
224 setup: oneshot::Receiver<InboundSetupData>,
225 ) -> Inbound {
226 Inbound {
227 setup: Setup::Pending {
228 full_verify_concurrency_limit,
229 setup,
230 },
231 }
232 }
233
234 /// Remove `self.setup`, temporarily replacing it with an invalid state.
235 fn take_setup(&mut self) -> Setup {
236 let mut setup = Setup::FailedInit;
237 std::mem::swap(&mut self.setup, &mut setup);
238 setup
239 }
240}
241
242impl Service<zn::Request> for Inbound {
243 type Response = zn::Response;
244 type Error = zn::BoxError;
245 type Future =
246 Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
247
248 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
249 // Check whether the setup is finished, but don't wait for it to
250 // become ready before reporting readiness. We expect to get it "soon",
251 // and reporting unreadiness might cause unwanted load-shedding, since
252 // the load-shed middleware is unable to distinguish being unready due
253 // to load from being unready while waiting on setup.
254
255 // Every setup variant handler must provide a result
256 let result;
257
258 self.setup = match self.take_setup() {
259 Setup::Pending {
260 full_verify_concurrency_limit,
261 mut setup,
262 } => match setup.try_recv() {
263 Ok(setup_data) => {
264 let InboundSetupData {
265 address_book,
266 block_download_peer_set,
267 block_verifier,
268 mempool,
269 state,
270 latest_chain_tip,
271 misbehavior_sender,
272 } = setup_data;
273
274 let cached_peer_addr_response = CachedPeerAddrResponse::new(address_book);
275
276 let block_downloads = Box::pin(BlockDownloads::new(
277 full_verify_concurrency_limit,
278 Timeout::new(block_download_peer_set, BLOCK_DOWNLOAD_TIMEOUT),
279 Timeout::new(block_verifier, BLOCK_VERIFY_TIMEOUT),
280 state.clone(),
281 latest_chain_tip,
282 ));
283
284 result = Ok(());
285 Setup::Initialized {
286 cached_peer_addr_response,
287 block_downloads,
288 mempool,
289 state,
290 misbehavior_sender,
291 }
292 }
293 Err(TryRecvError::Empty) => {
294 // There's no setup data yet, so keep waiting for it.
295 //
296 // We could use Future::poll() to get a waker and return Poll::Pending here.
297 // But we want to drop excess requests during startup instead. Otherwise,
298 // the inbound service gets overloaded, and starts disconnecting peers.
299 result = Ok(());
300 Setup::Pending {
301 full_verify_concurrency_limit,
302 setup,
303 }
304 }
305 Err(error @ TryRecvError::Closed) => {
306 // Mark the service as failed, because setup failed
307 error!(?error, "inbound setup failed");
308 let error: SharedRecvError = error.into();
309 result = Err(error.clone().into());
310 Setup::FailedRecv { error }
311 }
312 },
313 // Make sure previous setups were left in a valid state
314 Setup::FailedInit => unreachable!("incomplete previous Inbound initialization"),
315 // If setup failed, report service failure
316 Setup::FailedRecv { error } => {
317 result = Err(error.clone().into());
318 Setup::FailedRecv { error }
319 }
320 // Clean up completed download tasks, ignoring their results
321 Setup::Initialized {
322 cached_peer_addr_response,
323 mut block_downloads,
324 mempool,
325 state,
326 misbehavior_sender,
327 } => {
328 // # Correctness
329 //
330 // Clear the stream but ignore the final Pending return value.
331 // If we returned Pending here, and there were no waiting block downloads,
332 // then inbound requests would wait for the next block download, and hang forever.
333 while let Poll::Ready(Some(result)) = block_downloads.as_mut().poll_next(cx) {
334 let Err((err, Some(advertiser_addr))) = result else {
335 continue;
336 };
337
338 let Ok(err) = err.downcast::<VerifyBlockError>() else {
339 continue;
340 };
341
342 if err.misbehavior_score() != 0 {
343 let _ =
344 misbehavior_sender.try_send((advertiser_addr, err.misbehavior_score()));
345 }
346 }
347
348 result = Ok(());
349
350 Setup::Initialized {
351 cached_peer_addr_response,
352 block_downloads,
353 mempool,
354 state,
355 misbehavior_sender,
356 }
357 }
358 };
359
360 // Make sure we're leaving the setup in a valid state
361 if matches!(self.setup, Setup::FailedInit) {
362 unreachable!("incomplete Inbound initialization after poll_ready state handling");
363 }
364
365 // TODO:
366 // * do we want to propagate backpressure from the download queue or its outbound network?
367 // currently, the download queue waits for the outbound network in the download future,
368 // and drops new requests after it reaches a hard-coded limit. This is the
369 // "load shed directly" pattern from #1618.
370 // * currently, the state service is always ready, unless its buffer is full.
371 // So we might also want to propagate backpressure from its buffer.
372 // * poll_ready needs to be implemented carefully, to avoid hangs or deadlocks.
373 // See #1593 for details.
374 Poll::Ready(result)
375 }
376
377 /// Call the inbound service.
378 ///
379 /// Errors indicate that the peer has done something wrong or unexpected,
380 /// and will cause callers to disconnect from the remote peer.
381 #[instrument(name = "inbound", skip(self, req))]
382 fn call(&mut self, req: zn::Request) -> Self::Future {
383 let (cached_peer_addr_response, block_downloads, mempool, state) = match &mut self.setup {
384 Setup::Initialized {
385 cached_peer_addr_response,
386 block_downloads,
387 mempool,
388 state,
389 misbehavior_sender: _,
390 } => (cached_peer_addr_response, block_downloads, mempool, state),
391 _ => {
392 debug!("ignoring request from remote peer during setup");
393 return async { Ok(zn::Response::Nil) }.boxed();
394 }
395 };
396
397 match req {
398 zn::Request::Peers => {
399 // # Security
400 //
401 // We truncate the list to not reveal our entire peer set in one call.
402 // But we don't monitor repeated requests and the results are shuffled,
403 // a crawler could just send repeated queries and get the full list.
404 //
405 // # Correctness
406 //
407 // If the address book is busy, try again inside the future. If it can't be locked
408 // twice, ignore the request.
409 cached_peer_addr_response.try_refresh();
410 let response = cached_peer_addr_response.value();
411
412 async move {
413 Ok(response)
414 }.boxed()
415 }
416 zn::Request::BlocksByHash(hashes) => {
417 // We return an available or missing response to each inventory request,
418 // unless the request is empty, or it reaches a response limit.
419 if hashes.is_empty() {
420 return async { Ok(zn::Response::Nil) }.boxed();
421 }
422
423 let state = state.clone();
424
425 async move {
426 let mut blocks: Vec<InventoryResponse<(Arc<Block>, Option<PeerSocketAddr>), block::Hash>> = Vec::new();
427 let mut total_size = 0;
428
429 // Ignore any block hashes past the response limit.
430 // This saves us expensive database lookups.
431 for &hash in hashes.iter().take(GETDATA_MAX_BLOCK_COUNT) {
432 // We check the limit after including at least one block, so that we can
433 // send blocks greater than 1 MB (but only one at a time)
434 if total_size >= GETDATA_SENT_BYTES_LIMIT {
435 break;
436 }
437
438 let response = state.clone().ready().await?.call(zs::Request::Block(hash.into())).await?;
439
440 // Add the block responses to the list, while updating the size limit.
441 //
442 // If there was a database error, return the error,
443 // and stop processing further chunks.
444 match response {
445 zs::Response::Block(Some(block)) => {
446 // If checking the serialized size of the block performs badly,
447 // return the size from the state using a wrapper type.
448 total_size += block.zcash_serialized_size();
449
450 blocks.push(Available((block, None)))
451 },
452 // We don't need to limit the size of the missing block IDs list,
453 // because it is already limited to the size of the getdata request
454 // sent by the peer. (Their content and encodings are the same.)
455 zs::Response::Block(None) => blocks.push(Missing(hash)),
456 _ => unreachable!("wrong response from state"),
457 }
458
459 }
460
461 // The network layer handles splitting this response into multiple `block`
462 // messages, and a `notfound` message if needed.
463 Ok(zn::Response::Blocks(blocks))
464 }.boxed()
465 }
466 zn::Request::TransactionsById(req_tx_ids) => {
467 // We return an available or missing response to each inventory request,
468 // unless the request is empty, or it reaches a response limit.
469 if req_tx_ids.is_empty() {
470 return async { Ok(zn::Response::Nil) }.boxed();
471 }
472
473 let request = mempool::Request::TransactionsById(req_tx_ids.clone());
474 mempool.clone().oneshot(request).map_ok(move |resp| {
475 let mut total_size = 0;
476
477 let transactions = match resp {
478 mempool::Response::Transactions(transactions) => transactions,
479 _ => unreachable!("Mempool component should always respond to a `TransactionsById` request with a `Transactions` response"),
480 };
481
482 // Work out which transaction IDs were missing.
483 let available_tx_ids: HashSet<UnminedTxId> = transactions.iter().map(|tx| tx.id).collect();
484 // We don't need to limit the size of the missing transaction IDs list,
485 // because it is already limited to the size of the getdata request
486 // sent by the peer. (Their content and encodings are the same.)
487 let missing = req_tx_ids.into_iter().filter(|tx_id| !available_tx_ids.contains(tx_id)).map(Missing);
488
489 // If we skip sending some transactions because the limit has been reached,
490 // they aren't reported as missing. This matches `zcashd`'s behaviour:
491 // <https://github.com/zcash/zcash/blob/829dd94f9d253bb705f9e194f13cb8ca8e545e1e/src/main.cpp#L6410-L6412>
492 let available = transactions.into_iter().take_while(|tx| {
493 // We check the limit after including the transaction,
494 // so that we can send transactions greater than 1 MB
495 // (but only one at a time)
496 let within_limit = total_size < GETDATA_SENT_BYTES_LIMIT;
497
498 total_size += tx.size;
499
500 within_limit
501 }).map(|tx| Available((tx, None)));
502
503 // The network layer handles splitting this response into multiple `tx`
504 // messages, and a `notfound` message if needed.
505 zn::Response::Transactions(available.chain(missing).collect())
506 }).boxed()
507 }
508 // Find* responses are already size-limited by the state.
509 zn::Request::FindBlocks { known_blocks, stop } => {
510 let request = zs::Request::FindBlockHashes { known_blocks, stop };
511 state.clone().oneshot(request).map_ok(|resp| match resp {
512 zs::Response::BlockHashes(hashes) if hashes.is_empty() => zn::Response::Nil,
513 zs::Response::BlockHashes(hashes) => zn::Response::BlockHashes(hashes),
514 _ => unreachable!("zebra-state should always respond to a `FindBlockHashes` request with a `BlockHashes` response"),
515 })
516 .boxed()
517 }
518 zn::Request::FindHeaders { known_blocks, stop } => {
519 let request = zs::Request::FindBlockHeaders { known_blocks, stop };
520 state.clone().oneshot(request).map_ok(|resp| match resp {
521 // Always reply with a `headers` message, even when empty: the
522 // `getheaders` protocol requires a `headers` response, and
523 // returning `Nil` (which sends nothing) leaves a peer's
524 // `getheaders` request pending forever. A zcashd sidecar
525 // following Zebra defers every later inv-triggered
526 // `getheaders` behind that stuck request and never syncs.
527 zs::Response::BlockHeaders(headers) => zn::Response::BlockHeaders(headers),
528 _ => unreachable!("zebra-state should always respond to a `FindBlockHeaders` request with a `BlockHeaders` response"),
529 })
530 .boxed()
531 }
532 zn::Request::PushTransaction(transaction, sender) => {
533 // Tag a directly pushed transaction with the sending peer so the
534 // mempool downloader can enforce a per-peer queue cap, mirroring
535 // the advertisement path below. See `GHSA-m9xx-8rcj-vmgp`.
536 let request = match sender {
537 Some(peer_addr) => mempool::Request::QueueFromPeer {
538 candidates: vec![transaction.into()],
539 source: *peer_addr,
540 },
541 None => mempool::Request::Queue(vec![transaction.into()]),
542 };
543 mempool
544 .clone()
545 .oneshot(request)
546 // The response just indicates if processing was queued or not; ignore it
547 .map_ok(|_resp| zn::Response::Nil)
548 .boxed()
549 }
550 zn::Request::AdvertiseTransactionIds(transactions, advertiser) => {
551 // Tag the advertised txids with the announcing peer so the
552 // mempool downloader can enforce a per-peer queue cap.
553 // See `GHSA-4fc2-h7jh-287c`.
554 let request = match advertiser {
555 Some(peer_addr) => mempool::Request::QueueFromPeer {
556 candidates: transactions.into_iter().map(Into::into).collect(),
557 source: *peer_addr,
558 },
559 None => mempool::Request::Queue(
560 transactions.into_iter().map(Into::into).collect(),
561 ),
562 };
563 mempool
564 .clone()
565 .oneshot(request)
566 // The response just indicates if processing was queued or not; ignore it
567 .map_ok(|_resp| zn::Response::Nil)
568 .boxed()
569 }
570 zn::Request::AdvertiseBlock(hash, advertiser) => {
571 block_downloads.download_and_verify(hash, advertiser);
572 async { Ok(zn::Response::Nil) }.boxed()
573 }
574 // The size of this response is limited by the `Connection` state machine in the network layer
575 zn::Request::MempoolTransactionIds => {
576 mempool.clone().oneshot(mempool::Request::TransactionIds).map_ok(|resp| match resp {
577 mempool::Response::TransactionIds(transaction_ids) if transaction_ids.is_empty() => zn::Response::Nil,
578 mempool::Response::TransactionIds(transaction_ids) => zn::Response::TransactionIds(transaction_ids.into_iter().collect()),
579 _ => unreachable!("Mempool component should always respond to a `TransactionIds` request with a `TransactionIds` response"),
580 })
581 .boxed()
582 }
583 zn::Request::Ping(_) => {
584 unreachable!("ping requests are handled internally");
585 }
586
587 zn::Request::AdvertiseBlockToAll(_) => unreachable!("should always be decoded as `AdvertiseBlock` request")
588 }
589 }
590}