Merge bitcoindevkit/bdk#1487: Add support for custom sorting and deprecate BIP69

3bee563c810ace941e0e80ee8e410d843a05f5d8 refactor(wallet)!: remove TxOrdering::Bip69Lexicographic (nymius)
e5cb7b206619f5f3437e5ee95ed3e53885b745e6 feat(wallet): add TxOrdering::Custom (FadedCoder)

Pull request description:

  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description

  Resolves https://github.com/bitcoindevkit/bdk/issues/534.

  Resumes from the work in https://github.com/bitcoindevkit/bdk/pull/556.

  Add custom sorting function for inputs and outputs through `TxOrdering::Custom` and deprecates `TxOrdering::Bip69Lexicographic`.

  <!-- Describe the purpose of this PR, what's being adding and/or fixed -->

  ### Notes to the reviewers

  I tried consider all discussions in https://github.com/bitcoindevkit/bdk/issues/534 while implementing some changes to the original PR. I created a summary of the considerations I had while implementing this:

  ##### Why use smart pointers?
  The size of enums and structs should be known at compilation time. A struct whose fields implements some kind of trait cannot be specified without using a smart pointer because the size of the implementations of the trait cannot be known beforehand.

  ##### Why `Arc` or `Rc` instead of `Box`?
  The majority of the useful smart pointers that I know (`Arc`, `Box`, `Rc`) for this case implement `Drop` which rules out the implementation of `Copy`, making harder to manipulate a simple enum like `TxOrdering`. `Clone` can be used instead, implemented by `Arc` and `Rc`, but not implemented by `Box`.

  #####  Why `Arc` instead of `Rc`?
  Multi threading I guess.

  ##### Why using a type alias like `TxVecSort`?
  cargo-clippy was accusing a too complex type if using the whole type inlined in the struct inside the enum.

  ##### Why `Fn` and not `FnMut`?
  `FnMut` is not allowed inside `Arc`. I think this is due to the `&mut self` ocupies the first parameter of the `call` method when desugared (https://rustyyato.github.io/rust/syntactic/sugar/2019/01/17/Closures-Magic-Functions.html), which doesn't respects `Arc` limitation of not having mutable references to data stored inside `Arc`:
  Quoting the [docs](https://doc.rust-lang.org/std/sync/struct.Arc.html):
  > you cannot generally obtain a mutable reference to something inside an `Arc`.

  `FnOnce` > `FnMut` > `Fn`, where `>` stands for "is supertrait of", so, `Fn` can be used everywhere `FnMut` is expected.

  ##### Why not `&'a dyn FnMut`?
  It needs to include a lifetime parameter in `TxOrdering`, which will force the addition of a lifetime parameter in `TxParams`, which will require the addition of a lifetime parameter in a lot of places more. **Which one is preferable?**

  <!-- In this section you can include notes directed to the reviewers, like explaining why some parts
  of the PR were done in a specific way -->

  ### Changelog notice

  - Adds new `TxOrdering` variant: `TxOrdering::Custom`. A structure that stores the ordering functions to sort the inputs and outputs of a transaction.
  - Deprecates `TxOrdering::Bip69Lexicographic`.

  <!-- Notice the release manager should include in the release tag message changelog -->
  <!-- See https://keepachangelog.com/en/1.0.0/ for examples -->

  ### Checklists

  #### All Submissions:

  * [ ] 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
  * [ ] I've added docs for the new feature

Top commit has no ACKs.

Tree-SHA512: 0d3e3ea9aee3a6c9e9d5e1ae93215be84bd1bd99907a319976517819aeda768a7166860a48a8d24abb30c516e0129decb6a6aebd8f24783ea2230143e6dcd72a
This commit is contained in:
Steve Myers 2024-07-05 15:20:50 -05:00
commit 139eec7da0
No known key found for this signature in database
GPG Key ID: 8105A46B22C2D051
2 changed files with 128 additions and 15 deletions

View File

@ -42,10 +42,13 @@ use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use core::cell::RefCell; use core::cell::RefCell;
use core::fmt; use core::fmt;
use alloc::sync::Arc;
use bitcoin::psbt::{self, Psbt}; use bitcoin::psbt::{self, Psbt};
use bitcoin::script::PushBytes; use bitcoin::script::PushBytes;
use bitcoin::{ use bitcoin::{
absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Weight, absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid,
Weight,
}; };
use rand_core::RngCore; use rand_core::RngCore;
@ -763,16 +766,33 @@ impl fmt::Display for AddForeignUtxoError {
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl std::error::Error for AddForeignUtxoError {} impl std::error::Error for AddForeignUtxoError {}
type TxSort<T> = dyn Fn(&T, &T) -> core::cmp::Ordering;
/// Ordering of the transaction's inputs and outputs /// Ordering of the transaction's inputs and outputs
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] #[derive(Clone, Default)]
pub enum TxOrdering { pub enum TxOrdering {
/// Randomized (default) /// Randomized (default)
#[default] #[default]
Shuffle, Shuffle,
/// Unchanged /// Unchanged
Untouched, Untouched,
/// BIP69 / Lexicographic /// Provide custom comparison functions for sorting
Bip69Lexicographic, Custom {
/// Transaction inputs sort function
input_sort: Arc<TxSort<TxIn>>,
/// Transaction outputs sort function
output_sort: Arc<TxSort<TxOut>>,
},
}
impl core::fmt::Debug for TxOrdering {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
TxOrdering::Shuffle => write!(f, "Shuffle"),
TxOrdering::Untouched => write!(f, "Untouched"),
TxOrdering::Custom { .. } => write!(f, "Custom"),
}
}
} }
impl TxOrdering { impl TxOrdering {
@ -780,26 +800,26 @@ impl TxOrdering {
/// ///
/// Uses the thread-local random number generator (rng). /// Uses the thread-local random number generator (rng).
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub fn sort_tx(self, tx: &mut Transaction) { pub fn sort_tx(&self, tx: &mut Transaction) {
self.sort_tx_with_aux_rand(tx, &mut bitcoin::key::rand::thread_rng()) self.sort_tx_with_aux_rand(tx, &mut bitcoin::key::rand::thread_rng())
} }
/// Sort transaction inputs and outputs by [`TxOrdering`] variant. /// Sort transaction inputs and outputs by [`TxOrdering`] variant.
/// ///
/// Uses a provided random number generator (rng). /// Uses a provided random number generator (rng).
pub fn sort_tx_with_aux_rand(self, tx: &mut Transaction, rng: &mut impl RngCore) { 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 => {
shuffle_slice(&mut tx.input, rng); shuffle_slice(&mut tx.input, rng);
shuffle_slice(&mut tx.output, rng); shuffle_slice(&mut tx.output, rng);
} }
TxOrdering::Bip69Lexicographic => { TxOrdering::Custom {
tx.input.sort_unstable_by_key(|txin| { input_sort,
(txin.previous_output.txid, txin.previous_output.vout) output_sort,
}); } => {
tx.output tx.input.sort_unstable_by(|a, b| input_sort(a, b));
.sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone())); tx.output.sort_unstable_by(|a, b| output_sort(a, b));
} }
} }
} }
@ -910,13 +930,28 @@ mod test {
} }
#[test] #[test]
fn test_output_ordering_bip69() { fn test_output_ordering_custom_but_bip69() {
use core::str::FromStr; use core::str::FromStr;
let original_tx = ordering_test_tx!(); let original_tx = ordering_test_tx!();
let mut tx = original_tx; let mut tx = original_tx;
TxOrdering::Bip69Lexicographic.sort_tx(&mut tx); let bip69_txin_cmp = |tx_a: &TxIn, tx_b: &TxIn| {
let project_outpoint = |t: &TxIn| (t.previous_output.txid, t.previous_output.vout);
project_outpoint(tx_a).cmp(&project_outpoint(tx_b))
};
let bip69_txout_cmp = |tx_a: &TxOut, tx_b: &TxOut| {
let project_utxo = |t: &TxOut| (t.value, t.script_pubkey.clone());
project_utxo(tx_a).cmp(&project_utxo(tx_b))
};
let custom_bip69_ordering = TxOrdering::Custom {
input_sort: Arc::new(bip69_txin_cmp),
output_sort: Arc::new(bip69_txout_cmp),
};
custom_bip69_ordering.sort_tx(&mut tx);
assert_eq!( assert_eq!(
tx.input[0].previous_output, tx.input[0].previous_output,
@ -948,6 +983,63 @@ mod test {
); );
} }
#[test]
fn test_output_ordering_custom_with_sha256() {
use bitcoin::hashes::{sha256, Hash};
let original_tx = ordering_test_tx!();
let mut tx_1 = original_tx.clone();
let mut tx_2 = original_tx.clone();
let shared_secret = "secret_tweak";
let hash_txin_with_shared_secret_seed = Arc::new(|tx_a: &TxIn, tx_b: &TxIn| {
let secret_digest_from_txin = |txin: &TxIn| {
sha256::Hash::hash(
&[
&txin.previous_output.txid.to_raw_hash()[..],
&txin.previous_output.vout.to_be_bytes(),
shared_secret.as_bytes(),
]
.concat(),
)
};
secret_digest_from_txin(tx_a).cmp(&secret_digest_from_txin(tx_b))
});
let hash_txout_with_shared_secret_seed = Arc::new(|tx_a: &TxOut, tx_b: &TxOut| {
let secret_digest_from_txout = |txin: &TxOut| {
sha256::Hash::hash(
&[
&txin.value.to_sat().to_be_bytes(),
&txin.script_pubkey.clone().into_bytes()[..],
shared_secret.as_bytes(),
]
.concat(),
)
};
secret_digest_from_txout(tx_a).cmp(&secret_digest_from_txout(tx_b))
});
let custom_ordering_from_salted_sha256_1 = TxOrdering::Custom {
input_sort: hash_txin_with_shared_secret_seed.clone(),
output_sort: hash_txout_with_shared_secret_seed.clone(),
};
let custom_ordering_from_salted_sha256_2 = TxOrdering::Custom {
input_sort: hash_txin_with_shared_secret_seed,
output_sort: hash_txout_with_shared_secret_seed,
};
custom_ordering_from_salted_sha256_1.sort_tx(&mut tx_1);
custom_ordering_from_salted_sha256_2.sort_tx(&mut tx_2);
// Check the ordering is consistent between calls
assert_eq!(tx_1, tx_2);
// Check transaction order has changed
assert_ne!(tx_1, original_tx);
assert_ne!(tx_2, original_tx);
}
fn get_test_utxos() -> Vec<LocalOutput> { fn get_test_utxos() -> Vec<LocalOutput> {
use bitcoin::hashes::Hash; use bitcoin::hashes::Hash;

View File

@ -1,3 +1,5 @@
extern crate alloc;
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
@ -985,13 +987,32 @@ fn test_create_tx_drain_to_dust_amount() {
#[test] #[test]
fn test_create_tx_ordering_respected() { fn test_create_tx_ordering_respected() {
use alloc::sync::Arc;
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 bip69_txin_cmp = |tx_a: &TxIn, tx_b: &TxIn| {
let project_outpoint = |t: &TxIn| (t.previous_output.txid, t.previous_output.vout);
project_outpoint(tx_a).cmp(&project_outpoint(tx_b))
};
let bip69_txout_cmp = |tx_a: &TxOut, tx_b: &TxOut| {
let project_utxo = |t: &TxOut| (t.value, t.script_pubkey.clone());
project_utxo(tx_a).cmp(&project_utxo(tx_b))
};
let custom_bip69_ordering = bdk_wallet::wallet::tx_builder::TxOrdering::Custom {
input_sort: Arc::new(bip69_txin_cmp),
output_sort: Arc::new(bip69_txout_cmp),
};
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)) .add_recipient(addr.script_pubkey(), Amount::from_sat(10_000))
.ordering(bdk_wallet::wallet::tx_builder::TxOrdering::Bip69Lexicographic); .ordering(custom_bip69_ordering);
let psbt = builder.finish().unwrap(); let psbt = builder.finish().unwrap();
let fee = check_fee!(wallet, psbt); let fee = check_fee!(wallet, psbt);