Reject UTXO double spends (#2511)
* Reject transparent output double-spends Check that transparent spends use unspent outputs from: * earlier transaction in the same block, * earlier blocks in the parent non-finalized chain, or * the finalized state. * Fixup UTXOs in proptests * Add a comment * Clarify a consensus rule implementation * Fix an incorrect comment * Fix an incorrect error message * Clarify a comment * Document `unspent_utxos` * Simplify the UTXO check Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * Further simplify and fix the UTXO check - split each error case into a separate check - combine `contains` and `insert` - add a missing check against the non-finalized unspent UTXOs - rename arguments and edit error strings for clarity * Share test methods between check test modules * Make some chain fields available to tests * Make error field names consistent with transparent::Input * WIP: Add tests for UTXO double-spends - accept output and spend in the same block - accept output and spend in a later block - reject output and double-spend all in the same block - reject output then double-spend in a later block - reject output, spend, then double-spend all in different blocks * Use Extend rather than multiple pushes Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * Use Extend for more pushes * Limit the number of proptest cases, to speed up tests * Test rejection of UTXOs that were never in the chain * Test rejection of spends of later transactions in the same block Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com>
This commit is contained in:
parent
429ccf7f79
commit
e6e03247ba
|
@ -3,17 +3,19 @@ use proptest::{
|
|||
prelude::*,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashSet, convert::TryInto, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
block,
|
||||
fmt::SummaryDebug,
|
||||
orchard,
|
||||
parameters::{
|
||||
Network,
|
||||
NetworkUpgrade::{self, *},
|
||||
GENESIS_PREVIOUS_BLOCK_HASH,
|
||||
},
|
||||
serialization,
|
||||
transparent::Input::*,
|
||||
work::{difficulty::CompactDifficulty, equihash},
|
||||
};
|
||||
|
||||
|
@ -326,14 +328,79 @@ impl Block {
|
|||
current.height.0 += 1;
|
||||
}
|
||||
|
||||
// after the vec strategy generates blocks, update the previous block hashes
|
||||
// after the vec strategy generates blocks, fixup invalid parts of the blocks
|
||||
vec.prop_map(|mut vec| {
|
||||
let mut previous_block_hash = None;
|
||||
let mut utxos = HashSet::<transparent::OutPoint>::new();
|
||||
|
||||
for block in vec.iter_mut() {
|
||||
// fixup the previous block hash
|
||||
if let Some(previous_block_hash) = previous_block_hash {
|
||||
block.header.previous_block_hash = previous_block_hash;
|
||||
}
|
||||
previous_block_hash = Some(block.hash());
|
||||
|
||||
// fixup the transparent spends
|
||||
let mut new_transactions = Vec::new();
|
||||
for transaction in block.transactions.drain(..) {
|
||||
let mut transaction = (*transaction).clone();
|
||||
let mut new_inputs = Vec::new();
|
||||
|
||||
for mut input in transaction.inputs_mut().drain(..) {
|
||||
if let PrevOut {
|
||||
ref mut outpoint, ..
|
||||
} = input
|
||||
{
|
||||
// take a UTXO if available
|
||||
if utxos.remove(outpoint) {
|
||||
new_inputs.push(input);
|
||||
} else if let Some(arbitrary_utxo) = utxos.clone().iter().next() {
|
||||
*outpoint = *arbitrary_utxo;
|
||||
utxos.remove(arbitrary_utxo);
|
||||
new_inputs.push(input);
|
||||
}
|
||||
// otherwise, drop the invalid input, it has no UTXOs to spend
|
||||
} else {
|
||||
// preserve coinbase inputs
|
||||
new_inputs.push(input);
|
||||
}
|
||||
}
|
||||
|
||||
// delete invalid inputs
|
||||
*transaction.inputs_mut() = new_inputs;
|
||||
|
||||
// keep transactions with valid input counts
|
||||
// coinbase transactions will never fail this check
|
||||
// this is the input check from `has_inputs_and_outputs`
|
||||
if !transaction.inputs().is_empty()
|
||||
|| transaction.joinsplit_count() > 0
|
||||
|| transaction.sapling_spends_per_anchor().count() > 0
|
||||
|| (transaction.orchard_actions().count() > 0
|
||||
&& transaction
|
||||
.orchard_flags()
|
||||
.unwrap_or_else(orchard::Flags::empty)
|
||||
.contains(orchard::Flags::ENABLE_SPENDS))
|
||||
{
|
||||
// add the created UTXOs
|
||||
// these outputs can be spent from the next transaction in this block onwards
|
||||
// see `new_outputs` for details
|
||||
let hash = transaction.hash();
|
||||
for output_index_in_transaction in 0..transaction.outputs().len() {
|
||||
utxos.insert(transparent::OutPoint {
|
||||
hash,
|
||||
index: output_index_in_transaction.try_into().unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
// and keep the transaction
|
||||
new_transactions.push(Arc::new(transaction));
|
||||
}
|
||||
}
|
||||
|
||||
// delete invalid transactions
|
||||
block.transactions = new_transactions;
|
||||
|
||||
// TODO: fixup the history and authorizing data commitments, if needed
|
||||
}
|
||||
SummaryDebug(vec.into_iter().map(Arc::new).collect())
|
||||
})
|
||||
|
|
|
@ -223,6 +223,18 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modify the transparent inputs of this transaction, regardless of version.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn inputs_mut(&mut self) -> &mut Vec<transparent::Input> {
|
||||
match self {
|
||||
Transaction::V1 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V2 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V3 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V4 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V5 { ref mut inputs, .. } => inputs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Access the transparent outputs of this transaction, regardless of version.
|
||||
pub fn outputs(&self) -> &[transparent::Output] {
|
||||
match self {
|
||||
|
|
|
@ -103,6 +103,37 @@ pub enum Input {
|
|||
},
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// If this is a `PrevOut` input, returns this input's outpoint.
|
||||
/// Otherwise, returns `None`.
|
||||
pub fn outpoint(&self) -> Option<OutPoint> {
|
||||
if let Input::PrevOut { outpoint, .. } = self {
|
||||
Some(*outpoint)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Set this input's outpoint.
|
||||
///
|
||||
/// Should only be called on `PrevOut` inputs.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If `self` is a coinbase input.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn set_outpoint(&mut self, new_outpoint: OutPoint) {
|
||||
if let Input::PrevOut {
|
||||
ref mut outpoint, ..
|
||||
} = self
|
||||
{
|
||||
*outpoint = new_outpoint;
|
||||
} else {
|
||||
unreachable!("unexpected variant: Coinbase Inputs do not have OutPoints");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A transparent output from a transaction.
|
||||
///
|
||||
/// The most fundamental building block of a transaction is a
|
||||
|
|
|
@ -3,7 +3,9 @@ use std::sync::Arc;
|
|||
use chrono::{DateTime, Utc};
|
||||
use thiserror::Error;
|
||||
|
||||
use zebra_chain::{block, orchard, sapling, sprout, work::difficulty::CompactDifficulty};
|
||||
use zebra_chain::{
|
||||
block, orchard, sapling, sprout, transparent, work::difficulty::CompactDifficulty,
|
||||
};
|
||||
|
||||
/// A wrapper for type erased errors that is itself clonable and implements the
|
||||
/// Error trait
|
||||
|
@ -75,6 +77,24 @@ pub enum ValidateContextError {
|
|||
expected_difficulty: CompactDifficulty,
|
||||
},
|
||||
|
||||
#[error("transparent double-spend: {outpoint:?} is spent twice in {location:?}")]
|
||||
#[non_exhaustive]
|
||||
DuplicateTransparentSpend {
|
||||
outpoint: transparent::OutPoint,
|
||||
location: &'static str,
|
||||
},
|
||||
|
||||
#[error("missing transparent output: possible double-spend of {outpoint:?} in {location:?}")]
|
||||
#[non_exhaustive]
|
||||
MissingTransparentOutput {
|
||||
outpoint: transparent::OutPoint,
|
||||
location: &'static str,
|
||||
},
|
||||
|
||||
#[error("out-of-order transparent spend: {outpoint:?} is created by a later transaction in the same block")]
|
||||
#[non_exhaustive]
|
||||
EarlyTransparentSpend { outpoint: transparent::OutPoint },
|
||||
|
||||
#[error("sprout double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
|
||||
#[non_exhaustive]
|
||||
DuplicateSproutNullifier {
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use proptest::{
|
||||
num::usize::BinarySearch,
|
||||
prelude::*,
|
||||
strategy::{NewTree, ValueTree},
|
||||
test_runner::TestRunner,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use zebra_chain::{
|
||||
block::{Block, Height},
|
||||
fmt::SummaryDebug,
|
||||
parameters::NetworkUpgrade,
|
||||
parameters::{Network::*, NetworkUpgrade},
|
||||
serialization::ZcashDeserializeInto,
|
||||
LedgerState,
|
||||
};
|
||||
use zebra_test::prelude::*;
|
||||
|
||||
use crate::tests::Prepare;
|
||||
|
||||
|
@ -146,3 +148,46 @@ pub(crate) fn partial_nu5_chain_strategy(
|
|||
.prop_map(move |partial_chain| (network, nu_activation, partial_chain))
|
||||
})
|
||||
}
|
||||
|
||||
/// Return a new `StateService` containing the mainnet genesis block.
|
||||
/// Also returns the finalized genesis block itself.
|
||||
pub(super) fn new_state_with_mainnet_genesis() -> (StateService, FinalizedBlock) {
|
||||
let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
|
||||
.zcash_deserialize_into::<Arc<Block>>()
|
||||
.expect("block should deserialize");
|
||||
|
||||
let mut state = StateService::new(Config::ephemeral(), Mainnet);
|
||||
|
||||
assert_eq!(None, state.best_tip());
|
||||
|
||||
let genesis = FinalizedBlock::from(genesis);
|
||||
state
|
||||
.disk
|
||||
.commit_finalized_direct(genesis.clone(), "test")
|
||||
.expect("unexpected invalid genesis block test vector");
|
||||
|
||||
assert_eq!(Some((Height(0), genesis.hash)), state.best_tip());
|
||||
|
||||
(state, genesis)
|
||||
}
|
||||
|
||||
/// Return a `Transaction::V4` with the coinbase data from `coinbase`.
|
||||
///
|
||||
/// Used to convert a coinbase transaction to a version that the non-finalized state will accept.
|
||||
pub(super) fn transaction_v4_from_coinbase(coinbase: &Transaction) -> Transaction {
|
||||
assert!(
|
||||
!coinbase.has_sapling_shielded_data(),
|
||||
"conversion assumes sapling shielded data is None"
|
||||
);
|
||||
|
||||
Transaction::V4 {
|
||||
inputs: coinbase.inputs().to_vec(),
|
||||
outputs: coinbase.outputs().to_vec(),
|
||||
lock_time: coinbase.lock_time(),
|
||||
// `Height(0)` means that the expiry height is ignored
|
||||
expiry_height: coinbase.expiry_height().unwrap_or(Height(0)),
|
||||
// invalid for coinbase transactions
|
||||
joinsplit_data: None,
|
||||
sapling_shielded_data: None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ use difficulty::{AdjustedDifficulty, POW_MEDIAN_BLOCK_SPAN};
|
|||
|
||||
pub(crate) mod difficulty;
|
||||
pub(crate) mod nullifier;
|
||||
pub(crate) mod utxo;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
//! Tests for state contextual validation checks.
|
||||
|
||||
mod nullifier;
|
||||
mod utxo;
|
||||
mod vectors;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! Randomised property tests for state contextual validation
|
||||
//! Randomised property tests for nullifier contextual validation
|
||||
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
|
||||
|
@ -9,7 +9,7 @@ use zebra_chain::{
|
|||
block::{Block, Height},
|
||||
fmt::TypeNameToDebug,
|
||||
orchard,
|
||||
parameters::{Network::*, NetworkUpgrade::Nu5},
|
||||
parameters::NetworkUpgrade::Nu5,
|
||||
primitives::Groth16Proof,
|
||||
sapling::{self, FieldNotPresent, PerSpendAnchor, TransferData::*},
|
||||
serialization::ZcashDeserializeInto,
|
||||
|
@ -18,8 +18,7 @@ use zebra_chain::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
service::StateService,
|
||||
service::arbitrary::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
|
||||
tests::Prepare,
|
||||
FinalizedBlock,
|
||||
ValidateContextError::{
|
||||
|
@ -848,28 +847,6 @@ proptest! {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return a new `StateService` containing the mainnet genesis block.
|
||||
/// Also returns the finalized genesis block itself.
|
||||
fn new_state_with_mainnet_genesis() -> (StateService, FinalizedBlock) {
|
||||
let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
|
||||
.zcash_deserialize_into::<Arc<Block>>()
|
||||
.expect("block should deserialize");
|
||||
|
||||
let mut state = StateService::new(Config::ephemeral(), Mainnet);
|
||||
|
||||
assert_eq!(None, state.best_tip());
|
||||
|
||||
let genesis = FinalizedBlock::from(genesis);
|
||||
state
|
||||
.disk
|
||||
.commit_finalized_direct(genesis.clone(), "test")
|
||||
.expect("unexpected invalid genesis block test vector");
|
||||
|
||||
assert_eq!(Some((Height(0), genesis.hash)), state.best_tip());
|
||||
|
||||
(state, genesis)
|
||||
}
|
||||
|
||||
/// Make sure the supplied nullifiers are distinct, modifying them if necessary.
|
||||
fn make_distinct_nullifiers<'until_modified, NullifierT>(
|
||||
nullifiers: impl IntoIterator<Item = &'until_modified mut NullifierT>,
|
||||
|
@ -1042,24 +1019,3 @@ fn transaction_v5_with_orchard_shielded_data(
|
|||
orchard_shielded_data,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a `Transaction::V4` with the coinbase data from `coinbase`.
|
||||
///
|
||||
/// Used to convert a coinbase transaction to a version that the non-finalized state will accept.
|
||||
fn transaction_v4_from_coinbase(coinbase: &Transaction) -> Transaction {
|
||||
assert!(
|
||||
!coinbase.has_sapling_shielded_data(),
|
||||
"conversion assumes sapling shielded data is None"
|
||||
);
|
||||
|
||||
Transaction::V4 {
|
||||
inputs: coinbase.inputs().to_vec(),
|
||||
outputs: coinbase.outputs().to_vec(),
|
||||
lock_time: coinbase.lock_time(),
|
||||
// `Height(0)` means that the expiry height is ignored
|
||||
expiry_height: coinbase.expiry_height().unwrap_or(Height(0)),
|
||||
// invalid for coinbase transactions
|
||||
joinsplit_data: None,
|
||||
sapling_shielded_data: None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,764 @@
|
|||
//! Randomised property tests for UTXO contextual validation
|
||||
|
||||
use std::{convert::TryInto, env, sync::Arc};
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use zebra_chain::{
|
||||
block::{Block, Height},
|
||||
fmt::TypeNameToDebug,
|
||||
serialization::ZcashDeserializeInto,
|
||||
transaction::{LockTime, Transaction},
|
||||
transparent,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
service::{
|
||||
arbitrary::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
|
||||
StateService,
|
||||
},
|
||||
tests::Prepare,
|
||||
FinalizedBlock,
|
||||
ValidateContextError::{
|
||||
DuplicateTransparentSpend, EarlyTransparentSpend, MissingTransparentOutput,
|
||||
},
|
||||
};
|
||||
|
||||
// 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>(),
|
||||
) {
|
||||
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]);
|
||||
|
||||
// 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], []);
|
||||
|
||||
// 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 state, _genesis) = new_state_with_mainnet_genesis();
|
||||
let previous_mem = state.mem.clone();
|
||||
|
||||
// randomly choose to commit the block to the finalized or non-finalized state
|
||||
if use_finalized_state {
|
||||
let block1 = FinalizedBlock::from(Arc::new(block1));
|
||||
let commit_result = state.disk.commit_finalized_direct(block1.clone(), "test");
|
||||
|
||||
// the block was committed
|
||||
prop_assert_eq!(Some((Height(1), block1.hash)), state.best_tip());
|
||||
prop_assert!(commit_result.is_ok());
|
||||
|
||||
// the non-finalized state didn't change
|
||||
prop_assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
// the finalized state added then spent the UTXO
|
||||
prop_assert!(state.disk.utxo(&expected_outpoint).is_none());
|
||||
// the non-finalized state does not have the UTXO
|
||||
prop_assert!(state.mem.any_utxo(&expected_outpoint).is_none());
|
||||
} else {
|
||||
let block1 = Arc::new(block1).prepare();
|
||||
let commit_result =
|
||||
state.validate_and_commit(block1.clone());
|
||||
|
||||
// the block was committed
|
||||
prop_assert_eq!(commit_result, Ok(()));
|
||||
prop_assert_eq!(Some((Height(1), block1.hash)), state.best_tip());
|
||||
|
||||
// the block data is in the non-finalized state
|
||||
prop_assert!(!state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
// the non-finalized state has the spent its own UTXO
|
||||
prop_assert_eq!(state.mem.chain_set.len(), 1);
|
||||
prop_assert!(!state.mem.chain_set.iter().next().unwrap().unspent_utxos().contains_key(&expected_outpoint));
|
||||
prop_assert!(state.mem.chain_set.iter().next().unwrap().created_utxos.contains_key(&expected_outpoint));
|
||||
prop_assert!(state.mem.chain_set.iter().next().unwrap().spent_utxos.contains(&expected_outpoint));
|
||||
|
||||
// the finalized state does not have the UTXO
|
||||
prop_assert!(state.disk.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>(),
|
||||
) {
|
||||
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 state, block1, .. } = new_state_with_mainnet_transparent_data([], [output.0], use_finalized_state_output);
|
||||
let previous_mem = state.mem.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], []);
|
||||
|
||||
// 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 = FinalizedBlock::from(Arc::new(block2));
|
||||
let commit_result = state.disk.commit_finalized_direct(block2.clone(), "test");
|
||||
|
||||
// the block was committed
|
||||
prop_assert_eq!(Some((Height(2), block2.hash)), state.best_tip());
|
||||
prop_assert!(commit_result.is_ok());
|
||||
|
||||
// the non-finalized state didn't change
|
||||
prop_assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
// the finalized state has spent the UTXO
|
||||
prop_assert!(state.disk.utxo(&expected_outpoint).is_none());
|
||||
} else {
|
||||
let block2 = Arc::new(block2).prepare();
|
||||
let commit_result =
|
||||
state.validate_and_commit(block2.clone());
|
||||
|
||||
// the block was committed
|
||||
prop_assert_eq!(commit_result, Ok(()));
|
||||
prop_assert_eq!(Some((Height(2), block2.hash)), state.best_tip());
|
||||
|
||||
// the block data is in the non-finalized state
|
||||
prop_assert!(!state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
// the UTXO is spent
|
||||
prop_assert_eq!(state.mem.chain_set.len(), 1);
|
||||
prop_assert!(!state.mem.chain_set.iter().next().unwrap().unspent_utxos().contains_key(&expected_outpoint));
|
||||
|
||||
if use_finalized_state_output {
|
||||
// the chain has spent the UTXO from the finalized state
|
||||
prop_assert!(!state.mem.chain_set.iter().next().unwrap().created_utxos.contains_key(&expected_outpoint));
|
||||
prop_assert!(state.mem.chain_set.iter().next().unwrap().spent_utxos.contains(&expected_outpoint));
|
||||
// the finalized state has the UTXO, but it will get deleted on commit
|
||||
prop_assert!(state.disk.utxo(&expected_outpoint).is_some());
|
||||
} else {
|
||||
// the chain has spent its own UTXO
|
||||
prop_assert!(!state.mem.chain_set.iter().next().unwrap().unspent_utxos().contains_key(&expected_outpoint));
|
||||
prop_assert!(state.mem.chain_set.iter().next().unwrap().created_utxos.contains_key(&expected_outpoint));
|
||||
prop_assert!(state.mem.chain_set.iter().next().unwrap().spent_utxos.contains(&expected_outpoint));
|
||||
// the finalized state does not have the UTXO
|
||||
prop_assert!(state.disk.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),
|
||||
) {
|
||||
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]);
|
||||
|
||||
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], []);
|
||||
|
||||
// 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 state, genesis) = new_state_with_mainnet_genesis();
|
||||
let previous_mem = state.mem.clone();
|
||||
|
||||
let block1 = Arc::new(block1).prepare();
|
||||
let commit_result = state.validate_and_commit(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)), state.best_tip());
|
||||
|
||||
// the non-finalized state did not change
|
||||
prop_assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
// the finalized state does not have the UTXO
|
||||
prop_assert!(state.disk.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>(),
|
||||
) {
|
||||
zebra_test::init();
|
||||
|
||||
let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
|
||||
.zcash_deserialize_into::<Block>()
|
||||
.expect("block should deserialize");
|
||||
|
||||
let TestState { mut state, block1, .. } = new_state_with_mainnet_transparent_data([], [output.0], use_finalized_state_output);
|
||||
let previous_mem = state.mem.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], []);
|
||||
|
||||
// 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 = state.validate_and_commit(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())), state.best_tip());
|
||||
|
||||
// the non-finalized state did not change
|
||||
prop_assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
if use_finalized_state_output {
|
||||
// the finalized state has the UTXO
|
||||
prop_assert!(state.disk.utxo(&expected_outpoint).is_some());
|
||||
// the non-finalized state has no chains (so it can't have the UTXO)
|
||||
prop_assert!(state.mem.chain_set.iter().next().is_none());
|
||||
} else {
|
||||
// the non-finalized state has the UTXO
|
||||
prop_assert!(state.mem.chain_set.iter().next().unwrap().unspent_utxos().contains_key(&expected_outpoint));
|
||||
// the finalized state does not have the UTXO
|
||||
prop_assert!(state.disk.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>(),
|
||||
) {
|
||||
zebra_test::init();
|
||||
|
||||
let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
|
||||
.zcash_deserialize_into::<Block>()
|
||||
.expect("block should deserialize");
|
||||
|
||||
let TestState { mut state, block1, .. } = new_state_with_mainnet_transparent_data([], [output.0], use_finalized_state_output);
|
||||
let previous_mem = state.mem.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], []);
|
||||
let spend_transaction2 = transaction_v4_with_transparent_data([prevout_input2.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 = state.validate_and_commit(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())), state.best_tip());
|
||||
|
||||
// the non-finalized state did not change
|
||||
prop_assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
if use_finalized_state_output {
|
||||
// the finalized state has the UTXO
|
||||
prop_assert!(state.disk.utxo(&expected_outpoint).is_some());
|
||||
// the non-finalized state has no chains (so it can't have the UTXO)
|
||||
prop_assert!(state.mem.chain_set.iter().next().is_none());
|
||||
} else {
|
||||
// the non-finalized state has the UTXO
|
||||
prop_assert!(state.mem.chain_set.iter().next().unwrap().unspent_utxos().contains_key(&expected_outpoint));
|
||||
// the finalized state does not have the UTXO
|
||||
prop_assert!(state.disk.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>(),
|
||||
) {
|
||||
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 state, block1, .. } = new_state_with_mainnet_transparent_data([], [output.0], use_finalized_state_output);
|
||||
let mut previous_mem = state.mem.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], []);
|
||||
let spend_transaction2 = transaction_v4_with_transparent_data([prevout_input2.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 = FinalizedBlock::from(block2.clone());
|
||||
let commit_result = state.disk.commit_finalized_direct(block2.clone(), "test");
|
||||
|
||||
// the block was committed
|
||||
prop_assert_eq!(Some((Height(2), block2.hash)), state.best_tip());
|
||||
prop_assert!(commit_result.is_ok());
|
||||
|
||||
// the non-finalized state didn't change
|
||||
prop_assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
// the finalized state has spent the UTXO
|
||||
prop_assert!(state.disk.utxo(&expected_outpoint).is_none());
|
||||
// the non-finalized state does not have the UTXO
|
||||
prop_assert!(state.mem.any_utxo(&expected_outpoint).is_none());
|
||||
} else {
|
||||
let block2 = block2.clone().prepare();
|
||||
let commit_result = state.validate_and_commit(block2.clone());
|
||||
|
||||
// the block was committed
|
||||
prop_assert_eq!(commit_result, Ok(()));
|
||||
prop_assert_eq!(Some((Height(2), block2.hash)), state.best_tip());
|
||||
|
||||
// the block data is in the non-finalized state
|
||||
prop_assert!(!state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
prop_assert_eq!(state.mem.chain_set.len(), 1);
|
||||
|
||||
if use_finalized_state_output {
|
||||
// the finalized state has the unspent UTXO
|
||||
prop_assert!(state.disk.utxo(&expected_outpoint).is_some());
|
||||
// the non-finalized state has spent the UTXO
|
||||
prop_assert!(state
|
||||
.mem
|
||||
.chain_set
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.spent_utxos
|
||||
.contains(&expected_outpoint));
|
||||
} else {
|
||||
// the non-finalized state has created and spent the UTXO
|
||||
prop_assert!(!state
|
||||
.mem
|
||||
.chain_set
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unspent_utxos()
|
||||
.contains_key(&expected_outpoint));
|
||||
prop_assert!(state
|
||||
.mem
|
||||
.chain_set
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.created_utxos
|
||||
.contains_key(&expected_outpoint));
|
||||
prop_assert!(state
|
||||
.mem
|
||||
.chain_set
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.spent_utxos
|
||||
.contains(&expected_outpoint));
|
||||
// the finalized state does not have the UTXO
|
||||
prop_assert!(state.disk.utxo(&expected_outpoint).is_none());
|
||||
}
|
||||
|
||||
previous_mem = state.mem.clone();
|
||||
}
|
||||
|
||||
let block3 = Arc::new(block3).prepare();
|
||||
let commit_result = state.validate_and_commit(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())), state.best_tip());
|
||||
|
||||
// the non-finalized state did not change
|
||||
prop_assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
// 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!(state.disk.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!(state.disk.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!(state.disk.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(
|
||||
prevout_input in TypeNameToDebug::<transparent::Input>::arbitrary_with(None),
|
||||
) {
|
||||
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], []);
|
||||
|
||||
// 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 (mut state, genesis) = new_state_with_mainnet_genesis();
|
||||
let previous_mem = state.mem.clone();
|
||||
|
||||
let block1 = Arc::new(block1).prepare();
|
||||
let commit_result = state.validate_and_commit(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)), state.best_tip());
|
||||
|
||||
// the non-finalized state did not change
|
||||
prop_assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
// the finalized state does not have the UTXO
|
||||
prop_assert!(state.disk.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),
|
||||
) {
|
||||
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]);
|
||||
|
||||
// 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], []);
|
||||
|
||||
// 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 (mut state, genesis) = new_state_with_mainnet_genesis();
|
||||
let previous_mem = state.mem.clone();
|
||||
|
||||
let block1 = Arc::new(block1).prepare();
|
||||
let commit_result = state.validate_and_commit(block1);
|
||||
|
||||
// the block was rejected
|
||||
prop_assert_eq!(
|
||||
commit_result,
|
||||
Err(EarlyTransparentSpend {
|
||||
outpoint: expected_outpoint,
|
||||
}.into())
|
||||
);
|
||||
prop_assert_eq!(Some((Height(0), genesis.hash)), state.best_tip());
|
||||
|
||||
// the non-finalized state did not change
|
||||
prop_assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
// the finalized state does not have the UTXO
|
||||
prop_assert!(state.disk.utxo(&expected_outpoint).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// State associated with transparent UTXO tests.
|
||||
struct TestState {
|
||||
/// The pre-populated state service.
|
||||
state: StateService,
|
||||
|
||||
/// The genesis block that has already been committed to the `state` service's
|
||||
/// finalized state.
|
||||
#[allow(dead_code)]
|
||||
genesis: FinalizedBlock,
|
||||
|
||||
/// 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>,
|
||||
outputs: impl IntoIterator<Item = transparent::Output>,
|
||||
use_finalized_state: bool,
|
||||
) -> TestState {
|
||||
let (mut state, genesis) = new_state_with_mainnet_genesis();
|
||||
let previous_mem = state.mem.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, 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 = FinalizedBlock::from(block1.clone());
|
||||
let commit_result = state.disk.commit_finalized_direct(block1.clone(), "test");
|
||||
|
||||
// the block was committed
|
||||
assert_eq!(Some((Height(1), block1.hash)), state.best_tip());
|
||||
assert!(commit_result.is_ok());
|
||||
|
||||
// the non-finalized state didn't change
|
||||
assert!(state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
for expected_outpoint in expected_outpoints {
|
||||
// the finalized state has the UTXOs
|
||||
assert!(state.disk.utxo(&expected_outpoint).is_some());
|
||||
// the non-finalized state does not have the UTXOs
|
||||
assert!(state.mem.any_utxo(&expected_outpoint).is_none());
|
||||
}
|
||||
} else {
|
||||
let block1 = block1.clone().prepare();
|
||||
let commit_result = state.validate_and_commit(block1.clone());
|
||||
|
||||
// the block was committed
|
||||
assert_eq!(commit_result, Ok(()));
|
||||
assert_eq!(Some((Height(1), block1.hash)), state.best_tip());
|
||||
|
||||
// the block data is in the non-finalized state
|
||||
assert!(!state.mem.eq_internal_state(&previous_mem));
|
||||
|
||||
assert_eq!(state.mem.chain_set.len(), 1);
|
||||
|
||||
for expected_outpoint in expected_outpoints {
|
||||
// the non-finalized state has the unspent UTXOs
|
||||
assert!(state
|
||||
.mem
|
||||
.chain_set
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unspent_utxos()
|
||||
.contains_key(&expected_outpoint));
|
||||
// the finalized state does not have the UTXOs
|
||||
assert!(state.disk.utxo(&expected_outpoint).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
TestState {
|
||||
state,
|
||||
genesis,
|
||||
block1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a `Transaction::V4`, using transparent `inputs` and `outputs`,
|
||||
///
|
||||
/// Other fields have empty or default values.
|
||||
fn transaction_v4_with_transparent_data(
|
||||
inputs: impl IntoIterator<Item = transparent::Input>,
|
||||
outputs: impl IntoIterator<Item = transparent::Output>,
|
||||
) -> Transaction {
|
||||
let inputs: Vec<_> = inputs.into_iter().collect();
|
||||
let outputs: Vec<_> = outputs.into_iter().collect();
|
||||
|
||||
// do any fixups here, if required
|
||||
|
||||
Transaction::V4 {
|
||||
inputs,
|
||||
outputs,
|
||||
lock_time: LockTime::min_lock_time(),
|
||||
expiry_height: Height(0),
|
||||
joinsplit_data: None,
|
||||
sapling_shielded_data: None,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
//! Consensus rule checks for the finalized state.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use zebra_chain::transparent;
|
||||
|
||||
use crate::{
|
||||
service::finalized_state::FinalizedState,
|
||||
PreparedBlock,
|
||||
ValidateContextError::{
|
||||
self, DuplicateTransparentSpend, EarlyTransparentSpend, MissingTransparentOutput,
|
||||
},
|
||||
};
|
||||
|
||||
/// Reject double-spends of transparent outputs:
|
||||
/// - duplicate spends that are both in this block,
|
||||
/// - spends of an output that hasn't been created yet,
|
||||
/// (in linear chain and transaction order), and
|
||||
/// - spends of an output that was spent by a previous block.
|
||||
///
|
||||
/// Also rejects attempts to spend UTXOs that were never created (in this chain).
|
||||
///
|
||||
/// "each output of a particular transaction
|
||||
/// can only be used as an input once in the block chain.
|
||||
/// Any subsequent reference is a forbidden double spend-
|
||||
/// an attempt to spend the same satoshis twice."
|
||||
///
|
||||
/// https://developer.bitcoin.org/devguide/block_chain.html#introduction
|
||||
///
|
||||
/// "Any input within this block can spend an output which also appears in this block
|
||||
/// (assuming the spend is otherwise valid).
|
||||
/// However, the TXID corresponding to the output must be placed at some point
|
||||
/// before the TXID corresponding to the input.
|
||||
/// This ensures that any program parsing block chain transactions linearly
|
||||
/// will encounter each output before it is used as an input."
|
||||
///
|
||||
/// https://developer.bitcoin.org/reference/block_chain.html#merkle-trees
|
||||
pub fn transparent_double_spends(
|
||||
prepared: &PreparedBlock,
|
||||
non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
|
||||
non_finalized_chain_spent_utxos: &HashSet<transparent::OutPoint>,
|
||||
finalized_state: &FinalizedState,
|
||||
) -> Result<(), ValidateContextError> {
|
||||
let mut block_spends = HashSet::new();
|
||||
|
||||
for (spend_tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
|
||||
let spends = transaction.inputs().iter().filter_map(|input| match input {
|
||||
transparent::Input::PrevOut { outpoint, .. } => Some(outpoint),
|
||||
// Coinbase inputs represent new coins,
|
||||
// so there are no UTXOs to mark as spent.
|
||||
transparent::Input::Coinbase { .. } => None,
|
||||
});
|
||||
|
||||
for spend in spends {
|
||||
if !block_spends.insert(*spend) {
|
||||
// reject in-block duplicate spends
|
||||
return Err(DuplicateTransparentSpend {
|
||||
outpoint: *spend,
|
||||
location: "the same block",
|
||||
});
|
||||
}
|
||||
|
||||
// check spends occur in chain order
|
||||
//
|
||||
// because we are in the non-finalized state, we need to check spends within the same block,
|
||||
// spent non-finalized UTXOs, and unspent non-finalized and finalized UTXOs.
|
||||
|
||||
if let Some(output) = prepared.new_outputs.get(spend) {
|
||||
// reject the spend if it uses an output from this block,
|
||||
// but the output was not created by an earlier transaction
|
||||
//
|
||||
// we know the spend is invalid, because transaction IDs are unique
|
||||
//
|
||||
// (transaction IDs also commit to transaction inputs,
|
||||
// so it should be cryptographically impossible for a transaction
|
||||
// to spend its own outputs)
|
||||
if output.tx_index_in_block >= spend_tx_index_in_block {
|
||||
return Err(EarlyTransparentSpend { outpoint: *spend });
|
||||
} else {
|
||||
// a unique spend of a previous transaction's output is ok
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if non_finalized_chain_spent_utxos.contains(spend) {
|
||||
// reject the spend if its UTXO is already spent in the
|
||||
// non-finalized parent chain
|
||||
return Err(DuplicateTransparentSpend {
|
||||
outpoint: *spend,
|
||||
location: "the non-finalized chain",
|
||||
});
|
||||
}
|
||||
|
||||
if !non_finalized_chain_unspent_utxos.contains_key(spend)
|
||||
&& finalized_state.utxo(spend).is_none()
|
||||
{
|
||||
// we don't keep spent UTXOs in the finalized state,
|
||||
// so all we can say is that it's missing from both
|
||||
// the finalized and non-finalized chains
|
||||
// (it might have been spent in the finalized state,
|
||||
// or it might never have existed in this chain)
|
||||
return Err(MissingTransparentOutput {
|
||||
outpoint: *spend,
|
||||
location: "the non-finalized and finalized chain",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -25,7 +25,7 @@ use crate::{FinalizedBlock, HashOrHeight, PreparedBlock, ValidateContextError};
|
|||
|
||||
use self::chain::Chain;
|
||||
|
||||
use super::finalized_state::FinalizedState;
|
||||
use super::{check, finalized_state::FinalizedState};
|
||||
|
||||
/// The state of the chains in memory, incuding queued blocks.
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -169,9 +169,14 @@ impl NonFinalizedState {
|
|||
&self,
|
||||
parent_chain: Chain,
|
||||
prepared: PreparedBlock,
|
||||
_finalized_state: &FinalizedState,
|
||||
finalized_state: &FinalizedState,
|
||||
) -> Result<Chain, ValidateContextError> {
|
||||
// TODO: insert validation of `prepared` block and `parent_chain` here
|
||||
check::utxo::transparent_double_spends(
|
||||
&prepared,
|
||||
&parent_chain.unspent_utxos(),
|
||||
&parent_chain.spent_utxos,
|
||||
finalized_state,
|
||||
)?;
|
||||
|
||||
parent_chain.push(prepared)
|
||||
}
|
||||
|
|
|
@ -27,10 +27,10 @@ pub struct Chain {
|
|||
///
|
||||
/// Note that these UTXOs may not be unspent.
|
||||
/// Outputs can be spent by later transactions or blocks in the chain.
|
||||
pub(super) created_utxos: HashMap<transparent::OutPoint, transparent::Utxo>,
|
||||
pub(crate) created_utxos: HashMap<transparent::OutPoint, transparent::Utxo>,
|
||||
/// The [`OutPoint`]s spent by `blocks`,
|
||||
/// including those created by earlier transactions or blocks in the chain.
|
||||
pub(super) spent_utxos: HashSet<transparent::OutPoint>,
|
||||
pub(crate) spent_utxos: HashSet<transparent::OutPoint>,
|
||||
|
||||
/// The sprout anchors created by `blocks`.
|
||||
///
|
||||
|
@ -183,6 +183,17 @@ impl Chain {
|
|||
pub fn is_empty(&self) -> bool {
|
||||
self.blocks.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the unspent transaction outputs (UTXOs) in this non-finalized chain.
|
||||
///
|
||||
/// Callers should also check the finalized state for available UTXOs.
|
||||
/// If UTXOs remain unspent when a block is finalized, they are stored in the finalized state,
|
||||
/// and removed from the relevant chain(s).
|
||||
pub fn unspent_utxos(&self) -> HashMap<transparent::OutPoint, transparent::Utxo> {
|
||||
let mut unspent_utxos = self.created_utxos.clone();
|
||||
unspent_utxos.retain(|out_point, _utxo| !self.spent_utxos.contains(out_point));
|
||||
unspent_utxos
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait to organize inverse operations done on the `Chain` type. Used to
|
||||
|
|
Loading…
Reference in New Issue