diff --git a/zebra-chain/src/parameters/arbitrary.rs b/zebra-chain/src/parameters/arbitrary.rs index def06a9d504..966dfe4a3c2 100644 --- a/zebra-chain/src/parameters/arbitrary.rs +++ b/zebra-chain/src/parameters/arbitrary.rs @@ -18,4 +18,17 @@ impl NetworkUpgrade { ] .boxed() } + + /// Generates network upgrades from a reduced set + pub fn reduced_branch_id_strategy() -> BoxedStrategy { + // We use this strategy to test legacy chain + // TODO: We can add Canopy after we have a NU5 activation height + prop_oneof![ + Just(NetworkUpgrade::Overwinter), + Just(NetworkUpgrade::Sapling), + Just(NetworkUpgrade::Blossom), + Just(NetworkUpgrade::Heartwood), + ] + .boxed() + } } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 387c8723b79..362b04a0976 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -14,8 +14,8 @@ use tower::{util::BoxService, Service}; use tracing::instrument; use zebra_chain::{ block::{self, Block}, - parameters::Network, parameters::POW_AVERAGING_WINDOW, + parameters::{Network, NetworkUpgrade}, transaction, transaction::Transaction, transparent, @@ -26,6 +26,8 @@ use crate::{ Request, Response, Utxo, ValidateContextError, }; +#[cfg(any(test, feature = "proptest-impl"))] +pub mod arbitrary; mod check; mod finalized_state; mod non_finalized_state; @@ -64,6 +66,7 @@ impl StateService { pub fn new(config: Config, network: Network) -> Self { let disk = FinalizedState::new(&config, network); + let mem = NonFinalizedState { network, ..Default::default() @@ -71,14 +74,40 @@ impl StateService { let queued_blocks = QueuedBlocks::default(); let pending_utxos = PendingUtxos::default(); - Self { + let state = Self { disk, mem, queued_blocks, pending_utxos, network, last_prune: Instant::now(), + }; + + tracing::info!("starting legacy chain check"); + if let Some(tip) = state.best_tip() { + if let Some(nu5_activation_height) = NetworkUpgrade::Nu5.activation_height(network) { + if legacy_chain_check( + nu5_activation_height, + state.any_ancestor_blocks(tip.1), + state.network, + ) + .is_err() + { + let legacy_db_path = Some(state.disk.path().to_path_buf()); + panic!( + "Cached state contains a legacy chain. \ + An outdated Zebra version did not know about a recent network upgrade, \ + so it followed a legacy chain using outdated rules. \ + Hint: Delete your database, and restart Zebra to do a full sync. \ + Database path: {:?}", + legacy_db_path, + ); + } + } } + tracing::info!("no legacy chain found"); + + state } /// Queue a non finalized block for verification and check if any queued @@ -679,3 +708,56 @@ impl Service for StateService { pub fn init(config: Config, network: Network) -> BoxService { BoxService::new(StateService::new(config, network)) } + +/// Check if zebra is following a legacy chain and return an error if so. +fn legacy_chain_check( + nu5_activation_height: block::Height, + ancestors: I, + network: Network, +) -> Result<(), BoxError> +where + I: Iterator>, +{ + const MAX_BLOCKS_TO_CHECK: usize = 100; + + for (count, block) in ancestors.enumerate() { + // Stop checking if the chain reaches Canopy. We won't find any more V5 transactions, + // so the rest of our checks are useless. + // + // If the cached tip is close to NU5 activation, but there aren't any V5 transactions in the + // chain yet, we could reach MAX_BLOCKS_TO_CHECK in Canopy, and incorrectly return an error. + if block + .coinbase_height() + .expect("valid blocks have coinbase heights") + < nu5_activation_height + { + return Ok(()); + } + + // If we are past our NU5 activation height, but there are no V5 transactions in recent blocks, + // the Zebra instance that verified those blocks had no NU5 activation height. + if count >= MAX_BLOCKS_TO_CHECK { + return Err("giving up after checking too many blocks".into()); + } + + // If a transaction `network_upgrade` field is different from the network upgrade calculated + // using our activation heights, the Zebra instance that verified those blocks had different + // network upgrade heights. + block + .check_transaction_network_upgrade_consistency(network) + .map_err(|_| "inconsistent network upgrade found in transaction")?; + + // If we find at least one transaction with a valid `network_upgrade` field, the Zebra instance that + // verified those blocks used the same network upgrade heights. (Up to this point in the chain.) + let has_network_upgrade = block + .transactions + .iter() + .find_map(|trans| trans.network_upgrade()) + .is_some(); + if has_network_upgrade { + return Ok(()); + } + } + + Ok(()) +} diff --git a/zebra-state/src/service/non_finalized_state/arbitrary.rs b/zebra-state/src/service/arbitrary.rs similarity index 50% rename from zebra-state/src/service/non_finalized_state/arbitrary.rs rename to zebra-state/src/service/arbitrary.rs index 22f068761e9..6ff5325dfba 100644 --- a/zebra-state/src/service/non_finalized_state/arbitrary.rs +++ b/zebra-state/src/service/arbitrary.rs @@ -5,7 +5,12 @@ use proptest::{ }; use std::sync::Arc; -use zebra_chain::{block::Block, fmt::SummaryDebug, parameters::NetworkUpgrade::Nu5, LedgerState}; +use zebra_chain::{ + block::{Block, Height}, + fmt::SummaryDebug, + parameters::NetworkUpgrade, + LedgerState, +}; use zebra_test::prelude::*; use crate::tests::Prepare; @@ -55,7 +60,7 @@ impl Strategy for PreparedChain { let mut chain = self.chain.lock().unwrap(); if chain.is_none() { // TODO: use the latest network upgrade (#1974) - let ledger_strategy = LedgerState::genesis_strategy(Nu5, None, false); + let ledger_strategy = LedgerState::genesis_strategy(NetworkUpgrade::Nu5, None, false); let (network, blocks) = ledger_strategy .prop_flat_map(|ledger| { @@ -86,3 +91,57 @@ impl Strategy for PreparedChain { }) } } + +/// Generate a chain that allows us to make tests for the legacy chain rules. +/// +/// Arguments: +/// - `transaction_version_override`: See `LedgerState::height_strategy` for details. +/// - `transaction_has_valid_network_upgrade`: See `LedgerState::height_strategy` for details. +/// - `blocks_after_nu_activation`: The number of blocks the strategy will generate +/// after the provided `network_upgrade`. +/// - `network_upgrade` - The network upgrade that we are using to simulate from where the +/// legacy chain checks should start to apply. +/// +/// Returns: +/// A generated arbitrary strategy for the provided arguments. +pub(crate) fn partial_nu5_chain_strategy( + transaction_version_override: u32, + transaction_has_valid_network_upgrade: bool, + blocks_after_nu_activation: u32, + // TODO: This argument can be removed and just use Nu5 after we have an activation height #1841 + network_upgrade: NetworkUpgrade, +) -> impl Strategy< + Value = ( + Network, + Height, + zebra_chain::fmt::SummaryDebug>>, + ), +> { + ( + any::(), + NetworkUpgrade::reduced_branch_id_strategy(), + ) + .prop_flat_map(move |(network, random_nu)| { + // TODO: update this to Nu5 after we have a height #1841 + let mut nu = network_upgrade; + let nu_activation = nu.activation_height(network).unwrap(); + let height = Height(nu_activation.0 + blocks_after_nu_activation); + + // The `network_upgrade_override` will not be enough as when it is `None`, + // current network upgrade will be used (`NetworkUpgrade::Canopy`) which will be valid. + if !transaction_has_valid_network_upgrade { + nu = random_nu; + } + + zebra_chain::block::LedgerState::height_strategy( + height, + Some(nu), + Some(transaction_version_override), + transaction_has_valid_network_upgrade, + ) + .prop_flat_map(move |init| { + Block::partial_chain_strategy(init, blocks_after_nu_activation as usize) + }) + .prop_map(move |partial_chain| (network, nu_activation, partial_chain)) + }) +} diff --git a/zebra-state/src/service/finalized_state/tests/prop.rs b/zebra-state/src/service/finalized_state/tests/prop.rs index 232a90a5478..e04094e94ec 100644 --- a/zebra-state/src/service/finalized_state/tests/prop.rs +++ b/zebra-state/src/service/finalized_state/tests/prop.rs @@ -6,8 +6,8 @@ use zebra_test::prelude::*; use crate::{ config::Config, service::{ + arbitrary::PreparedChain, finalized_state::{FinalizedBlock, FinalizedState}, - non_finalized_state::arbitrary::PreparedChain, }, }; diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 4aa95a8b0bf..2ab479c99e8 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -5,8 +5,6 @@ mod chain; mod queued_blocks; -#[cfg(any(test, feature = "proptest-impl"))] -pub mod arbitrary; #[cfg(test)] mod tests; diff --git a/zebra-state/src/service/non_finalized_state/tests/prop.rs b/zebra-state/src/service/non_finalized_state/tests/prop.rs index 4d9f43c40d9..d5143f6d3bb 100644 --- a/zebra-state/src/service/non_finalized_state/tests/prop.rs +++ b/zebra-state/src/service/non_finalized_state/tests/prop.rs @@ -2,7 +2,7 @@ use std::env; use zebra_test::prelude::*; -use crate::service::non_finalized_state::{arbitrary::PreparedChain, Chain}; +use crate::service::{arbitrary::PreparedChain, non_finalized_state::Chain}; const DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES: u32 = 32; diff --git a/zebra-state/src/service/tests.rs b/zebra-state/src/service/tests.rs index 504a1b3ca20..90cda9edac5 100644 --- a/zebra-state/src/service/tests.rs +++ b/zebra-state/src/service/tests.rs @@ -1,14 +1,16 @@ -use std::sync::Arc; +use std::{env, sync::Arc}; use futures::stream::FuturesUnordered; use tower::{util::BoxService, Service, ServiceExt}; use zebra_chain::{ - block::Block, parameters::Network, serialization::ZcashDeserializeInto, transaction, - transparent, + block::Block, + parameters::{Network, NetworkUpgrade}, + serialization::ZcashDeserializeInto, + transaction, transparent, }; use zebra_test::{prelude::*, transcript::Transcript}; -use crate::{init, BoxError, Config, Request, Response, Utxo}; +use crate::{init, service::arbitrary, BoxError, Config, Request, Response, Utxo}; const LAST_BLOCK_HEIGHT: u32 = 10; @@ -183,3 +185,65 @@ fn state_behaves_when_blocks_are_committed_out_of_order() -> Result<()> { Ok(()) } + +const DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES: u32 = 2; +const BLOCKS_AFTER_NU5: u32 = 101; + +proptest! { + #![proptest_config( + proptest::test_runner::Config::with_cases(env::var("PROPTEST_CASES") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)) + )] + + /// Test blocks that are less than the NU5 activation height. + #[test] + fn some_block_less_than_network_upgrade( + (network, nu_activation_height, chain) in arbitrary::partial_nu5_chain_strategy(4, true, BLOCKS_AFTER_NU5/2, NetworkUpgrade::Canopy) + ) { + let response = crate::service::legacy_chain_check(nu_activation_height, chain.into_iter(), network) + .map_err(|error| error.to_string()); + + prop_assert_eq!(response, Ok(())); + } + + /// Test the maximum amount of blocks to check before chain is declared to be legacy. + #[test] + fn no_transaction_with_network_upgrade( + (network, nu_activation_height, chain) in arbitrary::partial_nu5_chain_strategy(4, true, BLOCKS_AFTER_NU5, NetworkUpgrade::Canopy) + ) { + let response = crate::service::legacy_chain_check(nu_activation_height, chain.into_iter(), network) + .map_err(|error| error.to_string()); + + prop_assert_eq!( + response, + Err("giving up after checking too many blocks".into()) + ); + } + + /// Test the `Block.check_transaction_network_upgrade()` error inside the legacy check. + #[test] + fn at_least_one_transaction_with_inconsistent_network_upgrade( + (network, nu_activation_height, chain) in arbitrary::partial_nu5_chain_strategy(5, false, BLOCKS_AFTER_NU5, NetworkUpgrade::Canopy) + ) { + let response = crate::service::legacy_chain_check(nu_activation_height, chain.into_iter(), network) + .map_err(|error| error.to_string()); + + prop_assert_eq!( + response, + Err("inconsistent network upgrade found in transaction".into()) + ); + } + + /// Test there is at least one transaction with a valid `network_upgrade` in the legacy check. + #[test] + fn at_least_one_transaction_with_valid_network_upgrade( + (network, nu_activation_height, chain) in arbitrary::partial_nu5_chain_strategy(5, true, BLOCKS_AFTER_NU5/2, NetworkUpgrade::Canopy) + ) { + let response = crate::service::legacy_chain_check(nu_activation_height, chain.into_iter(), network) + .map_err(|error| error.to_string()); + + prop_assert_eq!(response, Ok(())); + } +} diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index 80fc554c890..2205585555a 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -368,6 +368,10 @@ fn start_no_args() -> Result<()> { output.stdout_line_contains("Starting zebrad")?; + // Make sure the command passed the legacy chain check + output.stdout_line_contains("starting legacy chain check")?; + output.stdout_line_contains("no legacy chain found")?; + // Make sure the command was killed output.assert_was_killed()?; @@ -699,6 +703,7 @@ fn sync_one_checkpoint_mainnet() -> Result<()> { STOP_AT_HEIGHT_REGEX, SMALL_CHECKPOINT_TIMEOUT, None, + true, ) .map(|_tempdir| ()) } @@ -714,6 +719,7 @@ fn sync_one_checkpoint_testnet() -> Result<()> { STOP_AT_HEIGHT_REGEX, SMALL_CHECKPOINT_TIMEOUT, None, + true, ) .map(|_tempdir| ()) } @@ -736,6 +742,7 @@ fn restart_stop_at_height_for_network(network: Network, height: Height) -> Resul STOP_AT_HEIGHT_REGEX, SMALL_CHECKPOINT_TIMEOUT, None, + true, )?; // if stopping corrupts the rocksdb database, zebrad might hang or crash here // if stopping does not write the rocksdb database to disk, Zebra will @@ -746,6 +753,7 @@ fn restart_stop_at_height_for_network(network: Network, height: Height) -> Resul "state is already at the configured height", STOP_ON_LOAD_TIMEOUT, Some(reuse_tempdir), + false, )?; Ok(()) @@ -765,6 +773,7 @@ fn sync_large_checkpoints_mainnet() -> Result<()> { STOP_AT_HEIGHT_REGEX, LARGE_CHECKPOINT_TIMEOUT, None, + true, )?; // if this sync fails, see the failure notes in `restart_stop_at_height` sync_until( @@ -773,6 +782,7 @@ fn sync_large_checkpoints_mainnet() -> Result<()> { "previous state height is greater than the stop height", STOP_ON_LOAD_TIMEOUT, Some(reuse_tempdir), + false, )?; Ok(()) @@ -800,6 +810,7 @@ fn sync_until( stop_regex: &str, timeout: Duration, reuse_tempdir: Option, + check_legacy_chain: bool, ) -> Result { zebra_test::init(); @@ -826,6 +837,12 @@ fn sync_until( let network = format!("network: {},", network); 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")?; + } + child.expect_stdout_line_matches(stop_regex)?; child.kill()?; @@ -859,7 +876,12 @@ fn create_cached_database_height(network: Network, height: Height) -> Result<()> let network = format!("network: {},", network); child.expect_stdout_line_matches(&network)?; + + child.expect_stdout_line_matches("starting legacy chain check")?; + child.expect_stdout_line_matches("no legacy chain found")?; + child.expect_stdout_line_matches(STOP_AT_HEIGHT_REGEX)?; + child.kill()?; Ok(())