@@ -42,10 +42,13 @@ use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
4242use core:: cell:: RefCell ;
4343use core:: fmt;
4444
45+ use alloc:: sync:: Arc ;
46+
4547use bitcoin:: psbt:: { self , Psbt } ;
4648use bitcoin:: script:: PushBytes ;
4749use 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} ;
5053use rand_core:: RngCore ;
5154
@@ -763,8 +766,10 @@ impl fmt::Display for AddForeignUtxoError {
763766#[ cfg( feature = "std" ) ]
764767impl 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 ) ]
768773pub 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
778801impl 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