Skip to content

Commit 139eec7

Browse files
committed
Merge bitcoindevkit#1487: Add support for custom sorting and deprecate BIP69
3bee563 refactor(wallet)!: remove TxOrdering::Bip69Lexicographic (nymius) e5cb7b2 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 bitcoindevkit#534. Resumes from the work in bitcoindevkit#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 bitcoindevkit#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
2 parents a112b4d + 3bee563 commit 139eec7

File tree

2 files changed

+128
-15
lines changed

2 files changed

+128
-15
lines changed

crates/wallet/src/wallet/tx_builder.rs

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,13 @@ use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
4242
use core::cell::RefCell;
4343
use core::fmt;
4444

45+
use alloc::sync::Arc;
46+
4547
use bitcoin::psbt::{self, Psbt};
4648
use bitcoin::script::PushBytes;
4749
use bitcoin::{
48-
absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Weight,
50+
absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid,
51+
Weight,
4952
};
5053
use rand_core::RngCore;
5154

@@ -763,43 +766,60 @@ impl fmt::Display for AddForeignUtxoError {
763766
#[cfg(feature = "std")]
764767
impl std::error::Error for AddForeignUtxoError {}
765768

769+
type TxSort<T> = dyn Fn(&T, &T) -> core::cmp::Ordering;
770+
766771
/// Ordering of the transaction's inputs and outputs
767-
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
772+
#[derive(Clone, Default)]
768773
pub enum TxOrdering {
769774
/// Randomized (default)
770775
#[default]
771776
Shuffle,
772777
/// Unchanged
773778
Untouched,
774-
/// BIP69 / Lexicographic
775-
Bip69Lexicographic,
779+
/// Provide custom comparison functions for sorting
780+
Custom {
781+
/// Transaction inputs sort function
782+
input_sort: Arc<TxSort<TxIn>>,
783+
/// Transaction outputs sort function
784+
output_sort: Arc<TxSort<TxOut>>,
785+
},
786+
}
787+
788+
impl core::fmt::Debug for TxOrdering {
789+
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
790+
match self {
791+
TxOrdering::Shuffle => write!(f, "Shuffle"),
792+
TxOrdering::Untouched => write!(f, "Untouched"),
793+
TxOrdering::Custom { .. } => write!(f, "Custom"),
794+
}
795+
}
776796
}
777797

