fix(tests): add submitblock test to CI, and avoid copying the cached state directory in other tests (#5589)

* updates mod docs for tests that use future blocks

* updates submitblock test to use TestType methods

* prunes redundant code

* adds check_sync_logs_until

* adds assertion for needs cached state & rpc server

* updates get_raw_future_blocks fn with rpc calls

* updates to get_raw_future_blocks fn and submit_block test

* Rename LightwalletdTestType to TestType

* moves TestType and random_known_rpc_port_config to test_type.rs and config.rs

* moves get_raw_future_blocks to cached_state.rs

* updates ci workflows to include submit block test

* adds get_future_blocks fn and uses it in load_transactions_from_future_blocks

* updates CI docker

* Apply suggestions from code review

Co-authored-by: teor <teor@riseup.net>

* Applies suggestions from code review

* Updates misnamed closure param

* updates mod docs for test_type.rs

Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Arya 2022-11-09 22:40:21 -05:00 committed by GitHub
parent be24a364da
commit c447b03223
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 706 additions and 636 deletions

View File

@ -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

View File

@ -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'

View File

@ -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.
#

View File

@ -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) =

View File

@ -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<Vec<Block>> {
let blocks: Vec<Block> = 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<Vec<String>> {
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)
}

View File

@ -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<ZebradConfig> {
// [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)
}

View File

@ -1,5 +1,3 @@
//! Acceptance tests for getblocktemplate RPC methods in Zebra.
use super::*;
pub(crate) mod submit_block;

View File

