use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
use indexmap::IndexSet;
use tempfile::TempDir;
use tower::{service_fn, BoxError};
use zebra_chain::{
block::{Block, Height},
chain_tip::NoChainTip,
parameters::Network,
serialization::ZcashDeserializeInto,
};
use zebra_network::{AddressBook, InventoryResponse, Request, Response};
use zebrad::{components::sync, config::ZebradConfig};
use color_eyre::eyre::eyre;
use zebra_test::{args, prelude::*};
use super::{
config::{live_peers, persistent_test_config, testdir, use_live_peers},
launch::ZebradTestDirExt,
};
pub const TINY_CHECKPOINT_TEST_HEIGHT: Height = Height(0);
pub const MEDIUM_CHECKPOINT_TEST_HEIGHT: Height =
Height(zebra_consensus::MAX_CHECKPOINT_HEIGHT_GAP as u32);
pub const LARGE_CHECKPOINT_TEST_HEIGHT: Height =
Height((zebra_consensus::MAX_CHECKPOINT_HEIGHT_GAP * 2) as u32);
pub const STOP_AT_HEIGHT_REGEX: &str = "stopping at configured height";
pub const SYNC_FINISHED_REGEX: &str =
r"finished initial sync to chain tip, using gossiped blocks .*sync_percent.*=.*100\.";
#[cfg(feature = "lightwalletd-grpc-tests")]
pub const SYNC_PROGRESS_REGEX: &str = r"sync_percent";
#[cfg(feature = "zebra-checkpoints")]
pub const CHECKPOINT_VERIFIER_REGEX: &str =
r"initializing block verifier router.*max_checkpoint_height.*=.*Height";
pub const STOP_ON_LOAD_TIMEOUT: Duration = Duration::from_secs(10);
pub const TINY_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(240);
pub const LARGE_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(480);
pub const FINISH_PARTIAL_SYNC_TIMEOUT: Duration = Duration::from_secs(11 * 60 * 60);
pub const FINISH_FULL_SYNC_TIMEOUT: Duration = Duration::from_secs(72 * 60 * 60);
pub const MIN_HEIGHT_FOR_DEFAULT_LOOKAHEAD: Height =
Height(3 * sync::DEFAULT_CHECKPOINT_CONCURRENCY_LIMIT as u32);
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum MempoolBehavior {
ForceActivationAt(Height),
#[allow(dead_code)]
ShouldAutomaticallyActivate,
ShouldNotActivate,
}
impl MempoolBehavior {
pub fn enable_at_height(&self) -> Option<u32> {
match self {
MempoolBehavior::ForceActivationAt(height) => Some(height.0),
MempoolBehavior::ShouldAutomaticallyActivate | MempoolBehavior::ShouldNotActivate => {
None
}
}
}
pub fn require_activation(&self) -> bool {
self.require_forced_activation() || self.require_automatic_activation()
}
pub fn require_forced_activation(&self) -> bool {
matches!(self, MempoolBehavior::ForceActivationAt(_))
}
pub fn require_automatic_activation(&self) -> bool {
matches!(self, MempoolBehavior::ShouldAutomaticallyActivate)
}
#[allow(dead_code)]
pub fn require_no_activation(&self) -> bool {
matches!(self, MempoolBehavior::ShouldNotActivate)
}
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(reuse_tempdir))]
pub fn sync_until(
height: Height,
network: &Network,
stop_regex: &str,
timeout: Duration,
reuse_tempdir: impl Into<Option<TempDir>>,
mempool_behavior: MempoolBehavior,
checkpoint_sync: bool,
check_legacy_chain: bool,
) -> Result<TempDir> {
sync_until_with_peers(
height,
network,
stop_regex,
timeout,
reuse_tempdir,
mempool_behavior,
checkpoint_sync,
check_legacy_chain,
live_peers(network),
)
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(reuse_tempdir))]
pub fn sync_until_with_peers(
height: Height,
network: &Network,
stop_regex: &str,
timeout: Duration,
reuse_tempdir: impl Into<Option<TempDir>>,
mempool_behavior: MempoolBehavior,
checkpoint_sync: bool,
check_legacy_chain: bool,
initial_peers: IndexSet<String>,
) -> Result<TempDir> {
let _init_guard = zebra_test::init();
if zebra_test::net::zebra_skip_network_tests() {
return testdir();
}
let reuse_tempdir = reuse_tempdir.into();
let mut config = persistent_test_config(network)?;
config.network.initial_mainnet_peers = initial_peers.clone();
config.network.initial_testnet_peers = initial_peers;
config.state.debug_stop_at_height = Some(height.0);
config.mempool.debug_enable_at_height = mempool_behavior.enable_at_height();
config.consensus.checkpoint_sync = checkpoint_sync;
if height > MIN_HEIGHT_FOR_DEFAULT_LOOKAHEAD {
config.sync.checkpoint_verify_concurrency_limit =
sync::DEFAULT_CHECKPOINT_CONCURRENCY_LIMIT;
}
let tempdir = if let Some(reuse_tempdir) = reuse_tempdir {
reuse_tempdir.replace_config(&mut config)?
} else {
testdir()?.with_config(&mut config)?
};
let child = tempdir.spawn_child(args!["start"])?.with_timeout(timeout)?;
let network_log = format!("network: {network},");
if mempool_behavior.require_activation() {
let mut child = check_sync_logs_until(
child,
network,
stop_regex,
mempool_behavior,
check_legacy_chain,
)?;
child.kill(true)?;
let dir = child.dir.take().expect("dir was not already taken");
child.wait_with_output()?;
std::thread::sleep(std::time::Duration::from_secs(3));
Ok(dir)
} else {
assert!(
height.0 < 2_000_000,
"zebrad must exit by itself, so we can collect all the output",
);
let output = child.wait_with_output()?;
output.stdout_line_contains(&network_log)?;
if check_legacy_chain {
output.stdout_line_contains("starting legacy chain check")?;
output.stdout_line_contains("no legacy chain found")?;
}
assert!(output.stdout_line_contains("activating mempool").is_err());
assert!(output
.stdout_line_contains("sending mempool transaction broadcast")
.is_err());
output.stdout_line_matches(stop_regex)?;
output.assert_was_not_killed()?;
let output = output.assert_success()?;
Ok(output.dir.expect("wait_with_output sets dir"))
}
}
#[tracing::instrument(skip(zebrad))]
pub fn check_sync_logs_until(
mut zebrad: TestChild<TempDir>,
network: &Network,
stop_regex: &str,
mempool_behavior: MempoolBehavior,
check_legacy_chain: bool,
) -> Result<TestChild<TempDir>> {
zebrad.expect_stdout_line_matches(format!("network: {network},"))?;
if check_legacy_chain {
zebrad.expect_stdout_line_matches("starting legacy chain check")?;
zebrad.expect_stdout_line_matches("no legacy chain found")?;
zebrad.expect_stdout_line_matches("starting state checkpoint validation")?;
zebrad.expect_stdout_line_matches("finished state checkpoint validation")?;
}
if mempool_behavior.require_forced_activation() {
zebrad.expect_stdout_line_matches("enabling mempool for debugging")?;
}
zebrad.expect_stdout_line_matches("activating mempool")?;
zebrad.expect_stdout_line_matches(stop_regex)?;
Ok(zebrad)
}
fn get_zebra_cached_state_dir() -> PathBuf {
ZebradConfig::load(None)
.map(|c| c.state.cache_dir)
.unwrap_or_else(|_| "/zebrad-cache".into())
}
pub fn cached_mandatory_checkpoint_test_config(network: &Network) -> Result<ZebradConfig> {
let mut config = persistent_test_config(network)?;
use_live_peers(&mut config);
config.state.cache_dir = get_zebra_cached_state_dir();
config.sync.checkpoint_verify_concurrency_limit = sync::DEFAULT_CHECKPOINT_CONCURRENCY_LIMIT;
Ok(config)
}
#[allow(clippy::print_stderr)]
#[tracing::instrument]
pub fn create_cached_database_height(
network: &Network,
height: Height,
checkpoint_sync: bool,
stop_regex: &str,
) -> Result<()> {
eprintln!("creating cached database");
let mut config = cached_mandatory_checkpoint_test_config(network)?;
config.state.debug_stop_at_height = Some(height.0);
config.consensus.checkpoint_sync = checkpoint_sync;
let dir = get_zebra_cached_state_dir();
let mut child = dir
.with_exact_config(&config)?
.spawn_child(args!["start"])?
.with_timeout(FINISH_FULL_SYNC_TIMEOUT)?
.bypass_test_capture(true);
let network = format!("network: {network},");
child.expect_stdout_line_matches(network)?;
child.expect_stdout_line_matches("starting legacy chain check")?;
child.expect_stdout_line_matches("no legacy chain found")?;
child.expect_stdout_line_matches(stop_regex)?;
child.kill(true)?;
Ok(())
}
const GENESIS_PEER_MAX_CONNECTIONS_PER_IP: usize = 8;
pub struct GenesisPeer {
listen_addr: SocketAddr,
_network_handles: (
zebra_network::NetworkService,
Arc<std::sync::Mutex<AddressBook>>,
tokio::sync::mpsc::Sender<zebra_network::MisbehaviorReport>,
),
_runtime: tokio::runtime::Runtime,
}
impl GenesisPeer {
pub fn spawn(network: &Network) -> Result<Self> {
let genesis_bytes = match network {
Network::Mainnet => zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES.as_slice(),
_ if network.is_default_testnet() => {
zebra_test::vectors::BLOCK_TESTNET_GENESIS_BYTES.as_slice()
}
_ => return Err(eyre!("no genesis block test vector for {network}")),
};
let genesis: Arc<Block> = genesis_bytes.zcash_deserialize_into()?;
let genesis_hash = genesis.hash();
if genesis_hash != network.genesis_hash() {
return Err(eyre!(
"genesis block test vector for {network} has the wrong hash: {genesis_hash:?}"
));
}
let inbound_service = service_fn(move |request: Request| {
let genesis = genesis.clone();
async move {
let response = match request {
Request::BlocksByHash(hashes) => Response::Blocks(
hashes
.into_iter()
.map(|hash| {
if hash == genesis_hash {
InventoryResponse::Available((genesis.clone(), None))
} else {
InventoryResponse::Missing(hash)
}
})
.collect(),
),
Request::Peers => Response::Peers(Vec::new()),
_ => Response::Nil,
};
Ok::<_, BoxError>(response)
}
});
let config = zebra_network::Config {
listen_addr: "127.0.0.1:0"
.parse()
.expect("hard-coded IPv4 localhost address is valid"),
network: network.clone(),
initial_mainnet_peers: IndexSet::new(),
initial_testnet_peers: IndexSet::new(),
cache_dir: zebra_network::CacheDir::disabled(),
max_connections_per_ip: GENESIS_PEER_MAX_CONNECTIONS_PER_IP,
..zebra_network::Config::default()
};
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()?;
let (peer_set, address_book, misbehavior_tx, _advertiser) =
runtime.block_on(zebra_network::init(
config,
inbound_service,
NoChainTip,
"Genesis test peer".to_string(),
));
let listen_addr = address_book
.lock()
.expect("the address book lock is not held by a panicked thread")
.local_listener_socket_addr();
Ok(Self {
listen_addr,
_network_handles: (peer_set, address_book, misbehavior_tx),
_runtime: runtime,
})
}
pub fn initial_peers(&self) -> IndexSet<String> {
[self.listen_addr.to_string()].into_iter().collect()
}
}