Skip to main content

zebrad/commands/
start.rs

1//! `start` subcommand - entry point for starting a zebra node
2//!
3//! ## Application Structure
4//!
5//! A zebra node consists of the following major services and tasks:
6//!
7//! Peers:
8//!  * Peer Connection Pool Service
9//!    * primary external interface for outbound requests from this node to remote peers
10//!    * accepts requests from services and tasks in this node, and sends them to remote peers
11//!  * Peer Discovery Service
12//!    * maintains a list of peer addresses, and connection priority metadata
13//!    * discovers new peer addresses from existing peer connections
14//!    * initiates new outbound peer connections in response to demand from tasks within this node
15//!  * Peer Cache Service
16//!    * Reads previous peer cache on startup, and adds it to the configured DNS seed peers
17//!    * Periodically updates the peer cache on disk from the latest address book state
18//!
19//! Blocks & Mempool Transactions:
20//!  * Consensus Service
21//!    * handles all validation logic for the node
22//!    * verifies blocks using zebra-chain, then stores verified blocks in zebra-state
23//!    * verifies mempool and block transactions using zebra-chain and zebra-script,
24//!      and returns verified mempool transactions for mempool storage
25//!  * Inbound Service
26//!    * primary external interface for inbound peer requests to this node
27//!    * handles requests from peers for network data, chain data, and mempool transactions
28//!    * spawns download and verify tasks for each gossiped block
29//!    * sends gossiped transactions to the mempool service
30//!
31//! Blocks:
32//!  * Sync Task
33//!    * runs in the background and continuously queries the network for
34//!      new blocks to be verified and added to the local state
35//!    * spawns download and verify tasks for each crawled block
36//!  * State Service
37//!    * contextually verifies blocks
38//!    * handles in-memory storage of multiple non-finalized chains
39//!    * handles permanent storage of the best finalized chain
40//!  * Old State Version Cleanup Task
41//!    * deletes outdated state versions
42//!  * Block Gossip Task
43//!    * runs in the background and continuously queries the state for
44//!      newly committed blocks to be gossiped to peers
45//!  * Block Notify Task
46//!    * if the user has configured a `notify.block_notify_command`, runs that command
47//!      whenever the best chain tip changes (Zebra's equivalent of zcashd's `-blocknotify`)
48//!  * Progress Task
49//!    * logs progress towards the chain tip
50//!
51//! Block Mining:
52//!  * Internal Miner Task
53//!    * if the user has configured Zebra to mine blocks, spawns tasks to generate new blocks,
54//!      and submits them for verification. This automatically shares these new blocks with peers.
55//!
56//! Mempool Transactions:
57//!  * Mempool Service
58//!    * activates when the syncer is near the chain tip
59//!    * spawns download and verify tasks for each crawled or gossiped transaction
60//!    * handles in-memory storage of unmined transactions
61//!  * Queue Checker Task
62//!    * runs in the background, polling the mempool to store newly verified transactions
63//!  * Transaction Gossip Task
64//!    * runs in the background and gossips newly added mempool transactions
65//!      to peers
66//!
67//! Remote Procedure Calls:
68//!  * JSON-RPC Service
69//!    * answers RPC client requests using the State Service and Mempool Service
70//!    * submits client transactions to the node's mempool
71//!
72//! Zebra also has diagnostic support:
73//! * [metrics](https://github.com/ZcashFoundation/zebra/blob/main/book/src/user/metrics.md)
74//! * [tracing](https://github.com/ZcashFoundation/zebra/blob/main/book/src/user/tracing.md)
75//! * [progress-bar](https://docs.rs/howudoin/0.1.1/howudoin)
76//!
77//! Some of the diagnostic features are optional, and need to be enabled at compile-time.
78
79use std::{
80    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
81    path::Path,
82    sync::Arc,
83};
84
85use abscissa_core::{config, Command, FrameworkError};
86use color_eyre::eyre::{eyre, Report};
87use futures::FutureExt;
88use tokio::{
89    pin, select,
90    sync::{oneshot, watch},
91};
92use tower::{builder::ServiceBuilder, util::BoxService, ServiceExt};
93use tracing_futures::Instrument;
94
95use zebra_chain::block::genesis::regtest_genesis_block;
96use zebra_consensus::router::BackgroundTaskHandles;
97use zebra_rpc::{methods::RpcImpl, server::RpcServer, SubmitBlockChannel};
98
99use crate::{
100    application::{build_version, user_agent, LAST_WARN_ERROR_LOG_SENDER},
101    components::{
102        health,
103        inbound::{self, InboundSetupData, MAX_INBOUND_RESPONSE_TIME},
104        mempool::{self, Mempool},
105        notify::{self, BlockNotifyError},
106        sync::{self, show_block_chain_progress, VERIFICATION_PIPELINE_SCALING_MULTIPLIER},
107        tokio::{RuntimeRun, TokioComponent},
108        zcashd_compat, ChainSync, Inbound,
109    },
110    config::ZebradConfig,
111    prelude::*,
112};
113
114#[cfg(feature = "internal-miner")]
115use crate::components;
116
117/// Start the application (default command)
118#[derive(Command, Debug, Default, clap::Parser)]
119pub struct StartCmd {
120    /// Filter strings which override the config file and defaults
121    #[clap(help = "tracing filters which override the zebrad.toml config")]
122    filters: Vec<String>,
123
124    /// Enable zcashd-compat mode.
125    #[clap(long)]
126    zcashd_compat: bool,
127
128    /// Continue startup even when zcashd-compat preflight detects minimum hardware shortfalls.
129    #[clap(long = "unsafe-low-specs")]
130    unsafe_low_specs: bool,
131}
132
133/// Warns if Linux TCP slow-start-after-idle is enabled, which significantly
134/// reduces single-peer throughput for block propagation.
135///
136/// See `book/src/user/troubleshooting.md`.
137#[cfg(target_os = "linux")]
138fn check_tcp_slow_start_after_idle() {
139    const PATH: &str = "/proc/sys/net/ipv4/tcp_slow_start_after_idle";
140
141    let raw = match std::fs::read_to_string(PATH) {
142        Ok(raw) => raw,
143        Err(error) => {
144            debug!(
145                ?error,
146                path = PATH,
147                "could not read TCP sysctl, skipping check"
148            );
149            return;
150        }
151    };
152
153    if raw.trim() == "0" {
154        return;
155    }
156
157    warn!(
158        setting = "net.ipv4.tcp_slow_start_after_idle",
159        "TCP slow-start-after-idle is enabled, which resets TCP's congestion window \
160         between block requests and significantly reduces single-peer throughput for \
161         block propagation. \
162         Hint: set `net.ipv4.tcp_slow_start_after_idle=0` via sysctl. \
163         See https://zebra.zfnd.org/user/troubleshooting.html#linux-tcp-tuning-for-block-propagation"
164    );
165}
166
167#[cfg(not(target_os = "linux"))]
168fn check_tcp_slow_start_after_idle() {}
169
170impl StartCmd {
171    /// Extra time Zebra waits for the zcashd-compat supervisor task beyond the
172    /// child's `shutdown_grace_period`. The supervisor's `terminate_child` waits
173    /// the full grace period before its SIGKILL last resort, so the outer wait
174    /// must be strictly longer or aborting the task races the graceful path.
175    const ZCASHD_COMPAT_SHUTDOWN_TIMEOUT_MARGIN: std::time::Duration =
176        std::time::Duration::from_secs(30);
177
178    /// Returns the Zebra P2P address supervised zcashd should `-connect` to.
179    ///
180    /// Uses `zcashd_compat.p2p_connect_addr` when set, otherwise Zebra's bound
181    /// P2P listener, substituting loopback for unspecified addresses so
182    /// zcashd gets a dialable target on the same host.
183    fn zcashd_compat_p2p_connect_addr(
184        config: &ZebradConfig,
185        local_listener: SocketAddr,
186    ) -> SocketAddr {
187        if let Some(addr) = config.zcashd_compat.p2p_connect_addr {
188            return addr;
189        }
190
191        if local_listener.ip().is_unspecified() {
192            // Substitute the loopback address of the same IP family: an
193            // IPv6-only listener is not reachable via 127.0.0.1.
194            match local_listener.ip() {
195                IpAddr::V4(_) => SocketAddr::from(([127, 0, 0, 1], local_listener.port())),
196                IpAddr::V6(_) => {
197                    SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), local_listener.port())
198                }
199            }
200        } else {
201            local_listener
202        }
203    }
204
205    /// Returns the default inbound peer IPs that always receive block gossip in
206    /// zcashd-compat mode.
207    fn zcashd_compat_default_block_gossip_peer_ips() -> Vec<IpAddr> {
208        vec![
209            IpAddr::V4(Ipv4Addr::LOCALHOST),
210            IpAddr::V6(Ipv6Addr::LOCALHOST),
211        ]
212    }
213
214    /// Returns the supervisor shutdown timeout when zcashd-compat `zcashd` supervision is active.
215    ///
216    /// This is the configured `shutdown_grace_period` plus a fixed margin, so the
217    /// supervisor task always gets to finish its own SIGTERM → grace → SIGKILL
218    /// sequence before Zebra gives up on the task.
219    fn zcashd_compat_supervisor_shutdown_timeout(
220        config: &ZebradConfig,
221    ) -> Option<std::time::Duration> {
222        (config.zcashd_compat.enabled && config.zcashd_compat.manage_zcashd).then_some(
223            config
224                .zcashd_compat
225                .shutdown_grace_period
226                .saturating_add(Self::ZCASHD_COMPAT_SHUTDOWN_TIMEOUT_MARGIN),
227        )
228    }
229
230    /// Returns `false` so Zebra keeps running if zcashd-compat supervision exits unexpectedly.
231    fn zcashd_compat_supervisor_should_exit(
232        zcashd_compat_result: Result<Result<(), Report>, tokio::task::JoinError>,
233    ) -> bool {
234        zcashd_compat::set_supervision_unexpectedly_disabled_metrics();
235
236        match zcashd_compat_result {
237            Ok(Ok(())) => {
238                warn!(
239                    "zcashd-compat supervisor task exited unexpectedly in supervision mode; \
240                     continuing without zcashd supervision"
241                );
242            }
243            Ok(Err(err)) => {
244                warn!(
245                    ?err,
246                    "zcashd-compat supervisor task failed in supervision mode; \
247                     continuing without zcashd supervision"
248                );
249            }
250            Err(join_err) => {
251                warn!(
252                    ?join_err,
253                    "zcashd-compat supervisor task panicked in supervision mode; \
254                     continuing without zcashd supervision"
255                );
256            }
257        }
258
259        false
260    }
261
262    async fn start(&self) -> Result<(), Report> {
263        check_tcp_slow_start_after_idle();
264
265        let config = APPLICATION.config();
266        let is_regtest = config.network.network.is_regtest();
267
268        let config = if is_regtest {
269            Arc::new(ZebradConfig {
270                mempool: mempool::Config {
271                    debug_enable_at_height: Some(0),
272                    ..config.mempool
273                },
274                ..Arc::unwrap_or_clone(config)
275            })
276        } else {
277            config
278        };
279
280        let zcashd_compat_block_gossip_peer_ips = if config.zcashd_compat.enabled {
281            if config.zcashd_compat.block_gossip_peer_ips.is_empty() {
282                // The sidecar privileges (pinned gossip, reserved slot, stall
283                // exemption) match on the sidecar's *source* IP. In
284                // cross-container/cross-host topologies that source is not
285                // loopback, so the default list would silently strip the
286                // sidecar of everything this mode provides.
287                if config
288                    .zcashd_compat
289                    .p2p_connect_addr
290                    .is_some_and(|addr| !addr.ip().is_loopback())
291                {
292                    warn!(
293                        p2p_connect_addr = ?config.zcashd_compat.p2p_connect_addr,
294                        "zcashd_compat.p2p_connect_addr is not loopback, but \
295                         zcashd_compat.block_gossip_peer_ips defaults to loopback only; \
296                         if the sidecar connects from a non-loopback IP, set \
297                         block_gossip_peer_ips to that IP or it will not receive \
298                         pinned block gossip"
299                    );
300                }
301
302                Self::zcashd_compat_default_block_gossip_peer_ips()
303            } else {
304                config.zcashd_compat.block_gossip_peer_ips.clone()
305            }
306        } else {
307            Vec::new()
308        };
309
310        if config.zcashd_compat.enabled {
311            // Preflight does blocking filesystem and /proc reads, and can hash
312            // the cached zcashd binary, so keep it off the async runtime.
313            let preflight_config = config.clone();
314            let unsafe_low_specs = self.unsafe_low_specs;
315            tokio::task::spawn_blocking(move || {
316                zcashd_compat::run_preflight(&preflight_config, unsafe_low_specs)
317            })
318            .await
319            .map_err(|err| eyre!("failed to join zcashd-compat preflight task: {err}"))??;
320        }
321
322        let resolved_zcashd_path = if config.zcashd_compat.enabled
323            && config.zcashd_compat.manage_zcashd
324        {
325            let zcashd_compat_config = config.zcashd_compat.clone();
326            let state_cache_dir = config.state.cache_dir.clone();
327            Some(
328                tokio::task::spawn_blocking(move || {
329                    zcashd_compat::resolve_zcashd_binary_path(
330                        &zcashd_compat_config,
331                        &state_cache_dir,
332                    )
333                })
334                .await
335                .map_err(|err| eyre!("failed to join managed zcashd binary resolver: {err}"))??,
336            )
337        } else {
338            None
339        };
340
341        info!("initializing node state");
342        let (_, max_checkpoint_height) = zebra_consensus::router::init_checkpoint_list(
343            config.consensus.clone(),
344            &config.network.network,
345        );
346
347        info!("opening database, this may take a few minutes");
348
349        let (state_service, read_only_state_service, latest_chain_tip, chain_tip_change) =
350            zebra_state::init(
351                config.state.clone(),
352                &config.network.network,
353                max_checkpoint_height,
354                config.sync.checkpoint_verify_concurrency_limit
355                    * (VERIFICATION_PIPELINE_SCALING_MULTIPLIER + 1),
356            )
357            .await;
358
359        info!("logging database metrics on startup");
360        read_only_state_service.log_db_metrics();
361
362        let state = ServiceBuilder::new()
363            .buffer(Self::state_buffer_bound())
364            .service(state_service);
365
366        info!("initializing network");
367        // The service that our node uses to respond to requests by peers. The
368        // load_shed middleware ensures that we reduce the size of the peer set
369        // in response to excess load.
370        //
371        // # Security
372        //
373        // This layer stack is security-sensitive, modifying it can cause hangs,
374        // or enable denial of service attacks.
375        //
376        // See `zebra_network::Connection::drive_peer_request()` for details.
377        let (setup_tx, setup_rx) = oneshot::channel();
378        let inbound = ServiceBuilder::new()
379            .load_shed()
380            .buffer(inbound::downloads::MAX_INBOUND_CONCURRENCY)
381            .timeout(MAX_INBOUND_RESPONSE_TIME)
382            .service(Inbound::new(
383                config.sync.full_verify_concurrency_limit,
384                setup_rx,
385            ));
386
387        let (peer_set, address_book, misbehavior_sender) =
388            zebra_network::init_with_block_gossip_peer_ips(
389                config.network.clone(),
390                inbound,
391                latest_chain_tip.clone(),
392                user_agent(),
393                zcashd_compat_block_gossip_peer_ips,
394            )
395            .await;
396
397        // Start health server if configured (after sync_status is available)
398
399        info!("initializing verifiers");
400        let (tx_verifier_setup_tx, tx_verifier_setup_rx) = oneshot::channel();
401        let (block_verifier_router, tx_verifier, consensus_task_handles, max_checkpoint_height) =
402            zebra_consensus::router::init(
403                config.consensus.clone(),
404                &config.network.network,
405                state.clone(),
406                tx_verifier_setup_rx,
407            )
408            .await;
409
410        info!("initializing syncer");
411        let (mut syncer, sync_status) = ChainSync::new(
412            &config,
413            max_checkpoint_height,
414            peer_set.clone(),
415            block_verifier_router.clone(),
416            state.clone(),
417            latest_chain_tip.clone(),
418            misbehavior_sender.clone(),
419        );
420
421        info!("initializing mempool");
422        let (mempool, mempool_transaction_subscriber) = Mempool::new(
423            &config.mempool,
424            peer_set.clone(),
425            state.clone(),
426            tx_verifier,
427            sync_status.clone(),
428            latest_chain_tip.clone(),
429            chain_tip_change.clone(),
430            misbehavior_sender.clone(),
431        );
432        let mempool = BoxService::new(mempool);
433        let mempool = ServiceBuilder::new()
434            .buffer(mempool::downloads::MAX_INBOUND_CONCURRENCY)
435            .service(mempool);
436
437        if tx_verifier_setup_tx.send(mempool.clone()).is_err() {
438            warn!("error setting up the transaction verifier with a handle to the mempool service");
439        };
440
441        info!("fully initializing inbound peer request handler");
442        // Fully start the inbound service as soon as possible
443        let setup_data = InboundSetupData {
444            address_book: address_book.clone(),
445            block_download_peer_set: peer_set.clone(),
446            block_verifier: block_verifier_router.clone(),
447            mempool: mempool.clone(),
448            state: state.clone(),
449            latest_chain_tip: latest_chain_tip.clone(),
450            misbehavior_sender,
451        };
452        setup_tx
453            .send(setup_data)
454            .map_err(|_| eyre!("could not send setup data to inbound service"))?;
455        // And give it time to clear its queue
456        tokio::task::yield_now().await;
457
458        // Create a channel to send mined blocks to the gossip task
459        let submit_block_channel = SubmitBlockChannel::new();
460
461        // Launch RPC server
462        let (rpc_impl, mut rpc_tx_queue_handle) = RpcImpl::new(
463            config.network.network.clone(),
464            config.mining.clone(),
465            config.rpc.debug_force_finished_sync,
466            build_version(),
467            user_agent(),
468            mempool.clone(),
469            state.clone(),
470            read_only_state_service.clone(),
471            block_verifier_router.clone(),
472            sync_status.clone(),
473            latest_chain_tip.clone(),
474            address_book.clone(),
475            LAST_WARN_ERROR_LOG_SENDER.subscribe(),
476            Some(submit_block_channel.sender()),
477        );
478
479        let rpc_task_handle = if config.rpc.listen_addr.is_some() {
480            RpcServer::start(rpc_impl.clone(), config.rpc.clone())
481                .await
482                .expect("server should start")
483        } else {
484            tokio::spawn(std::future::pending().in_current_span())
485        };
486
487        let zcashd_compat_shutdown_timeout =
488            Self::zcashd_compat_supervisor_shutdown_timeout(&config);
489        let (zcashd_compat_shutdown_tx, zcashd_compat_shutdown_rx) = watch::channel(false);
490        let mut zcashd_compat_task_handle = if let Some(resolved_zcashd_path) = resolved_zcashd_path
491        {
492            let local_listener = address_book
493                .lock()
494                .expect("unexpected panic in address book mutex guard")
495                .local_listener_socket_addr();
496            let supervisor_config = zcashd_compat::SupervisorConfig::new(
497                &config.zcashd_compat,
498                resolved_zcashd_path,
499                &config.state.cache_dir,
500                config.network.network.kind(),
501                Self::zcashd_compat_p2p_connect_addr(&config, local_listener),
502            );
503
504            info!(
505                connect = %supervisor_config.zebra_p2p_addr,
506                "zcashd-compat mode enabled"
507            );
508
509            tokio::spawn(
510                zcashd_compat::run_supervisor(supervisor_config, zcashd_compat_shutdown_rx)
511                    .in_current_span(),
512            )
513        } else {
514            if config.zcashd_compat.enabled {
515                zcashd_compat::set_supervision_config_disabled_metrics();
516                info!("zcashd-compat mode enabled: zcashd supervision disabled");
517            }
518
519            tokio::spawn(std::future::pending().in_current_span())
520        };
521
522        // TODO: Add a shutdown signal and start the server with `serve_with_incoming_shutdown()` if
523        //       any related unit tests sometimes crash with memory errors
524        let indexer_rpc_task_handle = {
525            if let Some(indexer_listen_addr) = config.rpc.indexer_listen_addr {
526                info!("spawning indexer RPC server");
527                let (indexer_rpc_task_handle, _listen_addr) = zebra_rpc::indexer::server::init(
528                    indexer_listen_addr,
529                    read_only_state_service.clone(),
530                    latest_chain_tip.clone(),
531                    mempool_transaction_subscriber.clone(),
532                )
533                .await
534                .map_err(|err| eyre!(err))?;
535
536                indexer_rpc_task_handle
537            } else {
538                warn!("configure an indexer_listen_addr to start the indexer RPC server");
539                tokio::spawn(std::future::pending().in_current_span())
540            }
541        };
542
543        // Start concurrent tasks which don't add load to other tasks
544        info!("spawning block gossip task");
545        let block_gossip_task_handle = tokio::spawn(
546            sync::gossip_best_tip_block_hashes(
547                sync_status.clone(),
548                chain_tip_change.clone(),
549                peer_set.clone(),
550                Some(submit_block_channel.receiver()),
551            )
552            .in_current_span(),
553        );
554
555        info!("spawning block notify task");
556        let block_notify_task_handle: tokio::task::JoinHandle<Result<(), BlockNotifyError>> =
557            if let Some(command) = config.notify.block_notify_command.clone() {
558                tokio::spawn(
559                    notify::run_block_notify(
560                        command,
561                        sync_status.clone(),
562                        chain_tip_change.clone(),
563                    )
564                    .in_current_span(),
565                )
566            } else {
567                tokio::spawn(std::future::pending().in_current_span())
568            };
569
570        info!("spawning mempool queue checker task");
571        let mempool_queue_checker_task_handle = mempool::QueueChecker::spawn(mempool.clone());
572
573        info!("spawning mempool transaction gossip task");
574        let tx_gossip_task_handle = tokio::spawn(
575            mempool::gossip_mempool_transaction_id(
576                mempool_transaction_subscriber.subscribe(),
577                peer_set.clone(),
578            )
579            .in_current_span(),
580        );
581
582        info!("spawning delete old databases task");
583        let mut old_databases_task_handle = zebra_state::check_and_delete_old_state_databases(
584            &config.state,
585            &config.network.network,
586        );
587
588        info!("spawning progress logging task");
589        let (chain_tip_metrics_sender, chain_tip_metrics_receiver) =
590            health::ChainTipMetrics::channel();
591        let progress_task_handle = tokio::spawn(
592            show_block_chain_progress(
593                config.network.network.clone(),
594                latest_chain_tip.clone(),
595                sync_status.clone(),
596                chain_tip_metrics_sender,
597            )
598            .in_current_span(),
599        );
600
601        // Start health server if configured
602        info!("initializing health endpoints");
603        let (health_task_handle, _) = health::init(
604            config.health.clone(),
605            config.network.network.clone(),
606            chain_tip_metrics_receiver,
607            sync_status.clone(),
608            address_book.clone(),
609        )
610        .await;
611
612        // Spawn never ending end of support task.
613        info!("spawning end of support checking task");
614        let end_of_support_task_handle = tokio::spawn(
615            sync::end_of_support::start(config.network.network.clone(), latest_chain_tip.clone())
616                .in_current_span(),
617        );
618
619        // Give the inbound service more time to clear its queue,
620        // then start concurrent tasks that can add load to the inbound service
621        // (by opening more peer connections, so those peers send us requests)
622        tokio::task::yield_now().await;
623
624        // The crawler only activates immediately in tests that use mempool debug mode
625        info!("spawning mempool crawler task");
626        let mempool_crawler_task_handle = mempool::Crawler::spawn(
627            &config.mempool,
628            peer_set,
629            mempool.clone(),
630            sync_status.clone(),
631            chain_tip_change.clone(),
632        );
633
634        info!("spawning syncer task");
635        // In regtest, commit the genesis block directly (bypassing the syncer's genesis
636        // download, which requires a connected peer). Then run the syncer normally so
637        // that multi-hop block propagation works: gossiped blocks that arrive out of
638        // order (e.g. only the latest tip hash was gossiped) will be recovered by the
639        // syncer using block locators within REGTEST_SYNC_RESTART_DELAY (2 seconds).
640        if is_regtest
641            && !syncer
642                .state_contains(config.network.network.genesis_hash())
643                .await?
644        {
645            let genesis_hash = block_verifier_router
646                .clone()
647                .oneshot(zebra_consensus::Request::Commit(regtest_genesis_block()))
648                .await
649                .expect("should validate Regtest genesis block");
650
651            assert_eq!(
652                genesis_hash,
653                config.network.network.genesis_hash(),
654                "validated block hash should match network genesis hash"
655            )
656        }
657        let syncer_task_handle = tokio::spawn(syncer.sync().in_current_span());
658
659        // And finally, spawn the internal Zcash miner, if it is enabled.
660        //
661        // TODO: add a config to enable the miner rather than a feature.
662        #[cfg(feature = "internal-miner")]
663        let miner_task_handle = if config.mining.is_internal_miner_enabled() {
664            info!("spawning Zcash miner");
665            components::miner::spawn_init(&config.metrics, rpc_impl)
666        } else {
667            tokio::spawn(std::future::pending().in_current_span())
668        };
669
670        #[cfg(not(feature = "internal-miner"))]
671        // Spawn a dummy miner task which doesn't do anything and never finishes.
672        let miner_task_handle: tokio::task::JoinHandle<Result<(), Report>> =
673            tokio::spawn(std::future::pending().in_current_span());
674
675        info!("spawned initial Zebra tasks");
676
677        // TODO: put tasks into an ongoing FuturesUnordered and a startup FuturesUnordered?
678
679        // ongoing tasks
680        pin!(rpc_task_handle);
681        pin!(indexer_rpc_task_handle);
682        pin!(syncer_task_handle);
683        pin!(block_gossip_task_handle);
684        pin!(block_notify_task_handle);
685        pin!(mempool_crawler_task_handle);
686        pin!(mempool_queue_checker_task_handle);
687        pin!(tx_gossip_task_handle);
688        pin!(progress_task_handle);
689        pin!(end_of_support_task_handle);
690        pin!(miner_task_handle);
691
692        // startup tasks
693        let BackgroundTaskHandles {
694            mut state_checkpoint_verify_handle,
695        } = consensus_task_handles;
696
697        let state_checkpoint_verify_handle_fused = (&mut state_checkpoint_verify_handle).fuse();
698        pin!(state_checkpoint_verify_handle_fused);
699
700        let old_databases_task_handle_fused = (&mut old_databases_task_handle).fuse();
701        pin!(old_databases_task_handle_fused);
702
703        // The zcashd-compat supervisor exits when supervision is disabled or fails,
704        // but Zebra keeps running, so its handle must be fused.
705        let mut zcashd_compat_task_finished = false;
706        let zcashd_compat_task_handle_fused = (&mut zcashd_compat_task_handle).fuse();
707        pin!(zcashd_compat_task_handle_fused);
708
709        // Wait for tasks to finish
710        let exit_status = loop {
711            let mut exit_when_task_finishes = true;
712
713            let result = select! {
714                rpc_join_result = &mut rpc_task_handle => {
715                    let rpc_server_result = rpc_join_result
716                        .expect("unexpected panic in the rpc task");
717                    info!(?rpc_server_result, "rpc task exited");
718                    Ok(())
719                }
720
721                rpc_tx_queue_result = &mut rpc_tx_queue_handle => {
722                    rpc_tx_queue_result
723                        .expect("unexpected panic in the rpc transaction queue task");
724                    info!("rpc transaction queue task exited");
725                    Ok(())
726                }
727
728                indexer_rpc_join_result = &mut indexer_rpc_task_handle => {
729                    let indexer_rpc_server_result = indexer_rpc_join_result
730                        .expect("unexpected panic in the indexer task");
731                    info!(?indexer_rpc_server_result, "indexer rpc task exited");
732                    Ok(())
733                }
734
735                sync_result = &mut syncer_task_handle => sync_result
736                    .expect("unexpected panic in the syncer task")
737                    .map(|_| info!("syncer task exited")),
738
739                block_gossip_result = &mut block_gossip_task_handle => block_gossip_result
740                    .expect("unexpected panic in the chain tip block gossip task")
741                    .map(|_| info!("chain tip block gossip task exited"))
742                    .map_err(|e| eyre!(e)),
743
744                block_notify_result = &mut block_notify_task_handle => block_notify_result
745                    .expect("unexpected panic in the block notify task")
746                    .map(|_| info!("block notify task exited"))
747                    .map_err(|e| eyre!(e)),
748
749                mempool_crawl_result = &mut mempool_crawler_task_handle => mempool_crawl_result
750                    .expect("unexpected panic in the mempool crawler")
751                    .map(|_| info!("mempool crawler task exited"))
752                    .map_err(|e| eyre!(e)),
753
754                mempool_queue_result = &mut mempool_queue_checker_task_handle => mempool_queue_result
755                    .expect("unexpected panic in the mempool queue checker")
756                    .map(|_| info!("mempool queue checker task exited"))
757                    .map_err(|e| eyre!(e)),
758
759                tx_gossip_result = &mut tx_gossip_task_handle => tx_gossip_result
760                    .expect("unexpected panic in the transaction gossip task")
761                    .map(|_| info!("transaction gossip task exited"))
762                    .map_err(|e| eyre!(e)),
763
764                // The progress task runs forever, unless it panics.
765                // So we don't need to provide an exit status for it.
766                progress_result = &mut progress_task_handle => {
767                    info!("chain progress task exited");
768                    progress_result
769                        .expect("unexpected panic in the chain progress task");
770                }
771
772                end_of_support_result = &mut end_of_support_task_handle => end_of_support_result
773                    .expect("unexpected panic in the end of support task")
774                    .map(|_| info!("end of support task exited")),
775
776                // We also expect the state checkpoint verify task to finish.
777                state_checkpoint_verify_result = &mut state_checkpoint_verify_handle_fused => {
778                    state_checkpoint_verify_result
779                        .unwrap_or_else(|_| panic!(
780                            "unexpected panic checking previous state followed the best chain"));
781
782                    exit_when_task_finishes = false;
783                    Ok(())
784                }
785
786                // And the old databases task should finish while Zebra is running.
787                old_databases_result = &mut old_databases_task_handle_fused => {
788                    old_databases_result
789                        .unwrap_or_else(|_| panic!(
790                            "unexpected panic deleting old database directories"));
791
792                    exit_when_task_finishes = false;
793                    Ok(())
794                }
795
796                miner_result = &mut miner_task_handle => miner_result
797                    .expect("unexpected panic in the miner task")
798                    .map(|_| info!("miner task exited")),
799
800                zcashd_compat_result = &mut zcashd_compat_task_handle_fused => {
801                    zcashd_compat_task_finished = true;
802                    exit_when_task_finishes =
803                        Self::zcashd_compat_supervisor_should_exit(zcashd_compat_result);
804                    Ok(())
805                },
806            };
807
808            // Stop Zebra if a task finished and returned an error,
809            // or if an ongoing task exited.
810            if let Err(err) = result {
811                break Err(err);
812            }
813
814            if exit_when_task_finishes {
815                break Ok(());
816            }
817        };
818
819        info!("exiting Zebra because an ongoing task exited: asking other tasks to stop");
820
821        // ongoing tasks
822        rpc_task_handle.abort();
823        rpc_tx_queue_handle.abort();
824        health_task_handle.abort();
825        syncer_task_handle.abort();
826        block_gossip_task_handle.abort();
827        block_notify_task_handle.abort();
828        mempool_crawler_task_handle.abort();
829        mempool_queue_checker_task_handle.abort();
830        tx_gossip_task_handle.abort();
831        progress_task_handle.abort();
832        end_of_support_task_handle.abort();
833        miner_task_handle.abort();
834        if zcashd_compat_task_finished {
835            debug!("zcashd-compat supervisor task already exited before shutdown");
836        } else if let Some(zcashd_compat_shutdown_timeout) = zcashd_compat_shutdown_timeout {
837            info!(
838                ?zcashd_compat_shutdown_timeout,
839                "requesting zcashd-compat supervisor shutdown"
840            );
841            if zcashd_compat_shutdown_tx.send(true).is_err() {
842                warn!("zcashd-compat supervisor shutdown request was not delivered");
843            }
844            if tokio::time::timeout(
845                zcashd_compat_shutdown_timeout,
846                &mut zcashd_compat_task_handle,
847            )
848            .await
849            .is_err()
850            {
851                warn!(
852                    ?zcashd_compat_shutdown_timeout,
853                    "zcashd-compat supervisor did not finish before shutdown timeout; \
854                     abandoning child process handle"
855                );
856                // The supervisor spawns zcashd without kill_on_drop, so this
857                // abort abandons an already-signalled child rather than
858                // SIGKILLing it mid-flush.
859                zcashd_compat_task_handle.abort();
860            }
861        } else {
862            debug!("aborting zcashd-compat supervisor task without managed child shutdown");
863            zcashd_compat_task_handle.abort();
864        }
865
866        // startup tasks
867        state_checkpoint_verify_handle.abort();
868        old_databases_task_handle.abort();
869
870        info!(
871            "exiting Zebra: all tasks have been asked to stop, waiting for remaining tasks to finish"
872        );
873
874        exit_status
875    }
876
877    /// Returns the bound for the state service buffer,
878    /// based on the configurations of the services that use the state concurrently.
879    fn state_buffer_bound() -> usize {
880        let config = APPLICATION.config();
881
882        // Ignore the checkpoint verify limit, because it is very large.
883        //
884        // TODO: do we also need to account for concurrent use across services?
885        //       we could multiply the maximum by 3/2, or add a fixed constant
886        [
887            config.sync.download_concurrency_limit,
888            config.sync.full_verify_concurrency_limit,
889            inbound::downloads::MAX_INBOUND_CONCURRENCY,
890            mempool::downloads::MAX_INBOUND_CONCURRENCY,
891        ]
892        .into_iter()
893        .max()
894        .unwrap()
895    }
896}
897
898impl Runnable for StartCmd {
899    /// Start the application.
900    fn run(&self) {
901        info!("Starting zebrad");
902        let rt = APPLICATION
903            .state()
904            .components_mut()
905            .get_downcast_mut::<TokioComponent>()
906            .expect("TokioComponent should be available")
907            .rt
908            .take();
909
910        rt.expect("runtime should not already be taken")
911            .run(self.start());
912
913        info!("stopping zebrad");
914    }
915}
916
917impl config::Override<ZebradConfig> for StartCmd {
918    // Process the given command line options, overriding settings from
919    // a configuration file using explicit flags taken from command-line
920    // arguments.
921    fn override_config(&self, mut config: ZebradConfig) -> Result<ZebradConfig, FrameworkError> {
922        if !self.filters.is_empty() {
923            config.tracing.filter = Some(self.filters.join(","));
924        }
925
926        // `--zcashd-compat` is a one-way override that enables zcashd-compat mode.
927        // The actual zcashd-compat guardrails are applied below using
928        // `config.zcashd_compat.enabled` so CLI and config-file activation share one path.
929        if self.zcashd_compat {
930            config.zcashd_compat.enabled = true;
931        }
932
933        if !config.zcashd_compat.enabled && !config.zcashd_compat.block_gossip_peer_ips.is_empty() {
934            return Err(std::io::Error::other(
935                "zcashd_compat.block_gossip_peer_ips requires zcashd_compat.enabled = true",
936            )
937            .into());
938        }
939
940        if config.zcashd_compat.enabled && config.zcashd_compat.manage_zcashd {
941            zcashd_compat::reject_peer_selection_extra_args(
942                &config.zcashd_compat.zcashd_extra_args,
943            )
944            .map_err(|err| std::io::Error::other(err.to_string()))?;
945
946            match zcashd_compat::effective_zcashd_source(&config.zcashd_compat) {
947                Ok(zcashd_compat::ZcashdBinarySource::Path(path))
948                    if !zcashd_compat::is_command_resolvable(Path::new(&path)) =>
949                {
950                    return Err(std::io::Error::other(format!(
951                        "zcashd-compat mode could not resolve zcashd_path={}",
952                        path.display()
953                    ))
954                    .into());
955                }
956                Ok(_) => {}
957                Err(err) => return Err(std::io::Error::other(err.to_string()).into()),
958            }
959        }
960
961        Ok(config)
962    }
963}
964
965#[cfg(test)]
966mod tests {
967    use abscissa_core::config::Override;
968    use color_eyre::eyre::eyre;
969
970    use super::StartCmd;
971    use crate::components::zcashd_compat;
972    use crate::config::ZebradConfig;
973
974    #[test]
975    fn zcashd_compat_flag_enables_mode() {
976        let cmd = StartCmd {
977            filters: Vec::new(),
978            zcashd_compat: true,
979            unsafe_low_specs: false,
980        };
981        let mut config = ZebradConfig::default();
982        config.zcashd_compat.manage_zcashd = false;
983
984        let config = cmd
985            .override_config(config)
986            .expect("zcashd-compat override config should succeed");
987
988        assert!(config.zcashd_compat.enabled);
989    }
990
991    #[test]
992    fn zcashd_compat_config_enables_mode() {
993        let cmd = StartCmd {
994            filters: Vec::new(),
995            zcashd_compat: false,
996            unsafe_low_specs: false,
997        };
998        let mut config = ZebradConfig::default();
999        config.zcashd_compat.enabled = true;
1000        config.zcashd_compat.manage_zcashd = false;
1001
1002        let config = cmd
1003            .override_config(config)
1004            .expect("zcashd-compat override config should succeed");
1005
1006        assert!(config.zcashd_compat.enabled);
1007    }
1008
1009    #[test]
1010    fn block_gossip_peer_ips_require_zcashd_compat() {
1011        let cmd = StartCmd {
1012            filters: Vec::new(),
1013            zcashd_compat: false,
1014            unsafe_low_specs: false,
1015        };
1016        let mut config = ZebradConfig::default();
1017        config.zcashd_compat.block_gossip_peer_ips =
1018            vec![std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)];
1019
1020        let error = cmd
1021            .override_config(config)
1022            .expect_err("block gossip peers should require zcashd-compat");
1023
1024        assert!(
1025            error
1026                .to_string()
1027                .contains("zcashd_compat.block_gossip_peer_ips requires"),
1028            "error should explain the zcashd-compat requirement: {error}"
1029        );
1030    }
1031
1032    #[test]
1033    fn zcashd_compat_config_rejects_peer_selection_extra_args() {
1034        let cmd = StartCmd {
1035            filters: Vec::new(),
1036            zcashd_compat: false,
1037            unsafe_low_specs: false,
1038        };
1039        let mut config = ZebradConfig::default();
1040        config.zcashd_compat.enabled = true;
1041        config.zcashd_compat.manage_zcashd = true;
1042        config.zcashd_compat.zcashd_source = zcashd_compat::ConfigZcashdBinarySource::Embedded;
1043        config.zcashd_compat.zcashd_extra_args = vec!["-addnode=1.2.3.4".to_string()];
1044
1045        let error = cmd
1046            .override_config(config)
1047            .expect_err("peer-selection extra args should be rejected");
1048        assert!(
1049            error.to_string().contains("peer-selection"),
1050            "unexpected error: {error}"
1051        );
1052    }
1053
1054    #[test]
1055    fn zcashd_compat_manage_zcashd_requires_resolvable_path() {
1056        let cmd = StartCmd {
1057            filters: Vec::new(),
1058            zcashd_compat: true,
1059            unsafe_low_specs: false,
1060        };
1061        let mut config = ZebradConfig::default();
1062        config.zcashd_compat.manage_zcashd = true;
1063        config.zcashd_compat.zcashd_path = Some("/definitely/missing/zcashd-compat".into());
1064
1065        let error = cmd
1066            .override_config(config)
1067            .expect_err("zcashd-compat override should fail for an unresolvable zcashd path");
1068
1069        assert!(
1070            error
1071                .to_string()
1072                .contains("zcashd-compat mode could not resolve zcashd_path"),
1073            "unexpected error: {error}"
1074        );
1075    }
1076
1077    #[test]
1078    fn zcashd_compat_path_source_requires_explicit_path() {
1079        let cmd = StartCmd {
1080            filters: Vec::new(),
1081            zcashd_compat: true,
1082            unsafe_low_specs: false,
1083        };
1084        let mut config = ZebradConfig::default();
1085        config.zcashd_compat.manage_zcashd = true;
1086        config.zcashd_compat.zcashd_source = zcashd_compat::ConfigZcashdBinarySource::Path;
1087        config.zcashd_compat.zcashd_path = None;
1088
1089        let error = cmd
1090            .override_config(config)
1091            .expect_err("path source should require explicit zcashd_path");
1092        assert!(
1093            error.to_string().contains("zcashd_source=path"),
1094            "unexpected error: {error}"
1095        );
1096    }
1097
1098    #[test]
1099    fn zcashd_compat_embedded_source_allows_missing_local_path() {
1100        let cmd = StartCmd {
1101            filters: Vec::new(),
1102            zcashd_compat: true,
1103            unsafe_low_specs: false,
1104        };
1105        let mut config = ZebradConfig::default();
1106        config.zcashd_compat.manage_zcashd = true;
1107        config.zcashd_compat.zcashd_source = zcashd_compat::ConfigZcashdBinarySource::Embedded;
1108        config.zcashd_compat.zcashd_path = None;
1109
1110        cmd.override_config(config)
1111            .expect("embedded source should be validated at runtime, not override-time");
1112    }
1113
1114    #[test]
1115    fn zcashd_compat_config_manage_zcashd_requires_resolvable_path() {
1116        let cmd = StartCmd {
1117            filters: Vec::new(),
1118            zcashd_compat: false,
1119            unsafe_low_specs: false,
1120        };
1121        let mut config = ZebradConfig::default();
1122        config.zcashd_compat.enabled = true;
1123        config.zcashd_compat.manage_zcashd = true;
1124        config.zcashd_compat.zcashd_path = Some("/definitely/missing/zcashd-compat".into());
1125
1126        let error = cmd
1127            .override_config(config)
1128            .expect_err("zcashd-compat config should fail for an unresolvable zcashd path");
1129
1130        assert!(
1131            error
1132                .to_string()
1133                .contains("zcashd-compat mode could not resolve zcashd_path"),
1134            "unexpected error: {error}"
1135        );
1136    }
1137
1138    #[test]
1139    fn zcashd_compat_supervisor_shutdown_timeout_matches_config() {
1140        let mut config = ZebradConfig::default();
1141
1142        config.zcashd_compat.enabled = true;
1143        config.zcashd_compat.manage_zcashd = true;
1144        config.zcashd_compat.shutdown_grace_period = std::time::Duration::from_secs(42);
1145        assert_eq!(
1146            StartCmd::zcashd_compat_supervisor_shutdown_timeout(&config),
1147            Some(
1148                std::time::Duration::from_secs(42)
1149                    + StartCmd::ZCASHD_COMPAT_SHUTDOWN_TIMEOUT_MARGIN
1150            ),
1151            "outer supervisor wait must exceed the child grace period so task \
1152             abort cannot preempt graceful termination",
1153        );
1154
1155        config.zcashd_compat.manage_zcashd = false;
1156        assert_eq!(
1157            StartCmd::zcashd_compat_supervisor_shutdown_timeout(&config),
1158            None
1159        );
1160
1161        config.zcashd_compat.enabled = false;
1162        config.zcashd_compat.manage_zcashd = true;
1163        assert_eq!(
1164            StartCmd::zcashd_compat_supervisor_shutdown_timeout(&config),
1165            None
1166        );
1167    }
1168
1169    #[test]
1170    fn zcashd_compat_supervisor_ok_exit_does_not_exit_zebra() {
1171        assert!(!StartCmd::zcashd_compat_supervisor_should_exit(Ok(Ok(()))));
1172    }
1173
1174    #[test]
1175    fn zcashd_compat_supervisor_error_does_not_exit_zebra() {
1176        assert!(!StartCmd::zcashd_compat_supervisor_should_exit(Ok(Err(
1177            eyre!("simulated zcashd supervisor runtime failure"),
1178        ))));
1179    }
1180
1181    #[tokio::test]
1182    async fn zcashd_compat_supervisor_panic_does_not_exit_zebra() {
1183        let join_err = tokio::spawn(async {
1184            panic!("simulated zcashd supervisor panic");
1185        })
1186        .await
1187        .expect_err("task should panic");
1188
1189        assert!(!StartCmd::zcashd_compat_supervisor_should_exit(Err(
1190            join_err
1191        )));
1192    }
1193}