Skip to content

Commit e5cb7b2

Browse files
Yureiennotmandatory
authored andcommitted
feat(wallet): add TxOrdering::Custom
The deterministic sorting of transaction inputs and outputs proposed in BIP 69 doesn't improve the privacy of transactions as originally intended. In the search of not spreading bad practices but still provide flexibility for possible use cases depending on particular order of the inputs/outpus of a transaction, a new TxOrdering variant has been added to allow the implementation of these orders through the definition of comparison functions. Signed-off-by: Steve Myers <steve@notmandatory.org>
1 parent 22368ab commit e5cb7b2

File tree

1 file changed

+145
-4
lines changed

1 file changed

+145
-4
lines changed

crates/wallet/src/wallet/tx_builder.rs

Lines changed: 145 additions & 4 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,8 +766,10 @@ 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]
@@ -773,21 +778,39 @@ pub enum TxOrdering {
773778
Untouched,
774779
/// BIP69 / Lexicographic
775780
Bip69Lexicographic,
781+
/// Provide custom comparison functions for sorting
782+
Custom {
783+
/// Transaction inputs sort function
784+
input_sort: Arc<TxSort<TxIn>>,
785+
/// Transaction outputs sort function
786+
output_sort: Arc<TxSort<TxOut>>,
787+
},
788+
}
789+
790+
impl core::fmt::Debug for TxOrdering {
791+
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
792+
match self {
793+
TxOrdering::Shuffle => write!(f, "Shuffle"),
794+
TxOrdering::Untouched => write!(f, "Untouched"),
795+
TxOrdering::Bip69Lexicographic => write!(f, "Bip69Lexicographic"),
796+
TxOrdering::Custom { .. } => write!(f, "Custom"),
797+
}
798+
}
776799
}
777800

778801
impl TxOrdering {
779802
/// Sort transaction inputs and outputs by [`TxOrdering`] variant.
780803
///
781804
/// Uses the thread-local random number generator (rng).
782805
#[cfg(feature = "std")]
783-
pub fn sort_tx(self, tx: &mut Transaction) {
806+
pub fn sort_tx(&self, tx: &mut Transaction) {
784807
self.sort_tx_with_aux_rand(tx, &mut bitcoin::key::rand::thread_rng())
785808
}
786809

787810
/// Sort transaction inputs and outputs by [`TxOrdering`] variant.
788811
///
789812
/// Uses a provided random number generator (rng).
790-
pub fn sort_tx_with_aux_rand(self, tx: &mut Transaction, rng: &mut impl RngCore) {
813+
pub fn sort_tx_with_aux_rand(&self, tx: &mut Transaction, rng: &mut impl RngCore) {
791814
match self {
792815
TxOrdering::Untouched => {}
793816
TxOrdering::Shuffle => {
@@ -801,6 +824,13 @@ impl TxOrdering {
801824
tx.output
802825
.sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone()));
803826
}
827+
TxOrdering::Custom {
828+
input_sort,
829+
output_sort,
830+
} => {
831+
tx.input.sort_unstable_by(|a, b| input_sort(a, b));
832+
tx.output.sort_unstable_by(|a, b| output_sort(a, b));
833+
}
804834
}
805835
}
806836
}
@@ -948,6 +978,117 @@ mod test {
948978
);
949979
}
950980

