use std::{sync::Arc, time::Duration};
use zebra_chain::{
amount::{Amount, NonNegative},
block::{self, Block, Height},
history_tree::NonEmptyHistoryTree,
orchard,
parallel::tree::NoteCommitmentTrees,
parameters::{Network, NetworkUpgrade},
primitives::zcash_history::BlockCommitmentTreeRoots,
serialization::ZcashDeserializeInto,
subtree::NoteCommitmentSubtree,
transparent,
value_balance::ValueBalance,
};
use zebra_test::prelude::*;
use crate::{
arbitrary::Prepare,
service::{
finalized_state::{calculate_deferred_pool_balance_change, FinalizedState},
non_finalized_state::{Chain, NonFinalizedState, MIN_DURATION_BETWEEN_BACKUP_UPDATES},
ReconsiderError,
},
tests::FakeChainHelper,
Config,
};
#[test]
fn construct_empty() {
let _init_guard = zebra_test::init();
let _chain = Chain::new(
&Network::Mainnet,
Height(0),
NoteCommitmentTrees::default(),
Default::default(),
ValueBalance::zero(),
);
}
#[test]
fn construct_single() -> Result<()> {
let _init_guard = zebra_test::init();
let block: Arc<Block> =
zebra_test::vectors::BLOCK_MAINNET_434873_BYTES.zcash_deserialize_into()?;
let mut chain = Chain::new(
&Network::Mainnet,
Height(0),
NoteCommitmentTrees::default(),
Default::default(),
ValueBalance::fake_populated_pool(),
);
chain = chain.push(block.prepare().test_with_zero_spent_utxos())?;
assert_eq!(1, chain.blocks.len());
Ok(())
}
#[test]
fn construct_many() -> Result<()> {
let _init_guard = zebra_test::init();
let mut block: Arc<Block> =
zebra_test::vectors::BLOCK_MAINNET_434873_BYTES.zcash_deserialize_into()?;
let initial_height = block
.coinbase_height()
.expect("Block 434873 should have its height in its coinbase tx.");
let mut blocks = vec![];
while blocks.len() < 100 {
let next_block = block.make_fake_child();
blocks.push(block);
block = next_block;
}
let mut chain = Chain::new(
&Network::Mainnet,
(initial_height - 1).expect("Initial height should be at least 1."),
NoteCommitmentTrees::default(),
Default::default(),
ValueBalance::fake_populated_pool(),
);
for block in blocks {
chain = chain.push(block.prepare().test_with_zero_spent_utxos())?;
}
assert_eq!(100, chain.blocks.len());
Ok(())
}
#[test]
fn ord_matches_work() -> Result<()> {
let _init_guard = zebra_test::init();
let less_block = zebra_test::vectors::BLOCK_MAINNET_434873_BYTES
.zcash_deserialize_into::<Arc<Block>>()?
.set_work(1);
let more_block = less_block.clone().set_work(10);
let mut lesser_chain = Chain::new(
&Network::Mainnet,
Height(0),
NoteCommitmentTrees::default(),
Default::default(),
ValueBalance::fake_populated_pool(),
);
lesser_chain = lesser_chain.push(less_block.prepare().test_with_zero_spent_utxos())?;
let mut bigger_chain = Chain::new(
&Network::Mainnet,
Height(0),
NoteCommitmentTrees::default(),
Default::default(),
ValueBalance::zero(),
);
bigger_chain = bigger_chain.push(more_block.prepare().test_with_zero_spent_utxos())?;
assert!(bigger_chain > lesser_chain);
Ok(())
}
#[test]
fn best_chain_wins() -> Result<()> {
let _init_guard = zebra_test::init();
for network in Network::iter() {
best_chain_wins_for_network(network)?;
}
Ok(())
}
fn best_chain_wins_for_network(network: Network) -> Result<()> {
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2 = block1.make_fake_child().set_work(10);
let child = block1.make_fake_child().set_work(1);
let expected_hash = block2.hash();
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
state.commit_new_chain(block2.prepare(), &finalized_state)?;
state.commit_new_chain(child.prepare(), &finalized_state)?;
let best_chain = state.best_chain().unwrap();
assert!(best_chain.height_by_hash.contains_key(&expected_hash));
Ok(())
}
#[test]
fn finalize_pops_from_best_chain() -> Result<()> {
let _init_guard = zebra_test::init();
for network in Network::iter() {
finalize_pops_from_best_chain_for_network(network)?;
}
Ok(())
}
fn finalize_pops_from_best_chain_for_network(network: Network) -> Result<()> {
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2 = block1.make_fake_child().set_work(10);
let child = block1.make_fake_child().set_work(1);
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
finalized_state.set_finalized_value_pool(fake_value_pool);
state.commit_new_chain(block1.clone().prepare(), &finalized_state)?;
state.commit_block(block2.clone().prepare(), &finalized_state)?;
state.commit_block(child.prepare(), &finalized_state)?;
let finalized = state.finalize().inner_block();
assert_eq!(block1, finalized);
let finalized = state.finalize().inner_block();
assert_eq!(block2, finalized);
assert!(state.best_chain().is_none());
Ok(())
}
#[test]
fn invalidate_block_removes_block_and_descendants_from_chain() -> Result<()> {
let _init_guard = zebra_test::init();
for network in Network::iter() {
invalidate_block_removes_block_and_descendants_from_chain_for_network(network)?;
}
Ok(())
}
fn invalidate_block_removes_block_and_descendants_from_chain_for_network(
network: Network,
) -> Result<()> {
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2 = block1.make_fake_child().set_work(10);
let block3 = block2.make_fake_child().set_work(1);
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
finalized_state.set_finalized_value_pool(fake_value_pool);
state.commit_new_chain(block1.clone().prepare(), &finalized_state)?;
state.commit_block(block2.clone().prepare(), &finalized_state)?;
state.commit_block(block3.clone().prepare(), &finalized_state)?;
assert_eq!(
state
.best_chain()
.unwrap_or(&Arc::new(Chain::default()))
.blocks
.len(),
3
);
let _ = state.invalidate_block(block2.hash());
let post_invalidated_chain = state.best_chain().unwrap();
assert_eq!(post_invalidated_chain.blocks.len(), 1);
assert!(
post_invalidated_chain.contains_block_hash(block1.hash()),
"the new modified chain should contain block1"
);
assert!(
!post_invalidated_chain.contains_block_hash(block2.hash()),
"the new modified chain should not contain block2"
);
assert!(
!post_invalidated_chain.contains_block_hash(block3.hash()),
"the new modified chain should not contain block3"
);
let invalidated_blocks_state = &state.invalidated_blocks;
let (_, invalidated_blocks_state_descendants) = invalidated_blocks_state
.iter()
.find_map(|(height, blocks)| {
assert!(
blocks.iter().any(|block| block.hash == block2.hash()),
"invalidated_blocks should reference the hash of block2"
);
if blocks.iter().any(|block| block.hash == block2.hash()) {
Some((height, blocks))
} else {
None
}
})
.unwrap();
match network {
Network::Mainnet => assert!(
invalidated_blocks_state_descendants
.iter()
.any(|block| block.height == block::Height(653601)),
"invalidated descendants should contain block3"
),
Network::Testnet(_parameters) => assert!(
invalidated_blocks_state_descendants
.iter()
.any(|block| block.height == block::Height(584001)),
"invalidated descendants should contain block3"
),
}
Ok(())
}
fn new_invalidate_test_state(network: &Network) -> (NonFinalizedState, FinalizedState) {
let state = NonFinalizedState::new(network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
finalized_state.set_finalized_value_pool(ValueBalance::<NonNegative>::fake_populated_pool());
(state, finalized_state)
}
#[test]
fn invalidating_non_finalized_root_does_not_panic() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2 = block1.make_fake_child().set_work(10);
let (mut state, finalized_state) = new_invalidate_test_state(&network);
state
.commit_new_chain(block1.clone().prepare(), &finalized_state)
.expect("fake root block should commit to an empty non-finalized state");
state
.commit_block(block2.prepare(), &finalized_state)
.expect("fake child block should extend the fake root chain");
state
.invalidate_block(block1.hash())
.expect("invalidating the chain root should not panic");
assert!(
state.best_chain().is_none(),
"invalidating the root should leave no live non-finalized chain"
);
}
#[test]
fn invalidating_same_height_fork_tips_is_idempotent() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2a = block1.make_fake_child().set_work(10);
let block2b = block1.make_fake_child().set_work(11);
let (mut state, finalized_state) = new_invalidate_test_state(&network);
state
.commit_new_chain(block1.clone().prepare(), &finalized_state)
.expect("fake root block should commit to an empty non-finalized state");
state
.commit_block(block2a.clone().prepare(), &finalized_state)
.expect("first fork tip should extend the root chain");
state
.commit_block(block2b.clone().prepare(), &finalized_state)
.expect("second fork tip should fork from the root chain");
state
.invalidate_block(block2a.hash())
.expect("first fork tip should invalidate cleanly");
state
.invalidate_block(block2b.hash())
.expect("second sibling fork tip should not panic on collapse to shared parent");
let best_chain = state
.best_chain()
.expect("the parent chain should remain after both sibling tips are invalidated");
assert!(best_chain.contains_block_hash(block1.hash()));
assert!(!best_chain.contains_block_hash(block2a.hash()));
assert!(!best_chain.contains_block_hash(block2b.hash()));
}
#[test]
fn reconsider_block_removes_live_entry_and_second_call_returns_missing() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2 = block1.make_fake_child().set_work(10);
let block3 = block2.make_fake_child().set_work(1);
let (mut state, finalized_state) = new_invalidate_test_state(&network);
state
.commit_new_chain(block1.prepare(), &finalized_state)
.expect("fake root block should commit");
state
.commit_block(block2.clone().prepare(), &finalized_state)
.expect("fake child should extend the root chain");
state
.commit_block(block3.prepare(), &finalized_state)
.expect("fake grandchild should extend the child chain");
state
.invalidate_block(block2.hash())
.expect("invalidating the child should succeed");
state
.reconsider_block(block2.hash(), &finalized_state.db)
.expect("first reconsider should restore the invalidated chain");
assert!(
state.invalidated_blocks().values().all(|blocks| {
blocks
.first()
.map(|block| block.hash != block2.hash())
.unwrap_or(true)
}),
"first reconsider should remove the invalidated entry from the live map"
);
let second = state.reconsider_block(block2.hash(), &finalized_state.db);
assert!(
matches!(second, Err(ReconsiderError::MissingInvalidatedBlock(_))),
"a second reconsider for the same hash should return MissingInvalidatedBlock; got {second:?}"
);
}
#[test]
fn reconsider_block_preserves_record_when_parent_chain_missing() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2 = block1.make_fake_child().set_work(10);
let block3 = block2.make_fake_child().set_work(1);
let (mut state, finalized_state) = new_invalidate_test_state(&network);
state
.commit_new_chain(block1.clone().prepare(), &finalized_state)
.expect("fake root should commit");
state
.commit_block(block2.clone().prepare(), &finalized_state)
.expect("fake child should extend the root chain");
state
.commit_block(block3.prepare(), &finalized_state)
.expect("fake grandchild should extend the child chain");
state
.invalidate_block(block2.hash())
.expect("invalidating the child should succeed");
state
.invalidate_block(block1.hash())
.expect("invalidating the root should not panic and should succeed");
let result = state.reconsider_block(block2.hash(), &finalized_state.db);
assert!(
matches!(result, Err(ReconsiderError::ParentChainNotFound(_))),
"reconsider with missing parent chain should fail, got {result:?}"
);
assert!(
state.invalidated_blocks().values().any(|blocks| {
blocks
.first()
.map(|block| block.hash == block2.hash())
.unwrap_or(false)
}),
"a failed reconsider must not destroy the invalidation record"
);
}
#[test]
fn reconsider_block_and_reconsider_chain_correctly_reconsiders_blocks_and_descendants() -> Result<()>
{
let _init_guard = zebra_test::init();
for network in Network::iter() {
reconsider_block_inserts_block_and_descendants_into_chain_for_network(network.clone())?;
}
Ok(())
}
fn reconsider_block_inserts_block_and_descendants_into_chain_for_network(
network: Network,
) -> Result<()> {
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2 = block1.make_fake_child().set_work(10);
let block3 = block2.make_fake_child().set_work(1);
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
finalized_state.set_finalized_value_pool(fake_value_pool);
state.commit_new_chain(block1.clone().prepare(), &finalized_state)?;
state.commit_block(block2.clone().prepare(), &finalized_state)?;
state.commit_block(block3.clone().prepare(), &finalized_state)?;
assert_eq!(
state
.best_chain()
.unwrap_or(&Arc::new(Chain::default()))
.blocks
.len(),
3
);
let _ = state.invalidate_block(block2.hash());
let post_invalidated_chain = state.best_chain().unwrap();
assert_eq!(post_invalidated_chain.blocks.len(), 1);
assert!(
post_invalidated_chain.contains_block_hash(block1.hash()),
"the new modified chain should contain block1"
);
assert!(
!post_invalidated_chain.contains_block_hash(block2.hash()),
"the new modified chain should not contain block2"
);
assert!(
!post_invalidated_chain.contains_block_hash(block3.hash()),
"the new modified chain should not contain block3"
);
state.reconsider_block(block2.hash(), &finalized_state.db)?;
let best_chain = state.best_chain().unwrap();
assert!(
best_chain.contains_block_hash(block2.hash()),
"the best chain should again contain block2"
);
assert!(
best_chain.contains_block_hash(block3.hash()),
"the best chain should again contain block3"
);
Ok(())
}
#[test]
fn commit_block_extending_best_chain_doesnt_drop_worst_chains() -> Result<()> {
let _init_guard = zebra_test::init();
for network in Network::iter() {
commit_block_extending_best_chain_doesnt_drop_worst_chains_for_network(network)?;
}
Ok(())
}
fn commit_block_extending_best_chain_doesnt_drop_worst_chains_for_network(
network: Network,
) -> Result<()> {
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2 = block1.make_fake_child().set_work(10);
let child1 = block1.make_fake_child().set_work(1);
let child2 = block2.make_fake_child().set_work(1);
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
finalized_state.set_finalized_value_pool(fake_value_pool);
assert_eq!(0, state.chain_set.len());
state.commit_new_chain(block1.prepare(), &finalized_state)?;
assert_eq!(1, state.chain_set.len());
state.commit_block(block2.prepare(), &finalized_state)?;
assert_eq!(1, state.chain_set.len());
state.commit_block(child1.prepare(), &finalized_state)?;
assert_eq!(2, state.chain_set.len());
state.commit_block(child2.prepare(), &finalized_state)?;
assert_eq!(2, state.chain_set.len());
Ok(())
}
#[test]
fn shorter_chain_can_be_best_chain() -> Result<()> {
let _init_guard = zebra_test::init();
for network in Network::iter() {
shorter_chain_can_be_best_chain_for_network(network)?;
}
Ok(())
}
fn shorter_chain_can_be_best_chain_for_network(network: Network) -> Result<()> {
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let long_chain_block1 = block1.make_fake_child().set_work(1);
let long_chain_block2 = long_chain_block1.make_fake_child().set_work(1);
let short_chain_block = block1.make_fake_child().set_work(3);
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
finalized_state.set_finalized_value_pool(fake_value_pool);
state.commit_new_chain(block1.prepare(), &finalized_state)?;
state.commit_block(long_chain_block1.prepare(), &finalized_state)?;
state.commit_block(long_chain_block2.prepare(), &finalized_state)?;
state.commit_block(short_chain_block.prepare(), &finalized_state)?;
assert_eq!(2, state.chain_set.len());
assert_eq!(Some(2), state.best_chain_len());
Ok(())
}
#[test]
fn longer_chain_with_more_work_wins() -> Result<()> {
let _init_guard = zebra_test::init();
for network in Network::iter() {
longer_chain_with_more_work_wins_for_network(network)?;
}
Ok(())
}
fn longer_chain_with_more_work_wins_for_network(network: Network) -> Result<()> {
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let long_chain_block1 = block1.make_fake_child().set_work(1);
let long_chain_block2 = long_chain_block1.make_fake_child().set_work(1);
let long_chain_block3 = long_chain_block2.make_fake_child().set_work(1);
let long_chain_block4 = long_chain_block3.make_fake_child().set_work(1);
let short_chain_block = block1.make_fake_child().set_work(3);
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
finalized_state.set_finalized_value_pool(fake_value_pool);
state.commit_new_chain(block1.prepare(), &finalized_state)?;
state.commit_block(long_chain_block1.prepare(), &finalized_state)?;
state.commit_block(long_chain_block2.prepare(), &finalized_state)?;
state.commit_block(long_chain_block3.prepare(), &finalized_state)?;
state.commit_block(long_chain_block4.prepare(), &finalized_state)?;
state.commit_block(short_chain_block.prepare(), &finalized_state)?;
assert_eq!(2, state.chain_set.len());
assert_eq!(Some(5), state.best_chain_len());
Ok(())
}
#[test]
fn equal_length_goes_to_more_work() -> Result<()> {
let _init_guard = zebra_test::init();
for network in Network::iter() {
equal_length_goes_to_more_work_for_network(network)?;
}
Ok(())
}
fn equal_length_goes_to_more_work_for_network(network: Network) -> Result<()> {
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let less_work_child = block1.make_fake_child().set_work(1);
let more_work_child = block1.make_fake_child().set_work(3);
let expected_hash = more_work_child.hash();
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
finalized_state.set_finalized_value_pool(fake_value_pool);
state.commit_new_chain(block1.prepare(), &finalized_state)?;
state.commit_block(less_work_child.prepare(), &finalized_state)?;
state.commit_block(more_work_child.prepare(), &finalized_state)?;
assert_eq!(2, state.chain_set.len());
let tip_hash = state.best_tip().unwrap().1;
assert_eq!(expected_hash, tip_hash);
Ok(())
}
#[test]
fn history_tree_is_updated() -> Result<()> {
for network in Network::iter() {
history_tree_is_updated_for_network_upgrade(network, NetworkUpgrade::Heartwood)?;
}
Ok(())
}
fn history_tree_is_updated_for_network_upgrade(
network: Network,
network_upgrade: NetworkUpgrade,
) -> Result<()> {
let blocks = network.block_map();
let height = network_upgrade.activation_height(&network).unwrap().0;
let prev_block = Arc::new(
blocks
.get(&(height - 1))
.expect("test vector exists")
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid"),
);
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
state
.commit_new_chain(prev_block.clone().prepare(), &finalized_state)
.unwrap();
let chain = state.best_chain().unwrap();
if network_upgrade == NetworkUpgrade::Heartwood {
assert!(
chain.history_block_commitment_tree().as_ref().is_none(),
"history tree must not exist yet"
);
} else {
assert!(
chain.history_block_commitment_tree().as_ref().is_some(),
"history tree must already exist"
);
}
let activation_block = prev_block.make_fake_child().set_block_commitment([0u8; 32]);
state
.commit_block(activation_block.clone().prepare(), &finalized_state)
.unwrap();
let chain = state.best_chain().unwrap();
assert!(
chain.history_block_commitment_tree().as_ref().is_some(),
"history tree must have been (re)created"
);
assert_eq!(
chain
.history_block_commitment_tree()
.as_ref()
.as_ref()
.unwrap()
.size(),
1,
"history tree must have a single node"
);
let tree = NonEmptyHistoryTree::from_block(
&Network::Mainnet,
activation_block.clone(),
BlockCommitmentTreeRoots {
sapling: &chain.sapling_note_commitment_tree_for_tip().root(),
orchard: &chain.orchard_note_commitment_tree_for_tip().root(),
ironwood: &chain.ironwood_note_commitment_tree_for_tip().root(),
},
)
.unwrap();
let next_block = activation_block
.make_fake_child()
.set_block_commitment(tree.hash().into());
state
.commit_block(next_block.prepare(), &finalized_state)
.unwrap();
assert!(
state
.best_chain()
.unwrap()
.history_block_commitment_tree()
.as_ref()
.is_some(),
"history tree must still exist"
);
Ok(())
}
#[test]
fn commitment_is_validated() {
for network in Network::iter() {
commitment_is_validated_for_network_upgrade(network, NetworkUpgrade::Heartwood);
}
}
fn commitment_is_validated_for_network_upgrade(network: Network, network_upgrade: NetworkUpgrade) {
let blocks = network.block_map();
let height = network_upgrade.activation_height(&network).unwrap().0;
let prev_block = Arc::new(
blocks
.get(&(height - 1))
.expect("test vector exists")
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid"),
);
let mut state = NonFinalizedState::new(&network);
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
state
.commit_new_chain(prev_block.clone().prepare(), &finalized_state)
.unwrap();
let activation_block = prev_block.make_fake_child();
let err = state
.commit_block(activation_block.clone().prepare(), &finalized_state)
.unwrap_err();
match err {
crate::ValidateContextError::InvalidBlockCommitment(
zebra_chain::block::CommitmentError::InvalidChainHistoryActivationReserved { .. },
) => {},
_ => panic!("Error must be InvalidBlockCommitment::InvalidChainHistoryActivationReserved instead of {err:?}"),
};
let activation_block = activation_block.set_block_commitment([0u8; 32]);
state
.commit_block(activation_block.clone().prepare(), &finalized_state)
.unwrap();
let chain = state.best_chain().unwrap();
let tree = NonEmptyHistoryTree::from_block(
&Network::Mainnet,
activation_block.clone(),
BlockCommitmentTreeRoots {
sapling: &chain.sapling_note_commitment_tree_for_tip().root(),
orchard: &chain.orchard_note_commitment_tree_for_tip().root(),
ironwood: &chain.ironwood_note_commitment_tree_for_tip().root(),
},
)
.unwrap();
let next_block = activation_block.make_fake_child();
let err = state
.commit_block(next_block.clone().prepare(), &finalized_state)
.unwrap_err();
match err {
crate::ValidateContextError::InvalidBlockCommitment(
zebra_chain::block::CommitmentError::InvalidChainHistoryRoot { .. },
) => {}
_ => panic!(
"Error must be InvalidBlockCommitment::InvalidChainHistoryRoot instead of {err:?}"
),
};
let next_block = next_block.set_block_commitment(tree.hash().into());
state
.commit_block(next_block.prepare(), &finalized_state)
.unwrap();
}
#[tokio::test]
async fn non_finalized_state_writes_blocks_to_and_restores_blocks_from_backup_cache() {
let network = Network::Mainnet;
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)
.expect("opening an ephemeral database should succeed");
let backup_dir_path = tempfile::Builder::new()
.prefix("zebra-non-finalized-state-backup-cache")
.tempdir()
.expect("temporary directory is created successfully")
.keep();
let (mut non_finalized_state, non_finalized_state_sender, _receiver) =
NonFinalizedState::new(&network)
.with_backup(
Some(backup_dir_path.clone()),
&finalized_state.db,
false,
false,
)
.await;
let blocks = network.block_map();
let height = NetworkUpgrade::Heartwood
.activation_height(&network)
.unwrap()
.0;
let block = Arc::new(
blocks
.get(&(height - 1))
.expect("test vector exists")
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid"),
);
non_finalized_state
.commit_new_chain(block.into(), &finalized_state.db)
.expect("committing test block should succeed");
non_finalized_state_sender
.send(non_finalized_state.clone())
.expect("backup task should have a receiver, channel should be open");
tokio::time::sleep(Duration::from_secs(1) + MIN_DURATION_BETWEEN_BACKUP_UPDATES).await;
let (non_finalized_state, _sender, _receiver) = NonFinalizedState::new(&network)
.with_backup(Some(backup_dir_path), &finalized_state.db, true, false)
.await;
assert_eq!(
non_finalized_state.best_chain_len(),
Some(1),
"non-finalized state should have restored the block committed \
to the previous non-finalized state"
);
}
#[test]
fn fork_drops_subtrees_above_fork_point() -> Result<()> {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let block2 = block1.make_fake_child().set_work(10);
let block3 = block2.make_fake_child().set_work(1);
let mut chain = Chain::new(
&network,
(block1.coinbase_height().unwrap() - 1).unwrap(),
NoteCommitmentTrees::default(),
Default::default(),
ValueBalance::fake_populated_pool(),
);
chain = chain.push(block1.clone().prepare().test_with_zero_spent_utxos())?;
chain = chain.push(block2.clone().prepare().test_with_zero_spent_utxos())?;
chain = chain.push(block3.clone().prepare().test_with_zero_spent_utxos())?;
let tip_height = block3.coinbase_height().unwrap();
let sapling_node = sapling_crypto::Node::from_bytes([0; 32]).unwrap();
chain.insert_sapling_subtree(NoteCommitmentSubtree::new(0u16, tip_height, sapling_node));
let orchard_node = orchard::tree::Node::default();
chain.insert_orchard_subtree(NoteCommitmentSubtree::new(0u16, tip_height, orchard_node));
assert_eq!(chain.sapling_subtrees.len(), 1);
assert_eq!(chain.orchard_subtrees.len(), 1);
let forked = chain.fork(block1.hash()).expect("block1 is in the chain");
assert_eq!(forked.non_finalized_tip_hash(), block1.hash());
assert!(
forked.sapling_subtrees.is_empty(),
"fork should have dropped the Sapling subtree completed above the fork point"
);
assert!(
forked.orchard_subtrees.is_empty(),
"fork should have dropped the Orchard subtree completed above the fork point"
);
Ok(())
}
#[test]
fn deferred_pool_balance_change_uses_nsm_subsidy() -> Result<()> {
use zebra_chain::parameters::{
subsidy::{
block_subsidy_with_parent_pools, funding_stream_values, scheduled_block_subsidy,
FundingStreamReceiver,
},
testnet::{
ConfiguredActivationHeights, ConfiguredFundingStreamRecipient,
ConfiguredFundingStreams, RegtestParameters,
},
};
let _init_guard = zebra_test::init();
let network = Network::new_regtest(RegtestParameters {
activation_heights: ConfiguredActivationHeights {
nu5: Some(1),
nu6: Some(1),
nu6_3: Some(1),
nu7: Some(10),
..Default::default()
},
funding_streams: Some(vec![ConfiguredFundingStreams {
height_range: Some(Height(1)..Height(100)),
recipients: Some(vec![ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Deferred,
numerator: 12,
addresses: None,
}]),
}]),
nsm_reissuance_height: Some(Height(10)),
..Default::default()
});
let deferred = |height, subsidy| {
funding_stream_values(height, &network, subsidy)
.unwrap()
.remove(&FundingStreamReceiver::Deferred)
.expect("the deferred funding stream is active")
};
let height = Height(12);
let mut parent_pools = ValueBalance::<NonNegative>::zero();
parent_pools.set_nsm_amount(Amount::try_from(1_000_000_000_i64)?);
let subsidy = block_subsidy_with_parent_pools(height, &network, parent_pools)?;
let expected = deferred(height, subsidy);
assert_ne!(
expected,
deferred(height, scheduled_block_subsidy(height, &network)?)
);
assert_eq!(
calculate_deferred_pool_balance_change(height, &network, parent_pools)?.value(),
expected.constrain::<zebra_chain::amount::NegativeAllowed>()?,
);
let height = Height(9);
assert_eq!(
calculate_deferred_pool_balance_change(height, &network, parent_pools)?,
calculate_deferred_pool_balance_change(height, &network, ValueBalance::zero())?,
);
Ok(())
}
#[test]
fn nsm_subsidy_tracks_the_nsm_value_balance() -> Result<()> {
use chrono::Duration;
use zebra_chain::{
block::{
ChainHistoryBlockTxAuthCommitmentHash, ChainHistoryMmrRootHash,
CHAIN_HISTORY_ACTIVATION_RESERVED,
},
parameters::{
subsidy::{
additional_block_subsidy, block_subsidy_with_parent_pools, nsm_fee_contribution,
scheduled_block_subsidy, CoinbaseTransactionError, SubsidyError,
},
testnet::{ConfiguredActivationHeights, RegtestParameters},
},
transaction::{self, LockTime, Transaction},
};
use crate::ValidateContextError;
let _init_guard = zebra_test::init();
const ACTIVATION_HEIGHT: u32 = 11;
const DEPLOYMENT_HEIGHT: u32 = ACTIVATION_HEIGHT + 2;
const INITIAL_NSM_VALUE_BALANCE: i64 = 1_000_000_000;
const FUNDING_VALUE: i64 = 10_000;
const FEES: [i64; 2] = [1_001, 1];
let zats = |zats: i64| Amount::<NonNegative>::try_from(zats).expect("valid amount");
let network = Network::new_regtest(RegtestParameters {
activation_heights: ConfiguredActivationHeights {
sapling: Some(1),
blossom: Some(1),
heartwood: Some(ACTIVATION_HEIGHT),
canopy: Some(ACTIVATION_HEIGHT),
nu5: Some(ACTIVATION_HEIGHT),
nu6: Some(ACTIVATION_HEIGHT),
nu6_3: Some(ACTIVATION_HEIGHT),
nu7: Some(ACTIVATION_HEIGHT),
..Default::default()
},
funding_streams: Some(Vec::new()),
initial_nsm_value_balance: Some(zats(INITIAL_NSM_VALUE_BALANCE)),
nsm_reissuance_height: Some(Height(DEPLOYMENT_HEIGHT)),
..Default::default()
});
let funding_outpoints = |height: Height| {
(0..FEES.len() as u32).map(move |index| transparent::OutPoint {
hash: transaction::Hash([height.0 as u8; 32]),
index,
})
};
let funding_output = || transparent::Output {
value: zats(FUNDING_VALUE),
lock_script: transparent::Script::new(&[]),
};
let prepare_with_funding = |block: Arc<Block>, height: Height| {
let mut prepared = block.prepare();
prepared
.new_outputs
.extend(funding_outpoints(height).map(|outpoint| {
let utxo = transparent::Utxo::new(funding_output(), height, false);
(outpoint, transparent::OrderedUtxo::from_utxo(utxo, 0))
}));
prepared
};
let spends = |height: Height| -> Vec<Arc<Transaction>> {
funding_outpoints(height)
.zip(FEES)
.map(|(outpoint, fee)| {
Arc::new(Transaction::test_v4(
vec![transparent::Input::PrevOut {
outpoint,
unlock_script: transparent::Script::new(&[]),
sequence: u32::MAX,
}],
vec![transparent::Output {
value: zats(FUNDING_VALUE - fee),
lock_script: transparent::Script::new(&[]),
}],
LockTime::unlocked(),
height,
))
})
.collect()
};
let fees = zats(FEES.iter().sum());
let block = |height: Height,
previous_block_hash,
coinbase_value,
transactions: Vec<Arc<Transaction>>|
-> Arc<Block> {
let mut block = zebra_test::vectors::BLOCK_MAINNET_347499_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let header = Arc::make_mut(&mut block.header);
header.time += Duration::minutes(height.0.into());
block.transactions = vec![Arc::new(Transaction::test_v4(
vec![transparent::Input::Coinbase {
height,
data: vec![0],
sequence: u32::MAX,
}],
vec![transparent::Output {
value: coinbase_value,
lock_script: transparent::Script::new(&[]),
}],
LockTime::unlocked(),
height,
))];
block.transactions.extend(transactions);
Arc::make_mut(&mut block.header).previous_block_hash = previous_block_hash;
Arc::new(block)
};
let committed_block = |height: Height,
previous_block_hash,
coinbase_value,
transactions,
history_root: &ChainHistoryMmrRootHash| {
let child = block(height, previous_block_hash, coinbase_value, transactions);
let commitment = ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
history_root,
&child.auth_data_root(),
);
child.set_block_commitment(commitment.into())
};
let subsidy_block = |height: Height,
previous_block_hash,
parent_pools,
fees: Amount<NonNegative>,
transactions,
history_root: &_| {
let subsidy = block_subsidy_with_parent_pools(height, &network, parent_pools)
.expect("the block subsidy is valid");
let miner_fees = (fees - nsm_fee_contribution(height, &network, fees))
.expect("the contribution is at most the fees");
committed_block(
height,
previous_block_hash,
(subsidy + miner_fees).expect("valid amount"),
transactions,
history_root,
)
};
let finalized_state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
)?;
let parent_height = Height(ACTIVATION_HEIGHT - 1);
let activation = Height(ACTIVATION_HEIGHT);
let parent = block(
parent_height,
block::Hash([1; 32]),
scheduled_block_subsidy(parent_height, &network)?,
Vec::new(),
);
let mut state = NonFinalizedState::new(&network);
state.commit_new_chain(parent.clone().prepare(), &finalized_state)?;
let pools_before_activation = state
.best_chain()
.expect("the chain was just committed")
.chain_value_pools;
assert_eq!(pools_before_activation.nsm_amount(), zats(0));
let initial = zats(INITIAL_NSM_VALUE_BALANCE);
let activation_block = subsidy_block(
activation,
parent.hash(),
pools_before_activation,
zats(0),
Vec::new(),
&CHAIN_HISTORY_ACTIVATION_RESERVED.into(),
);
assert_eq!(
activation_block.transactions[0].outputs()[0].value(),
scheduled_block_subsidy(activation, &network)?,
);
state.commit_block(activation_block.clone().prepare(), &finalized_state)?;
let chain = state
.best_chain()
.expect("the activation block was committed");
assert_eq!(chain.chain_value_pools.nsm_amount(), initial);
let sapling_root = chain.sapling_note_commitment_tree_for_tip().root();
let orchard_root = chain.orchard_note_commitment_tree_for_tip().root();
let ironwood_root = chain.ironwood_note_commitment_tree_for_tip().root();
let roots = || BlockCommitmentTreeRoots {
sapling: &sapling_root,
orchard: &orchard_root,
ironwood: &ironwood_root,
};
let mut tree = NonEmptyHistoryTree::from_block(&network, activation_block, roots())?;
let mut previous = state.best_tip().expect("the chain has a tip");
for height in (ACTIVATION_HEIGHT + 1..=DEPLOYMENT_HEIGHT + 2).map(Height) {
let parent_pools = state
.best_chain()
.expect("the chain has blocks")
.chain_value_pools;
let reissued = additional_block_subsidy(height, &network, parent_pools.nsm_amount());
let contributed = nsm_fee_contribution(height, &network, fees);
let miner_fees = (fees - contributed)?;
assert_eq!(contributed, zats(601));
if height < Height(DEPLOYMENT_HEIGHT) {
assert_eq!(
reissued,
zats(0),
"no reissuance before the deployment height"
);
} else {
assert!(reissued > zats(0), "reissuance from the deployment height");
let subsidy = block_subsidy_with_parent_pools(height, &network, parent_pools)?;
let mut invalid_coinbase_values =
vec![(scheduled_block_subsidy(height, &network)? + miner_fees)?];
if !contributed.is_zero() {
invalid_coinbase_values.push((subsidy + fees)?);
}
for coinbase_value in invalid_coinbase_values {
let invalid = committed_block(
height,
previous.1,
coinbase_value,
spends(height),
&tree.hash(),
);
assert!(matches!(
state.commit_block(prepare_with_funding(invalid, height), &finalized_state),
Err(ValidateContextError::InvalidSubsidy {
subsidy_error: CoinbaseTransactionError::Subsidy(
SubsidyError::InvalidMinerFees
),
..
})
));
}
}
let next_block = subsidy_block(
height,
previous.1,
parent_pools,
fees,
spends(height),
&tree.hash(),
);
state.commit_block(
prepare_with_funding(next_block.clone(), height),
&finalized_state,
)?;
let chain = state.best_chain().expect("the block was committed");
assert_eq!(
chain.chain_value_pools.nsm_amount(),
((parent_pools.nsm_amount() - reissued)? + contributed)?,
);
tree.push(next_block, roots())?;
previous = state.best_tip().expect("the chain has a tip");
}
Ok(())
}
#[test]
fn equal_work_ties_prefer_first_received() -> Result<()> {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let block1: Arc<Block> = Arc::new(network.test_block(653599, 583999).unwrap());
let sibling_a = block1
.make_fake_child()
.set_work(10)
.set_block_commitment([0x01; 32]);
let sibling_b = block1
.make_fake_child()
.set_work(10)
.set_block_commitment([0x02; 32]);
assert_ne!(
sibling_a.hash(),
sibling_b.hash(),
"siblings must have distinct hashes"
);
for (first, second) in [(&sibling_a, &sibling_b), (&sibling_b, &sibling_a)] {
let (mut state, finalized_state) = new_invalidate_test_state(&network);
state
.commit_new_chain(block1.clone().prepare(), &finalized_state)
.expect("fake root block should commit to an empty non-finalized state");
let earlier = std::time::Instant::now();
let later = earlier + Duration::from_secs(1);
let mut first_prepared = (*first).clone().prepare();
first_prepared.received_time = Some(earlier);
let mut second_prepared = (*second).clone().prepare();
second_prepared.received_time = Some(later);
state
.commit_block(first_prepared, &finalized_state)
.expect("first sibling should commit");
assert_eq!(
state.best_chain().unwrap().non_finalized_tip_hash(),
first.hash(),
"the first-received sibling is adopted as the best tip"
);
state
.commit_block(second_prepared, &finalized_state)
.expect("second sibling should commit");
assert_eq!(2, state.chain_set.len(), "both sibling chains are tracked");
assert_eq!(
state.best_chain().unwrap().non_finalized_tip_hash(),
first.hash(),
"a later equal-work sibling must not displace the adopted tip"
);
let second_child = second.make_fake_child().set_work(10);
state
.commit_block(second_child.clone().prepare(), &finalized_state)
.expect("child of the second sibling should commit");
assert_eq!(
state.best_chain().unwrap().non_finalized_tip_hash(),
second_child.hash(),
"more cumulative work must override receipt order"
);
}
Ok(())
}