661 lines
26 KiB
Rust
661 lines
26 KiB
Rust
//! Randomised property tests for the non-finalized state.
|
|
|
|
use std::{collections::BTreeMap, env, sync::Arc};
|
|
|
|
use zebra_test::prelude::*;
|
|
|
|
use zebra_chain::{
|
|
amount::NonNegative,
|
|
block::{self, arbitrary::allow_all_transparent_coinbase_spends, Block, Height},
|
|
fmt::DisplayToDebug,
|
|
history_tree::{HistoryTree, NonEmptyHistoryTree},
|
|
parameters::NetworkUpgrade::*,
|
|
parameters::{Network, *},
|
|
value_balance::ValueBalance,
|
|
LedgerState,
|
|
};
|
|
|
|
use crate::{
|
|
arbitrary::Prepare,
|
|
request::ContextuallyValidBlock,
|
|
service::{
|
|
arbitrary::PreparedChain,
|
|
finalized_state::FinalizedState,
|
|
non_finalized_state::{Chain, NonFinalizedState},
|
|
},
|
|
Config,
|
|
};
|
|
|
|
/// The default number of proptest cases for long partial chain tests.
|
|
const DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES: u32 = 1;
|
|
|
|
/// The default number of proptest cases for short partial chain tests.
|
|
const DEFAULT_SHORT_CHAIN_PROPTEST_CASES: u32 = 16;
|
|
|
|
/// Check that chain block pushes work with blocks from genesis
|
|
///
|
|
/// Logs extra debugging information when the chain value balances fail.
|
|
#[test]
|
|
fn push_genesis_chain() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
proptest!(
|
|
ProptestConfig::with_cases(env::var("PROPTEST_CASES")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)),
|
|
|((chain, count, network, empty_tree) in PreparedChain::default())| {
|
|
prop_assert!(empty_tree.is_none());
|
|
|
|
let mut only_chain = Chain::new(network, Height(0), Default::default(), Default::default(), Default::default(), empty_tree, ValueBalance::zero());
|
|
// contains the block value pool changes and chain value pool balances for each height
|
|
let mut chain_values = BTreeMap::new();
|
|
|
|
chain_values.insert(None, (None, only_chain.chain_value_pools.into()));
|
|
|
|
for block in chain.iter().take(count).cloned() {
|
|
let block =
|
|
ContextuallyValidBlock::with_block_and_spent_utxos(
|
|
block,
|
|
only_chain.unspent_utxos(),
|
|
)
|
|
.map_err(|e| (e, chain_values.clone()))
|
|
.expect("invalid block value pool change");
|
|
|
|
chain_values.insert(block.height.into(), (block.chain_value_pool_change.into(), None));
|
|
|
|
only_chain = only_chain
|
|
.push(block.clone())
|
|
.map_err(|e| (e, chain_values.clone()))
|
|
.expect("invalid chain value pools");
|
|
|
|
chain_values.insert(block.height.into(), (block.chain_value_pool_change.into(), only_chain.chain_value_pools.into()));
|
|
}
|
|
|
|
prop_assert_eq!(only_chain.blocks.len(), count);
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check that chain block pushes work with history tree blocks
|
|
#[test]
|
|
fn push_history_tree_chain() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
proptest!(
|
|
ProptestConfig::with_cases(env::var("PROPTEST_CASES")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)),
|
|
|((chain, count, network, finalized_tree) in PreparedChain::new_heartwood())| {
|
|
prop_assert!(finalized_tree.is_some());
|
|
|
|
// Skip first block which was used for the history tree.
|
|
// This skips some transactions which are required to calculate value balances,
|
|
// so we zero all transparent inputs in this test.
|
|
|
|
// make sure count is still valid
|
|
let count = std::cmp::min(count, chain.len() - 1);
|
|
let chain = &chain[1..];
|
|
|
|
let mut only_chain = Chain::new(network, Height(0), Default::default(), Default::default(), Default::default(), finalized_tree, ValueBalance::zero());
|
|
|
|
for block in chain
|
|
.iter()
|
|
.take(count)
|
|
.map(ContextuallyValidBlock::test_with_zero_chain_pool_change) {
|
|
only_chain = only_chain.push(block)?;
|
|
}
|
|
|
|
prop_assert_eq!(only_chain.blocks.len(), count);
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Checks that a forked genesis chain is the same as a chain that had the same
|
|
/// blocks appended.
|
|
///
|
|
/// In other words, this test checks that we get the same chain if we:
|
|
/// - fork the original chain, then push some blocks, or
|
|
/// - push the same blocks to the original chain.
|
|
///
|
|
/// Also checks that:
|
|
/// - There are no transparent spends in the chain from the genesis block,
|
|
/// because genesis transparent outputs are ignored.
|
|
/// - Transactions only spend transparent outputs from earlier in the block or
|
|
/// chain.
|
|
/// - Chain value balances are non-negative.
|
|
#[test]
|
|
fn forked_equals_pushed_genesis() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
proptest!(
|
|
ProptestConfig::with_cases(env::var("PROPTEST_CASES")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)),
|
|
|((chain, fork_at_count, network, empty_tree) in PreparedChain::default())| {
|
|
prop_assert!(empty_tree.is_none());
|
|
|
|
// This chain will be used to check if the blocks in the forked chain
|
|
// correspond to the blocks in the original chain before the fork.
|
|
let mut partial_chain = Chain::new(
|
|
network,
|
|
Height(0),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
empty_tree.clone(),
|
|
ValueBalance::zero(),
|
|
);
|
|
for block in chain.iter().take(fork_at_count).cloned() {
|
|
let block = ContextuallyValidBlock::with_block_and_spent_utxos(
|
|
block,
|
|
partial_chain.unspent_utxos(),
|
|
)?;
|
|
partial_chain = partial_chain
|
|
.push(block)
|
|
.expect("partial chain push is valid");
|
|
}
|
|
|
|
// This chain will be forked.
|
|
let mut full_chain = Chain::new(
|
|
network,
|
|
Height(0),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
empty_tree,
|
|
ValueBalance::zero(),
|
|
);
|
|
for block in chain.iter().cloned() {
|
|
let block =
|
|
ContextuallyValidBlock::with_block_and_spent_utxos(block, full_chain.unspent_utxos())?;
|
|
full_chain = full_chain
|
|
.push(block.clone())
|
|
.expect("full chain push is valid");
|
|
|
|
// Check some other properties of generated chains.
|
|
if block.height == block::Height(0) {
|
|
prop_assert_eq!(
|
|
block
|
|
.block
|
|
.transactions
|
|
.iter()
|
|
.flat_map(|t| t.inputs())
|
|
.filter_map(|i| i.outpoint())
|
|
.count(),
|
|
0,
|
|
"unexpected transparent prevout input at height {:?}: \
|
|
genesis transparent outputs must be ignored, \
|
|
so there can not be any spends in the genesis block",
|
|
block.height,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Use [`fork_at_count`] as the fork tip.
|
|
let fork_tip_height = fork_at_count - 1;
|
|
let fork_tip_hash = chain[fork_tip_height].hash;
|
|
|
|
// Fork the chain.
|
|
let mut forked = full_chain
|
|
.fork(fork_tip_hash)
|
|
.expect("hash is present");
|
|
|
|
// This check is redundant, but it's useful for debugging.
|
|
prop_assert_eq!(forked.blocks.len(), partial_chain.blocks.len());
|
|
|
|
// Check that the entire internal state of the forked chain corresponds to the state of
|
|
// the original chain.
|
|
prop_assert!(forked.eq_internal_state(&partial_chain));
|
|
|
|
// Re-add blocks to the fork and check if we arrive at the
|
|
// same original full chain.
|
|
for block in chain.iter().skip(fork_at_count).cloned() {
|
|
let block =
|
|
ContextuallyValidBlock::with_block_and_spent_utxos(block, forked.unspent_utxos())?;
|
|
forked = forked.push(block).expect("forked chain push is valid");
|
|
}
|
|
|
|
prop_assert_eq!(forked.blocks.len(), full_chain.blocks.len());
|
|
prop_assert!(forked.eq_internal_state(&full_chain));
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check that a forked history tree chain is the same as a chain that had the same blocks appended.
|
|
#[test]
|
|
fn forked_equals_pushed_history_tree() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
proptest!(
|
|
ProptestConfig::with_cases(env::var("PROPTEST_CASES")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)),
|
|
|((chain, fork_at_count, network, finalized_tree) in PreparedChain::new_heartwood())| {
|
|
prop_assert!(finalized_tree.is_some());
|
|
|
|
// Skip first block which was used for the history tree.
|
|
// This skips some transactions which are required to calculate value balances,
|
|
// so we zero all transparent inputs in this test.
|
|
|
|
// make sure fork_at_count is still valid
|
|
let fork_at_count = std::cmp::min(fork_at_count, chain.len() - 1);
|
|
let chain = &chain[1..];
|
|
// use `fork_at_count` as the fork tip
|
|
let fork_tip_hash = chain[fork_at_count - 1].hash;
|
|
|
|
let mut full_chain = Chain::new(network, Height(0), Default::default(), Default::default(), Default::default(), finalized_tree.clone(), ValueBalance::zero());
|
|
let mut partial_chain = Chain::new(network, Height(0), Default::default(), Default::default(), Default::default(), finalized_tree, ValueBalance::zero());
|
|
|
|
for block in chain
|
|
.iter()
|
|
.take(fork_at_count)
|
|
.map(ContextuallyValidBlock::test_with_zero_chain_pool_change) {
|
|
partial_chain = partial_chain.push(block)?;
|
|
}
|
|
|
|
for block in chain
|
|
.iter()
|
|
.map(ContextuallyValidBlock::test_with_zero_chain_pool_change) {
|
|
full_chain = full_chain.push(block.clone())?;
|
|
}
|
|
|
|
let mut forked = full_chain
|
|
.fork(fork_tip_hash)
|
|
.expect("hash is present");
|
|
|
|
// the first check is redundant, but it's useful for debugging
|
|
prop_assert_eq!(forked.blocks.len(), partial_chain.blocks.len());
|
|
prop_assert!(forked.eq_internal_state(&partial_chain));
|
|
|
|
// Re-add blocks to the fork and check if we arrive at the
|
|
// same original full chain
|
|
for block in chain
|
|
.iter()
|
|
.skip(fork_at_count)
|
|
.map(ContextuallyValidBlock::test_with_zero_chain_pool_change) {
|
|
forked = forked.push(block)?;
|
|
}
|
|
|
|
prop_assert_eq!(forked.blocks.len(), full_chain.blocks.len());
|
|
prop_assert!(forked.eq_internal_state(&full_chain));
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check that a genesis chain with some blocks finalized is the same as
|
|
/// a chain that never had those blocks added.
|
|
#[test]
|
|
fn finalized_equals_pushed_genesis() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
proptest!(ProptestConfig::with_cases(env::var("PROPTEST_CASES")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)),
|
|
|((chain, end_count, network, empty_tree) in PreparedChain::default())| {
|
|
|
|
// This test starts a partial chain from the middle of `chain`,
|
|
// so it doesn't have the unspent UTXOs needed to calculate value balances.
|
|
|
|
prop_assert!(empty_tree.is_none());
|
|
|
|
// TODO: fix this test or the code so the full_chain temporary trees aren't overwritten
|
|
let chain = chain.iter().filter(|block| block.height != Height(0));
|
|
|
|
// use `end_count` as the number of non-finalized blocks at the end of the chain
|
|
let finalized_count = chain.clone().count() - end_count;
|
|
|
|
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
|
|
|
|
let mut full_chain = Chain::new(network, Height(0), Default::default(), Default::default(), Default::default(), empty_tree, fake_value_pool);
|
|
for block in chain
|
|
.clone()
|
|
.take(finalized_count)
|
|
.map(ContextuallyValidBlock::test_with_zero_spent_utxos) {
|
|
full_chain = full_chain.push(block)?;
|
|
}
|
|
|
|
let mut partial_chain = Chain::new(
|
|
network,
|
|
full_chain.non_finalized_tip_height(),
|
|
full_chain.sprout_note_commitment_tree(),
|
|
full_chain.sapling_note_commitment_tree(),
|
|
full_chain.orchard_note_commitment_tree(),
|
|
full_chain.history_block_commitment_tree(),
|
|
full_chain.chain_value_pools,
|
|
);
|
|
for block in chain
|
|
.clone()
|
|
.skip(finalized_count)
|
|
.map(ContextuallyValidBlock::test_with_zero_spent_utxos) {
|
|
partial_chain = partial_chain.push(block.clone())?;
|
|
}
|
|
|
|
for block in chain
|
|
.skip(finalized_count)
|
|
.map(ContextuallyValidBlock::test_with_zero_spent_utxos) {
|
|
full_chain = full_chain.push(block.clone())?;
|
|
}
|
|
|
|
for _ in 0..finalized_count {
|
|
full_chain.pop_root();
|
|
}
|
|
|
|
// Make sure the temporary trees from finalized tip forks are removed.
|
|
// TODO: update the test or the code so this extra step isn't needed?
|
|
full_chain.pop_root();
|
|
partial_chain.pop_root();
|
|
|
|
prop_assert_eq!(full_chain.blocks.len(), partial_chain.blocks.len());
|
|
prop_assert!(
|
|
full_chain.eq_internal_state(&partial_chain),
|
|
"\n\
|
|
full chain:\n{full_chain:#?}\n\n\
|
|
partial chain:\n{partial_chain:#?}\n",
|
|
);
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check that a history tree chain with some blocks finalized is the same as
|
|
/// a chain that never had those blocks added.
|
|
#[test]
|
|
fn finalized_equals_pushed_history_tree() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
proptest!(ProptestConfig::with_cases(env::var("PROPTEST_CASES")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)),
|
|
|((chain, end_count, network, finalized_tree) in PreparedChain::new_heartwood())| {
|
|
|
|
|
|
prop_assert!(finalized_tree.is_some());
|
|
|
|
// Skip first block which was used for the history tree; make sure end_count is still valid
|
|
//
|
|
// This skips some transactions which are required to calculate value balances,
|
|
// so we zero all transparent inputs in this test.
|
|
//
|
|
// This test also starts a partial chain from the middle of `chain`,
|
|
// so it doesn't have the unspent UTXOs needed to calculate value balances.
|
|
let end_count = std::cmp::min(end_count, chain.len() - 1);
|
|
let chain = &chain[1..];
|
|
// use `end_count` as the number of non-finalized blocks at the end of the chain
|
|
let finalized_count = chain.len() - end_count;
|
|
|
|
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
|
|
|
|
let mut full_chain = Chain::new(network, Height(0), Default::default(), Default::default(), Default::default(), finalized_tree, fake_value_pool);
|
|
for block in chain
|
|
.iter()
|
|
.take(finalized_count)
|
|
.map(ContextuallyValidBlock::test_with_zero_spent_utxos) {
|
|
full_chain = full_chain.push(block)?;
|
|
}
|
|
|
|
let mut partial_chain = Chain::new(
|
|
network,
|
|
Height(finalized_count.try_into().unwrap()),
|
|
full_chain.sprout_note_commitment_tree(),
|
|
full_chain.sapling_note_commitment_tree(),
|
|
full_chain.orchard_note_commitment_tree(),
|
|
full_chain.history_block_commitment_tree(),
|
|
full_chain.chain_value_pools,
|
|
);
|
|
|
|
for block in chain
|
|
.iter()
|
|
.skip(finalized_count)
|
|
.map(ContextuallyValidBlock::test_with_zero_spent_utxos) {
|
|
partial_chain = partial_chain.push(block.clone())?;
|
|
}
|
|
|
|
for block in chain
|
|
.iter()
|
|
.skip(finalized_count)
|
|
.map(ContextuallyValidBlock::test_with_zero_spent_utxos) {
|
|
full_chain= full_chain.push(block.clone())?;
|
|
}
|
|
|
|
for _ in 0..finalized_count {
|
|
full_chain.pop_root();
|
|
}
|
|
|
|
// Make sure the temporary trees from finalized tip forks are removed.
|
|
// TODO: update the test or the code so this extra step isn't needed?
|
|
full_chain.pop_root();
|
|
partial_chain.pop_root();
|
|
|
|
prop_assert_eq!(full_chain.blocks.len(), partial_chain.blocks.len());
|
|
prop_assert!(
|
|
full_chain.eq_internal_state(&partial_chain),
|
|
"\n\
|
|
full chain:\n{full_chain:#?}\n\n\
|
|
partial chain:\n{partial_chain:#?}\n",
|
|
);
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check that rejected blocks do not change the internal state of a genesis chain
|
|
/// in a non-finalized state.
|
|
#[test]
|
|
fn rejection_restores_internal_state_genesis() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
proptest!(ProptestConfig::with_cases(env::var("PROPTEST_CASES")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)),
|
|
|((chain, valid_count, network, mut bad_block) in (PreparedChain::default(), any::<bool>(), any::<bool>())
|
|
.prop_flat_map(|((chain, valid_count, network, _history_tree), is_nu5, is_v5)| {
|
|
let next_height = chain[valid_count - 1].height;
|
|
(
|
|
Just(chain),
|
|
Just(valid_count),
|
|
Just(network),
|
|
// generate a Canopy or NU5 block with v4 or v5 transactions
|
|
LedgerState::height_strategy(
|
|
next_height,
|
|
if is_nu5 { Nu5 } else { Canopy },
|
|
if is_nu5 && is_v5 { 5 } else { 4 },
|
|
true,
|
|
)
|
|
.prop_flat_map(Block::arbitrary_with)
|
|
.prop_map(DisplayToDebug)
|
|
)
|
|
}
|
|
))| {
|
|
let mut state = NonFinalizedState::new(network);
|
|
let finalized_state = FinalizedState::new(&Config::ephemeral(), network);
|
|
|
|
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
|
|
finalized_state.set_finalized_value_pool(fake_value_pool);
|
|
|
|
// use `valid_count` as the number of valid blocks before an invalid block
|
|
let valid_tip_height = chain[valid_count - 1].height;
|
|
let valid_tip_hash = chain[valid_count - 1].hash;
|
|
let mut chain = chain.iter().take(valid_count).cloned();
|
|
|
|
prop_assert!(state.eq_internal_state(&state));
|
|
|
|
if let Some(first_block) = chain.next() {
|
|
// Allows anchor checks to pass
|
|
finalized_state.populate_with_anchors(&first_block.block);
|
|
|
|
let result = state.commit_new_chain(first_block, &finalized_state);
|
|
prop_assert_eq!(
|
|
result,
|
|
Ok(()),
|
|
"PreparedChain should generate a valid first block"
|
|
);
|
|
prop_assert!(state.eq_internal_state(&state));
|
|
}
|
|
|
|
for block in chain {
|
|
// Allows anchor checks to pass
|
|
finalized_state.populate_with_anchors(&block.block);
|
|
|
|
let result = state.commit_block(block.clone(), &finalized_state);
|
|
prop_assert_eq!(
|
|
result,
|
|
Ok(()),
|
|
"PreparedChain should generate a valid block at {:?}",
|
|
block.height,
|
|
);
|
|
prop_assert!(state.eq_internal_state(&state));
|
|
}
|
|
|
|
prop_assert_eq!(state.best_tip(), Some((valid_tip_height, valid_tip_hash)));
|
|
|
|
let mut reject_state = state.clone();
|
|
// the tip check is redundant, but it's useful for debugging
|
|
prop_assert_eq!(state.best_tip(), reject_state.best_tip());
|
|
prop_assert!(state.eq_internal_state(&reject_state));
|
|
|
|
Arc::make_mut(&mut bad_block.header).previous_block_hash = valid_tip_hash;
|
|
let bad_block = Arc::new(bad_block.0).prepare();
|
|
let reject_result = reject_state.commit_block(bad_block, &finalized_state);
|
|
|
|
if reject_result.is_err() {
|
|
prop_assert_eq!(state.best_tip(), reject_state.best_tip());
|
|
prop_assert!(state.eq_internal_state(&reject_state));
|
|
} else {
|
|
// the block just happened to pass all the non-finalized checks
|
|
prop_assert_ne!(state.best_tip(), reject_state.best_tip());
|
|
prop_assert!(!state.eq_internal_state(&reject_state));
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check that different blocks create different internal chain states,
|
|
/// and that all the state fields are covered by `eq_internal_state`.
|
|
#[test]
|
|
fn different_blocks_different_chains() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
proptest!(ProptestConfig::with_cases(env::var("PROPTEST_CASES")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(DEFAULT_SHORT_CHAIN_PROPTEST_CASES)),
|
|
|((vec1, vec2) in (any::<bool>(), any::<bool>())
|
|
.prop_flat_map(|(is_nu5, is_v5)| {
|
|
// generate a Canopy or NU5 block with v4 or v5 transactions
|
|
LedgerState::coinbase_strategy(
|
|
if is_nu5 { Nu5 } else { Canopy },
|
|
if is_nu5 && is_v5 { 5 } else { 4 },
|
|
true,
|
|
)})
|
|
.prop_map(|ledger_state| Block::partial_chain_strategy(ledger_state, 2, allow_all_transparent_coinbase_spends, false))
|
|
.prop_flat_map(|block_strategy| (block_strategy.clone(), block_strategy))
|
|
)| {
|
|
let prev_block1 = vec1[0].clone();
|
|
let prev_block2 = vec2[0].clone();
|
|
|
|
let height1 = prev_block1.coinbase_height().unwrap();
|
|
let height2 = prev_block1.coinbase_height().unwrap();
|
|
|
|
let finalized_tree1: Arc<HistoryTree> = if height1 >= Heartwood.activation_height(Network::Mainnet).unwrap() {
|
|
Arc::new(
|
|
NonEmptyHistoryTree::from_block(Network::Mainnet, prev_block1, &Default::default(), &Default::default()).unwrap().into()
|
|
)
|
|
} else {
|
|
Default::default()
|
|
};
|
|
let finalized_tree2: Arc<HistoryTree> = if height2 >= NetworkUpgrade::Heartwood.activation_height(Network::Mainnet).unwrap() {
|
|
Arc::new(
|
|
NonEmptyHistoryTree::from_block(Network::Mainnet, prev_block2, &Default::default(), &Default::default()).unwrap().into()
|
|
)
|
|
} else {
|
|
Default::default()
|
|
};
|
|
|
|
let chain1 = Chain::new(Network::Mainnet, Height(0), Default::default(), Default::default(), Default::default(), finalized_tree1, ValueBalance::fake_populated_pool());
|
|
let chain2 = Chain::new(Network::Mainnet, Height(0), Default::default(), Default::default(), Default::default(), finalized_tree2, ValueBalance::fake_populated_pool());
|
|
|
|
let block1 = vec1[1].clone().prepare().test_with_zero_spent_utxos();
|
|
let block2 = vec2[1].clone().prepare().test_with_zero_spent_utxos();
|
|
|
|
let result1 = chain1.push(block1.clone());
|
|
let result2 = chain2.push(block2.clone());
|
|
|
|
// if there is an error, we don't get the chains back
|
|
if let (Ok(mut chain1), Ok(chain2)) = (result1, result2) {
|
|
if block1 == block2 {
|
|
// the blocks were equal, so the chains should be equal
|
|
|
|
// the first check is redundant, but it's useful for debugging
|
|
prop_assert_eq!(&chain1.height_by_hash, &chain2.height_by_hash);
|
|
prop_assert!(chain1.eq_internal_state(&chain2));
|
|
} else {
|
|
// the blocks were different, so the chains should be different
|
|
|
|
prop_assert_ne!(&chain1.height_by_hash, &chain2.height_by_hash);
|
|
prop_assert!(!chain1.eq_internal_state(&chain2));
|
|
|
|
// We can't derive eq_internal_state,
|
|
// so we check for missing fields here.
|
|
|
|
// blocks, heights, hashes
|
|
chain1.blocks = chain2.blocks.clone();
|
|
chain1.height_by_hash = chain2.height_by_hash.clone();
|
|
chain1.tx_loc_by_hash = chain2.tx_loc_by_hash.clone();
|
|
|
|
// transparent UTXOs
|
|
chain1.created_utxos = chain2.created_utxos.clone();
|
|
chain1.spent_utxos = chain2.spent_utxos.clone();
|
|
|
|
// note commitment trees
|
|
chain1.sprout_trees_by_anchor = chain2.sprout_trees_by_anchor.clone();
|
|
chain1.sprout_trees_by_height = chain2.sprout_trees_by_height.clone();
|
|
chain1.sapling_trees_by_height = chain2.sapling_trees_by_height.clone();
|
|
chain1.orchard_trees_by_height = chain2.orchard_trees_by_height.clone();
|
|
|
|
// history trees
|
|
chain1.history_trees_by_height = chain2.history_trees_by_height.clone();
|
|
|
|
// anchors
|
|
chain1.sprout_anchors = chain2.sprout_anchors.clone();
|
|
chain1.sprout_anchors_by_height = chain2.sprout_anchors_by_height.clone();
|
|
chain1.sapling_anchors = chain2.sapling_anchors.clone();
|
|
chain1.sapling_anchors_by_height = chain2.sapling_anchors_by_height.clone();
|
|
chain1.orchard_anchors = chain2.orchard_anchors.clone();
|
|
chain1.orchard_anchors_by_height = chain2.orchard_anchors_by_height.clone();
|
|
|
|
// nullifiers
|
|
chain1.sprout_nullifiers = chain2.sprout_nullifiers.clone();
|
|
chain1.sapling_nullifiers = chain2.sapling_nullifiers.clone();
|
|
chain1.orchard_nullifiers = chain2.orchard_nullifiers.clone();
|
|
|
|
// proof of work
|
|
chain1.partial_cumulative_work = chain2.partial_cumulative_work;
|
|
|
|
// chain value pool
|
|
chain1.chain_value_pools = chain2.chain_value_pools;
|
|
|
|
// If this check fails, the `Chain` fields are out
|
|
// of sync with `eq_internal_state` or this test.
|
|
prop_assert!(
|
|
chain1.eq_internal_state(&chain2),
|
|
"Chain fields, eq_internal_state, and this test must be consistent"
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
}
|