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