use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::Path,
sync::Arc,
};
use abscissa_core::{config, Command, FrameworkError};
use color_eyre::eyre::{eyre, Report};
use futures::FutureExt;
use tokio::{
pin, select,
sync::{oneshot, watch},
};
use tower::{builder::ServiceBuilder, util::BoxService, ServiceExt};
use tracing_futures::Instrument;
use zebra_chain::block::genesis::regtest_genesis_block;
use zebra_consensus::router::BackgroundTaskHandles;
use zebra_rpc::{methods::RpcImpl, server::RpcServer, SubmitBlockChannel};
use crate::{
application::{build_version, user_agent, LAST_WARN_ERROR_LOG_SENDER},
components::{
health,
inbound::{self, InboundSetupData, MAX_INBOUND_RESPONSE_TIME},
mempool::{self, Mempool},
notify::{self, BlockNotifyError},
sync::{self, show_block_chain_progress, VERIFICATION_PIPELINE_SCALING_MULTIPLIER},
tokio::{RuntimeRun, TokioComponent},
zcashd_compat, ChainSync, Inbound,
},
config::ZebradConfig,
prelude::*,
};
#[cfg(feature = "internal-miner")]
use crate::components;
#[derive(Command, Debug, Default, clap::Parser)]
pub struct StartCmd {
#[clap(help = "tracing filters which override the zebrad.toml config")]
filters: Vec<String>,
#[clap(long)]
zcashd_compat: bool,
#[clap(long = "unsafe-low-specs")]
unsafe_low_specs: bool,
}
#[cfg(target_os = "linux")]
fn check_tcp_slow_start_after_idle() {
const PATH: &str = "/proc/sys/net/ipv4/tcp_slow_start_after_idle";
let raw = match std::fs::read_to_string(PATH) {
Ok(raw) => raw,
Err(error) => {
debug!(
?error,
path = PATH,
"could not read TCP sysctl, skipping check"
);
return;
}
};
if raw.trim() == "0" {
return;
}
warn!(
setting = "net.ipv4.tcp_slow_start_after_idle",
"TCP slow-start-after-idle is enabled, which resets TCP's congestion window \
between block requests and significantly reduces single-peer throughput for \
block propagation. \
Hint: set `net.ipv4.tcp_slow_start_after_idle=0` via sysctl. \
See https://zebra.zfnd.org/user/troubleshooting.html#linux-tcp-tuning-for-block-propagation"
);
}
#[cfg(not(target_os = "linux"))]
fn check_tcp_slow_start_after_idle() {}
impl StartCmd {
const ZCASHD_COMPAT_SHUTDOWN_TIMEOUT_MARGIN: std::time::Duration =
std::time::Duration::from_secs(30);
fn zcashd_compat_p2p_connect_addr(
config: &ZebradConfig,
local_listener: SocketAddr,
) -> SocketAddr {
if let Some(addr) = config.zcashd_compat.p2p_connect_addr {
return addr;
}
if local_listener.ip().is_unspecified() {
match local_listener.ip() {
IpAddr::V4(_) => SocketAddr::from(([127, 0, 0, 1], local_listener.port())),
IpAddr::V6(_) => {
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), local_listener.port())
}
}
} else {
local_listener
}
}
fn zcashd_compat_default_block_gossip_peer_ips() -> Vec<IpAddr> {
vec![
IpAddr::V4(Ipv4Addr::LOCALHOST),
IpAddr::V6(Ipv6Addr::LOCALHOST),
]
}
fn zcashd_compat_supervisor_shutdown_timeout(
config: &ZebradConfig,
) -> Option<std::time::Duration> {
(config.zcashd_compat.enabled && config.zcashd_compat.manage_zcashd).then_some(
config
.zcashd_compat
.shutdown_grace_period
.saturating_add(Self::ZCASHD_COMPAT_SHUTDOWN_TIMEOUT_MARGIN),
)
}
fn zcashd_compat_supervisor_should_exit(
zcashd_compat_result: Result<Result<(), Report>, tokio::task::JoinError>,
) -> bool {
zcashd_compat::set_supervision_unexpectedly_disabled_metrics();
match zcashd_compat_result {
Ok(Ok(())) => {
warn!(
"zcashd-compat supervisor task exited unexpectedly in supervision mode; \
continuing without zcashd supervision"
);
}
Ok(Err(err)) => {
warn!(
?err,
"zcashd-compat supervisor task failed in supervision mode; \
continuing without zcashd supervision"
);
}
Err(join_err) => {
warn!(
?join_err,
"zcashd-compat supervisor task panicked in supervision mode; \
continuing without zcashd supervision"
);
}
}
false
}
async fn start(&self) -> Result<(), Report> {
check_tcp_slow_start_after_idle();
let config = APPLICATION.config();
let is_regtest = config.network.network.is_regtest();
let config = if is_regtest {
Arc::new(ZebradConfig {
mempool: mempool::Config {
debug_enable_at_height: Some(0),
..config.mempool
},
..Arc::unwrap_or_clone(config)
})
} else {
config
};
let zcashd_compat_block_gossip_peer_ips = if config.zcashd_compat.enabled {
if config.zcashd_compat.block_gossip_peer_ips.is_empty() {
if config
.zcashd_compat
.p2p_connect_addr
.is_some_and(|addr| !addr.ip().is_loopback())
{
warn!(
p2p_connect_addr = ?config.zcashd_compat.p2p_connect_addr,
"zcashd_compat.p2p_connect_addr is not loopback, but \
zcashd_compat.block_gossip_peer_ips defaults to loopback only; \
if the sidecar connects from a non-loopback IP, set \
block_gossip_peer_ips to that IP or it will not receive \
pinned block gossip"
);
}
Self::zcashd_compat_default_block_gossip_peer_ips()
} else {
config.zcashd_compat.block_gossip_peer_ips.clone()
}
} else {
Vec::new()
};
if config.zcashd_compat.enabled {
let preflight_config = config.clone();
let unsafe_low_specs = self.unsafe_low_specs;
tokio::task::spawn_blocking(move || {
zcashd_compat::run_preflight(&preflight_config, unsafe_low_specs)
})
.await
.map_err(|err| eyre!("failed to join zcashd-compat preflight task: {err}"))??;
}
let resolved_zcashd_path = if config.zcashd_compat.enabled
&& config.zcashd_compat.manage_zcashd
{
let zcashd_compat_config = config.zcashd_compat.clone();
let state_cache_dir = config.state.cache_dir.clone();
Some(
tokio::task::spawn_blocking(move || {
zcashd_compat::resolve_zcashd_binary_path(
&zcashd_compat_config,
&state_cache_dir,
)
})
.await
.map_err(|err| eyre!("failed to join managed zcashd binary resolver: {err}"))??,
)
} else {
None
};
info!("initializing node state");
let (_, max_checkpoint_height) = zebra_consensus::router::init_checkpoint_list(
config.consensus.clone(),
&config.network.network,
);
info!("opening database, this may take a few minutes");
let (state_service, read_only_state_service, latest_chain_tip, chain_tip_change) =
zebra_state::init(
config.state.clone(),
&config.network.network,
max_checkpoint_height,
config.sync.checkpoint_verify_concurrency_limit
* (VERIFICATION_PIPELINE_SCALING_MULTIPLIER + 1),
)
.await;
info!("logging database metrics on startup");
read_only_state_service.log_db_metrics();
let state = ServiceBuilder::new()
.buffer(Self::state_buffer_bound())
.service(state_service);
info!("initializing network");
let (setup_tx, setup_rx) = oneshot::channel();
let inbound = ServiceBuilder::new()
.load_shed()
.buffer(inbound::downloads::MAX_INBOUND_CONCURRENCY)
.timeout(MAX_INBOUND_RESPONSE_TIME)
.service(Inbound::new(
config.sync.full_verify_concurrency_limit,
setup_rx,
));
let (peer_set, address_book, misbehavior_sender) =
zebra_network::init_with_block_gossip_peer_ips(
config.network.clone(),
inbound,
latest_chain_tip.clone(),
user_agent(),
zcashd_compat_block_gossip_peer_ips,
)
.await;
info!("initializing verifiers");
let (tx_verifier_setup_tx, tx_verifier_setup_rx) = oneshot::channel();
let (block_verifier_router, tx_verifier, consensus_task_handles, max_checkpoint_height) =
zebra_consensus::router::init(
config.consensus.clone(),
&config.network.network,
state.clone(),
tx_verifier_setup_rx,
)
.await;
info!("initializing syncer");
let (mut syncer, sync_status) = ChainSync::new(
&config,
max_checkpoint_height,
peer_set.clone(),
block_verifier_router.clone(),
state.clone(),
latest_chain_tip.clone(),
misbehavior_sender.clone(),
);
info!("initializing mempool");
let (mempool, mempool_transaction_subscriber) = Mempool::new(
&config.network.network,
&config.mempool,
peer_set.clone(),
state.clone(),
tx_verifier,
sync_status.clone(),
latest_chain_tip.clone(),
chain_tip_change.clone(),
misbehavior_sender.clone(),
);
let mempool = BoxService::new(mempool);
let mempool = ServiceBuilder::new()
.buffer(mempool::downloads::MAX_INBOUND_CONCURRENCY)
.service(mempool);
if tx_verifier_setup_tx.send(mempool.clone()).is_err() {
warn!("error setting up the transaction verifier with a handle to the mempool service");
};
info!("fully initializing inbound peer request handler");
let setup_data = InboundSetupData {
address_book: address_book.clone(),
block_download_peer_set: peer_set.clone(),
block_verifier: block_verifier_router.clone(),
mempool: mempool.clone(),
state: state.clone(),
latest_chain_tip: latest_chain_tip.clone(),
misbehavior_sender,
};
setup_tx
.send(setup_data)
.map_err(|_| eyre!("could not send setup data to inbound service"))?;
tokio::task::yield_now().await;
let submit_block_channel = SubmitBlockChannel::new();
let (rpc_impl, mut rpc_tx_queue_handle) = RpcImpl::new(
config.network.network.clone(),
config.mining.clone(),
config.rpc.debug_force_finished_sync,
build_version(),
user_agent(),
mempool.clone(),
state.clone(),
read_only_state_service.clone(),
block_verifier_router.clone(),
sync_status.clone(),
latest_chain_tip.clone(),
address_book.clone(),
LAST_WARN_ERROR_LOG_SENDER.subscribe(),
Some(submit_block_channel.sender()),
);
let rpc_task_handle = if config.rpc.listen_addr.is_some() {
RpcServer::start(rpc_impl.clone(), config.rpc.clone())
.await
.expect("server should start")
} else {
tokio::spawn(std::future::pending().in_current_span())
};
let zcashd_compat_shutdown_timeout =
Self::zcashd_compat_supervisor_shutdown_timeout(&config);
let (zcashd_compat_shutdown_tx, zcashd_compat_shutdown_rx) = watch::channel(false);
let mut zcashd_compat_task_handle = if let Some(resolved_zcashd_path) = resolved_zcashd_path
{
let local_listener = address_book
.lock()
.expect("unexpected panic in address book mutex guard")
.local_listener_socket_addr();
let supervisor_config = zcashd_compat::SupervisorConfig::new(
&config.zcashd_compat,
resolved_zcashd_path,
&config.state.cache_dir,
config.network.network.kind(),
Self::zcashd_compat_p2p_connect_addr(&config, local_listener),
);
info!(
connect = %supervisor_config.zebra_p2p_addr,
"zcashd-compat mode enabled"
);
tokio::spawn(
zcashd_compat::run_supervisor(supervisor_config, zcashd_compat_shutdown_rx)
.in_current_span(),
)
} else {
if config.zcashd_compat.enabled {
zcashd_compat::set_supervision_config_disabled_metrics();
info!("zcashd-compat mode enabled: zcashd supervision disabled");
}
tokio::spawn(std::future::pending().in_current_span())
};
let indexer_rpc_task_handle = {
if let Some(indexer_listen_addr) = config.rpc.indexer_listen_addr {
info!("spawning indexer RPC server");
let (indexer_rpc_task_handle, _listen_addr) = zebra_rpc::indexer::server::init(
indexer_listen_addr,
read_only_state_service.clone(),
latest_chain_tip.clone(),
mempool_transaction_subscriber.clone(),
)
.await
.map_err(|err| eyre!(err))?;
indexer_rpc_task_handle
} else {
warn!("configure an indexer_listen_addr to start the indexer RPC server");
tokio::spawn(std::future::pending().in_current_span())
}
};
info!("spawning block gossip task");
let block_gossip_task_handle = tokio::spawn(
sync::gossip_best_tip_block_hashes(
sync_status.clone(),
chain_tip_change.clone(),
peer_set.clone(),
Some(submit_block_channel.receiver()),
)
.in_current_span(),
);
info!("spawning block notify task");
let block_notify_task_handle: tokio::task::JoinHandle<Result<(), BlockNotifyError>> =
if let Some(command) = config.notify.block_notify_command.clone() {
tokio::spawn(
notify::run_block_notify(
command,
sync_status.clone(),
chain_tip_change.clone(),
)
.in_current_span(),
)
} else {
tokio::spawn(std::future::pending().in_current_span())
};
info!("spawning mempool queue checker task");
let mempool_queue_checker_task_handle = mempool::QueueChecker::spawn(mempool.clone());
info!("spawning mempool transaction gossip task");
let tx_gossip_task_handle = tokio::spawn(
mempool::gossip_mempool_transaction_id(
mempool_transaction_subscriber.subscribe(),
peer_set.clone(),
)
.in_current_span(),
);
info!("spawning delete old databases task");
let mut old_databases_task_handle = zebra_state::check_and_delete_old_state_databases(
&config.state,
&config.network.network,
);
info!("spawning progress logging task");
let (chain_tip_metrics_sender, chain_tip_metrics_receiver) =
health::ChainTipMetrics::channel();
let progress_task_handle = tokio::spawn(
show_block_chain_progress(
config.network.network.clone(),
latest_chain_tip.clone(),
sync_status.clone(),
chain_tip_metrics_sender,
)
.in_current_span(),
);
info!("initializing health endpoints");
let (health_task_handle, _) = health::init(
config.health.clone(),
config.network.network.clone(),
chain_tip_metrics_receiver,
sync_status.clone(),
address_book.clone(),
)
.await;
info!("spawning end of support checking task");
let end_of_support_task_handle = tokio::spawn(
sync::end_of_support::start(config.network.network.clone(), latest_chain_tip.clone())
.in_current_span(),
);
tokio::task::yield_now().await;
info!("spawning mempool crawler task");
let mempool_crawler_task_handle = mempool::Crawler::spawn(
&config.mempool,
peer_set,
mempool.clone(),
sync_status.clone(),
chain_tip_change.clone(),
);
info!("spawning syncer task");
if is_regtest
&& !syncer
.state_contains(config.network.network.genesis_hash())
.await?
{
let genesis_hash = block_verifier_router
.clone()
.oneshot(zebra_consensus::Request::Commit(regtest_genesis_block()))
.await
.expect("should validate Regtest genesis block");
assert_eq!(
genesis_hash,
config.network.network.genesis_hash(),
"validated block hash should match network genesis hash"
)
}
let syncer_task_handle = tokio::spawn(syncer.sync().in_current_span());
#[cfg(feature = "internal-miner")]
let miner_task_handle = if config.mining.is_internal_miner_enabled() {
info!("spawning Zcash miner");
components::miner::spawn_init(&config.metrics, rpc_impl)
} else {
tokio::spawn(std::future::pending().in_current_span())
};
#[cfg(not(feature = "internal-miner"))]
let miner_task_handle: tokio::task::JoinHandle<Result<(), Report>> =
tokio::spawn(std::future::pending().in_current_span());
info!("spawned initial Zebra tasks");
pin!(rpc_task_handle);
pin!(indexer_rpc_task_handle);
pin!(syncer_task_handle);
pin!(block_gossip_task_handle);
pin!(block_notify_task_handle);
pin!(mempool_crawler_task_handle);
pin!(mempool_queue_checker_task_handle);
pin!(tx_gossip_task_handle);
pin!(progress_task_handle);
pin!(end_of_support_task_handle);
pin!(miner_task_handle);
let BackgroundTaskHandles {
mut state_checkpoint_verify_handle,
} = consensus_task_handles;
let state_checkpoint_verify_handle_fused = (&mut state_checkpoint_verify_handle).fuse();
pin!(state_checkpoint_verify_handle_fused);
let old_databases_task_handle_fused = (&mut old_databases_task_handle).fuse();
pin!(old_databases_task_handle_fused);
let mut zcashd_compat_task_finished = false;
let zcashd_compat_task_handle_fused = (&mut zcashd_compat_task_handle).fuse();
pin!(zcashd_compat_task_handle_fused);
let exit_status = loop {
let mut exit_when_task_finishes = true;
let result = select! {
rpc_join_result = &mut rpc_task_handle => {
let rpc_server_result = rpc_join_result
.expect("unexpected panic in the rpc task");
info!(?rpc_server_result, "rpc task exited");
Ok(())
}
rpc_tx_queue_result = &mut rpc_tx_queue_handle => {
rpc_tx_queue_result
.expect("unexpected panic in the rpc transaction queue task");
info!("rpc transaction queue task exited");
Ok(())
}
indexer_rpc_join_result = &mut indexer_rpc_task_handle => {
let indexer_rpc_server_result = indexer_rpc_join_result
.expect("unexpected panic in the indexer task");
info!(?indexer_rpc_server_result, "indexer rpc task exited");
Ok(())
}
sync_result = &mut syncer_task_handle => sync_result
.expect("unexpected panic in the syncer task")
.map(|_| info!("syncer task exited")),
block_gossip_result = &mut block_gossip_task_handle => block_gossip_result
.expect("unexpected panic in the chain tip block gossip task")
.map(|_| info!("chain tip block gossip task exited"))
.map_err(|e| eyre!(e)),
block_notify_result = &mut block_notify_task_handle => block_notify_result
.expect("unexpected panic in the block notify task")
.map(|_| info!("block notify task exited"))
.map_err(|e| eyre!(e)),
mempool_crawl_result = &mut mempool_crawler_task_handle => mempool_crawl_result
.expect("unexpected panic in the mempool crawler")
.map(|_| info!("mempool crawler task exited"))
.map_err(|e| eyre!(e)),
mempool_queue_result = &mut mempool_queue_checker_task_handle => mempool_queue_result
.expect("unexpected panic in the mempool queue checker")
.map(|_| info!("mempool queue checker task exited"))
.map_err(|e| eyre!(e)),
tx_gossip_result = &mut tx_gossip_task_handle => tx_gossip_result
.expect("unexpected panic in the transaction gossip task")
.map(|_| info!("transaction gossip task exited"))
.map_err(|e| eyre!(e)),
progress_result = &mut progress_task_handle => {
info!("chain progress task exited");
progress_result
.expect("unexpected panic in the chain progress task");
}
end_of_support_result = &mut end_of_support_task_handle => end_of_support_result
.expect("unexpected panic in the end of support task")
.map(|_| info!("end of support task exited")),
state_checkpoint_verify_result = &mut state_checkpoint_verify_handle_fused => {
state_checkpoint_verify_result
.unwrap_or_else(|_| panic!(
"unexpected panic checking previous state followed the best chain"));
exit_when_task_finishes = false;
Ok(())
}
old_databases_result = &mut old_databases_task_handle_fused => {
old_databases_result
.unwrap_or_else(|_| panic!(
"unexpected panic deleting old database directories"));
exit_when_task_finishes = false;
Ok(())
}
miner_result = &mut miner_task_handle => miner_result
.expect("unexpected panic in the miner task")
.map(|_| info!("miner task exited")),
zcashd_compat_result = &mut zcashd_compat_task_handle_fused => {
zcashd_compat_task_finished = true;
exit_when_task_finishes =
Self::zcashd_compat_supervisor_should_exit(zcashd_compat_result);
Ok(())
},
};
if let Err(err) = result {
break Err(err);
}
if exit_when_task_finishes {
break Ok(());
}
};
info!("exiting Zebra because an ongoing task exited: asking other tasks to stop");
rpc_task_handle.abort();
rpc_tx_queue_handle.abort();
health_task_handle.abort();
syncer_task_handle.abort();
block_gossip_task_handle.abort();
block_notify_task_handle.abort();
mempool_crawler_task_handle.abort();
mempool_queue_checker_task_handle.abort();
tx_gossip_task_handle.abort();
progress_task_handle.abort();
end_of_support_task_handle.abort();
miner_task_handle.abort();
if zcashd_compat_task_finished {
debug!("zcashd-compat supervisor task already exited before shutdown");
} else if let Some(zcashd_compat_shutdown_timeout) = zcashd_compat_shutdown_timeout {
info!(
?zcashd_compat_shutdown_timeout,
"requesting zcashd-compat supervisor shutdown"
);
if zcashd_compat_shutdown_tx.send(true).is_err() {
warn!("zcashd-compat supervisor shutdown request was not delivered");
}
if tokio::time::timeout(
zcashd_compat_shutdown_timeout,
&mut zcashd_compat_task_handle,
)
.await
.is_err()
{
warn!(
?zcashd_compat_shutdown_timeout,
"zcashd-compat supervisor did not finish before shutdown timeout; \
abandoning child process handle"
);
zcashd_compat_task_handle.abort();
}
} else {
debug!("aborting zcashd-compat supervisor task without managed child shutdown");
zcashd_compat_task_handle.abort();
}
state_checkpoint_verify_handle.abort();
old_databases_task_handle.abort();
info!(
"exiting Zebra: all tasks have been asked to stop, waiting for remaining tasks to finish"
);
exit_status
}
fn state_buffer_bound() -> usize {
let config = APPLICATION.config();
[
config.sync.download_concurrency_limit,
config.sync.full_verify_concurrency_limit,
inbound::downloads::MAX_INBOUND_CONCURRENCY,
mempool::downloads::MAX_INBOUND_CONCURRENCY,
]
.into_iter()
.max()
.unwrap()
}
}
impl Runnable for StartCmd {
fn run(&self) {
info!("Starting zebrad");
let rt = APPLICATION
.state()
.components_mut()
.get_downcast_mut::<TokioComponent>()
.expect("TokioComponent should be available")
.rt
.take();
rt.expect("runtime should not already be taken")
.run(self.start());
info!("stopping zebrad");
}
}
impl config::Override<ZebradConfig> for StartCmd {
fn override_config(&self, mut config: ZebradConfig) -> Result<ZebradConfig, FrameworkError> {
if !self.filters.is_empty() {
config.tracing.filter = Some(self.filters.join(","));
}
if self.zcashd_compat {
config.zcashd_compat.enabled = true;
}
if !config.zcashd_compat.enabled && !config.zcashd_compat.block_gossip_peer_ips.is_empty() {
return Err(std::io::Error::other(
"zcashd_compat.block_gossip_peer_ips requires zcashd_compat.enabled = true",
)
.into());
}
if config.zcashd_compat.enabled && config.zcashd_compat.manage_zcashd {
zcashd_compat::reject_peer_selection_extra_args(
&config.zcashd_compat.zcashd_extra_args,
)
.map_err(|err| std::io::Error::other(err.to_string()))?;
match zcashd_compat::effective_zcashd_source(&config.zcashd_compat) {
Ok(zcashd_compat::ZcashdBinarySource::Path(path))
if !zcashd_compat::is_command_resolvable(Path::new(&path)) =>
{
return Err(std::io::Error::other(format!(
"zcashd-compat mode could not resolve zcashd_path={}",
path.display()
))
.into());
}
Ok(_) => {}
Err(err) => return Err(std::io::Error::other(err.to_string()).into()),
}
}
Ok(config)
}
}
#[cfg(test)]
mod tests {
use abscissa_core::config::Override;
use color_eyre::eyre::eyre;
use super::StartCmd;
use crate::components::zcashd_compat;
use crate::config::ZebradConfig;
#[test]
fn zcashd_compat_flag_enables_mode() {
let cmd = StartCmd {
filters: Vec::new(),
zcashd_compat: true,
unsafe_low_specs: false,
};
let mut config = ZebradConfig::default();
config.zcashd_compat.manage_zcashd = false;
let config = cmd
.override_config(config)
.expect("zcashd-compat override config should succeed");
assert!(config.zcashd_compat.enabled);
}
#[test]
fn zcashd_compat_config_enables_mode() {
let cmd = StartCmd {
filters: Vec::new(),
zcashd_compat: false,
unsafe_low_specs: false,
};
let mut config = ZebradConfig::default();
config.zcashd_compat.enabled = true;
config.zcashd_compat.manage_zcashd = false;
let config = cmd
.override_config(config)
.expect("zcashd-compat override config should succeed");
assert!(config.zcashd_compat.enabled);
}
#[test]
fn block_gossip_peer_ips_require_zcashd_compat() {
let cmd = StartCmd {
filters: Vec::new(),
zcashd_compat: false,
unsafe_low_specs: false,
};
let mut config = ZebradConfig::default();
config.zcashd_compat.block_gossip_peer_ips =
vec![std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)];
let error = cmd
.override_config(config)
.expect_err("block gossip peers should require zcashd-compat");
assert!(
error
.to_string()
.contains("zcashd_compat.block_gossip_peer_ips requires"),
"error should explain the zcashd-compat requirement: {error}"
);
}
#[test]
fn zcashd_compat_config_rejects_peer_selection_extra_args() {
let cmd = StartCmd {
filters: Vec::new(),
zcashd_compat: false,
unsafe_low_specs: false,
};
let mut config = ZebradConfig::default();
config.zcashd_compat.enabled = true;
config.zcashd_compat.manage_zcashd = true;
config.zcashd_compat.zcashd_source = zcashd_compat::ConfigZcashdBinarySource::Embedded;
config.zcashd_compat.zcashd_extra_args = vec!["-addnode=1.2.3.4".to_string()];
let error = cmd
.override_config(config)
.expect_err("peer-selection extra args should be rejected");
assert!(
error.to_string().contains("peer-selection"),
"unexpected error: {error}"
);
}
#[test]
fn zcashd_compat_manage_zcashd_requires_resolvable_path() {
let cmd = StartCmd {
filters: Vec::new(),
zcashd_compat: true,
unsafe_low_specs: false,
};
let mut config = ZebradConfig::default();
config.zcashd_compat.manage_zcashd = true;
config.zcashd_compat.zcashd_path = Some("/definitely/missing/zcashd-compat".into());
let error = cmd
.override_config(config)
.expect_err("zcashd-compat override should fail for an unresolvable zcashd path");
assert!(
error
.to_string()
.contains("zcashd-compat mode could not resolve zcashd_path"),
"unexpected error: {error}"
);
}
#[test]
fn zcashd_compat_path_source_requires_explicit_path() {
let cmd = StartCmd {
filters: Vec::new(),
zcashd_compat: true,
unsafe_low_specs: false,
};
let mut config = ZebradConfig::default();
config.zcashd_compat.manage_zcashd = true;
config.zcashd_compat.zcashd_source = zcashd_compat::ConfigZcashdBinarySource::Path;
config.zcashd_compat.zcashd_path = None;
let error = cmd
.override_config(config)
.expect_err("path source should require explicit zcashd_path");
assert!(
error.to_string().contains("zcashd_source=path"),
"unexpected error: {error}"
);
}
#[test]
fn zcashd_compat_embedded_source_allows_missing_local_path() {
let cmd = StartCmd {
filters: Vec::new(),
zcashd_compat: true,
unsafe_low_specs: false,
};
let mut config = ZebradConfig::default();
config.zcashd_compat.manage_zcashd = true;
config.zcashd_compat.zcashd_source = zcashd_compat::ConfigZcashdBinarySource::Embedded;
config.zcashd_compat.zcashd_path = None;
cmd.override_config(config)
.expect("embedded source should be validated at runtime, not override-time");
}
#[test]
fn zcashd_compat_config_manage_zcashd_requires_resolvable_path() {
let cmd = StartCmd {
filters: Vec::new(),
zcashd_compat: false,
unsafe_low_specs: false,
};
let mut config = ZebradConfig::default();
config.zcashd_compat.enabled = true;
config.zcashd_compat.manage_zcashd = true;
config.zcashd_compat.zcashd_path = Some("/definitely/missing/zcashd-compat".into());
let error = cmd
.override_config(config)
.expect_err("zcashd-compat config should fail for an unresolvable zcashd path");
assert!(
error
.to_string()
.contains("zcashd-compat mode could not resolve zcashd_path"),
"unexpected error: {error}"
);
}
#[test]
fn zcashd_compat_supervisor_shutdown_timeout_matches_config() {
let mut config = ZebradConfig::default();
config.zcashd_compat.enabled = true;
config.zcashd_compat.manage_zcashd = true;
config.zcashd_compat.shutdown_grace_period = std::time::Duration::from_secs(42);
assert_eq!(
StartCmd::zcashd_compat_supervisor_shutdown_timeout(&config),
Some(
std::time::Duration::from_secs(42)
+ StartCmd::ZCASHD_COMPAT_SHUTDOWN_TIMEOUT_MARGIN
),
"outer supervisor wait must exceed the child grace period so task \
abort cannot preempt graceful termination",
);
config.zcashd_compat.manage_zcashd = false;
assert_eq!(
StartCmd::zcashd_compat_supervisor_shutdown_timeout(&config),
None
);
config.zcashd_compat.enabled = false;
config.zcashd_compat.manage_zcashd = true;
assert_eq!(
StartCmd::zcashd_compat_supervisor_shutdown_timeout(&config),
None
);
}
#[test]
fn zcashd_compat_supervisor_ok_exit_does_not_exit_zebra() {
assert!(!StartCmd::zcashd_compat_supervisor_should_exit(Ok(Ok(()))));
}
#[test]
fn zcashd_compat_supervisor_error_does_not_exit_zebra() {
assert!(!StartCmd::zcashd_compat_supervisor_should_exit(Ok(Err(
eyre!("simulated zcashd supervisor runtime failure"),
))));
}
#[tokio::test]
async fn zcashd_compat_supervisor_panic_does_not_exit_zebra() {
let join_err = tokio::spawn(async {
panic!("simulated zcashd supervisor panic");
})
.await
.expect_err("task should panic");
assert!(!StartCmd::zcashd_compat_supervisor_should_exit(Err(
join_err
)));
}
}