diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index e192691ea5f..a1b085d34ca 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -32,14 +32,11 @@ use zebra_chain::{ serialization::{ZcashDeserialize, ZcashSerialize}, subtree::NoteCommitmentSubtreeIndex, transaction::{self, LockTime, SerializedTransaction, Transaction, UnminedTx}, - transparent::{self, opcodes::OpCode, Address, Input, OutPoint, Script}, + transparent::{self, opcodes::OpCode, Address, Input, OutPoint, Output, Script}, }; use zebra_node_services::mempool; -use zebra_state::{ - GetBlockTemplateChainInfo, HashOrHeight, MinedTx, OutputIndex, OutputLocation, - TransactionLocation, -}; +use zebra_state::{HashOrHeight, MinedTx, OutputIndex, OutputLocation, TransactionLocation}; use crate::{ constants::{INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE}, @@ -1051,23 +1048,11 @@ where locktime: Option, expiryheight: Option, ) -> BoxFuture> { - let mut state = self.state.clone(); let network = self.network.clone(); + let latest_chain_tip = self.latest_chain_tip.clone(); async move { - let chain_info_request = zebra_state::ReadRequest::ChainInfo; - let response: zebra_state::ReadResponse = state - .ready() - .and_then(|service| service.call(chain_info_request)) - .await - .map_server_error()?; - - let zebra_state::ReadResponse::ChainInfo(GetBlockTemplateChainInfo { - tip_height, .. - }) = response - else { - unreachable!("unmatched response to a chain info request") - }; + let tip_height = best_chain_tip_height(&latest_chain_tip).map_server_error()?; let lock_time = if let Some(lock_time) = locktime { LockTime::Height(block::Height(lock_time)) @@ -1078,37 +1063,42 @@ where // Use next block height if exceeds MAX_EXPIRY_HEIGHT or is beyond tip let next_block_height = tip_height.0 + 1; let current_upgrade = NetworkUpgrade::current(&network, tip_height); - let next_network_upgrade = NetworkUpgrade::current(&network, block::Height(next_block_height)); + let next_network_upgrade = NetworkUpgrade::current(&network, Height(next_block_height)); let expiry_height_as_u32 = if let Some(expiry_height) = expiryheight { - // DoS mitigation: reject transactions expiring soon (as is done in zcashd) - if expiry_height != 0 && next_block_height + block::Height::BLOCK_EXPIRY_HEIGHT_THRESHOLD > expiry_height { - Error::invalid_params(format!("invalid parameter, expiryheight should be at least {} to avoid - transaction expiring soon", next_block_height + block::Height::BLOCK_EXPIRY_HEIGHT_THRESHOLD)); + if next_network_upgrade < NetworkUpgrade::Overwinter { + return Err(Error::invalid_params( + "invalid parameter, expiryheight can only be used if Overwinter is active when the transaction is mined" + )).map_server_error(); } - if next_network_upgrade < NetworkUpgrade::Overwinter { - Error::invalid_params("invalid parameter, expiryheight can only be used if Overwinter is active when the transaction is mined"); + if block::Height(expiry_height) >= block::Height::MAX_EXPIRY_HEIGHT { + return Err(Error::invalid_params( + format!("Invalid parameter, expiryheight must be nonnegative and less than {:?}.", + block::Height::MAX_EXPIRY_HEIGHT) + )); } - if block::Height(expiry_height) > block::Height::MAX_EXPIRY_HEIGHT - || expiry_height > tip_height.0 - { - Error::invalid_params("invalid parameter, expiryheight out of range"); + // DoS mitigation: reject transactions expiring soon (as is done in zcashd) + if expiry_height != 0 && next_block_height + block::Height::BLOCK_EXPIRY_HEIGHT_THRESHOLD > expiry_height { + return Err(Error::invalid_params( + format!("invalid parameter, expiryheight should be at least {} to avoid transaction expiring soon", + next_block_height + block::Height::BLOCK_EXPIRY_HEIGHT_THRESHOLD) + )); } + expiry_height } else { next_block_height }; - - // Set a default sequence based on the lock time. + // Set a default sequence based on the lock time let default_tx_input_sequence = if lock_time == LockTime::Height(block::Height(0)) { u32::MAX } else { u32::MAX - 1 }; - // Compute tx inputs + // Handle tx inputs let tx_inputs: Vec = transactions .iter() .map(|input| { @@ -1125,15 +1115,14 @@ where }) .collect::>>()?; - // Comput tx outputs - let mut unique_addresses = HashSet::new(); - let mut tx_outputs: Vec = Vec::new(); + // Handle tx outputs + let mut tx_outputs: Vec = Vec::new(); - for (address, amount) in addresses { - if !unique_addresses.insert(address.clone()) { - return Err(Error::invalid_params("invalid parameter, invalid duplicate address")); - } + // Check if addresses contained in params are valid + let address_strings = AddressStrings { addresses: addresses.clone().keys().cloned().collect() }; + let _result = address_strings.valid_addresses().map_err(|e| e.to_string()).map_server_error()?; + for (address, amount) in addresses { let address = address.parse().map_server_error()?; let lock_script = match address { @@ -1159,7 +1148,7 @@ where let value = Amount::::try_from(zatoshi_amount) .map_err(|e| Error::invalid_params(format!("invalid amount: {}", e)))?; - let tx_out = zebra_chain::transparent::Output { + let tx_out = Output { value, lock_script }; @@ -1913,7 +1902,7 @@ impl Default for GetBlockHash { /// A struct used for the `transactions` parameter for /// [`Rpc::create_raw_transaction` method]. -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct TxInput { txid: String, vout: u32, diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 5b5a21e23d0..eff1861a0b8 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -423,6 +423,340 @@ async fn rpc_getbestblockhash() { assert!(rpc_tx_queue_task_result.is_none()); } +#[tokio::test(flavor = "multi_thread")] +async fn rpc_createrawtransaction() { + use zebra_chain::block::{Hash, Height}; + + let _init_guard = zebra_test::init(); + + let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests(); + let state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests(); + + // nu5 block height + let fake_tip_height = NetworkUpgrade::Nu5.activation_height(&Mainnet).unwrap(); + // nu5 block hash + let fake_tip_hash = + Hash::from_hex("0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8").unwrap(); + + let (mock_chain_tip, mock_chain_tip_sender) = MockChainTip::new(); + mock_chain_tip_sender.send_best_tip_height(fake_tip_height); + mock_chain_tip_sender.send_best_tip_hash(fake_tip_hash); + mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(0)); + + let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new( + "RPC test", + "RPC test", + Mainnet, + false, + true, + Buffer::new(mempool.clone(), 1), + Buffer::new(state.clone(), 1), + mock_chain_tip.clone(), + ); + + // Test basic transaction creation + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses = HashMap::new(); + addresses.insert("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(), 1.0); + addresses + }, + None, + None, + ); + + let response = tx_future.await; + + assert!( + response.is_ok(), + "Basic transaction creation should succeed" + ); + + // Test transaction with invalid input transaction id + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "00000".to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses = HashMap::new(); + addresses.insert("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(), 1.0); + addresses + }, + None, + None, + ); + + let response = tx_future.await; + + assert!( + response.is_err(), + "RPC should not return transaction hash with invalid transaction id in input" + ); + assert!(response + .unwrap_err() + .message + .contains("invalid parameter, transaction id")); + + // Test invalid inputs: invalid transparent address in HashMap + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses = HashMap::new(); + addresses.insert("t3Vz2".to_string(), 1.0); + addresses + }, + None, + None, + ); + + let response = tx_future.await; + + assert!( + response.is_err(), + "RPC should err if address to value mapping contains invalid address" + ); + assert!(response + .unwrap_err() + .message + .contains("t-addr decoding error")); + + // Test inputs containing a valid locktime + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses = HashMap::new(); + addresses.insert("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(), 1.0); + addresses + }, + Some(100), + None, + ); + + let response = tx_future.await; + + assert!( + response.is_ok(), + "RPC with valid locktime input should succeed" + ); + + // Test inputs containing a valid expiry_height + + let expiry_height = fake_tip_height.0 + 40; + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses = HashMap::new(); + addresses.insert("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(), 1.0); + addresses + }, + None, + Some(expiry_height), + ); + + let response = tx_future.await; + + assert!( + response.is_ok(), + "RPC with valid expiry_height should succeed" + ); + + // Test inputs containing an invalid expiry_height: expiry_height is not more than block::Height::BLOCK_EXPIRY_HEIGHT_THRESHOLD + + let expiry_height = fake_tip_height.0 + 1; + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses = HashMap::new(); + addresses.insert("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(), 1.0); + addresses + }, + None, + Some(expiry_height), + ); + + let response = tx_future.await; + + assert!( + response.is_err(), + "RPC should err if expiry_height is within range of block expiry threshold" + ); + assert!(response + .unwrap_err() + .message + .contains("invalid parameter, expiryheight should be at least")); + + // Test inputs containing an invalid expiry_height: expiry_height greater than block::Height::MAX_EXPIRY_HEIGHT + + let expiry_height = Height::MAX_EXPIRY_HEIGHT.0; + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses = HashMap::new(); + addresses.insert("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(), 1.0); + addresses + }, + None, + Some(expiry_height), + ); + + let response = tx_future.await; + + assert!( + response.is_err(), + "RPC should err if expiry_height is greater than the MAX_EXPIRY_HEIGHT" + ); + assert!(response + .unwrap_err() + .message + .contains("Invalid parameter, expiryheight must be nonnegative and less than")); + + // Test inputs containing an invalid expiry_height: expiry_height can only be used if NetworkUpgrade::Overwinter is active + { + let (mock_chain_tip, mock_chain_tip_sender) = MockChainTip::new(); + mock_chain_tip_sender.send_best_tip_height(Height( + NetworkUpgrade::BeforeOverwinter + .activation_height(&Mainnet) + .unwrap() + .0, + )); + mock_chain_tip_sender.send_best_tip_hash(fake_tip_hash); + mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(0)); + + let (rpc, _) = RpcImpl::new( + "RPC test", + "RPC test", + Mainnet, + false, + true, + Buffer::new(mempool.clone(), 1), + Buffer::new(state.clone(), 1), + mock_chain_tip.clone(), + ); + + let expiry_height = fake_tip_height.0 + 40; + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "0000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses: HashMap = HashMap::new(); + addresses.insert("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(), 1.0); + addresses + }, + None, + Some(expiry_height), + ); + + let response = tx_future.await; + + assert!( + response.is_err(), + "RPC should err if NetworkUpgrade::Overwinter is not active and expiryheight is used." + ); + assert!(response + .unwrap_err() + .message + .contains("invalid parameter, expiryheight can only be used if Overwinter is active when the transaction is mined")); + } + + // Test transaction creation with invalid ZEC value: more than 21 million + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses: HashMap = HashMap::new(); + addresses.insert( + "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(), + 21_000_001.0, + ); + addresses + }, + None, + None, + ); + + let response = tx_future.await; + + assert!( + response.is_err(), + "RPC should err if inputs contains an invalid ZEC value" + ); + assert!(response.unwrap_err().message.contains("invalid amount")); + + // Test transaction creation with invalid ZEC value: negative amount + + let tx_future = rpc.create_raw_transaction( + vec![TxInput { + txid: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + vout: 0, + sequence: None, + }], + { + let mut addresses = HashMap::new(); + addresses.insert("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(), -4.0); + addresses + }, + None, + None, + ); + + let response = tx_future.await; + + assert!( + response.is_err(), + "RPC should err if inputs contains an invalid ZEC value" + ); + assert!(response.unwrap_err().message.contains("invalid amount")); + + mempool.expect_no_requests().await; + + // The queue task should continue without errors or panics + let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never(); + assert!(rpc_tx_queue_task_result.is_none()); +} + #[tokio::test(flavor = "multi_thread")] async fn rpc_getrawtransaction() { let _init_guard = zebra_test::init();