use std::{collections::HashMap, 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},
DateTime32,
},
transaction::{LockTime, Transaction, UnminedTx},
transparent,
};
use zebra_state as zs;
use crate::{error::TransactionError, transaction, BoxError};
use super::mock_transparent_transfer;
const MAX_TRANSPARENT_INPUTS: usize = 10;
const MEMPOOL_FUNDED_INPUT_VALUE: i64 = 100_000;
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 (transaction, known_utxos) = mock_transparent_transaction(
&network,
block_height,
relative_source_fund_heights,
transaction_version,
lock_time,
);
let mut inputs = transaction.inputs();
for input in &mut inputs {
input.set_sequence(u32::MAX);
}
let transaction = transaction.with_transparent_inputs(inputs);
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,
block::Height::MAX_EXPIRY_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 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)
};
prop_assume!(unlock_time.timestamp() >= 500_000_000);
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))
};
prop_assume!(unlock_time.timestamp() >= 500_000_000);
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);
}
#[test]
fn mempool_acceptance_implies_block_acceptance(
(network, block_height) in sapling_onwards_strategy(),
next_median_time_past in datetime_u32(),
relative_source_fund_heights in vec(0.0..1.0, 1..=MAX_TRANSPARENT_INPUTS),
transaction_version in 4_u8..=5,
lock_time in lock_time_strategy(),
) {
let _init_guard = zebra_test::init();
prop_assume!(next_median_time_past < DateTime::<Utc>::MAX_UTC);
let block_time = next_median_time_past + Duration::seconds(1);
let (transaction, known_utxos) = mock_funded_transparent_transaction(
&network,
block_height,
relative_source_fund_heights,
transaction_version,
lock_time,
);
let mempool_result = validate_mempool(
transaction.clone(),
block_height,
next_median_time_past,
known_utxos.clone(),
network.clone(),
);
if let Ok(mempool_response) = mempool_result {
let block_result = validate_with_state(
transaction,
block_height,
block_time,
next_median_time_past,
known_utxos,
network,
);
prop_assert!(
block_result.is_ok(),
"the mempool accepted a transaction that block verification rejected, \
so it would make a block template unmineable: {}",
block_result.unwrap_err()
);
prop_assert_eq!(
block_result.unwrap().tx_id.mined_id(),
mempool_response.transaction.transaction.id.mined_id()
);
}
}
}
fn lock_time_strategy() -> impl Strategy<Value = LockTime> {
prop_oneof![
2 => Just(LockTime::Height(block::Height(0))),
1 => any::<LockTime>(),
]
}
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;
let transaction = match transaction_version {
4 => Transaction::test_v4(inputs, outputs, lock_time, expiry_height),
5 => Transaction::test_v5(network_upgrade, inputs, outputs, lock_time, expiry_height),
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 min_version = if network_upgrade >= NetworkUpgrade::Nu7 {
5
} else {
1
};
let max_version = {
use NetworkUpgrade::*;
match network_upgrade {
Genesis => 1,
BeforeOverwinter => 2,
Overwinter => 3,
Sapling | Blossom | Heartwood | Canopy => 4,
Nu5 | Nu6 | Nu6_1 | Nu6_2 | Nu6_3 | Nu7 => 5,
#[cfg(zcash_unstable = "zfuture")]
NetworkUpgrade::ZFuture => u8::MAX,
}
};
let sanitized_version = transaction_version.clamp(min_version, 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 mock_funded_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 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(MEMPOOL_FUNDED_INPUT_VALUE).expect("invalid value"),
);
inputs.push(input);
outputs.push(output);
known_utxos.extend(new_utxos);
}
let expiry_height = block_height;
let transaction = match transaction_version {
4 => Transaction::test_v4(inputs, outputs, lock_time, expiry_height),
5 => Transaction::test_v5(network_upgrade, inputs, outputs, lock_time, expiry_height),
6 => Transaction::test_v6(network_upgrade, inputs, outputs, lock_time, expiry_height),
invalid_version => unreachable!("invalid transaction version: {}", invalid_version),
};
(transaction, known_utxos)
}
fn mock_state_service(
known_utxos: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
next_median_time_past: DateTime32,
) -> impl tower::Service<
zs::Request,
Response = zs::Response,
Error = BoxError,
Future = impl Send + 'static,
> + Send
+ Clone
+ 'static {
let known_utxos = Arc::new(known_utxos);
tower::service_fn(move |request: zs::Request| {
let known_utxos = known_utxos.clone();
async move {
let response = match request {
zs::Request::UnspentBestChainUtxo(outpoint) => zs::Response::UnspentBestChainUtxo(
known_utxos
.get(&outpoint)
.map(|ordered_utxo| ordered_utxo.utxo.clone()),
),
zs::Request::AwaitUtxo(outpoint) => zs::Response::Utxo(
known_utxos
.get(&outpoint)
.expect("mock state contains every spent UTXO")
.utxo
.clone(),
),
zs::Request::BestChainNextMedianTimePast => {
zs::Response::BestChainNextMedianTimePast(next_median_time_past)
}
zs::Request::CheckBestChainTipNullifiersAndAnchors(_) => {
zs::Response::ValidBestChainTipNullifiersAndAnchors
}
request => unreachable!("unexpected state request: {request:?}"),
};
Ok::<_, BoxError>(response)
}
})
}
fn validate_mempool(
transaction: Transaction,
height: block::Height,
next_median_time_past: DateTime<Utc>,
known_utxos: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
network: Network,
) -> Result<transaction::MempoolResponse, TransactionError> {
zebra_test::MULTI_THREADED_RUNTIME.block_on(async {
let next_median_time_past = DateTime32::try_from(next_median_time_past)
.expect("test median-time-past is generated by `datetime_u32()`");
let state_service = mock_state_service(known_utxos, next_median_time_past);
let verifier = transaction::MempoolTxVerifier::new_for_tests(&network, state_service);
let verifier = Buffer::new(verifier, 10);
verifier
.oneshot(transaction::MempoolRequest {
transaction: UnminedTx::from(Arc::new(transaction)),
height,
})
.await
.map_err(|err| {
*err.downcast()
.expect("error type should be TransactionError")
})
})
}
fn validate_with_state(
transaction: Transaction,
height: block::Height,
block_time: DateTime<Utc>,
next_median_time_past: DateTime<Utc>,
known_utxos: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
network: Network,
) -> Result<transaction::BlockResponse, TransactionError> {
zebra_test::MULTI_THREADED_RUNTIME.block_on(async {
let next_median_time_past = DateTime32::try_from(next_median_time_past)
.expect("test median-time-past is generated by `datetime_u32()`");
let state_service = mock_state_service(known_utxos, next_median_time_past);
let verifier = transaction::BlockTxVerifier::new(&network, state_service);
let verifier = Buffer::new(verifier, 10);
let transaction_hash = transaction.hash();
verifier
.oneshot(transaction::BlockRequest {
transaction_hash,
transaction: Arc::new(transaction),
known_utxos: Arc::new(HashMap::new()),
height,
time: block_time,
})
.await
.map_err(|err| {
*err.downcast()
.expect("error type should be TransactionError")
})
})
}
fn validate(
transaction: Transaction,
height: block::Height,
block_time: DateTime<Utc>,
known_utxos: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
network: Network,
) -> Result<transaction::BlockResponse, 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::BlockTxVerifier::new(&network, state_service);
let verifier = Buffer::new(verifier, 10);
let transaction_hash = transaction.hash();
verifier
.clone()
.oneshot(transaction::BlockRequest {
transaction_hash,
transaction: Arc::new(transaction),
known_utxos: Arc::new(known_utxos),
height,
time: block_time,
})
.await
.map_err(|err| {
*err.downcast()
.expect("error type should be TransactionError")
})
})
}