use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use chrono::{DateTime, Duration, Utc};
use proptest::{collection::vec, prelude::*};
use tower::{buffer::Buffer, ServiceExt};
use zebra_chain::{
amount::Amount,
block,
parameters::{Network, NetworkUpgrade},
serialization::arbitrary::{datetime_full, datetime_u32},
transaction::{LockTime, Transaction},
transparent,
};
use crate::{error::TransactionError, transaction};
use super::mock_transparent_transfer;
const MAX_TRANSPARENT_INPUTS: usize = 10;
proptest! {
#[test]
fn zero_lock_time_is_always_unlocked(
(network, block_height) in sapling_onwards_strategy(),
block_time in datetime_full(),
relative_source_fund_heights in vec(0.0..1.0, 1..=MAX_TRANSPARENT_INPUTS),
transaction_version in 4_u8..=5,
) {
let _init_guard = zebra_test::init();
let zero_lock_time = LockTime::Height(block::Height(0));
let (transaction, known_utxos) = mock_transparent_transaction(
&network,
block_height,
relative_source_fund_heights,
transaction_version,
zero_lock_time,
);
let transaction_id = transaction.unmined_id();
let result = validate(transaction, block_height, block_time, known_utxos, network);
prop_assert!(
result.is_ok(),
"Unexpected validation error: {}",
result.unwrap_err()
);
prop_assert_eq!(result.unwrap().tx_id(), transaction_id);
}
#[test]
fn lock_time_is_ignored_because_of_sequence_numbers(
(network, block_height) in sapling_onwards_strategy(),
block_time in datetime_full(),
relative_source_fund_heights in vec(0.0..1.0, 1..=MAX_TRANSPARENT_INPUTS),
transaction_version in 4_u8..=5,
lock_time in any::<LockTime>(),
) {
let _init_guard = zebra_test::init();
let (mut transaction, known_utxos) = mock_transparent_transaction(
&network,
block_height,
relative_source_fund_heights,
transaction_version,
lock_time,
);
for input in transaction.inputs_mut() {
input.set_sequence(u32::MAX);
}
let transaction_id = transaction.unmined_id();
let result = validate(transaction, block_height, block_time, known_utxos, network);
prop_assert!(
result.is_ok(),
"Unexpected validation error: {}",
result.unwrap_err()
);
prop_assert_eq!(result.unwrap().tx_id(), transaction_id);
}
#[test]
fn transaction_is_rejected_based_on_lock_height(
(network, block_height) in sapling_onwards_strategy(),
block_time in datetime_full(),
relative_source_fund_heights in vec(0.0..1.0, 1..=MAX_TRANSPARENT_INPUTS),
transaction_version in 4_u8..=5,
relative_unlock_height in 0.0..1.0,
) {
let _init_guard = zebra_test::init();
let unlock_height = scale_block_height(block_height, None, relative_unlock_height);
let lock_time = LockTime::Height(unlock_height);
let (transaction, known_utxos) = mock_transparent_transaction(
&network,
block_height,
relative_source_fund_heights,
transaction_version,
lock_time,
);
let result = validate(transaction, block_height, block_time, known_utxos, network);
prop_assert_eq!(
result,
Err(TransactionError::LockedUntilAfterBlockHeight(unlock_height))
);
}
#[test]
fn transaction_is_rejected_based_on_lock_time(
(network, block_height) in sapling_onwards_strategy(),
first_datetime in datetime_u32(),
second_datetime in datetime_u32(),
relative_source_fund_heights in vec(0.0..1.0, 1..=MAX_TRANSPARENT_INPUTS),
transaction_version in 4_u8..=5,
) {
let _init_guard = zebra_test::init();
let (unlock_time, block_time) = if first_datetime >= second_datetime {
(first_datetime, second_datetime)
} else {
(second_datetime, first_datetime)
};
let (transaction, known_utxos) = mock_transparent_transaction(
&network,
block_height,
relative_source_fund_heights,
transaction_version,
LockTime::Time(unlock_time),
);
let result = validate(transaction, block_height, block_time, known_utxos, network);
prop_assert_eq!(
result,
Err(TransactionError::LockedUntilAfterBlockTime(unlock_time))
);
}
#[test]
fn transaction_with_lock_height_is_accepted(
(network, block_height) in sapling_onwards_strategy(),
block_time in datetime_full(),
relative_source_fund_heights in vec(0.0..1.0, 1..=MAX_TRANSPARENT_INPUTS),
transaction_version in 4_u8..=5,
relative_unlock_height in 0.0..1.0,
) {
let _init_guard = zebra_test::init();
let exclusive_max_height = block::Height(block_height.0 + 1);
let unlock_height = scale_block_height(None, exclusive_max_height, relative_unlock_height);
let lock_time = LockTime::Height(unlock_height);
let (transaction, known_utxos) = mock_transparent_transaction(
&network,
block_height,
relative_source_fund_heights,
transaction_version,
lock_time,
);
let transaction_id = transaction.unmined_id();
let result = validate(transaction, block_height, block_time, known_utxos, network);
prop_assert!(
result.is_ok(),
"Unexpected validation error: {}",
result.unwrap_err()
);
prop_assert_eq!(result.unwrap().tx_id(), transaction_id);
}
#[test]
fn transaction_with_lock_time_is_accepted(
(network, block_height) in sapling_onwards_strategy(),
first_datetime in datetime_u32(),
second_datetime in datetime_u32(),
relative_source_fund_heights in vec(0.0..1.0, 1..=MAX_TRANSPARENT_INPUTS),
transaction_version in 4_u8..=5,
) {
let _init_guard = zebra_test::init();
let (unlock_time, block_time) = if first_datetime < second_datetime {
(first_datetime, second_datetime)
} else if first_datetime > second_datetime {
(second_datetime, first_datetime)
} else if first_datetime == DateTime::<Utc>::MAX_UTC {
(first_datetime - Duration::nanoseconds(1), first_datetime)
} else {
(first_datetime, first_datetime + Duration::nanoseconds(1))
};
let (transaction, known_utxos) = mock_transparent_transaction(
&network,
block_height,
relative_source_fund_heights,
transaction_version,
LockTime::Time(unlock_time),
);
let transaction_id = transaction.unmined_id();
let result = validate(transaction, block_height, block_time, known_utxos, network);
prop_assert!(
result.is_ok(),
"Unexpected validation error: {}",
result.unwrap_err()
);
prop_assert_eq!(result.unwrap().tx_id(), transaction_id);
}
}
fn sapling_onwards_strategy() -> impl Strategy<Value = (Network, block::Height)> {
any::<Network>().prop_flat_map(|network| {
let start_height_value = NetworkUpgrade::Sapling
.activation_height(&network)
.expect("Sapling to have an activation height")
.0;
let end_height_value = block::Height::MAX_EXPIRY_HEIGHT.0;
(start_height_value..=end_height_value)
.prop_map(move |height_value| (network.clone(), block::Height(height_value)))
})
}
fn mock_transparent_transaction(
network: &Network,
block_height: block::Height,
relative_source_heights: Vec<f64>,
transaction_version: u8,
lock_time: LockTime,
) -> (
Transaction,
HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
) {
let (transaction_version, network_upgrade) =
sanitize_transaction_version(network, transaction_version, block_height);
let (inputs, outputs, known_utxos) =
mock_transparent_transfers(relative_source_heights, block_height);
let expiry_height = block_height;
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
let zip233_amount = Amount::zero();
let transaction = match transaction_version {
4 => Transaction::V4 {
inputs,
outputs,
lock_time,
expiry_height,
joinsplit_data: None,
sapling_shielded_data: None,
},
5 => Transaction::V5 {
inputs,
outputs,
lock_time,
expiry_height,
sapling_shielded_data: None,
orchard_shielded_data: None,
network_upgrade,
},
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
6 => Transaction::V6 {
inputs,
outputs,
lock_time,
expiry_height,
zip233_amount,
sapling_shielded_data: None,
orchard_shielded_data: None,
network_upgrade,
},
invalid_version => unreachable!("invalid transaction version: {}", invalid_version),
};
(transaction, known_utxos)
}
fn sanitize_transaction_version(
network: &Network,
transaction_version: u8,
block_height: block::Height,
) -> (u8, NetworkUpgrade) {
let network_upgrade = NetworkUpgrade::current(network, block_height);
let max_version = {
use NetworkUpgrade::*;
match network_upgrade {
Genesis => 1,
BeforeOverwinter => 2,
Overwinter => 3,
Sapling | Blossom | Heartwood | Canopy => 4,
Nu5 | Nu6 | Nu6_1 | Nu7 => 5,
#[cfg(zcash_unstable = "zfuture")]
NetworkUpgrade::ZFuture => u8::MAX,
}
};
let sanitized_version = transaction_version.min(max_version);
(sanitized_version, network_upgrade)
}
fn mock_transparent_transfers(
relative_source_heights: Vec<f64>,
block_height: block::Height,
) -> (
Vec<transparent::Input>,
Vec<transparent::Output>,
HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
) {
let transfer_count = relative_source_heights.len();
let mut inputs = Vec::with_capacity(transfer_count);
let mut outputs = Vec::with_capacity(transfer_count);
let mut known_utxos = HashMap::with_capacity(transfer_count);
for (index, relative_source_height) in relative_source_heights.into_iter().enumerate() {
let fake_source_fund_height =
scale_block_height(None, block_height, relative_source_height);
let outpoint_index = index
.try_into()
.expect("too many mock transparent transfers requested");
let (input, output, new_utxos) = mock_transparent_transfer(
fake_source_fund_height,
true,
outpoint_index,
Amount::try_from(1).expect("invalid value"),
);
inputs.push(input);
outputs.push(output);
known_utxos.extend(new_utxos);
}
(inputs, outputs, known_utxos)
}
fn scale_block_height(
min_height: impl Into<Option<block::Height>>,
max_height: impl Into<Option<block::Height>>,
scale: f64,
) -> block::Height {
assert!(scale >= 0.0);
assert!(scale < 1.0);
let min_height = min_height.into().unwrap_or(block::Height(0));
let max_height = max_height.into().unwrap_or(block::Height::MAX);
assert!(min_height <= max_height);
let min_height_value = f64::from(min_height.0);
let max_height_value = f64::from(max_height.0);
let height_range = max_height_value - min_height_value;
let new_height_value = (height_range * scale + min_height_value).floor();
block::Height(new_height_value as u32)
}
fn validate(
transaction: Transaction,
height: block::Height,
block_time: DateTime<Utc>,
known_utxos: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
network: Network,
) -> Result<transaction::Response, TransactionError> {
zebra_test::MULTI_THREADED_RUNTIME.block_on(async {
let state_service =
tower::service_fn(|_| async { unreachable!("State service should not be called") });
let verifier = transaction::Verifier::new_for_tests(&network, state_service);
let verifier = Buffer::new(verifier, 10);
let transaction_hash = transaction.hash();
verifier
.clone()
.oneshot(transaction::Request::Block {
transaction_hash,
transaction: Arc::new(transaction),
known_utxos: Arc::new(known_utxos),
known_outpoint_hashes: Arc::new(HashSet::new()),
height,
time: block_time,
})
.await
.map_err(|err| {
*err.downcast()
.expect("error type should be TransactionError")
})
})
}