Zebra/zebra-state/src/service/check/tests/utxo.rs

981 lines
39 KiB
Rust

//! Test vectors and randomised property tests for UTXO contextual validation
use std::{env, sync::Arc};
use proptest::prelude::*;
use zebra_chain::{
amount::Amount,
block::{Block, Height},
fmt::TypeNameToDebug,
serialization::ZcashDeserializeInto,
transaction::{self, LockTime, Transaction},
transparent,
};
use crate::{
arbitrary::Prepare,
constants::MIN_TRANSPARENT_COINBASE_MATURITY,
service::{
check, finalized_state::FinalizedState, non_finalized_state::NonFinalizedState, read,
write::validate_and_commit_non_finalized,
},
tests::setup::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
CheckpointVerifiedBlock,
ValidateContextError::{
DuplicateTransparentSpend, EarlyTransparentSpend, ImmatureTransparentCoinbaseSpend,
MissingTransparentOutput, UnshieldedTransparentCoinbaseSpend,
},
};
/// Check that shielded, mature spends of coinbase transparent outputs succeed.
///
/// This test makes sure there are no spurious rejections that might hide bugs in the other tests.
/// (And that the test infrastructure generally works.)
#[test]
fn accept_shielded_mature_coinbase_utxo_spend() {
let _init_guard = zebra_test::init();
let created_height = Height(1);
let outpoint = transparent::OutPoint {
hash: transaction::Hash([0u8; 32]),
index: 0,
};
let output = transparent::Output {
value: Amount::zero(),
lock_script: transparent::Script::new(&[]),
};
let ordered_utxo = transparent::OrderedUtxo::new(output, created_height, 0);
let min_spend_height = Height(created_height.0 + MIN_TRANSPARENT_COINBASE_MATURITY);
let spend_restriction = transparent::CoinbaseSpendRestriction::OnlyShieldedOutputs {
spend_height: min_spend_height,
};
let result =
check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, ordered_utxo.as_ref());
assert_eq!(
result,
Ok(()),
"mature transparent coinbase spend check should return Ok(())"
);
}
/// Check that non-shielded spends of coinbase transparent outputs fail.
#[test]
fn reject_unshielded_coinbase_utxo_spend() {
let _init_guard = zebra_test::init();
let created_height = Height(1);
let outpoint = transparent::OutPoint {
hash: transaction::Hash([0u8; 32]),
index: 0,
};
let output = transparent::Output {
value: Amount::zero(),
lock_script: transparent::Script::new(&[]),
};
let ordered_utxo = transparent::OrderedUtxo::new(output, created_height, 0);
let spend_restriction = transparent::CoinbaseSpendRestriction::SomeTransparentOutputs;
let result =
check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, ordered_utxo.as_ref());
assert_eq!(result, Err(UnshieldedTransparentCoinbaseSpend { outpoint }));
}
/// Check that early spends of coinbase transparent outputs fail.
#[test]
fn reject_immature_coinbase_utxo_spend() {
let _init_guard = zebra_test::init();
let created_height = Height(1);
let outpoint = transparent::OutPoint {
hash: transaction::Hash([0u8; 32]),
index: 0,
};
let output = transparent::Output {
value: Amount::zero(),
lock_script: transparent::Script::new(&[]),
};
let ordered_utxo = transparent::OrderedUtxo::new(output, created_height, 0);
let min_spend_height = Height(created_height.0 + MIN_TRANSPARENT_COINBASE_MATURITY);
let spend_height = Height(min_spend_height.0 - 1);
let spend_restriction =
transparent::CoinbaseSpendRestriction::OnlyShieldedOutputs { spend_height };
let result =
check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, ordered_utxo.as_ref());
assert_eq!(
result,
Err(ImmatureTransparentCoinbaseSpend {
outpoint,
spend_height,
min_spend_height,
created_height
})
);
}
// These tests use the `Arbitrary` trait to easily generate complex types,
// then modify those types to cause an error (or to ensure success).
//
// We could use mainnet or testnet blocks in these tests,
// but the differences shouldn't matter,
// because we're only interested in spend validation,
// (and passing various other state checks).
const DEFAULT_UTXO_PROPTEST_CASES: u32 = 16;
proptest! {
#![proptest_config(
proptest::test_runner::Config::with_cases(env::var("PROPTEST_CASES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_UTXO_PROPTEST_CASES))
)]
/// Make sure an arbitrary transparent spend from a previous transaction in this block
/// is accepted by state contextual validation.
///
/// This test makes sure there are no spurious rejections that might hide bugs in the other tests.
/// (And that the test infrastructure generally works.)
///
/// It also covers a potential edge case where later transactions can spend outputs
/// of previous transactions in a block, but earlier transactions can not spend later outputs.
#[test]
fn accept_later_transparent_spend_from_this_block(
output in TypeNameToDebug::<transparent::Output>::arbitrary(),
mut prevout_input in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
use_finalized_state in any::<bool>(),
) {
let _init_guard = zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
// create an output
let output_transaction = transaction_v4_with_transparent_data([], [], [output.0.clone()]);
// create a spend
let expected_outpoint = transparent::OutPoint {
hash: output_transaction.hash(),
index: 0,
};
prevout_input.set_outpoint(expected_outpoint);
let spend_transaction = transaction_v4_with_transparent_data(
[prevout_input.0],
[(expected_outpoint, output.0)],
[]
);
// convert the coinbase transaction to a version that the non-finalized state will accept
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1
.transactions
.extend([output_transaction.into(), spend_transaction.into()]);
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
let previous_non_finalized_state = non_finalized_state.clone();
// randomly choose to commit the block to the finalized or non-finalized state
if use_finalized_state {
let block1 = CheckpointVerifiedBlock::from(Arc::new(block1));
let commit_result = finalized_state.commit_finalized_direct(block1.clone().into(), "test");
// the block was committed
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(commit_result.is_ok());
// the non-finalized state didn't change
prop_assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
// the finalized state added then spent the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
// the non-finalized state does not have the UTXO
prop_assert!(non_finalized_state.any_utxo(&expected_outpoint).is_none());
} else {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1.clone()
);
// the block was committed
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
// the block data is in the non-finalized state
prop_assert!(!non_finalized_state.eq_internal_state(&previous_non_finalized_state));
// the non-finalized state has created and spent the UTXO
prop_assert_eq!(non_finalized_state.chain_count(), 1);
let chain = non_finalized_state
.chain_iter()
.next()
.unwrap();
prop_assert!(!chain.unspent_utxos().contains_key(&expected_outpoint));
prop_assert!(chain.created_utxos.contains_key(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
}
/// Make sure an arbitrary transparent spend from a previous block
/// is accepted by state contextual validation.
#[test]
fn accept_arbitrary_transparent_spend_from_previous_block(
output in TypeNameToDebug::<transparent::Output>::arbitrary(),
mut prevout_input in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
use_finalized_state_output in any::<bool>(),
mut use_finalized_state_spend in any::<bool>(),
) {
let _init_guard = zebra_test::init();
// if we use the non-finalized state for the first block,
// we have to use it for the second as well
if !use_finalized_state_output {
use_finalized_state_spend = false;
}
let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let TestState {
mut finalized_state, mut non_finalized_state, block1, ..
} = new_state_with_mainnet_transparent_data([], [], [output.0.clone()], use_finalized_state_output);
let previous_non_finalized_state = non_finalized_state.clone();
let expected_outpoint = transparent::OutPoint {
hash: block1.transactions[1].hash(),
index: 0,
};
prevout_input.set_outpoint(expected_outpoint);
let spend_transaction = transaction_v4_with_transparent_data(
[prevout_input.0],
[(expected_outpoint, output.0)],
[]
);
// convert the coinbase transaction to a version that the non-finalized state will accept
block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into();
block2.transactions.push(spend_transaction.into());
if use_finalized_state_spend {
let block2 = CheckpointVerifiedBlock::from(Arc::new(block2));
let commit_result = finalized_state.commit_finalized_direct(block2.clone().into(), "test");
// the block was committed
prop_assert_eq!(Some((Height(2), block2.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(commit_result.is_ok());
// the non-finalized state didn't change
prop_assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
// the finalized state has spent the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
} else {
let block2 = Arc::new(block2).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block2.clone()
);
// the block was committed
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(2), block2.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
// the block data is in the non-finalized state
prop_assert!(!non_finalized_state.eq_internal_state(&previous_non_finalized_state));
// the UTXO is spent
prop_assert_eq!(non_finalized_state.chain_count(), 1);
let chain = non_finalized_state
.chain_iter()
.next()
.unwrap();
prop_assert!(!chain.unspent_utxos().contains_key(&expected_outpoint));
if use_finalized_state_output {
// the chain has spent the UTXO from the finalized state
prop_assert!(!chain.created_utxos.contains_key(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
// the finalized state has the UTXO, but it will get deleted on commit
prop_assert!(finalized_state.utxo(&expected_outpoint).is_some());
} else {
// the chain has spent its own UTXO
prop_assert!(!chain.unspent_utxos().contains_key(&expected_outpoint));
prop_assert!(chain.created_utxos.contains_key(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
}
}
/// Make sure a duplicate transparent spend, by two inputs in the same transaction,
/// using an output from a previous transaction in this block,
/// is rejected by state contextual validation.
#[test]
fn reject_duplicate_transparent_spend_in_same_transaction_from_same_block(
output in TypeNameToDebug::<transparent::Output>::arbitrary(),
mut prevout_input1 in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
mut prevout_input2 in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
) {
let _init_guard = zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let output_transaction = transaction_v4_with_transparent_data([], [], [output.0.clone()]);
let expected_outpoint = transparent::OutPoint {
hash: output_transaction.hash(),
index: 0,
};
prevout_input1.set_outpoint(expected_outpoint);
prevout_input2.set_outpoint(expected_outpoint);
let spend_transaction = transaction_v4_with_transparent_data(
[prevout_input1.0, prevout_input2.0],
[(expected_outpoint, output.0)],
[]
);
// convert the coinbase transaction to a version that the non-finalized state will accept
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1
.transactions
.extend([output_transaction.into(), spend_transaction.into()]);
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
let previous_non_finalized_state = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1
);
// the block was rejected
prop_assert_eq!(
commit_result,
Err(DuplicateTransparentSpend {
outpoint: expected_outpoint,
location: "the same block",
}
.into())
);
prop_assert_eq!(Some((Height(0), genesis.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
// the non-finalized state did not change
prop_assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
/// Make sure a duplicate transparent spend, by two inputs in the same transaction,
/// using an output from a previous block in this chain,
/// is rejected by state contextual validation.
#[test]
fn reject_duplicate_transparent_spend_in_same_transaction_from_previous_block(
output in TypeNameToDebug::<transparent::Output>::arbitrary(),
mut prevout_input1 in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
mut prevout_input2 in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
use_finalized_state_output in any::<bool>(),
) {
let _init_guard = zebra_test::init();
let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let TestState {
finalized_state, mut non_finalized_state, block1, ..
} = new_state_with_mainnet_transparent_data([], [], [output.0.clone()], use_finalized_state_output);
let previous_non_finalized_state = non_finalized_state.clone();
let expected_outpoint = transparent::OutPoint {
hash: block1.transactions[1].hash(),
index: 0,
};
prevout_input1.set_outpoint(expected_outpoint);
prevout_input2.set_outpoint(expected_outpoint);
let spend_transaction = transaction_v4_with_transparent_data(
[prevout_input1.0, prevout_input2.0],
[(expected_outpoint, output.0)],
[]
);
// convert the coinbase transaction to a version that the non-finalized state will accept
block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into();
block2.transactions.push(spend_transaction.into());
let block2 = Arc::new(block2).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block2
);
// the block was rejected
prop_assert_eq!(
commit_result,
Err(DuplicateTransparentSpend {
outpoint: expected_outpoint,
location: "the same block",
}
.into())
);
prop_assert_eq!(Some((Height(1), block1.hash())), read::best_tip(&non_finalized_state, &finalized_state.db));
// the non-finalized state did not change
prop_assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
if use_finalized_state_output {
// the finalized state has the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_some());
// the non-finalized state has no chains (so it can't have the UTXO)
prop_assert!(non_finalized_state.chain_iter().next().is_none());
} else {
let chain = non_finalized_state
.chain_iter()
.next()
.unwrap();
// the non-finalized state has the UTXO
prop_assert!(chain.unspent_utxos().contains_key(&expected_outpoint));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
}
/// Make sure a duplicate transparent spend,
/// by two inputs in different transactions in the same block,
/// using an output from a previous block in this chain,
/// is rejected by state contextual validation.
#[test]
fn reject_duplicate_transparent_spend_in_same_block_from_previous_block(
output in TypeNameToDebug::<transparent::Output>::arbitrary(),
mut prevout_input1 in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
mut prevout_input2 in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
use_finalized_state_output in any::<bool>(),
) {
let _init_guard = zebra_test::init();
let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let TestState {
finalized_state, mut non_finalized_state, block1, ..
} = new_state_with_mainnet_transparent_data([], [], [output.0.clone()], use_finalized_state_output);
let previous_non_finalized_state = non_finalized_state.clone();
let expected_outpoint = transparent::OutPoint {
hash: block1.transactions[1].hash(),
index: 0,
};
prevout_input1.set_outpoint(expected_outpoint);
prevout_input2.set_outpoint(expected_outpoint);
let spend_transaction1 = transaction_v4_with_transparent_data(
[prevout_input1.0],
[(expected_outpoint, output.0.clone())],
[]
);
let spend_transaction2 = transaction_v4_with_transparent_data(
[prevout_input2.0],
[(expected_outpoint, output.0)],
[]
);
// convert the coinbase transaction to a version that the non-finalized state will accept
block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into();
block2
.transactions
.extend([spend_transaction1.into(), spend_transaction2.into()]);
let block2 = Arc::new(block2).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block2
);
// the block was rejected
prop_assert_eq!(
commit_result,
Err(DuplicateTransparentSpend {
outpoint: expected_outpoint,
location: "the same block",
}
.into())
);
prop_assert_eq!(Some((Height(1), block1.hash())), read::best_tip(&non_finalized_state, &finalized_state.db));
// the non-finalized state did not change
prop_assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
if use_finalized_state_output {
// the finalized state has the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_some());
// the non-finalized state has no chains (so it can't have the UTXO)
prop_assert!(non_finalized_state.chain_iter().next().is_none());
} else {
let chain = non_finalized_state
.chain_iter()
.next()
.unwrap();
// the non-finalized state has the UTXO
prop_assert!(chain.unspent_utxos().contains_key(&expected_outpoint));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
}
/// Make sure a duplicate transparent spend,
/// by two inputs in different blocks in the same chain,
/// using an output from a previous block in this chain,
/// is rejected by state contextual validation.
#[test]
fn reject_duplicate_transparent_spend_in_same_chain_from_previous_block(
output in TypeNameToDebug::<transparent::Output>::arbitrary(),
mut prevout_input1 in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
mut prevout_input2 in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
use_finalized_state_output in any::<bool>(),
mut use_finalized_state_spend in any::<bool>(),
) {
let _init_guard = zebra_test::init();
// if we use the non-finalized state for the first block,
// we have to use it for the second as well
if !use_finalized_state_output {
use_finalized_state_spend = false;
}
let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let mut block3 = zebra_test::vectors::BLOCK_MAINNET_3_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let TestState {
mut finalized_state, mut non_finalized_state, block1, ..
} = new_state_with_mainnet_transparent_data([], [], [output.0.clone()], use_finalized_state_output);
let mut previous_non_finalized_state = non_finalized_state.clone();
let expected_outpoint = transparent::OutPoint {
hash: block1.transactions[1].hash(),
index: 0,
};
prevout_input1.set_outpoint(expected_outpoint);
prevout_input2.set_outpoint(expected_outpoint);
let spend_transaction1 = transaction_v4_with_transparent_data(
[prevout_input1.0],
[(expected_outpoint, output.0.clone())],
[]
);
let spend_transaction2 = transaction_v4_with_transparent_data(
[prevout_input2.0],
[(expected_outpoint, output.0)],
[]
);
// convert the coinbase transactions to a version that the non-finalized state will accept
block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into();
block3.transactions[0] = transaction_v4_from_coinbase(&block3.transactions[0]).into();
block2.transactions.push(spend_transaction1.into());
block3.transactions.push(spend_transaction2.into());
let block2 = Arc::new(block2);
if use_finalized_state_spend {
let block2 = CheckpointVerifiedBlock::from(block2.clone());
let commit_result = finalized_state.commit_finalized_direct(block2.clone().into(), "test");
// the block was committed
prop_assert_eq!(Some((Height(2), block2.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(commit_result.is_ok());
// the non-finalized state didn't change
prop_assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
// the finalized state has spent the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
// the non-finalized state does not have the UTXO
prop_assert!(non_finalized_state.any_utxo(&expected_outpoint).is_none());
} else {
let block2 = block2.clone().prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block2.clone()
);
// the block was committed
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(2), block2.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
// the block data is in the non-finalized state
prop_assert!(!non_finalized_state.eq_internal_state(&previous_non_finalized_state));
prop_assert_eq!(non_finalized_state.chain_count(), 1);
let chain = non_finalized_state
.chain_iter()
.next()
.unwrap();
if use_finalized_state_output {
// the finalized state has the unspent UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_some());
// the non-finalized state has spent the UTXO
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
} else {
// the non-finalized state has created and spent the UTXO
prop_assert!(!chain.unspent_utxos().contains_key(&expected_outpoint));
prop_assert!(chain.created_utxos.contains_key(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
previous_non_finalized_state = non_finalized_state.clone();
}
let block3 = Arc::new(block3).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block3
);
// the block was rejected
if use_finalized_state_spend {
prop_assert_eq!(
commit_result,
Err(MissingTransparentOutput {
outpoint: expected_outpoint,
location: "the non-finalized and finalized chain",
}
.into())
);
} else {
prop_assert_eq!(
commit_result,
Err(DuplicateTransparentSpend {
outpoint: expected_outpoint,
location: "the non-finalized chain",
}
.into())
);
}
prop_assert_eq!(Some((Height(2), block2.hash())), read::best_tip(&non_finalized_state, &finalized_state.db));
// the non-finalized state did not change
prop_assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
// Since the non-finalized state has not changed, we don't need to check it again
if use_finalized_state_spend {
// the finalized state has spent the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
} else if use_finalized_state_output {
// the finalized state has the unspent UTXO
// but the non-finalized state has spent it
prop_assert!(finalized_state.utxo(&expected_outpoint).is_some());
} else {
// the non-finalized state has created and spent the UTXO
// and the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
}
/// Make sure a transparent spend with a missing UTXO
/// is rejected by state contextual validation.
#[test]
fn reject_missing_transparent_spend(
unused_output in TypeNameToDebug::<transparent::Output>::arbitrary(),
prevout_input in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
) {
let _init_guard = zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let expected_outpoint = prevout_input.outpoint().unwrap();
let spend_transaction = transaction_v4_with_transparent_data(
[prevout_input.0],
// provide an fake spent output for value fixups
[(expected_outpoint, unused_output.0)],
[]
);
// convert the coinbase transaction to a version that the non-finalized state will accept
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1.transactions.push(spend_transaction.into());
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
let previous_non_finalized_state = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1
);
// the block was rejected
prop_assert_eq!(
commit_result,
Err(MissingTransparentOutput {
outpoint: expected_outpoint,
location: "the non-finalized and finalized chain",
}
.into())
);
prop_assert_eq!(Some((Height(0), genesis.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
// the non-finalized state did not change
prop_assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
/// Make sure transparent output spends are rejected by state contextual validation,
/// if they spend an output in the same or later transaction in the block.
///
/// This test covers a potential edge case where later transactions can spend outputs
/// of previous transactions in a block, but earlier transactions can not spend later outputs.
#[test]
fn reject_earlier_transparent_spend_from_this_block(
output in TypeNameToDebug::<transparent::Output>::arbitrary(),
mut prevout_input in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
) {
let _init_guard = zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
// create an output
let output_transaction = transaction_v4_with_transparent_data([], [], [output.0.clone()]);
// create a spend
let expected_outpoint = transparent::OutPoint {
hash: output_transaction.hash(),
index: 0,
};
prevout_input.set_outpoint(expected_outpoint);
let spend_transaction = transaction_v4_with_transparent_data(
[prevout_input.0],
[(expected_outpoint, output.0)],
[]
);
// convert the coinbase transaction to a version that the non-finalized state will accept
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
// put the spend transaction before the output transaction in the block
block1
.transactions
.extend([spend_transaction.into(), output_transaction.into()]);
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
let previous_non_finalized_state = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1
);
// the block was rejected
prop_assert_eq!(
commit_result,
Err(EarlyTransparentSpend {
outpoint: expected_outpoint,
}
.into())
);
prop_assert_eq!(Some((Height(0), genesis.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
// the non-finalized state did not change
prop_assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
}
/// State associated with transparent UTXO tests.
struct TestState {
/// The pre-populated finalized state.
finalized_state: FinalizedState,
/// The pre-populated non-finalized state.
non_finalized_state: NonFinalizedState,
/// The genesis block that has already been committed to the `state` service's
/// finalized state.
#[allow(dead_code)]
genesis: CheckpointVerifiedBlock,
/// A block at height 1, that has already been committed to the `state` service.
block1: Arc<Block>,
}
/// Return a new `StateService` containing the mainnet genesis block.
/// Also returns the finalized genesis block itself.
fn new_state_with_mainnet_transparent_data(
inputs: impl IntoIterator<Item = transparent::Input>,
spent_outputs: impl IntoIterator<Item = (transparent::OutPoint, transparent::Output)>,
outputs: impl IntoIterator<Item = transparent::Output>,
use_finalized_state: bool,
) -> TestState {
let (mut finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
let previous_non_finalized_state = non_finalized_state.clone();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let outputs: Vec<_> = outputs.into_iter().collect();
let outputs_len: u32 = outputs
.len()
.try_into()
.expect("unexpectedly large output iterator");
let transaction = transaction_v4_with_transparent_data(inputs, spent_outputs, outputs);
let transaction_hash = transaction.hash();
let expected_outpoints = (0..outputs_len).map(|index| transparent::OutPoint {
hash: transaction_hash,
index,
});
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1.transactions.push(transaction.into());
let block1 = Arc::new(block1);
if use_finalized_state {
let block1 = CheckpointVerifiedBlock::from(block1.clone());
let commit_result = finalized_state.commit_finalized_direct(block1.clone().into(), "test");
// the block was committed
assert_eq!(
Some((Height(1), block1.hash)),
read::best_tip(&non_finalized_state, &finalized_state.db)
);
assert!(commit_result.is_ok());
// the non-finalized state didn't change
assert!(non_finalized_state.eq_internal_state(&previous_non_finalized_state));
for expected_outpoint in expected_outpoints {
// the finalized state has the UTXOs
assert!(finalized_state.utxo(&expected_outpoint).is_some());
// the non-finalized state does not have the UTXOs
assert!(non_finalized_state.any_utxo(&expected_outpoint).is_none());
}
} else {
let block1 = block1.clone().prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1.clone(),
);
// the block was committed
assert_eq!(
commit_result,
Ok(()),
"unexpected invalid block 1, modified with generated transactions: \n\
converted coinbase: {:?} \n\
generated non-coinbase: {:?}",
block1.block.transactions[0],
block1.block.transactions[1],
);
assert_eq!(
Some((Height(1), block1.hash)),
read::best_tip(&non_finalized_state, &finalized_state.db)
);
// the block data is in the non-finalized state
assert!(!non_finalized_state.eq_internal_state(&previous_non_finalized_state));
assert_eq!(non_finalized_state.chain_count(), 1);
for expected_outpoint in expected_outpoints {
// the non-finalized state has the unspent UTXOs
assert!(non_finalized_state
.chain_iter()
.next()
.unwrap()
.unspent_utxos()
.contains_key(&expected_outpoint));
// the finalized state does not have the UTXOs
assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
}
TestState {
finalized_state,
non_finalized_state,
genesis,
block1,
}
}
/// Return a `Transaction::V4`, using transparent `inputs` and their `spent_outputs`,
/// and newly created `outputs`.
///
/// Other fields have empty or default values.
fn transaction_v4_with_transparent_data(
inputs: impl IntoIterator<Item = transparent::Input>,
spent_outputs: impl IntoIterator<Item = (transparent::OutPoint, transparent::Output)>,
outputs: impl IntoIterator<Item = transparent::Output>,
) -> Transaction {
let inputs: Vec<_> = inputs.into_iter().collect();
let outputs: Vec<_> = outputs.into_iter().collect();
let mut transaction = Transaction::V4 {
inputs,
outputs,
lock_time: LockTime::min_lock_time_timestamp(),
expiry_height: Height(0),
joinsplit_data: None,
sapling_shielded_data: None,
};
// do required fixups, but ignore any errors,
// because we're not checking all the consensus rules here
let _ = transaction.fix_remaining_value(&spent_outputs.into_iter().collect());
transaction
}