diff --git a/wallet/core/src/account/mod.rs b/wallet/core/src/account/mod.rs index 3314978e6..ee75d49bb 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -315,6 +315,7 @@ pub trait Account: AnySync + Send + Sync + 'static { self.clone().as_dyn_arc(), PaymentDestination::Change, fee_rate, + None, Fees::None, None, )?; @@ -351,8 +352,14 @@ pub trait Account: AnySync + Send + Sync + 'static { let keydata = self.prv_key_data(wallet_secret).await?; let signer = Arc::new(Signer::new(self.clone().as_dyn_arc(), keydata, payment_secret)); - let settings = - GeneratorSettings::try_new_with_account(self.clone().as_dyn_arc(), destination, fee_rate, priority_fee_sompi, payload)?; + let settings = GeneratorSettings::try_new_with_account( + self.clone().as_dyn_arc(), + destination, + fee_rate, + None, + priority_fee_sompi, + payload, + )?; let generator = Generator::try_new(settings, Some(signer), Some(abortable))?; @@ -381,8 +388,14 @@ pub trait Account: AnySync + Send + Sync + 'static { payment_secret: Option, abortable: &Abortable, ) -> Result { - let settings = - GeneratorSettings::try_new_with_account(self.clone().as_dyn_arc(), destination, fee_rate, priority_fee_sompi, payload)?; + let settings = GeneratorSettings::try_new_with_account( + self.clone().as_dyn_arc(), + destination, + fee_rate, + None, + priority_fee_sompi, + payload, + )?; let keydata = self.prv_key_data(wallet_secret).await?; let signer = Arc::new(PSKBSigner::new(self.clone().as_dyn_arc(), keydata, payment_secret)); let generator = Generator::try_new(settings, None, Some(abortable))?; @@ -463,6 +476,7 @@ pub trait Account: AnySync + Send + Sync + 'static { self.clone().as_dyn_arc(), final_transaction_destination, fee_rate, + None, priority_fee_sompi, final_transaction_payload, )? @@ -493,7 +507,8 @@ pub trait Account: AnySync + Send + Sync + 'static { payload: Option>, abortable: &Abortable, ) -> Result { - let settings = GeneratorSettings::try_new_with_account(self.as_dyn_arc(), destination, fee_rate, priority_fee_sompi, payload)?; + let settings = + GeneratorSettings::try_new_with_account(self.as_dyn_arc(), destination, fee_rate, None, priority_fee_sompi, payload)?; let generator = Generator::try_new(settings, None, Some(abortable))?; @@ -620,6 +635,7 @@ pub trait DerivationCapableAccount: Account { 1, PaymentDestination::Change, fee_rate, + None, Fees::None, None, None, diff --git a/wallet/core/src/account/pskb.rs b/wallet/core/src/account/pskb.rs index 7aa817e90..a5d8f61a6 100644 --- a/wallet/core/src/account/pskb.rs +++ b/wallet/core/src/account/pskb.rs @@ -334,6 +334,7 @@ pub fn pskt_to_pending_transaction( source_utxo_context: None, destination_utxo_context: None, fee_rate: None, + shuffle_outputs: None, final_transaction_priority_fee: fee_u.into(), final_transaction_destination, final_transaction_payload: None, diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index cbbbbc34d..f17a979b0 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -70,6 +70,8 @@ use kaspa_consensus_core::mass::Kip9Version; use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; use kaspa_txscript::pay_to_address_script; +use rand::seq::SliceRandom; +use rand::thread_rng; use std::collections::VecDeque; use super::SignerT; @@ -296,6 +298,8 @@ struct Inner { signature_mass_per_input: u64, // fee rate fee_rate: Option, + // shuffle outputs of final transaction + shuffle_outputs: Option, // final transaction amount and fees // `None` is used for sweep transactions final_transaction: Option, @@ -362,6 +366,7 @@ impl Generator { minimum_signatures, change_address, fee_rate, + shuffle_outputs, final_transaction_priority_fee, final_transaction_destination, final_transaction_payload, @@ -469,6 +474,7 @@ impl Generator { standard_change_output_compute_mass: standard_change_output_mass, signature_mass_per_input, fee_rate, + shuffle_outputs, final_transaction, final_transaction_priority_fee, final_transaction_outputs, @@ -1069,7 +1075,7 @@ impl Generator { let change_output_value = change_output_value.unwrap_or(0); - let mut final_outputs = self.inner.final_transaction_outputs.clone(); + let mut final_outputs: Vec = self.inner.final_transaction_outputs.clone(); if self.inner.final_transaction_priority_fee.receiver_pays() { let output = final_outputs.get_mut(0).expect("include fees requires one output"); @@ -1080,10 +1086,23 @@ impl Generator { } } - let change_output_index = if change_output_value > 0 { - let change_output_index = Some(final_outputs.len()); - final_outputs.push(TransactionOutput::new(change_output_value, pay_to_address_script(&self.inner.change_address))); - change_output_index + // Cache the change output (if any) before shuffling so we can find its index. + let change_output = if change_output_value > 0 { + let change_output = TransactionOutput::new(change_output_value, pay_to_address_script(&self.inner.change_address)); + final_outputs.push(change_output.clone()); + Some(change_output) + } else { + None + }; + + // Shuffle the outputs if required for extra privacy. + if self.inner.shuffle_outputs.unwrap_or(true) { + final_outputs.shuffle(&mut thread_rng()); + } + + // Find the new change_output_index after shuffling if there was a change output. + let change_output_index = if let Some(change_output) = change_output { + final_outputs.iter().position(|output| output == &change_output) } else { None }; diff --git a/wallet/core/src/tx/generator/settings.rs b/wallet/core/src/tx/generator/settings.rs index a1fcf2acf..b7b3b0c73 100644 --- a/wallet/core/src/tx/generator/settings.rs +++ b/wallet/core/src/tx/generator/settings.rs @@ -30,6 +30,8 @@ pub struct GeneratorSettings { pub change_address: Address, // fee rate pub fee_rate: Option, + // Whether to shuffle the outputs of the final transaction for privacy reasons. + pub shuffle_outputs: Option, // applies only to the final transaction pub final_transaction_priority_fee: Fees, // final transaction outputs @@ -63,6 +65,7 @@ impl GeneratorSettings { account: Arc, final_transaction_destination: PaymentDestination, fee_rate: Option, + shuffle_outputs: Option, final_priority_fee: Fees, final_transaction_payload: Option>, ) -> Result { @@ -83,8 +86,8 @@ impl GeneratorSettings { utxo_iterator: Box::new(utxo_iterator), source_utxo_context: Some(account.utxo_context().clone()), priority_utxo_entries: None, - fee_rate, + shuffle_outputs, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, @@ -94,6 +97,7 @@ impl GeneratorSettings { Ok(settings) } + #[allow(clippy::too_many_arguments)] pub fn try_new_with_context( utxo_context: UtxoContext, priority_utxo_entries: Option>, @@ -102,6 +106,7 @@ impl GeneratorSettings { minimum_signatures: u16, final_transaction_destination: PaymentDestination, fee_rate: Option, + shuffle_outputs: Option, final_priority_fee: Fees, final_transaction_payload: Option>, multiplexer: Option>>, @@ -118,8 +123,8 @@ impl GeneratorSettings { utxo_iterator: Box::new(utxo_iterator), source_utxo_context: Some(utxo_context), priority_utxo_entries, - fee_rate, + shuffle_outputs, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, @@ -139,6 +144,7 @@ impl GeneratorSettings { minimum_signatures: u16, final_transaction_destination: PaymentDestination, fee_rate: Option, + shuffle_outputs: Option, final_priority_fee: Fees, final_transaction_payload: Option>, multiplexer: Option>>, @@ -152,8 +158,8 @@ impl GeneratorSettings { utxo_iterator: Box::new(utxo_iterator), source_utxo_context: None, priority_utxo_entries, - fee_rate, + shuffle_outputs, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index 156f7ec8d..46a644bc5 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -436,6 +436,7 @@ where priority_utxo_entries, destination_utxo_context, fee_rate, + shuffle_outputs: None, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, diff --git a/wallet/core/src/wasm/tx/generator/generator.rs b/wallet/core/src/wasm/tx/generator/generator.rs index 8cc236238..b1ba6dd14 100644 --- a/wallet/core/src/wasm/tx/generator/generator.rs +++ b/wallet/core/src/wasm/tx/generator/generator.rs @@ -169,6 +169,7 @@ impl Generator { final_transaction_destination, change_address, fee_rate, + shuffle_outputs, final_priority_fee, sig_op_count, minimum_signatures, @@ -192,6 +193,7 @@ impl Generator { minimum_signatures, final_transaction_destination, fee_rate, + shuffle_outputs, final_priority_fee, payload, multiplexer, @@ -209,6 +211,7 @@ impl Generator { minimum_signatures, final_transaction_destination, fee_rate, + shuffle_outputs, final_priority_fee, payload, multiplexer, @@ -272,6 +275,7 @@ struct GeneratorSettings { pub final_transaction_destination: PaymentDestination, pub change_address: Option
, pub fee_rate: Option, + pub shuffle_outputs: Option, pub final_priority_fee: Fees, pub sig_op_count: u8, pub minimum_signatures: u16, @@ -292,6 +296,8 @@ impl TryFrom for GeneratorSettings { let fee_rate = args.get_f64("feeRate").ok().and_then(|v| (v.is_finite() && !v.is_nan() && v >= 1e-8).then_some(v)); + let shuffle_outputs = args.get_bool("shuffleOutputs").ok(); + let final_priority_fee = args.get::("priorityFee")?.try_into()?; let generator_source = if let Ok(Some(context)) = args.try_cast_into::("entries") { @@ -325,6 +331,7 @@ impl TryFrom for GeneratorSettings { final_transaction_destination, change_address, fee_rate, + shuffle_outputs, final_priority_fee, sig_op_count, minimum_signatures,