Legacy chain check and tests (#2366)

* add legacy chain check and tests
* improve has_network_upgrade check
* add docs to legacy_chain_check()
* change arbitrary module structure
* change the panic message
* move legacy chain acceptance into existing tests
* use a reduced_branch_id_strategy()
* add docs to strategy function
* add argument to check for legacy chain into sync_until()
This commit is contained in:
Alfredo Garcia 2021-06-29 02:03:51 -03:00 committed by GitHub
parent 4c321f6fa1
commit 1624377da7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 250 additions and 12 deletions

View File

@ -18,4 +18,17 @@ impl NetworkUpgrade {
]
.boxed()
}
/// Generates network upgrades from a reduced set
pub fn reduced_branch_id_strategy() -> BoxedStrategy<NetworkUpgrade> {
// 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()
}
}

View File

@ -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<Request> for StateService {
pub fn init(config: Config, network: Network) -> BoxService<Request, Response, BoxError> {
BoxService::new(StateService::new(config, network))
}
/// Check if zebra is following a legacy chain and return an error if so.
fn legacy_chain_check<I>(
nu5_activation_height: block::Height,
ancestors: I,
network: Network,
) -> Result<(), BoxError>
where
I: Iterator<Item = Arc<Block>>,
{
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(())
}

View File

@ -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<Vec<Arc<Block>>>,
),
> {
(
any::<Network>(),
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))
})
}

View File

@ -6,8 +6,8 @@ use zebra_test::prelude::*;
use crate::{
config::Config,
service::{
arbitrary::PreparedChain,
finalized_state::{FinalizedBlock, FinalizedState},
non_finalized_state::arbitrary::PreparedChain,
},
};

View File

@ -5,8 +5,6 @@
mod chain;
mod queued_blocks;
#[cfg(any(test, feature = "proptest-impl"))]
pub mod arbitrary;
#[cfg(test)]
mod tests;

View File

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

View File

@ -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(()));
}
}

View File

@ -378,6 +378,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()?;
@ -709,6 +713,7 @@ fn sync_one_checkpoint_mainnet() -> Result<()> {
STOP_AT_HEIGHT_REGEX,
SMALL_CHECKPOINT_TIMEOUT,
None,
true,
)
.map(|_tempdir| ())
}
@ -724,6 +729,7 @@ fn sync_one_checkpoint_testnet() -> Result<()> {
STOP_AT_HEIGHT_REGEX,
SMALL_CHECKPOINT_TIMEOUT,
None,
true,
)
.map(|_tempdir| ())
}
@ -746,6 +752,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
@ -756,6 +763,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(())
@ -775,6 +783,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(
@ -783,6 +792,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(())
@ -810,6 +820,7 @@ fn sync_until(
stop_regex: &str,
timeout: Duration,
reuse_tempdir: Option<TempDir>,
check_legacy_chain: bool,
) -> Result<TempDir> {
zebra_test::init();
@ -833,6 +844,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()?;
@ -866,7 +883,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(())