Skip to main content

zebrad/components/
sync.rs

1//! The syncer downloads and verifies large numbers of blocks from peers to Zebra.
2//!
3//! It is used when Zebra is a long way behind the current chain tip.
4
5use std::{
6    cmp::max,
7    collections::{HashMap, HashSet},
8    convert,
9    pin::Pin,
10    task::Poll,
11    time::Duration,
12};
13
14use color_eyre::eyre::{eyre, Report};
15use futures::stream::{FuturesUnordered, StreamExt};
16use indexmap::IndexSet;
17use serde::{Deserialize, Serialize};
18use tokio::{
19    sync::{mpsc, watch},
20    task::JoinError,
21    time::{sleep, timeout},
22};
23use tower::{
24    builder::ServiceBuilder, limit::ConcurrencyLimit, retry::Retry, timeout::Timeout, Service,
25    ServiceExt,
26};
27
28use zebra_chain::{
29    block::{self, Height, HeightDiff},
30    chain_tip::ChainTip,
31};
32use zebra_consensus::{error::TransactionError, RouterError, VerifyBlockError};
33use zebra_network as zn;
34use zebra_state as zs;
35
36use crate::{
37    components::sync::downloads::BlockDownloadVerifyError, config::ZebradConfig, BoxError,
38};
39
40mod downloads;
41pub mod end_of_support;
42mod gossip;
43mod hedge;
44mod progress;
45mod recent_sync_lengths;
46mod status;
47
48#[cfg(test)]
49mod tests;
50
51use downloads::Downloads;
52use hedge::SuccessHedge;
53
54pub use downloads::VERIFICATION_PIPELINE_SCALING_MULTIPLIER;
55pub use gossip::{gossip_best_tip_block_hashes, BlockGossipError};
56pub use progress::show_block_chain_progress;
57pub use recent_sync_lengths::RecentSyncLengths;
58pub use status::SyncStatus;
59
60/// Match the missing-block errors used by both hedging and sync recovery.
61fn is_not_found(error: &dyn std::error::Error) -> bool {
62    // TODO: improve this by checking the type (#2908).
63    format!("{error:?}").contains("NotFound")
64}
65
66/// Controls the number of peers used for each ObtainTips and ExtendTips request.
67const FANOUT: usize = 3;
68
69/// Controls how many times we will retry each block download.
70///
71/// Failing block downloads is important because it defends against peers who
72/// feed us bad hashes. But spurious failures of valid blocks cause the syncer to
73/// restart from the previous checkpoint, potentially re-downloading blocks.
74///
75/// We also hedge requests, so we may retry up to twice this many times. Hedged
76/// retries may be concurrent, inner retries are sequential.
77const BLOCK_DOWNLOAD_RETRY_LIMIT: usize = 3;
78
79/// Controls how many times the syncer will re-request a block whose download
80/// failed because no peer delivered it (a `NotFound`), before giving up and
81/// letting the normal tip re-walk handle it.
82///
83/// Without this re-request, a single missing block at the checkpoint frontier
84/// is dropped and never re-fetched, wedging the whole verify pipeline until the
85/// 8-minute `BLOCK_VERIFY_TIMEOUT` fires (#5709). Each attempt already goes
86/// through the tower-level `BLOCK_DOWNLOAD_RETRY_LIMIT` (and hedging), so this
87/// is a coarse, hash-scoped retry on top of an exhausted per-request retry.
88const MAX_BLOCK_REOBTAIN_RETRIES: u8 = 3;
89
90/// The fewest `TransparentInputNotFound` drops without a verified block before the sync
91/// restarts, so a low `full_verify_concurrency_limit` can't disable the #11168 exemption.
92const MIN_UTXO_RACE_DROPS_BEFORE_RESTART: usize = 4;
93
94/// A lower bound on the user-specified checkpoint verification concurrency limit.
95///
96/// Set to the maximum checkpoint interval, so the pipeline holds around a checkpoint's
97/// worth of blocks.
98///
99/// ## Security
100///
101/// If a malicious node is chosen for an ObtainTips or ExtendTips request, it can
102/// provide up to 500 malicious block hashes. These block hashes will be
103/// distributed across all available peers. Assuming there are around 50 connected
104/// peers, the malicious node will receive approximately 10 of those block requests.
105///
106/// Malicious deserialized blocks can take up a large amount of RAM, see
107/// [`super::inbound::downloads::MAX_INBOUND_CONCURRENCY`] and #1880 for details.
108/// So we want to keep the lookahead limit reasonably small.
109///
110/// Once these malicious blocks start failing validation, the syncer will cancel all
111/// the pending download and verify tasks, drop all the blocks, and start a new
112/// ObtainTips with a new set of peers.
113pub const MIN_CHECKPOINT_CONCURRENCY_LIMIT: usize = zebra_consensus::MAX_CHECKPOINT_HEIGHT_GAP;
114
115/// The default for the user-specified lookahead limit.
116///
117/// See [`MIN_CHECKPOINT_CONCURRENCY_LIMIT`] for details.
118pub const DEFAULT_CHECKPOINT_CONCURRENCY_LIMIT: usize = MAX_TIPS_RESPONSE_HASH_COUNT * 2;
119
120/// A lower bound on the user-specified concurrency limit.
121///
122/// If the concurrency limit is 0, Zebra can't download or verify any blocks.
123pub const MIN_CONCURRENCY_LIMIT: usize = 1;
124
125/// The expected maximum number of hashes in an ObtainTips or ExtendTips response.
126///
127/// This is used to allow block heights that are slightly beyond the lookahead limit,
128/// but still limit the number of blocks in the pipeline between the downloader and
129/// the state.
130///
131/// See [`MIN_CHECKPOINT_CONCURRENCY_LIMIT`] for details.
132pub const MAX_TIPS_RESPONSE_HASH_COUNT: usize = 500;
133
134/// Controls how long we wait for a tips response to return.
135///
136/// ## Correctness
137///
138/// If this timeout is removed (or set too high), the syncer will sometimes hang.
139///
140/// If this timeout is set too low, the syncer will sometimes get stuck in a
141/// failure loop.
142pub const TIPS_RESPONSE_TIMEOUT: Duration = Duration::from_secs(6);
143
144/// Controls how long we wait between gossiping successive blocks or transactions.
145///
146/// ## Correctness
147///
148/// If this timeout is set too high, blocks and transactions won't propagate through
149/// the network efficiently.
150///
151/// If this timeout is set too low, the peer set and remote peers can get overloaded.
152pub const PEER_GOSSIP_DELAY: Duration = Duration::from_secs(7);
153
154/// The per-peer wire-response budget used to derive complete download deadlines.
155///
156/// Connections start this timeout at dispatch, not while waiting for capacity. The complete sync
157/// operation is separately bounded by [`BLOCK_DOWNLOAD_ROUND_TIMEOUT`].
158pub(super) const BLOCK_DOWNLOAD_TIMEOUT: Duration = zn::constants::REQUEST_TIMEOUT;
159
160/// Bounds one complete block download round, including readiness, capacity waits and both hedges.
161///
162/// Reuse the existing four-attempt response budget rather than charging capacity waits as attempts.
163/// The retry limit is the constant 3, so conversion to u32 is lossless.
164const BLOCK_DOWNLOAD_ROUND_TIMEOUT: Duration =
165    BLOCK_DOWNLOAD_TIMEOUT.saturating_mul(BLOCK_DOWNLOAD_RETRY_LIMIT as u32 + 1);
166
167/// Controls how long we wait for a block verify request to complete.
168///
169/// This timeout makes sure that the syncer doesn't hang when:
170///  - the lookahead queue is full, and
171///  - all pending verifications:
172///    - are waiting on a missing download request,
173///    - are waiting on a download or verify request that has failed, but we have
174///      deliberately ignored the error,
175///    - are for blocks a long way ahead of the current tip, or
176///    - are for invalid blocks which will never verify, because they depend on
177///      missing blocks or transactions.
178///
179/// These conditions can happen during normal operation - they are not bugs.
180///
181/// This timeout also mitigates or hides the following kinds of bugs:
182///  - all pending verifications:
183///    - are waiting on a download or verify request that has failed, but we have
184///      accidentally dropped the error,
185///    - are waiting on a download request that has hung inside Zebra,
186///    - are on tokio threads that are waiting for blocked operations.
187///
188/// ## Correctness
189///
190/// If this timeout is removed (or set too high), the syncer will sometimes hang.
191///
192/// If this timeout is set too low, the syncer will sometimes get stuck in a
193/// failure loop.
194///
195/// We've observed spurious 15 minute timeouts when a lot of blocks are being committed to
196/// the state. But there are also some blocks that seem to hang entirely, and never return.
197///
198/// So we allow about half the spurious timeout, which might cause some re-downloads.
199pub(super) const BLOCK_VERIFY_TIMEOUT: Duration = Duration::from_secs(8 * 60);
200
201/// A shorter timeout used for the first few blocks after the final checkpoint.
202///
203/// This is a workaround for bug #5125, where the first fully validated blocks
204/// after the final checkpoint fail with a timeout, due to a UTXO race condition.
205const FINAL_CHECKPOINT_BLOCK_VERIFY_TIMEOUT: Duration = Duration::from_secs(2 * 60);
206
207/// The number of blocks after the final checkpoint that get the shorter timeout.
208///
209/// We've only seen this error on the first few blocks after the final checkpoint.
210const FINAL_CHECKPOINT_BLOCK_VERIFY_TIMEOUT_LIMIT: HeightDiff = 100;
211
212/// Controls how long we wait to restart syncing after finishing a sync run.
213///
214/// This delay should be long enough to:
215///   - allow zcashd peers to process pending requests. If the node only has a
216///     few peers, we want to clear as much peer state as possible. In
217///     particular, zcashd sends "next block range" hints, based on zcashd's
218///     internal model of our sync progress. But we want to discard these hints,
219///     so they don't get confused with ObtainTips and ExtendTips responses, and
220///   - allow in-progress downloads to time out.
221///
222/// This delay is particularly important on instances with slow or unreliable
223/// networks, and on testnet, which has a small number of slow peers.
224///
225/// Using a prime number makes sure that syncer fanouts don't synchronise with other crawls.
226///
227/// ## Correctness
228///
229/// If this delay is removed (or set too low), the syncer will
230/// sometimes get stuck in a failure loop, due to leftover downloads from
231/// previous sync runs.
232const SYNC_RESTART_DELAY: Duration = Duration::from_secs(67);
233
234/// In regtest, use a much shorter restart delay so that downstream nodes pick up
235/// newly-mined blocks quickly (e.g. after `generate(N)` in integration tests).
236/// The default 67-second delay exceeds the typical `sync_all` timeout of 60 seconds.
237const REGTEST_SYNC_RESTART_DELAY: Duration = Duration::from_secs(2);
238
239/// Controls how long we wait to retry a failed attempt to download
240/// and verify the genesis block.
241///
242/// This timeout gives the crawler time to find better peers.
243///
244/// ## Security
245///
246/// If this timeout is removed (or set too low), Zebra will immediately retry
247/// to download and verify the genesis block from its peers. This can cause
248/// a denial of service on those peers.
249///
250/// If this timeout is too short, old or buggy nodes will keep making useless
251/// network requests. If there are a lot of them, it could overwhelm the network.
252const GENESIS_TIMEOUT_RETRY: Duration = Duration::from_secs(10);
253
254/// Sync configuration section.
255#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
256#[serde(deny_unknown_fields, default)]
257pub struct Config {
258    /// The number of parallel block download requests.
259    ///
260    /// This is set to a low value by default, to avoid task and
261    /// network contention. Increasing this value may improve
262    /// performance on machines with a fast network connection.
263    #[serde(alias = "max_concurrent_block_requests")]
264    pub download_concurrency_limit: usize,
265
266    /// The number of blocks submitted in parallel to the checkpoint verifier.
267    ///
268    /// Increasing this limit increases the buffer size, so it reduces
269    /// the impact of an individual block request failing. However, it
270    /// also increases memory and CPU usage if block validation stalls,
271    /// or there are some large blocks in the pipeline.
272    ///
273    /// The block size limit is 2MB, so in theory, this could represent multiple
274    /// gigabytes of data, if we downloaded arbitrary blocks. However,
275    /// because we randomly load balance outbound requests, and separate
276    /// block download from obtaining block hashes, an adversary would
277    /// have to control a significant fraction of our peers to lead us
278    /// astray.
279    ///
280    /// For reliable checkpoint syncing, Zebra enforces a
281    /// [`MIN_CHECKPOINT_CONCURRENCY_LIMIT`].
282    ///
283    /// This is set to a high value by default, to avoid verification pipeline stalls.
284    /// Decreasing this value reduces RAM usage.
285    #[serde(alias = "lookahead_limit")]
286    pub checkpoint_verify_concurrency_limit: usize,
287
288    /// The number of blocks submitted in parallel to the full verifier.
289    ///
290    /// This is set to a low value by default, to avoid verification timeouts on large blocks.
291    /// Increasing this value may improve performance on machines with many cores.
292    pub full_verify_concurrency_limit: usize,
293
294    /// The number of threads used to verify signatures, proofs, and other CPU-intensive code.
295    ///
296    /// If the number of threads is not configured or zero, Zebra uses the number of logical cores.
297    /// If the number of logical cores can't be detected, Zebra uses one thread.
298    /// For details, see [the `rayon` documentation](https://docs.rs/rayon/latest/rayon/struct.ThreadPoolBuilder.html#method.num_threads).
299    pub parallel_cpu_threads: usize,
300}
301
302impl Default for Config {
303    fn default() -> Self {
304        Self {
305            // 2/3 of the default outbound peer limit.
306            download_concurrency_limit: 50,
307
308            // A few max-length checkpoints.
309            checkpoint_verify_concurrency_limit: DEFAULT_CHECKPOINT_CONCURRENCY_LIMIT,
310
311            // This default is deliberately very low, so Zebra can verify a few large blocks in under 60 seconds,
312            // even on machines with only a few cores.
313            //
314            // This lets users see the committed block height changing in every progress log,
315            // and avoids hangs due to out-of-order verifications flooding the CPUs.
316            //
317            // TODO:
318            // - limit full verification concurrency based on block transaction counts?
319            // - move more disk work to blocking tokio threads,
320            //   and CPU work to the rayon thread pool inside blocking tokio threads
321            full_verify_concurrency_limit: 20,
322
323            // Use one thread per CPU.
324            //
325            // If this causes tokio executor starvation, move CPU-intensive tasks to rayon threads,
326            // or reserve a few cores for tokio threads, based on `num_cpus()`.
327            parallel_cpu_threads: 0,
328        }
329    }
330}
331
332/// Helps work around defects in the bitcoin protocol by checking whether
333/// the returned hashes actually extend a chain tip.
334#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
335struct CheckedTip {
336    tip: block::Hash,
337    expected_next: block::Hash,
338}
339
340pub struct ChainSync<ZN, ZS, ZSR, ZV, ZSTip>
341where
342    ZN: Service<zn::Request, Response = zn::Response, Error = BoxError>
343        + Send
344        + Sync
345        + Clone
346        + 'static,
347    ZN::Future: Send + 'static,
348    ZS: Service<zs::Request, Response = zs::Response, Error = BoxError>
349        + Send
350        + Sync
351        + Clone
352        + 'static,
353    ZS::Future: Send,
354    ZSR: Service<zs::ReadRequest, Response = zs::ReadResponse, Error = BoxError>
355        + Send
356        + Sync
357        + Clone
358        + 'static,
359    ZSR::Future: Send,
360    ZV: Service<zebra_consensus::Request, Response = block::Hash, Error = BoxError>
361        + Send
362        + Sync
363        + Clone
364        + 'static,
365    ZV::Future: Send,
366    ZSTip: ChainTip + Clone + Send + 'static,
367{
368    // Configuration
369    //
370    /// The genesis hash for the configured network
371    genesis_hash: block::Hash,
372
373    /// The largest block height for the checkpoint verifier, based on the current config.
374    max_checkpoint_height: Height,
375
376    /// The configured checkpoint verification concurrency limit, after applying the minimum limit.
377    checkpoint_verify_concurrency_limit: usize,
378
379    /// The configured full verification concurrency limit, after applying the minimum limit.
380    full_verify_concurrency_limit: usize,
381
382    /// Whether the node is running on regtest. Used to apply a shorter sync restart delay.
383    is_regtest: bool,
384
385    // Services
386    //
387    /// A network service which is used to perform ObtainTips and ExtendTips
388    /// requests.
389    ///
390    /// Has no retry logic, because failover is handled using fanout.
391    tip_network: Timeout<ZN>,
392
393    /// A service which downloads and verifies blocks, using the provided
394    /// network and verifier services.
395    downloads: Pin<
396        Box<
397            Downloads<
398                ConcurrencyLimit<SuccessHedge<ConcurrencyLimit<Retry<zn::RetryLimit, ZN>>>>,
399                Timeout<ZV>,
400                ZSR,
401                ZSTip,
402            >,
403        >,
404    >,
405
406    /// The cached block chain state.
407    state: ZS,
408
409    /// Allows efficient access to the best tip of the blockchain.
410    latest_chain_tip: ZSTip,
411
412    // Internal sync state
413    //
414    /// The tips that the syncer is currently following.
415    prospective_tips: HashSet<CheckedTip>,
416
417    /// The lengths of recent sync responses.
418    recent_syncs: RecentSyncLengths,
419
420    /// Receiver that is `true` when the downloader is past the lookahead limit.
421    /// This is based on the downloaded block height and the state tip height.
422    past_lookahead_limit_receiver: zs::WatchReceiver<bool>,
423
424    /// Sender for reporting peer addresses that advertised unexpectedly invalid transactions.
425    misbehavior_sender: mpsc::Sender<zn::MisbehaviorReport>,
426
427    /// Blocks whose download failed with `NotFound` and should be re-requested on
428    /// the next sync round, instead of being silently dropped (#5709).
429    reobtain_hashes: IndexSet<block::Hash>,
430
431    /// Per-hash count of how many times a `NotFound` block has been re-requested,
432    /// bounded by [`MAX_BLOCK_REOBTAIN_RETRIES`].
433    block_reobtain_retries: HashMap<block::Hash, u8>,
434
435    /// `TransparentInputNotFound` drops since the last verified block, bounded by the
436    /// full-verify concurrency limit so a poisoned hash batch can't suppress restarts.
437    utxo_race_drops: usize,
438}
439
440/// Polls the network to determine whether further blocks are available and
441/// downloads them.
442///
443/// This component is used for initial block sync, but the `Inbound` service is
444/// responsible for participating in the gossip protocols used for block
445/// diffusion.
446impl<ZN, ZS, ZSR, ZV, ZSTip> ChainSync<ZN, ZS, ZSR, ZV, ZSTip>
447where
448    ZN: Service<zn::Request, Response = zn::Response, Error = BoxError>
449        + Send
450        + Sync
451        + Clone
452        + 'static,
453    ZN::Future: Send + 'static,
454    ZS: Service<zs::Request, Response = zs::Response, Error = BoxError>
455        + Send
456        + Sync
457        + Clone
458        + 'static,
459    ZS::Future: Send,
460    ZSR: Service<zs::ReadRequest, Response = zs::ReadResponse, Error = BoxError>
461        + Send
462        + Sync
463        + Clone
464        + 'static,
465    ZSR::Future: Send,
466    ZV: Service<zebra_consensus::Request, Response = block::Hash, Error = BoxError>
467        + Send
468        + Sync
469        + Clone
470        + 'static,
471    ZV::Future: Send,
472    ZSTip: ChainTip + Clone + Send + 'static,
473{
474    /// Returns a new syncer instance, using:
475    ///  - chain: the zebra-chain `Network` to download (Mainnet or Testnet)
476    ///  - peers: the zebra-network peers to contact for downloads
477    ///  - verifier: the zebra-consensus verifier that checks the chain
478    ///  - state: the zebra-state that stores the chain
479    ///  - read_state: a read-only handle to the zebra-state, used to check downloaded block heights
480    ///  - latest_chain_tip: the latest chain tip from `state`
481    ///
482    /// Also returns a [`SyncStatus`] to check if the syncer has likely reached the chain tip.
483    #[allow(clippy::too_many_arguments)]
484    pub fn new(
485        config: &ZebradConfig,
486        max_checkpoint_height: Height,
487        peers: ZN,
488        verifier: ZV,
489        state: ZS,
490        read_state: ZSR,
491        latest_chain_tip: ZSTip,
492        misbehavior_sender: mpsc::Sender<zn::MisbehaviorReport>,
493    ) -> (Self, SyncStatus) {
494        let mut download_concurrency_limit = config.sync.download_concurrency_limit;
495        let mut checkpoint_verify_concurrency_limit =
496            config.sync.checkpoint_verify_concurrency_limit;
497        let mut full_verify_concurrency_limit = config.sync.full_verify_concurrency_limit;
498
499        if download_concurrency_limit < MIN_CONCURRENCY_LIMIT {
500            warn!(
501                "configured download concurrency limit {} too low, increasing to {}",
502                config.sync.download_concurrency_limit, MIN_CONCURRENCY_LIMIT,
503            );
504
505            download_concurrency_limit = MIN_CONCURRENCY_LIMIT;
506        }
507
508        if checkpoint_verify_concurrency_limit < MIN_CHECKPOINT_CONCURRENCY_LIMIT {
509            warn!(
510                "configured checkpoint verify concurrency limit {} too low, increasing to {}",
511                config.sync.checkpoint_verify_concurrency_limit, MIN_CHECKPOINT_CONCURRENCY_LIMIT,
512            );
513
514            checkpoint_verify_concurrency_limit = MIN_CHECKPOINT_CONCURRENCY_LIMIT;
515        }
516
517        if full_verify_concurrency_limit < MIN_CONCURRENCY_LIMIT {
518            warn!(
519                "configured full verify concurrency limit {} too low, increasing to {}",
520                config.sync.full_verify_concurrency_limit, MIN_CONCURRENCY_LIMIT,
521            );
522
523            full_verify_concurrency_limit = MIN_CONCURRENCY_LIMIT;
524        }
525
526        let tip_network = Timeout::new(peers.clone(), TIPS_RESPONSE_TIMEOUT);
527
528        // Capacity waiting stays inside the network request and does not consume a retry.
529        // Connections start response deadlines at wire dispatch; Downloads bounds readiness
530        // and the complete hedged retry operation with one absolute deadline.
531        // Hedge entire retry sequences so individual peer failures are retried before hedging.
532        // The inner limit bounds active sequences across both arms; exhausted arms release it
533        // immediately so their delayed siblings can start even at a limit of one.
534        // The outer limit bounds logical downloads, holding admission while any arm can succeed.
535        // Success, final failure, or cancellation releases it before block verification starts.
536        let block_network = SuccessHedge::new(
537            ServiceBuilder::new()
538                .concurrency_limit(download_concurrency_limit)
539                .retry(zn::RetryLimit::new(BLOCK_DOWNLOAD_RETRY_LIMIT))
540                .service(peers),
541        );
542        let block_network = ConcurrencyLimit::new(block_network, download_concurrency_limit);
543
544        // We apply a timeout to the verifier to avoid hangs due to missing earlier blocks.
545        let verifier = Timeout::new(verifier, BLOCK_VERIFY_TIMEOUT);
546
547        let (sync_status, recent_syncs) = SyncStatus::new_for_network(&config.network.network);
548
549        let (past_lookahead_limit_sender, past_lookahead_limit_receiver) = watch::channel(false);
550        let past_lookahead_limit_receiver = zs::WatchReceiver::new(past_lookahead_limit_receiver);
551
552        let downloads = Box::pin(Downloads::new(
553            block_network,
554            verifier,
555            read_state,
556            latest_chain_tip.clone(),
557            past_lookahead_limit_sender,
558            max(
559                checkpoint_verify_concurrency_limit,
560                full_verify_concurrency_limit,
561            ),
562            max_checkpoint_height,
563        ));
564
565        let new_syncer = Self {
566            genesis_hash: config.network.network.genesis_hash(),
567            max_checkpoint_height,
568            checkpoint_verify_concurrency_limit,
569            full_verify_concurrency_limit,
570            is_regtest: config.network.network.is_regtest(),
571            tip_network,
572            downloads,
573            state,
574            latest_chain_tip,
575            prospective_tips: HashSet::new(),
576            recent_syncs,
577            past_lookahead_limit_receiver,
578            misbehavior_sender,
579            reobtain_hashes: IndexSet::new(),
580            block_reobtain_retries: HashMap::new(),
581            utxo_race_drops: 0,
582        };
583
584        (new_syncer, sync_status)
585    }
586
587    /// Runs the syncer to synchronize the chain and keep it synchronized.
588    #[instrument(skip(self))]
589    pub async fn sync(mut self) -> Result<(), Report> {
590        // We can't download the genesis block using our normal algorithm,
591        // due to protocol limitations
592        self.request_genesis().await?;
593
594        loop {
595            if self.try_to_sync().await.is_err() {
596                self.downloads.cancel_all();
597            }
598
599            self.update_metrics();
600
601            let restart_delay = if self.is_regtest {
602                REGTEST_SYNC_RESTART_DELAY
603            } else {
604                SYNC_RESTART_DELAY
605            };
606            info!(
607                timeout = ?restart_delay,
608                state_tip = ?self.latest_chain_tip.best_tip_height(),
609                "waiting to restart sync"
610            );
611            sleep(restart_delay).await;
612        }
613    }
614
615    /// Tries to synchronize the chain as far as it can.
616    ///
617    /// Obtains some prospective tips and iteratively tries to extend them and download the missing
618    /// blocks.
619    ///
620    /// Returns `Ok` if it was able to synchronize as much of the chain as it could, and then ran
621    /// out of prospective tips. This happens when synchronization finishes or if Zebra ended up
622    /// following a fork. Either way, Zebra should attempt to obtain some more tips.
623    ///
624    /// Returns `Err` if there was an unrecoverable error and restarting the synchronization is
625    /// necessary. This includes outer timeouts, where an entire syncing step takes an extremely
626    /// long time. (These usually indicate hangs.)
627    #[instrument(skip(self))]
628    async fn try_to_sync(&mut self) -> Result<(), Report> {
629        self.prospective_tips = HashSet::new();
630
631        self.reobtain_hashes.clear();
632        self.block_reobtain_retries.clear();
633        self.utxo_race_drops = 0;
634
635        info!(
636            state_tip = ?self.latest_chain_tip.best_tip_height(),
637            "starting sync, obtaining new tips"
638        );
639        let mut extra_hashes = timeout(SYNC_RESTART_DELAY, self.obtain_tips())
640            .await
641            .map_err(Into::into)
642            // TODO: replace with flatten() when it stabilises (#70142)
643            .and_then(convert::identity)
644            .map_err(|e| {
645                info!("temporary error obtaining tips: {:#}", e);
646                e
647            })?;
648        self.update_metrics();
649
650        while !self.prospective_tips.is_empty() || !extra_hashes.is_empty() {
651            // Avoid hangs due to service readiness or other internal operations
652            extra_hashes = timeout(BLOCK_VERIFY_TIMEOUT, self.try_to_sync_once(extra_hashes))
653                .await
654                .map_err(Into::into)
655                // TODO: replace with flatten() when it stabilises (#70142)
656                .and_then(convert::identity)?;
657        }
658
659        info!("exhausted prospective tip set");
660
661        Ok(())
662    }
663
664    /// Tries to synchronize the chain once, using the existing `extra_hashes`.
665    ///
666    /// Tries to extend the existing tips and download the missing blocks.
667    ///
668    /// Returns `Ok(extra_hashes)` if it was able to extend once and synchronize sone of the chain.
669    /// Returns `Err` if there was an unrecoverable error and restarting the synchronization is
670    /// necessary.
671    #[instrument(skip(self, extra_hashes))]
672    async fn try_to_sync_once(
673        &mut self,
674        mut extra_hashes: IndexSet<block::Hash>,
675    ) -> Result<IndexSet<block::Hash>, Report> {
676        // Check whether any block tasks are currently ready.
677        while let Poll::Ready(Some(rsp)) = futures::poll!(self.downloads.next()) {
678            // Some temporary errors are ignored, and syncing continues with other blocks.
679            // If it turns out they were actually important, syncing will run out of blocks, and
680            // the syncer will reset itself.
681            self.handle_block_response(rsp)?;
682        }
683        // Re-request any blocks that just failed with `NotFound`, before pausing
684        // on the lookahead limit (#5709).
685        self.reobtain_missing_blocks().await;
686        self.update_metrics();
687
688        // Pause new downloads while the syncer or downloader are past their lookahead limits.
689        //
690        // To avoid a deadlock or long waits for blocks to expire, we ignore the download
691        // lookahead limit when there are only a small number of blocks waiting.
692        while self.downloads.in_flight() >= self.lookahead_limit(extra_hashes.len())
693            || (self.downloads.in_flight() >= self.lookahead_limit(extra_hashes.len()) / 2
694                && self.past_lookahead_limit_receiver.cloned_watch_data())
695        {
696            trace!(
697                tips.len = self.prospective_tips.len(),
698                in_flight = self.downloads.in_flight(),
699                extra_hashes = extra_hashes.len(),
700                lookahead_limit = self.lookahead_limit(extra_hashes.len()),
701                state_tip = ?self.latest_chain_tip.best_tip_height(),
702                "waiting for pending blocks",
703            );
704
705            let response = self.downloads.next().await.expect("downloads is nonempty");
706
707            self.handle_block_response(response)?;
708            // A block that just failed with `NotFound` is what unblocks the
709            // verifier, so re-request it now rather than waiting for the pause
710            // loop to clear — which it cannot until this block arrives (#5709).
711            self.reobtain_missing_blocks().await;
712            self.update_metrics();
713        }
714
715        // Once we're below the lookahead limit, we can request more blocks or hashes.
716        if !extra_hashes.is_empty() {
717            debug!(
718                tips.len = self.prospective_tips.len(),
719                in_flight = self.downloads.in_flight(),
720                extra_hashes = extra_hashes.len(),
721                lookahead_limit = self.lookahead_limit(extra_hashes.len()),
722                state_tip = ?self.latest_chain_tip.best_tip_height(),
723                "requesting more blocks",
724            );
725
726            let response = self.request_blocks(extra_hashes).await;
727            extra_hashes = Self::handle_hash_response(response)?;
728        } else {
729            info!(
730                tips.len = self.prospective_tips.len(),
731                in_flight = self.downloads.in_flight(),
732                extra_hashes = extra_hashes.len(),
733                lookahead_limit = self.lookahead_limit(extra_hashes.len()),
734                state_tip = ?self.latest_chain_tip.best_tip_height(),
735                "extending tips",
736            );
737
738            extra_hashes = self.extend_tips().await.map_err(|e| {
739                info!("temporary error extending tips: {:#}", e);
740                e
741            })?;
742        }
743        self.update_metrics();
744
745        Ok(extra_hashes)
746    }
747
748    /// Re-issues downloads for blocks that failed with `NotFound` (#5709).
749    ///
750    /// These are re-requested even while the download pipeline is past its
751    /// lookahead limit, because a missing low block is exactly what stops the
752    /// checkpoint verifier from advancing. Waiting for the lookahead pause to
753    /// clear would deadlock — the pause cannot clear until this block arrives.
754    /// The per-hash retry count is bounded by [`MAX_BLOCK_REOBTAIN_RETRIES`].
755    async fn reobtain_missing_blocks(&mut self) {
756        if self.reobtain_hashes.is_empty() {
757            return;
758        }
759
760        for hash in std::mem::take(&mut self.reobtain_hashes) {
761            // The block was removed from the in-flight set when its download
762            // failed, so this re-queues it. A residual duplicate/queue error is
763            // benign — it means the block is already being handled.
764            if let Err(error) = self.downloads.download_and_verify(hash).await {
765                trace!(?hash, ?error, "re-download of missing block not queued");
766            }
767        }
768    }
769
770    /// Given a block_locator list fan out request for subsequent hashes to
771    /// multiple peers
772    #[instrument(skip(self))]
773    async fn obtain_tips(&mut self) -> Result<IndexSet<block::Hash>, Report> {
774        let stage_start = std::time::Instant::now();
775
776        let block_locator = self
777            .state
778            .ready()
779            .await
780            .map_err(|e| eyre!(e))?
781            .call(zebra_state::Request::BlockLocator)
782            .await
783            .map(|response| match response {
784                zebra_state::Response::BlockLocator(block_locator) => block_locator,
785                _ => unreachable!(
786                    "GetBlockLocator request can only result in Response::BlockLocator"
787                ),
788            })
789            .map_err(|e| eyre!(e))?;
790
791        debug!(
792            tip = ?block_locator.first().expect("we have at least one block locator object"),
793            ?block_locator,
794            "got block locator and trying to obtain new chain tips"
795        );
796
797        let mut requests = FuturesUnordered::new();
798        for attempt in 0..FANOUT {
799            if attempt > 0 {
800                // Let other tasks run, so we're more likely to choose a different peer.
801                //
802                // TODO: move fanouts into the PeerSet, so we always choose different peers (#2214)
803                tokio::task::yield_now().await;
804            }
805
806            let ready_tip_network = self.tip_network.ready().await;
807            requests.push(tokio::spawn(ready_tip_network.map_err(|e| eyre!(e))?.call(
808                zn::Request::FindBlocks {
809                    known_blocks: block_locator.clone(),
810                    stop: None,
811                },
812            )));
813        }
814
815        let mut download_set = IndexSet::new();
816        while let Some(res) = requests.next().await {
817            match res
818                .unwrap_or_else(|e @ JoinError { .. }| {
819                    if e.is_panic() {
820                        panic!("panic in obtain tips task: {e:?}");
821                    } else {
822                        info!(
823                            "task error during obtain tips task: {e:?},\
824                     is Zebra shutting down?"
825                        );
826                        Err(e.into())
827                    }
828                })
829                .map_err::<Report, _>(|e| eyre!(e))
830            {
831                Ok(zn::Response::BlockHashes(hashes)) => {
832                    trace!(?hashes);
833
834                    let hashes = hashes.as_slice();
835                    if hashes.is_empty() {
836                        continue;
837                    }
838
839                    let mut first_unknown = None;
840                    for (i, &hash) in hashes.iter().enumerate() {
841                        if !self.state_contains(hash).await? {
842                            first_unknown = Some(i);
843                            break;
844                        }
845                    }
846
847                    debug!(hashes.len = ?hashes.len(), ?first_unknown);
848
849                    let unknown_hashes = if let Some(index) = first_unknown {
850                        &hashes[index..]
851                    } else {
852                        continue;
853                    };
854
855                    trace!(?unknown_hashes);
856
857                    if let Some(end) = unknown_hashes.rchunks_exact(2).next() {
858                        let new_tip = CheckedTip {
859                            tip: end[0],
860                            expected_next: end[1],
861                        };
862
863                        // Make sure we get the same tips, regardless of the
864                        // order of peer responses
865                        if !download_set.contains(&new_tip.expected_next) {
866                            debug!(
867                                ?new_tip,
868                                "adding new prospective tip, and removing existing tips in the \
869                                new block hash list",
870                            );
871                            self.prospective_tips
872                                .retain(|t| !unknown_hashes.contains(&t.expected_next));
873                            self.prospective_tips.insert(new_tip);
874                        } else {
875                            debug!(
876                                ?new_tip,
877                                "discarding prospective tip: already in download set"
878                            );
879                        }
880                    } else {
881                        debug!("downloading response that extends only one block");
882                    }
883
884                    // security: the first response determines our download order
885                    //
886                    // TODO: can we make the download order independent of response order?
887                    let prev_download_len = download_set.len();
888                    download_set.extend(unknown_hashes);
889                    let new_download_len = download_set.len();
890                    let new_hashes = new_download_len - prev_download_len;
891                    debug!(new_hashes, "added hashes to download set");
892                    metrics::histogram!("sync.obtain.response.hash.count")
893                        .record(new_hashes as f64);
894                }
895                Ok(_) => unreachable!("network returned wrong response"),
896                // We ignore this error because we made multiple fanout requests.
897                Err(e) => debug!(?e),
898            }
899        }
900
901        debug!(?self.prospective_tips);
902
903        // Check that the new tips we got are actually unknown.
904        for hash in &download_set {
905            debug!(?hash, "checking if state contains hash");
906            if self.state_contains(*hash).await? {
907                return Err(eyre!("queued download of hash behind our chain tip"));
908            }
909        }
910
911        let new_downloads = download_set.len();
912        debug!(new_downloads, "queueing new downloads");
913        metrics::gauge!("sync.obtain.queued.hash.count").set(new_downloads as f64);
914
915        // security: use the actual number of new downloads from all peers,
916        // so the last peer to respond can't toggle our mempool
917        self.recent_syncs.push_obtain_tips_length(new_downloads);
918
919        let response = self.request_blocks(download_set).await;
920
921        metrics::histogram!("sync.stage.duration_seconds", "stage" => "obtain_tips")
922            .record(stage_start.elapsed().as_secs_f64());
923
924        Self::handle_hash_response(response).map_err(Into::into)
925    }
926
927    #[instrument(skip(self))]
928    async fn extend_tips(&mut self) -> Result<IndexSet<block::Hash>, Report> {
929        let stage_start = std::time::Instant::now();
930
931        let tips = std::mem::take(&mut self.prospective_tips);
932
933        let mut download_set = IndexSet::new();
934        debug!(tips = ?tips.len(), "trying to extend chain tips");
935        for tip in tips {
936            debug!(?tip, "asking peers to extend chain tip");
937            let mut responses = FuturesUnordered::new();
938            for attempt in 0..FANOUT {
939                if attempt > 0 {
940                    // Let other tasks run, so we're more likely to choose a different peer.
941                    //
942                    // TODO: move fanouts into the PeerSet, so we always choose different peers (#2214)
943                    tokio::task::yield_now().await;
944                }
945
946                let ready_tip_network = self.tip_network.ready().await;
947                responses.push(tokio::spawn(ready_tip_network.map_err(|e| eyre!(e))?.call(
948                    zn::Request::FindBlocks {
949                        known_blocks: vec![tip.tip],
950                        stop: None,
951                    },
952                )));
953            }
954            while let Some(res) = responses.next().await {
955                match res
956                    .expect("panic in spawned extend tips request")
957                    .map_err::<Report, _>(|e| eyre!(e))
958                {
959                    Ok(zn::Response::BlockHashes(hashes)) => {
960                        debug!(first = ?hashes.first(), len = ?hashes.len());
961                        trace!(?hashes);
962
963                        // Legacy zcashd nodes could prepend an unrelated hash
964                        // to their response. Check the first hash against the
965                        // previous response, and discard mismatches.
966                        let unknown_hashes = match hashes.as_slice() {
967                            [expected_hash, rest @ ..] if expected_hash == &tip.expected_next => {
968                                rest
969                            }
970                            // If the first hash doesn't match, retry with the second.
971                            [first_hash, expected_hash, rest @ ..]
972                                if expected_hash == &tip.expected_next =>
973                            {
974                                debug!(?first_hash,
975                                                ?tip.expected_next,
976                                                ?tip.tip,
977                                                "unexpected first hash, but the second matches: using the hashes after the match");
978                                rest
979                            }
980                            // We ignore these responses
981                            [] => continue,
982                            [single_hash] => {
983                                debug!(?single_hash,
984                                                ?tip.expected_next,
985                                                ?tip.tip,
986                                                "discarding response containing a single unexpected hash");
987                                continue;
988                            }
989                            [first_hash, second_hash, rest @ ..] => {
990                                debug!(?first_hash,
991                                                ?second_hash,
992                                                rest_len = ?rest.len(),
993                                                ?tip.expected_next,
994                                                ?tip.tip,
995                                                "discarding response that starts with two unexpected hashes");
996                                continue;
997                            }
998                        };
999
1000                        if unknown_hashes.is_empty() {
1001                            debug!(?tip.tip, "response contained no new hashes after the expected overlap");
1002                            continue;
1003                        }
1004
1005                        trace!(?unknown_hashes);
1006
1007                        if let Some(end) = unknown_hashes.rchunks_exact(2).next() {
1008                            let new_tip = CheckedTip {
1009                                tip: end[0],
1010                                expected_next: end[1],
1011                            };
1012
1013                            // Make sure we get the same tips, regardless of the
1014                            // order of peer responses
1015                            if !download_set.contains(&new_tip.expected_next) {
1016                                debug!(
1017                                    ?new_tip,
1018                                    "adding new prospective tip, and removing any existing tips \
1019                                    in the new block hash list",
1020                                );
1021                                self.prospective_tips
1022                                    .retain(|t| !unknown_hashes.contains(&t.expected_next));
1023                                self.prospective_tips.insert(new_tip);
1024                            } else {
1025                                debug!(
1026                                    ?new_tip,
1027                                    "discarding prospective tip: already in download set"
1028                                );
1029                            }
1030                        } else {
1031                            debug!("downloading response that extends only one block");
1032                        }
1033
1034                        // security: the first response determines our download order
1035                        //
1036                        // TODO: can we make the download order independent of response order?
1037                        let prev_download_len = download_set.len();
1038                        download_set.extend(unknown_hashes);
1039                        let new_download_len = download_set.len();
1040                        let new_hashes = new_download_len - prev_download_len;
1041                        debug!(new_hashes, "added hashes to download set");
1042                        metrics::histogram!("sync.extend.response.hash.count")
1043                            .record(new_hashes as f64);
1044                    }
1045                    Ok(_) => unreachable!("network returned wrong response"),
1046                    // We ignore this error because we made multiple fanout requests.
1047                    Err(e) => debug!(?e),
1048                }
1049            }
1050        }
1051
1052        let new_downloads = download_set.len();
1053        debug!(new_downloads, "queueing new downloads");
1054        metrics::gauge!("sync.extend.queued.hash.count").set(new_downloads as f64);
1055
1056        // security: use the actual number of new downloads from all peers,
1057        // so the last peer to respond can't toggle our mempool
1058        self.recent_syncs.push_extend_tips_length(new_downloads);
1059
1060        let response = self.request_blocks(download_set).await;
1061
1062        metrics::histogram!("sync.stage.duration_seconds", "stage" => "extend_tips")
1063            .record(stage_start.elapsed().as_secs_f64());
1064
1065        Self::handle_hash_response(response).map_err(Into::into)
1066    }
1067
1068    /// Download and verify the genesis block, if it isn't currently known to
1069    /// our node.
1070    async fn request_genesis(&mut self) -> Result<(), Report> {
1071        // Due to Bitcoin protocol limitations, we can't request the genesis
1072        // block using our standard tip-following algorithm:
1073        //  - getblocks requires at least one hash
1074        //  - responses start with the block *after* the requested block, and
1075        //  - the genesis hash is used as a placeholder for "no matches".
1076        //
1077        // So we just download and verify the genesis block here.
1078        while !self.state_contains(self.genesis_hash).await? {
1079            info!("starting genesis block download and verify");
1080
1081            let response = timeout(SYNC_RESTART_DELAY, self.request_genesis_once())
1082                .await
1083                .map_err(Into::into);
1084
1085            // 3 layers of results is not ideal, but we need the timeout on the outside.
1086            match response {
1087                Ok(Ok(Ok(response))) => self
1088                    .handle_block_response(Ok(response))
1089                    .expect("never returns Err for Ok"),
1090                // Handle fatal errors
1091                Ok(Err(fatal_error)) => Err(fatal_error)?,
1092                // Handle timeouts and block errors
1093                Err(error) | Ok(Ok(Err(error))) => {
1094                    // TODO: exit syncer on permanent service errors (NetworkError, VerifierError)
1095                    if Self::should_restart_sync(&error) {
1096                        warn!(
1097                            ?error,
1098                            "could not download or verify genesis block, retrying"
1099                        );
1100                    } else {
1101                        info!(
1102                            ?error,
1103                            "temporary error downloading or verifying genesis block, retrying"
1104                        );
1105                    }
1106
1107                    tokio::time::sleep(GENESIS_TIMEOUT_RETRY).await;
1108                }
1109            }
1110        }
1111
1112        Ok(())
1113    }
1114
1115    /// Try to download and verify the genesis block once.
1116    ///
1117    /// Fatal errors are returned in the outer result, temporary errors in the inner one.
1118    async fn request_genesis_once(
1119        &mut self,
1120    ) -> Result<Result<(Height, block::Hash), BlockDownloadVerifyError>, Report> {
1121        let response = self.downloads.download_and_verify(self.genesis_hash).await;
1122        Self::handle_response(response).map_err(|e| eyre!(e))?;
1123
1124        let response = self.downloads.next().await.expect("downloads is nonempty");
1125
1126        Ok(response)
1127    }
1128
1129    /// Queue download and verify tasks for each block that isn't currently known to our node.
1130    ///
1131    /// TODO: turn obtain and extend tips into a separate task, which sends hashes via a channel?
1132    async fn request_blocks(
1133        &mut self,
1134        mut hashes: IndexSet<block::Hash>,
1135    ) -> Result<IndexSet<block::Hash>, BlockDownloadVerifyError> {
1136        let lookahead_limit = self.lookahead_limit(hashes.len());
1137
1138        debug!(
1139            hashes.len = hashes.len(),
1140            ?lookahead_limit,
1141            "requesting blocks",
1142        );
1143
1144        let extra_hashes = if hashes.len() > lookahead_limit {
1145            hashes.split_off(lookahead_limit)
1146        } else {
1147            IndexSet::new()
1148        };
1149
1150        // Dispatch blocks with duplicate-tolerant error handling.
1151        // DuplicateBlockQueuedForDownload is caught and skipped instead of
1152        // propagating — this prevents dropping unprocessed hashes from the
1153        // batch, which would create frontier gaps and stalls (#5709).
1154        for hash in hashes.into_iter() {
1155            match self.downloads.download_and_verify(hash).await {
1156                Ok(()) => {}
1157                Err(BlockDownloadVerifyError::DuplicateBlockQueuedForDownload { .. }) => {
1158                    debug!("block request was already queued, continuing");
1159                }
1160                Err(error) => return Err(error),
1161            }
1162        }
1163
1164        Ok(extra_hashes)
1165    }
1166
1167    /// The configured lookahead limit, based on the currently verified height,
1168    /// and the number of hashes we haven't queued yet.
1169    fn lookahead_limit(&self, new_hashes: usize) -> usize {
1170        let max_checkpoint_height: usize = self
1171            .max_checkpoint_height
1172            .0
1173            .try_into()
1174            .expect("fits in usize");
1175
1176        // When the state is empty, we want to verify using checkpoints
1177        let verified_height: usize = self
1178            .latest_chain_tip
1179            .best_tip_height()
1180            .unwrap_or(Height(0))
1181            .0
1182            .try_into()
1183            .expect("fits in usize");
1184
1185        if verified_height >= max_checkpoint_height {
1186            self.full_verify_concurrency_limit
1187        } else if (verified_height + new_hashes) >= max_checkpoint_height {
1188            // If we're just about to start full verification, allow enough for the remaining checkpoint,
1189            // and also enough for a separate full verification lookahead.
1190            let checkpoint_hashes = verified_height + new_hashes - max_checkpoint_height;
1191
1192            self.full_verify_concurrency_limit + checkpoint_hashes
1193        } else {
1194            self.checkpoint_verify_concurrency_limit
1195        }
1196    }
1197
1198    /// Handles a response for a requested block.
1199    ///
1200    /// See [`Self::handle_response`] for more details.
1201    #[allow(unknown_lints)]
1202    fn handle_block_response(
1203        &mut self,
1204        response: Result<(Height, block::Hash), BlockDownloadVerifyError>,
1205    ) -> Result<(), BlockDownloadVerifyError> {
1206        match response {
1207            Ok((height, hash)) => {
1208                trace!(?height, ?hash, "verified and committed block to state");
1209
1210                // The block arrived, so forget any re-request bookkeeping for it.
1211                self.block_reobtain_retries.remove(&hash);
1212                self.utxo_race_drops = 0;
1213
1214                return Ok(());
1215            }
1216
1217            Err(
1218                ref verify_error @ BlockDownloadVerifyError::Invalid {
1219                    ref error,
1220                    advertiser_addr: Some(advertiser_addr),
1221                    ..
1222                },
1223            ) if error.misbehavior_score() != 0 => {
1224                let _ = self.misbehavior_sender.try_send(zn::MisbehaviorReport::new(
1225                    advertiser_addr,
1226                    error.misbehavior_score(),
1227                    verify_error.to_string(),
1228                ));
1229            }
1230
1231            Err(
1232                ref verify_error @ BlockDownloadVerifyError::InvalidHeight {
1233                    advertiser_addr: Some(advertiser_addr),
1234                    ..
1235                },
1236            ) => {
1237                let _ = self.misbehavior_sender.try_send(zn::MisbehaviorReport::new(
1238                    advertiser_addr,
1239                    100,
1240                    verify_error.to_string(),
1241                ));
1242            }
1243
1244            // The downloader only sets `advertiser_addr` here when Zebra already holds the parent
1245            // header and it proves the body's claimed height wrong. A peer can answer with a
1246            // canonical header and a rewritten coinbase height because the initial hash check does
1247            // not recompute the header's commitment to the body's authorizing data
1248            // (GHSA-g95h-hw6g-pvgv). Peers that serve genuinely old blocks arrive unattributed and
1249            // fall through unscored. Score only a parent-proven rewrite.
1250            Err(
1251                ref verify_error @ BlockDownloadVerifyError::BehindTipHeightLimit {
1252                    advertiser_addr: Some(advertiser_addr),
1253                    ..
1254                },
1255            ) => {
1256                let _ = self.misbehavior_sender.try_send(zn::MisbehaviorReport::new(
1257                    advertiser_addr,
1258                    100,
1259                    format!("parent header contradicts coinbase height: {verify_error}"),
1260                ));
1261            }
1262
1263            // `AboveLookaheadHeightLimit` deliberately falls through unscored, and must
1264            // stay that way (GHSA-qhr3-cvch-5fh2): `FindBlocks` responses carry no
1265            // address, so the follow-up request goes to an independently chosen, honest
1266            // peer that served the block but did not choose its height. Scoring it let a
1267            // malicious `FindBlocks` responder evict honest peers throughout IBD. Unlike
1268            // the behind-tip sibling advisory, the block here is genuine, so there is no
1269            // local proof of forgery to re-attribute with. Do not add scoring back.
1270            Err(_) => {}
1271        };
1272
1273        // A hash the syncer still needs, whose download did not produce a usable block, is
1274        // otherwise dropped here and only rediscovered by a later sync round. Re-queue it for the
1275        // next sync round, bounded by `MAX_BLOCK_REOBTAIN_RETRIES`:
1276        // - `DownloadFailed`/`NotFound`: no peer delivered the block, which wedges the checkpoint
1277        //   frontier until the verify timeout (#5709).
1278        // - `BehindTipHeightLimit`: the body was not a usable block for this hash, so the hash is
1279        //   still missing (GHSA-g95h-hw6g-pvgv). This re-request runs whether or not the peer could
1280        //   be attributed, and covers the window before a score reaches the address book, which
1281        //   only applies misbehavior reports in batches.
1282        // - UTXO races (#11168): the block was never rejected, and the state parks its
1283        //   children until it arrives, so re-request it instead of waiting for a tip walk.
1284        // - `Invalid` with a mismatched authorizing data commitment: the block itself is fine, only
1285        //   the served body was forged, so the hash is still valid and still wanted. Misbehavior
1286        //   reports are flushed to the address book on `MISBEHAVIOR_FLUSH_INTERVAL`, so the first
1287        //   re-request can briefly land on the same peer again; `MAX_BLOCK_REOBTAIN_RETRIES` bounds
1288        //   that.
1289        // - `Invalid` because an ancestor had a mismatched authorizing data commitment: the state
1290        //   rejects queued descendants along with the forged ancestor, but their hashes are still
1291        //   wanted, and the peers that served them are not scored.
1292        // Other consensus failures (`Invalid`/`ValidationRequestError`) are deliberately
1293        // excluded — re-downloading a block the network already rejected is pointless.
1294        let reobtain_hash = match &response {
1295            Err(BlockDownloadVerifyError::DownloadFailed { error, hash })
1296                if is_not_found(error.as_ref()) =>
1297            {
1298                Some(*hash)
1299            }
1300            Err(BlockDownloadVerifyError::BehindTipHeightLimit { hash, .. }) => Some(*hash),
1301            Err(e @ BlockDownloadVerifyError::Invalid { hash, .. })
1302                if Self::is_utxo_lookup_timeout(e) =>
1303            {
1304                Some(*hash)
1305            }
1306            Err(BlockDownloadVerifyError::Invalid { error, hash, .. })
1307                if error.is_auth_commitment_mismatch()
1308                    || error.is_descendant_of_auth_commitment_mismatch() =>
1309            {
1310                Some(*hash)
1311            }
1312            Err(e @ BlockDownloadVerifyError::ValidationRequestError { hash, .. })
1313                if Self::is_post_checkpoint_verify_timeout(e) =>
1314            {
1315                Some(*hash)
1316            }
1317            _ => None,
1318        };
1319
1320        if let Some(hash) = reobtain_hash {
1321            let attempts = self.block_reobtain_retries.entry(hash).or_insert(0);
1322            if *attempts < MAX_BLOCK_REOBTAIN_RETRIES {
1323                *attempts += 1;
1324                self.reobtain_hashes.insert(hash);
1325                debug!(
1326                    ?hash,
1327                    attempts = *attempts,
1328                    "re-queueing missing block for re-download"
1329                );
1330            } else {
1331                debug!(
1332                    ?hash,
1333                    "missing block exceeded re-download retries, dropping"
1334                );
1335                self.block_reobtain_retries.remove(&hash);
1336            }
1337        }
1338
1339        // A UTXO race resolves as soon as the parent commits. A whole lookahead wave of
1340        // timeouts with no commit isn't the race, so restart instead of draining the batch.
1341        if let Err(error) = &response {
1342            if Self::is_utxo_lookup_timeout(error) {
1343                self.utxo_race_drops += 1;
1344                if self.utxo_race_drops
1345                    >= self
1346                        .full_verify_concurrency_limit
1347                        .max(MIN_UTXO_RACE_DROPS_BEFORE_RESTART)
1348                {
1349                    warn!(
1350                        drops = self.utxo_race_drops,
1351                        "no block verified across a full wave of UTXO lookup timeouts, restarting sync"
1352                    );
1353                    return response.map(|_| ());
1354                }
1355            }
1356        }
1357
1358        Self::handle_response(response)
1359    }
1360
1361    /// Returns `true` for the `AwaitUtxo` timeout that `should_restart_sync` exempts (#11168).
1362    fn is_utxo_lookup_timeout(e: &BlockDownloadVerifyError) -> bool {
1363        matches!(
1364            e,
1365            BlockDownloadVerifyError::Invalid {
1366                error: RouterError::Block { source },
1367                ..
1368            } if matches!(
1369                **source,
1370                VerifyBlockError::Transaction(TransactionError::TransparentInputNotFound)
1371            )
1372        )
1373    }
1374
1375    /// Returns `true` for the short post-final-checkpoint verify timeout (#5125) that
1376    /// `should_restart_sync` exempts; the 8-minute tower timeout is a different type.
1377    fn is_post_checkpoint_verify_timeout(e: &BlockDownloadVerifyError) -> bool {
1378        matches!(
1379            e,
1380            BlockDownloadVerifyError::ValidationRequestError { error, .. }
1381                if error.is::<tokio::time::error::Elapsed>()
1382        )
1383    }
1384
1385    /// Handles a response to block hash submission, passing through any extra hashes.
1386    ///
1387    /// See [`Self::handle_response`] for more details.
1388    #[allow(unknown_lints)]
1389    fn handle_hash_response(
1390        response: Result<IndexSet<block::Hash>, BlockDownloadVerifyError>,
1391    ) -> Result<IndexSet<block::Hash>, BlockDownloadVerifyError> {
1392        match response {
1393            Ok(extra_hashes) => Ok(extra_hashes),
1394            Err(_) => Self::handle_response(response).map(|()| IndexSet::new()),
1395        }
1396    }
1397
1398    /// Handles a response to a syncer request.
1399    ///
1400    /// Returns `Ok` if the request was successful, or if an expected error occurred,
1401    /// so that the synchronization can continue normally.
1402    ///
1403    /// Returns `Err` if an unexpected error occurred, to force the synchronizer to restart.
1404    #[allow(unknown_lints)]
1405    fn handle_response<T>(
1406        response: Result<T, BlockDownloadVerifyError>,
1407    ) -> Result<(), BlockDownloadVerifyError> {
1408        match response {
1409            Ok(_t) => Ok(()),
1410            Err(error) => {
1411                // TODO: exit syncer on permanent service errors (NetworkError, VerifierError)
1412                if Self::should_restart_sync(&error) {
1413                    Err(error)
1414                } else {
1415                    Ok(())
1416                }
1417            }
1418        }
1419    }
1420
1421    /// Returns `true` if the hash is present in the state, and `false`
1422    /// if the hash is not present in the state.
1423    ///
1424    /// Blocks that are only queued in the state, waiting for their parent, are treated as not
1425    /// present. A sync restart cancels in-flight downloads, so the parent of a queued block may
1426    /// never arrive unless the syncer downloads it again along with its queued children. If the
1427    /// syncer skipped queued blocks, `obtain_tips` would reject every download set that contains
1428    /// them, and the sync would stall until Zebra restarts. Re-submitting a queued block is
1429    /// harmless: the block verifier rejects it as a duplicate, which doesn't restart the sync.
1430    pub(crate) async fn state_contains(&mut self, hash: block::Hash) -> Result<bool, Report> {
1431        match self
1432            .state
1433            .ready()
1434            .await
1435            .map_err(|e| eyre!(e))?
1436            .call(zebra_state::Request::KnownBlock(hash))
1437            .await
1438            .map_err(|e| eyre!(e))?
1439        {
1440            zs::Response::KnownBlock(loc) => {
1441                Ok(loc.is_some_and(|loc| loc != zs::KnownBlock::Queue))
1442            }
1443            _ => unreachable!("wrong response to known block request"),
1444        }
1445    }
1446
1447    fn update_metrics(&mut self) {
1448        metrics::gauge!("sync.prospective_tips.len",).set(self.prospective_tips.len() as f64);
1449        metrics::gauge!("sync.downloads.in_flight",).set(self.downloads.in_flight() as f64);
1450    }
1451
1452    /// Return if the sync should be restarted based on the given error
1453    /// from the block downloader and verifier stream.
1454    fn should_restart_sync(e: &BlockDownloadVerifyError) -> bool {
1455        match e {
1456            // Structural matches: downcasts
1457            BlockDownloadVerifyError::Invalid { error, .. } if error.is_duplicate_request() => {
1458                debug!(error = ?e, "block was already verified or committed, possibly from a previous sync run, continuing");
1459                false
1460            }
1461            BlockDownloadVerifyError::Invalid { error, .. }
1462                if error.is_auth_commitment_mismatch() =>
1463            {
1464                debug!(
1465                    error = ?e,
1466                    "served block body did not match the authorizing data commitment in its \
1467                     header: the block hash is still valid and is re-requested, and the serving \
1468                     peer is scored, continuing"
1469                );
1470                false
1471            }
1472            BlockDownloadVerifyError::Invalid { error, .. }
1473                if error.is_descendant_of_auth_commitment_mismatch() =>
1474            {
1475                debug!(
1476                    error = ?e,
1477                    "an ancestor's served body did not match the authorizing data commitment in \
1478                     its header: this block hash is still valid and is re-requested, continuing"
1479                );
1480                false
1481            }
1482            // An `AwaitUtxo` timeout: the spent output is usually in a recent block whose
1483            // commit a restart would cancel, looping near the tip (#11168, #11132).
1484            e if Self::is_utxo_lookup_timeout(e) => {
1485                debug!(error = ?e, "block spends an output that is not in our state yet, re-requesting, continuing");
1486                false
1487            }
1488
1489            // Structural matches: direct
1490            BlockDownloadVerifyError::CancelledDuringDownload { .. }
1491            | BlockDownloadVerifyError::CancelledDuringVerification { .. } => {
1492                debug!(error = ?e, "block verification was cancelled, continuing");
1493                false
1494            }
1495            BlockDownloadVerifyError::BehindTipHeightLimit { .. } => {
1496                debug!(
1497                    error = ?e,
1498                    "block height is behind the current state tip: re-requesting the hash, \
1499                     and scoring the peer if the body contradicts the parent we hold, continuing"
1500                );
1501                false
1502            }
1503            BlockDownloadVerifyError::AboveLookaheadHeightLimit { .. } => {
1504                debug!(
1505                    error = ?e,
1506                    "block height is above the lookahead limit, \
1507                     dropping the block and continuing sync"
1508                );
1509                false
1510            }
1511            BlockDownloadVerifyError::InvalidHeight { .. } => {
1512                debug!(
1513                    error = ?e,
1514                    "block has no valid height, \
1515                     dropping the block and continuing sync"
1516                );
1517                false
1518            }
1519            BlockDownloadVerifyError::DuplicateBlockQueuedForDownload { .. } => {
1520                debug!(
1521                    error = ?e,
1522                    "queued duplicate block hash for download, \
1523                     assuming the syncer will eventually resolve duplicates, continuing"
1524                );
1525                false
1526            }
1527
1528            BlockDownloadVerifyError::DownloadFailed { ref error, .. }
1529                if is_not_found(error.as_ref()) =>
1530            {
1531                // Covers these errors:
1532                // - NotFoundResponse
1533                // - NotFoundRegistry
1534                //
1535                // TODO: improve this by checking the type (#2908)
1536                //       restart after a certain number of NotFound errors?
1537                debug!(error = ?e, "block was not found, possibly from a peer that doesn't have the block yet, continuing");
1538                false
1539            }
1540
1541            // The short post-final-checkpoint verify timeout is a UTXO race (#5125), not an
1542            // invalid block; the 8-minute tower timeout stays in the catch-all (#5709).
1543            e if Self::is_post_checkpoint_verify_timeout(e) => {
1544                debug!(error = ?e, "initial fully verified block timed out waiting for its parent's outputs, re-requesting, continuing");
1545                false
1546            }
1547
1548            _ => {
1549                // download_and_verify downcasts errors from the block verifier
1550                // into VerifyChainError, and puts the result inside one of the
1551                // BlockDownloadVerifyError enumerations. This downcast could
1552                // become incorrect e.g. after some refactoring, and it is difficult
1553                // to write a test to check it. The test below is a best-effort
1554                // attempt to catch if that happens and log it.
1555                //
1556                // TODO: add a proper test and remove this
1557                // https://github.com/ZcashFoundation/zebra/issues/2909
1558                let err_str = format!("{e:?}");
1559                if err_str.contains("NotFound") {
1560                    error!(?e,
1561                        "a BlockDownloadVerifyError that should have been filtered out was detected, \
1562                        which possibly indicates a programming error in the downcast inside \
1563                        zebrad::components::sync::downloads::Downloads::download_and_verify"
1564                    )
1565                }
1566
1567                warn!(?e, "error downloading and verifying block");
1568                true
1569            }
1570        }
1571    }
1572}