@ -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.
//! 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.
// 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 color_eyre::eyre::{Context, Result};
use std::path::PathBuf;
use color_eyre::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};
async fn get_future_block_hex_data(
network: Network,
zebrad_state_path: &PathBuf,
) -> Result<Option<String>> {
tracing::info!(
?zebrad_state_path,
"getting cached sync height from ZEBRA_CACHED_STATE_DIR path"
);
let cached_sync_height =
load_tip_height_from_state_directory(network, zebrad_state_path.as_ref()).await?;
let future_block_height = Height(cached_sync_height.0 + 1);
tracing::info!(
?cached_sync_height,
?future_block_height,
"got cached sync height, copying state dir to tempdir"
);
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 mut child = copied_state_path
.with_config(&mut config)?
.spawn_child(args!["start"])?
.bypass_test_capture(true);
while child.is_running() {
tokio::task::yield_now().await;
}
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))
}
/// Number of blocks past the finalized to retrieve and submit.
const MAX_NUM_FUTURE_BLOCKS: u32 = 3;
#[allow(clippy::print_stderr)]
pub(crate) async fn run() -> Result<(), color_eyre::Report> {
pub(crate) async fn run() -> Result<()> {
let _init_guard = zebra_test::init();
let mut config = random_known_rpc_port_config(true)?;
let network = config.network.network;
let rpc_address = config.rpc.listen_addr.unwrap();
// We want a zebra state dir in place,
let test_type = TestType::UpdateZebraCachedStateWithRpc;
let test_name = "submit_block_test";
let network = Network::Mainnet;
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",
);
// 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(());
}
return Ok(());
}
};
tracing::info!(
?network,
?test_type,
"running submitblock test using zebrad",
);
// 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",
);
let raw_blocks: Vec<String> =
get_raw_future_blocks(network, test_type, test_name, MAX_NUM_FUTURE_BLOCKS).await?;
// 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);
tracing::info!("got raw future blocks, spawning isolated zebrad...",);
// We're using the cached state
config.state.ephemeral = false;
// 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 mut child = testdir()?
.with_exact_config(&config)?
.spawn_child(args!["start"])?
.bypass_test_capture(true);
let rpc_address = zebra_rpc_address.expect("submitblock test must have RPC port");
child.expect_stdout_line_matches(&format!("Opened RPC endpoint at {rpc_address}"))?;
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}"))?;
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?;
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?;
assert!(res.status().is_success());
let res_text = res.text().await?;
// Test rpc endpoint response
assert!(res_text.contains(r#""result":"null""#));
// 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)

View File

@ -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<S: AsRef<str> + std::fmt::Debug>(
network: Network,
test_name: S,
test_type: LightwalletdTestType,
test_type: TestType,
use_internet_connection: bool,
) -> Result<Option<(TestChild<TempDir>, Option<SocketAddr>)>> {
let test_name = test_name.as_ref();
@ -255,7 +254,7 @@ pub fn spawn_zebrad_for_rpc<S: AsRef<str> + std::fmt::Debug>(
#[tracing::instrument]
pub fn can_spawn_zebrad_for_rpc<S: AsRef<str> + std::fmt::Debug>(
test_name: S,
test_type: LightwalletdTestType,
test_type: TestType,
) -> bool {
if zebra_test::net::zebra_skip_network_tests() {
return false;

View File

@ -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<ZebradConfig> {
// [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<Zebrad
pub fn spawn_lightwalletd_for_rpc<S: AsRef<str> + std::fmt::Debug>(
network: Network,
test_name: S,
test_type: LightwalletdTestType,
test_type: TestType,
zebrad_rpc_address: SocketAddr,
) -> Result<Option<(TestChild<TempDir>, u16)>> {
assert_eq!(network, Mainnet, "this test only supports Mainnet for now");
@ -165,7 +126,7 @@ pub fn spawn_lightwalletd_for_rpc<S: AsRef<str> + std::fmt::Debug>(
#[tracing::instrument]
pub fn can_spawn_lightwalletd_for_rpc<S: AsRef<str> + 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<S: AsRef<str>>(&self, test_name: S) -> Option<PathBuf> {
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<S: AsRef<str>>(&self, test_name: S) -> Option<Result<ZebradConfig>> {
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<S: AsRef<str>>(&self, test_name: S) -> Option<PathBuf> {
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<String>, Vec<String>) {
let mut zebrad_failure_messages: Vec<String> = 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<String>, Vec<String>) {
if !self.launches_lightwalletd() {
panic!("lightwalletd must not be launched in the {self:?} test");
}
let mut lightwalletd_failure_messages: Vec<String> = 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)
}
}

View File

@ -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.
/// Loads transactions from a few block(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 non-coinbase transactions from blocks that have not been finalized to disk
/// in the `ZEBRA_CACHED_STATE_DIR`.
///
/// Returns a list of valid transactions that are not in any of the blocks present in the
/// original `zebrad_state_path`.
/// ## Panics
///
/// If the provided `test_type` doesn't need an rpc server and cached state
#[tracing::instrument]
async fn load_transactions_from_future_blocks(
network: Network,
zebrad_state_path: PathBuf,
test_type: TestType,
test_name: &str,
) -> Result<Vec<Arc<Transaction>>> {
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?;
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)
}
/// Loads transactions from a block that's after the specified `height`.
///
/// 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.
///
/// # Panics
///
/// If the specified `zebrad_state_path` contains a chain state that's not synchronized to a tip that's
/// after `height`.
#[tracing::instrument]
async fn load_transactions_from_block_after(
height: block::Height,
network: Network,
zebrad_state_path: &Path,
) -> Result<Vec<Arc<Transaction>>> {
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,
);
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<ReadStateService>(
height: block::Height,
state: &mut ReadStateService,
) -> Result<Option<Vec<Arc<Transaction>>>>
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<Transaction>) -> wallet_grpc::RawTransaction {
let transaction_bytes = transaction.zcash_serialize_to_vec().unwrap();

View File

@ -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<P>,
zebra_rpc_address: SocketAddr,
test_type: LightwalletdTestType,
test_type: TestType,
wait_for_zebrad_mempool: bool,
wait_for_zebrad_tip: bool,
) -> Result<(TestChild<TempDir>, TestChild<P>)> {

View File

@ -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.

View File

@ -18,3 +18,4 @@ pub mod get_block_template_rpcs;
pub mod launch;
pub mod lightwalletd;
pub mod sync;
pub mod test_type;

View File

@ -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<TempDir>,
network: Network,
stop_regex: &str,
// Test Settings
mempool_behavior: MempoolBehavior,
check_legacy_chain: bool,
) -> Result<TestChild<TempDir>> {
zebrad.expect_stdout_line_matches(&format!("network: {network},"))?;
if check_legacy_chain {
zebrad.expect_stdout_line_matches("starting legacy chain check")?;
zebrad.expect_stdout_line_matches("no legacy chain found")?;
}
// 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

View File

@ -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<S: AsRef<str>>(&self, test_name: S) -> Option<PathBuf> {
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<S: AsRef<str>>(&self, test_name: S) -> Option<Result<ZebradConfig>> {
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<S: AsRef<str>>(&self, test_name: S) -> Option<PathBuf> {
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<String>, Vec<String>) {
let mut zebrad_failure_messages: Vec<String> = 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<String>, Vec<String>) {
if !self.launches_lightwalletd() {
panic!("lightwalletd must not be launched in the {self:?} test");
}
let mut lightwalletd_failure_messages: Vec<String> = 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)
}
}