diff --git a/.github/workflows/continous-integration-docker.patch.yml b/.github/workflows/continous-integration-docker.patch.yml index a9a4bd47fd7..06225b59b13 100644 --- a/.github/workflows/continous-integration-docker.patch.yml +++ b/.github/workflows/continous-integration-docker.patch.yml @@ -90,6 +90,12 @@ jobs: steps: - run: 'echo "No build required"' + submit-block-test: + name: submit block / Run submit-block test + runs-on: ubuntu-latest + steps: + - run: 'echo "No build required"' + lightwalletd-full-sync: name: lightwalletd tip / Run lwd-full-sync test runs-on: ubuntu-latest diff --git a/.github/workflows/continous-integration-docker.yml b/.github/workflows/continous-integration-docker.yml index 77e7a9c1fab..c99ede8d492 100644 --- a/.github/workflows/continous-integration-docker.yml +++ b/.github/workflows/continous-integration-docker.yml @@ -578,3 +578,28 @@ jobs: root_state_path: '/var/cache' zebra_state_dir: 'zebrad-cache' lwd_state_dir: 'lwd-cache' + + # Test that Zebra can handle a submit block RPC call, using a cached Zebra tip state + # + # Runs: + # - after every PR is merged to `main` + # - on every PR update + # + # If the state version has changed, waits for the new cached states to be created. + # Otherwise, if the state rebuild was skipped, runs immediately after the build job. + submit-block-test: + name: submit block + needs: test-full-sync + uses: ./.github/workflows/deploy-gcp-tests.yml + if: ${{ !cancelled() && !failure() && github.event.inputs.regenerate-disks != 'true' && github.event.inputs.run-full-sync != 'true' && github.event.inputs.run-lwd-sync != 'true' && github.event.inputs.run-lwd-send-tx != 'true' }} + with: + app_name: zebrad + test_id: submit-block + test_description: Test submitting blocks via Zebra's rpc server + test_variables: '-e TEST_SUBMIT_BLOCK=1 -e ZEBRA_FORCE_USE_COLOR=1 -e ZEBRA_CACHED_STATE_DIR=/var/cache/zebrad-cache' + needs_zebra_state: true + needs_lwd_state: false + saves_to_disk: false + disk_suffix: tip + root_state_path: '/var/cache' + zebra_state_dir: 'zebrad-cache' diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0e4ecf65b07..6129f8bf32e 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -78,6 +78,10 @@ case "$1" in ls -lh "$ZEBRA_CACHED_STATE_DIR"/*/* || (echo "No $ZEBRA_CACHED_STATE_DIR/*/*"; ls -lhR "$ZEBRA_CACHED_STATE_DIR" | head -50 || echo "No $ZEBRA_CACHED_STATE_DIR directory") ls -lhR "$LIGHTWALLETD_DATA_DIR/db" || (echo "No $LIGHTWALLETD_DATA_DIR/db"; ls -lhR "$LIGHTWALLETD_DATA_DIR" | head -50 || echo "No $LIGHTWALLETD_DATA_DIR directory") cargo test --locked --release --features lightwalletd-grpc-tests --package zebrad --test acceptance -- --nocapture --include-ignored sending_transactions_using_lightwalletd + elif [[ "$TEST_SUBMIT_BLOCK" -eq "1" ]]; then + # Starting with a cached Zebra tip, test sending a block to Zebra's RPC port. + ls -lh "$ZEBRA_CACHED_STATE_DIR"/*/* || (echo "No $ZEBRA_CACHED_STATE_DIR/*/*"; ls -lhR "$ZEBRA_CACHED_STATE_DIR" | head -50 || echo "No $ZEBRA_CACHED_STATE_DIR directory") + cargo test --locked --release --features getblocktemplate-rpcs --package zebrad --test acceptance -- --nocapture --include-ignored submit_block # These command-lines are provided by the caller. # diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index 4e91e8ae527..ae25f819041 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -146,20 +146,19 @@ mod common; use common::{ check::{is_zebrad_version, EphemeralCheck, EphemeralConfig}, + config::random_known_rpc_port_config, config::{ config_file_full_path, configs_dir, default_test_config, persistent_test_config, testdir, }, launch::{spawn_zebrad_for_rpc, ZebradTestDirExt, BETWEEN_NODES_DELAY, LAUNCH_DELAY}, - lightwalletd::{ - can_spawn_lightwalletd_for_rpc, random_known_rpc_port_config, spawn_lightwalletd_for_rpc, - LightwalletdTestType::{self, *}, - }, + 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, *}, }; /// The maximum amount of time that we allow the creation of a future to block the `tokio` executor. @@ -1585,7 +1584,7 @@ async fn lightwalletd_test_suite() -> Result<()> { /// 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: LightwalletdTestType) -> Result<()> { +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. @@ -2029,7 +2028,7 @@ 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 = LightwalletdTestType::UpdateCachedState; + let test_type = TestType::UpdateCachedState; let network = Network::Mainnet; let (mut zebrad, zebra_rpc_address) = if let Some(zebrad_and_address) = diff --git a/zebrad/tests/common/cached_state.rs b/zebrad/tests/common/cached_state.rs index d758a7fffcb..ad70983f588 100644 --- a/zebrad/tests/common/cached_state.rs +++ b/zebrad/tests/common/cached_state.rs @@ -7,11 +7,17 @@ use std::path::{Path, PathBuf}; +use std::time::Duration; + +use reqwest::Client; + use color_eyre::eyre::{eyre, Result}; use tempfile::TempDir; use tokio::fs; use tower::{util::BoxService, Service}; +use zebra_chain::block::Block; +use zebra_chain::serialization::ZcashDeserializeInto; use zebra_chain::{ block::{self, Height}, chain_tip::ChainTip, @@ -21,6 +27,14 @@ use zebra_state::{ChainTipChange, LatestChainTip}; use crate::common::config::testdir; +use zebra_state::MAX_BLOCK_REORG_HEIGHT; + +use crate::common::{ + launch::spawn_zebrad_for_rpc, + sync::{check_sync_logs_until, MempoolBehavior, SYNC_FINISHED_REGEX}, + test_type::TestType, +}; + /// Path to a directory containing a cached Zebra state. pub const ZEBRA_CACHED_STATE_DIR: &str = "ZEBRA_CACHED_STATE_DIR"; @@ -144,3 +158,152 @@ async fn copy_directory( Ok(sub_directories) } + +/// Accepts a network, test_type, test_name, and num_blocks (how many blocks past the finalized tip to try getting) +/// +/// Syncs zebra until the tip, gets some blocks near the tip, via getblock rpc calls, +/// shuts down zebra, and gets the finalized tip height of the updated cached state. +/// +/// Returns retrieved and deserialized blocks that are above the finalized tip height of the cached state. +/// +/// ## Panics +/// +/// If the provided `test_type` doesn't need an rpc server and cached state, or if `max_num_blocks` is 0 +pub async fn get_future_blocks( + network: Network, + test_type: TestType, + test_name: &str, + max_num_blocks: u32, +) -> Result> { + let blocks: Vec = get_raw_future_blocks(network, test_type, test_name, max_num_blocks) + .await? + .into_iter() + .map(hex::decode) + .map(|block_bytes| { + block_bytes + .expect("getblock rpc calls in get_raw_future_blocks should return valid hexdata") + .zcash_deserialize_into() + .expect("decoded hex data from getblock rpc calls should deserialize into blocks") + }) + .collect(); + + Ok(blocks) +} + +/// Accepts a network, test_type, test_name, and num_blocks (how many blocks past the finalized tip to try getting) +/// +/// Syncs zebra until the tip, gets some blocks near the tip, via getblock rpc calls, +/// shuts down zebra, and gets the finalized tip height of the updated cached state. +/// +/// Returns hexdata of retrieved blocks that are above the finalized tip height of the cached state. +/// +/// ## Panics +/// +/// If the provided `test_type` doesn't need an rpc server and cached state, or if `max_num_blocks` is 0 +pub async fn get_raw_future_blocks( + network: Network, + test_type: TestType, + test_name: &str, + max_num_blocks: u32, +) -> Result> { + assert!(max_num_blocks > 0); + + let max_num_blocks = max_num_blocks.min(MAX_BLOCK_REORG_HEIGHT); + let mut raw_blocks = Vec::with_capacity(max_num_blocks as usize); + + assert!( + test_type.needs_zebra_cached_state() && test_type.needs_zebra_rpc_server(), + "get_raw_future_blocks needs zebra cached state and rpc server" + ); + + let should_sync = true; + let (zebrad, zebra_rpc_address) = + spawn_zebrad_for_rpc(network, test_name, test_type, should_sync)? + .ok_or_else(|| eyre!("get_raw_future_blocks requires a cached state"))?; + let rpc_address = zebra_rpc_address.expect("test type must have RPC port"); + + let mut zebrad = check_sync_logs_until( + zebrad, + network, + SYNC_FINISHED_REGEX, + MempoolBehavior::ShouldAutomaticallyActivate, + true, + )?; + + // Create an http client + let client = Client::new(); + + let send_rpc_request = |method, params| { + client + .post(format!("http://{}", &rpc_address)) + .body(format!( + r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"# + )) + .header("Content-Type", "application/json") + .send() + }; + + let blockchain_info: serde_json::Value = serde_json::from_str( + &send_rpc_request("getblockchaininfo", "[]".to_string()) + .await? + .text() + .await?, + )?; + + let tip_height: u32 = blockchain_info["result"]["blocks"] + .as_u64() + .expect("unexpected block height: doesn't fit in u64") + .try_into() + .expect("unexpected block height: doesn't fit in u32"); + + let estimated_finalized_tip_height = tip_height - MAX_BLOCK_REORG_HEIGHT; + + tracing::info!( + ?tip_height, + ?estimated_finalized_tip_height, + "got tip height from blockchaininfo", + ); + + for block_height in (0..max_num_blocks).map(|idx| idx + estimated_finalized_tip_height) { + let raw_block: serde_json::Value = serde_json::from_str( + &send_rpc_request("getblock", format!(r#"["{block_height}", 0]"#)) + .await? + .text() + .await?, + )?; + + raw_blocks.push(( + block_height, + raw_block["result"] + .as_str() + .expect("unexpected getblock result: not a string") + .to_string(), + )); + } + + zebrad.kill(true)?; + + // Sleep for a few seconds to make sure zebrad releases lock on cached state directory + std::thread::sleep(Duration::from_secs(3)); + + let zebrad_state_path = test_type + .zebrad_state_path(test_name) + .expect("already checked that there is a cached state path"); + + let Height(finalized_tip_height) = + load_tip_height_from_state_directory(network, zebrad_state_path.as_ref()).await?; + + tracing::info!( + ?finalized_tip_height, + non_finalized_tip_height = ?tip_height, + ?estimated_finalized_tip_height, + "got finalized tip height from state directory" + ); + + let raw_future_blocks = raw_blocks + .into_iter() + .filter_map(|(height, raw_block)| height.gt(&finalized_tip_height).then_some(raw_block)) + .collect(); + + Ok(raw_future_blocks) +} diff --git a/zebrad/tests/common/config.rs b/zebrad/tests/common/config.rs index e4d6712a7eb..637a479affc 100644 --- a/zebrad/tests/common/config.rs +++ b/zebrad/tests/common/config.rs @@ -7,6 +7,7 @@ use std::{ env, + net::SocketAddr, path::{Path, PathBuf}, time::Duration, }; @@ -14,6 +15,7 @@ use std::{ use color_eyre::eyre::Result; use tempfile::TempDir; +use zebra_test::net::random_known_port; use zebrad::{ components::{mempool, sync, tracing}, config::ZebradConfig, @@ -95,3 +97,27 @@ pub fn config_file_full_path(config_file: PathBuf) -> PathBuf { let path = configs_dir().join(config_file); Path::new(&path).into() } + +/// Returns a `zebrad` config with a random known RPC port. +/// +/// Set `parallel_cpu_threads` to true to auto-configure based on the number of CPU cores. +pub fn random_known_rpc_port_config(parallel_cpu_threads: bool) -> Result { + // [Note on port conflict](#Note on port conflict) + let listen_port = random_known_port(); + let listen_ip = "127.0.0.1".parse().expect("hard-coded IP is valid"); + let zebra_rpc_listener = SocketAddr::new(listen_ip, listen_port); + + // Write a configuration that has the rpc listen_addr option set + // TODO: split this config into another function? + let mut config = default_test_config()?; + config.rpc.listen_addr = Some(zebra_rpc_listener); + if parallel_cpu_threads { + // Auto-configure to the number of CPU cores: most users configre this + config.rpc.parallel_cpu_threads = 0; + } else { + // Default config, users who want to detect port conflicts configure this + config.rpc.parallel_cpu_threads = 1; + } + + Ok(config) +} diff --git a/zebrad/tests/common/get_block_template_rpcs.rs b/zebrad/tests/common/get_block_template_rpcs.rs index 716c91005c0..03c046814ee 100644 --- a/zebrad/tests/common/get_block_template_rpcs.rs +++ b/zebrad/tests/common/get_block_template_rpcs.rs @@ -1,5 +1,3 @@ //! Acceptance tests for getblocktemplate RPC methods in Zebra. -use super::*; - pub(crate) mod submit_block; diff --git a/zebrad/tests/common/get_block_template_rpcs/submit_block.rs b/zebrad/tests/common/get_block_template_rpcs/submit_block.rs index 6f07f0dae5a..657816dde0f 100644 --- a/zebrad/tests/common/get_block_template_rpcs/submit_block.rs +++ b/zebrad/tests/common/get_block_template_rpcs/submit_block.rs @@ -1,170 +1,92 @@ //! Test submitblock RPC method. //! -//! This test requires a cached chain state that is synchronized past the max checkpoint height, -//! and will sync to the next block without updating the cached chain state. - -// TODO: Update this test and the doc to: -// -// This test requires a cached chain state that is partially synchronized close to the -// network chain tip height, and will finish the sync and update the cached chain state. -// -// After finishing the sync, it will get the first 20 blocks in the non-finalized state -// (past the MAX_BLOCK_REORG_HEIGHT) via getblock rpc calls, get the finalized tip height -// of the updated cached state, restart zebra without peers, and submit blocks above the -// finalized tip height. - -use std::path::PathBuf; +//! This test requires a cached chain state that is partially synchronized close to the +//! network chain tip height. It will finish the sync and update the cached chain state. +//! +//! After finishing the sync, it will get the first few blocks in the non-finalized state +//! (past the MAX_BLOCK_REORG_HEIGHT) via getblock rpc calls, get the finalized tip height +//! of the updated cached state, restart zebra without peers, and submit blocks above the +//! finalized tip height. -use color_eyre::eyre::{eyre, Context, Result}; +use color_eyre::eyre::{Context, Result}; -use futures::TryFutureExt; -use indexmap::IndexSet; use reqwest::Client; -use tower::{Service, ServiceExt}; -use zebra_chain::{block::Height, parameters::Network, serialization::ZcashSerialize}; -use zebra_state::HashOrHeight; -use zebra_test::args; +use zebra_chain::parameters::Network; use crate::common::{ - cached_state::{copy_state_directory, start_state_service_with_cache_dir}, - config::{persistent_test_config, testdir}, - launch::ZebradTestDirExt, - lightwalletd::random_known_rpc_port_config, + cached_state::get_raw_future_blocks, + launch::{can_spawn_zebrad_for_rpc, spawn_zebrad_for_rpc}, + test_type::TestType, }; -use super::cached_state::{load_tip_height_from_state_directory, ZEBRA_CACHED_STATE_DIR}; +/// Number of blocks past the finalized to retrieve and submit. +const MAX_NUM_FUTURE_BLOCKS: u32 = 3; -async fn get_future_block_hex_data( - network: Network, - zebrad_state_path: &PathBuf, -) -> Result> { - tracing::info!( - ?zebrad_state_path, - "getting cached sync height from ZEBRA_CACHED_STATE_DIR path" - ); +#[allow(clippy::print_stderr)] +pub(crate) async fn run() -> Result<()> { + let _init_guard = zebra_test::init(); - let cached_sync_height = - load_tip_height_from_state_directory(network, zebrad_state_path.as_ref()).await?; + // We want a zebra state dir in place, + let test_type = TestType::UpdateZebraCachedStateWithRpc; + let test_name = "submit_block_test"; + let network = Network::Mainnet; - let future_block_height = Height(cached_sync_height.0 + 1); + // Skip the test unless the user specifically asked for it and there is a zebrad_state_path + if !can_spawn_zebrad_for_rpc(test_name, test_type) { + return Ok(()); + } tracing::info!( - ?cached_sync_height, - ?future_block_height, - "got cached sync height, copying state dir to tempdir" + ?network, + ?test_type, + "running submitblock test using zebrad", ); - let copied_state_path = copy_state_directory(network, &zebrad_state_path).await?; - - let mut config = persistent_test_config()?; - config.state.debug_stop_at_height = Some(future_block_height.0); + let raw_blocks: Vec = + get_raw_future_blocks(network, test_type, test_name, MAX_NUM_FUTURE_BLOCKS).await?; - let mut child = copied_state_path - .with_config(&mut config)? - .spawn_child(args!["start"])? - .bypass_test_capture(true); + tracing::info!("got raw future blocks, spawning isolated zebrad...",); - while child.is_running() { - tokio::task::yield_now().await; - } + // Start zebrad with no peers, we run the rest of the submitblock test without syncing. + let should_sync = false; + let (mut zebrad, zebra_rpc_address) = + spawn_zebrad_for_rpc(network, test_name, test_type, should_sync)? + .expect("Already checked zebra state path with can_spawn_zebrad_for_rpc"); - let _ = child.kill(true); - let copied_state_path = child.dir.take().unwrap(); - - let (_read_write_state_service, mut state, _latest_chain_tip, _chain_tip_change) = - start_state_service_with_cache_dir(network, copied_state_path.as_ref()).await?; - let request = zebra_state::ReadRequest::Block(HashOrHeight::Height(future_block_height)); - - let response = state - .ready() - .and_then(|ready_service| ready_service.call(request)) - .map_err(|error| eyre!(error)) - .await?; - - let block_hex_data = match response { - zebra_state::ReadResponse::Block(Some(block)) => { - hex::encode(block.zcash_serialize_to_vec()?) - } - zebra_state::ReadResponse::Block(None) => { - tracing::info!( - "Reached the end of the finalized chain, state is missing block at {future_block_height:?}", - ); - return Ok(None); - } - _ => unreachable!("Incorrect response from state service: {response:?}"), - }; - - Ok(Some(block_hex_data)) -} + let rpc_address = zebra_rpc_address.expect("submitblock test must have RPC port"); -#[allow(clippy::print_stderr)] -pub(crate) async fn run() -> Result<(), color_eyre::Report> { - let _init_guard = zebra_test::init(); + tracing::info!( + ?test_type, + ?rpc_address, + "spawned isolated zebrad with shorter chain, waiting for zebrad to open its RPC port..." + ); + zebrad.expect_stdout_line_matches(&format!("Opened RPC endpoint at {rpc_address}"))?; - let mut config = random_known_rpc_port_config(true)?; - let network = config.network.network; - let rpc_address = config.rpc.listen_addr.unwrap(); - - config.state.cache_dir = match std::env::var_os(ZEBRA_CACHED_STATE_DIR) { - Some(path) => path.into(), - None => { - eprintln!( - "skipped submitblock test, \ - set the {ZEBRA_CACHED_STATE_DIR:?} environment variable to run the test", - ); - - return Ok(()); - } - }; - - // TODO: As part of or as a pre-cursor to issue #5015, - // - Use only original cached state, - // - sync until the tip - // - get first 3 blocks in non-finalized state via getblock rpc calls - // - restart zebra without peers - // - submit block(s) above the finalized tip height - let block_hex_data = get_future_block_hex_data(network, &config.state.cache_dir) - .await? - .expect( - "spawned zebrad in get_future_block_hex_data should live until it gets the next block", - ); - - // Runs the rest of this test without an internet connection - config.network.initial_mainnet_peers = IndexSet::new(); - config.network.initial_testnet_peers = IndexSet::new(); - config.mempool.debug_enable_at_height = Some(0); - - // We're using the cached state - config.state.ephemeral = false; - - let mut child = testdir()? - .with_exact_config(&config)? - .spawn_child(args!["start"])? - .bypass_test_capture(true); - - child.expect_stdout_line_matches(&format!("Opened RPC endpoint at {rpc_address}"))?; + tracing::info!(?rpc_address, "zebrad opened its RPC port",); // Create an http client let client = Client::new(); - let res = client - .post(format!("http://{}", &rpc_address)) - .body(format!( - r#"{{"jsonrpc": "2.0", "method": "submitblock", "params": ["{block_hex_data}"], "id":123 }}"# - )) - .header("Content-Type", "application/json") - .send() - .await?; - - assert!(res.status().is_success()); - let res_text = res.text().await?; - - // Test rpc endpoint response - assert!(res_text.contains(r#""result":"null""#)); + for raw_block in raw_blocks { + let res = client + .post(format!("http://{}", &rpc_address)) + .body(format!( + r#"{{"jsonrpc": "2.0", "method": "submitblock", "params": ["{raw_block}"], "id":123 }}"# + )) + .header("Content-Type", "application/json") + .send() + .await?; + + assert!(res.status().is_success()); + let res_text = res.text().await?; + + // Test rpc endpoint response + assert!(res_text.contains(r#""result":null"#)); + } - child.kill(false)?; + zebrad.kill(false)?; - let output = child.wait_with_output()?; + let output = zebrad.wait_with_output()?; let output = output.assert_failure()?; // [Note on port conflict](#Note on port conflict) diff --git a/zebrad/tests/common/launch.rs b/zebrad/tests/common/launch.rs index 82b21ff461c..ac8590ccba1 100644 --- a/zebrad/tests/common/launch.rs +++ b/zebrad/tests/common/launch.rs @@ -25,9 +25,8 @@ use zebra_test::{ use zebrad::config::ZebradConfig; use crate::common::{ - config::testdir, - lightwalletd::{zebra_skip_lightwalletd_tests, LightwalletdTestType}, - sync::FINISH_PARTIAL_SYNC_TIMEOUT, + config::testdir, lightwalletd::zebra_skip_lightwalletd_tests, + sync::FINISH_PARTIAL_SYNC_TIMEOUT, test_type::TestType, }; /// After we launch `zebrad`, wait this long for the command to start up, @@ -213,7 +212,7 @@ where pub fn spawn_zebrad_for_rpc + std::fmt::Debug>( network: Network, test_name: S, - test_type: LightwalletdTestType, + test_type: TestType, use_internet_connection: bool, ) -> Result, Option)>> { let test_name = test_name.as_ref(); @@ -255,7 +254,7 @@ pub fn spawn_zebrad_for_rpc + std::fmt::Debug>( #[tracing::instrument] pub fn can_spawn_zebrad_for_rpc + std::fmt::Debug>( test_name: S, - test_type: LightwalletdTestType, + test_type: TestType, ) -> bool { if zebra_test::net::zebra_skip_network_tests() { return false; diff --git a/zebrad/tests/common/lightwalletd.rs b/zebrad/tests/common/lightwalletd.rs index d1301aa9742..b8f60326942 100644 --- a/zebrad/tests/common/lightwalletd.rs +++ b/zebrad/tests/common/lightwalletd.rs @@ -9,7 +9,6 @@ use std::{ env, net::SocketAddr, path::{Path, PathBuf}, - time::Duration, }; use tempfile::TempDir; @@ -17,26 +16,12 @@ use tempfile::TempDir; use zebra_chain::parameters::Network::{self, *}; use zebra_test::{ args, - command::{Arguments, TestChild, TestDirExt, NO_MATCHES_REGEX_ITER}, + command::{Arguments, TestChild, TestDirExt}, net::random_known_port, prelude::*, }; -use zebrad::config::ZebradConfig; - -use super::{ - cached_state::ZEBRA_CACHED_STATE_DIR, - config::{default_test_config, testdir}, - failure_messages::{ - LIGHTWALLETD_EMPTY_ZEBRA_STATE_IGNORE_MESSAGES, LIGHTWALLETD_FAILURE_MESSAGES, - PROCESS_FAILURE_MESSAGES, ZEBRA_FAILURE_MESSAGES, - }, - launch::{ - ZebradTestDirExt, LIGHTWALLETD_DELAY, LIGHTWALLETD_FULL_SYNC_TIP_DELAY, - LIGHTWALLETD_UPDATE_TIP_DELAY, - }, -}; -use LightwalletdTestType::*; +use super::{config::testdir, launch::ZebradTestDirExt, test_type::TestType}; #[cfg(feature = "lightwalletd-grpc-tests")] pub mod send_transaction_test; @@ -60,7 +45,7 @@ pub const ZEBRA_TEST_LIGHTWALLETD: &str = "ZEBRA_TEST_LIGHTWALLETD"; /// Optional environment variable with the cached state for lightwalletd. /// -/// Required for [`LightwalletdTestType::UpdateCachedState`], +/// Required for [`TestType::UpdateCachedState`], /// so we can test lightwalletd RPC integration with a populated state. /// /// Can also be used to speed up the [`sending_transactions_using_lightwalletd`] test, @@ -88,30 +73,6 @@ pub fn zebra_skip_lightwalletd_tests() -> bool { false } -/// Returns a `zebrad` config with a random known RPC port. -/// -/// Set `parallel_cpu_threads` to true to auto-configure based on the number of CPU cores. -pub fn random_known_rpc_port_config(parallel_cpu_threads: bool) -> Result { - // [Note on port conflict](#Note on port conflict) - let listen_port = random_known_port(); - let listen_ip = "127.0.0.1".parse().expect("hard-coded IP is valid"); - let zebra_rpc_listener = SocketAddr::new(listen_ip, listen_port); - - // Write a configuration that has the rpc listen_addr option set - // TODO: split this config into another function? - let mut config = default_test_config()?; - config.rpc.listen_addr = Some(zebra_rpc_listener); - if parallel_cpu_threads { - // Auto-configure to the number of CPU cores: most users configre this - config.rpc.parallel_cpu_threads = 0; - } else { - // Default config, users who want to detect port conflicts configure this - config.rpc.parallel_cpu_threads = 1; - } - - Ok(config) -} - /// Spawns a lightwalletd instance on `network`, connected to `zebrad_rpc_address`, /// with its gRPC server functionality enabled. /// @@ -126,7 +87,7 @@ pub fn random_known_rpc_port_config(parallel_cpu_threads: bool) -> Result + std::fmt::Debug>( network: Network, test_name: S, - test_type: LightwalletdTestType, + test_type: TestType, zebrad_rpc_address: SocketAddr, ) -> Result, u16)>> { assert_eq!(network, Mainnet, "this test only supports Mainnet for now"); @@ -165,7 +126,7 @@ pub fn spawn_lightwalletd_for_rpc + std::fmt::Debug>( #[tracing::instrument] pub fn can_spawn_lightwalletd_for_rpc + std::fmt::Debug>( test_name: S, - test_type: LightwalletdTestType, + test_type: TestType, ) -> bool { if zebra_test::net::zebra_skip_network_tests() { return false; @@ -304,278 +265,3 @@ where Ok(self) } } - -/// The type of lightwalletd integration test that we're running. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum LightwalletdTestType { - /// Launch with an empty Zebra and lightwalletd state. - LaunchWithEmptyState, - - /// Do a full sync from an empty lightwalletd state. - /// - /// This test requires a cached Zebra state. - // - // Only used with `--features=lightwalletd-grpc-tests`. - #[allow(dead_code)] - FullSyncFromGenesis { - /// Should the test allow a cached lightwalletd state? - /// - /// If `false`, the test fails if the lightwalletd state is populated. - allow_lightwalletd_cached_state: bool, - }, - - /// Sync to tip from a lightwalletd cached state. - /// - /// This test requires a cached Zebra and lightwalletd state. - UpdateCachedState, - - /// Launch `zebrad` and sync it to the tip, but don't launch `lightwalletd`. - /// - /// If this test fails, the failure is in `zebrad` without RPCs or `lightwalletd`. - /// If it succeeds, but the RPC tests fail, the problem is caused by RPCs or `lightwalletd`. - /// - /// This test requires a cached Zebra state. - UpdateZebraCachedStateNoRpc, -} - -impl LightwalletdTestType { - /// Does this test need a Zebra cached state? - pub fn needs_zebra_cached_state(&self) -> bool { - // Handle the Zebra state directory based on the test type: - // - LaunchWithEmptyState: ignore the state directory - // - FullSyncFromGenesis, UpdateCachedState, UpdateZebraCachedStateNoRpc: - // skip the test if it is not available - match self { - LaunchWithEmptyState => false, - FullSyncFromGenesis { .. } | UpdateCachedState | UpdateZebraCachedStateNoRpc => true, - } - } - - /// Does this test launch `lightwalletd`? - pub fn launches_lightwalletd(&self) -> bool { - match self { - UpdateZebraCachedStateNoRpc => false, - LaunchWithEmptyState | FullSyncFromGenesis { .. } | UpdateCachedState => true, - } - } - - /// Does this test need a `lightwalletd` cached state? - pub fn needs_lightwalletd_cached_state(&self) -> bool { - // Handle the lightwalletd state directory based on the test type: - // - LaunchWithEmptyState, UpdateZebraCachedStateNoRpc: ignore the state directory - // - FullSyncFromGenesis: use it if available, timeout if it is already populated - // - UpdateCachedState: skip the test if it is not available - match self { - LaunchWithEmptyState | FullSyncFromGenesis { .. } | UpdateZebraCachedStateNoRpc => { - false - } - UpdateCachedState => true, - } - } - - /// Does this test allow a `lightwalletd` cached state, even if it is not required? - pub fn allow_lightwalletd_cached_state(&self) -> bool { - match self { - LaunchWithEmptyState => false, - FullSyncFromGenesis { - allow_lightwalletd_cached_state, - } => *allow_lightwalletd_cached_state, - UpdateCachedState | UpdateZebraCachedStateNoRpc => true, - } - } - - /// Can this test create a new `LIGHTWALLETD_DATA_DIR` cached state? - pub fn can_create_lightwalletd_cached_state(&self) -> bool { - match self { - LaunchWithEmptyState => false, - FullSyncFromGenesis { .. } | UpdateCachedState => true, - UpdateZebraCachedStateNoRpc => false, - } - } - - /// Returns the Zebra state path for this test, if set. - #[allow(clippy::print_stderr)] - pub fn zebrad_state_path>(&self, test_name: S) -> Option { - match env::var_os(ZEBRA_CACHED_STATE_DIR) { - Some(path) => Some(path.into()), - None => { - let test_name = test_name.as_ref(); - eprintln!( - "skipped {test_name:?} {self:?} lightwalletd test, \ - set the {ZEBRA_CACHED_STATE_DIR:?} environment variable to run the test", - ); - - None - } - } - } - - /// Returns a Zebra config for this test. - /// - /// Returns `None` if the test should be skipped, - /// and `Some(Err(_))` if the config could not be created. - pub fn zebrad_config>(&self, test_name: S) -> Option> { - let config = if self.launches_lightwalletd() { - // This is what we recommend our users configure. - random_known_rpc_port_config(true) - } else { - default_test_config() - }; - - let mut config = match config { - Ok(config) => config, - Err(error) => return Some(Err(error)), - }; - - // We want to preload the consensus parameters, - // except when we're doing the quick empty state test - config.consensus.debug_skip_parameter_preload = !self.needs_zebra_cached_state(); - - // We want to run multi-threaded RPCs, if we're using them - if self.launches_lightwalletd() { - // Automatically runs one thread per available CPU core - config.rpc.parallel_cpu_threads = 0; - } - - if !self.needs_zebra_cached_state() { - return Some(Ok(config)); - } - - let zebra_state_path = self.zebrad_state_path(test_name)?; - - config.sync.checkpoint_verify_concurrency_limit = - zebrad::components::sync::DEFAULT_CHECKPOINT_CONCURRENCY_LIMIT; - - config.state.ephemeral = false; - config.state.cache_dir = zebra_state_path; - - Some(Ok(config)) - } - - /// Returns the `lightwalletd` state path for this test, if set, and if allowed for this test. - pub fn lightwalletd_state_path>(&self, test_name: S) -> Option { - let test_name = test_name.as_ref(); - - // Can this test type use a lwd cached state, or create/update one? - let use_or_create_lwd_cache = - self.allow_lightwalletd_cached_state() || self.can_create_lightwalletd_cached_state(); - - if !self.launches_lightwalletd() || !use_or_create_lwd_cache { - tracing::info!( - "running {test_name:?} {self:?} lightwalletd test, \ - ignoring any cached state in the {LIGHTWALLETD_DATA_DIR:?} environment variable", - ); - - return None; - } - - match env::var_os(LIGHTWALLETD_DATA_DIR) { - Some(path) => Some(path.into()), - None => { - if self.needs_lightwalletd_cached_state() { - tracing::info!( - "skipped {test_name:?} {self:?} lightwalletd test, \ - set the {LIGHTWALLETD_DATA_DIR:?} environment variable to run the test", - ); - } else if self.allow_lightwalletd_cached_state() { - tracing::info!( - "running {test_name:?} {self:?} lightwalletd test without cached state, \ - set the {LIGHTWALLETD_DATA_DIR:?} environment variable to run with cached state", - ); - } - - None - } - } - } - - /// Returns the `zebrad` timeout for this test type. - pub fn zebrad_timeout(&self) -> Duration { - match self { - LaunchWithEmptyState => LIGHTWALLETD_DELAY, - FullSyncFromGenesis { .. } => LIGHTWALLETD_FULL_SYNC_TIP_DELAY, - UpdateCachedState | UpdateZebraCachedStateNoRpc => LIGHTWALLETD_UPDATE_TIP_DELAY, - } - } - - /// Returns the `lightwalletd` timeout for this test type. - #[track_caller] - pub fn lightwalletd_timeout(&self) -> Duration { - if !self.launches_lightwalletd() { - panic!("lightwalletd must not be launched in the {self:?} test"); - } - - // We use the same timeouts for zebrad and lightwalletd, - // because the tests check zebrad and lightwalletd concurrently. - match self { - LaunchWithEmptyState => LIGHTWALLETD_DELAY, - FullSyncFromGenesis { .. } => LIGHTWALLETD_FULL_SYNC_TIP_DELAY, - UpdateCachedState | UpdateZebraCachedStateNoRpc => LIGHTWALLETD_UPDATE_TIP_DELAY, - } - } - - /// Returns Zebra log regexes that indicate the tests have failed, - /// and regexes of any failures that should be ignored. - pub fn zebrad_failure_messages(&self) -> (Vec, Vec) { - let mut zebrad_failure_messages: Vec = ZEBRA_FAILURE_MESSAGES - .iter() - .chain(PROCESS_FAILURE_MESSAGES) - .map(ToString::to_string) - .collect(); - - if self.needs_zebra_cached_state() { - // Fail if we need a cached Zebra state, but it's empty - zebrad_failure_messages.push("loaded Zebra state cache .*tip.*=.*None".to_string()); - } - if *self == LaunchWithEmptyState { - // Fail if we need an empty Zebra state, but it has blocks - zebrad_failure_messages - .push(r"loaded Zebra state cache .*tip.*=.*Height\([1-9][0-9]*\)".to_string()); - } - - let zebrad_ignore_messages = Vec::new(); - - (zebrad_failure_messages, zebrad_ignore_messages) - } - - /// Returns `lightwalletd` log regexes that indicate the tests have failed, - /// and regexes of any failures that should be ignored. - #[track_caller] - pub fn lightwalletd_failure_messages(&self) -> (Vec, Vec) { - if !self.launches_lightwalletd() { - panic!("lightwalletd must not be launched in the {self:?} test"); - } - - let mut lightwalletd_failure_messages: Vec = LIGHTWALLETD_FAILURE_MESSAGES - .iter() - .chain(PROCESS_FAILURE_MESSAGES) - .map(ToString::to_string) - .collect(); - - // Zebra state failures - if self.needs_zebra_cached_state() { - // Fail if we need a cached Zebra state, but it's empty - lightwalletd_failure_messages.push("No Chain tip available yet".to_string()); - } - - // lightwalletd state failures - if self.needs_lightwalletd_cached_state() { - // Fail if we need a cached lightwalletd state, but it isn't near the tip - lightwalletd_failure_messages.push("Found [0-9]{1,6} blocks in cache".to_string()); - } - if !self.allow_lightwalletd_cached_state() { - // Fail if we need an empty lightwalletd state, but it has blocks - lightwalletd_failure_messages.push("Found [1-9][0-9]* blocks in cache".to_string()); - } - - let lightwalletd_ignore_messages = if *self == LaunchWithEmptyState { - LIGHTWALLETD_EMPTY_ZEBRA_STATE_IGNORE_MESSAGES.iter() - } else { - NO_MATCHES_REGEX_ITER.iter() - } - .map(ToString::to_string) - .collect(); - - (lightwalletd_failure_messages, lightwalletd_ignore_messages) - } -} diff --git a/zebrad/tests/common/lightwalletd/send_transaction_test.rs b/zebrad/tests/common/lightwalletd/send_transaction_test.rs index 50aabf0b689..ac6a9d6e7e6 100644 --- a/zebrad/tests/common/lightwalletd/send_transaction_test.rs +++ b/zebrad/tests/common/lightwalletd/send_transaction_test.rs @@ -1,49 +1,42 @@ //! Test sending transactions using a lightwalletd instance connected to a zebrad instance. //! -//! This test requires a cached chain state that is partially synchronized, i.e., it should be a -//! few blocks below the network chain tip height. We open this state during the test, but we don't -//! add any blocks to it. +//! This test requires a cached chain state that is partially synchronized close to the +//! network chain tip height. It will finish the sync and update the cached chain state. //! -//! The transactions to use to send are obtained from the blocks synchronized by a temporary zebrad -//! instance that are higher than the chain tip of the cached state. This instance uses a copy of -//! the state. +//! After finishing the sync, it will get the first 20 blocks in the non-finalized state +//! (past the MAX_BLOCK_REORG_HEIGHT) via getblock rpc calls, shuts down the zebrad instance +//! so that the retrieved blocks aren't finalized into the cached state, and get the finalized +//! tip height of the updated cached state. +//! +//! The transactions to use to send are obtained from those blocks that are above the finalized +//! tip height of the updated cached state. //! //! The zebrad instance connected to lightwalletd uses the cached state and does not connect to any //! external peers, which prevents it from downloading the blocks from where the test transactions //! were obtained. This is to ensure that zebra does not reject the transactions because they have //! already been seen in a block. -use std::{ - cmp::min, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{cmp::min, sync::Arc}; -use color_eyre::eyre::{eyre, Result}; -use futures::TryFutureExt; -use tower::{Service, ServiceExt}; +use color_eyre::eyre::Result; use zebra_chain::{ - block, - chain_tip::ChainTip, parameters::Network::{self, *}, serialization::ZcashSerialize, transaction::{self, Transaction}, }; use zebra_rpc::queue::CHANNEL_AND_QUEUE_CAPACITY; -use zebra_state::HashOrHeight; use zebrad::components::mempool::downloads::MAX_INBOUND_CONCURRENCY; use crate::common::{ - cached_state::{load_tip_height_from_state_directory, start_state_service_with_cache_dir}, + cached_state::get_future_blocks, launch::{can_spawn_zebrad_for_rpc, spawn_zebrad_for_rpc}, lightwalletd::{ can_spawn_lightwalletd_for_rpc, spawn_lightwalletd_for_rpc, sync::wait_for_zebrad_and_lightwalletd_sync, wallet_grpc::{self, connect_to_lightwalletd, Empty, Exclude}, - LightwalletdTestType::*, }, - sync::copy_state_and_perform_full_sync, + test_type::TestType::{self, *}, }; /// The maximum number of transactions we want to send in the test. @@ -55,6 +48,9 @@ fn max_sent_transactions() -> usize { min(CHANNEL_AND_QUEUE_CAPACITY, MAX_INBOUND_CONCURRENCY) - 1 } +/// Number of blocks past the finalized to load transactions from. +const MAX_NUM_FUTURE_BLOCKS: u32 = 50; + /// The test entry point. // // TODO: @@ -94,7 +90,7 @@ pub async fn run() -> Result<()> { ); let mut transactions = - load_transactions_from_future_blocks(network, zebrad_state_path.clone()).await?; + load_transactions_from_future_blocks(network, test_type, test_name).await?; tracing::info!( transaction_count = ?transactions.len(), @@ -252,136 +248,31 @@ pub async fn run() -> Result<()> { Ok(()) } -/// Loads transactions from a block that's after the chain tip of the cached state. -/// -/// We copy the cached state to avoid modifying `zebrad_state_path`. -/// This copy is used to launch a `zebrad` instance connected to the network, -/// which finishes synchronizing the chain. -/// Then we load transactions from this updated state. -/// -/// Returns a list of valid transactions that are not in any of the blocks present in the -/// original `zebrad_state_path`. -#[tracing::instrument] -async fn load_transactions_from_future_blocks( - network: Network, - zebrad_state_path: PathBuf, -) -> Result>> { - let partial_sync_height = - load_tip_height_from_state_directory(network, zebrad_state_path.as_ref()).await?; - - tracing::info!( - ?partial_sync_height, - partial_sync_path = ?zebrad_state_path, - "performing full sync...", - ); - - let full_sync_path = - copy_state_and_perform_full_sync(network, zebrad_state_path.as_ref()).await?; - - tracing::info!(?full_sync_path, "loading transactions..."); - - let transactions = - load_transactions_from_block_after(partial_sync_height, network, full_sync_path.as_ref()) - .await?; - - Ok(transactions) -} - -/// Loads transactions from a block that's after the specified `height`. +/// Loads transactions from a few block(s) after the chain tip of the cached state. /// -/// Starts at the block after the block at the specified `height`, and stops when it finds a block -/// from where it can load at least one non-coinbase transaction. +/// Returns a list of non-coinbase transactions from blocks that have not been finalized to disk +/// in the `ZEBRA_CACHED_STATE_DIR`. /// -/// # Panics +/// ## Panics /// -/// If the specified `zebrad_state_path` contains a chain state that's not synchronized to a tip that's -/// after `height`. +/// If the provided `test_type` doesn't need an rpc server and cached state #[tracing::instrument] -async fn load_transactions_from_block_after( - height: block::Height, +async fn load_transactions_from_future_blocks( network: Network, - zebrad_state_path: &Path, + test_type: TestType, + test_name: &str, ) -> Result>> { - let (_read_write_state_service, mut state, latest_chain_tip, _chain_tip_change) = - start_state_service_with_cache_dir(network, zebrad_state_path).await?; - - let tip_height = latest_chain_tip - .best_tip_height() - .ok_or_else(|| eyre!("State directory doesn't have a chain tip block"))?; - - assert!( - tip_height > height, - "Chain not synchronized to a block after the specified height", - ); - - let mut target_height = height.0; - let mut transactions = Vec::new(); - - while transactions.len() < max_sent_transactions() { - let new_transactions = - load_transactions_from_block(block::Height(target_height), &mut state).await?; - - if let Some(mut new_transactions) = new_transactions { - new_transactions.retain(|transaction| !transaction.is_coinbase()); - transactions.append(&mut new_transactions); - } else { - tracing::info!( - "Reached the end of the finalized chain\n\ - collected {} transactions from {} blocks before {target_height:?}", - transactions.len(), - target_height - height.0 - 1, - ); - break; - } - - target_height += 1; - } - - tracing::info!( - "Collected {} transactions from {} blocks before {target_height:?}", - transactions.len(), - target_height - height.0 - 1, - ); + let transactions = get_future_blocks(network, test_type, test_name, MAX_NUM_FUTURE_BLOCKS) + .await? + .into_iter() + .flat_map(|block| block.transactions) + .filter(|transaction| !transaction.is_coinbase()) + .take(max_sent_transactions()) + .collect(); Ok(transactions) } -/// Performs a request to the provided read-only `state` service to fetch all transactions from a -/// block at the specified `height`. -#[tracing::instrument(skip(state))] -async fn load_transactions_from_block( - height: block::Height, - state: &mut ReadStateService, -) -> Result>>> -where - ReadStateService: Service< - zebra_state::ReadRequest, - Response = zebra_state::ReadResponse, - Error = zebra_state::BoxError, - >, -{ - let request = zebra_state::ReadRequest::Block(HashOrHeight::Height(height)); - - let response = state - .ready() - .and_then(|ready_service| ready_service.call(request)) - .map_err(|error| eyre!(error)) - .await?; - - let block = match response { - zebra_state::ReadResponse::Block(Some(block)) => block, - zebra_state::ReadResponse::Block(None) => { - tracing::info!( - "Reached the end of the finalized chain, state is missing block at {height:?}", - ); - return Ok(None); - } - _ => unreachable!("Incorrect response from state service: {response:?}"), - }; - - Ok(Some(block.transactions.to_vec())) -} - /// Prepare a request to send to lightwalletd that contains a transaction to be sent. fn prepare_send_transaction_request(transaction: Arc) -> wallet_grpc::RawTransaction { let transaction_bytes = transaction.zcash_serialize_to_vec().unwrap(); diff --git a/zebrad/tests/common/lightwalletd/sync.rs b/zebrad/tests/common/lightwalletd/sync.rs index 5808ed2be14..82ebc031161 100644 --- a/zebrad/tests/common/lightwalletd/sync.rs +++ b/zebrad/tests/common/lightwalletd/sync.rs @@ -12,10 +12,8 @@ use zebra_test::prelude::*; use crate::common::{ launch::ZebradTestDirExt, - lightwalletd::{ - wallet_grpc::{connect_to_lightwalletd, ChainSpec}, - LightwalletdTestType, - }, + lightwalletd::wallet_grpc::{connect_to_lightwalletd, ChainSpec}, + test_type::TestType, }; /// The amount of time we wait between each tip check. @@ -33,7 +31,7 @@ pub fn wait_for_zebrad_and_lightwalletd_sync< lightwalletd_rpc_port: u16, mut zebrad: TestChild

