diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs index 8a22afec070..2b3fafb1381 100644 --- a/zebra-chain/src/orchard_zsa.rs +++ b/zebra-chain/src/orchard_zsa.rs @@ -4,8 +4,8 @@ #[cfg(any(test, feature = "proptest-impl"))] pub(crate) mod arbitrary; -#[cfg(test)] -mod tests; +#[cfg(any(test, feature = "proptest-impl"))] +pub mod tests; mod asset_state; mod burn; diff --git a/zebra-chain/src/orchard_zsa/tests.rs b/zebra-chain/src/orchard_zsa/tests.rs index a9301a7461e..0f61d2fcdd8 100644 --- a/zebra-chain/src/orchard_zsa/tests.rs +++ b/zebra-chain/src/orchard_zsa/tests.rs @@ -1,2 +1,4 @@ +#[cfg(test)] mod issuance; -mod vectors; + +pub mod vectors; diff --git a/zebra-chain/src/orchard_zsa/tests/issuance.rs b/zebra-chain/src/orchard_zsa/tests/issuance.rs index 9dc90d881c7..6eb0c586641 100644 --- a/zebra-chain/src/orchard_zsa/tests/issuance.rs +++ b/zebra-chain/src/orchard_zsa/tests/issuance.rs @@ -1,11 +1,17 @@ -use crate::{block::Block, serialization::ZcashDeserialize, transaction::Transaction}; +use crate::{ + block::Block, orchard_zsa::IssuedAssetsChange, serialization::ZcashDeserialize, + transaction::Transaction, +}; use super::vectors::BLOCKS; #[test] fn issuance_block() { let issuance_block = - Block::zcash_deserialize(BLOCKS[0].as_ref()).expect("issuance block should deserialize"); + Block::zcash_deserialize(BLOCKS[0]).expect("issuance block should deserialize"); + + IssuedAssetsChange::from_transactions(&issuance_block.transactions) + .expect("issuance in block should be valid"); for transaction in issuance_block.transactions { if let Transaction::V6 { diff --git a/zebra-chain/src/orchard_zsa/tests/vectors.rs b/zebra-chain/src/orchard_zsa/tests/vectors.rs index d5664e50b19..2812ee86c35 100644 --- a/zebra-chain/src/orchard_zsa/tests/vectors.rs +++ b/zebra-chain/src/orchard_zsa/tests/vectors.rs @@ -1,3 +1,19 @@ mod blocks; +use std::sync::Arc; + pub(crate) use blocks::BLOCKS; +use itertools::Itertools; + +use crate::{block::Block, serialization::ZcashDeserializeInto}; + +// TODO: Move this to zebra-test. +pub fn valid_issuance_blocks() -> Vec> { + BLOCKS + .iter() + .copied() + .map(ZcashDeserializeInto::zcash_deserialize_into) + .map(|result| result.map(Arc::new)) + .try_collect() + .expect("hard-coded block data must deserialize successfully") +} diff --git a/zebra-state/src/service/check/issuance.rs b/zebra-state/src/service/check/issuance.rs index abff882c33c..b130e1c9f90 100644 --- a/zebra-state/src/service/check/issuance.rs +++ b/zebra-state/src/service/check/issuance.rs @@ -59,9 +59,7 @@ pub fn valid_burns_and_issuance( .apply_change(change) .ok_or(ValidateContextError::InvalidIssuance)?; - issued_assets - .insert(asset_base, updated_asset_state) - .expect("transactions must have only one burn item per asset base"); + issued_assets.insert(asset_base, updated_asset_state); } } diff --git a/zebra-state/src/service/check/tests.rs b/zebra-state/src/service/check/tests.rs index 9608105766d..8d51105ea26 100644 --- a/zebra-state/src/service/check/tests.rs +++ b/zebra-state/src/service/check/tests.rs @@ -4,3 +4,6 @@ mod anchors; mod nullifier; mod utxo; mod vectors; + +#[cfg(feature = "tx-v6")] +mod issuance; diff --git a/zebra-state/src/service/check/tests/issuance.rs b/zebra-state/src/service/check/tests/issuance.rs new file mode 100644 index 00000000000..71db34328bf --- /dev/null +++ b/zebra-state/src/service/check/tests/issuance.rs @@ -0,0 +1,83 @@ +use std::sync::Arc; + +use zebra_chain::{ + block::{self, genesis::regtest_genesis_block, Block}, + orchard_zsa::{tests::vectors::valid_issuance_blocks, IssuedAssets}, + parameters::Network, +}; + +use crate::{ + check::{self, Chain}, + service::{finalized_state::FinalizedState, write::validate_and_commit_non_finalized}, + CheckpointVerifiedBlock, Config, NonFinalizedState, +}; + +#[test] +fn check_burns_and_issuance() { + let _init_guard = zebra_test::init(); + + let network = Network::new_regtest(Some(1), None, Some(1)); + + let mut finalized_state = FinalizedState::new_with_debug( + &Config::ephemeral(), + &network, + true, + #[cfg(feature = "elasticsearch")] + false, + false, + ); + + let mut non_finalized_state = NonFinalizedState::new(&network); + + let regtest_genesis_block = regtest_genesis_block(); + let regtest_genesis_hash = regtest_genesis_block.hash(); + + finalized_state + .commit_finalized_direct(regtest_genesis_block.into(), None, "test") + .expect("unexpected invalid genesis block test vector"); + + let block = valid_issuance_blocks().first().unwrap().clone(); + let mut header = Arc::::unwrap_or_clone(block.header.clone()); + header.previous_block_hash = regtest_genesis_hash; + header.commitment_bytes = [0; 32].into(); + let block = Arc::new(Block { + header: Arc::new(header), + transactions: block.transactions.clone(), + }); + + let CheckpointVerifiedBlock(block) = CheckpointVerifiedBlock::new(block, None, None) + .expect("semantic validation of issued assets changes should pass"); + + let empty_chain = Chain::new( + &network, + finalized_state + .db + .finalized_tip_height() + .unwrap_or(block::Height::MIN), + finalized_state.db.sprout_tree_for_tip(), + finalized_state.db.sapling_tree_for_tip(), + finalized_state.db.orchard_tree_for_tip(), + finalized_state.db.history_tree(), + finalized_state.db.finalized_value_pool(), + ); + + let block_1_issued_assets = check::issuance::valid_burns_and_issuance( + &finalized_state.db, + &Arc::new(empty_chain), + &block, + ) + .expect("test transactions should be valid"); + + validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block) + .expect("validation should succeed"); + + let best_chain = non_finalized_state + .best_chain() + .expect("should have a non-finalized chain"); + + assert_eq!( + IssuedAssets::from(best_chain.issued_assets.clone()), + block_1_issued_assets, + "issued assets for chain should match those of block 1" + ); +}