981+
#[test]
982+
fn test_output_ordering_custom_but_bip69() {
983+
use core::str::FromStr;
984+
985+
let original_tx = ordering_test_tx!();
986+
let mut tx = original_tx;
987+
988+
let bip69_txin_cmp = |tx_a: &TxIn, tx_b: &TxIn| {
989+
let project_outpoint = |t: &TxIn| (t.previous_output.txid, t.previous_output.vout);
990+
project_outpoint(tx_a).cmp(&project_outpoint(tx_b))
991+
};
992+
993+
let bip69_txout_cmp = |tx_a: &TxOut, tx_b: &TxOut| {
994+
let project_utxo = |t: &TxOut| (t.value, t.script_pubkey.clone());
995+
project_utxo(tx_a).cmp(&project_utxo(tx_b))
996+
};
997+
998+
let custom_bip69_ordering = TxOrdering::Custom {
999+
input_sort: Arc::new(bip69_txin_cmp),
1000+
output_sort: Arc::new(bip69_txout_cmp),
1001+
};
1002+
1003+
custom_bip69_ordering.sort_tx(&mut tx);
1004+
1005+
assert_eq!(
1006+
tx.input[0].previous_output,
1007+
bitcoin::OutPoint::from_str(
1008+
"0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57:5"
1009+
)
1010+
.unwrap()
1011+
);
1012+
assert_eq!(
1013+
tx.input[1].previous_output,
1014+
bitcoin::OutPoint::from_str(
1015+
"0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:0"
1016+
)
1017+
.unwrap()
1018+
);
1019+
assert_eq!(
1020+
tx.input[2].previous_output,
1021+
bitcoin::OutPoint::from_str(
1022+
"0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:1"
1023+
)
1024+
.unwrap()
1025+
);
1026+
1027+
assert_eq!(tx.output[0].value.to_sat(), 800);
1028+
assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA]));
1029+
assert_eq!(
1030+
tx.output[2].script_pubkey,
1031+
ScriptBuf::from(vec![0xAA, 0xEE])
1032+
);
1033+
}
1034+
1035+
#[test]
1036+
fn test_output_ordering_custom_with_sha256() {
1037+
use bitcoin::hashes::{sha256, Hash};
1038+
1039+
let original_tx = ordering_test_tx!();
1040+
let mut tx_1 = original_tx.clone();
1041+
let mut tx_2 = original_tx.clone();
1042+
let shared_secret = "secret_tweak";
1043+
1044+
let hash_txin_with_shared_secret_seed = Arc::new(|tx_a: &TxIn, tx_b: &TxIn| {
1045+
let secret_digest_from_txin = |txin: &TxIn| {
1046+
sha256::Hash::hash(
1047+
&[
1048+
&txin.previous_output.txid.to_raw_hash()[..],
1049+
&txin.previous_output.vout.to_be_bytes(),
1050+
shared_secret.as_bytes(),
1051+
]
1052+
.concat(),
1053+
)
1054+
};
1055+
secret_digest_from_txin(tx_a).cmp(&secret_digest_from_txin(tx_b))
1056+
});
1057+
1058+
let hash_txout_with_shared_secret_seed = Arc::new(|tx_a: &TxOut, tx_b: &TxOut| {
1059+
let secret_digest_from_txout = |txin: &TxOut| {
1060+
sha256::Hash::hash(
1061+
&[
1062+
&txin.value.to_sat().to_be_bytes(),
1063+
&txin.script_pubkey.clone().into_bytes()[..],
1064+
shared_secret.as_bytes(),
1065+
]
1066+
.concat(),
1067+
)
1068+
};
1069+
secret_digest_from_txout(tx_a).cmp(&secret_digest_from_txout(tx_b))
1070+
});
1071+
1072+
let custom_ordering_from_salted_sha256_1 = TxOrdering::Custom {
1073+
input_sort: hash_txin_with_shared_secret_seed.clone(),
1074+
output_sort: hash_txout_with_shared_secret_seed.clone(),
1075+
};
1076+
1077+
let custom_ordering_from_salted_sha256_2 = TxOrdering::Custom {
1078+
input_sort: hash_txin_with_shared_secret_seed,
1079+
output_sort: hash_txout_with_shared_secret_seed,
1080+
};
1081+
1082+
custom_ordering_from_salted_sha256_1.sort_tx(&mut tx_1);
1083+
custom_ordering_from_salted_sha256_2.sort_tx(&mut tx_2);
1084+
1085+
// Check the ordering is consistent between calls
1086+
assert_eq!(tx_1, tx_2);
1087+
// Check transaction order has changed
1088+
assert_ne!(tx_1, original_tx);
1089+
assert_ne!(tx_2, original_tx);
1090+
}
1091+
9511092
fn get_test_utxos() -> Vec<LocalOutput> {
9521093
use bitcoin::hashes::Hash;
9531094

0 commit comments

Comments
 (0)