//! Acceptance test: runs zebrad as a subprocess and asserts its //! output for given argument combinations matches what is expected. //! //! ## Note on port conflict //! //! If the test child has a cache or port conflict with another test, or a //! running zebrad or zcashd, then it will panic. But the acceptance tests //! expect it to run until it is killed. //! //! If these conflicts cause test failures: //! - run the tests in an isolated environment, //! - run zebrad on a custom cache path and port, //! - run zcashd on a custom port. //! //! ## Failures due to Configured Network Interfaces or Network Connectivity //! //! If your test environment does not have any IPv6 interfaces configured, skip IPv6 tests //! by setting the `ZEBRA_SKIP_IPV6_TESTS` environmental variable. //! //! If it does not have any IPv4 interfaces, IPv4 localhost is not on `127.0.0.1`, //! or you have poor network connectivity, //! skip all the network tests by setting the `ZEBRA_SKIP_NETWORK_TESTS` environmental variable. //! //! ## Large/full sync tests //! //! This file has sync tests that are marked as ignored because they take too much time to run. //! Some of them require environment variables or directories to be present: //! //! - `FULL_SYNC_MAINNET_TIMEOUT_MINUTES` env variable: The total number of minutes we //! will allow this test to run or give up. Value for the Mainnet full sync tests. //! - `FULL_SYNC_TESTNET_TIMEOUT_MINUTES` env variable: The total number of minutes we //! will allow this test to run or give up. Value for the Testnet full sync tests. //! - `ZEBRA_CACHED_STATE_DIR` env variable: The path to a Zebra cached state directory. //! If not set, it defaults to `/zebrad-cache`. For some sync tests, this directory needs to be //! created in the file system with write permissions. //! //! Here are some examples on how to run each of the tests: //! //! ```console //! $ cargo test sync_large_checkpoints_mainnet -- --ignored --nocapture //! //! $ cargo test sync_large_checkpoints_mempool_mainnet -- --ignored --nocapture //! //! $ export ZEBRA_CACHED_STATE_DIR="/zebrad-cache" //! $ sudo mkdir -p "$ZEBRA_CACHED_STATE_DIR" //! $ sudo chmod 777 "$ZEBRA_CACHED_STATE_DIR" //! $ export FULL_SYNC_MAINNET_TIMEOUT_MINUTES=600 //! $ cargo test full_sync_mainnet -- --ignored --nocapture //! //! $ export ZEBRA_CACHED_STATE_DIR="/zebrad-cache" //! $ sudo mkdir -p "$ZEBRA_CACHED_STATE_DIR" //! $ sudo chmod 777 "$ZEBRA_CACHED_STATE_DIR" //! $ export FULL_SYNC_TESTNET_TIMEOUT_MINUTES=600 //! $ cargo test full_sync_testnet -- --ignored --nocapture //! ``` //! //! Please refer to the documentation of each test for more information. //! //! ## Lightwalletd tests //! //! The lightwalletd software is an interface service that uses zebrad or zcashd RPC methods to serve wallets or other applications with blockchain content in an efficient manner. //! There are several versions of lightwalled in the form of different forks. The original //! repo is , zecwallet Lite uses a custom fork: . //! Initially this tests were made with `adityapk00/lightwalletd` fork but changes for fast spendability support had //! been made to `zcash/lightwalletd` only. //! //! We expect `adityapk00/lightwalletd` to remain working with Zebra but for this tests we are using `zcash/lightwalletd`. //! //! Zebra lightwalletd tests are not all marked as ignored but none will run unless //! at least the `ZEBRA_TEST_LIGHTWALLETD` environment variable is present: //! //! - `ZEBRA_TEST_LIGHTWALLETD` env variable: Needs to be present to run any of the lightwalletd tests. //! - `ZEBRA_CACHED_STATE_DIR` env variable: The path to a Zebra cached state directory. //! If not set, it defaults to `/zebrad-cache`. //! - `LIGHTWALLETD_DATA_DIR` env variable: The path to a lightwalletd database. //! - `--features lightwalletd-grpc-tests` cargo flag: The flag given to cargo to build the source code of the running test. //! //! Here are some examples of running each test: //! //! ```console //! $ export ZEBRA_TEST_LIGHTWALLETD=true //! $ cargo test lightwalletd_integration -- --nocapture //! //! $ export ZEBRA_TEST_LIGHTWALLETD=true //! $ export ZEBRA_CACHED_STATE_DIR="/path/to/zebra/state" //! $ export LIGHTWALLETD_DATA_DIR="/path/to/lightwalletd/database" //! $ cargo test lightwalletd_update_sync -- --nocapture //! //! $ export ZEBRA_TEST_LIGHTWALLETD=true //! $ export ZEBRA_CACHED_STATE_DIR="/path/to/zebra/state" //! $ cargo test lightwalletd_full_sync -- --ignored --nocapture //! //! $ export ZEBRA_TEST_LIGHTWALLETD=true //! $ cargo test lightwalletd_test_suite -- --ignored --nocapture //! //! $ export ZEBRA_TEST_LIGHTWALLETD=true //! $ export ZEBRA_CACHED_STATE_DIR="/path/to/zebra/state" //! $ cargo test fully_synced_rpc_test -- --ignored --nocapture //! //! $ export ZEBRA_TEST_LIGHTWALLETD=true //! $ export ZEBRA_CACHED_STATE_DIR="/path/to/zebra/state" //! $ export LIGHTWALLETD_DATA_DIR="/path/to/lightwalletd/database" //! $ cargo test sending_transactions_using_lightwalletd --features lightwalletd-grpc-tests -- --ignored --nocapture //! //! $ export ZEBRA_TEST_LIGHTWALLETD=true //! $ export ZEBRA_CACHED_STATE_DIR="/path/to/zebra/state" //! $ export LIGHTWALLETD_DATA_DIR="/path/to/lightwalletd/database" //! $ cargo test lightwalletd_wallet_grpc_tests --features lightwalletd-grpc-tests -- --ignored --nocapture //! ``` //! //! ## Getblocktemplate tests //! //! Example of how to run the get_block_template test: //! //! ```console //! ZEBRA_CACHED_STATE_DIR=/path/to/zebra/state cargo test get_block_template --features getblocktemplate-rpcs --release -- --ignored --nocapture //! ``` //! //! Example of how to run the submit_block test: //! //! ```console //! ZEBRA_CACHED_STATE_DIR=/path/to/zebra/state cargo test submit_block --features getblocktemplate-rpcs --release -- --ignored --nocapture //! ``` //! //! Please refer to the documentation of each test for more information. //! //! ## Checkpoint Generation Tests //! //! Generate checkpoints on mainnet and testnet using a cached state: //! ```console //! GENERATE_CHECKPOINTS_MAINNET=1 ENTRYPOINT_FEATURES=zebra-checkpoints ZEBRA_CACHED_STATE_DIR=/path/to/zebra/state docker/entrypoint.sh //! GENERATE_CHECKPOINTS_TESTNET=1 ENTRYPOINT_FEATURES=zebra-checkpoints ZEBRA_CACHED_STATE_DIR=/path/to/zebra/state docker/entrypoint.sh //! ``` //! //! ## Disk Space for Testing //! //! The full sync and lightwalletd tests with cached state expect a temporary directory with //! at least 300 GB of disk space (2 copies of the full chain). To use another disk for the //! temporary test files: //! //! ```sh //! export TMPDIR=/path/to/disk/directory //! ``` use std::{ cmp::Ordering, collections::HashSet, env, fs, panic, path::PathBuf, time::{Duration, Instant}, }; use color_eyre::{ eyre::{eyre, WrapErr}, Help, }; use semver::Version; use serde_json::Value; use tower::ServiceExt; use zebra_chain::{ block::{self, genesis::regtest_genesis_block, Height}, parameters::Network::{self, *}, }; use zebra_consensus::ParameterCheckpoint; use zebra_node_services::rpc_client::RpcRequestClient; use zebra_rpc::server::OPENED_RPC_ENDPOINT_MSG; use zebra_state::{constants::LOCK_FILE_ERROR, state_database_format_version_in_code}; #[cfg(not(target_os = "windows"))] use zebra_network::constants::PORT_IN_USE_ERROR; use zebra_test::{ args, command::{to_regex::CollectRegexSet, ContextFrom}, prelude::*, }; #[cfg(not(target_os = "windows"))] use zebra_test::net::random_known_port; mod common; use common::{ check::{is_zebrad_version, EphemeralCheck, EphemeralConfig}, config::{ config_file_full_path, configs_dir, default_test_config, external_address_test_config, os_assigned_rpc_port_config, persistent_test_config, random_known_rpc_port_config, read_listen_addr_from_logs, testdir, }, launch::{ spawn_zebrad_for_rpc, spawn_zebrad_without_rpc, ZebradTestDirExt, BETWEEN_NODES_DELAY, EXTENDED_LAUNCH_DELAY, LAUNCH_DELAY, }, lightwalletd::{can_spawn_lightwalletd_for_rpc, spawn_lightwalletd_for_rpc}, sync::{ create_cached_database_height, sync_until, MempoolBehavior, LARGE_CHECKPOINT_TEST_HEIGHT, LARGE_CHECKPOINT_TIMEOUT, MEDIUM_CHECKPOINT_TEST_HEIGHT, STOP_AT_HEIGHT_REGEX, STOP_ON_LOAD_TIMEOUT, SYNC_FINISHED_REGEX, TINY_CHECKPOINT_TEST_HEIGHT, TINY_CHECKPOINT_TIMEOUT, }, test_type::TestType::{self, *}, }; use crate::common::cached_state::{ wait_for_state_version_message, wait_for_state_version_upgrade, DATABASE_FORMAT_UPGRADE_IS_LONG, }; /// The maximum amount of time that we allow the creation of a future to block the `tokio` executor. /// /// This should be larger than the amount of time between thread time slices on a busy test VM. /// /// This limit only applies to some tests. pub const MAX_ASYNC_BLOCKING_TIME: Duration = zebra_test::mock_service::DEFAULT_MAX_REQUEST_DELAY; /// The test config file prefix for `--feature getblocktemplate-rpcs` configs. pub const GET_BLOCK_TEMPLATE_CONFIG_PREFIX: &str = "getblocktemplate-"; /// The test config file prefix for `--feature shielded-scan` configs. pub const SHIELDED_SCAN_CONFIG_PREFIX: &str = "shieldedscan-"; #[test] fn generate_no_args() -> Result<()> { let _init_guard = zebra_test::init(); let child = testdir()? .with_config(&mut default_test_config(&Mainnet)?)? .spawn_child(args!["generate"])?; let output = child.wait_with_output()?; let output = output.assert_success()?; // First line output.stdout_line_contains("# Default configuration for zebrad")?; Ok(()) } #[test] fn generate_args() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?; let testdir = &testdir; // unexpected free argument `argument` let child = testdir.spawn_child(args!["generate", "argument"])?; let output = child.wait_with_output()?; output.assert_failure()?; // unrecognized option `-f` let child = testdir.spawn_child(args!["generate", "-f"])?; let output = child.wait_with_output()?; output.assert_failure()?; // missing argument to option `-o` let child = testdir.spawn_child(args!["generate", "-o"])?; let output = child.wait_with_output()?; output.assert_failure()?; // Add a config file name to tempdir path let generated_config_path = testdir.path().join("zebrad.toml"); // Valid let child = testdir.spawn_child(args!["generate", "-o": generated_config_path.to_str().unwrap()])?; let output = child.wait_with_output()?; let output = output.assert_success()?; assert_with_context!( testdir.path().exists(), &output, "test temp directory not found" ); assert_with_context!( generated_config_path.exists(), &output, "generated config file not found" ); Ok(()) } #[test] fn help_no_args() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; let child = testdir.spawn_child(args!["help"])?; let output = child.wait_with_output()?; let output = output.assert_success()?; // The first line should have the version output.any_output_line( is_zebrad_version, &output.output.stdout, "stdout", "are valid zebrad semantic versions", )?; // Make sure we are in help by looking for the usage string output.stdout_line_contains("Usage:")?; Ok(()) } #[test] fn help_args() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?; let testdir = &testdir; // The subcommand "argument" wasn't recognized. let child = testdir.spawn_child(args!["help", "argument"])?; let output = child.wait_with_output()?; output.assert_failure()?; // option `-f` does not accept an argument let child = testdir.spawn_child(args!["help", "-f"])?; let output = child.wait_with_output()?; output.assert_failure()?; Ok(()) } #[test] fn start_no_args() -> Result<()> { let _init_guard = zebra_test::init(); // start caches state, so run one of the start tests with persistent state let testdir = testdir()?.with_config(&mut persistent_test_config(&Mainnet)?)?; let mut child = testdir.spawn_child(args!["-v", "start"])?; // Run the program and kill it after a few seconds std::thread::sleep(LAUNCH_DELAY); child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; output.stdout_line_contains("Starting zebrad")?; // Make sure the command passed the legacy chain check output.stdout_line_contains("starting legacy chain check")?; output.stdout_line_contains("no legacy chain found")?; // Make sure the command was killed output.assert_was_killed()?; Ok(()) } #[test] fn start_args() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; let testdir = &testdir; let mut child = testdir.spawn_child(args!["start"])?; // Run the program and kill it after a few seconds std::thread::sleep(LAUNCH_DELAY); child.kill(false)?; let output = child.wait_with_output()?; // Make sure the command was killed output.assert_was_killed()?; output.assert_failure()?; // unrecognized option `-f` let child = testdir.spawn_child(args!["start", "-f"])?; let output = child.wait_with_output()?; output.assert_failure()?; Ok(()) } #[tokio::test] async fn db_init_outside_future_executor() -> Result<()> { let _init_guard = zebra_test::init(); let config = default_test_config(&Mainnet)?; let start = Instant::now(); // This test doesn't need UTXOs to be verified efficiently, because it uses an empty state. let db_init_handle = zebra_state::spawn_init( config.state.clone(), &config.network.network, Height::MAX, 0, ); // it's faster to panic if it takes longer than expected, since the executor // will wait indefinitely for blocking operation to finish once started let block_duration = start.elapsed(); assert!( block_duration <= MAX_ASYNC_BLOCKING_TIME, "futures executor was blocked longer than expected ({block_duration:?})", ); db_init_handle.await?; Ok(()) } /// Check that the block state and peer list caches are written to disk. #[test] fn persistent_mode() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?.with_config(&mut persistent_test_config(&Mainnet)?)?; let testdir = &testdir; let mut child = testdir.spawn_child(args!["-v", "start"])?; // Run the program and kill it after a few seconds std::thread::sleep(EXTENDED_LAUNCH_DELAY); child.kill(false)?; let output = child.wait_with_output()?; // Make sure the command was killed output.assert_was_killed()?; let cache_dir = testdir.path().join("state"); assert_with_context!( cache_dir.read_dir()?.count() > 0, &output, "state directory empty despite persistent state config" ); let cache_dir = testdir.path().join("network"); assert_with_context!( cache_dir.read_dir()?.count() > 0, &output, "network directory empty despite persistent network config" ); Ok(()) } #[test] fn ephemeral_existing_directory() -> Result<()> { ephemeral(EphemeralConfig::Default, EphemeralCheck::ExistingDirectory) } #[test] fn ephemeral_missing_directory() -> Result<()> { ephemeral(EphemeralConfig::Default, EphemeralCheck::MissingDirectory) } #[test] fn misconfigured_ephemeral_existing_directory() -> Result<()> { ephemeral( EphemeralConfig::MisconfiguredCacheDir, EphemeralCheck::ExistingDirectory, ) } #[test] fn misconfigured_ephemeral_missing_directory() -> Result<()> { ephemeral( EphemeralConfig::MisconfiguredCacheDir, EphemeralCheck::MissingDirectory, ) } /// Check that the state directory created on disk matches the state config. /// /// TODO: do a similar test for `network.cache_dir` #[tracing::instrument] fn ephemeral(cache_dir_config: EphemeralConfig, cache_dir_check: EphemeralCheck) -> Result<()> { use std::io::ErrorKind; let _init_guard = zebra_test::init(); let mut config = default_test_config(&Mainnet)?; let run_dir = testdir()?; let ignored_cache_dir = run_dir.path().join("state"); if cache_dir_config == EphemeralConfig::MisconfiguredCacheDir { // Write a configuration that sets both the cache_dir and ephemeral options config.state.cache_dir.clone_from(&ignored_cache_dir); } if cache_dir_check == EphemeralCheck::ExistingDirectory { // We set the cache_dir config to a newly created empty temp directory, // then make sure that it is empty after the test fs::create_dir(&ignored_cache_dir)?; } let mut child = run_dir .path() .with_config(&mut config)? .spawn_child(args!["start"])?; // Run the program and kill it after a few seconds std::thread::sleep(EXTENDED_LAUNCH_DELAY); child.kill(false)?; let output = child.wait_with_output()?; // Make sure the command was killed output.assert_was_killed()?; let expected_run_dir_file_names = match cache_dir_check { // we created the state directory, so it should still exist EphemeralCheck::ExistingDirectory => { assert_with_context!( ignored_cache_dir .read_dir() .expect("ignored_cache_dir should still exist") .count() == 0, &output, "ignored_cache_dir not empty for ephemeral {:?} {:?}: {:?}", cache_dir_config, cache_dir_check, ignored_cache_dir.read_dir().unwrap().collect::>() ); ["state", "network", "zebrad.toml"].iter() } // we didn't create the state directory, so it should not exist EphemeralCheck::MissingDirectory => { assert_with_context!( ignored_cache_dir .read_dir() .expect_err("ignored_cache_dir should not exist") .kind() == ErrorKind::NotFound, &output, "unexpected creation of ignored_cache_dir for ephemeral {:?} {:?}: the cache dir exists and contains these files: {:?}", cache_dir_config, cache_dir_check, ignored_cache_dir.read_dir().unwrap().collect::>() ); ["network", "zebrad.toml"].iter() } }; let expected_run_dir_file_names = expected_run_dir_file_names.map(Into::into).collect(); let run_dir_file_names = run_dir .path() .read_dir() .expect("run_dir should still exist") .map(|dir_entry| dir_entry.expect("run_dir is readable").file_name()) // ignore directory list order, because it can vary based on the OS and filesystem .collect::>(); assert_with_context!( run_dir_file_names == expected_run_dir_file_names, &output, "run_dir not empty for ephemeral {:?} {:?}: expected {:?}, actual: {:?}", cache_dir_config, cache_dir_check, expected_run_dir_file_names, run_dir_file_names ); Ok(()) } #[test] fn version_no_args() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; let child = testdir.spawn_child(args!["--version"])?; let output = child.wait_with_output()?; let output = output.assert_success()?; // The output should only contain the version output.output_check( is_zebrad_version, &output.output.stdout, "stdout", "a valid zebrad semantic version", )?; Ok(()) } #[test] fn version_args() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; let testdir = &testdir; // unrecognized option `-f` let child = testdir.spawn_child(args!["tip-height", "-f"])?; let output = child.wait_with_output()?; output.assert_failure()?; // unrecognized option `-f` is ignored let child = testdir.spawn_child(args!["--version", "-f"])?; let output = child.wait_with_output()?; let output = output.assert_success()?; // The output should only contain the version output.output_check( is_zebrad_version, &output.output.stdout, "stdout", "a valid zebrad semantic version", )?; Ok(()) } /// Run config tests that use the default ports and paths. /// /// Unlike the other tests, these tests can not be run in parallel, because /// they use the generated config. So parallel execution can cause port and /// cache conflicts. #[test] fn config_tests() -> Result<()> { valid_generated_config("start", "Starting zebrad")?; // Check what happens when Zebra parses an invalid config invalid_generated_config()?; // Check that we have a current version of the config stored #[cfg(not(target_os = "windows"))] last_config_is_stored()?; // Check that Zebra's previous configurations still work stored_configs_work()?; // We run the `zebrad` app test after the config tests, to avoid potential port conflicts app_no_args()?; Ok(()) } /// Test that `zebrad` runs the start command with no args #[tracing::instrument] fn app_no_args() -> Result<()> { let _init_guard = zebra_test::init(); // start caches state, so run one of the start tests with persistent state let testdir = testdir()?.with_config(&mut persistent_test_config(&Mainnet)?)?; tracing::info!(?testdir, "running zebrad with no config (default settings)"); let mut child = testdir.spawn_child(args![])?; // Run the program and kill it after a few seconds std::thread::sleep(LAUNCH_DELAY); child.kill(true)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; output.stdout_line_contains("Starting zebrad")?; // Make sure the command passed the legacy chain check output.stdout_line_contains("starting legacy chain check")?; output.stdout_line_contains("no legacy chain found")?; // Make sure the command was killed output.assert_was_killed()?; Ok(()) } /// Test that `zebrad start` can parse the output from `zebrad generate`. #[tracing::instrument] fn valid_generated_config(command: &str, expect_stdout_line_contains: &str) -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?; let testdir = &testdir; // Add a config file name to tempdir path let generated_config_path = testdir.path().join("zebrad.toml"); tracing::info!(?generated_config_path, "generating valid config"); // Generate configuration in temp dir path let child = testdir.spawn_child(args!["generate", "-o": generated_config_path.to_str().unwrap()])?; let output = child.wait_with_output()?; let output = output.assert_success()?; assert_with_context!( generated_config_path.exists(), &output, "generated config file not found" ); tracing::info!(?generated_config_path, "testing valid config parsing"); // Run command using temp dir and kill it after a few seconds let mut child = testdir.spawn_child(args![command])?; std::thread::sleep(LAUNCH_DELAY); child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; output.stdout_line_contains(expect_stdout_line_contains)?; // [Note on port conflict](#Note on port conflict) output.assert_was_killed().wrap_err("Possible port or cache conflict. Are there other acceptance test, zebrad, or zcashd processes running?")?; assert_with_context!( testdir.path().exists(), &output, "test temp directory not found" ); assert_with_context!( generated_config_path.exists(), &output, "generated config file not found" ); Ok(()) } /// Check if the config produced by current zebrad is stored. #[tracing::instrument] #[allow(clippy::print_stdout)] fn last_config_is_stored() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?; // Add a config file name to tempdir path let generated_config_path = testdir.path().join("zebrad.toml"); tracing::info!(?generated_config_path, "generated current config"); // Generate configuration in temp dir path let child = testdir.spawn_child(args!["generate", "-o": generated_config_path.to_str().unwrap()])?; let output = child.wait_with_output()?; let output = output.assert_success()?; assert_with_context!( generated_config_path.exists(), &output, "generated config file not found" ); tracing::info!( ?generated_config_path, "testing current config is in stored configs" ); // Get the contents of the generated config file let generated_content = fs::read_to_string(generated_config_path).expect("Should have been able to read the file"); // We need to replace the cache dir path as stored configs has a dummy `cache_dir` string there. let processed_generated_content = generated_content .replace( zebra_state::Config::default() .cache_dir .to_str() .expect("a valid cache dir"), "cache_dir", ) .trim() .to_string(); // Loop all the stored configs // // TODO: use the same filename list code in last_config_is_stored() and stored_configs_work() for config_file in configs_dir() .read_dir() .expect("read_dir call failed") .flatten() { let config_file_path = config_file.path(); let config_file_name = config_file_path .file_name() .expect("config files must have a file name") .to_string_lossy(); if config_file_name.as_ref().starts_with('.') || config_file_name.as_ref().starts_with('#') { // Skip editor files and other invalid config paths tracing::info!( ?config_file_path, "skipping hidden/temporary config file path" ); continue; } // Read stored config let stored_content = fs::read_to_string(config_file_full_path(config_file_path)) .expect("Should have been able to read the file") .trim() .to_string(); // If any stored config is equal to the generated then we are good. if stored_content.eq(&processed_generated_content) { return Ok(()); } } println!( "\n\ Here is the missing config file: \n\ \n\ {processed_generated_content}\n" ); Err(eyre!( "latest zebrad config is not being tested for compatibility. \n\ \n\ Take the missing config file logged above, \n\ and commit it to Zebra's git repository as:\n\ zebrad/tests/common/configs/.toml \n\ \n\ Or run: \n\ cargo build --bin zebrad && \n\ zebrad generate | \n\ sed 's/cache_dir = \".*\"/cache_dir = \"cache_dir\"/' > \n\ zebrad/tests/common/configs/.toml", )) } /// Checks that Zebra prints an informative message when it cannot parse the /// config file. #[tracing::instrument] fn invalid_generated_config() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = &testdir()?; // Add a config file name to tempdir path. let config_path = testdir.path().join("zebrad.toml"); tracing::info!( ?config_path, "testing invalid config parsing: generating valid config" ); // Generate a valid config file in the temp dir. let child = testdir.spawn_child(args!["generate", "-o": config_path.to_str().unwrap()])?; let output = child.wait_with_output()?; let output = output.assert_success()?; assert_with_context!( config_path.exists(), &output, "generated config file not found" ); // Load the valid config file that Zebra generated. let mut config_file = fs::read_to_string(config_path.to_str().unwrap()).unwrap(); // Let's now alter the config file so that it contains a deprecated format // of `mempool.eviction_memory_time`. config_file = config_file .lines() // Remove the valid `eviction_memory_time` key/value pair from the // config. .filter(|line| !line.contains("eviction_memory_time")) .map(|line| line.to_owned() + "\n") .collect(); // Append the `eviction_memory_time` key/value pair in a deprecated format. config_file += r" [mempool.eviction_memory_time] nanos = 0 secs = 3600 "; tracing::info!(?config_path, "writing invalid config"); // Write the altered config file so that Zebra can pick it up. fs::write(config_path.to_str().unwrap(), config_file.as_bytes()) .expect("Could not write the altered config file."); tracing::info!(?config_path, "testing invalid config parsing"); // Run Zebra in a temp dir so that it loads the config. let mut child = testdir.spawn_child(args!["start"])?; // Return an error if Zebra is running for more than two seconds. // // Since the config is invalid, Zebra should terminate instantly after its // start. Two seconds should be sufficient for Zebra to read the config file // and terminate. std::thread::sleep(Duration::from_secs(2)); if child.is_running() { // We're going to error anyway, so return an error that makes sense to the developer. child.kill(true)?; return Err(eyre!( "Zebra should have exited after reading the invalid config" )); } let output = child.wait_with_output()?; // Check that Zebra produced an informative message. output.stderr_contains("Zebra could not parse the provided config file. This might mean you are using a deprecated format of the file.")?; Ok(()) } /// Test all versions of `zebrad.toml` we have stored can be parsed by the latest `zebrad`. #[tracing::instrument] #[test] fn stored_configs_parsed_correctly() -> Result<()> { let old_configs_dir = configs_dir(); use abscissa_core::Application; use zebrad::application::ZebradApp; tracing::info!(?old_configs_dir, "testing older config parsing"); for config_file in old_configs_dir .read_dir() .expect("read_dir call failed") .flatten() { let config_file_path = config_file.path(); let config_file_name = config_file_path .file_name() .expect("config files must have a file name") .to_str() .expect("config file names are valid unicode"); if config_file_name.starts_with('.') || config_file_name.starts_with('#') { // Skip editor files and other invalid config paths tracing::info!( ?config_file_path, "skipping hidden/temporary config file path" ); continue; } // ignore files starting with getblocktemplate prefix // if we were not built with the getblocktemplate-rpcs feature. #[cfg(not(feature = "getblocktemplate-rpcs"))] if config_file_name.starts_with(GET_BLOCK_TEMPLATE_CONFIG_PREFIX) { tracing::info!( ?config_file_path, "skipping getblocktemplate-rpcs config file path" ); continue; } // ignore files starting with shieldedscan prefix // if we were not built with the shielded-scan feature. if config_file_name.starts_with(SHIELDED_SCAN_CONFIG_PREFIX) { tracing::info!(?config_file_path, "skipping shielded-scan config file path"); continue; } tracing::info!( ?config_file_path, "testing old config can be parsed by current zebrad" ); ZebradApp::default() .load_config(&config_file_path) .expect("config should parse"); } Ok(()) } /// Test all versions of `zebrad.toml` we have stored can be parsed by the latest `zebrad`. #[tracing::instrument] fn stored_configs_work() -> Result<()> { let old_configs_dir = configs_dir(); tracing::info!(?old_configs_dir, "testing older config parsing"); for config_file in old_configs_dir .read_dir() .expect("read_dir call failed") .flatten() { let config_file_path = config_file.path(); let config_file_name = config_file_path .file_name() .expect("config files must have a file name") .to_str() .expect("config file names are valid unicode"); if config_file_name.starts_with('.') || config_file_name.starts_with('#') { // Skip editor files and other invalid config paths tracing::info!( ?config_file_path, "skipping hidden/temporary config file path" ); continue; } // ignore files starting with getblocktemplate prefix // if we were not built with the getblocktemplate-rpcs feature. #[cfg(not(feature = "getblocktemplate-rpcs"))] if config_file_name.starts_with(GET_BLOCK_TEMPLATE_CONFIG_PREFIX) { tracing::info!( ?config_file_path, "skipping getblocktemplate-rpcs config file path" ); continue; } let run_dir = testdir()?; let stored_config_path = config_file_full_path(config_file.path()); tracing::info!( ?stored_config_path, "testing old config can be parsed by current zebrad" ); // run zebra with stored config let mut child = run_dir.spawn_child(args!["-c", stored_config_path.to_str().unwrap(), "start"])?; let success_regexes = [ // When logs are sent to the terminal, we see the config loading message and path. format!( "loaded zebrad config.*config_path.*=.*{}", regex::escape(config_file_name) ), // If they are sent to a file, we see a log file message on stdout, // and a logo, welcome message, and progress bar on stderr. "Sending logs to".to_string(), // TODO: add expect_stdout_or_stderr_line_matches() and check for this instead: //"Thank you for running a mainnet zebrad".to_string(), ]; tracing::info!( ?stored_config_path, ?success_regexes, "waiting for zebrad to parse config and start logging" ); let success_regexes = success_regexes .iter() .collect_regex_set() .expect("regexes are valid"); // Zebra was able to start with the stored config. child.expect_stdout_line_matches(success_regexes)?; // finish child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; // [Note on port conflict](#Note on port conflict) output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; } Ok(()) } /// Test if `zebrad` can sync the first checkpoint on mainnet. /// /// The first checkpoint contains a single genesis block. #[test] fn sync_one_checkpoint_mainnet() -> Result<()> { sync_until( TINY_CHECKPOINT_TEST_HEIGHT, &Mainnet, STOP_AT_HEIGHT_REGEX, TINY_CHECKPOINT_TIMEOUT, None, MempoolBehavior::ShouldNotActivate, // checkpoint sync is irrelevant here - all tested checkpoints are mandatory true, true, ) .map(|_tempdir| ()) } /// Test if `zebrad` can sync the first checkpoint on testnet. /// /// The first checkpoint contains a single genesis block. // TODO: disabled because testnet is not currently reliable // #[test] #[allow(dead_code)] fn sync_one_checkpoint_testnet() -> Result<()> { sync_until( TINY_CHECKPOINT_TEST_HEIGHT, &Network::new_default_testnet(), STOP_AT_HEIGHT_REGEX, TINY_CHECKPOINT_TIMEOUT, None, MempoolBehavior::ShouldNotActivate, // checkpoint sync is irrelevant here - all tested checkpoints are mandatory true, true, ) .map(|_tempdir| ()) } /// Test if `zebrad` can sync the first checkpoint, restart, and stop on load. #[test] fn restart_stop_at_height() -> Result<()> { let _init_guard = zebra_test::init(); restart_stop_at_height_for_network(Network::Mainnet, TINY_CHECKPOINT_TEST_HEIGHT)?; // TODO: disabled because testnet is not currently reliable // restart_stop_at_height_for_network(Network::Testnet, TINY_CHECKPOINT_TEST_HEIGHT)?; Ok(()) } fn restart_stop_at_height_for_network(network: Network, height: block::Height) -> Result<()> { let reuse_tempdir = sync_until( height, &network, STOP_AT_HEIGHT_REGEX, TINY_CHECKPOINT_TIMEOUT, None, MempoolBehavior::ShouldNotActivate, // checkpoint sync is irrelevant here - all tested checkpoints are mandatory true, true, )?; // if stopping corrupts the rocksdb database, zebrad might hang or crash here // if stopping does not write the rocksdb database to disk, Zebra will // sync, rather than stopping immediately at the configured height sync_until( height, &network, "state is already at the configured height", STOP_ON_LOAD_TIMEOUT, reuse_tempdir, MempoolBehavior::ShouldNotActivate, // checkpoint sync is irrelevant here - all tested checkpoints are mandatory true, false, )?; Ok(()) } /// Test if `zebrad` can activate the mempool on mainnet. /// Debug activation happens after committing the genesis block. #[test] fn activate_mempool_mainnet() -> Result<()> { sync_until( block::Height(TINY_CHECKPOINT_TEST_HEIGHT.0 + 1), &Mainnet, STOP_AT_HEIGHT_REGEX, TINY_CHECKPOINT_TIMEOUT, None, MempoolBehavior::ForceActivationAt(TINY_CHECKPOINT_TEST_HEIGHT), // checkpoint sync is irrelevant here - all tested checkpoints are mandatory true, true, ) .map(|_tempdir| ()) } /// Test if `zebrad` can sync some larger checkpoints on mainnet. /// /// This test might fail or timeout on slow or unreliable networks, /// so we don't run it by default. It also takes a lot longer than /// our 10 second target time for default tests. #[test] #[ignore] fn sync_large_checkpoints_mainnet() -> Result<()> { let reuse_tempdir = sync_until( LARGE_CHECKPOINT_TEST_HEIGHT, &Mainnet, STOP_AT_HEIGHT_REGEX, LARGE_CHECKPOINT_TIMEOUT, None, MempoolBehavior::ShouldNotActivate, // checkpoint sync is irrelevant here - all tested checkpoints are mandatory true, true, )?; // if this sync fails, see the failure notes in `restart_stop_at_height` sync_until( (LARGE_CHECKPOINT_TEST_HEIGHT - 1).unwrap(), &Mainnet, "previous state height is greater than the stop height", STOP_ON_LOAD_TIMEOUT, reuse_tempdir, MempoolBehavior::ShouldNotActivate, // checkpoint sync is irrelevant here - all tested checkpoints are mandatory true, false, )?; Ok(()) } // TODO: We had `sync_large_checkpoints_testnet` and `sync_large_checkpoints_mempool_testnet`, // but they were removed because the testnet is unreliable (#1222). // We should re-add them after we have more testnet instances (#1791). /// Test if `zebrad` can run side by side with the mempool. /// This is done by running the mempool and syncing some checkpoints. #[test] #[ignore] fn sync_large_checkpoints_mempool_mainnet() -> Result<()> { sync_until( MEDIUM_CHECKPOINT_TEST_HEIGHT, &Mainnet, STOP_AT_HEIGHT_REGEX, LARGE_CHECKPOINT_TIMEOUT, None, MempoolBehavior::ForceActivationAt(TINY_CHECKPOINT_TEST_HEIGHT), // checkpoint sync is irrelevant here - all tested checkpoints are mandatory true, true, ) .map(|_tempdir| ()) } #[tracing::instrument] fn create_cached_database(network: Network) -> Result<()> { let height = network.mandatory_checkpoint_height(); let checkpoint_stop_regex = format!("{STOP_AT_HEIGHT_REGEX}.*commit checkpoint-verified request"); create_cached_database_height( &network, height, // Use checkpoints to increase sync performance while caching the database true, // Check that we're still using checkpoints when we finish the cached sync &checkpoint_stop_regex, ) } #[tracing::instrument] fn sync_past_mandatory_checkpoint(network: Network) -> Result<()> { let height = network.mandatory_checkpoint_height() + (32_257 + 1200); let full_validation_stop_regex = format!("{STOP_AT_HEIGHT_REGEX}.*commit contextually-verified request"); create_cached_database_height( &network, height.unwrap(), // Test full validation by turning checkpoints off false, // Check that we're doing full validation when we finish the cached sync &full_validation_stop_regex, ) } /// Sync `network` until the chain tip is reached, or a timeout elapses. /// /// The timeout is specified using an environment variable, with the name configured by the /// `timeout_argument_name` parameter. The value of the environment variable must the number of /// minutes specified as an integer. #[allow(clippy::print_stderr)] #[tracing::instrument] fn full_sync_test(network: Network, timeout_argument_name: &str) -> Result<()> { let timeout_argument: Option = env::var(timeout_argument_name) .ok() .and_then(|timeout_string| timeout_string.parse().ok()); // # TODO // // Replace hard-coded values in create_cached_database_height with: // - the timeout in the environmental variable // - the path from ZEBRA_CACHED_STATE_DIR if let Some(_timeout_minutes) = timeout_argument { create_cached_database_height( &network, // Just keep going until we reach the chain tip block::Height::MAX, // Use the checkpoints to sync quickly, then do full validation until the chain tip true, // Finish when we reach the chain tip SYNC_FINISHED_REGEX, ) } else { eprintln!( "Skipped full sync test for {network}, \ set the {timeout_argument_name:?} environmental variable to run the test", ); Ok(()) } } // These tests are ignored because they're too long running to run during our // traditional CI, and they depend on persistent state that cannot be made // available in github actions or google cloud build. Instead we run these tests // directly in a vm we spin up on google compute engine, where we can mount // drives populated by the sync_to_mandatory_checkpoint tests, snapshot those drives, // and then use them to more quickly run the sync_past_mandatory_checkpoint tests. /// Sync up to the mandatory checkpoint height on mainnet and stop. #[allow(dead_code)] #[cfg_attr(feature = "test_sync_to_mandatory_checkpoint_mainnet", test)] fn sync_to_mandatory_checkpoint_mainnet() -> Result<()> { let _init_guard = zebra_test::init(); let network = Mainnet; create_cached_database(network) } /// Sync to the mandatory checkpoint height testnet and stop. #[allow(dead_code)] #[cfg_attr(feature = "test_sync_to_mandatory_checkpoint_testnet", test)] fn sync_to_mandatory_checkpoint_testnet() -> Result<()> { let _init_guard = zebra_test::init(); let network = Network::new_default_testnet(); create_cached_database(network) } /// Test syncing 1200 blocks (3 checkpoints) past the mandatory checkpoint on mainnet. /// /// This assumes that the config'd state is already synced at or near the mandatory checkpoint /// activation on mainnet. If the state has already synced past the mandatory checkpoint /// activation by 1200 blocks, it will fail. #[allow(dead_code)] #[cfg_attr(feature = "test_sync_past_mandatory_checkpoint_mainnet", test)] fn sync_past_mandatory_checkpoint_mainnet() -> Result<()> { let _init_guard = zebra_test::init(); let network = Mainnet; sync_past_mandatory_checkpoint(network) } /// Test syncing 1200 blocks (3 checkpoints) past the mandatory checkpoint on testnet. /// /// This assumes that the config'd state is already synced at or near the mandatory checkpoint /// activation on testnet. If the state has already synced past the mandatory checkpoint /// activation by 1200 blocks, it will fail. #[allow(dead_code)] #[cfg_attr(feature = "test_sync_past_mandatory_checkpoint_testnet", test)] fn sync_past_mandatory_checkpoint_testnet() -> Result<()> { let _init_guard = zebra_test::init(); let network = Network::new_default_testnet(); sync_past_mandatory_checkpoint(network) } /// Test if `zebrad` can fully sync the chain on mainnet. /// /// This test takes a long time to run, so we don't run it by default. This test is only executed /// if there is an environment variable named `FULL_SYNC_MAINNET_TIMEOUT_MINUTES` set with the number /// of minutes to wait for synchronization to complete before considering that the test failed. #[test] #[ignore] fn full_sync_mainnet() -> Result<()> { // TODO: add "ZEBRA" at the start of this env var, to avoid clashes full_sync_test(Mainnet, "FULL_SYNC_MAINNET_TIMEOUT_MINUTES") } /// Test if `zebrad` can fully sync the chain on testnet. /// /// This test takes a long time to run, so we don't run it by default. This test is only executed /// if there is an environment variable named `FULL_SYNC_TESTNET_TIMEOUT_MINUTES` set with the number /// of minutes to wait for synchronization to complete before considering that the test failed. #[test] #[ignore] fn full_sync_testnet() -> Result<()> { // TODO: add "ZEBRA" at the start of this env var, to avoid clashes full_sync_test( Network::new_default_testnet(), "FULL_SYNC_TESTNET_TIMEOUT_MINUTES", ) } #[cfg(all(feature = "prometheus", not(target_os = "windows")))] #[tokio::test] async fn metrics_endpoint() -> Result<()> { use bytes::Bytes; use http_body_util::BodyExt; use http_body_util::Full; use hyper_util::{client::legacy::Client, rt::TokioExecutor}; use std::io::Write; let _init_guard = zebra_test::init(); // [Note on port conflict](#Note on port conflict) let port = random_known_port(); let endpoint = format!("127.0.0.1:{port}"); let url = format!("http://{endpoint}"); // Write a configuration that has metrics endpoint_addr set let mut config = default_test_config(&Mainnet)?; config.metrics.endpoint_addr = Some(endpoint.parse().unwrap()); let dir = testdir()?.with_config(&mut config)?; let child = dir.spawn_child(args!["start"])?; // Run `zebrad` for a few seconds before testing the endpoint // Since we're an async function, we have to use a sleep future, not thread sleep. tokio::time::sleep(LAUNCH_DELAY).await; // Create an http client let client: Client<_, Full> = Client::builder(TokioExecutor::new()).build_http(); // Test metrics endpoint let res = client.get(url.try_into().expect("url is valid")).await; let (res, child) = child.kill_on_error(res)?; assert!(res.status().is_success()); // Get the body of the response let mut body = Vec::new(); let mut body_stream = res.into_body(); while let Some(next) = body_stream.frame().await { body.write_all(next?.data_ref().unwrap())?; } let (body, mut child) = child.kill_on_error::, hyper::Error>(Ok(body))?; child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; output.any_output_line_contains( "# TYPE zebrad_build_info counter", &body, "metrics exporter response", "the metrics response header", )?; std::str::from_utf8(&body).expect("unexpected invalid UTF-8 in metrics exporter response"); // Make sure metrics was started output.stdout_line_contains(format!("Opened metrics endpoint at {endpoint}").as_str())?; // [Note on port conflict](#Note on port conflict) output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; Ok(()) } #[cfg(all(feature = "filter-reload", not(target_os = "windows")))] #[tokio::test] async fn tracing_endpoint() -> Result<()> { use bytes::Bytes; use http_body_util::BodyExt; use http_body_util::Full; use hyper_util::{client::legacy::Client, rt::TokioExecutor}; use std::io::Write; let _init_guard = zebra_test::init(); // [Note on port conflict](#Note on port conflict) let port = random_known_port(); let endpoint = format!("127.0.0.1:{port}"); let url_default = format!("http://{endpoint}"); let url_filter = format!("{url_default}/filter"); // Write a configuration that has tracing endpoint_addr option set let mut config = default_test_config(&Mainnet)?; config.tracing.endpoint_addr = Some(endpoint.parse().unwrap()); let dir = testdir()?.with_config(&mut config)?; let child = dir.spawn_child(args!["start"])?; // Run `zebrad` for a few seconds before testing the endpoint // Since we're an async function, we have to use a sleep future, not thread sleep. tokio::time::sleep(LAUNCH_DELAY).await; // Create an http client let client: Client<_, Full> = Client::builder(TokioExecutor::new()).build_http(); // Test tracing endpoint let res = client .get(url_default.try_into().expect("url_default is valid")) .await; let (res, child) = child.kill_on_error(res)?; assert!(res.status().is_success()); // Get the body of the response let mut body = Vec::new(); let mut body_stream = res.into_body(); while let Some(next) = body_stream.frame().await { body.write_all(next?.data_ref().unwrap())?; } let (body, child) = child.kill_on_error::, hyper::Error>(Ok(body))?; // Set a filter and make sure it was changed let request = hyper::Request::post(url_filter.clone()) .body("zebrad=debug".to_string().into()) .unwrap(); let post = client.request(request).await; let (_post, child) = child.kill_on_error(post)?; let tracing_res = client .get(url_filter.try_into().expect("url_filter is valid")) .await; let (tracing_res, child) = child.kill_on_error(tracing_res)?; assert!(tracing_res.status().is_success()); // Get the body of the response let mut tracing_body = Vec::new(); let mut body_stream = tracing_res.into_body(); while let Some(next) = body_stream.frame().await { tracing_body.write_all(next?.data_ref().unwrap())?; } let (tracing_body, mut child) = child.kill_on_error::, hyper::Error>(Ok(tracing_body.clone()))?; child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; // Make sure tracing endpoint was started output.stdout_line_contains(format!("Opened tracing endpoint at {endpoint}").as_str())?; // TODO: Match some trace level messages from output // Make sure the endpoint header is correct // The header is split over two lines. But we don't want to require line // breaks at a specific word, so we run two checks for different substrings. output.any_output_line_contains( "HTTP endpoint allows dynamic control of the filter", &body, "tracing filter endpoint response", "the tracing response header", )?; output.any_output_line_contains( "tracing events", &body, "tracing filter endpoint response", "the tracing response header", )?; std::str::from_utf8(&tracing_body) .expect("unexpected invalid UTF-8 in tracing filter response"); // Make sure endpoint requests change the filter output.any_output_line_contains( "zebrad=debug", &tracing_body, "tracing filter endpoint response", "the modified tracing filter", )?; std::str::from_utf8(&tracing_body) .expect("unexpected invalid UTF-8 in modified tracing filter response"); // [Note on port conflict](#Note on port conflict) output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; Ok(()) } /// Test that the JSON-RPC endpoint responds to a request, /// when configured with a single thread. #[tokio::test] async fn rpc_endpoint_single_thread() -> Result<()> { rpc_endpoint(false).await } /// Test that the JSON-RPC endpoint responds to a request, /// when configured with multiple threads. #[tokio::test] async fn rpc_endpoint_parallel_threads() -> Result<()> { rpc_endpoint(true).await } /// Test that the JSON-RPC endpoint responds to a request. /// /// Set `parallel_cpu_threads` to true to auto-configure based on the number of CPU cores. #[tracing::instrument] async fn rpc_endpoint(parallel_cpu_threads: bool) -> Result<()> { let _init_guard = zebra_test::init(); if zebra_test::net::zebra_skip_network_tests() { return Ok(()); } // Write a configuration that has RPC listen_addr set // [Note on port conflict](#Note on port conflict) let mut config = os_assigned_rpc_port_config(parallel_cpu_threads, &Mainnet)?; let dir = testdir()?.with_config(&mut config)?; let mut child = dir.spawn_child(args!["start"])?; // Wait until port is open. let rpc_address = read_listen_addr_from_logs(&mut child, OPENED_RPC_ENDPOINT_MSG)?; // Create an http client let client = RpcRequestClient::new(rpc_address); // Make the call to the `getinfo` RPC method let res = client.call("getinfo", "[]".to_string()).await?; // Test rpc endpoint response assert!(res.status().is_success()); let body = res.bytes().await; let (body, mut child) = child.kill_on_error(body)?; let parsed: Value = serde_json::from_slice(&body)?; // Check that we have at least 4 characters in the `build` field. let build = parsed["result"]["build"].as_str().unwrap(); assert!(build.len() > 4, "Got {build}"); // Check that the `subversion` field has "Zebra" in it. let subversion = parsed["result"]["subversion"].as_str().unwrap(); assert!(subversion.contains("Zebra"), "Got {subversion}"); child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; // [Note on port conflict](#Note on port conflict) output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; Ok(()) } /// Test that the JSON-RPC endpoint responds to requests with different content types. /// /// This test ensures that the curl examples of zcashd rpc methods will also work in Zebra. /// /// https://zcash.github.io/rpc/getblockchaininfo.html #[tokio::test] async fn rpc_endpoint_client_content_type() -> Result<()> { let _init_guard = zebra_test::init(); if zebra_test::net::zebra_skip_network_tests() { return Ok(()); } // Write a configuration that has RPC listen_addr set // [Note on port conflict](#Note on port conflict) let mut config = random_known_rpc_port_config(true, &Mainnet)?; let dir = testdir()?.with_config(&mut config)?; let mut child = dir.spawn_child(args!["start"])?; // Wait until port is open. let rpc_address = read_listen_addr_from_logs(&mut child, OPENED_RPC_ENDPOINT_MSG)?; // Create an http client let client = RpcRequestClient::new(rpc_address); // Call to `getinfo` RPC method with a no content type. let res = client .call_with_no_content_type("getinfo", "[]".to_string()) .await?; // Zebra will insert valid `application/json` content type and succeed. assert!(res.status().is_success()); // Call to `getinfo` RPC method with a `text/plain`. let res = client .call_with_content_type("getinfo", "[]".to_string(), "text/plain".to_string()) .await?; // Zebra will replace to the valid `application/json` content type and succeed. assert!(res.status().is_success()); // Call to `getinfo` RPC method with a `text/plain` content type as the zcashd rpc docs. let res = client .call_with_content_type("getinfo", "[]".to_string(), "text/plain;".to_string()) .await?; // Zebra will replace to the valid `application/json` content type and succeed. assert!(res.status().is_success()); // Call to `getinfo` RPC method with a `text/plain; other string` content type. let res = client .call_with_content_type( "getinfo", "[]".to_string(), "text/plain; other string".to_string(), ) .await?; // Zebra will replace to the valid `application/json` content type and succeed. assert!(res.status().is_success()); // Call to `getinfo` RPC method with a valid `application/json` content type. let res = client .call_with_content_type("getinfo", "[]".to_string(), "application/json".to_string()) .await?; // Zebra will not replace valid content type and succeed. assert!(res.status().is_success()); // Call to `getinfo` RPC method with invalid string as content type. let res = client .call_with_content_type("getinfo", "[]".to_string(), "whatever".to_string()) .await?; // Zebra will not replace unrecognized content type and fail. assert!(res.status().is_client_error()); Ok(()) } /// Test that Zebra's non-blocking logger works, by creating lots of debug output, but not reading the logs. /// Then make sure Zebra drops excess log lines. (Previously, it would block waiting for logs to be read.) /// /// This test is unreliable and sometimes hangs on macOS. #[test] #[cfg(not(target_os = "macos"))] fn non_blocking_logger() -> Result<()> { use futures::FutureExt; use std::{sync::mpsc, time::Duration}; let rt = tokio::runtime::Runtime::new().unwrap(); let (done_tx, done_rx) = mpsc::channel(); let test_task_handle: tokio::task::JoinHandle> = rt.spawn(async move { let _init_guard = zebra_test::init(); let mut config = os_assigned_rpc_port_config(false, &Mainnet)?; config.tracing.filter = Some("trace".to_string()); config.tracing.buffer_limit = 100; let dir = testdir()?.with_config(&mut config)?; let mut child = dir .spawn_child(args!["start"])? .with_timeout(TINY_CHECKPOINT_TIMEOUT); // Wait until port is open. let rpc_address = read_listen_addr_from_logs(&mut child, OPENED_RPC_ENDPOINT_MSG)?; // Create an http client let client = RpcRequestClient::new(rpc_address); // Most of Zebra's lines are 100-200 characters long, so 500 requests should print enough to fill the unix pipe, // fill the channel that tracing logs are queued onto, and drop logs rather than block execution. for _ in 0..500 { let res = client.call("getinfo", "[]".to_string()).await?; // Test that zebrad rpc endpoint is still responding to requests assert!(res.status().is_success()); } child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; // [Note on port conflict](#Note on port conflict) output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; done_tx.send(())?; Ok(()) }); // Wait until the spawned task finishes up to 45 seconds before shutting down tokio runtime if done_rx.recv_timeout(Duration::from_secs(90)).is_ok() { rt.shutdown_timeout(Duration::from_secs(3)); } match test_task_handle.now_or_never() { Some(Ok(result)) => result, Some(Err(error)) => Err(eyre!("join error: {:?}", error)), None => Err(eyre!("unexpected test task hang")), } } /// Make sure `lightwalletd` works with Zebra, when both their states are empty. /// /// This test only runs when the `ZEBRA_TEST_LIGHTWALLETD` env var is set. /// /// This test doesn't work on Windows, so it is always skipped on that platform. #[test] #[cfg(not(target_os = "windows"))] fn lightwalletd_integration() -> Result<()> { lightwalletd_integration_test(LaunchWithEmptyState { launches_lightwalletd: true, }) } /// Make sure `zebrad` can sync from peers, but don't actually launch `lightwalletd`. /// /// This test only runs when the `ZEBRA_CACHED_STATE_DIR` env var is set. /// /// This test might work on Windows. #[test] fn zebrad_update_sync() -> Result<()> { lightwalletd_integration_test(UpdateZebraCachedStateNoRpc) } /// Make sure `lightwalletd` can sync from Zebra, in update sync mode. /// /// This test only runs when: /// - the `ZEBRA_TEST_LIGHTWALLETD`, `ZEBRA_CACHED_STATE_DIR`, and /// `LIGHTWALLETD_DATA_DIR` env vars are set, and /// - Zebra is compiled with `--features=lightwalletd-grpc-tests`. /// /// This test doesn't work on Windows, so it is always skipped on that platform. #[test] #[cfg(not(target_os = "windows"))] #[cfg(feature = "lightwalletd-grpc-tests")] fn lightwalletd_update_sync() -> Result<()> { lightwalletd_integration_test(UpdateCachedState) } /// Make sure `lightwalletd` can fully sync from genesis using Zebra. /// /// This test only runs when: /// - the `ZEBRA_TEST_LIGHTWALLETD` and `ZEBRA_CACHED_STATE_DIR` env vars are set, and /// - Zebra is compiled with `--features=lightwalletd-grpc-tests`. /// /// /// This test doesn't work on Windows, so it is always skipped on that platform. #[test] #[ignore] #[cfg(not(target_os = "windows"))] #[cfg(feature = "lightwalletd-grpc-tests")] fn lightwalletd_full_sync() -> Result<()> { lightwalletd_integration_test(FullSyncFromGenesis { allow_lightwalletd_cached_state: false, }) } /// Make sure `lightwalletd` can sync from Zebra, in all available modes. /// /// Runs the tests in this order: /// - launch lightwalletd with empty states, /// - if `ZEBRA_CACHED_STATE_DIR` is set: /// - run a full sync /// - if `ZEBRA_CACHED_STATE_DIR` and `LIGHTWALLETD_DATA_DIR` are set: /// - run a quick update sync, /// - run a send transaction gRPC test, /// - run read-only gRPC tests. /// /// The lightwalletd full, update, and gRPC tests only run with `--features=lightwalletd-grpc-tests`. /// /// These tests don't work on Windows, so they are always skipped on that platform. #[tokio::test] #[ignore] #[cfg(not(target_os = "windows"))] async fn lightwalletd_test_suite() -> Result<()> { lightwalletd_integration_test(LaunchWithEmptyState { launches_lightwalletd: true, })?; // Only runs when ZEBRA_CACHED_STATE_DIR is set. lightwalletd_integration_test(UpdateZebraCachedStateNoRpc)?; // These tests need the compile-time gRPC feature #[cfg(feature = "lightwalletd-grpc-tests")] { // Do the quick tests first // Only runs when LIGHTWALLETD_DATA_DIR and ZEBRA_CACHED_STATE_DIR are set lightwalletd_integration_test(UpdateCachedState)?; // Only runs when LIGHTWALLETD_DATA_DIR and ZEBRA_CACHED_STATE_DIR are set common::lightwalletd::wallet_grpc_test::run().await?; // Then do the slow tests // Only runs when ZEBRA_CACHED_STATE_DIR is set. // When manually running the test suite, allow cached state in the full sync test. lightwalletd_integration_test(FullSyncFromGenesis { allow_lightwalletd_cached_state: true, })?; // Only runs when LIGHTWALLETD_DATA_DIR and ZEBRA_CACHED_STATE_DIR are set common::lightwalletd::send_transaction_test::run().await?; } Ok(()) } /// Run a lightwalletd integration test with a configuration for `test_type`. /// /// Tests that sync `lightwalletd` to the chain tip require the `lightwalletd-grpc-tests` feature`: /// - [`FullSyncFromGenesis`] /// - [`UpdateCachedState`] /// /// Set `FullSyncFromGenesis { allow_lightwalletd_cached_state: true }` to speed up manual full sync tests. /// /// # Relibility /// /// The random ports in this test can cause [rare port conflicts.](#Note on port conflict) /// /// # Panics /// /// If the `test_type` requires `--features=lightwalletd-grpc-tests`, /// but Zebra was not compiled with that feature. #[tracing::instrument] fn lightwalletd_integration_test(test_type: TestType) -> Result<()> { let _init_guard = zebra_test::init(); // We run these sync tests with a network connection, for better test coverage. let use_internet_connection = true; let network = Mainnet; let test_name = "lightwalletd_integration_test"; if test_type.launches_lightwalletd() && !can_spawn_lightwalletd_for_rpc(test_name, test_type) { tracing::info!("skipping test due to missing lightwalletd network or cached state"); return Ok(()); } // Launch zebra with peers and using a predefined zebrad state path. let (mut zebrad, zebra_rpc_address) = if let Some(zebrad_and_address) = spawn_zebrad_for_rpc( network.clone(), test_name, test_type, use_internet_connection, )? { tracing::info!( ?test_type, "running lightwalletd & zebrad integration test, launching zebrad...", ); zebrad_and_address } else { // Skip the test, we don't have the required cached state return Ok(()); }; // Store the state version message so we can wait for the upgrade later if needed. let state_version_message = wait_for_state_version_message(&mut zebrad)?; if test_type.needs_zebra_cached_state() { zebrad .expect_stdout_line_matches(r"loaded Zebra state cache .*tip.*=.*Height\([0-9]{7}\)")?; } else { // Timeout the test if we're somehow accidentally using a cached state zebrad.expect_stdout_line_matches("loaded Zebra state cache .*tip.*=.*None")?; } // Wait for the state to upgrade and the RPC port, if the upgrade is short. // // If incompletely upgraded states get written to the CI cache, // change DATABASE_FORMAT_UPGRADE_IS_LONG to true. if test_type.launches_lightwalletd() && !DATABASE_FORMAT_UPGRADE_IS_LONG { tracing::info!( ?test_type, ?zebra_rpc_address, "waiting for zebrad to open its RPC port..." ); wait_for_state_version_upgrade( &mut zebrad, &state_version_message, state_database_format_version_in_code(), [format!( "Opened RPC endpoint at {}", zebra_rpc_address.expect("lightwalletd test must have RPC port") )], )?; } else { wait_for_state_version_upgrade( &mut zebrad, &state_version_message, state_database_format_version_in_code(), None, )?; } // Launch lightwalletd, if needed let lightwalletd_and_port = if test_type.launches_lightwalletd() { tracing::info!( ?zebra_rpc_address, "launching lightwalletd connected to zebrad", ); // Launch lightwalletd let (mut lightwalletd, lightwalletd_rpc_port) = spawn_lightwalletd_for_rpc( network, test_name, test_type, zebra_rpc_address.expect("lightwalletd test must have RPC port"), )? .expect("already checked for lightwalletd cached state and network"); tracing::info!( ?lightwalletd_rpc_port, "spawned lightwalletd connected to zebrad", ); // Check that `lightwalletd` is calling the expected Zebra RPCs // getblockchaininfo if test_type.needs_zebra_cached_state() { lightwalletd.expect_stdout_line_matches( "Got sapling height 419200 block height [0-9]{7} chain main branchID [0-9a-f]{8}", )?; } else { // Timeout the test if we're somehow accidentally using a cached state in our temp dir lightwalletd.expect_stdout_line_matches( "Got sapling height 419200 block height [0-9]{1,6} chain main branchID 00000000", )?; } if test_type.needs_lightwalletd_cached_state() { lightwalletd .expect_stdout_line_matches("Done reading [0-9]{7} blocks from disk cache")?; } else if !test_type.allow_lightwalletd_cached_state() { // Timeout the test if we're somehow accidentally using a cached state in our temp dir lightwalletd.expect_stdout_line_matches("Done reading 0 blocks from disk cache")?; } // getblock with the first Sapling block in Zebra's state // // zcash/lightwalletd calls getbestblockhash here, but // adityapk00/lightwalletd calls getblock // // The log also depends on what is in Zebra's state: // // # Cached Zebra State // // lightwalletd ingests blocks into its cache. // // # Empty Zebra State // // lightwalletd tries to download the Sapling activation block, but it's not in the state. // // Until the Sapling activation block has been downloaded, // lightwalletd will keep retrying getblock. if !test_type.allow_lightwalletd_cached_state() { if test_type.needs_zebra_cached_state() { lightwalletd.expect_stdout_line_matches( "([Aa]dding block to cache)|([Ww]aiting for block)", )?; } else { lightwalletd.expect_stdout_line_matches(regex::escape( "Waiting for zcashd height to reach Sapling activation height (419200)", ))?; } } Some((lightwalletd, lightwalletd_rpc_port)) } else { None }; // Wait for zebrad and lightwalletd to sync, if needed. let (mut zebrad, lightwalletd) = if test_type.needs_zebra_cached_state() { if let Some((lightwalletd, lightwalletd_rpc_port)) = lightwalletd_and_port { #[cfg(feature = "lightwalletd-grpc-tests")] { use common::lightwalletd::sync::wait_for_zebrad_and_lightwalletd_sync; tracing::info!( ?lightwalletd_rpc_port, "waiting for zebrad and lightwalletd to sync...", ); let (lightwalletd, mut zebrad) = wait_for_zebrad_and_lightwalletd_sync( lightwalletd, lightwalletd_rpc_port, zebrad, zebra_rpc_address.expect("lightwalletd test must have RPC port"), test_type, // We want to wait for the mempool and network for better coverage true, use_internet_connection, )?; // Wait for the state to upgrade, if the upgrade is long. // If this line hangs, change DATABASE_FORMAT_UPGRADE_IS_LONG to false, // or combine "wait for sync" with "wait for state version upgrade". if DATABASE_FORMAT_UPGRADE_IS_LONG { wait_for_state_version_upgrade( &mut zebrad, &state_version_message, state_database_format_version_in_code(), None, )?; } (zebrad, Some(lightwalletd)) } #[cfg(not(feature = "lightwalletd-grpc-tests"))] panic!( "the {test_type:?} test requires `cargo test --feature lightwalletd-grpc-tests`\n\ zebrad: {zebrad:?}\n\ lightwalletd: {lightwalletd:?}\n\ lightwalletd_rpc_port: {lightwalletd_rpc_port:?}" ); } else { // We're just syncing Zebra, so there's no lightwalletd to check tracing::info!(?test_type, "waiting for zebrad to sync to the tip"); zebrad.expect_stdout_line_matches(SYNC_FINISHED_REGEX)?; // Wait for the state to upgrade, if the upgrade is long. // If this line hangs, change DATABASE_FORMAT_UPGRADE_IS_LONG to false. if DATABASE_FORMAT_UPGRADE_IS_LONG { wait_for_state_version_upgrade( &mut zebrad, &state_version_message, state_database_format_version_in_code(), None, )?; } (zebrad, None) } } else { let lightwalletd = lightwalletd_and_port.map(|(lightwalletd, _port)| lightwalletd); // We don't have a cached state, so we don't do any tip checks for Zebra or lightwalletd (zebrad, lightwalletd) }; tracing::info!( ?test_type, "cleaning up child processes and checking for errors", ); // Cleanup both processes // // If the test fails here, see the [note on port conflict](#Note on port conflict) // // zcash/lightwalletd exits by itself, but // adityapk00/lightwalletd keeps on going, so it gets killed by the test harness. zebrad.kill(false)?; if let Some(mut lightwalletd) = lightwalletd { lightwalletd.kill(false)?; let lightwalletd_output = lightwalletd.wait_with_output()?.assert_failure()?; lightwalletd_output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; } let zebrad_output = zebrad.wait_with_output()?.assert_failure()?; zebrad_output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; Ok(()) } /// Test will start 2 zebrad nodes one after the other using the same Zcash listener. /// It is expected that the first node spawned will get exclusive use of the port. /// The second node will panic with the Zcash listener conflict hint added in #1535. #[test] #[cfg(not(target_os = "windows"))] fn zebra_zcash_listener_conflict() -> Result<()> { let _init_guard = zebra_test::init(); // [Note on port conflict](#Note on port conflict) let port = random_known_port(); let listen_addr = format!("127.0.0.1:{port}"); // Write a configuration that has our created network listen_addr let mut config = default_test_config(&Mainnet)?; config.network.listen_addr = listen_addr.parse().unwrap(); let dir1 = testdir()?.with_config(&mut config)?; let regex1 = regex::escape(&format!("Opened Zcash protocol endpoint at {listen_addr}")); // From another folder create a configuration with the same listener. // `network.listen_addr` will be the same in the 2 nodes. // (But since the config is ephemeral, they will have different state paths.) let dir2 = testdir()?.with_config(&mut config)?; check_config_conflict(dir1, regex1.as_str(), dir2, PORT_IN_USE_ERROR.as_str())?; Ok(()) } /// Start 2 zebrad nodes using the same metrics listener port, but different /// state directories and Zcash listener ports. The first node should get /// exclusive use of the port. The second node will panic with the Zcash metrics /// conflict hint added in #1535. #[test] #[cfg(all(feature = "prometheus", not(target_os = "windows")))] fn zebra_metrics_conflict() -> Result<()> { let _init_guard = zebra_test::init(); // [Note on port conflict](#Note on port conflict) let port = random_known_port(); let listen_addr = format!("127.0.0.1:{port}"); // Write a configuration that has our created metrics endpoint_addr let mut config = default_test_config(&Mainnet)?; config.metrics.endpoint_addr = Some(listen_addr.parse().unwrap()); let dir1 = testdir()?.with_config(&mut config)?; let regex1 = regex::escape(&format!(r"Opened metrics endpoint at {listen_addr}")); // From another folder create a configuration with the same endpoint. // `metrics.endpoint_addr` will be the same in the 2 nodes. // But they will have different Zcash listeners (auto port) and states (ephemeral) let dir2 = testdir()?.with_config(&mut config)?; check_config_conflict(dir1, regex1.as_str(), dir2, PORT_IN_USE_ERROR.as_str())?; Ok(()) } /// Start 2 zebrad nodes using the same tracing listener port, but different /// state directories and Zcash listener ports. The first node should get /// exclusive use of the port. The second node will panic with the Zcash tracing /// conflict hint added in #1535. #[test] #[cfg(all(feature = "filter-reload", not(target_os = "windows")))] fn zebra_tracing_conflict() -> Result<()> { let _init_guard = zebra_test::init(); // [Note on port conflict](#Note on port conflict) let port = random_known_port(); let listen_addr = format!("127.0.0.1:{port}"); // Write a configuration that has our created tracing endpoint_addr let mut config = default_test_config(&Mainnet)?; config.tracing.endpoint_addr = Some(listen_addr.parse().unwrap()); let dir1 = testdir()?.with_config(&mut config)?; let regex1 = regex::escape(&format!(r"Opened tracing endpoint at {listen_addr}")); // From another folder create a configuration with the same endpoint. // `tracing.endpoint_addr` will be the same in the 2 nodes. // But they will have different Zcash listeners (auto port) and states (ephemeral) let dir2 = testdir()?.with_config(&mut config)?; check_config_conflict(dir1, regex1.as_str(), dir2, PORT_IN_USE_ERROR.as_str())?; Ok(()) } /// Start 2 zebrad nodes using the same RPC listener port, but different /// state directories and Zcash listener ports. The first node should get /// exclusive use of the port. The second node will panic. /// /// This test is sometimes unreliable on Windows, and hangs on macOS. /// We believe this is a CI infrastructure issue, not a platform-specific issue. #[test] #[cfg(not(any(target_os = "windows", target_os = "macos")))] fn zebra_rpc_conflict() -> Result<()> { let _init_guard = zebra_test::init(); if zebra_test::net::zebra_skip_network_tests() { return Ok(()); } // Write a configuration that has RPC listen_addr set // [Note on port conflict](#Note on port conflict) // // This is the required setting to detect port conflicts. let mut config = random_known_rpc_port_config(false, &Mainnet)?; let dir1 = testdir()?.with_config(&mut config)?; let regex1 = regex::escape(&format!( r"Opened RPC endpoint at {}", config.rpc.listen_addr.unwrap(), )); // From another folder create a configuration with the same endpoint. // `rpc.listen_addr` will be the same in the 2 nodes. // But they will have different Zcash listeners (auto port) and states (ephemeral) let dir2 = testdir()?.with_config(&mut config)?; check_config_conflict(dir1, regex1.as_str(), dir2, "Unable to start RPC server")?; Ok(()) } /// Start 2 zebrad nodes using the same state directory, but different Zcash /// listener ports. The first node should get exclusive access to the database. /// The second node will panic with the Zcash state conflict hint added in #1535. #[test] fn zebra_state_conflict() -> Result<()> { let _init_guard = zebra_test::init(); // A persistent config has a fixed temp state directory, but asks the OS to // automatically choose an unused port let mut config = persistent_test_config(&Mainnet)?; let dir_conflict = testdir()?.with_config(&mut config)?; // Windows problems with this match will be worked on at #1654 // We are matching the whole opened path only for unix by now. let contains = if cfg!(unix) { let mut dir_conflict_full = PathBuf::new(); dir_conflict_full.push(dir_conflict.path()); dir_conflict_full.push("state"); dir_conflict_full.push(format!( "v{}", zebra_state::state_database_format_version_in_code().major, )); dir_conflict_full.push(config.network.network.to_string().to_lowercase()); format!( "Opened Zebra state cache at {}", dir_conflict_full.display() ) } else { String::from("Opened Zebra state cache at ") }; check_config_conflict( dir_conflict.path(), regex::escape(&contains).as_str(), dir_conflict.path(), LOCK_FILE_ERROR.as_str(), )?; Ok(()) } /// Launch a node in `first_dir`, wait a few seconds, then launch a node in /// `second_dir`. Check that the first node's stdout contains /// `first_stdout_regex`, and the second node's stderr contains /// `second_stderr_regex`. #[tracing::instrument] fn check_config_conflict( first_dir: T, first_stdout_regex: &str, second_dir: U, second_stderr_regex: &str, ) -> Result<()> where T: ZebradTestDirExt + std::fmt::Debug, U: ZebradTestDirExt + std::fmt::Debug, { // Start the first node let mut node1 = first_dir.spawn_child(args!["start"])?; // Wait until node1 has used the conflicting resource. node1.expect_stdout_line_matches(first_stdout_regex)?; // Wait a bit before launching the second node. std::thread::sleep(BETWEEN_NODES_DELAY); // Spawn the second node let node2 = second_dir.spawn_child(args!["start"]); let (node2, mut node1) = node1.kill_on_error(node2)?; // Wait a few seconds and kill first node. // Second node is terminated by panic, no need to kill. std::thread::sleep(LAUNCH_DELAY); let node1_kill_res = node1.kill(false); let (_, mut node2) = node2.kill_on_error(node1_kill_res)?; // node2 should have panicked due to a conflict. Kill it here anyway, so it // doesn't outlive the test on error. // // This code doesn't work on Windows or macOS. It's cleanup code that only // runs when node2 doesn't panic as expected. So it's ok to skip it. // See #1781. #[cfg(target_os = "linux")] if node2.is_running() { return node2 .kill_on_error::<(), _>(Err(eyre!( "conflicted node2 was still running, but the test expected a panic" ))) .context_from(&mut node1) .map(|_| ()); } // Now we're sure both nodes are dead, and we have both their outputs let output1 = node1.wait_with_output().context_from(&mut node2)?; let output2 = node2.wait_with_output().context_from(&output1)?; // Make sure the first node was killed, rather than exiting with an error. output1 .assert_was_killed() .warning("Possible port conflict. Are there other acceptance tests running?") .context_from(&output2)?; // Make sure node2 has the expected resource conflict. output2 .stderr_line_matches(second_stderr_regex) .context_from(&output1)?; output2 .assert_was_not_killed() .warning("Possible port conflict. Are there other acceptance tests running?") .context_from(&output1)?; Ok(()) } #[tokio::test] #[ignore] async fn fully_synced_rpc_test() -> Result<()> { let _init_guard = zebra_test::init(); // We're only using cached Zebra state here, so this test type is the most similar let test_type = TestType::UpdateCachedState; let network = Network::Mainnet; let (mut zebrad, zebra_rpc_address) = if let Some(zebrad_and_address) = spawn_zebrad_for_rpc(network, "fully_synced_rpc_test", test_type, false)? { tracing::info!("running fully synced zebrad RPC test"); zebrad_and_address } else { // Skip the test, we don't have the required cached state return Ok(()); }; let zebra_rpc_address = zebra_rpc_address.expect("lightwalletd test must have RPC port"); zebrad.expect_stdout_line_matches(format!("Opened RPC endpoint at {zebra_rpc_address}"))?; let client = RpcRequestClient::new(zebra_rpc_address); // Make a getblock test that works only on synced node (high block number). // The block is before the mandatory checkpoint, so the checkpoint cached state can be used // if desired. let res = client .text_from_call("getblock", r#"["1180900", 0]"#.to_string()) .await?; // Simple textual check to avoid fully parsing the response, for simplicity let expected_bytes = zebra_test::vectors::MAINNET_BLOCKS .get(&1_180_900) .expect("test block must exist"); let expected_hex = hex::encode(expected_bytes); assert!( res.contains(&expected_hex), "response did not contain the desired block: {res}" ); Ok(()) } #[test] fn delete_old_databases() -> Result<()> { use std::fs::{canonicalize, create_dir}; let _init_guard = zebra_test::init(); // Skip this test because it can be very slow without a network. // // The delete databases task is launched last during startup, after network setup. // If there is no network, network setup can take a long time to timeout, // so the task takes a long time to launch, slowing down this test. if zebra_test::net::zebra_skip_network_tests() { return Ok(()); } let mut config = default_test_config(&Mainnet)?; let run_dir = testdir()?; let cache_dir = run_dir.path().join("state"); // create cache dir create_dir(cache_dir.clone())?; // create a v1 dir outside cache dir that should not be deleted let outside_dir = run_dir.path().join("v1"); create_dir(&outside_dir)?; assert!(outside_dir.as_path().exists()); // create a `v1` dir inside cache dir that should be deleted let inside_dir = cache_dir.join("v1"); create_dir(&inside_dir)?; let canonicalized_inside_dir = canonicalize(inside_dir.clone()).ok().unwrap(); assert!(inside_dir.as_path().exists()); // modify config with our cache dir and not ephemeral configuration // (delete old databases function will not run when ephemeral = true) config.state.cache_dir = cache_dir; config.state.ephemeral = false; // run zebra with our config let mut child = run_dir .with_config(&mut config)? .spawn_child(args!["start"])?; // delete checker running child.expect_stdout_line_matches("checking for old database versions".to_string())?; // inside dir was deleted child.expect_stdout_line_matches(format!( "deleted outdated state database directory.*deleted_db.*=.*{canonicalized_inside_dir:?}" ))?; assert!(!inside_dir.as_path().exists()); // deleting old databases task ended child.expect_stdout_line_matches("finished old database version cleanup task".to_string())?; // outside dir was not deleted assert!(outside_dir.as_path().exists()); // finish child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; // [Note on port conflict](#Note on port conflict) output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; Ok(()) } /// Test sending transactions using a lightwalletd instance connected to a zebrad instance. /// /// See [`common::lightwalletd::send_transaction_test`] for more information. /// /// This test doesn't work on Windows, so it is always skipped on that platform. #[tokio::test] #[ignore] #[cfg(feature = "lightwalletd-grpc-tests")] #[cfg(not(target_os = "windows"))] async fn sending_transactions_using_lightwalletd() -> Result<()> { common::lightwalletd::send_transaction_test::run().await } /// Test all the rpc methods a wallet connected to lightwalletd can call. /// /// See [`common::lightwalletd::wallet_grpc_test`] for more information. /// /// This test doesn't work on Windows, so it is always skipped on that platform. #[tokio::test] #[ignore] #[cfg(feature = "lightwalletd-grpc-tests")] #[cfg(not(target_os = "windows"))] async fn lightwalletd_wallet_grpc_tests() -> Result<()> { common::lightwalletd::wallet_grpc_test::run().await } /// Test successful getpeerinfo rpc call /// /// See [`common::get_block_template_rpcs::get_peer_info`] for more information. #[tokio::test] #[cfg(feature = "getblocktemplate-rpcs")] async fn get_peer_info() -> Result<()> { common::get_block_template_rpcs::get_peer_info::run().await } /// Test successful getblocktemplate rpc call /// /// See [`common::get_block_template_rpcs::get_block_template`] for more information. #[tokio::test] #[ignore] #[cfg(feature = "getblocktemplate-rpcs")] async fn get_block_template() -> Result<()> { common::get_block_template_rpcs::get_block_template::run().await } /// Test successful submitblock rpc call /// /// See [`common::get_block_template_rpcs::submit_block`] for more information. #[tokio::test] #[ignore] #[cfg(feature = "getblocktemplate-rpcs")] async fn submit_block() -> Result<()> { common::get_block_template_rpcs::submit_block::run().await } /// Check that the the end of support code is called at least once. #[test] fn end_of_support_is_checked_at_start() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; let mut child = testdir.spawn_child(args!["start"])?; // Give enough time to start up the eos task. std::thread::sleep(Duration::from_secs(30)); child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; // Zebra started output.stdout_line_contains("Starting zebrad")?; // End of support task started. output.stdout_line_contains("Starting end of support task")?; // Make sure the command was killed output.assert_was_killed()?; Ok(()) } /// Test `zebra-checkpoints` on mainnet. /// /// If you want to run this test individually, see the module documentation. /// See [`common::checkpoints`] for more information. #[tokio::test] #[ignore] #[cfg(feature = "zebra-checkpoints")] async fn generate_checkpoints_mainnet() -> Result<()> { common::checkpoints::run(Mainnet).await } /// Test `zebra-checkpoints` on testnet. /// This test might fail if testnet is unstable. /// /// If you want to run this test individually, see the module documentation. /// See [`common::checkpoints`] for more information. #[tokio::test] #[ignore] #[cfg(feature = "zebra-checkpoints")] async fn generate_checkpoints_testnet() -> Result<()> { common::checkpoints::run(Network::new_default_testnet()).await } /// Check that new states are created with the current state format version, /// and that restarting `zebrad` doesn't change the format version. #[tokio::test] async fn new_state_format() -> Result<()> { for network in Network::iter() { state_format_test("new_state_format_test", &network, 2, None).await?; } Ok(()) } /// Check that outdated states are updated to the current state format version, /// and that restarting `zebrad` doesn't change the updated format version. /// /// TODO: test partial updates, once we have some updates that take a while. /// (or just add a delay during tests) #[tokio::test] async fn update_state_format() -> Result<()> { let mut fake_version = state_database_format_version_in_code(); fake_version.minor = 0; fake_version.patch = 0; for network in Network::iter() { state_format_test("update_state_format_test", &network, 3, Some(&fake_version)).await?; } Ok(()) } /// Check that newer state formats are downgraded to the current state format version, /// and that restarting `zebrad` doesn't change the format version. /// /// Future version compatibility is a best-effort attempt, this test can be disabled if it fails. #[tokio::test] async fn downgrade_state_format() -> Result<()> { let mut fake_version = state_database_format_version_in_code(); fake_version.minor = u16::MAX.into(); fake_version.patch = 0; for network in Network::iter() { state_format_test( "downgrade_state_format_test", &network, 3, Some(&fake_version), ) .await?; } Ok(()) } /// Test state format changes, see calling tests for details. async fn state_format_test( base_test_name: &str, network: &Network, reopen_count: usize, fake_version: Option<&Version>, ) -> Result<()> { let _init_guard = zebra_test::init(); let test_name = &format!("{base_test_name}/new"); // # Create a new state and check it has the current version let zebrad = spawn_zebrad_without_rpc(network.clone(), test_name, false, false, None, false)?; // Skip the test unless it has the required state and environmental variables. let Some(mut zebrad) = zebrad else { return Ok(()); }; tracing::info!(?network, "running {test_name} using zebrad"); zebrad.expect_stdout_line_matches("creating new database with the current format")?; zebrad.expect_stdout_line_matches("loaded Zebra state cache")?; // Give Zebra enough time to actually write the database to disk. tokio::time::sleep(Duration::from_secs(1)).await; let logs = zebrad.kill_and_return_output(false)?; assert!( !logs.contains("marked database format as upgraded"), "unexpected format upgrade in logs:\n\ {logs}" ); assert!( !logs.contains("marked database format as downgraded"), "unexpected format downgrade in logs:\n\ {logs}" ); let output = zebrad.wait_with_output()?; let mut output = output.assert_failure()?; let mut dir = output .take_dir() .expect("dir should not already have been taken"); // [Note on port conflict](#Note on port conflict) output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; // # Apply the fake version if needed let mut expect_older_version = false; let mut expect_newer_version = false; if let Some(fake_version) = fake_version { let test_name = &format!("{base_test_name}/apply_fake_version/{fake_version}"); tracing::info!(?network, "running {test_name} using zebra-state"); let config = UseAnyState .zebrad_config(test_name, false, Some(dir.path()), network) .expect("already checked config")?; zebra_state::write_state_database_format_version_to_disk( &config.state, fake_version, network, ) .expect("can't write fake database version to disk"); // Give zebra_state enough time to actually write the database version to disk. tokio::time::sleep(Duration::from_secs(1)).await; let running_version = state_database_format_version_in_code(); match fake_version.cmp(&running_version) { Ordering::Less => expect_older_version = true, Ordering::Equal => {} Ordering::Greater => expect_newer_version = true, } } // # Reopen that state and check the version hasn't changed for reopened in 0..reopen_count { let test_name = &format!("{base_test_name}/reopen/{reopened}"); if reopened > 0 { expect_older_version = false; expect_newer_version = false; } let mut zebrad = spawn_zebrad_without_rpc(network.clone(), test_name, false, false, dir, false)? .expect("unexpectedly missing required state or env vars"); tracing::info!(?network, "running {test_name} using zebrad"); if expect_older_version { zebrad.expect_stdout_line_matches("trying to open older database format")?; zebrad.expect_stdout_line_matches("marked database format as upgraded")?; zebrad.expect_stdout_line_matches("database is fully upgraded")?; } else if expect_newer_version { zebrad.expect_stdout_line_matches("trying to open newer database format")?; zebrad.expect_stdout_line_matches("marked database format as downgraded")?; } else { zebrad.expect_stdout_line_matches("trying to open current database format")?; zebrad.expect_stdout_line_matches("loaded Zebra state cache")?; } // Give Zebra enough time to actually write the database to disk. tokio::time::sleep(Duration::from_secs(1)).await; let logs = zebrad.kill_and_return_output(false)?; if !expect_older_version { assert!( !logs.contains("marked database format as upgraded"), "unexpected format upgrade in logs:\n\ {logs}" ); } if !expect_newer_version { assert!( !logs.contains("marked database format as downgraded"), "unexpected format downgrade in logs:\n\ {logs}" ); } let output = zebrad.wait_with_output()?; let mut output = output.assert_failure()?; dir = output .take_dir() .expect("dir should not already have been taken"); // [Note on port conflict](#Note on port conflict) output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; } Ok(()) } /// Snapshot the `z_getsubtreesbyindex` method in a synchronized chain. /// /// This test name must have the same prefix as the `fully_synced_rpc_test`, so they can be run in the same test job. #[tokio::test] #[ignore] async fn fully_synced_rpc_z_getsubtreesbyindex_snapshot_test() -> Result<()> { let _init_guard = zebra_test::init(); // We're only using cached Zebra state here, so this test type is the most similar let test_type = TestType::UpdateZebraCachedStateWithRpc; let network = Network::Mainnet; let (mut zebrad, zebra_rpc_address) = if let Some(zebrad_and_address) = spawn_zebrad_for_rpc( network, "rpc_z_getsubtreesbyindex_sync_snapshots", test_type, true, )? { tracing::info!("running fully synced zebrad z_getsubtreesbyindex RPC test"); zebrad_and_address } else { // Skip the test, we don't have the required cached state return Ok(()); }; // Store the state version message so we can wait for the upgrade later if needed. let state_version_message = wait_for_state_version_message(&mut zebrad)?; // It doesn't matter how long the state version upgrade takes, // because the sync finished regex is repeated every minute. wait_for_state_version_upgrade( &mut zebrad, &state_version_message, state_database_format_version_in_code(), None, )?; // Wait for zebrad to load the full cached blockchain. zebrad.expect_stdout_line_matches(SYNC_FINISHED_REGEX)?; // Create an http client let client = RpcRequestClient::new(zebra_rpc_address.expect("already checked that address is valid")); // Create test vector matrix let zcashd_test_vectors = vec![ ( "z_getsubtreesbyindex_mainnet_sapling_0_1".to_string(), r#"["sapling", 0, 1]"#.to_string(), ), ( "z_getsubtreesbyindex_mainnet_sapling_0_11".to_string(), r#"["sapling", 0, 11]"#.to_string(), ), ( "z_getsubtreesbyindex_mainnet_sapling_17_1".to_string(), r#"["sapling", 17, 1]"#.to_string(), ), ( "z_getsubtreesbyindex_mainnet_sapling_1090_6".to_string(), r#"["sapling", 1090, 6]"#.to_string(), ), ( "z_getsubtreesbyindex_mainnet_orchard_0_1".to_string(), r#"["orchard", 0, 1]"#.to_string(), ), ( "z_getsubtreesbyindex_mainnet_orchard_338_1".to_string(), r#"["orchard", 338, 1]"#.to_string(), ), ( "z_getsubtreesbyindex_mainnet_orchard_585_1".to_string(), r#"["orchard", 585, 1]"#.to_string(), ), ]; for i in zcashd_test_vectors { let res = client.call("z_getsubtreesbyindex", i.1).await?; let body = res.bytes().await; let parsed: Value = serde_json::from_slice(&body.expect("Response is valid json"))?; insta::assert_json_snapshot!(i.0, parsed); } zebrad.kill(false)?; let output = zebrad.wait_with_output()?; let output = output.assert_failure()?; // [Note on port conflict](#Note on port conflict) output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; Ok(()) } /// Checks that the Regtest genesis block can be validated. #[tokio::test] async fn validate_regtest_genesis_block() { let _init_guard = zebra_test::init(); let network = Network::new_regtest(None, None); let state = zebra_state::init_test(&network); let ( block_verifier_router, _transaction_verifier, _parameter_download_task_handle, _max_checkpoint_height, ) = zebra_consensus::router::init(zebra_consensus::Config::default(), &network, state).await; let genesis_hash = block_verifier_router .oneshot(zebra_consensus::Request::Commit(regtest_genesis_block())) .await .expect("should validate Regtest genesis block"); assert_eq!( genesis_hash, network.genesis_hash(), "validated block hash should match network genesis hash" ) } /// Test that Version messages are sent with the external address when configured to do so. #[test] fn external_address() -> Result<()> { let _init_guard = zebra_test::init(); let testdir = testdir()?.with_config(&mut external_address_test_config(&Mainnet)?)?; let mut child = testdir.spawn_child(args!["start"])?; // Give enough time to start connecting to some peers. std::thread::sleep(Duration::from_secs(10)); child.kill(false)?; let output = child.wait_with_output()?; let output = output.assert_failure()?; // Zebra started output.stdout_line_contains("Starting zebrad")?; // Make sure we are using external address for Version messages. output.stdout_line_contains("using external address for Version messages")?; // Make sure the command was killed. output.assert_was_killed()?; Ok(()) } /// Test successful `getblocktemplate` and `submitblock` RPC calls on Regtest on Canopy. /// /// See [`common::regtest::submit_blocks`] for more information. // TODO: Test this with an NU5 activation height too once config can be serialized. #[tokio::test] #[cfg(feature = "getblocktemplate-rpcs")] async fn regtest_block_templates_are_valid_block_submissions() -> Result<()> { common::regtest::submit_blocks_test().await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] #[cfg(feature = "getblocktemplate-rpcs")] async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { use std::sync::Arc; use common::regtest::MiningRpcMethods; use eyre::Error; use tokio::time::timeout; use zebra_chain::{ chain_tip::ChainTip, parameters::NetworkUpgrade, primitives::byte_array::increment_big_endian, }; use zebra_rpc::methods::GetBlockHash; use zebra_state::{ReadResponse, Response}; let _init_guard = zebra_test::init(); let mut config = os_assigned_rpc_port_config(false, &Network::new_regtest(None, None))?; config.state.ephemeral = false; let network = config.network.network.clone(); let test_dir = testdir()?.with_config(&mut config)?; let mut child = test_dir.spawn_child(args!["start"])?; let rpc_address = read_listen_addr_from_logs(&mut child, OPENED_RPC_ENDPOINT_MSG)?; tracing::info!("waiting for Zebra state cache to be opened"); tokio::time::sleep(LAUNCH_DELAY).await; tracing::info!("starting read state with syncer"); // Spawn a read state with the RPC syncer to check that it has the same best chain as Zebra let (read_state, _latest_chain_tip, mut chain_tip_change, _sync_task) = zebra_rpc::sync::init_read_state_with_syncer( config.state, &config.network.network, rpc_address, ) .await? .map_err(|err| eyre!(err))?; tracing::info!("waiting for first chain tip change"); // Wait for Zebrad to start up let tip_action = timeout(LAUNCH_DELAY, chain_tip_change.wait_for_tip_change()).await??; assert!( tip_action.is_reset(), "first tip action should be a reset for the genesis block" ); tracing::info!("got genesis chain tip change, submitting more blocks .."); let rpc_client = RpcRequestClient::new(rpc_address); let mut blocks = Vec::new(); for _ in 0..10 { let (block, height) = rpc_client.submit_block_from_template().await?; blocks.push(block); let tip_action = timeout( Duration::from_secs(1), chain_tip_change.wait_for_tip_change(), ) .await??; assert_eq!( tip_action.best_tip_height(), height, "tip action height should match block submission" ); } tracing::info!("checking that read state has the new non-finalized best chain blocks"); for expected_block in blocks.clone() { let height = expected_block.coinbase_height().unwrap(); let zebra_block = rpc_client .get_block(height.0 as i32) .await .map_err(|err| eyre!(err))? .expect("Zebra test child should have the expected block"); assert_eq!( zebra_block, Arc::new(expected_block), "Zebra should have the same block" ); let ReadResponse::Block(read_state_block) = read_state .clone() .oneshot(zebra_state::ReadRequest::Block(height.into())) .await .map_err(|err| eyre!(err))? else { unreachable!("unexpected read response to a block request") }; assert_eq!( zebra_block, read_state_block.expect("read state should have the block"), "read state should have the same block" ); } tracing::info!("getting next block template"); let (block_11, _) = rpc_client.block_from_template(Height(100)).await?; let next_blocks: Vec<_> = blocks .split_off(5) .into_iter() .chain(std::iter::once(block_11)) .collect(); tracing::info!("creating populated state"); let genesis_block = regtest_genesis_block(); let (state2, read_state2, latest_chain_tip2, _chain_tip_change2) = zebra_state::populated_state( std::iter::once(genesis_block).chain(blocks.iter().cloned().map(Arc::new)), &network, ) .await; tracing::info!("attempting to trigger a best chain change"); for mut block in next_blocks { let is_chain_history_activation_height = NetworkUpgrade::Heartwood .activation_height(&network) == Some(block.coinbase_height().unwrap()); let header = Arc::make_mut(&mut block.header); increment_big_endian(header.nonce.as_mut()); let ReadResponse::ChainInfo(chain_info) = read_state2 .clone() .oneshot(zebra_state::ReadRequest::ChainInfo) .await .map_err(|err| eyre!(err))? else { unreachable!("wrong response variant"); }; header.previous_block_hash = chain_info.tip_hash; header.commitment_bytes = chain_info .history_tree .hash() .or(is_chain_history_activation_height.then_some([0; 32].into())) .expect("history tree can't be empty") .bytes_in_serialized_order() .into(); let Response::Committed(block_hash) = state2 .clone() .oneshot(zebra_state::Request::CommitSemanticallyVerifiedBlock( Arc::new(block.clone()).into(), )) .await .map_err(|err| eyre!(err))? else { unreachable!("wrong response variant"); }; assert!( chain_tip_change.last_tip_change().is_none(), "there should be no tip change until the last block is submitted" ); rpc_client.submit_block(block.clone()).await?; blocks.push(block); let GetBlockHash(best_block_hash) = rpc_client .json_result_from_call("getbestblockhash", "[]") .await .map_err(|err| eyre!(err))?; if block_hash == best_block_hash { break; } } tracing::info!("newly submitted blocks are in the best chain, checking for reset"); tokio::time::sleep(Duration::from_secs(3)).await; let tip_action = timeout( Duration::from_secs(1), chain_tip_change.wait_for_tip_change(), ) .await??; let (expected_height, expected_hash) = latest_chain_tip2 .best_tip_height_and_hash() .expect("should have a chain tip"); assert!(tip_action.is_reset(), "tip action should be reset"); assert_eq!( tip_action.best_tip_hash_and_height(), (expected_hash, expected_height), "tip action hashes and heights should match" ); tracing::info!("checking that read state has the new non-finalized best chain blocks"); for expected_block in blocks { let height = expected_block.coinbase_height().unwrap(); let zebra_block = rpc_client .get_block(height.0 as i32) .await .map_err(|err| eyre!(err))? .expect("Zebra test child should have the expected block"); assert_eq!( zebra_block, Arc::new(expected_block), "Zebra should have the same block" ); let ReadResponse::Block(read_state_block) = read_state .clone() .oneshot(zebra_state::ReadRequest::Block(height.into())) .await .map_err(|err| eyre!(err))? else { unreachable!("unexpected read response to a block request") }; assert_eq!( zebra_block, read_state_block.expect("read state should have the block"), "read state should have the same block" ); } tracing::info!("restarting Zebra on Mainnet"); child.kill(false)?; let output = child.wait_with_output()?; // Make sure the command was killed output.assert_was_killed()?; output.assert_failure()?; let mut config = random_known_rpc_port_config(false, &Network::Mainnet)?; config.state.ephemeral = false; let rpc_address = config.rpc.listen_addr.unwrap(); let test_dir = testdir()?.with_config(&mut config)?; let _child = test_dir.spawn_child(args!["start"])?; tracing::info!("waiting for Zebra state cache to be opened"); tokio::time::sleep(LAUNCH_DELAY).await; tracing::info!("starting read state with syncer"); // Spawn a read state with the RPC syncer to check that it has the same best chain as Zebra let (_read_state, _latest_chain_tip, mut chain_tip_change, _sync_task) = zebra_rpc::sync::init_read_state_with_syncer( config.state, &config.network.network, rpc_address, ) .await? .map_err(|err| eyre!(err))?; tracing::info!("waiting for finalized chain tip changes"); timeout( Duration::from_secs(100), tokio::spawn(async move { for _ in 0..2 { chain_tip_change .wait_for_tip_change() .await .map_err(|err| eyre!(err))?; } Ok::<(), Error>(()) }), ) .await???; Ok(()) } /// Test successful block template submission as a block proposal or submission on a custom Testnet. /// /// This test can be run locally with: /// `cargo test --package zebrad --test acceptance --features getblocktemplate-rpcs -- nu6_funding_streams_and_coinbase_balance --exact --show-output` #[tokio::test(flavor = "multi_thread")] #[cfg(feature = "getblocktemplate-rpcs")] async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { use zebra_chain::{ chain_sync_status::MockSyncStatus, parameters::{ subsidy::{FundingStreamReceiver, FUNDING_STREAM_MG_ADDRESSES_TESTNET}, testnet::{ self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient, ConfiguredFundingStreams, }, NetworkUpgrade, }, serialization::ZcashSerialize, work::difficulty::U256, }; use zebra_network::address_book_peers::MockAddressBookPeers; use zebra_node_services::mempool; use zebra_rpc::methods::{ get_block_template_rpcs::{ get_block_template::{ fetch_state_tip_and_local_time, generate_coinbase_and_roots, proposal_block_from_template, GetBlockTemplate, GetBlockTemplateRequestMode, }, types::get_block_template, types::submit_block, }, hex_data::HexData, GetBlockTemplateRpc, GetBlockTemplateRpcImpl, }; use zebra_test::mock_service::MockService; let _init_guard = zebra_test::init(); tracing::info!("running nu6_funding_streams_and_coinbase_balance test"); let base_network_params = testnet::Parameters::build() // Regtest genesis hash .with_genesis_hash("029f11d80ef9765602235e1bc9727e3eb6ba20839319f761fee920d63401e327") .with_target_difficulty_limit(U256::from_big_endian(&[0x0f; 32])) .with_disable_pow(true) .with_slow_start_interval(Height::MIN) .with_activation_heights(ConfiguredActivationHeights { nu6: Some(1), ..Default::default() }); let network = base_network_params .clone() .with_post_nu6_funding_streams(ConfiguredFundingStreams { // Start checking funding streams from block height 1 height_range: Some(Height(1)..Height(100)), // Use default post-NU6 recipients recipients: None, }) .to_network(); tracing::info!("built configured Testnet, starting state service and block verifier"); let default_test_config = default_test_config(&network)?; let mining_config = default_test_config.mining; let miner_address = mining_config .miner_address .clone() .expect("hard-coded config should have a miner address"); let (state, read_state, latest_chain_tip, _chain_tip_change) = zebra_state::init_test_services(&network); let ( block_verifier_router, _transaction_verifier, _parameter_download_task_handle, _max_checkpoint_height, ) = zebra_consensus::router::init(zebra_consensus::Config::default(), &network, state.clone()) .await; tracing::info!("started state service and block verifier, committing Regtest genesis block"); let genesis_hash = block_verifier_router .clone() .oneshot(zebra_consensus::Request::Commit(regtest_genesis_block())) .await .expect("should validate Regtest genesis block"); let mut mempool = MockService::build() .with_max_request_delay(Duration::from_secs(5)) .for_unit_tests(); let mut mock_sync_status = MockSyncStatus::default(); mock_sync_status.set_is_close_to_tip(true); let get_block_template_rpc_impl = GetBlockTemplateRpcImpl::new( &network, mining_config, mempool.clone(), read_state.clone(), latest_chain_tip, block_verifier_router, mock_sync_status, MockAddressBookPeers::default(), ); let make_mock_mempool_request_handler = || async move { mempool .expect_request(mempool::Request::FullTransactions) .await .respond(mempool::Response::FullTransactions { transactions: vec![], // tip hash needs to match chain info for long poll requests last_seen_tip_hash: genesis_hash, }); }; let block_template_fut = get_block_template_rpc_impl.get_block_template(None); let mock_mempool_request_handler = make_mock_mempool_request_handler.clone()(); let (block_template, _) = tokio::join!(block_template_fut, mock_mempool_request_handler); let get_block_template::Response::TemplateMode(block_template) = block_template.expect("unexpected error in getblocktemplate RPC call") else { panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response") }; let proposal_block = proposal_block_from_template(&block_template, None, NetworkUpgrade::Nu6)?; let hex_proposal_block = HexData(proposal_block.zcash_serialize_to_vec()?); // Check that the block template is a valid block proposal let get_block_template::Response::ProposalMode(block_proposal_result) = get_block_template_rpc_impl .get_block_template(Some(get_block_template::JsonParameters { mode: GetBlockTemplateRequestMode::Proposal, data: Some(hex_proposal_block), ..Default::default() })) .await? else { panic!( "this getblocktemplate call should return the `ProposalMode` variant of the response" ) }; assert!( block_proposal_result.is_valid(), "block proposal should succeed" ); // Submit the same block let submit_block_response = get_block_template_rpc_impl .submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None) .await?; assert_eq!( submit_block_response, submit_block::Response::Accepted, "valid block should be accepted" ); // Use an invalid coinbase transaction (with an output value greater than the `block_subsidy + miner_fees - expected_lockbox_funding_stream`) let make_configured_recipients_with_lockbox_numerator = |numerator| { Some(vec![ ConfiguredFundingStreamRecipient { receiver: FundingStreamReceiver::Deferred, numerator, addresses: None, }, ConfiguredFundingStreamRecipient { receiver: FundingStreamReceiver::MajorGrants, numerator: 8, addresses: Some( FUNDING_STREAM_MG_ADDRESSES_TESTNET .map(ToString::to_string) .to_vec(), ), }, ]) }; // Gets the next block template let block_template_fut = get_block_template_rpc_impl.get_block_template(None); let mock_mempool_request_handler = make_mock_mempool_request_handler.clone()(); let (block_template, _) = tokio::join!(block_template_fut, mock_mempool_request_handler); let get_block_template::Response::TemplateMode(block_template) = block_template.expect("unexpected error in getblocktemplate RPC call") else { panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response") }; let valid_original_block_template = block_template.clone(); let zebra_state::GetBlockTemplateChainInfo { history_tree, .. } = fetch_state_tip_and_local_time(read_state.clone()).await?; let network = base_network_params .clone() .with_post_nu6_funding_streams(ConfiguredFundingStreams { height_range: Some(Height(1)..Height(100)), recipients: make_configured_recipients_with_lockbox_numerator(0), }) .to_network(); let (coinbase_txn, default_roots) = generate_coinbase_and_roots( &network, Height(block_template.height), &miner_address, &[], history_tree.clone(), true, vec![], ); let block_template = GetBlockTemplate { coinbase_txn, block_commitments_hash: default_roots.block_commitments_hash, light_client_root_hash: default_roots.block_commitments_hash, final_sapling_root_hash: default_roots.block_commitments_hash, default_roots, ..(*block_template) }; let proposal_block = proposal_block_from_template(&block_template, None, NetworkUpgrade::Nu6)?; // Submit the invalid block with an excessive coinbase output value let submit_block_response = get_block_template_rpc_impl .submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None) .await?; tracing::info!(?submit_block_response, "submitted invalid block"); assert_eq!( submit_block_response, submit_block::Response::ErrorResponse(submit_block::ErrorResponse::Rejected), "invalid block with excessive coinbase output value should be rejected" ); // Use an invalid coinbase transaction (with an output value less than than the `block_subsidy + miner_fees - expected_lockbox_funding_stream`) let network = base_network_params .clone() .with_post_nu6_funding_streams(ConfiguredFundingStreams { height_range: Some(Height(1)..Height(100)), recipients: make_configured_recipients_with_lockbox_numerator(20), }) .to_network(); let (coinbase_txn, default_roots) = generate_coinbase_and_roots( &network, Height(block_template.height), &miner_address, &[], history_tree.clone(), true, vec![], ); let block_template = GetBlockTemplate { coinbase_txn, block_commitments_hash: default_roots.block_commitments_hash, light_client_root_hash: default_roots.block_commitments_hash, final_sapling_root_hash: default_roots.block_commitments_hash, default_roots, ..block_template }; let proposal_block = proposal_block_from_template(&block_template, None, NetworkUpgrade::Nu6)?; // Submit the invalid block with an excessive coinbase input value let submit_block_response = get_block_template_rpc_impl .submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None) .await?; tracing::info!(?submit_block_response, "submitted invalid block"); assert_eq!( submit_block_response, submit_block::Response::ErrorResponse(submit_block::ErrorResponse::Rejected), "invalid block with insufficient coinbase output value should be rejected" ); // Check that the original block template can be submitted successfully let proposal_block = proposal_block_from_template(&valid_original_block_template, None, NetworkUpgrade::Nu6)?; let submit_block_response = get_block_template_rpc_impl .submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None) .await?; assert_eq!( submit_block_response, submit_block::Response::Accepted, "valid block should be accepted" ); Ok(()) }