Validate spends of transparent coinbase outputs (#2525)
* Validate transparent coinbase output maturity and shielding - Add a CoinbaseSpendRestriction enum and Transaction method - Validate transparent coinbase spends in non-finalized chains * Don't use genesis created UTXOs for spends in generated block chains * Refactor out a new_transaction_ordered_outputs function * Add Transaction::outputs_mut for tests * Generate valid transparent spends in arbitrary block chains * When generating blocks, fixup the block contents, then the block hash * Test that generated chains contain at least one transparent spend * Make generated chains long enough for reliable tests * Add transparent and shielded input and output methods to Transaction * Split chain generation into 3 functions * Test that unshielded and immature transparent coinbase spends fail * Comment punctuation * Clarify a comment * Clarify probability calculation * Test that shielded mature coinbase output spends succeed
This commit is contained in:
parent
ee3c992ca6
commit
3d792f7195
|
@ -10,7 +10,7 @@ mod serialize;
|
|||
pub mod merkle;
|
||||
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
mod arbitrary;
|
||||
pub mod arbitrary;
|
||||
#[cfg(any(test, feature = "bench"))]
|
||||
pub mod tests;
|
||||
|
||||
|
|
|
@ -1,26 +1,45 @@
|
|||
//! Randomised property testing for [`Block`]s.
|
||||
|
||||
use proptest::{
|
||||
arbitrary::{any, Arbitrary},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use std::{collections::HashSet, convert::TryInto, sync::Arc};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
block,
|
||||
fmt::SummaryDebug,
|
||||
orchard,
|
||||
parameters::{
|
||||
Network,
|
||||
NetworkUpgrade::{self, *},
|
||||
GENESIS_PREVIOUS_BLOCK_HASH,
|
||||
},
|
||||
serialization,
|
||||
transparent::Input::*,
|
||||
transparent::{new_transaction_ordered_outputs, CoinbaseSpendRestriction},
|
||||
work::{difficulty::CompactDifficulty, equihash},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The chain height used to test for prevout inputs.
|
||||
///
|
||||
/// This impacts the probability of `has_prevouts` failures in
|
||||
/// `arbitrary_height_partial_chain_strategy`.
|
||||
///
|
||||
/// The failure probability calculation is:
|
||||
/// ```text
|
||||
/// shielded_input = shielded_pool_count / pool_count
|
||||
/// expected_transactions = expected_inputs = MAX_ARBITRARY_ITEMS/2
|
||||
/// proptest_cases = 256
|
||||
/// number_of_proptests = 5 as of July 2021 (PREVOUTS_CHAIN_HEIGHT and PartialChain tests)
|
||||
/// shielded_input^(expected_transactions * expected_inputs * PREVOUTS_CHAIN_HEIGHT) * proptest_cases * number_of_proptests
|
||||
/// ```
|
||||
///
|
||||
/// `PREVOUTS_CHAIN_HEIGHT` should be increased, and `proptest_cases` should be reduced,
|
||||
/// so that the failure probability is less than 1 in 1 million.
|
||||
pub const PREVOUTS_CHAIN_HEIGHT: usize = 20;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
/// The configuration data for proptest when generating arbitrary chains
|
||||
|
@ -313,56 +332,134 @@ impl Arbitrary for Block {
|
|||
type Strategy = BoxedStrategy<Self>;
|
||||
}
|
||||
|
||||
/// Skip checking transparent coinbase spends in [`Block::partial_chain_strategy`].
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn allow_all_transparent_coinbase_spends(
|
||||
_: transparent::OutPoint,
|
||||
_: transparent::CoinbaseSpendRestriction,
|
||||
_: transparent::Utxo,
|
||||
) -> Result<(), ()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Block {
|
||||
/// Returns a strategy for creating Vecs of blocks with increasing height of
|
||||
/// the given length.
|
||||
pub fn partial_chain_strategy(
|
||||
/// Returns a strategy for creating vectors of blocks with increasing height.
|
||||
///
|
||||
/// Each vector is `count` blocks long.
|
||||
///
|
||||
/// `check_transparent_coinbase_spend` is used to check if
|
||||
/// transparent coinbase UTXOs are valid, before using them in blocks.
|
||||
/// Use [`allow_all_transparent_coinbase_spends`] to disable this check.
|
||||
pub fn partial_chain_strategy<F, T, E>(
|
||||
mut current: LedgerState,
|
||||
count: usize,
|
||||
) -> BoxedStrategy<SummaryDebug<Vec<Arc<Self>>>> {
|
||||
check_transparent_coinbase_spend: F,
|
||||
) -> BoxedStrategy<SummaryDebug<Vec<Arc<Self>>>>
|
||||
where
|
||||
F: Fn(
|
||||
transparent::OutPoint,
|
||||
transparent::CoinbaseSpendRestriction,
|
||||
transparent::Utxo,
|
||||
) -> Result<T, E>
|
||||
+ Copy
|
||||
+ 'static,
|
||||
{
|
||||
let mut vec = Vec::with_capacity(count);
|
||||
|
||||
// generate block strategies with the correct heights
|
||||
for _ in 0..count {
|
||||
vec.push(Block::arbitrary_with(current));
|
||||
vec.push((Just(current.height), Block::arbitrary_with(current)));
|
||||
current.height.0 += 1;
|
||||
}
|
||||
|
||||
// after the vec strategy generates blocks, fixup invalid parts of the blocks
|
||||
vec.prop_map(|mut vec| {
|
||||
vec.prop_map(move |mut vec| {
|
||||
let mut previous_block_hash = None;
|
||||
let mut utxos = HashSet::<transparent::OutPoint>::new();
|
||||
let mut utxos = HashMap::new();
|
||||
|
||||
for block in vec.iter_mut() {
|
||||
for (height, 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();
|
||||
for (tx_index_in_block, transaction) in block.transactions.drain(..).enumerate() {
|
||||
if let Some(transaction) = fix_generated_transaction(
|
||||
(*transaction).clone(),
|
||||
tx_index_in_block,
|
||||
*height,
|
||||
&mut utxos,
|
||||
check_transparent_coinbase_spend,
|
||||
) {
|
||||
new_transactions.push(Arc::new(transaction));
|
||||
}
|
||||
}
|
||||
|
||||
// delete invalid transactions
|
||||
block.transactions = new_transactions;
|
||||
|
||||
// TODO: if needed, fixup:
|
||||
// - transaction output counts (currently 0..=16, consensus rules require 1..)
|
||||
// - history and authorizing data commitments
|
||||
|
||||
// now that we've made all the changes, calculate our block hash,
|
||||
// so the next block can use it
|
||||
previous_block_hash = Some(block.hash());
|
||||
}
|
||||
SummaryDebug(
|
||||
vec.into_iter()
|
||||
.map(|(_height, block)| Arc::new(block))
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
/// Fix `transaction` so it obeys more consensus rules.
|
||||
///
|
||||
/// Spends [`OutPoint`]s from `utxos`, and adds newly created outputs.
|
||||
///
|
||||
/// If the transaction can't be fixed, returns `None`.
|
||||
pub fn fix_generated_transaction<F, T, E>(
|
||||
mut transaction: Transaction,
|
||||
tx_index_in_block: usize,
|
||||
height: Height,
|
||||
utxos: &mut HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||||
check_transparent_coinbase_spend: F,
|
||||
) -> Option<Transaction>
|
||||
where
|
||||
F: Fn(
|
||||
transparent::OutPoint,
|
||||
transparent::CoinbaseSpendRestriction,
|
||||
transparent::Utxo,
|
||||
) -> Result<T, E>
|
||||
+ Copy
|
||||
+ 'static,
|
||||
{
|
||||
let mut spend_restriction = transaction.coinbase_spend_restriction(height);
|
||||
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);
|
||||
// fixup the transparent spends
|
||||
for mut input in transaction.inputs().to_vec().into_iter() {
|
||||
if input.outpoint().is_some() {
|
||||
if let Some(selected_outpoint) = find_valid_utxo_for_spend(
|
||||
&mut transaction,
|
||||
&mut spend_restriction,
|
||||
height,
|
||||
utxos,
|
||||
check_transparent_coinbase_spend,
|
||||
) {
|
||||
input.set_outpoint(selected_outpoint);
|
||||
new_inputs.push(input);
|
||||
|
||||
utxos.remove(&selected_outpoint);
|
||||
}
|
||||
// otherwise, drop the invalid input, it has no UTXOs to spend
|
||||
// otherwise, drop the invalid input, because it has no valid UTXOs to spend
|
||||
} else {
|
||||
// preserve coinbase inputs
|
||||
new_inputs.push(input);
|
||||
new_inputs.push(input.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -371,41 +468,87 @@ impl Block {
|
|||
|
||||
// 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))
|
||||
if transaction.has_transparent_or_shielded_inputs() {
|
||||
// skip genesis created UTXOs
|
||||
if height > Height(0) {
|
||||
// non-coinbase outputs can be spent from the next transaction in this block onwards
|
||||
// coinbase outputs have to wait 100 blocks, and be shielded
|
||||
utxos.extend(new_transaction_ordered_outputs(
|
||||
&transaction,
|
||||
transaction.hash(),
|
||||
tx_index_in_block,
|
||||
height,
|
||||
));
|
||||
}
|
||||
|
||||
Some(transaction)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a valid [`OutPoint`] in `utxos` to spend in `transaction`.
|
||||
///
|
||||
/// Modifies `transaction` and updates `spend_restriction` if needed.
|
||||
///
|
||||
/// If there is no valid output, or many search attempts have failed, returns `None`.
|
||||
pub fn find_valid_utxo_for_spend<F, T, E>(
|
||||
transaction: &mut Transaction,
|
||||
spend_restriction: &mut CoinbaseSpendRestriction,
|
||||
spend_height: Height,
|
||||
utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||||
check_transparent_coinbase_spend: F,
|
||||
) -> Option<transparent::OutPoint>
|
||||
where
|
||||
F: Fn(
|
||||
transparent::OutPoint,
|
||||
transparent::CoinbaseSpendRestriction,
|
||||
transparent::Utxo,
|
||||
) -> Result<T, E>
|
||||
+ Copy
|
||||
+ 'static,
|
||||
{
|
||||
// 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(),
|
||||
});
|
||||
let has_shielded_outputs = transaction.has_shielded_outputs();
|
||||
let delete_transparent_outputs = CoinbaseSpendRestriction::OnlyShieldedOutputs { spend_height };
|
||||
let mut attempts: usize = 0;
|
||||
|
||||
// choose an arbitrary spendable UTXO, in hash set order
|
||||
while let Some((candidate_outpoint, candidate_utxo)) = utxos.iter().next() {
|
||||
let candidate_utxo = candidate_utxo.clone().utxo;
|
||||
|
||||
attempts += 1;
|
||||
|
||||
// Avoid O(n^2) algorithmic complexity by giving up early,
|
||||
// rather than exhausively checking the entire UTXO set
|
||||
if attempts > 100 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// and keep the transaction
|
||||
new_transactions.push(Arc::new(transaction));
|
||||
// try the utxo as-is, then try it with deleted transparent outputs
|
||||
if check_transparent_coinbase_spend(
|
||||
*candidate_outpoint,
|
||||
*spend_restriction,
|
||||
candidate_utxo.clone(),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return Some(*candidate_outpoint);
|
||||
} else if has_shielded_outputs
|
||||
&& check_transparent_coinbase_spend(
|
||||
*candidate_outpoint,
|
||||
delete_transparent_outputs,
|
||||
candidate_utxo.clone(),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
*transaction.outputs_mut() = Vec::new();
|
||||
*spend_restriction = delete_transparent_outputs;
|
||||
|
||||
return Some(*candidate_outpoint);
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl Arbitrary for Commitment {
|
||||
|
|
|
@ -11,7 +11,11 @@ use crate::{
|
|||
LedgerState,
|
||||
};
|
||||
|
||||
use super::super::{serialize::MAX_BLOCK_BYTES, *};
|
||||
use super::super::{
|
||||
arbitrary::{allow_all_transparent_coinbase_spends, PREVOUTS_CHAIN_HEIGHT},
|
||||
serialize::MAX_BLOCK_BYTES,
|
||||
*,
|
||||
};
|
||||
|
||||
const DEFAULT_BLOCK_ROUNDTRIP_PROPTEST_CASES: u32 = 16;
|
||||
|
||||
|
@ -157,21 +161,46 @@ fn block_genesis_strategy() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Make sure our partial chain strategy generates a chain with the correct coinbase
|
||||
/// heights and previous block hashes.
|
||||
/// Make sure our genesis partial chain strategy generates a chain with:
|
||||
/// - correct coinbase heights
|
||||
/// - correct previous block hashes
|
||||
/// - no transparent spends in the genesis block, because genesis transparent outputs are ignored
|
||||
#[test]
|
||||
fn partial_chain_strategy() -> Result<()> {
|
||||
fn genesis_partial_chain_strategy() -> Result<()> {
|
||||
zebra_test::init();
|
||||
|
||||
let strategy = LedgerState::genesis_strategy(None, None, false)
|
||||
.prop_flat_map(|init| Block::partial_chain_strategy(init, MAX_ARBITRARY_ITEMS));
|
||||
let strategy = LedgerState::genesis_strategy(None, None, false).prop_flat_map(|init| {
|
||||
Block::partial_chain_strategy(
|
||||
init,
|
||||
MAX_ARBITRARY_ITEMS,
|
||||
allow_all_transparent_coinbase_spends,
|
||||
)
|
||||
});
|
||||
|
||||
proptest!(|(chain in strategy)| {
|
||||
let mut height = Height(0);
|
||||
let mut previous_block_hash = GENESIS_PREVIOUS_BLOCK_HASH;
|
||||
|
||||
for block in chain {
|
||||
prop_assert_eq!(block.coinbase_height(), Some(height));
|
||||
prop_assert_eq!(block.header.previous_block_hash, previous_block_hash);
|
||||
|
||||
// block 1 can have spends of transparent outputs
|
||||
// of previous transactions in the same block
|
||||
if height == Height(0) {
|
||||
prop_assert_eq!(
|
||||
block
|
||||
.transactions
|
||||
.iter()
|
||||
.flat_map(|t| t.inputs())
|
||||
.filter_map(|i| i.outpoint())
|
||||
.count(),
|
||||
0,
|
||||
"unexpected transparent prevout inputs at height {:?}: genesis transparent outputs are ignored",
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
height = Height(height.0 + 1);
|
||||
previous_block_hash = block.hash();
|
||||
}
|
||||
|
@ -180,19 +209,29 @@ fn partial_chain_strategy() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Make sure our block height strategy generates a chain with the correct coinbase
|
||||
/// heights and hashes.
|
||||
/// Make sure our block height strategy generates a chain with:
|
||||
/// - correct coinbase heights
|
||||
/// - correct previous block hashes
|
||||
/// - at least one transparent PrevOut input in the entire chain
|
||||
#[test]
|
||||
fn arbitrary_height_partial_chain_strategy() -> Result<()> {
|
||||
zebra_test::init();
|
||||
|
||||
let strategy = any::<Height>()
|
||||
.prop_flat_map(|height| LedgerState::height_strategy(height, None, None, false))
|
||||
.prop_flat_map(|init| Block::partial_chain_strategy(init, MAX_ARBITRARY_ITEMS));
|
||||
.prop_flat_map(|init| {
|
||||
Block::partial_chain_strategy(
|
||||
init,
|
||||
PREVOUTS_CHAIN_HEIGHT,
|
||||
allow_all_transparent_coinbase_spends,
|
||||
)
|
||||
});
|
||||
|
||||
proptest!(|(chain in strategy)| {
|
||||
let mut height = None;
|
||||
let mut previous_block_hash = None;
|
||||
let mut has_prevouts = false;
|
||||
|
||||
for block in chain {
|
||||
if height.is_none() {
|
||||
prop_assert!(block.coinbase_height().is_some());
|
||||
|
@ -202,8 +241,19 @@ fn arbitrary_height_partial_chain_strategy() -> Result<()> {
|
|||
prop_assert_eq!(block.coinbase_height(), height);
|
||||
prop_assert_eq!(Some(block.header.previous_block_hash), previous_block_hash);
|
||||
}
|
||||
|
||||
has_prevouts |= block
|
||||
.transactions
|
||||
.iter()
|
||||
.flat_map(|t| t.inputs())
|
||||
.find_map(|i| i.outpoint())
|
||||
.is_some();
|
||||
|
||||
previous_block_hash = Some(block.hash());
|
||||
}
|
||||
|
||||
// this assertion checks that we're covering transparent spends
|
||||
prop_assert!(has_prevouts, "unexpected missing prevouts in entire chain");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -28,7 +28,11 @@ use crate::{
|
|||
block, orchard,
|
||||
parameters::NetworkUpgrade,
|
||||
primitives::{Bctv14Proof, Groth16Proof},
|
||||
sapling, sprout, transparent,
|
||||
sapling, sprout,
|
||||
transparent::{
|
||||
self,
|
||||
CoinbaseSpendRestriction::{self, *},
|
||||
},
|
||||
value_balance::ValueBalance,
|
||||
};
|
||||
|
||||
|
@ -155,6 +159,75 @@ impl Transaction {
|
|||
sighash::SigHasher::new(self, hash_type, network_upgrade, input).sighash()
|
||||
}
|
||||
|
||||
// other properties
|
||||
|
||||
/// Does this transaction have transparent or shielded inputs?
|
||||
///
|
||||
/// "[Sapling onward] If effectiveVersion < 5, then at least one of tx_in_count,
|
||||
/// nSpendsSapling, and nJoinSplit MUST be nonzero.
|
||||
///
|
||||
/// [NU5 onward] If effectiveVersion ≥ 5 then this condition MUST hold:
|
||||
/// tx_in_count > 0 or nSpendsSapling > 0 or (nActionsOrchard > 0 and enableSpendsOrchard = 1)."
|
||||
///
|
||||
/// https://zips.z.cash/protocol/protocol.pdf#txnconsensus
|
||||
pub fn has_transparent_or_shielded_inputs(&self) -> bool {
|
||||
!self.inputs().is_empty() || self.has_shielded_inputs()
|
||||
}
|
||||
|
||||
/// Does this transaction have shielded inputs?
|
||||
///
|
||||
/// See [`has_transparent_or_shielded_inputs`] for details.
|
||||
pub fn has_shielded_inputs(&self) -> bool {
|
||||
self.joinsplit_count() > 0
|
||||
|| self.sapling_spends_per_anchor().count() > 0
|
||||
|| (self.orchard_actions().count() > 0
|
||||
&& self
|
||||
.orchard_flags()
|
||||
.unwrap_or_else(orchard::Flags::empty)
|
||||
.contains(orchard::Flags::ENABLE_SPENDS))
|
||||
}
|
||||
|
||||
/// Does this transaction have transparent or shielded outputs?
|
||||
///
|
||||
/// "[Sapling onward] If effectiveVersion < 5, then at least one of tx_out_count,
|
||||
/// nOutputsSapling, and nJoinSplit MUST be nonzero.
|
||||
///
|
||||
/// [NU5 onward] If effectiveVersion ≥ 5 then this condition MUST hold:
|
||||
/// tx_out_count > 0 or nOutputsSapling > 0 or (nActionsOrchard > 0 and enableOutputsOrchard = 1)."
|
||||
///
|
||||
/// https://zips.z.cash/protocol/protocol.pdf#txnconsensus
|
||||
pub fn has_transparent_or_shielded_outputs(&self) -> bool {
|
||||
!self.outputs().is_empty() || self.has_shielded_outputs()
|
||||
}
|
||||
|
||||
/// Does this transaction have shielded outputs?
|
||||
///
|
||||
/// See [`has_transparent_or_shielded_outputs`] for details.
|
||||
pub fn has_shielded_outputs(&self) -> bool {
|
||||
self.joinsplit_count() > 0
|
||||
|| self.sapling_outputs().count() > 0
|
||||
|| (self.orchard_actions().count() > 0
|
||||
&& self
|
||||
.orchard_flags()
|
||||
.unwrap_or_else(orchard::Flags::empty)
|
||||
.contains(orchard::Flags::ENABLE_OUTPUTS))
|
||||
}
|
||||
|
||||
/// Returns the [`CoinbaseSpendRestriction`] for this transaction,
|
||||
/// assuming it is mined at `spend_height`.
|
||||
pub fn coinbase_spend_restriction(
|
||||
&self,
|
||||
spend_height: block::Height,
|
||||
) -> CoinbaseSpendRestriction {
|
||||
if self.outputs().is_empty() {
|
||||
// we know this transaction must have shielded outputs,
|
||||
// because of other consensus rules
|
||||
OnlyShieldedOutputs { spend_height }
|
||||
} else {
|
||||
SomeTransparentOutputs
|
||||
}
|
||||
}
|
||||
|
||||
// header
|
||||
|
||||
/// Return if the `fOverwintered` flag of this transaction is set.
|
||||
|
@ -250,6 +323,28 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modify the transparent outputs of this transaction, regardless of version.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn outputs_mut(&mut self) -> &mut Vec<transparent::Output> {
|
||||
match self {
|
||||
Transaction::V1 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V2 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V3 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V4 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V5 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this transaction is a coinbase transaction.
|
||||
pub fn is_coinbase(&self) -> bool {
|
||||
self.inputs().len() == 1
|
||||
|
|
|
@ -9,7 +9,13 @@ mod utxo;
|
|||
|
||||
pub use address::Address;
|
||||
pub use script::Script;
|
||||
pub use utxo::{new_ordered_outputs, new_outputs, utxos_from_ordered_utxos, OrderedUtxo, Utxo};
|
||||
pub use utxo::{
|
||||
new_ordered_outputs, new_outputs, utxos_from_ordered_utxos, CoinbaseSpendRestriction,
|
||||
OrderedUtxo, Utxo,
|
||||
};
|
||||
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub(crate) use utxo::new_transaction_ordered_outputs;
|
||||
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
use proptest_derive::Arbitrary;
|
||||
|
|
|
@ -4,7 +4,8 @@ use std::{collections::HashMap, convert::TryInto};
|
|||
|
||||
use crate::{
|
||||
block::{self, Block},
|
||||
transaction, transparent,
|
||||
transaction::{self, Transaction},
|
||||
transparent,
|
||||
};
|
||||
|
||||
/// An unspent `transparent::Output`, with accompanying metadata.
|
||||
|
@ -68,6 +69,26 @@ impl OrderedUtxo {
|
|||
}
|
||||
}
|
||||
|
||||
/// A restriction that must be checked before spending a transparent output of a
|
||||
/// coinbase transaction.
|
||||
///
|
||||
/// See [`CoinbaseSpendRestriction::check_spend`] for the consensus rules.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "proptest-impl"),
|
||||
derive(proptest_derive::Arbitrary)
|
||||
)]
|
||||
pub enum CoinbaseSpendRestriction {
|
||||
/// The UTXO is spent in a transaction with one or more transparent outputs
|
||||
SomeTransparentOutputs,
|
||||
|
||||
/// The UTXO is spent in a transaction which only has shielded outputs
|
||||
OnlyShieldedOutputs {
|
||||
/// The height at which the UTXO is spent
|
||||
spend_height: block::Height,
|
||||
},
|
||||
}
|
||||
|
||||
/// Compute an index of [`Utxo`]s, given an index of [`OrderedUtxo`]s.
|
||||
pub fn utxos_from_ordered_utxos(
|
||||
ordered_utxos: HashMap<transparent::OutPoint, OrderedUtxo>,
|
||||
|
@ -93,18 +114,41 @@ pub fn new_ordered_outputs(
|
|||
block: &Block,
|
||||
transaction_hashes: &[transaction::Hash],
|
||||
) -> HashMap<transparent::OutPoint, OrderedUtxo> {
|
||||
let mut new_ordered_outputs = HashMap::default();
|
||||
let mut new_ordered_outputs = HashMap::new();
|
||||
let height = block.coinbase_height().expect("block has coinbase height");
|
||||
|
||||
for (tx_index_in_block, (transaction, hash)) in block
|
||||
.transactions
|
||||
.iter()
|
||||
.zip(transaction_hashes.iter().cloned())
|
||||
.enumerate()
|
||||
{
|
||||
new_ordered_outputs.extend(new_transaction_ordered_outputs(
|
||||
transaction,
|
||||
hash,
|
||||
tx_index_in_block,
|
||||
height,
|
||||
));
|
||||
}
|
||||
|
||||
new_ordered_outputs
|
||||
}
|
||||
|
||||
/// Compute an index of newly created [`OrderedUtxo`]s, given a transaction,
|
||||
/// its precomputed transaction hash, the transaction's index in its block,
|
||||
/// and the block's height.
|
||||
///
|
||||
/// This function is only intended for use in tests.
|
||||
pub(crate) fn new_transaction_ordered_outputs(
|
||||
transaction: &Transaction,
|
||||
hash: transaction::Hash,
|
||||
tx_index_in_block: usize,
|
||||
height: block::Height,
|
||||
) -> HashMap<transparent::OutPoint, OrderedUtxo> {
|
||||
let mut new_ordered_outputs = HashMap::new();
|
||||
|
||||
let from_coinbase = transaction.is_coinbase();
|
||||
for (output_index_in_transaction, output) in
|
||||
transaction.outputs().iter().cloned().enumerate()
|
||||
{
|
||||
for (output_index_in_transaction, output) in transaction.outputs().iter().cloned().enumerate() {
|
||||
let output_index_in_transaction = output_index_in_transaction
|
||||
.try_into()
|
||||
.expect("unexpectedly large number of outputs");
|
||||
|
@ -116,7 +160,6 @@ pub fn new_ordered_outputs(
|
|||
OrderedUtxo::new(output, height, from_coinbase, tx_index_in_block),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
new_ordered_outputs
|
||||
}
|
||||
|
|
|
@ -31,28 +31,9 @@ use std::convert::TryFrom;
|
|||
///
|
||||
/// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
|
||||
pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> {
|
||||
let tx_in_count = tx.inputs().len();
|
||||
let tx_out_count = tx.outputs().len();
|
||||
let n_joinsplit = tx.joinsplit_count();
|
||||
let n_spends_sapling = tx.sapling_spends_per_anchor().count();
|
||||
let n_outputs_sapling = tx.sapling_outputs().count();
|
||||
let n_actions_orchard = tx.orchard_actions().count();
|
||||
let flags_orchard = tx.orchard_flags().unwrap_or_else(Flags::empty);
|
||||
|
||||
// TODO: Improve the code to express the spec rules better #2410.
|
||||
if tx_in_count
|
||||
+ n_spends_sapling
|
||||
+ n_joinsplit
|
||||
+ (n_actions_orchard > 0 && flags_orchard.contains(Flags::ENABLE_SPENDS)) as usize
|
||||
== 0
|
||||
{
|
||||
if !tx.has_transparent_or_shielded_inputs() {
|
||||
Err(TransactionError::NoInputs)
|
||||
} else if tx_out_count
|
||||
+ n_outputs_sapling
|
||||
+ n_joinsplit
|
||||
+ (n_actions_orchard > 0 && flags_orchard.contains(Flags::ENABLE_OUTPUTS)) as usize
|
||||
== 0
|
||||
{
|
||||
} else if !tx.has_transparent_or_shielded_outputs() {
|
||||
Err(TransactionError::NoOutputs)
|
||||
} else {
|
||||
Ok(())
|
||||
|
|
|
@ -2,15 +2,6 @@
|
|||
|
||||
/// The maturity threshold for transparent coinbase outputs.
|
||||
///
|
||||
/// This threshold uses the relevant chain for the block being verified by the
|
||||
/// non-finalized state.
|
||||
///
|
||||
/// For the best chain, coinbase spends are only allowed from blocks at or below
|
||||
/// the finalized tip. For other chains, coinbase spends can use outputs from
|
||||
/// early non-finalized blocks, or finalized blocks. But if that chain becomes
|
||||
/// the best chain, all non-finalized blocks past the [`MAX_BLOCK_REORG_HEIGHT`]
|
||||
/// will be finalized. This includes all mature coinbase outputs.
|
||||
///
|
||||
/// "A transaction MUST NOT spend a transparent output of a coinbase transaction
|
||||
/// from a block less than 100 blocks prior to the spend. Note that transparent
|
||||
/// outputs of coinbase transactions include Founders' Reward outputs and
|
||||
|
@ -21,8 +12,16 @@ pub const MIN_TRANSPARENT_COINBASE_MATURITY: u32 = 100;
|
|||
/// The maximum chain reorganisation height.
|
||||
///
|
||||
/// This threshold determines the maximum length of the best non-finalized chain.
|
||||
///
|
||||
/// Larger reorganisations would allow double-spends of coinbase transactions.
|
||||
///
|
||||
/// This threshold uses the relevant chain for the block being verified by the
|
||||
/// non-finalized state.
|
||||
///
|
||||
/// For the best chain, coinbase spends are only allowed from blocks at or below
|
||||
/// the finalized tip. For other chains, coinbase spends can use outputs from
|
||||
/// early non-finalized blocks, or finalized blocks. But if that chain becomes
|
||||
/// the best chain, all non-finalized blocks past the [`MAX_BLOCK_REORG_HEIGHT`]
|
||||
/// will be finalized. This includes all mature coinbase outputs.
|
||||
pub const MAX_BLOCK_REORG_HEIGHT: u32 = MIN_TRANSPARENT_COINBASE_MATURITY - 1;
|
||||
|
||||
/// The database format version, incremented each time the database format changes.
|
||||
|
|
|
@ -7,6 +7,8 @@ use zebra_chain::{
|
|||
block, orchard, sapling, sprout, transparent, work::difficulty::CompactDifficulty,
|
||||
};
|
||||
|
||||
use crate::constants::MIN_TRANSPARENT_COINBASE_MATURITY;
|
||||
|
||||
/// A wrapper for type erased errors that is itself clonable and implements the
|
||||
/// Error trait
|
||||
#[derive(Debug, Error, Clone)]
|
||||
|
@ -95,6 +97,28 @@ pub enum ValidateContextError {
|
|||
#[non_exhaustive]
|
||||
EarlyTransparentSpend { outpoint: transparent::OutPoint },
|
||||
|
||||
#[error(
|
||||
"unshielded transparent coinbase spend: {outpoint:?} \
|
||||
must be spent in a transaction which only has shielded outputs"
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
UnshieldedTransparentCoinbaseSpend { outpoint: transparent::OutPoint },
|
||||
|
||||
#[error(
|
||||
"immature transparent coinbase spend: \
|
||||
attempt to spend {outpoint:?} at {spend_height:?}, \
|
||||
but spends are invalid before {min_spend_height:?}, \
|
||||
which is {MIN_TRANSPARENT_COINBASE_MATURITY:?} blocks \
|
||||
after it was created at {created_height:?}"
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
ImmatureTransparentCoinbaseSpend {
|
||||
outpoint: transparent::OutPoint,
|
||||
spend_height: block::Height,
|
||||
min_spend_height: block::Height,
|
||||
created_height: block::Height,
|
||||
},
|
||||
|
||||
#[error("sprout double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
|
||||
#[non_exhaustive]
|
||||
DuplicateSproutNullifier {
|
||||
|
|
|
@ -27,7 +27,6 @@ mod response;
|
|||
mod service;
|
||||
mod util;
|
||||
|
||||
// TODO: move these to integration tests.
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
|
|
|
@ -28,12 +28,14 @@ use crate::{
|
|||
PreparedBlock, Request, Response, ValidateContextError,
|
||||
};
|
||||
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub mod arbitrary;
|
||||
mod check;
|
||||
pub(crate) mod check;
|
||||
mod finalized_state;
|
||||
mod non_finalized_state;
|
||||
mod pending_utxos;
|
||||
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub mod arbitrary;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
|
|
|
@ -7,13 +7,24 @@ use proptest::{
|
|||
test_runner::TestRunner,
|
||||
};
|
||||
|
||||
use zebra_chain::{block::Block, fmt::SummaryDebug, parameters::NetworkUpgrade, LedgerState};
|
||||
use zebra_chain::{
|
||||
block::{self, Block},
|
||||
fmt::SummaryDebug,
|
||||
parameters::NetworkUpgrade,
|
||||
LedgerState,
|
||||
};
|
||||
|
||||
use crate::arbitrary::Prepare;
|
||||
use crate::{arbitrary::Prepare, constants};
|
||||
|
||||
use super::*;
|
||||
|
||||
const MAX_PARTIAL_CHAIN_BLOCKS: usize = 102;
|
||||
/// The chain length for state proptests.
|
||||
///
|
||||
/// Shorter lengths increase the probability of proptest failures.
|
||||
///
|
||||
/// See [`block::arbitrary::PREVOUTS_CHAIN_HEIGHT`] for details.
|
||||
pub const MAX_PARTIAL_CHAIN_BLOCKS: usize =
|
||||
constants::MIN_TRANSPARENT_COINBASE_MATURITY as usize + block::arbitrary::PREVOUTS_CHAIN_HEIGHT;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PreparedChainTree {
|
||||
|
@ -62,7 +73,11 @@ impl Strategy for PreparedChain {
|
|||
.prop_flat_map(|ledger| {
|
||||
(
|
||||
Just(ledger.network),
|
||||
Block::partial_chain_strategy(ledger, MAX_PARTIAL_CHAIN_BLOCKS),
|
||||
Block::partial_chain_strategy(
|
||||
ledger,
|
||||
MAX_PARTIAL_CHAIN_BLOCKS,
|
||||
check::utxo::transparent_coinbase_spend,
|
||||
),
|
||||
)
|
||||
})
|
||||
.prop_map(|(network, vec)| {
|
||||
|
|
|
@ -1,27 +1,126 @@
|
|||
//! Randomised property tests for UTXO contextual validation
|
||||
//! Test vectors and randomised property tests for UTXO contextual validation
|
||||
|
||||
use std::{convert::TryInto, env, sync::Arc};
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use zebra_chain::{
|
||||
amount::Amount,
|
||||
block::{Block, Height},
|
||||
fmt::TypeNameToDebug,
|
||||
serialization::ZcashDeserializeInto,
|
||||
transaction::{LockTime, Transaction},
|
||||
transaction::{self, LockTime, Transaction},
|
||||
transparent,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arbitrary::Prepare,
|
||||
constants::MIN_TRANSPARENT_COINBASE_MATURITY,
|
||||
service::check,
|
||||
service::StateService,
|
||||
tests::setup::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
|
||||
FinalizedBlock,
|
||||
ValidateContextError::{
|
||||
DuplicateTransparentSpend, EarlyTransparentSpend, MissingTransparentOutput,
|
||||
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() {
|
||||
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 utxo = transparent::Utxo {
|
||||
output,
|
||||
height: created_height,
|
||||
from_coinbase: true,
|
||||
};
|
||||
|
||||
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, utxo.clone());
|
||||
assert_eq!(result, Ok(utxo));
|
||||
}
|
||||
|
||||
/// Check that non-shielded spends of coinbase transparent outputs fail.
|
||||
#[test]
|
||||
fn reject_unshielded_coinbase_utxo_spend() {
|
||||
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 utxo = transparent::Utxo {
|
||||
output,
|
||||
height: created_height,
|
||||
from_coinbase: true,
|
||||
};
|
||||
|
||||
let spend_restriction = transparent::CoinbaseSpendRestriction::SomeTransparentOutputs;
|
||||
|
||||
let result = check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, utxo);
|
||||
assert_eq!(result, Err(UnshieldedTransparentCoinbaseSpend { outpoint }));
|
||||
}
|
||||
|
||||
/// Check that early spends of coinbase transparent outputs fail.
|
||||
#[test]
|
||||
fn reject_immature_coinbase_utxo_spend() {
|
||||
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 utxo = transparent::Utxo {
|
||||
output,
|
||||
height: created_height,
|
||||
from_coinbase: true,
|
||||
};
|
||||
|
||||
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, utxo);
|
||||
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).
|
||||
//
|
||||
|
|
|
@ -2,40 +2,36 @@
|
|||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use zebra_chain::transparent;
|
||||
use zebra_chain::{
|
||||
block,
|
||||
transparent::{self, CoinbaseSpendRestriction::*},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
constants::MIN_TRANSPARENT_COINBASE_MATURITY,
|
||||
service::finalized_state::FinalizedState,
|
||||
PreparedBlock,
|
||||
ValidateContextError::{
|
||||
self, DuplicateTransparentSpend, EarlyTransparentSpend, MissingTransparentOutput,
|
||||
self, DuplicateTransparentSpend, EarlyTransparentSpend, ImmatureTransparentCoinbaseSpend,
|
||||
MissingTransparentOutput, UnshieldedTransparentCoinbaseSpend,
|
||||
},
|
||||
};
|
||||
|
||||
/// Reject double-spends of transparent outputs:
|
||||
/// Reject invalid spends of transparent outputs.
|
||||
///
|
||||
/// Double-spends:
|
||||
/// - duplicate spends that are both in this block,
|
||||
/// - spends of an output that was spent by a previous block,
|
||||
///
|
||||
/// Missing spends:
|
||||
/// - 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.
|
||||
/// (in linear chain and transaction order),
|
||||
/// - spends of UTXOs that were never created in this chain,
|
||||
///
|
||||
/// 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(
|
||||
/// Invalid spends:
|
||||
/// - spends of an immature transparent coinbase output,
|
||||
/// - unshielded spends of a transparent coinbase output.
|
||||
pub fn transparent_spend(
|
||||
prepared: &PreparedBlock,
|
||||
non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
|
||||
non_finalized_chain_spent_utxos: &HashSet<transparent::OutPoint>,
|
||||
|
@ -52,6 +48,7 @@ pub fn transparent_double_spends(
|
|||
});
|
||||
|
||||
for spend in spends {
|
||||
// see `transparent_spend_chain_order` for the consensus rule
|
||||
if !block_spends.insert(*spend) {
|
||||
// reject in-block duplicate spends
|
||||
return Err(DuplicateTransparentSpend {
|
||||
|
@ -60,12 +57,62 @@ pub fn transparent_double_spends(
|
|||
});
|
||||
}
|
||||
|
||||
// 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.
|
||||
let utxo = transparent_spend_chain_order(
|
||||
*spend,
|
||||
spend_tx_index_in_block,
|
||||
&prepared.new_outputs,
|
||||
non_finalized_chain_unspent_utxos,
|
||||
non_finalized_chain_spent_utxos,
|
||||
finalized_state,
|
||||
)?;
|
||||
|
||||
if let Some(output) = prepared.new_outputs.get(spend) {
|
||||
// The state service returns UTXOs from pending blocks,
|
||||
// which can be rejected by later contextual checks.
|
||||
// This is a particular issue for v5 transactions,
|
||||
// because their authorizing data is only bound to the block data
|
||||
// during contextual validation (#2336).
|
||||
//
|
||||
// We don't want to use UTXOs from invalid pending blocks,
|
||||
// so we check transparent coinbase maturity and shielding
|
||||
// using known valid UTXOs during non-finalized chain validation.
|
||||
|
||||
let spend_restriction = transaction.coinbase_spend_restriction(prepared.height);
|
||||
transparent_coinbase_spend(*spend, spend_restriction, utxo)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that transparent 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.
|
||||
///
|
||||
/// "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
|
||||
///
|
||||
/// "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
|
||||
fn transparent_spend_chain_order(
|
||||
spend: transparent::OutPoint,
|
||||
spend_tx_index_in_block: usize,
|
||||
block_new_outputs: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||||
non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
|
||||
non_finalized_chain_spent_utxos: &HashSet<transparent::OutPoint>,
|
||||
finalized_state: &FinalizedState,
|
||||
) -> Result<transparent::Utxo, ValidateContextError> {
|
||||
if let Some(output) = block_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
|
||||
//
|
||||
|
@ -75,39 +122,83 @@ pub fn transparent_double_spends(
|
|||
// 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 });
|
||||
return Err(EarlyTransparentSpend { outpoint: spend });
|
||||
} else {
|
||||
// a unique spend of a previous transaction's output is ok
|
||||
continue;
|
||||
return Ok(output.utxo.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if non_finalized_chain_spent_utxos.contains(spend) {
|
||||
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,
|
||||
outpoint: spend,
|
||||
location: "the non-finalized chain",
|
||||
});
|
||||
}
|
||||
|
||||
if !non_finalized_chain_unspent_utxos.contains_key(spend)
|
||||
&& finalized_state.utxo(spend).is_none()
|
||||
{
|
||||
match (
|
||||
non_finalized_chain_unspent_utxos.get(&spend),
|
||||
finalized_state.utxo(&spend),
|
||||
) {
|
||||
(None, 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,
|
||||
Err(MissingTransparentOutput {
|
||||
outpoint: spend,
|
||||
location: "the non-finalized and finalized chain",
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
(Some(utxo), _) => Ok(utxo.clone()),
|
||||
(_, Some(utxo)) => Ok(utxo),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
/// Check that `utxo` is spendable, based on the coinbase `spend_restriction`.
|
||||
///
|
||||
/// "A transaction with one or more transparent inputs from coinbase transactions
|
||||
/// MUST have no transparent outputs (i.e.tx_out_count MUST be 0)."
|
||||
///
|
||||
/// "A transaction MUST NOT spend a transparent output of a coinbase transaction
|
||||
/// from a block less than 100 blocks prior to the spend.
|
||||
///
|
||||
/// Note that transparent outputs of coinbase transactions include Founders’
|
||||
/// Reward outputs and transparent funding stream outputs."
|
||||
///
|
||||
/// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
|
||||
pub fn transparent_coinbase_spend(
|
||||
outpoint: transparent::OutPoint,
|
||||
spend_restriction: transparent::CoinbaseSpendRestriction,
|
||||
utxo: transparent::Utxo,
|
||||
) -> Result<transparent::Utxo, ValidateContextError> {
|
||||
if !utxo.from_coinbase {
|
||||
return Ok(utxo);
|
||||
}
|
||||
|
||||
match spend_restriction {
|
||||
OnlyShieldedOutputs { spend_height } => {
|
||||
let min_spend_height = utxo.height + block::Height(MIN_TRANSPARENT_COINBASE_MATURITY);
|
||||
// TODO: allow full u32 range of block heights (#1113)
|
||||
let min_spend_height =
|
||||
min_spend_height.expect("valid UTXOs have coinbase heights far below Height::MAX");
|
||||
if spend_height >= min_spend_height {
|
||||
Ok(utxo)
|
||||
} else {
|
||||
Err(ImmatureTransparentCoinbaseSpend {
|
||||
outpoint,
|
||||
spend_height,
|
||||
min_spend_height,
|
||||
created_height: utxo.height,
|
||||
})
|
||||
}
|
||||
}
|
||||
SomeTransparentOutputs => Err(UnshieldedTransparentCoinbaseSpend { outpoint }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reject negative remaining transaction value.
|
||||
|
|
|
@ -172,7 +172,7 @@ impl NonFinalizedState {
|
|||
prepared: PreparedBlock,
|
||||
finalized_state: &FinalizedState,
|
||||
) -> Result<Chain, ValidateContextError> {
|
||||
check::utxo::transparent_double_spends(
|
||||
check::utxo::transparent_spend(
|
||||
&prepared,
|
||||
&parent_chain.unspent_utxos(),
|
||||
&parent_chain.spent_utxos,
|
||||
|
|
|
@ -2,7 +2,12 @@ use std::{env, sync::Arc};
|
|||
|
||||
use zebra_test::prelude::*;
|
||||
|
||||
use zebra_chain::{block::Block, fmt::DisplayToDebug, parameters::NetworkUpgrade::*, LedgerState};
|
||||
use zebra_chain::{
|
||||
block::{self, Block},
|
||||
fmt::DisplayToDebug,
|
||||
parameters::NetworkUpgrade::*,
|
||||
LedgerState,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arbitrary::Prepare,
|
||||
|
@ -17,6 +22,10 @@ use crate::{
|
|||
const DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES: u32 = 16;
|
||||
|
||||
/// Check that a forked chain is the same as a chain that had the same blocks appended.
|
||||
///
|
||||
/// Also check for:
|
||||
/// - no transparent spends in the genesis block, because genesis transparent outputs are ignored
|
||||
/// - at least one transparent PrevOut input in the entire chain
|
||||
#[test]
|
||||
fn forked_equals_pushed() -> Result<()> {
|
||||
zebra_test::init();
|
||||
|
@ -30,12 +39,37 @@ fn forked_equals_pushed() -> Result<()> {
|
|||
let fork_tip_hash = chain[fork_at_count - 1].hash;
|
||||
let mut full_chain = Chain::default();
|
||||
let mut partial_chain = Chain::default();
|
||||
let mut has_prevouts = false;
|
||||
|
||||
for block in chain.iter().take(fork_at_count) {
|
||||
partial_chain = partial_chain.push(block.clone())?;
|
||||
}
|
||||
for block in chain.iter() {
|
||||
full_chain = full_chain.push(block.clone())?;
|
||||
|
||||
// check some other properties of generated chains
|
||||
if block.height == block::Height(0) {
|
||||
prop_assert_eq!(
|
||||
block
|
||||
.block
|
||||
.transactions
|
||||
.iter()
|
||||
.flat_map(|t| t.inputs())
|
||||
.filter_map(|i| i.outpoint())
|
||||
.count(),
|
||||
0,
|
||||
"unexpected transparent prevout inputs at height {:?}: genesis transparent outputs are ignored",
|
||||
block.height,
|
||||
);
|
||||
}
|
||||
|
||||
has_prevouts |= block
|
||||
.block
|
||||
.transactions
|
||||
.iter()
|
||||
.flat_map(|t| t.inputs())
|
||||
.find_map(|i| i.outpoint())
|
||||
.is_some();
|
||||
}
|
||||
|
||||
let forked = full_chain.fork(fork_tip_hash).expect("fork works").expect("hash is present");
|
||||
|
@ -43,6 +77,10 @@ fn forked_equals_pushed() -> Result<()> {
|
|||
// the first check is redundant, but it's useful for debugging
|
||||
prop_assert_eq!(forked.blocks.len(), partial_chain.blocks.len());
|
||||
prop_assert!(forked.eq_internal_state(&partial_chain));
|
||||
|
||||
// this assertion checks that we're still generating some transparent spends,
|
||||
// after proptests remove unshielded and immature transparent coinbase spends
|
||||
prop_assert!(has_prevouts, "no blocks in chain had prevouts");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -12,7 +12,10 @@ use zebra_chain::{
|
|||
transaction::Transaction,
|
||||
};
|
||||
|
||||
use crate::{service::StateService, Config, FinalizedBlock};
|
||||
use crate::{
|
||||
service::{check, StateService},
|
||||
Config, FinalizedBlock,
|
||||
};
|
||||
|
||||
/// Generate a chain that allows us to make tests for the legacy chain rules.
|
||||
///
|
||||
|
@ -63,7 +66,11 @@ pub(crate) fn partial_nu5_chain_strategy(
|
|||
transaction_has_valid_network_upgrade,
|
||||
)
|
||||
.prop_flat_map(move |init| {
|
||||
Block::partial_chain_strategy(init, blocks_after_nu_activation as usize)
|
||||
Block::partial_chain_strategy(
|
||||
init,
|
||||
blocks_after_nu_activation as usize,
|
||||
check::utxo::transparent_coinbase_spend,
|
||||
)
|
||||
})
|
||||
.prop_map(move |partial_chain| (network, nu_activation, partial_chain))
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue