Add some proptests for lock time validation (#3089)
* Create a strategy for block heights after Sapling Provides an arbitrary network (mainnet or testnet) and a block height between the Sapling activation height on that network and the maximum block height. * Create a helper function to select block heights Allows generating block heights inside a range using a scale factor between 0 and 1. * Allow specifying the outpoint index for mock UTXOs Avoid creating multiple transparent transfers in the same transaction with the same source UTXO, which would lead to a double spend. * Create helper function to mock multiple transfers Given relative block height scale factors, create a mock transparent transfer for each one of them. Also add a constant that serves as a guideline for the maximum number of transparent transfers to mock. * Create helper function to sanitize tx. version Make sure the arbitrary transaction version is valid for the network (testnet or mainnet) at the specified block height. * Create `mock_transparent_transaction` helper func. Creates a V4 or V5 mock transaction that only includes transparent inputs and outputs. * Create helper function for transaction validation Performs the actual tested action of verifying a transaction. It sets up the verifier and uses it to obtain the verification result. * Test if zero lock time means unlocked Generate arbitrary transactions with zero lock time, and check that they are accepted by the transaction verifier. * Allow changing the sequence number of an input Add a setter method for a `transparent::Input`'s sequence number. This setter is only available for testing. * Test if sequence numbers can disable lock time Create arbitrary transactions and set the sequence numbers of all of its inputs to `u32::MAX` to see if that disables the lock time and the transactions are accepted by the verifier. * Test block height lock times Make sure that the transaction verifier rejects transactions that are still locked at a certain block height. * Test block time lock times Test that the transaction verifier rejects a transaction that is validated at a block time that's before the transaction's lock time. * Test unlocking by block height Test that transactions unlocked at an earlier block height are accepted by the transaction verifier. * Test transactions unlocked by the block time Test that transactions that were unlocked at a previous block time are accepted by the transaction verifier. * Fix an incorrect method comment Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
6cbd7dce43
commit
410133435e
|
@ -158,6 +158,18 @@ impl Input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the input's sequence number.
|
||||||
|
///
|
||||||
|
/// Only for use in tests.
|
||||||
|
#[cfg(any(test, feature = "proptest-impl"))]
|
||||||
|
pub fn set_sequence(&mut self, new_sequence: u32) {
|
||||||
|
match self {
|
||||||
|
Input::PrevOut { sequence, .. } | Input::Coinbase { sequence, .. } => {
|
||||||
|
*sequence = new_sequence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// If this is a `PrevOut` input, returns this input's outpoint.
|
/// If this is a `PrevOut` input, returns this input's outpoint.
|
||||||
/// Otherwise, returns `None`.
|
/// Otherwise, returns `None`.
|
||||||
pub fn outpoint(&self) -> Option<OutPoint> {
|
pub fn outpoint(&self) -> Option<OutPoint> {
|
||||||
|
|
|
@ -31,6 +31,9 @@ use super::{check, Request, Verifier};
|
||||||
use crate::{error::TransactionError, script};
|
use crate::{error::TransactionError, script};
|
||||||
use color_eyre::eyre::Report;
|
use color_eyre::eyre::Report;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod prop;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn v5_fake_transactions() -> Result<(), Report> {
|
fn v5_fake_transactions() -> Result<(), Report> {
|
||||||
zebra_test::init();
|
zebra_test::init();
|
||||||
|
@ -358,7 +361,7 @@ async fn v4_transaction_with_transparent_transfer_is_accepted() {
|
||||||
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
||||||
|
|
||||||
// Create a fake transparent transfer that should succeed
|
// Create a fake transparent transfer that should succeed
|
||||||
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true);
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true, 0);
|
||||||
|
|
||||||
// Create a V4 transaction
|
// Create a V4 transaction
|
||||||
let transaction = Transaction::V4 {
|
let transaction = Transaction::V4 {
|
||||||
|
@ -458,7 +461,7 @@ async fn v4_transaction_with_transparent_transfer_is_rejected_by_the_script() {
|
||||||
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
||||||
|
|
||||||
// Create a fake transparent transfer that should not succeed
|
// Create a fake transparent transfer that should not succeed
|
||||||
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, false);
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, false, 0);
|
||||||
|
|
||||||
// Create a V4 transaction
|
// Create a V4 transaction
|
||||||
let transaction = Transaction::V4 {
|
let transaction = Transaction::V4 {
|
||||||
|
@ -509,7 +512,7 @@ async fn v4_transaction_with_conflicting_transparent_spend_is_rejected() {
|
||||||
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
||||||
|
|
||||||
// Create a fake transparent transfer that should succeed
|
// Create a fake transparent transfer that should succeed
|
||||||
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true);
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true, 0);
|
||||||
|
|
||||||
// Create a V4 transaction
|
// Create a V4 transaction
|
||||||
let transaction = Transaction::V4 {
|
let transaction = Transaction::V4 {
|
||||||
|
@ -700,7 +703,7 @@ async fn v5_transaction_with_transparent_transfer_is_accepted() {
|
||||||
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
||||||
|
|
||||||
// Create a fake transparent transfer that should succeed
|
// Create a fake transparent transfer that should succeed
|
||||||
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true);
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true, 0);
|
||||||
|
|
||||||
// Create a V5 transaction
|
// Create a V5 transaction
|
||||||
let transaction = Transaction::V5 {
|
let transaction = Transaction::V5 {
|
||||||
|
@ -805,7 +808,7 @@ async fn v5_transaction_with_transparent_transfer_is_rejected_by_the_script() {
|
||||||
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
||||||
|
|
||||||
// Create a fake transparent transfer that should not succeed
|
// Create a fake transparent transfer that should not succeed
|
||||||
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, false);
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, false, 0);
|
||||||
|
|
||||||
// Create a V5 transaction
|
// Create a V5 transaction
|
||||||
let transaction = Transaction::V5 {
|
let transaction = Transaction::V5 {
|
||||||
|
@ -858,7 +861,7 @@ async fn v5_transaction_with_conflicting_transparent_spend_is_rejected() {
|
||||||
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
||||||
|
|
||||||
// Create a fake transparent transfer that should succeed
|
// Create a fake transparent transfer that should succeed
|
||||||
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true);
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true, 0);
|
||||||
|
|
||||||
// Create a V4 transaction
|
// Create a V4 transaction
|
||||||
let transaction = Transaction::V5 {
|
let transaction = Transaction::V5 {
|
||||||
|
@ -1330,6 +1333,7 @@ fn v5_with_duplicate_orchard_action() {
|
||||||
fn mock_transparent_transfer(
|
fn mock_transparent_transfer(
|
||||||
previous_utxo_height: block::Height,
|
previous_utxo_height: block::Height,
|
||||||
script_should_succeed: bool,
|
script_should_succeed: bool,
|
||||||
|
outpoint_index: u32,
|
||||||
) -> (
|
) -> (
|
||||||
transparent::Input,
|
transparent::Input,
|
||||||
transparent::Output,
|
transparent::Output,
|
||||||
|
@ -1343,7 +1347,7 @@ fn mock_transparent_transfer(
|
||||||
// Mock an unspent transaction output
|
// Mock an unspent transaction output
|
||||||
let previous_outpoint = transparent::OutPoint {
|
let previous_outpoint = transparent::OutPoint {
|
||||||
hash: Hash([1u8; 32]),
|
hash: Hash([1u8; 32]),
|
||||||
index: 0,
|
index: outpoint_index,
|
||||||
};
|
};
|
||||||
|
|
||||||
let lock_script = if script_should_succeed {
|
let lock_script = if script_should_succeed {
|
||||||
|
|
|
@ -0,0 +1,458 @@
|
||||||
|
use std::{collections::HashMap, convert::TryInto, sync::Arc};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use proptest::{collection::vec, prelude::*};
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
use zebra_chain::{
|
||||||
|
block,
|
||||||
|
parameters::{Network, NetworkUpgrade},
|
||||||
|
serialization::arbitrary::{datetime_full, datetime_u32},
|
||||||
|
transaction::{LockTime, Transaction},
|
||||||
|
transparent,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::mock_transparent_transfer;
|
||||||
|
use crate::{error::TransactionError, script, transaction};
|
||||||
|
|
||||||
|
/// The maximum number of transparent inputs to include in a mock transaction.
|
||||||
|
const MAX_TRANSPARENT_INPUTS: usize = 10;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
/// Test if a transaction that has a zero value as the lock time is always unlocked.
|
||||||
|
#[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,
|
||||||
|
) {
|
||||||
|
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 if having [`u32::MAX`] as the sequence number of all inputs disables the lock time.
|
||||||
|
#[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>(),
|
||||||
|
) {
|
||||||
|
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 if a transaction locked at a certain block height is rejected.
|
||||||
|
#[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,
|
||||||
|
) {
|
||||||
|
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 if a transaction locked at a certain block time is rejected.
|
||||||
|
#[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,
|
||||||
|
) {
|
||||||
|
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 if a transaction unlocked at an earlier block time is accepted.
|
||||||
|
#[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,
|
||||||
|
) {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
// Because `scale_block_height` uses the range `[min, max)`, with `max` being
|
||||||
|
// non-inclusive, we have to use `block_height + 1` as the upper bound in order to test
|
||||||
|
// verifying at a block height equal to the lock height.
|
||||||
|
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 if transaction unlocked at a previous block time is accepted.
|
||||||
|
#[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,
|
||||||
|
) {
|
||||||
|
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 == chrono::MAX_DATETIME {
|
||||||
|
(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate an arbitrary block height after the Sapling activation height on an arbitrary network.
|
||||||
|
///
|
||||||
|
/// A proptest [`Strategy`] that generates random tuples with
|
||||||
|
///
|
||||||
|
/// - a network (mainnet or testnet)
|
||||||
|
/// - a block height between the Sapling activation height (inclusive) on that network and the
|
||||||
|
/// maximum block height.
|
||||||
|
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.0;
|
||||||
|
|
||||||
|
(start_height_value..=end_height_value)
|
||||||
|
.prop_map(move |height_value| (network, block::Height(height_value)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a mock transaction that only transfers transparent amounts.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// - `network`: the network to use for the transaction (mainnet or testnet)
|
||||||
|
/// - `block_height`: the block height to be used for the transaction's expiry height as well as
|
||||||
|
/// the height that the transaction was (hypothetically) included in a block
|
||||||
|
/// - `relative_source_heights`: a list of values in the range `0.0..1.0`; each item results in the
|
||||||
|
/// creation of a transparent input and output, where the item itself represents a scaled value
|
||||||
|
/// to be converted into a block height between zero and `block_height` (see
|
||||||
|
/// [`scale_block_height`] for details) to serve as the block height that created the input UTXO
|
||||||
|
/// - `transaction_version`: a value that's either `4` or `5` indicating the transaction version to
|
||||||
|
/// be generated; this value is sanitized by [`sanitize_transaction_version`], so it may not be
|
||||||
|
/// able to create a V5 transaction if the `block_height` is before the NU5 activation height
|
||||||
|
/// - `lock_time`: the transaction lock time to be used (note that all transparent inputs have a
|
||||||
|
/// sequence number of `0`, so the lock time is enabled by default)
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// - if `transaction_version` is not `4` or `5` (the only transaction versions that are currently
|
||||||
|
/// supported by the transaction verifier)
|
||||||
|
/// - if `relative_source_heights` has more than `u32::MAX` items (see
|
||||||
|
/// [`mock_transparent_transfers`] for details)
|
||||||
|
/// - if any item of `relative_source_heights` is not in the range `0.0..1.0` (see
|
||||||
|
/// [`scale_block_height`] for details)
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Create fake transparent transfers that should succeed
|
||||||
|
let (inputs, outputs, known_utxos) =
|
||||||
|
mock_transparent_transfers(relative_source_heights, block_height);
|
||||||
|
|
||||||
|
// Create the mock transaction
|
||||||
|
let expiry_height = block_height;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
invalid_version => unreachable!("invalid transaction version: {}", invalid_version),
|
||||||
|
};
|
||||||
|
|
||||||
|
(transaction, known_utxos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitize a transaction version so that it is supported at the specified `block_height` of the
|
||||||
|
/// `network`.
|
||||||
|
///
|
||||||
|
/// The `transaction_version` might be reduced if it is not supported by the network upgrade active
|
||||||
|
/// at the `block_height` of the specified `network`.
|
||||||
|
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 => 5,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sanitized_version = transaction_version.min(max_version);
|
||||||
|
|
||||||
|
(sanitized_version, network_upgrade)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create multiple mock transparent transfers.
|
||||||
|
///
|
||||||
|
/// Creates one mock transparent transfer per item in the `relative_source_heights` vector. Each
|
||||||
|
/// item represents a relative scale (in the range `0.0..1.0`) representing the scale to obtain a
|
||||||
|
/// block height between the genesis block and the specified `block_height`. Each block height is
|
||||||
|
/// then used as the height for the source of the UTXO that will be spent by the transfer.
|
||||||
|
///
|
||||||
|
/// The function returns a list of inputs and outputs to be included in a mock transaction, as well
|
||||||
|
/// as a [`HashMap`] of source UTXOs to be sent to the transaction verifier.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This will panic if there are more than [`u32::MAX`] items in `relative_source_heights`. Ideally
|
||||||
|
/// the tests should use a number of items at most [`MAX_TRANSPARENT_INPUTS`].
|
||||||
|
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);
|
||||||
|
|
||||||
|
inputs.push(input);
|
||||||
|
outputs.push(output);
|
||||||
|
known_utxos.extend(new_utxos);
|
||||||
|
}
|
||||||
|
|
||||||
|
(inputs, outputs, known_utxos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects a [`block::Height`] between `min_height` and `max_height` using the `scale` factor.
|
||||||
|
///
|
||||||
|
/// The `scale` must be in the range `0.0..1.0`, where `0.0` results in the selection of
|
||||||
|
/// `min_height` and `1.0` would select the `max_height` if the range was inclusive. The range is
|
||||||
|
/// exclusive however, so `max_height` is never selected (unless it is equal to `min_height`).
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// - if `scale` is not in the range `0.0..1.0`
|
||||||
|
/// - if `min_height` is greater than `max_height`
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a `transaction` using a [`transaction::Verifier`] and return the result.
|
||||||
|
///
|
||||||
|
/// Configures an asynchronous runtime to run the verifier, sets it up and then uses it verify a
|
||||||
|
/// `transaction` using the provided parameters.
|
||||||
|
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::RUNTIME.block_on(async {
|
||||||
|
// Initialize the verifier
|
||||||
|
let state_service =
|
||||||
|
tower::service_fn(|_| async { unreachable!("State service should not be called") });
|
||||||
|
let script_verifier = script::Verifier::new(state_service);
|
||||||
|
let verifier = transaction::Verifier::new(network, script_verifier);
|
||||||
|
|
||||||
|
// Test the transaction verifier
|
||||||
|
verifier
|
||||||
|
.clone()
|
||||||
|
.oneshot(transaction::Request::Block {
|
||||||
|
transaction: Arc::new(transaction),
|
||||||
|
known_utxos: Arc::new(known_utxos),
|
||||||
|
height,
|
||||||
|
time: block_time,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue