Merge bitcoindevkit/bdk#1395: Remove rand dependency from bdk

4bddb0de6262fb4014d51baf8c9453eb45a3d0ef feat(wallet): add back TxBuilder finish() and sort_tx() with thread_rng() (Steve Myers)
45c0cae0a461232bf746375083e2005c5df5f913 fix(bdk): remove rand dependency (rustaceanrob)

Pull request description:

  ### Description

  WIP towards removing `rand` fixes #871

  The `rand` dependency was imported explicitly, but `rand` is also implicitly used through the `rand-std` feature flag on `bitcoin`.

  ### Notes to he reviewers

  **Updated:**

  `rand` was used primarily in two parts of `bdk`. Particularly in signing and in building a transaction.

  Signing:
  - Used implicitly in [`sign_schnorr`](https://docs.rs/bitcoin/latest/bitcoin/key/struct.Secp256k1.html#method.sign_schnorr), but nowhere else within `signer`.

  Transaction ordering:
  - Used to shuffle the inputs and outputs of a transaction, the default
  - Used in the single random draw __as a fallback__ to branch and bound during coin selection. Branch and bound is the default coin selection option.

  See conversation for proposed solutions.

  ### Changelog notice

  - Remove the `rand` dependency from `bdk`

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature

  #### Bugfixes:

  * [x] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  ValuedMammal:
    ACK 4bddb0de6262fb4014d51baf8c9453eb45a3d0ef
  notmandatory:
    ACK 4bddb0de6262fb4014d51baf8c9453eb45a3d0ef

Tree-SHA512: 662d9bcb1e02f8195d73df16789b8c2aba8ccd7b37ba713ebb0bfd19c66163acbcb6f266b64f88347cbb1f96b88c8a150581012cbf818d1dc8b4437b3e53fc62
This commit is contained in:
Steve Myers 2024-06-22 21:21:58 -05:00
commit 6dab68d35b
No known key found for this signature in database
GPG Key ID: 8105A46B22C2D051
11 changed files with 268 additions and 150 deletions

View File

@ -92,7 +92,7 @@ jobs:
uses: Swatinem/rust-cache@v2.2.1 uses: Swatinem/rust-cache@v2.2.1
- name: Check bdk wallet - name: Check bdk wallet
working-directory: ./crates/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 - name: Check esplora
working-directory: ./crates/esplora working-directory: ./crates/esplora
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,async run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,async

View File

@ -1,6 +1,5 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::thread::JoinHandle; use std::thread::JoinHandle;
use std::usize;
use bdk_chain::collections::BTreeMap; use bdk_chain::collections::BTreeMap;
use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult};

View File

