diff --git a/zebra-chain/src/transparent.rs b/zebra-chain/src/transparent.rs index 97a09683..b1a74b1f 100644 --- a/zebra-chain/src/transparent.rs +++ b/zebra-chain/src/transparent.rs @@ -34,6 +34,16 @@ pub struct CoinbaseData( pub(super) Vec, ); +#[cfg(any(test, feature = "proptest-impl"))] +impl CoinbaseData { + /// Create a new `CoinbaseData` containing `data`. + /// + /// Only for use in tests. + pub fn new(data: Vec) -> CoinbaseData { + CoinbaseData(data) + } +} + impl AsRef<[u8]> for CoinbaseData { fn as_ref(&self) -> &[u8] { self.0.as_ref() diff --git a/zebra-consensus/src/script.rs b/zebra-consensus/src/script.rs index 1a95b7ba..f57577ee 100644 --- a/zebra-consensus/src/script.rs +++ b/zebra-consensus/src/script.rs @@ -53,6 +53,8 @@ pub struct Request { /// A cached transaction, in the format required by the script verifier FFI interface. pub cached_ffi_transaction: Arc, /// The index of an input in `cached_ffi_transaction`, used for verifying this request + /// + /// Coinbase inputs are rejected by the script verifier, because they do not spend a UTXO. pub input_index: usize, /// A set of additional UTXOs known in the context of this verification request. /// diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index d171be9c..da8ab87f 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -453,27 +453,33 @@ where ) -> Result { let transaction = request.transaction(); - // feed all of the inputs to the script and shielded verifiers - // the script_verifier also checks transparent sighashes, using its own implementation - let cached_ffi_transaction = Arc::new(CachedFfiTransaction::new(transaction)); - let known_utxos = request.known_utxos(); - let upgrade = request.upgrade(network); + if transaction.is_coinbase() { + // The script verifier only verifies PrevOut inputs and their corresponding UTXOs. + // Coinbase transactions don't have any PrevOut inputs. + Ok(AsyncChecks::new()) + } else { + // feed all of the inputs to the script and shielded verifiers + // the script_verifier also checks transparent sighashes, using its own implementation + let cached_ffi_transaction = Arc::new(CachedFfiTransaction::new(transaction)); + let known_utxos = request.known_utxos(); + let upgrade = request.upgrade(network); - let script_checks = (0..inputs.len()) - .into_iter() - .map(move |input_index| { - let request = script::Request { - upgrade, - known_utxos: known_utxos.clone(), - cached_ffi_transaction: cached_ffi_transaction.clone(), - input_index, - }; + let script_checks = (0..inputs.len()) + .into_iter() + .map(move |input_index| { + let request = script::Request { + upgrade, + known_utxos: known_utxos.clone(), + cached_ffi_transaction: cached_ffi_transaction.clone(), + input_index, + }; - script_verifier.clone().oneshot(request).boxed() - }) - .collect(); + script_verifier.clone().oneshot(request).boxed() + }) + .collect(); - Ok(script_checks) + Ok(script_checks) + } } /// Verifies a transaction's Sprout shielded join split data. diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 13787119..fbc7eba2 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -13,7 +13,7 @@ use zebra_chain::{ arbitrary::{fake_v5_transactions_for_network, insert_fake_orchard_shielded_data}, Hash, HashType, JoinSplitData, LockTime, Transaction, }, - transparent, + transparent::{self, CoinbaseData}, }; use zebra_state::Utxo; @@ -321,6 +321,49 @@ async fn v4_transaction_with_transparent_transfer_is_accepted() { assert_eq!(result, Ok(transaction_hash)); } +/// 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, + }; + + let transaction_hash = transaction.hash(); + + 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; + + assert_eq!(result, Ok(transaction_hash)); +} + /// 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 @@ -379,7 +422,7 @@ async fn v4_transaction_with_transparent_transfer_is_rejected_by_the_script() { // defined. #[should_panic] async fn v5_transaction_with_transparent_transfer_is_accepted() { - let network = Network::Mainnet; + let network = Network::Testnet; let network_upgrade = NetworkUpgrade::Nu5; let nu5_activation_height = network_upgrade @@ -424,6 +467,55 @@ async fn v5_transaction_with_transparent_transfer_is_accepted() { assert_eq!(result, Ok(transaction_hash)); } +/// Test if V5 coinbase transaction is accepted. +#[tokio::test] +// TODO: Remove `should_panic` once the NU5 activation heights for testnet and mainnet have been +// defined. +#[should_panic] +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, + }; + + let transaction_hash = transaction.hash(); + + 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, Ok(transaction_hash)); +} + /// 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 @@ -433,7 +525,7 @@ async fn v5_transaction_with_transparent_transfer_is_accepted() { // defined. #[should_panic] async fn v5_transaction_with_transparent_transfer_is_rejected_by_the_script() { - let network = Network::Mainnet; + let network = Network::Testnet; let network_upgrade = NetworkUpgrade::Nu5; let nu5_activation_height = network_upgrade @@ -570,7 +662,7 @@ async fn v4_with_sprout_transfers() { /// that can either accept or reject any spend attempt, depending on if `script_should_succeed` is /// `true` or `false`. /// -/// Then, a [`transparent::Input`] is created that attempts to spends the previously created fake +/// Then, a [`transparent::Input::PrevOut`] is created that attempts to spend the previously created fake /// UTXO. A new UTXO is created with the [`transparent::Output`] resulting from the spend. /// /// Finally, the initial fake UTXO is placed in a `known_utxos` [`HashMap`] so that it can be @@ -635,6 +727,34 @@ fn mock_transparent_transfer( (input, output, known_utxos) } +/// 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) +} + /// 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