, zebra_rpc_address: SocketAddr, - test_type: LightwalletdTestType, + test_type: TestType, wait_for_zebrad_mempool: bool, wait_for_zebrad_tip: bool, ) -> Result<(TestChild, TestChild

)> { diff --git a/zebrad/tests/common/lightwalletd/wallet_grpc_test.rs b/zebrad/tests/common/lightwalletd/wallet_grpc_test.rs index 71d76de9350..cf969302eca 100644 --- a/zebrad/tests/common/lightwalletd/wallet_grpc_test.rs +++ b/zebrad/tests/common/lightwalletd/wallet_grpc_test.rs @@ -54,8 +54,8 @@ use crate::common::{ connect_to_lightwalletd, Address, AddressList, BlockId, BlockRange, ChainSpec, Empty, GetAddressUtxosArg, TransparentAddressBlockFilter, TxFilter, }, - LightwalletdTestType::UpdateCachedState, }, + test_type::TestType::UpdateCachedState, }; /// The test entry point. diff --git a/zebrad/tests/common/mod.rs b/zebrad/tests/common/mod.rs index fe318719e55..a1ef1365cb4 100644 --- a/zebrad/tests/common/mod.rs +++ b/zebrad/tests/common/mod.rs @@ -18,3 +18,4 @@ pub mod get_block_template_rpcs; pub mod launch; pub mod lightwalletd; pub mod sync; +pub mod test_type; diff --git a/zebrad/tests/common/sync.rs b/zebrad/tests/common/sync.rs index 735f65749c9..56e4ee7e7e8 100644 --- a/zebrad/tests/common/sync.rs +++ b/zebrad/tests/common/sync.rs @@ -225,29 +225,21 @@ pub fn sync_until( testdir()?.with_config(&mut config)? }; - let mut child = tempdir.spawn_child(args!["start"])?.with_timeout(timeout); + let child = tempdir.spawn_child(args!["start"])?.with_timeout(timeout); - let network = format!("network: {network},"); + let network_log = format!("network: {network},"); if mempool_behavior.require_activation() { // require that the mempool activated, // checking logs as they arrive - child.expect_stdout_line_matches(&network)?; - - if check_legacy_chain { - child.expect_stdout_line_matches("starting legacy chain check")?; - child.expect_stdout_line_matches("no legacy chain found")?; - } - - // before the stop regex, expect mempool activation - if mempool_behavior.require_forced_activation() { - child.expect_stdout_line_matches("enabling mempool for debugging")?; - } - child.expect_stdout_line_matches("activating mempool")?; - - // then wait for the stop log, which must happen after the mempool becomes active - child.expect_stdout_line_matches(stop_regex)?; + let mut child = check_sync_logs_until( + child, + network, + stop_regex, + mempool_behavior, + check_legacy_chain, + )?; // make sure the child process is dead // if it has already exited, ignore that error @@ -271,7 +263,7 @@ pub fn sync_until( ); let output = child.wait_with_output()?; - output.stdout_line_contains(&network)?; + output.stdout_line_contains(&network_log)?; if check_legacy_chain { output.stdout_line_contains("starting legacy chain check")?; @@ -295,6 +287,47 @@ pub fn sync_until( } } +/// Check sync logs on `network` until `zebrad` logs `stop_regex`. +/// +/// ## Test Settings +/// +/// Checks the logs for the expected `mempool_behavior`. +/// +/// If `check_legacy_chain` is true, make sure the logs contain the legacy chain check. +/// +/// ## Test Status +/// +/// Returns the provided `zebrad` [`TestChild`] when `stop_regex` is encountered. +/// +/// Returns an error if the child exits or `timeout` elapses before `stop_regex` is found. +#[tracing::instrument(skip(zebrad))] +pub fn check_sync_logs_until( + mut zebrad: TestChild, + network: Network, + stop_regex: &str, + // Test Settings + mempool_behavior: MempoolBehavior, + check_legacy_chain: bool, +) -> Result> { + zebrad.expect_stdout_line_matches(&format!("network: {network},"))?; + + if check_legacy_chain { + zebrad.expect_stdout_line_matches("starting legacy chain check")?; + zebrad.expect_stdout_line_matches("no legacy chain found")?; + } + + // before the stop regex, expect mempool activation + if mempool_behavior.require_forced_activation() { + zebrad.expect_stdout_line_matches("enabling mempool for debugging")?; + } + zebrad.expect_stdout_line_matches("activating mempool")?; + + // then wait for the stop log, which must happen after the mempool becomes active + zebrad.expect_stdout_line_matches(stop_regex)?; + + Ok(zebrad) +} + /// Runs a zebrad instance to synchronize the chain to the network tip. /// /// The zebrad instance is executed on a copy of the partially synchronized chain state. This copy diff --git a/zebrad/tests/common/test_type.rs b/zebrad/tests/common/test_type.rs new file mode 100644 index 00000000000..4f03f86e90a --- /dev/null +++ b/zebrad/tests/common/test_type.rs @@ -0,0 +1,319 @@ +//! Provides TestType enum with shared code for acceptance tests + +use std::{env, path::PathBuf, time::Duration}; + +use zebra_test::{command::NO_MATCHES_REGEX_ITER, prelude::*}; +use zebrad::config::ZebradConfig; + +use super::{ + cached_state::ZEBRA_CACHED_STATE_DIR, + config::{default_test_config, random_known_rpc_port_config}, + failure_messages::{ + LIGHTWALLETD_EMPTY_ZEBRA_STATE_IGNORE_MESSAGES, LIGHTWALLETD_FAILURE_MESSAGES, + PROCESS_FAILURE_MESSAGES, ZEBRA_FAILURE_MESSAGES, + }, + launch::{LIGHTWALLETD_DELAY, LIGHTWALLETD_FULL_SYNC_TIP_DELAY, LIGHTWALLETD_UPDATE_TIP_DELAY}, + lightwalletd::LIGHTWALLETD_DATA_DIR, + sync::FINISH_PARTIAL_SYNC_TIMEOUT, +}; + +use TestType::*; + +/// The type of integration test that we're running. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum TestType { + /// Launch with an empty Zebra and lightwalletd state. + LaunchWithEmptyState, + + /// Do a full sync from an empty lightwalletd state. + /// + /// This test requires a cached Zebra state. + // + // Only used with `--features=lightwalletd-grpc-tests`. + #[allow(dead_code)] + FullSyncFromGenesis { + /// Should the test allow a cached lightwalletd state? + /// + /// If `false`, the test fails if the lightwalletd state is populated. + allow_lightwalletd_cached_state: bool, + }, + + /// Sync to tip from a lightwalletd cached state. + /// + /// This test requires a cached Zebra and lightwalletd state. + UpdateCachedState, + + /// Launch `zebrad` and sync it to the tip, but don't launch `lightwalletd`. + /// + /// If this test fails, the failure is in `zebrad` without RPCs or `lightwalletd`. + /// If it succeeds, but the RPC tests fail, the problem is caused by RPCs or `lightwalletd`. + /// + /// This test requires a cached Zebra state. + UpdateZebraCachedStateNoRpc, + + /// Launch `zebrad` and sync it to the tip, but don't launch `lightwalletd`. + /// + /// This test requires a cached Zebra state. + #[allow(dead_code)] + UpdateZebraCachedStateWithRpc, +} + +impl TestType { + /// Does this test need a Zebra cached state? + pub fn needs_zebra_cached_state(&self) -> bool { + // Handle the Zebra state directory based on the test type: + // - LaunchWithEmptyState: ignore the state directory + // - FullSyncFromGenesis, UpdateCachedState, UpdateZebraCachedStateNoRpc: + // skip the test if it is not available + match self { + LaunchWithEmptyState => false, + FullSyncFromGenesis { .. } + | UpdateCachedState + | UpdateZebraCachedStateNoRpc + | UpdateZebraCachedStateWithRpc => true, + } + } + + /// Does this test need a Zebra rpc server? + pub fn needs_zebra_rpc_server(&self) -> bool { + match self { + UpdateZebraCachedStateWithRpc => true, + UpdateZebraCachedStateNoRpc + | LaunchWithEmptyState + | FullSyncFromGenesis { .. } + | UpdateCachedState => self.launches_lightwalletd(), + } + } + + /// Does this test launch `lightwalletd`? + pub fn launches_lightwalletd(&self) -> bool { + match self { + UpdateZebraCachedStateNoRpc | UpdateZebraCachedStateWithRpc => false, + LaunchWithEmptyState | FullSyncFromGenesis { .. } | UpdateCachedState => true, + } + } + + /// Does this test need a `lightwalletd` cached state? + pub fn needs_lightwalletd_cached_state(&self) -> bool { + // Handle the lightwalletd state directory based on the test type: + // - LaunchWithEmptyState, UpdateZebraCachedStateNoRpc: ignore the state directory + // - FullSyncFromGenesis: use it if available, timeout if it is already populated + // - UpdateCachedState: skip the test if it is not available + match self { + LaunchWithEmptyState + | FullSyncFromGenesis { .. } + | UpdateZebraCachedStateNoRpc + | UpdateZebraCachedStateWithRpc => false, + UpdateCachedState => true, + } + } + + /// Does this test allow a `lightwalletd` cached state, even if it is not required? + pub fn allow_lightwalletd_cached_state(&self) -> bool { + match self { + LaunchWithEmptyState => false, + FullSyncFromGenesis { + allow_lightwalletd_cached_state, + } => *allow_lightwalletd_cached_state, + UpdateCachedState | UpdateZebraCachedStateNoRpc | UpdateZebraCachedStateWithRpc => true, + } + } + + /// Can this test create a new `LIGHTWALLETD_DATA_DIR` cached state? + pub fn can_create_lightwalletd_cached_state(&self) -> bool { + match self { + LaunchWithEmptyState => false, + FullSyncFromGenesis { .. } | UpdateCachedState => true, + UpdateZebraCachedStateNoRpc | UpdateZebraCachedStateWithRpc => false, + } + } + + /// Returns the Zebra state path for this test, if set. + #[allow(clippy::print_stderr)] + pub fn zebrad_state_path>(&self, test_name: S) -> Option { + match env::var_os(ZEBRA_CACHED_STATE_DIR) { + Some(path) => Some(path.into()), + None => { + let test_name = test_name.as_ref(); + eprintln!( + "skipped {test_name:?} {self:?} lightwalletd test, \ + set the {ZEBRA_CACHED_STATE_DIR:?} environment variable to run the test", + ); + + None + } + } + } + + /// Returns a Zebra config for this test. + /// + /// Returns `None` if the test should be skipped, + /// and `Some(Err(_))` if the config could not be created. + pub fn zebrad_config>(&self, test_name: S) -> Option> { + let config = if self.needs_zebra_rpc_server() { + // This is what we recommend our users configure. + random_known_rpc_port_config(true) + } else { + default_test_config() + }; + + let mut config = match config { + Ok(config) => config, + Err(error) => return Some(Err(error)), + }; + + // We want to preload the consensus parameters, + // except when we're doing the quick empty state test + config.consensus.debug_skip_parameter_preload = !self.needs_zebra_cached_state(); + + // We want to run multi-threaded RPCs, if we're using them + if self.launches_lightwalletd() { + // Automatically runs one thread per available CPU core + config.rpc.parallel_cpu_threads = 0; + } + + if !self.needs_zebra_cached_state() { + return Some(Ok(config)); + } + + let zebra_state_path = self.zebrad_state_path(test_name)?; + + config.sync.checkpoint_verify_concurrency_limit = + zebrad::components::sync::DEFAULT_CHECKPOINT_CONCURRENCY_LIMIT; + + config.state.ephemeral = false; + config.state.cache_dir = zebra_state_path; + + Some(Ok(config)) + } + + /// Returns the `lightwalletd` state path for this test, if set, and if allowed for this test. + pub fn lightwalletd_state_path>(&self, test_name: S) -> Option { + let test_name = test_name.as_ref(); + + // Can this test type use a lwd cached state, or create/update one? + let use_or_create_lwd_cache = + self.allow_lightwalletd_cached_state() || self.can_create_lightwalletd_cached_state(); + + if !self.launches_lightwalletd() || !use_or_create_lwd_cache { + tracing::info!( + "running {test_name:?} {self:?} lightwalletd test, \ + ignoring any cached state in the {LIGHTWALLETD_DATA_DIR:?} environment variable", + ); + + return None; + } + + match env::var_os(LIGHTWALLETD_DATA_DIR) { + Some(path) => Some(path.into()), + None => { + if self.needs_lightwalletd_cached_state() { + tracing::info!( + "skipped {test_name:?} {self:?} lightwalletd test, \ + set the {LIGHTWALLETD_DATA_DIR:?} environment variable to run the test", + ); + } else if self.allow_lightwalletd_cached_state() { + tracing::info!( + "running {test_name:?} {self:?} lightwalletd test without cached state, \ + set the {LIGHTWALLETD_DATA_DIR:?} environment variable to run with cached state", + ); + } + + None + } + } + } + + /// Returns the `zebrad` timeout for this test type. + pub fn zebrad_timeout(&self) -> Duration { + match self { + LaunchWithEmptyState => LIGHTWALLETD_DELAY, + FullSyncFromGenesis { .. } => LIGHTWALLETD_FULL_SYNC_TIP_DELAY, + UpdateCachedState | UpdateZebraCachedStateNoRpc => LIGHTWALLETD_UPDATE_TIP_DELAY, + UpdateZebraCachedStateWithRpc => FINISH_PARTIAL_SYNC_TIMEOUT, + } + } + + /// Returns the `lightwalletd` timeout for this test type. + #[track_caller] + pub fn lightwalletd_timeout(&self) -> Duration { + if !self.launches_lightwalletd() { + panic!("lightwalletd must not be launched in the {self:?} test"); + } + + // We use the same timeouts for zebrad and lightwalletd, + // because the tests check zebrad and lightwalletd concurrently. + match self { + LaunchWithEmptyState => LIGHTWALLETD_DELAY, + FullSyncFromGenesis { .. } => LIGHTWALLETD_FULL_SYNC_TIP_DELAY, + UpdateCachedState | UpdateZebraCachedStateNoRpc | UpdateZebraCachedStateWithRpc => { + LIGHTWALLETD_UPDATE_TIP_DELAY + } + } + } + + /// Returns Zebra log regexes that indicate the tests have failed, + /// and regexes of any failures that should be ignored. + pub fn zebrad_failure_messages(&self) -> (Vec, Vec) { + let mut zebrad_failure_messages: Vec = ZEBRA_FAILURE_MESSAGES + .iter() + .chain(PROCESS_FAILURE_MESSAGES) + .map(ToString::to_string) + .collect(); + + if self.needs_zebra_cached_state() { + // Fail if we need a cached Zebra state, but it's empty + zebrad_failure_messages.push("loaded Zebra state cache .*tip.*=.*None".to_string()); + } + if *self == LaunchWithEmptyState { + // Fail if we need an empty Zebra state, but it has blocks + zebrad_failure_messages + .push(r"loaded Zebra state cache .*tip.*=.*Height\([1-9][0-9]*\)".to_string()); + } + + let zebrad_ignore_messages = Vec::new(); + + (zebrad_failure_messages, zebrad_ignore_messages) + } + + /// Returns `lightwalletd` log regexes that indicate the tests have failed, + /// and regexes of any failures that should be ignored. + #[track_caller] + pub fn lightwalletd_failure_messages(&self) -> (Vec, Vec) { + if !self.launches_lightwalletd() { + panic!("lightwalletd must not be launched in the {self:?} test"); + } + + let mut lightwalletd_failure_messages: Vec = LIGHTWALLETD_FAILURE_MESSAGES + .iter() + .chain(PROCESS_FAILURE_MESSAGES) + .map(ToString::to_string) + .collect(); + + // Zebra state failures + if self.needs_zebra_cached_state() { + // Fail if we need a cached Zebra state, but it's empty + lightwalletd_failure_messages.push("No Chain tip available yet".to_string()); + } + + // lightwalletd state failures + if self.needs_lightwalletd_cached_state() { + // Fail if we need a cached lightwalletd state, but it isn't near the tip + lightwalletd_failure_messages.push("Found [0-9]{1,6} blocks in cache".to_string()); + } + if !self.allow_lightwalletd_cached_state() { + // Fail if we need an empty lightwalletd state, but it has blocks + lightwalletd_failure_messages.push("Found [1-9][0-9]* blocks in cache".to_string()); + } + + let lightwalletd_ignore_messages = if *self == LaunchWithEmptyState { + LIGHTWALLETD_EMPTY_ZEBRA_STATE_IGNORE_MESSAGES.iter() + } else { + NO_MATCHES_REGEX_ITER.iter() + } + .map(ToString::to_string) + .collect(); + + (lightwalletd_failure_messages, lightwalletd_ignore_messages) + } +}