diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 510af1dbf..8d63a8638 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -92,7 +92,7 @@ jobs: uses: Swatinem/rust-cache@v2.2.1 - name: Check bdk wallet working-directory: ./crates/wallet - run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm + run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown - name: Check esplora working-directory: ./crates/esplora run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,async diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 15036a99a..5093b4ef5 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -1,6 +1,5 @@ use std::collections::BTreeSet; use std::thread::JoinHandle; -use std::usize; use bdk_chain::collections::BTreeMap; use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index e7e02093f..9c141336d 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -13,9 +13,9 @@ edition = "2021" rust-version = "1.63" [dependencies] -rand = "^0.8" +rand_core = { version = "0.6.0" } miniscript = { version = "12.0.0", features = ["serde"], default-features = false } -bitcoin = { version = "0.32.0", features = ["serde", "base64", "rand-std"], default-features = false } +bitcoin = { version = "0.32.0", features = ["serde", "base64"], default-features = false } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", "serde"], default-features = false } @@ -23,22 +23,13 @@ bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", " # Optional dependencies bip39 = { version = "2.0", optional = true } -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = "0.2" -js-sys = "0.3" - [features] default = ["std"] -std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"] +std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"] compiler = ["miniscript/compiler"] all-keys = ["keys-bip39"] keys-bip39 = ["bip39"] -# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended -# for libraries to explicitly include the "getrandom/js" feature, so we only do it when -# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support -dev-getrandom-wasm = ["getrandom/js"] - [dev-dependencies] lazy_static = "1.4" assert_matches = "1.5.0" @@ -46,6 +37,7 @@ tempfile = "3" bdk_sqlite = { path = "../sqlite" } bdk_file_store = { path = "../file_store" } anyhow = "1" +rand = "^0.8" [package.metadata.docs.rs] all-features = true diff --git a/crates/wallet/README.md b/crates/wallet/README.md index ffe99474e..be780b6c3 100644 --- a/crates/wallet/README.md +++ b/crates/wallet/README.md @@ -70,30 +70,28 @@ To persist `Wallet` state data use a data store crate that reads and writes [`bd ```rust,no_run use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}}; -fn main() { - // Open or create a new file store for wallet data. - let mut db = - bdk_file_store::Store::::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db") - .expect("create store"); - - // Create a wallet with initial wallet data read from the file store. - let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; - let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)"; - let changeset = db.aggregate_changesets().expect("changeset loaded"); - let mut wallet = - Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet) - .expect("create or load wallet"); - - // Get a new address to receive bitcoin. - let receive_address = wallet.reveal_next_address(KeychainKind::External); - // Persist staged wallet data changes to the file store. - let staged_changeset = wallet.take_staged(); - if let Some(changeset) = staged_changeset { - db.append_changeset(&changeset) - .expect("must commit changes to database"); - } - println!("Your new receive address is: {}", receive_address.address); +// Open or create a new file store for wallet data. +let mut db = + bdk_file_store::Store::::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db") + .expect("create store"); + +// Create a wallet with initial wallet data read from the file store. +let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; +let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)"; +let changeset = db.aggregate_changesets().expect("changeset loaded"); +let mut wallet = + Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet) + .expect("create or load wallet"); + +// Get a new address to receive bitcoin. +let receive_address = wallet.reveal_next_address(KeychainKind::External); +// Persist staged wallet data changes to the file store. +let staged_changeset = wallet.take_staged(); +if let Some(changeset) = staged_changeset { + db.append_changeset(&changeset) + .expect("must commit changes to database"); } +println!("Your new receive address is: {}", receive_address.address); ``` diff --git a/crates/wallet/src/keys/mod.rs b/crates/wallet/src/keys/mod.rs index 108270348..907deb7ba 100644 --- a/crates/wallet/src/keys/mod.rs +++ b/crates/wallet/src/keys/mod.rs @@ -20,6 +20,8 @@ use core::marker::PhantomData; use core::ops::Deref; use core::str::FromStr; +use rand_core::{CryptoRng, RngCore}; + use bitcoin::secp256k1::{self, Secp256k1, Signing}; use bitcoin::bip32; @@ -631,12 +633,23 @@ pub trait GeneratableKey: Sized { entropy: Self::Entropy, ) -> Result, Self::Error>; - /// Generate a key given the options with a random entropy + /// Generate a key given the options with random entropy. + /// + /// Uses the thread-local random number generator. + #[cfg(feature = "std")] fn generate(options: Self::Options) -> Result, Self::Error> { - use rand::{thread_rng, Rng}; + Self::generate_with_aux_rand(options, &mut bitcoin::key::rand::thread_rng()) + } + /// Generate a key given the options with random entropy. + /// + /// Uses a provided random number generator (rng). + fn generate_with_aux_rand( + options: Self::Options, + rng: &mut (impl CryptoRng + RngCore), + ) -> Result, Self::Error> { let mut entropy = Self::Entropy::default(); - thread_rng().fill(entropy.as_mut()); + rng.fill_bytes(entropy.as_mut()); Self::generate_with_entropy(options, entropy) } } @@ -657,8 +670,20 @@ where } /// Generate a key with the default options and a random entropy + /// + /// Uses the thread-local random number generator. + #[cfg(feature = "std")] fn generate_default() -> Result, Self::Error> { - Self::generate(Default::default()) + Self::generate_with_aux_rand(Default::default(), &mut bitcoin::key::rand::thread_rng()) + } + + /// Generate a key with the default options and a random entropy + /// + /// Uses a provided random number generator (rng). + fn generate_default_with_aux_rand( + rng: &mut (impl CryptoRng + RngCore), + ) -> Result, Self::Error> { + Self::generate_with_aux_rand(Default::default(), rng) } } diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs index 819a15edb..4016e05ca 100644 --- a/crates/wallet/src/wallet/coin_selection.rs +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -114,8 +114,9 @@ use bitcoin::{Script, Weight}; use core::convert::TryInto; use core::fmt::{self, Formatter}; -use rand::seq::SliceRandom; +use rand_core::RngCore; +use super::utils::shuffle_slice; /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not /// overridden pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection; @@ -516,27 +517,16 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { )); } - Ok(self - .bnb( - required_utxos.clone(), - optional_utxos.clone(), - curr_value, - curr_available_value, - target_amount, - cost_of_change, - drain_script, - fee_rate, - ) - .unwrap_or_else(|_| { - self.single_random_draw( - required_utxos, - optional_utxos, - curr_value, - target_amount, - drain_script, - fee_rate, - ) - })) + self.bnb( + required_utxos.clone(), + optional_utxos.clone(), + curr_value, + curr_available_value, + target_amount, + cost_of_change, + drain_script, + fee_rate, + ) } } @@ -663,40 +653,6 @@ impl BranchAndBoundCoinSelection { )) } - #[allow(clippy::too_many_arguments)] - fn single_random_draw( - &self, - required_utxos: Vec, - mut optional_utxos: Vec, - curr_value: i64, - target_amount: i64, - drain_script: &Script, - fee_rate: FeeRate, - ) -> CoinSelectionResult { - optional_utxos.shuffle(&mut rand::thread_rng()); - let selected_utxos = optional_utxos.into_iter().fold( - (curr_value, vec![]), - |(mut amount, mut utxos), utxo| { - if amount >= target_amount { - (amount, utxos) - } else { - amount += utxo.effective_value; - utxos.push(utxo); - (amount, utxos) - } - }, - ); - - // remaining_amount can't be negative as that would mean the - // selection wasn't successful - // target_amount = amount_needed + (fee_amount - vin_fees) - let remaining_amount = (selected_utxos.0 - target_amount) as u64; - - let excess = decide_change(remaining_amount, fee_rate, drain_script); - - BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess) - } - fn calculate_cs_result( mut selected_utxos: Vec, mut required_utxos: Vec, @@ -717,6 +673,58 @@ impl BranchAndBoundCoinSelection { } } +// Pull UTXOs at random until we have enough to meet the target +pub(crate) fn single_random_draw( + required_utxos: Vec, + optional_utxos: Vec, + target_amount: u64, + drain_script: &Script, + fee_rate: FeeRate, + rng: &mut impl RngCore, +) -> CoinSelectionResult { + let target_amount = target_amount + .try_into() + .expect("Bitcoin amount to fit into i64"); + + let required_utxos: Vec = required_utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let mut optional_utxos: Vec = optional_utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let curr_value = required_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value); + + shuffle_slice(&mut optional_utxos, rng); + + let selected_utxos = + optional_utxos + .into_iter() + .fold((curr_value, vec![]), |(mut amount, mut utxos), utxo| { + if amount >= target_amount { + (amount, utxos) + } else { + amount += utxo.effective_value; + utxos.push(utxo); + (amount, utxos) + } + }); + + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // target_amount = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (selected_utxos.0 - target_amount) as u64; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess) +} + /// Remove duplicate UTXOs. /// /// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept. @@ -740,6 +748,7 @@ where mod test { use assert_matches::assert_matches; use core::str::FromStr; + use rand::rngs::StdRng; use bdk_chain::ConfirmationTime; use bitcoin::{Amount, ScriptBuf, TxIn, TxOut}; @@ -748,8 +757,7 @@ mod test { use crate::types::*; use crate::wallet::coin_selection::filter_duplicates; - use rand::rngs::StdRng; - use rand::seq::SliceRandom; + use rand::prelude::SliceRandom; use rand::{Rng, RngCore, SeedableRng}; // signature len (1WU) + signature and sighash (72WU) @@ -1090,13 +1098,12 @@ mod test { } #[test] + #[ignore = "SRD fn was moved out of BnB"] fn test_bnb_coin_selection_success() { // In this case bnb won't find a suitable match and single random draw will // select three outputs let utxos = generate_same_value_utxos(100_000, 20); - let drain_script = ScriptBuf::default(); - let target_amount = 250_000 + FEE_AMOUNT; let result = BranchAndBoundCoinSelection::default() @@ -1136,6 +1143,7 @@ mod test { } #[test] + #[ignore = "no exact match for bnb, previously fell back to SRD"] fn test_bnb_coin_selection_optional_are_enough() { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); @@ -1156,6 +1164,26 @@ mod test { assert_eq!(result.fee_amount, 136); } + #[test] + fn test_single_random_draw_function_success() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut utxos = generate_random_utxos(&mut rng, 300); + let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT; + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); + let drain_script = ScriptBuf::default(); + let result = single_random_draw( + vec![], + utxos, + target_amount, + &drain_script, + fee_rate, + &mut rng, + ); + assert!(result.selected_amount() > target_amount); + assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64); + } + #[test] #[ignore] fn test_bnb_coin_selection_required_not_enough() { @@ -1410,34 +1438,6 @@ mod test { } } - #[test] - fn test_single_random_draw_function_success() { - let seed = [0; 32]; - let mut rng: StdRng = SeedableRng::from_seed(seed); - let mut utxos = generate_random_utxos(&mut rng, 300); - let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT; - - let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); - let utxos: Vec = utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - let drain_script = ScriptBuf::default(); - - let result = BranchAndBoundCoinSelection::default().single_random_draw( - vec![], - utxos, - 0, - target_amount as i64, - &drain_script, - fee_rate, - ); - - assert!(result.selected_amount() > target_amount); - assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64); - } - #[test] fn test_bnb_exclude_negative_effective_value() { let utxos = get_test_utxos(); diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 883e954a7..3a8361ca5 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -41,6 +41,8 @@ use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt}; use bitcoin::{constants::genesis_block, Amount}; use core::fmt; use core::ops::Deref; +use rand_core::RngCore; + use descriptor::error::Error as DescriptorError; use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; @@ -1237,6 +1239,7 @@ impl Wallet { &mut self, coin_selection: Cs, params: TxParams, + rng: &mut impl RngCore, ) -> Result { let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect(); let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist"); @@ -1463,13 +1466,31 @@ impl Wallet { let (required_utxos, optional_utxos) = coin_selection::filter_duplicates(required_utxos, optional_utxos); - let coin_selection = coin_selection.coin_select( - required_utxos, - optional_utxos, + let coin_selection = match coin_selection.coin_select( + required_utxos.clone(), + optional_utxos.clone(), fee_rate, outgoing.to_sat() + fee_amount, &drain_script, - )?; + ) { + Ok(res) => res, + Err(e) => match e { + coin_selection::Error::InsufficientFunds { .. } => { + return Err(CreateTxError::CoinSelection(e)); + } + coin_selection::Error::BnBNoExactMatch + | coin_selection::Error::BnBTotalTriesExceeded => { + coin_selection::single_random_draw( + required_utxos, + optional_utxos, + outgoing.to_sat() + fee_amount, + &drain_script, + fee_rate, + rng, + ) + } + }, + }; fee_amount += coin_selection.fee_amount; let excess = &coin_selection.excess; @@ -1533,7 +1554,7 @@ impl Wallet { }; // sort input/outputs according to the chosen algorithm - params.ordering.sort_tx(&mut tx); + params.ordering.sort_tx_with_aux_rand(&mut tx, rng); let psbt = self.complete_transaction(tx, coin_selection.selected, params)?; Ok(psbt) diff --git a/crates/wallet/src/wallet/signer.rs b/crates/wallet/src/wallet/signer.rs index 16194961a..3967ba413 100644 --- a/crates/wallet/src/wallet/signer.rs +++ b/crates/wallet/src/wallet/signer.rs @@ -607,7 +607,7 @@ fn sign_psbt_schnorr( }; let msg = &Message::from(hash); - let signature = secp.sign_schnorr(msg, &keypair); + let signature = secp.sign_schnorr_no_aux_rand(msg, &keypair); secp.verify_schnorr(&signature, msg, &XOnlyPublicKey::from_keypair(&keypair).0) .expect("invalid or corrupted schnorr signature"); diff --git a/crates/wallet/src/wallet/tx_builder.rs b/crates/wallet/src/wallet/tx_builder.rs index 02d6df59d..0026025de 100644 --- a/crates/wallet/src/wallet/tx_builder.rs +++ b/crates/wallet/src/wallet/tx_builder.rs @@ -45,8 +45,10 @@ use core::fmt; use bitcoin::psbt::{self, Psbt}; use bitcoin::script::PushBytes; use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid}; +use rand_core::RngCore; use super::coin_selection::CoinSelectionAlgorithm; +use super::utils::shuffle_slice; use super::{CreateTxError, Wallet}; use crate::collections::{BTreeMap, HashSet}; use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; @@ -669,16 +671,33 @@ impl<'a, Cs> TxBuilder<'a, Cs> { impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> { /// Finish building the transaction. /// + /// Uses the thread-local random number generator (rng). + /// /// Returns a new [`Psbt`] per [`BIP174`]. /// /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki /// /// **WARNING**: To avoid change address reuse you must persist the changes resulting from one /// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`]. + #[cfg(feature = "std")] pub fn finish(self) -> Result { + self.finish_with_aux_rand(&mut bitcoin::key::rand::thread_rng()) + } + + /// Finish building the transaction. + /// + /// Uses a provided random number generator (rng). + /// + /// Returns a new [`Psbt`] per [`BIP174`]. + /// + /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki + /// + /// **WARNING**: To avoid change address reuse you must persist the changes resulting from one + /// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`]. + pub fn finish_with_aux_rand(self, rng: &mut impl RngCore) -> Result { self.wallet .borrow_mut() - .create_tx(self.coin_selection, self.params) + .create_tx(self.coin_selection, self.params, rng) } } @@ -757,15 +776,23 @@ pub enum TxOrdering { } impl TxOrdering { - /// Sort transaction inputs and outputs by [`TxOrdering`] variant - pub fn sort_tx(&self, tx: &mut Transaction) { + /// Sort transaction inputs and outputs by [`TxOrdering`] variant. + /// + /// Uses the thread-local random number generator (rng). + #[cfg(feature = "std")] + pub fn sort_tx(self, tx: &mut Transaction) { + self.sort_tx_with_aux_rand(tx, &mut bitcoin::key::rand::thread_rng()) + } + + /// Sort transaction inputs and outputs by [`TxOrdering`] variant. + /// + /// Uses a provided random number generator (rng). + pub fn sort_tx_with_aux_rand(self, tx: &mut Transaction, rng: &mut impl RngCore) { match self { TxOrdering::Untouched => {} TxOrdering::Shuffle => { - use rand::seq::SliceRandom; - let mut rng = rand::thread_rng(); - tx.input.shuffle(&mut rng); - tx.output.shuffle(&mut rng); + shuffle_slice(&mut tx.input, rng); + shuffle_slice(&mut tx.output, rng); } TxOrdering::Bip69Lexicographic => { tx.input.sort_unstable_by_key(|txin| { @@ -851,12 +878,6 @@ mod test { use bitcoin::TxOut; use super::*; - - #[test] - fn test_output_ordering_default_shuffle() { - assert_eq!(TxOrdering::default(), TxOrdering::Shuffle); - } - #[test] fn test_output_ordering_untouched() { let original_tx = ordering_test_tx!(); diff --git a/crates/wallet/src/wallet/utils.rs b/crates/wallet/src/wallet/utils.rs index 402361134..b3ec51cb0 100644 --- a/crates/wallet/src/wallet/utils.rs +++ b/crates/wallet/src/wallet/utils.rs @@ -14,6 +14,8 @@ use bitcoin::{absolute, relative, Script, Sequence}; use miniscript::{MiniscriptKey, Satisfier, ToPublicKey}; +use rand_core::RngCore; + /// Trait to check if a value is below the dust limit. /// We are performing dust value calculation for a given script public key using rust-bitcoin to /// keep it compatible with network dust rate @@ -110,6 +112,19 @@ impl Satisfier for Older { } } +// The Knuth shuffling algorithm based on the original [Fisher-Yates method](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) +pub(crate) fn shuffle_slice(list: &mut [T], rng: &mut impl RngCore) { + if list.is_empty() { + return; + } + let mut current_index = list.len() - 1; + while current_index > 0 { + let random_index = rng.next_u32() as usize % (current_index + 1); + list.swap(current_index, random_index); + current_index -= 1; + } +} + pub(crate) type SecpCtx = Secp256k1; #[cfg(test)] @@ -118,9 +133,11 @@ mod test { // otherwise it's time-based pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22; - use super::{check_nsequence_rbf, IsDust}; + use super::{check_nsequence_rbf, shuffle_slice, IsDust}; use crate::bitcoin::{Address, Network, Sequence}; + use alloc::vec::Vec; use core::str::FromStr; + use rand::{rngs::StdRng, thread_rng, SeedableRng}; #[test] fn test_is_dust() { @@ -182,4 +199,46 @@ mod test { ); assert!(result); } + + #[test] + #[cfg(feature = "std")] + fn test_shuffle_slice_empty_vec() { + let mut test: Vec = vec![]; + shuffle_slice(&mut test, &mut thread_rng()); + } + + #[test] + #[cfg(feature = "std")] + fn test_shuffle_slice_single_vec() { + let mut test: Vec = vec![0]; + shuffle_slice(&mut test, &mut thread_rng()); + } + + #[test] + fn test_shuffle_slice_duple_vec() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut test: Vec = vec![0, 1]; + shuffle_slice(&mut test, &mut rng); + assert_eq!(test, &[0, 1]); + let seed = [6; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut test: Vec = vec![0, 1]; + shuffle_slice(&mut test, &mut rng); + assert_eq!(test, &[1, 0]); + } + + #[test] + fn test_shuffle_slice_multi_vec() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut test: Vec = vec![0, 1, 2, 4, 5]; + shuffle_slice(&mut test, &mut rng); + assert_eq!(test, &[2, 1, 0, 4, 5]); + let seed = [25; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut test: Vec = vec![0, 1, 2, 4, 5]; + shuffle_slice(&mut test, &mut rng); + assert_eq!(test, &[0, 4, 1, 2, 5]); + } } diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 7303bdcd8..2afc437eb 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -24,6 +24,8 @@ use bitcoin::{ absolute, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Weight, }; +use rand::rngs::StdRng; +use rand::SeedableRng; mod common; use common::*; @@ -907,14 +909,15 @@ fn test_create_tx_absolute_high_fee() { #[test] fn test_create_tx_add_change() { use bdk_wallet::wallet::tx_builder::TxOrdering; - + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); let (mut wallet, _) = get_funded_wallet_wpkh(); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .ordering(TxOrdering::Untouched); - let psbt = builder.finish().unwrap(); + .ordering(TxOrdering::Shuffle); + let psbt = builder.finish_with_aux_rand(&mut rng).unwrap(); let fee = check_fee!(wallet, psbt); assert_eq!(psbt.unsigned_tx.output.len(), 2);