778798
impl TxOrdering {
779799
/// Sort transaction inputs and outputs by [`TxOrdering`] variant.
780800
///
781801
/// Uses the thread-local random number generator (rng).
782802
#[cfg(feature = "std")]
783-
pub fn sort_tx(self, tx: &mut Transaction) {
803+
pub fn sort_tx(&self, tx: &mut Transaction) {
784804
self.sort_tx_with_aux_rand(tx, &mut bitcoin::key::rand::thread_rng())
785805
}
786806

787807
/// Sort transaction inputs and outputs by [`TxOrdering`] variant.
788808
///
789809
/// Uses a provided random number generator (rng).
790-
pub fn sort_tx_with_aux_rand(self, tx: &mut Transaction, rng: &mut impl RngCore) {
810+
pub fn sort_tx_with_aux_rand(&self, tx: &mut Transaction, rng: &mut impl RngCore) {
791811
match self {
792812
TxOrdering::Untouched => {}
793813
TxOrdering::Shuffle => {
794814
shuffle_slice(&mut tx.input, rng);
795815
shuffle_slice(&mut tx.output, rng);
796816
}
797-
TxOrdering::Bip69Lexicographic => {
798-
tx.input.sort_unstable_by_key(|txin| {
799-
(txin.previous_output.txid, txin.previous_output.vout)
800-
});
801-
tx.output
802-
.sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone()));
817+
TxOrdering::Custom {
818+
input_sort,
819+
output_sort,
820+
} => {
821+
tx.input.sort_unstable_by(|a, b| input_sort(a, b));
822+
tx.output.sort_unstable_by(|a, b| output_sort(a, b));
803823
}
804824
}
805825
}
@@ -910,13 +930,28 @@ mod test {
910930
}
911931

912932
#[test]
913-
fn test_output_ordering_bip69() {
933+
fn test_output_ordering_custom_but_bip69() {
914934
use core::str::FromStr;
915935

916936
let original_tx = ordering_test_tx!();
917937
let mut tx = original_tx;
918938

919-
TxOrdering::Bip69Lexicographic.sort_tx(&mut tx);
939+
let bip69_txin_cmp = |tx_a: &TxIn, tx_b: &TxIn| {
940+
let project_outpoint = |t: &TxIn| (t.previous_output.txid, t.previous_output.vout);
941+
project_outpoint(tx_a).cmp(&project_outpoint(tx_b))
942+
};
943+
944+
let bip69_txout_cmp = |tx_a: &TxOut, tx_b: &TxOut| {
945+
let project_utxo = |t: &TxOut| (t.value, t.script_pubkey.clone());
946+
project_utxo(tx_a).cmp(&project_utxo(tx_b))
947+
};
948+
949+
let custom_bip69_ordering = TxOrdering::Custom {
950+
input_sort: Arc::new(bip69_txin_cmp),
951+
output_sort: Arc::new(bip69_txout_cmp),
952+
};
953+
954+
custom_bip69_ordering.sort_tx(&mut tx);
920955

921956
assert_eq!(
922957
tx.input[0].previous_output,
@@ -948,6 +983,63 @@ mod test {
948983
);
949984
}
950985

986+
#[test]
987+
fn test_output_ordering_custom_with_sha256() {
988+
use bitcoin::hashes::{sha256, Hash};
989+
990+
let original_tx = ordering_test_tx!();
991+
let mut tx_1 = original_tx.clone();
992+
let mut tx_2 = original_tx.clone();
993+
let shared_secret = "secret_tweak";
994+
995+
let hash_txin_with_shared_secret_seed = Arc::new(|tx_a: &TxIn, tx_b: &TxIn| {
996+
let secret_digest_from_txin = |txin: &TxIn| {
997+
sha256::Hash::hash(
998+
&[
999+
&txin.previous_output.txid.to_raw_hash()[..],
1000+
&txin.previous_output.vout.to_be_bytes(),
1001+
shared_secret.as_bytes(),
1002+
]
1003+
.concat(),
1004+
)
1005+
};
1006+
secret_digest_from_txin(tx_a).cmp(&secret_digest_from_txin(tx_b))
1007+
});
1008+
1009+
let hash_txout_with_shared_secret_seed = Arc::new(|tx_a: &TxOut, tx_b: &TxOut| {
1010+
let secret_digest_from_txout = |txin: &TxOut| {
1011+
sha256::Hash::hash(
1012+
&[
1013+
&txin.value.to_sat().to_be_bytes(),
1014+
&txin.script_pubkey.clone().into_bytes()[..],
1015+
shared_secret.as_bytes(),
1016+
]
1017+
.concat(),
1018+
)
1019+
};
1020+
secret_digest_from_txout(tx_a).cmp(&secret_digest_from_txout(tx_b))
1021+
});
1022+
1023+
let custom_ordering_from_salted_sha256_1 = TxOrdering::Custom {
1024+
input_sort: hash_txin_with_shared_secret_seed.clone(),
1025+
output_sort: hash_txout_with_shared_secret_seed.clone(),
1026+
};
1027+
1028+
let custom_ordering_from_salted_sha256_2 = TxOrdering::Custom {
1029+
input_sort: hash_txin_with_shared_secret_seed,
1030+
output_sort: hash_txout_with_shared_secret_seed,
1031+
};
1032+
1033+
custom_ordering_from_salted_sha256_1.sort_tx(&mut tx_1);
1034+
custom_ordering_from_salted_sha256_2.sort_tx(&mut tx_2);
1035+
1036+
// Check the ordering is consistent between calls
1037+
assert_eq!(tx_1, tx_2);
1038+
// Check transaction order has changed
1039+
assert_ne!(tx_1, original_tx);
1040+
assert_ne!(tx_2, original_tx);
1041+
}
1042+
9511043
fn get_test_utxos() -> Vec<LocalOutput> {
9521044
use bitcoin::hashes::Hash;
9531045

crates/wallet/tests/wallet.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
extern crate alloc;
2+
13
use std::path::Path;
24
use std::str::FromStr;
35

@@ -985,13 +987,32 @@ fn test_create_tx_drain_to_dust_amount() {
985987

986988
#[test]
987989
fn test_create_tx_ordering_respected() {
990+
use alloc::sync::Arc;
991+
988992
let (mut wallet, _) = get_funded_wallet_wpkh();
989993
let addr = wallet.next_unused_address(KeychainKind::External);
994+
995+
let bip69_txin_cmp = |tx_a: &TxIn, tx_b: &TxIn| {
996+
let project_outpoint = |t: &TxIn| (t.previous_output.txid, t.previous_output.vout);
997+
project_outpoint(tx_a).cmp(&project_outpoint(tx_b))
998+
};
999+
1000+
let bip69_txout_cmp = |tx_a: &TxOut, tx_b: &TxOut| {
1001+
let project_utxo = |t: &TxOut| (t.value, t.script_pubkey.clone());
1002+
project_utxo(tx_a).cmp(&project_utxo(tx_b))
1003+
};
1004+
1005+
let custom_bip69_ordering = bdk_wallet::wallet::tx_builder::TxOrdering::Custom {
1006+
input_sort: Arc::new(bip69_txin_cmp),
1007+
output_sort: Arc::new(bip69_txout_cmp),
1008+
};
1009+
9901010
let mut builder = wallet.build_tx();
9911011
builder
9921012
.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
9931013
.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000))
994-
.ordering(bdk_wallet::wallet::tx_builder::TxOrdering::Bip69Lexicographic);
1014+
.ordering(custom_bip69_ordering);
1015+
9951016
let psbt = builder.finish().unwrap();
9961017
let fee = check_fee!(wallet, psbt);
9971018

0 commit comments

Comments
 (0)