2021-06-24 17:47:39 -07:00
|
|
|
use std::{collections::HashMap, convert::TryFrom, convert::TryInto, sync::Arc};
|
2021-06-14 17:15:59 -07:00
|
|
|
|
|
|
|
use tower::{service_fn, ServiceExt};
|
|
|
|
|
2021-04-27 17:43:00 -07:00
|
|
|
use zebra_chain::{
|
2021-07-01 16:03:34 -07:00
|
|
|
amount::{Amount, NonNegative},
|
2021-06-22 18:54:00 -07:00
|
|
|
block, orchard,
|
2021-06-14 17:15:59 -07:00
|
|
|
parameters::{Network, NetworkUpgrade},
|
2021-06-24 17:47:39 -07:00
|
|
|
primitives::{ed25519, x25519, Groth16Proof},
|
|
|
|
serialization::ZcashDeserialize,
|
|
|
|
sprout,
|
2021-06-06 19:02:18 -07:00
|
|
|
transaction::{
|
2021-06-30 19:17:37 -07:00
|
|
|
arbitrary::{
|
|
|
|
fake_v5_transactions_for_network, insert_fake_orchard_shielded_data, test_transactions,
|
|
|
|
},
|
2021-06-24 17:47:39 -07:00
|
|
|
Hash, HashType, JoinSplitData, LockTime, Transaction,
|
2021-06-06 19:02:18 -07:00
|
|
|
},
|
2021-06-28 17:49:40 -07:00
|
|
|
transparent::{self, CoinbaseData},
|
2021-04-27 17:43:00 -07:00
|
|
|
};
|
|
|
|
|
2021-06-14 17:15:59 -07:00
|
|
|
use super::{check, Request, Verifier};
|
2021-06-01 18:32:52 -07:00
|
|
|
|
2021-06-14 17:15:59 -07:00
|
|
|
use crate::{error::TransactionError, script};
|
2021-04-27 17:43:00 -07:00
|
|
|
use color_eyre::eyre::Report;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn v5_fake_transactions() -> Result<(), Report> {
|
|
|
|
zebra_test::init();
|
|
|
|
|
2021-06-01 18:32:52 -07:00
|
|
|
let networks = vec![
|
|
|
|
(Network::Mainnet, zebra_test::vectors::MAINNET_BLOCKS.iter()),
|
|
|
|
(Network::Testnet, zebra_test::vectors::TESTNET_BLOCKS.iter()),
|
|
|
|
];
|
2021-04-27 17:43:00 -07:00
|
|
|
|
2021-06-01 18:32:52 -07:00
|
|
|
for (network, blocks) in networks {
|
|
|
|
for transaction in fake_v5_transactions_for_network(network, blocks) {
|
|
|
|
match check::has_inputs_and_outputs(&transaction) {
|
2021-04-27 17:43:00 -07:00
|
|
|
Ok(()) => (),
|
2021-06-01 18:32:52 -07:00
|
|
|
Err(TransactionError::NoInputs) | Err(TransactionError::NoOutputs) => (),
|
|
|
|
Err(_) => panic!("error must be NoInputs or NoOutputs"),
|
2021-04-27 17:43:00 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
// make sure there are no joinsplits nor spends in coinbase
|
2021-06-01 18:32:52 -07:00
|
|
|
check::coinbase_tx_no_prevout_joinsplit_spend(&transaction)?;
|
2021-04-27 17:43:00 -07:00
|
|
|
|
|
|
|
// validate the sapling shielded data
|
2021-08-19 05:44:38 -07:00
|
|
|
if transaction.version() == 5 {
|
|
|
|
for spend in transaction.sapling_spends_per_anchor() {
|
|
|
|
check::spend_cv_rk_not_small_order(&spend)?;
|
2021-04-27 17:43:00 -07:00
|
|
|
}
|
2021-08-19 05:44:38 -07:00
|
|
|
for output in transaction.sapling_outputs() {
|
|
|
|
check::output_cv_epk_not_small_order(output)?;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
panic!("we should have no tx other than 5");
|
2021-04-27 17:43:00 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-01 18:32:52 -07:00
|
|
|
|
2021-04-27 17:43:00 -07:00
|
|
|
Ok(())
|
|
|
|
}
|
2021-06-01 18:32:52 -07:00
|
|
|
|
2021-06-06 19:02:18 -07:00
|
|
|
#[test]
|
|
|
|
fn fake_v5_transaction_with_orchard_actions_has_inputs_and_outputs() {
|
|
|
|
// Find a transaction with no inputs or outputs to use as base
|
|
|
|
let mut transaction = fake_v5_transactions_for_network(
|
|
|
|
Network::Mainnet,
|
|
|
|
zebra_test::vectors::MAINNET_BLOCKS.iter(),
|
|
|
|
)
|
|
|
|
.rev()
|
|
|
|
.find(|transaction| {
|
|
|
|
transaction.inputs().is_empty()
|
|
|
|
&& transaction.outputs().is_empty()
|
|
|
|
&& transaction.sapling_spends_per_anchor().next().is_none()
|
|
|
|
&& transaction.sapling_outputs().next().is_none()
|
|
|
|
&& transaction.joinsplit_count() == 0
|
|
|
|
})
|
|
|
|
.expect("At least one fake V5 transaction with no inputs and no outputs");
|
|
|
|
|
|
|
|
// Insert fake Orchard shielded data to the transaction, which has at least one action (this is
|
|
|
|
// guaranteed structurally by `orchard::ShieldedData`)
|
|
|
|
insert_fake_orchard_shielded_data(&mut transaction);
|
|
|
|
|
2021-06-28 15:28:49 -07:00
|
|
|
// The check will fail if the transaction has no flags
|
|
|
|
assert_eq!(
|
|
|
|
check::has_inputs_and_outputs(&transaction),
|
|
|
|
Err(TransactionError::NoInputs)
|
|
|
|
);
|
|
|
|
|
|
|
|
// If we add ENABLE_SPENDS flag it will pass the inputs check but fails with the outputs
|
|
|
|
// TODO: Avoid new calls to `insert_fake_orchard_shielded_data` for each check #2409.
|
|
|
|
let shielded_data = insert_fake_orchard_shielded_data(&mut transaction);
|
|
|
|
shielded_data.flags = orchard::Flags::ENABLE_SPENDS;
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
check::has_inputs_and_outputs(&transaction),
|
|
|
|
Err(TransactionError::NoOutputs)
|
|
|
|
);
|
|
|
|
|
|
|
|
// If we add ENABLE_OUTPUTS flag it will pass the outputs check but fails with the inputs
|
|
|
|
let shielded_data = insert_fake_orchard_shielded_data(&mut transaction);
|
|
|
|
shielded_data.flags = orchard::Flags::ENABLE_OUTPUTS;
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
check::has_inputs_and_outputs(&transaction),
|
|
|
|
Err(TransactionError::NoInputs)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Finally make it valid by adding both required flags
|
|
|
|
let shielded_data = insert_fake_orchard_shielded_data(&mut transaction);
|
|
|
|
shielded_data.flags = orchard::Flags::ENABLE_SPENDS | orchard::Flags::ENABLE_OUTPUTS;
|
|
|
|
|
2021-06-06 19:02:18 -07:00
|
|
|
assert!(check::has_inputs_and_outputs(&transaction).is_ok());
|
|
|
|
}
|
|
|
|
|
2021-06-01 18:32:52 -07:00
|
|
|
#[test]
|
|
|
|
fn v5_transaction_with_no_inputs_fails_validation() {
|
|
|
|
let transaction = fake_v5_transactions_for_network(
|
|
|
|
Network::Mainnet,
|
|
|
|
zebra_test::vectors::MAINNET_BLOCKS.iter(),
|
|
|
|
)
|
|
|
|
.rev()
|
|
|
|
.find(|transaction| {
|
|
|
|
transaction.inputs().is_empty()
|
|
|
|
&& transaction.sapling_spends_per_anchor().next().is_none()
|
|
|
|
&& transaction.orchard_actions().next().is_none()
|
|
|
|
&& transaction.joinsplit_count() == 0
|
|
|
|
&& (!transaction.outputs().is_empty() || transaction.sapling_outputs().next().is_some())
|
|
|
|
})
|
|
|
|
.expect("At least one fake v5 transaction with no inputs in the test vectors");
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
check::has_inputs_and_outputs(&transaction),
|
|
|
|
Err(TransactionError::NoInputs)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn v5_transaction_with_no_outputs_fails_validation() {
|
|
|
|
let transaction = fake_v5_transactions_for_network(
|
|
|
|
Network::Mainnet,
|
|
|
|
zebra_test::vectors::MAINNET_BLOCKS.iter(),
|
|
|
|
)
|
|
|
|
.rev()
|
|
|
|
.find(|transaction| {
|
|
|
|
transaction.outputs().is_empty()
|
|
|
|
&& transaction.sapling_outputs().next().is_none()
|
|
|
|
&& transaction.orchard_actions().next().is_none()
|
|
|
|
&& transaction.joinsplit_count() == 0
|
|
|
|
&& (!transaction.inputs().is_empty()
|
|
|
|
|| transaction.sapling_spends_per_anchor().next().is_some())
|
|
|
|
})
|
|
|
|
.expect("At least one fake v5 transaction with no outputs in the test vectors");
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
check::has_inputs_and_outputs(&transaction),
|
|
|
|
Err(TransactionError::NoOutputs)
|
|
|
|
);
|
|
|
|
}
|
2021-06-06 19:02:18 -07:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn v5_coinbase_transaction_without_enable_spends_flag_passes_validation() {
|
|
|
|
let mut transaction = fake_v5_transactions_for_network(
|
|
|
|
Network::Mainnet,
|
|
|
|
zebra_test::vectors::MAINNET_BLOCKS.iter(),
|
|
|
|
)
|
|
|
|
.rev()
|
2021-09-01 17:06:20 -07:00
|
|
|
.find(|transaction| transaction.has_valid_coinbase_transaction_inputs())
|
2021-06-06 19:02:18 -07:00
|
|
|
.expect("At least one fake V5 coinbase transaction in the test vectors");
|
|
|
|
|
|
|
|
insert_fake_orchard_shielded_data(&mut transaction);
|
|
|
|
|
|
|
|
assert!(check::coinbase_tx_no_prevout_joinsplit_spend(&transaction).is_ok(),);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn v5_coinbase_transaction_with_enable_spends_flag_fails_validation() {
|
|
|
|
let mut transaction = fake_v5_transactions_for_network(
|
|
|
|
Network::Mainnet,
|
|
|
|
zebra_test::vectors::MAINNET_BLOCKS.iter(),
|
|
|
|
)
|
|
|
|
.rev()
|
2021-09-01 17:06:20 -07:00
|
|
|
.find(|transaction| transaction.has_valid_coinbase_transaction_inputs())
|
2021-06-06 19:02:18 -07:00
|
|
|
.expect("At least one fake V5 coinbase transaction in the test vectors");
|
|
|
|
|
|
|
|
let shielded_data = insert_fake_orchard_shielded_data(&mut transaction);
|
|
|
|
|
|
|
|
shielded_data.flags = orchard::Flags::ENABLE_SPENDS;
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
check::coinbase_tx_no_prevout_joinsplit_spend(&transaction),
|
|
|
|
Err(TransactionError::CoinbaseHasEnableSpendsOrchard)
|
|
|
|
);
|
|
|
|
}
|
2021-06-14 17:15:59 -07:00
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn v5_transaction_is_rejected_before_nu5_activation() {
|
|
|
|
const V5_TRANSACTION_VERSION: u32 = 5;
|
|
|
|
|
|
|
|
let canopy = NetworkUpgrade::Canopy;
|
|
|
|
let networks = vec![
|
|
|
|
(Network::Mainnet, zebra_test::vectors::MAINNET_BLOCKS.iter()),
|
|
|
|
(Network::Testnet, zebra_test::vectors::TESTNET_BLOCKS.iter()),
|
|
|
|
];
|
|
|
|
|
|
|
|
for (network, blocks) in networks {
|
|
|
|
let state_service = service_fn(|_| async { unreachable!("Service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
let transaction = fake_v5_transactions_for_network(network, blocks)
|
|
|
|
.rev()
|
|
|
|
.next()
|
|
|
|
.expect("At least one fake V5 transaction in the test vectors");
|
|
|
|
|
|
|
|
let result = verifier
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(HashMap::new()),
|
|
|
|
height: canopy
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("Canopy activation height is specified"),
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
result,
|
|
|
|
Err(TransactionError::UnsupportedByNetworkUpgrade(
|
|
|
|
V5_TRANSACTION_VERSION,
|
|
|
|
canopy
|
|
|
|
))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
2021-10-05 18:08:41 -07:00
|
|
|
// TODO: Remove `should_panic` once the NU5 activation height for mainnet has been
|
2021-06-14 17:15:59 -07:00
|
|
|
// defined.
|
|
|
|
#[should_panic]
|
2021-10-05 18:08:41 -07:00
|
|
|
async fn v5_transaction_is_accepted_after_nu5_activation_mainnet() {
|
|
|
|
v5_transaction_is_accepted_after_nu5_activation_for_network(Network::Mainnet).await
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn v5_transaction_is_accepted_after_nu5_activation_testnet() {
|
|
|
|
v5_transaction_is_accepted_after_nu5_activation_for_network(Network::Testnet).await
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn v5_transaction_is_accepted_after_nu5_activation_for_network(network: Network) {
|
2021-06-14 17:15:59 -07:00
|
|
|
let nu5 = NetworkUpgrade::Nu5;
|
2021-10-05 18:08:41 -07:00
|
|
|
let blocks = match network {
|
|
|
|
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
|
|
|
|
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
|
|
|
|
};
|
2021-06-14 17:15:59 -07:00
|
|
|
|
2021-10-05 18:08:41 -07:00
|
|
|
let state_service = service_fn(|_| async { unreachable!("Service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
2021-06-14 17:15:59 -07:00
|
|
|
|
2021-10-05 18:08:41 -07:00
|
|
|
let transaction = fake_v5_transactions_for_network(network, blocks)
|
|
|
|
.rev()
|
|
|
|
.next()
|
|
|
|
.expect("At least one fake V5 transaction in the test vectors");
|
2021-06-14 17:15:59 -07:00
|
|
|
|
2021-10-05 18:08:41 -07:00
|
|
|
let expected_hash = transaction.unmined_id();
|
2021-06-14 17:15:59 -07:00
|
|
|
|
2021-10-05 18:08:41 -07:00
|
|
|
let result = verifier
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(HashMap::new()),
|
|
|
|
height: nu5
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("NU5 activation height is specified"),
|
|
|
|
})
|
|
|
|
.await;
|
2021-06-14 17:15:59 -07:00
|
|
|
|
2021-10-14 14:15:10 -07:00
|
|
|
assert_eq!(
|
|
|
|
result.expect("expected a tx_id and tx_fee").0,
|
|
|
|
expected_hash
|
|
|
|
);
|
2021-06-14 17:15:59 -07:00
|
|
|
}
|
2021-06-22 18:54:00 -07:00
|
|
|
|
|
|
|
/// Test if V4 transaction with transparent funds is accepted.
|
|
|
|
#[tokio::test]
|
|
|
|
async fn v4_transaction_with_transparent_transfer_is_accepted() {
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
|
|
|
|
let canopy_activation_height = NetworkUpgrade::Canopy
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("Canopy activation height is specified");
|
|
|
|
|
|
|
|
let transaction_block_height =
|
|
|
|
(canopy_activation_height + 10).expect("transaction block height is too large");
|
|
|
|
|
|
|
|
let fake_source_fund_height =
|
|
|
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
|
|
|
|
|
|
|
// Create a fake transparent transfer that should succeed
|
|
|
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true);
|
|
|
|
|
|
|
|
// Create a V4 transaction
|
|
|
|
let transaction = Transaction::V4 {
|
|
|
|
inputs: vec![input],
|
|
|
|
outputs: vec![output],
|
|
|
|
lock_time: LockTime::Height(block::Height(0)),
|
|
|
|
expiry_height: (transaction_block_height + 1).expect("expiry height is too large"),
|
|
|
|
joinsplit_data: None,
|
|
|
|
sapling_shielded_data: None,
|
|
|
|
};
|
|
|
|
|
2021-08-25 14:02:47 -07:00
|
|
|
let transaction_hash = transaction.unmined_id();
|
2021-06-22 18:54:00 -07:00
|
|
|
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
let result = verifier
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(known_utxos),
|
|
|
|
height: transaction_block_height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2021-10-14 14:15:10 -07:00
|
|
|
assert_eq!(
|
|
|
|
result.expect("expected a tx_id and tx_fee").0,
|
|
|
|
transaction_hash
|
|
|
|
);
|
2021-06-22 18:54:00 -07:00
|
|
|
}
|
|
|
|
|
2021-06-28 17:49:40 -07:00
|
|
|
/// Test if V4 coinbase transaction is accepted.
|
|
|
|
#[tokio::test]
|
|
|
|
async fn v4_coinbase_transaction_is_accepted() {
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
|
|
|
|
let canopy_activation_height = NetworkUpgrade::Canopy
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("Canopy activation height is specified");
|
|
|
|
|
|
|
|
let transaction_block_height =
|
|
|
|
(canopy_activation_height + 10).expect("transaction block height is too large");
|
|
|
|
|
|
|
|
// Create a fake transparent coinbase that should succeed
|
|
|
|
let (input, output) = mock_coinbase_transparent_output(transaction_block_height);
|
|
|
|
|
|
|
|
// Create a V4 coinbase transaction
|
|
|
|
let transaction = Transaction::V4 {
|
|
|
|
inputs: vec![input],
|
|
|
|
outputs: vec![output],
|
|
|
|
lock_time: LockTime::Height(block::Height(0)),
|
|
|
|
expiry_height: transaction_block_height,
|
|
|
|
joinsplit_data: None,
|
|
|
|
sapling_shielded_data: None,
|
|
|
|
};
|
|
|
|
|
2021-08-25 14:02:47 -07:00
|
|
|
let transaction_hash = transaction.unmined_id();
|
2021-06-28 17:49:40 -07:00
|
|
|
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
let result = verifier
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(HashMap::new()),
|
|
|
|
height: transaction_block_height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2021-10-14 14:15:10 -07:00
|
|
|
assert_eq!(
|
|
|
|
result.expect("expected a tx_id and tx_fee").0,
|
|
|
|
transaction_hash
|
|
|
|
);
|
2021-06-28 17:49:40 -07:00
|
|
|
}
|
|
|
|
|
2021-06-22 18:54:00 -07:00
|
|
|
/// Test if V4 transaction with transparent funds is rejected if the source script prevents it.
|
|
|
|
///
|
|
|
|
/// This test simulates the case where the script verifier rejects the transaction because the
|
|
|
|
/// script prevents spending the source UTXO.
|
|
|
|
#[tokio::test]
|
|
|
|
async fn v4_transaction_with_transparent_transfer_is_rejected_by_the_script() {
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
|
|
|
|
let canopy_activation_height = NetworkUpgrade::Canopy
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("Canopy activation height is specified");
|
|
|
|
|
|
|
|
let transaction_block_height =
|
|
|
|
(canopy_activation_height + 10).expect("transaction block height is too large");
|
|
|
|
|
|
|
|
let fake_source_fund_height =
|
|
|
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
|
|
|
|
|
|
|
// Create a fake transparent transfer that should not succeed
|
|
|
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, false);
|
|
|
|
|
|
|
|
// Create a V4 transaction
|
|
|
|
let transaction = Transaction::V4 {
|
|
|
|
inputs: vec![input],
|
|
|
|
outputs: vec![output],
|
|
|
|
lock_time: LockTime::Height(block::Height(0)),
|
|
|
|
expiry_height: (transaction_block_height + 1).expect("expiry height is too large"),
|
|
|
|
joinsplit_data: None,
|
|
|
|
sapling_shielded_data: None,
|
|
|
|
};
|
|
|
|
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
let result = verifier
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(known_utxos),
|
|
|
|
height: transaction_block_height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
result,
|
|
|
|
Err(TransactionError::InternalDowncastError(
|
2021-08-25 08:07:26 -07:00
|
|
|
"downcast to known transaction error type failed, original error: ScriptInvalid"
|
|
|
|
.to_string()
|
2021-06-22 18:54:00 -07:00
|
|
|
))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test if V5 transaction with transparent funds is accepted.
|
|
|
|
#[tokio::test]
|
|
|
|
async fn v5_transaction_with_transparent_transfer_is_accepted() {
|
2021-06-28 17:49:40 -07:00
|
|
|
let network = Network::Testnet;
|
2021-06-22 18:54:00 -07:00
|
|
|
let network_upgrade = NetworkUpgrade::Nu5;
|
|
|
|
|
|
|
|
let nu5_activation_height = network_upgrade
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("NU5 activation height is specified");
|
|
|
|
|
|
|
|
let transaction_block_height =
|
|
|
|
(nu5_activation_height + 10).expect("transaction block height is too large");
|
|
|
|
|
|
|
|
let fake_source_fund_height =
|
|
|
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
|
|
|
|
|
|
|
// Create a fake transparent transfer that should succeed
|
|
|
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true);
|
|
|
|
|
|
|
|
// Create a V5 transaction
|
|
|
|
let transaction = Transaction::V5 {
|
|
|
|
inputs: vec![input],
|
|
|
|
outputs: vec![output],
|
|
|
|
lock_time: LockTime::Height(block::Height(0)),
|
|
|
|
expiry_height: (transaction_block_height + 1).expect("expiry height is too large"),
|
|
|
|
sapling_shielded_data: None,
|
|
|
|
orchard_shielded_data: None,
|
|
|
|
network_upgrade,
|
|
|
|
};
|
|
|
|
|
2021-08-25 14:02:47 -07:00
|
|
|
let transaction_hash = transaction.unmined_id();
|
2021-06-22 18:54:00 -07:00
|
|
|
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
let result = verifier
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(known_utxos),
|
|
|
|
height: transaction_block_height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2021-10-14 14:15:10 -07:00
|
|
|
assert_eq!(
|
|
|
|
result.expect("expected a tx_id and tx_fee").0,
|
|
|
|
transaction_hash
|
|
|
|
);
|
2021-06-22 18:54:00 -07:00
|
|
|
}
|
|
|
|
|
2021-06-28 17:49:40 -07:00
|
|
|
/// Test if V5 coinbase transaction is accepted.
|
|
|
|
#[tokio::test]
|
|
|
|
async fn v5_coinbase_transaction_is_accepted() {
|
|
|
|
let network = Network::Testnet;
|
|
|
|
let network_upgrade = NetworkUpgrade::Nu5;
|
|
|
|
|
|
|
|
let nu5_activation_height = network_upgrade
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("NU5 activation height is specified");
|
|
|
|
|
|
|
|
let transaction_block_height =
|
|
|
|
(nu5_activation_height + 10).expect("transaction block height is too large");
|
|
|
|
|
|
|
|
// Create a fake transparent coinbase that should succeed
|
|
|
|
let (input, output) = mock_coinbase_transparent_output(transaction_block_height);
|
|
|
|
let known_utxos = HashMap::new();
|
|
|
|
|
|
|
|
// Create a V5 coinbase transaction
|
|
|
|
let transaction = Transaction::V5 {
|
|
|
|
network_upgrade,
|
|
|
|
inputs: vec![input],
|
|
|
|
outputs: vec![output],
|
|
|
|
lock_time: LockTime::Height(block::Height(0)),
|
|
|
|
expiry_height: transaction_block_height,
|
|
|
|
sapling_shielded_data: None,
|
|
|
|
orchard_shielded_data: None,
|
|
|
|
};
|
|
|
|
|
2021-08-25 14:02:47 -07:00
|
|
|
let transaction_hash = transaction.unmined_id();
|
2021-06-28 17:49:40 -07:00
|
|
|
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
let result = verifier
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(known_utxos),
|
|
|
|
height: transaction_block_height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2021-10-14 14:15:10 -07:00
|
|
|
assert_eq!(
|
|
|
|
result.expect("expected a tx_id and tx_fee").0,
|
|
|
|
transaction_hash
|
|
|
|
);
|
2021-06-28 17:49:40 -07:00
|
|
|
}
|
|
|
|
|
2021-06-22 18:54:00 -07:00
|
|
|
/// Test if V5 transaction with transparent funds is rejected if the source script prevents it.
|
|
|
|
///
|
|
|
|
/// This test simulates the case where the script verifier rejects the transaction because the
|
|
|
|
/// script prevents spending the source UTXO.
|
|
|
|
#[tokio::test]
|
|
|
|
async fn v5_transaction_with_transparent_transfer_is_rejected_by_the_script() {
|
2021-06-28 17:49:40 -07:00
|
|
|
let network = Network::Testnet;
|
2021-06-22 18:54:00 -07:00
|
|
|
let network_upgrade = NetworkUpgrade::Nu5;
|
|
|
|
|
|
|
|
let nu5_activation_height = network_upgrade
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("NU5 activation height is specified");
|
|
|
|
|
|
|
|
let transaction_block_height =
|
|
|
|
(nu5_activation_height + 10).expect("transaction block height is too large");
|
|
|
|
|
|
|
|
let fake_source_fund_height =
|
|
|
|
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
|
|
|
|
|
|
|
// Create a fake transparent transfer that should not succeed
|
|
|
|
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, false);
|
|
|
|
|
|
|
|
// Create a V5 transaction
|
|
|
|
let transaction = Transaction::V5 {
|
|
|
|
inputs: vec![input],
|
|
|
|
outputs: vec![output],
|
|
|
|
lock_time: LockTime::Height(block::Height(0)),
|
|
|
|
expiry_height: (transaction_block_height + 1).expect("expiry height is too large"),
|
|
|
|
sapling_shielded_data: None,
|
|
|
|
orchard_shielded_data: None,
|
|
|
|
network_upgrade,
|
|
|
|
};
|
|
|
|
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
let result = verifier
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(known_utxos),
|
|
|
|
height: transaction_block_height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
result,
|
|
|
|
Err(TransactionError::InternalDowncastError(
|
2021-10-05 18:08:41 -07:00
|
|
|
"downcast to known transaction error type failed, original error: ScriptInvalid"
|
|
|
|
.to_string()
|
2021-06-22 18:54:00 -07:00
|
|
|
))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-06-30 17:30:31 -07:00
|
|
|
/// Test if signed V4 transaction with a dummy [`sprout::JoinSplit`] is accepted.
|
2021-06-24 17:47:39 -07:00
|
|
|
/// - Test if an unsigned V4 transaction with a dummy [`sprout::JoinSplit`] is rejected.
|
|
|
|
///
|
2021-06-30 17:30:31 -07:00
|
|
|
/// This test verifies if the transaction verifier correctly accepts a signed transaction.
|
|
|
|
#[test]
|
|
|
|
fn v4_with_signed_sprout_transfer_is_accepted() {
|
|
|
|
zebra_test::init();
|
|
|
|
zebra_test::RUNTIME.block_on(async {
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
let network_upgrade = NetworkUpgrade::Canopy;
|
2021-06-24 17:47:39 -07:00
|
|
|
|
2021-06-30 17:30:31 -07:00
|
|
|
let canopy_activation_height = network_upgrade
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("Canopy activation height is not set");
|
2021-06-24 17:47:39 -07:00
|
|
|
|
2021-06-30 17:30:31 -07:00
|
|
|
let transaction_block_height =
|
|
|
|
(canopy_activation_height + 10).expect("Canopy activation height is too large");
|
2021-06-24 17:47:39 -07:00
|
|
|
|
2021-06-30 17:30:31 -07:00
|
|
|
// Initialize the verifier
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
2021-06-24 17:47:39 -07:00
|
|
|
|
|
|
|
// Create a fake Sprout join split
|
|
|
|
let (joinsplit_data, signing_key) = mock_sprout_join_split_data();
|
|
|
|
|
|
|
|
let mut transaction = Transaction::V4 {
|
|
|
|
inputs: vec![],
|
|
|
|
outputs: vec![],
|
|
|
|
lock_time: LockTime::Height(block::Height(0)),
|
|
|
|
expiry_height: (transaction_block_height + 1).expect("expiry height is too large"),
|
|
|
|
joinsplit_data: Some(joinsplit_data),
|
|
|
|
sapling_shielded_data: None,
|
|
|
|
};
|
|
|
|
|
2021-06-30 17:30:31 -07:00
|
|
|
// Sign the transaction
|
|
|
|
let sighash = transaction.sighash(network_upgrade, HashType::ALL, None);
|
2021-06-24 17:47:39 -07:00
|
|
|
|
2021-06-30 17:30:31 -07:00
|
|
|
match &mut transaction {
|
|
|
|
Transaction::V4 {
|
|
|
|
joinsplit_data: Some(joinsplit_data),
|
|
|
|
..
|
2021-07-06 15:27:10 -07:00
|
|
|
} => joinsplit_data.sig = signing_key.sign(sighash.as_ref()),
|
2021-06-30 17:30:31 -07:00
|
|
|
_ => unreachable!("Mock transaction was created incorrectly"),
|
|
|
|
}
|
2021-06-24 17:47:39 -07:00
|
|
|
|
2021-08-25 14:02:47 -07:00
|
|
|
let expected_hash = transaction.unmined_id();
|
2021-06-30 17:30:31 -07:00
|
|
|
|
|
|
|
// Test the transaction verifier
|
|
|
|
let result = verifier
|
|
|
|
.clone()
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(HashMap::new()),
|
|
|
|
height: transaction_block_height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2021-10-14 14:15:10 -07:00
|
|
|
assert_eq!(
|
|
|
|
result.expect("expected a tx_id and tx_fee").0,
|
|
|
|
expected_hash
|
|
|
|
);
|
2021-06-30 17:30:31 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test if an unsigned V4 transaction with a dummy [`sprout::JoinSplit`] is rejected.
|
|
|
|
///
|
|
|
|
/// This test verifies if the transaction verifier correctly rejects the transaction because of the
|
|
|
|
/// invalid signature.
|
|
|
|
#[test]
|
|
|
|
fn v4_with_unsigned_sprout_transfer_is_rejected() {
|
|
|
|
zebra_test::init();
|
|
|
|
zebra_test::RUNTIME.block_on(async {
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
let network_upgrade = NetworkUpgrade::Canopy;
|
|
|
|
|
|
|
|
let canopy_activation_height = network_upgrade
|
|
|
|
.activation_height(network)
|
|
|
|
.expect("Canopy activation height is not set");
|
|
|
|
|
|
|
|
let transaction_block_height =
|
|
|
|
(canopy_activation_height + 10).expect("Canopy activation height is too large");
|
|
|
|
|
|
|
|
// Initialize the verifier
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
// Create a fake Sprout join split
|
|
|
|
let (joinsplit_data, _) = mock_sprout_join_split_data();
|
|
|
|
|
|
|
|
let transaction = Transaction::V4 {
|
|
|
|
inputs: vec![],
|
|
|
|
outputs: vec![],
|
|
|
|
lock_time: LockTime::Height(block::Height(0)),
|
|
|
|
expiry_height: (transaction_block_height + 1).expect("expiry height is too large"),
|
|
|
|
joinsplit_data: Some(joinsplit_data),
|
|
|
|
sapling_shielded_data: None,
|
2021-06-24 17:47:39 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
// Test the transaction verifier
|
|
|
|
let result = verifier
|
|
|
|
.clone()
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(HashMap::new()),
|
|
|
|
height: transaction_block_height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2021-06-30 17:30:31 -07:00
|
|
|
assert_eq!(
|
|
|
|
result,
|
|
|
|
Err(
|
|
|
|
// TODO: Fix error downcast
|
|
|
|
// Err(TransactionError::Ed25519(ed25519::Error::InvalidSignature))
|
|
|
|
TransactionError::InternalDowncastError(
|
2021-08-25 08:07:26 -07:00
|
|
|
"downcast to known transaction error type failed, original error: InvalidSignature"
|
2021-06-30 17:30:31 -07:00
|
|
|
.to_string(),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
});
|
2021-06-24 17:47:39 -07:00
|
|
|
}
|
|
|
|
|
2021-06-30 19:17:37 -07:00
|
|
|
/// Test if a V4 transaction with Sapling spends is accepted by the verifier.
|
|
|
|
#[test]
|
|
|
|
fn v4_with_sapling_spends() {
|
|
|
|
zebra_test::init();
|
|
|
|
zebra_test::RUNTIME.block_on(async {
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
|
|
|
|
let (height, transaction) = test_transactions(network)
|
|
|
|
.rev()
|
|
|
|
.filter(|(_, transaction)| {
|
2021-09-01 17:06:20 -07:00
|
|
|
!transaction.has_valid_coinbase_transaction_inputs()
|
|
|
|
&& transaction.inputs().is_empty()
|
2021-06-30 19:17:37 -07:00
|
|
|
})
|
|
|
|
.find(|(_, transaction)| transaction.sapling_spends_per_anchor().next().is_some())
|
|
|
|
.expect("No transaction found with Sapling spends");
|
|
|
|
|
2021-08-25 14:02:47 -07:00
|
|
|
let expected_hash = transaction.unmined_id();
|
2021-06-30 19:17:37 -07:00
|
|
|
|
|
|
|
// Initialize the verifier
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
// Test the transaction verifier
|
|
|
|
let result = verifier
|
|
|
|
.clone()
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction,
|
|
|
|
known_utxos: Arc::new(HashMap::new()),
|
|
|
|
height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2021-10-14 14:15:10 -07:00
|
|
|
assert_eq!(
|
|
|
|
result.expect("expected a tx_id and tx_fee").0,
|
|
|
|
expected_hash
|
|
|
|
);
|
2021-06-30 19:17:37 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test if a V4 transaction with Sapling outputs but no spends is accepted by the verifier.
|
|
|
|
#[test]
|
|
|
|
fn v4_with_sapling_outputs_and_no_spends() {
|
|
|
|
zebra_test::init();
|
|
|
|
zebra_test::RUNTIME.block_on(async {
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
|
|
|
|
let (height, transaction) = test_transactions(network)
|
|
|
|
.rev()
|
|
|
|
.filter(|(_, transaction)| {
|
2021-09-01 17:06:20 -07:00
|
|
|
!transaction.has_valid_coinbase_transaction_inputs()
|
|
|
|
&& transaction.inputs().is_empty()
|
2021-06-30 19:17:37 -07:00
|
|
|
})
|
|
|
|
.find(|(_, transaction)| {
|
|
|
|
transaction.sapling_spends_per_anchor().next().is_none()
|
|
|
|
&& transaction.sapling_outputs().next().is_some()
|
|
|
|
})
|
2021-07-02 09:48:53 -07:00
|
|
|
.expect("No transaction found with Sapling outputs and no Sapling spends");
|
2021-06-30 19:17:37 -07:00
|
|
|
|
2021-08-25 14:02:47 -07:00
|
|
|
let expected_hash = transaction.unmined_id();
|
2021-06-30 19:17:37 -07:00
|
|
|
|
|
|
|
// Initialize the verifier
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
// Test the transaction verifier
|
|
|
|
let result = verifier
|
|
|
|
.clone()
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction,
|
|
|
|
known_utxos: Arc::new(HashMap::new()),
|
|
|
|
height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2021-10-14 14:15:10 -07:00
|
|
|
assert_eq!(
|
|
|
|
result.expect("expected a tx_id and tx_fee").0,
|
|
|
|
expected_hash
|
|
|
|
);
|
2021-06-30 19:17:37 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-07-02 09:48:53 -07:00
|
|
|
/// Test if a V5 transaction with Sapling spends is accepted by the verifier.
|
|
|
|
#[test]
|
|
|
|
// TODO: Remove `should_panic` once V5 transaction verification is complete.
|
|
|
|
#[should_panic]
|
|
|
|
fn v5_with_sapling_spends() {
|
|
|
|
zebra_test::init();
|
|
|
|
zebra_test::RUNTIME.block_on(async {
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
|
|
|
|
let transaction =
|
|
|
|
fake_v5_transactions_for_network(network, zebra_test::vectors::MAINNET_BLOCKS.iter())
|
|
|
|
.rev()
|
2021-09-01 17:06:20 -07:00
|
|
|
.filter(|transaction| {
|
|
|
|
!transaction.has_valid_coinbase_transaction_inputs()
|
|
|
|
&& transaction.inputs().is_empty()
|
|
|
|
})
|
2021-07-02 09:48:53 -07:00
|
|
|
.find(|transaction| transaction.sapling_spends_per_anchor().next().is_some())
|
|
|
|
.expect("No transaction found with Sapling spends");
|
|
|
|
|
2021-08-25 14:02:47 -07:00
|
|
|
let expected_hash = transaction.unmined_id();
|
2021-07-02 09:48:53 -07:00
|
|
|
let height = transaction
|
|
|
|
.expiry_height()
|
|
|
|
.expect("Transaction is missing expiry height");
|
|
|
|
|
|
|
|
// Initialize the verifier
|
|
|
|
let state_service =
|
|
|
|
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
|
|
let script_verifier = script::Verifier::new(state_service);
|
|
|
|
let verifier = Verifier::new(network, script_verifier);
|
|
|
|
|
|
|
|
// Test the transaction verifier
|
|
|
|
let result = verifier
|
|
|
|
.clone()
|
|
|
|
.oneshot(Request::Block {
|
|
|
|
transaction: Arc::new(transaction),
|
|
|
|
known_utxos: Arc::new(HashMap::new()),
|
|
|
|
height,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2021-10-14 14:15:10 -07:00
|
|
|
assert_eq!(
|
|
|
|
result.expect("expected a tx_id and tx_fee").0,
|
|
|
|
expected_hash
|
|
|
|
);
|
2021-07-02 09:48:53 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-06-22 18:54:00 -07:00
|
|
|
// Utility functions
|
|
|
|
|
|
|
|
/// Create a mock transparent transfer to be included in a transaction.
|
|
|
|
///
|
|
|
|
/// First, this creates a fake unspent transaction output from a fake transaction included in the
|
|
|
|
/// specified `previous_utxo_height` block height. This fake [`Utxo`] also contains a simple script
|
|
|
|
/// that can either accept or reject any spend attempt, depending on if `script_should_succeed` is
|
2021-07-19 06:52:32 -07:00
|
|
|
/// `true` or `false`. Since the `tx_index_in_block` is irrelevant for blocks that have already
|
|
|
|
/// been verified, it is set to `1`.
|
2021-06-22 18:54:00 -07:00
|
|
|
///
|
2021-06-28 17:49:40 -07:00
|
|
|
/// Then, a [`transparent::Input::PrevOut`] is created that attempts to spend the previously created fake
|
2021-07-19 06:52:32 -07:00
|
|
|
/// UTXO to a new [`transparent::Output`].
|
2021-06-22 18:54:00 -07:00
|
|
|
///
|
|
|
|
/// Finally, the initial fake UTXO is placed in a `known_utxos` [`HashMap`] so that it can be
|
|
|
|
/// retrieved during verification.
|
|
|
|
///
|
|
|
|
/// The function then returns the generated transparent input and output, as well as the
|
|
|
|
/// `known_utxos` map.
|
2021-07-19 06:52:32 -07:00
|
|
|
///
|
|
|
|
/// Note: `known_utxos` is only intended to be used for UTXOs within the same block,
|
|
|
|
/// so future verification changes might break this mocking function.
|
2021-06-22 18:54:00 -07:00
|
|
|
fn mock_transparent_transfer(
|
|
|
|
previous_utxo_height: block::Height,
|
|
|
|
script_should_succeed: bool,
|
|
|
|
) -> (
|
|
|
|
transparent::Input,
|
|
|
|
transparent::Output,
|
2021-07-19 06:52:32 -07:00
|
|
|
HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
2021-06-22 18:54:00 -07:00
|
|
|
) {
|
|
|
|
// A script with a single opcode that accepts the transaction (pushes true on the stack)
|
|
|
|
let accepting_script = transparent::Script::new(&[1, 1]);
|
|
|
|
// A script with a single opcode that rejects the transaction (OP_FALSE)
|
|
|
|
let rejecting_script = transparent::Script::new(&[0]);
|
|
|
|
|
|
|
|
// Mock an unspent transaction output
|
|
|
|
let previous_outpoint = transparent::OutPoint {
|
|
|
|
hash: Hash([1u8; 32]),
|
|
|
|
index: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
let lock_script = if script_should_succeed {
|
|
|
|
accepting_script.clone()
|
|
|
|
} else {
|
|
|
|
rejecting_script.clone()
|
|
|
|
};
|
|
|
|
|
|
|
|
let previous_output = transparent::Output {
|
|
|
|
value: Amount::try_from(1).expect("1 is an invalid amount"),
|
|
|
|
lock_script,
|
|
|
|
};
|
|
|
|
|
2021-07-19 06:52:32 -07:00
|
|
|
let previous_utxo =
|
|
|
|
transparent::OrderedUtxo::new(previous_output, previous_utxo_height, false, 1);
|
2021-06-22 18:54:00 -07:00
|
|
|
|
|
|
|
// Use the `previous_outpoint` as input
|
|
|
|
let input = transparent::Input::PrevOut {
|
|
|
|
outpoint: previous_outpoint,
|
|
|
|
unlock_script: accepting_script,
|
|
|
|
sequence: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
// The output resulting from the transfer
|
|
|
|
// Using the rejecting script pretends the amount is burned because it can't be spent again
|
|
|
|
let output = transparent::Output {
|
|
|
|
value: Amount::try_from(1).expect("1 is an invalid amount"),
|
|
|
|
lock_script: rejecting_script,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Cache the source of the fund so that it can be used during verification
|
|
|
|
let mut known_utxos = HashMap::new();
|
|
|
|
known_utxos.insert(previous_outpoint, previous_utxo);
|
|
|
|
|
|
|
|
(input, output, known_utxos)
|
|
|
|
}
|
2021-06-24 17:47:39 -07:00
|
|
|
|
2021-06-28 17:49:40 -07:00
|
|
|
/// Create a mock coinbase input with a transparent output.
|
|
|
|
///
|
|
|
|
/// Create a [`transparent::Input::Coinbase`] at `coinbase_height`.
|
|
|
|
/// Then create UTXO with a [`transparent::Output`] spending some coinbase funds.
|
|
|
|
///
|
|
|
|
/// Returns the generated coinbase input and transparent output.
|
|
|
|
fn mock_coinbase_transparent_output(
|
|
|
|
coinbase_height: block::Height,
|
|
|
|
) -> (transparent::Input, transparent::Output) {
|
|
|
|
// A script with a single opcode that rejects the transaction (OP_FALSE)
|
|
|
|
let rejecting_script = transparent::Script::new(&[0]);
|
|
|
|
|
|
|
|
let input = transparent::Input::Coinbase {
|
|
|
|
height: coinbase_height,
|
|
|
|
data: CoinbaseData::new(Vec::new()),
|
|
|
|
sequence: u32::MAX,
|
|
|
|
};
|
|
|
|
|
|
|
|
// The output resulting from the transfer
|
|
|
|
// Using the rejecting script pretends the amount is burned because it can't be spent again
|
|
|
|
let output = transparent::Output {
|
|
|
|
value: Amount::try_from(1).expect("1 is an invalid amount"),
|
|
|
|
lock_script: rejecting_script,
|
|
|
|
};
|
|
|
|
|
|
|
|
(input, output)
|
|
|
|
}
|
|
|
|
|
2021-06-24 17:47:39 -07:00
|
|
|
/// Create a mock [`sprout::JoinSplit`] and include it in a [`transaction::JoinSplitData`].
|
|
|
|
///
|
|
|
|
/// This creates a dummy join split. By itself it is invalid, but it is useful for including in a
|
|
|
|
/// transaction to check the signatures.
|
|
|
|
///
|
|
|
|
/// The [`transaction::JoinSplitData`] with the dummy [`sprout::JoinSplit`] is returned together
|
|
|
|
/// with the [`ed25519::SigningKey`] that can be used to create a signature to later add to the
|
|
|
|
/// returned join split data.
|
|
|
|
fn mock_sprout_join_split_data() -> (JoinSplitData<Groth16Proof>, ed25519::SigningKey) {
|
|
|
|
// Prepare dummy inputs for the join split
|
|
|
|
let zero_amount = 0_i32
|
|
|
|
.try_into()
|
|
|
|
.expect("Invalid JoinSplit transparent input");
|
|
|
|
let anchor = sprout::tree::Root::default();
|
|
|
|
let nullifier = sprout::note::Nullifier([0u8; 32]);
|
|
|
|
let commitment = sprout::commitment::NoteCommitment::from([0u8; 32]);
|
|
|
|
let ephemeral_key =
|
|
|
|
x25519::PublicKey::from(&x25519::EphemeralSecret::new(rand07::thread_rng()));
|
|
|
|
let random_seed = [0u8; 32];
|
|
|
|
let mac = sprout::note::Mac::zcash_deserialize(&[0u8; 32][..])
|
|
|
|
.expect("Failure to deserialize dummy MAC");
|
|
|
|
let zkproof = Groth16Proof([0u8; 192]);
|
|
|
|
let encrypted_note = sprout::note::EncryptedNote([0u8; 601]);
|
|
|
|
|
|
|
|
// Create an dummy join split
|
|
|
|
let joinsplit = sprout::JoinSplit {
|
|
|
|
vpub_old: zero_amount,
|
|
|
|
vpub_new: zero_amount,
|
|
|
|
anchor,
|
|
|
|
nullifiers: [nullifier; 2],
|
|
|
|
commitments: [commitment; 2],
|
|
|
|
ephemeral_key,
|
|
|
|
random_seed,
|
|
|
|
vmacs: [mac.clone(), mac],
|
|
|
|
zkproof,
|
|
|
|
enc_ciphertexts: [encrypted_note; 2],
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create a usable signing key
|
|
|
|
let signing_key = ed25519::SigningKey::new(rand::thread_rng());
|
|
|
|
let verification_key = ed25519::VerificationKey::from(&signing_key);
|
|
|
|
|
|
|
|
// Populate join split data with the dummy join split.
|
|
|
|
let joinsplit_data = JoinSplitData {
|
|
|
|
first: joinsplit,
|
|
|
|
rest: vec![],
|
|
|
|
pub_key: verification_key.into(),
|
|
|
|
sig: [0u8; 64].into(),
|
|
|
|
};
|
|
|
|
|
|
|
|
(joinsplit_data, signing_key)
|
|
|
|
}
|
2021-07-01 16:03:34 -07:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn add_to_sprout_pool_after_nu() {
|
|
|
|
zebra_test::init();
|
|
|
|
|
|
|
|
// get a block that we know it haves a transaction with `vpub_old` field greater than 0.
|
|
|
|
let block: Arc<_> = zebra_chain::block::Block::zcash_deserialize(
|
|
|
|
&zebra_test::vectors::BLOCK_MAINNET_419199_BYTES[..],
|
|
|
|
)
|
|
|
|
.unwrap()
|
|
|
|
.into();
|
|
|
|
|
|
|
|
// create a block height at canopy activation.
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
let block_height = NetworkUpgrade::Canopy.activation_height(network).unwrap();
|
|
|
|
|
|
|
|
// create a zero amount.
|
|
|
|
let zero = Amount::<NonNegative>::try_from(0).expect("an amount of 0 is always valid");
|
|
|
|
|
|
|
|
// the coinbase transaction should pass the check.
|
|
|
|
assert_eq!(
|
|
|
|
check::disabled_add_to_sprout_pool(&block.transactions[0], block_height, network),
|
|
|
|
Ok(())
|
|
|
|
);
|
|
|
|
|
|
|
|
// the 2nd transaction has no joinsplits, should pass the check.
|
|
|
|
assert_eq!(block.transactions[1].joinsplit_count(), 0);
|
|
|
|
assert_eq!(
|
|
|
|
check::disabled_add_to_sprout_pool(&block.transactions[1], block_height, network),
|
|
|
|
Ok(())
|
|
|
|
);
|
|
|
|
|
|
|
|
// the 5th transaction has joinsplits and the `vpub_old` cumulative is greater than 0,
|
|
|
|
// should fail the check.
|
|
|
|
assert!(block.transactions[4].joinsplit_count() > 0);
|
|
|
|
let vpub_old: Amount<NonNegative> = block.transactions[4]
|
2021-08-09 10:22:26 -07:00
|
|
|
.output_values_to_sprout()
|
2021-07-01 16:03:34 -07:00
|
|
|
.fold(zero, |acc, &x| (acc + x).unwrap());
|
|
|
|
assert!(vpub_old > zero);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
check::disabled_add_to_sprout_pool(&block.transactions[3], block_height, network),
|
|
|
|
Err(TransactionError::DisabledAddToSproutPool)
|
|
|
|
);
|
|
|
|
|
|
|
|
// the 8th transaction has joinsplits and the `vpub_old` cumulative is 0,
|
|
|
|
// should pass the check.
|
|
|
|
assert!(block.transactions[7].joinsplit_count() > 0);
|
|
|
|
let vpub_old: Amount<NonNegative> = block.transactions[7]
|
2021-08-09 10:22:26 -07:00
|
|
|
.output_values_to_sprout()
|
2021-07-01 16:03:34 -07:00
|
|
|
.fold(zero, |acc, &x| (acc + x).unwrap());
|
|
|
|
assert_eq!(vpub_old, zero);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
check::disabled_add_to_sprout_pool(&block.transactions[7], block_height, network),
|
|
|
|
Ok(())
|
|
|
|
);
|
|
|
|
}
|