@ -13,9 +13,9 @@ edition = "2021"
rust-version = "1.63" rust-version = "1.63"
[dependencies] [dependencies]
rand = "^0.8" rand_core = { version = "0.6.0" }
miniscript = { version = "12.0.0", features = ["serde"], default-features = false } 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 = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" } serde_json = { version = "^1.0" }
bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", "serde"], default-features = false } 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 # Optional dependencies
bip39 = { version = "2.0", optional = true } bip39 = { version = "2.0", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = "0.2"
js-sys = "0.3"
[features] [features]
default = ["std"] default = ["std"]
std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"] std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"]
compiler = ["miniscript/compiler"] compiler = ["miniscript/compiler"]
all-keys = ["keys-bip39"] all-keys = ["keys-bip39"]
keys-bip39 = ["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] [dev-dependencies]
lazy_static = "1.4" lazy_static = "1.4"
assert_matches = "1.5.0" assert_matches = "1.5.0"
@ -46,6 +37,7 @@ tempfile = "3"
bdk_sqlite = { path = "../sqlite" } bdk_sqlite = { path = "../sqlite" }
bdk_file_store = { path = "../file_store" } bdk_file_store = { path = "../file_store" }
anyhow = "1" anyhow = "1"
rand = "^0.8"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@ -70,30 +70,28 @@ To persist `Wallet` state data use a data store crate that reads and writes [`bd
```rust,no_run ```rust,no_run
use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}}; use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}};
fn main() { // Open or create a new file store for wallet data.
// Open or create a new file store for wallet data. let mut db =
let mut db = bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db")
bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db") .expect("create store");
.expect("create store");
// Create a wallet with initial wallet data read from the file store. // Create a wallet with initial wallet data read from the file store.
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)"; let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
let changeset = db.aggregate_changesets().expect("changeset loaded"); let changeset = db.aggregate_changesets().expect("changeset loaded");
let mut wallet = let mut wallet =
Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet) Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet)
.expect("create or load wallet"); .expect("create or load wallet");
// Get a new address to receive bitcoin. // Get a new address to receive bitcoin.
let receive_address = wallet.reveal_next_address(KeychainKind::External); let receive_address = wallet.reveal_next_address(KeychainKind::External);
// Persist staged wallet data changes to the file store. // Persist staged wallet data changes to the file store.
let staged_changeset = wallet.take_staged(); let staged_changeset = wallet.take_staged();
if let Some(changeset) = staged_changeset { if let Some(changeset) = staged_changeset {
db.append_changeset(&changeset) db.append_changeset(&changeset)
.expect("must commit changes to database"); .expect("must commit changes to database");
}
println!("Your new receive address is: {}", receive_address.address);
} }
println!("Your new receive address is: {}", receive_address.address);
``` ```
<!-- ### Sync the balance of a descriptor --> <!-- ### Sync the balance of a descriptor -->

View File

@ -20,6 +20,8 @@ use core::marker::PhantomData;
use core::ops::Deref; use core::ops::Deref;
use core::str::FromStr; use core::str::FromStr;
use rand_core::{CryptoRng, RngCore};
use bitcoin::secp256k1::{self, Secp256k1, Signing}; use bitcoin::secp256k1::{self, Secp256k1, Signing};
use bitcoin::bip32; use bitcoin::bip32;
@ -631,12 +633,23 @@ pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
entropy: Self::Entropy, entropy: Self::Entropy,
) -> Result<GeneratedKey<Self, Ctx>, Self::Error>; ) -> Result<GeneratedKey<Self, Ctx>, 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<GeneratedKey<Self, Ctx>, Self::Error> { fn generate(options: Self::Options) -> Result<GeneratedKey<Self, Ctx>, 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<GeneratedKey<Self, Ctx>, Self::Error> {
let mut entropy = Self::Entropy::default(); let mut entropy = Self::Entropy::default();
thread_rng().fill(entropy.as_mut()); rng.fill_bytes(entropy.as_mut());
Self::generate_with_entropy(options, entropy) Self::generate_with_entropy(options, entropy)
} }
} }
@ -657,8 +670,20 @@ where
} }
/// Generate a key with the default options and a random entropy /// 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<GeneratedKey<Self, Ctx>, Self::Error> { fn generate_default() -> Result<GeneratedKey<Self, Ctx>, 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<GeneratedKey<Self, Ctx>, Self::Error> {
Self::generate_with_aux_rand(Default::default(), rng)
} }
} }

View File

@ -114,8 +114,9 @@ use bitcoin::{Script, Weight};
use core::convert::TryInto; use core::convert::TryInto;
use core::fmt::{self, Formatter}; 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 /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
/// overridden /// overridden
pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection; pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
@ -516,27 +517,16 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
)); ));
} }
Ok(self self.bnb(
.bnb( required_utxos.clone(),
required_utxos.clone(), optional_utxos.clone(),
optional_utxos.clone(), curr_value,
curr_value, curr_available_value,
curr_available_value, target_amount,
target_amount, cost_of_change,
cost_of_change, drain_script,
drain_script, fee_rate,
fee_rate, )
)
.unwrap_or_else(|_| {
self.single_random_draw(
required_utxos,
optional_utxos,
curr_value,
target_amount,
drain_script,
fee_rate,
)
}))
} }
} }
@ -663,40 +653,6 @@ impl BranchAndBoundCoinSelection {
)) ))
} }
#[allow(clippy::too_many_arguments)]
fn single_random_draw(
&self,
required_utxos: Vec<OutputGroup>,
mut optional_utxos: Vec<OutputGroup>,
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( fn calculate_cs_result(
mut selected_utxos: Vec<OutputGroup>, mut selected_utxos: Vec<OutputGroup>,
mut required_utxos: Vec<OutputGroup>, mut required_utxos: Vec<OutputGroup>,
@ -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<WeightedUtxo>,
optional_utxos: Vec<WeightedUtxo>,
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<OutputGroup> = required_utxos
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
.collect();
let mut optional_utxos: Vec<OutputGroup> = 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. /// Remove duplicate UTXOs.
/// ///
/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept. /// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept.
@ -740,6 +748,7 @@ where
mod test { mod test {
use assert_matches::assert_matches; use assert_matches::assert_matches;
use core::str::FromStr; use core::str::FromStr;
use rand::rngs::StdRng;
use bdk_chain::ConfirmationTime; use bdk_chain::ConfirmationTime;
use bitcoin::{Amount, ScriptBuf, TxIn, TxOut}; use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
@ -748,8 +757,7 @@ mod test {
use crate::types::*; use crate::types::*;
use crate::wallet::coin_selection::filter_duplicates; use crate::wallet::coin_selection::filter_duplicates;
use rand::rngs::StdRng; use rand::prelude::SliceRandom;
use rand::seq::SliceRandom;
use rand::{Rng, RngCore, SeedableRng}; use rand::{Rng, RngCore, SeedableRng};
// signature len (1WU) + signature and sighash (72WU) // signature len (1WU) + signature and sighash (72WU)
@ -1090,13 +1098,12 @@ mod test {
} }
#[test] #[test]
#[ignore = "SRD fn was moved out of BnB"]
fn test_bnb_coin_selection_success() { fn test_bnb_coin_selection_success() {
// In this case bnb won't find a suitable match and single random draw will // In this case bnb won't find a suitable match and single random draw will
// select three outputs // select three outputs
let utxos = generate_same_value_utxos(100_000, 20); let utxos = generate_same_value_utxos(100_000, 20);
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 250_000 + FEE_AMOUNT; let target_amount = 250_000 + FEE_AMOUNT;
let result = BranchAndBoundCoinSelection::default() let result = BranchAndBoundCoinSelection::default()
@ -1136,6 +1143,7 @@ mod test {
} }
#[test] #[test]
#[ignore = "no exact match for bnb, previously fell back to SRD"]
fn test_bnb_coin_selection_optional_are_enough() { fn test_bnb_coin_selection_optional_are_enough() {
let utxos = get_test_utxos(); let utxos = get_test_utxos();
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
@ -1156,6 +1164,26 @@ mod test {
assert_eq!(result.fee_amount, 136); 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] #[test]
#[ignore] #[ignore]
fn test_bnb_coin_selection_required_not_enough() { 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<OutputGroup> = 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] #[test]
fn test_bnb_exclude_negative_effective_value() { fn test_bnb_exclude_negative_effective_value() {
let utxos = get_test_utxos(); let utxos = get_test_utxos();

View File

@ -42,6 +42,8 @@ use bitcoin::{constants::genesis_block, Amount};
use core::fmt; use core::fmt;
use core::mem; use core::mem;
use core::ops::Deref; use core::ops::Deref;
use rand_core::RngCore;
use descriptor::error::Error as DescriptorError; use descriptor::error::Error as DescriptorError;
use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
@ -1238,6 +1240,7 @@ impl Wallet {
&mut self, &mut self,
coin_selection: Cs, coin_selection: Cs,
params: TxParams, params: TxParams,
rng: &mut impl RngCore,
) -> Result<Psbt, CreateTxError> { ) -> Result<Psbt, CreateTxError> {
let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect(); let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect();
let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist"); let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist");
@ -1464,13 +1467,31 @@ impl Wallet {
let (required_utxos, optional_utxos) = let (required_utxos, optional_utxos) =
coin_selection::filter_duplicates(required_utxos, optional_utxos); coin_selection::filter_duplicates(required_utxos, optional_utxos);
let coin_selection = coin_selection.coin_select( let coin_selection = match coin_selection.coin_select(
required_utxos, required_utxos.clone(),
optional_utxos, optional_utxos.clone(),
fee_rate, fee_rate,
outgoing.to_sat() + fee_amount, outgoing.to_sat() + fee_amount,
&drain_script, &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; fee_amount += coin_selection.fee_amount;
let excess = &coin_selection.excess; let excess = &coin_selection.excess;
@ -1534,7 +1555,7 @@ impl Wallet {
}; };
// sort input/outputs according to the chosen algorithm // 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)?; let psbt = self.complete_transaction(tx, coin_selection.selected, params)?;
Ok(psbt) Ok(psbt)

View File

@ -607,7 +607,7 @@ fn sign_psbt_schnorr(
}; };
let msg = &Message::from(hash); 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) secp.verify_schnorr(&signature, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
.expect("invalid or corrupted schnorr signature"); .expect("invalid or corrupted schnorr signature");

View File

@ -45,8 +45,10 @@ use core::fmt;
use bitcoin::psbt::{self, Psbt}; use bitcoin::psbt::{self, Psbt};
use bitcoin::script::PushBytes; use bitcoin::script::PushBytes;
use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid}; use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
use rand_core::RngCore;
use super::coin_selection::CoinSelectionAlgorithm; use super::coin_selection::CoinSelectionAlgorithm;
use super::utils::shuffle_slice;
use super::{CreateTxError, Wallet}; use super::{CreateTxError, Wallet};
use crate::collections::{BTreeMap, HashSet}; use crate::collections::{BTreeMap, HashSet};
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
@ -669,16 +671,33 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> { impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> {
/// Finish building the transaction. /// Finish building the transaction.
/// ///
/// Uses the thread-local random number generator (rng).
///
/// Returns a new [`Psbt`] per [`BIP174`]. /// Returns a new [`Psbt`] per [`BIP174`].
/// ///
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki /// [`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 /// **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`]. /// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`].
#[cfg(feature = "std")]
pub fn finish(self) -> Result<Psbt, CreateTxError> { pub fn finish(self) -> Result<Psbt, CreateTxError> {
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<Psbt, CreateTxError> {
self.wallet self.wallet
.borrow_mut() .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 { impl TxOrdering {
/// Sort transaction inputs and outputs by [`TxOrdering`] variant /// Sort transaction inputs and outputs by [`TxOrdering`] variant.
pub fn sort_tx(&self, tx: &mut Transaction) { ///
/// 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 { match self {
TxOrdering::Untouched => {} TxOrdering::Untouched => {}
TxOrdering::Shuffle => { TxOrdering::Shuffle => {
use rand::seq::SliceRandom; shuffle_slice(&mut tx.input, rng);
let mut rng = rand::thread_rng(); shuffle_slice(&mut tx.output, rng);
tx.input.shuffle(&mut rng);
tx.output.shuffle(&mut rng);
} }
TxOrdering::Bip69Lexicographic => { TxOrdering::Bip69Lexicographic => {
tx.input.sort_unstable_by_key(|txin| { tx.input.sort_unstable_by_key(|txin| {
@ -851,12 +878,6 @@ mod test {
use bitcoin::TxOut; use bitcoin::TxOut;
use super::*; use super::*;
#[test]
fn test_output_ordering_default_shuffle() {
assert_eq!(TxOrdering::default(), TxOrdering::Shuffle);
}
#[test] #[test]
fn test_output_ordering_untouched() { fn test_output_ordering_untouched() {
let original_tx = ordering_test_tx!(); let original_tx = ordering_test_tx!();

View File

@ -14,6 +14,8 @@ use bitcoin::{absolute, relative, Script, Sequence};
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey}; use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
use rand_core::RngCore;
/// Trait to check if a value is below the dust limit. /// 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 /// We are performing dust value calculation for a given script public key using rust-bitcoin to
/// keep it compatible with network dust rate /// keep it compatible with network dust rate
@ -110,6 +112,19 @@ impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> 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<T>(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<All>; pub(crate) type SecpCtx = Secp256k1<All>;
#[cfg(test)] #[cfg(test)]
@ -118,9 +133,11 @@ mod test {
// otherwise it's time-based // otherwise it's time-based
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22; 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 crate::bitcoin::{Address, Network, Sequence};
use alloc::vec::Vec;
use core::str::FromStr; use core::str::FromStr;
use rand::{rngs::StdRng, thread_rng, SeedableRng};
#[test] #[test]
fn test_is_dust() { fn test_is_dust() {
@ -182,4 +199,46 @@ mod test {
); );
assert!(result); assert!(result);
} }
#[test]
#[cfg(feature = "std")]
fn test_shuffle_slice_empty_vec() {
let mut test: Vec<u8> = vec![];
shuffle_slice(&mut test, &mut thread_rng());
}
#[test]
#[cfg(feature = "std")]
fn test_shuffle_slice_single_vec() {
let mut test: Vec<u8> = 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<u8> = 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<u8> = 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<u8> = 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<u8> = vec![0, 1, 2, 4, 5];
shuffle_slice(&mut test, &mut rng);
assert_eq!(test, &[0, 4, 1, 2, 5]);
}
} }

View File

@ -24,6 +24,8 @@ use bitcoin::{
absolute, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, absolute, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf,
Sequence, Transaction, TxIn, TxOut, Txid, Weight, Sequence, Transaction, TxIn, TxOut, Txid, Weight,
}; };
use rand::rngs::StdRng;
use rand::SeedableRng;
mod common; mod common;
use common::*; use common::*;
@ -907,14 +909,15 @@ fn test_create_tx_absolute_high_fee() {
#[test] #[test]
fn test_create_tx_add_change() { fn test_create_tx_add_change() {
use bdk_wallet::wallet::tx_builder::TxOrdering; 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 (mut wallet, _) = get_funded_wallet_wpkh();
let addr = wallet.next_unused_address(KeychainKind::External); let addr = wallet.next_unused_address(KeychainKind::External);
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
.ordering(TxOrdering::Untouched); .ordering(TxOrdering::Shuffle);
let psbt = builder.finish().unwrap(); let psbt = builder.finish_with_aux_rand(&mut rng).unwrap();
let fee = check_fee!(wallet, psbt); let fee = check_fee!(wallet, psbt);
assert_eq!(psbt.unsigned_tx.output.len(), 2); assert_eq!(psbt.unsigned_tx.output.len(), 2);