@@ -1364,24 +1364,26 @@ impl Wallet {
13641364 return Err ( ( ) ) ;
13651365 }
13661366
1367+ // Keep selected wallet inputs unavailable until LDK either broadcasts a transaction
1368+ // spending them or returns them through `DiscardFunding`.
1369+ for txin in unsigned_tx. input . iter ( ) . filter ( |txin| {
1370+ must_spend. iter ( ) . all ( |input| input. outpoint != txin. previous_output )
1371+ } ) {
1372+ locked_wallet. lock_outpoint ( txin. previous_output ) ;
1373+ }
1374+
13671375 let change_output = unsigned_tx
13681376 . output
13691377 . into_iter ( )
13701378 . find ( |txout| must_pay_to. iter ( ) . all ( |output| output != txout) ) ;
1371- let change_set = if change_output. is_some ( ) {
1372- Some ( locked_wallet. take_staged ( ) . unwrap_or_default ( ) )
1373- } else {
1374- None
1375- } ;
1379+ let change_set = locked_wallet. take_staged ( ) . unwrap_or_default ( ) ;
13761380
13771381 ( CoinSelection { confirmed_utxos, change_output } , change_set)
13781382 } ;
13791383
1380- if let Some ( change_set) = change_set {
1381- locked_persister. persist_changeset ( change_set) . await . map_err ( |e| {
1382- log_error ! ( self . logger, "Failed to persist wallet: {}" , e) ;
1383- } ) ?;
1384- }
1384+ locked_persister. persist_changeset ( change_set) . await . map_err ( |e| {
1385+ log_error ! ( self . logger, "Failed to persist wallet: {}" , e) ;
1386+ } ) ?;
13851387
13861388 Ok ( coin_selection)
13871389 }
@@ -2837,7 +2839,7 @@ mod tests {
28372839 use std:: sync:: atomic:: { AtomicBool , Ordering } ;
28382840 use std:: time:: Duration ;
28392841
2840- use bdk_chain:: { BlockId , ConfirmationBlockTime } ;
2842+ use bdk_chain:: { BlockId , CheckPoint , ConfirmationBlockTime , TxUpdate } ;
28412843 use bdk_wallet:: Wallet as BdkWallet ;
28422844 use bitcoin:: hashes:: Hash ;
28432845 use bitcoin:: { Network , TxIn } ;
@@ -3002,6 +3004,132 @@ mod tests {
30023004 ) )
30033005 }
30043006
3007+ #[ tokio:: test]
3008+ async fn splice_coin_selection_locks_inputs_until_cancelled ( ) {
3009+ let store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
3010+ let wallet = new_test_wallet ( Arc :: clone ( & store) , false ) . await ;
3011+ let ( funding_tx, block_id) = {
3012+ let mut locked_wallet = wallet. inner . lock ( ) . unwrap ( ) ;
3013+ let outputs = ( 0 ..2 )
3014+ . map ( |_| TxOut {
3015+ value : Amount :: from_sat ( 100_000 ) ,
3016+ script_pubkey : locked_wallet
3017+ . reveal_next_address ( KeychainKind :: External )
3018+ . address
3019+ . script_pubkey ( ) ,
3020+ } )
3021+ . collect ( ) ;
3022+ let funding_tx = Transaction {
3023+ version : bitcoin:: transaction:: Version :: TWO ,
3024+ lock_time : LockTime :: ZERO ,
3025+ input : Vec :: new ( ) ,
3026+ output : outputs,
3027+ } ;
3028+ let block_id = BlockId {
3029+ height : locked_wallet. latest_checkpoint ( ) . height ( ) + 1 ,
3030+ hash : bitcoin:: BlockHash :: from_byte_array ( [ 42 ; 32 ] ) ,
3031+ } ;
3032+ ( funding_tx, block_id)
3033+ } ;
3034+ let funding_txid = funding_tx. compute_txid ( ) ;
3035+ let mut tx_update = TxUpdate :: default ( ) ;
3036+ tx_update. txs = vec ! [ Arc :: new( funding_tx) ] ;
3037+ tx_update. anchors =
3038+ [ ( ConfirmationBlockTime { block_id, confirmation_time : 1 } , funding_txid) ] . into ( ) ;
3039+ let chain = CheckPoint :: from_block_ids ( [
3040+ wallet. inner . lock ( ) . unwrap ( ) . latest_checkpoint ( ) . block_id ( ) ,
3041+ block_id,
3042+ ] )
3043+ . unwrap ( ) ;
3044+ wallet
3045+ . apply_update ( Update { tx_update, chain : Some ( chain) , ..Default :: default ( ) } )
3046+ . await
3047+ . unwrap ( ) ;
3048+
3049+ let payment = TxOut {
3050+ value : Amount :: from_sat ( 50_000 ) ,
3051+ script_pubkey : ScriptBuf :: new_p2wpkh ( & WPubkeyHash :: from_slice ( & [ 1 ; 20 ] ) . unwrap ( ) ) ,
3052+ } ;
3053+ let fee_rate = FeeRate :: from_sat_per_kwu ( 250 ) ;
3054+ let selection =
3055+ Wallet :: select_confirmed_utxos ( & wallet, Vec :: new ( ) , & [ payment. clone ( ) ] , fee_rate)
3056+ . await
3057+ . unwrap ( ) ;
3058+ let selected_outpoints = selection
3059+ . confirmed_utxos
3060+ . iter ( )
3061+ . cloned ( )
3062+ . map ( ConfirmedUtxo :: into_utxo)
3063+ . map ( |utxo| utxo. outpoint )
3064+ . collect :: < Vec < _ > > ( ) ;
3065+ assert ! ( !selected_outpoints. is_empty( ) ) ;
3066+ assert ! (
3067+ selected_outpoints. iter( ) . all( |outpoint| wallet
3068+ . inner
3069+ . lock( )
3070+ . unwrap( )
3071+ . is_outpoint_locked( * outpoint) ) ,
3072+ "splice coin selection must lock selected wallet inputs" ,
3073+ ) ;
3074+ drop ( wallet) ;
3075+
3076+ let reloaded = new_test_wallet ( Arc :: clone ( & store) , true ) . await ;
3077+ assert ! (
3078+ selected_outpoints. iter( ) . all( |outpoint| reloaded
3079+ . inner
3080+ . lock( )
3081+ . unwrap( )
3082+ . is_outpoint_locked( * outpoint) ) ,
3083+ "splice input locks must survive a wallet reload" ,
3084+ ) ;
3085+ let second_selection =
3086+ Wallet :: select_confirmed_utxos ( & reloaded, Vec :: new ( ) , & [ payment. clone ( ) ] , fee_rate)
3087+ . await
3088+ . unwrap ( ) ;
3089+ let second_outpoints = second_selection
3090+ . confirmed_utxos
3091+ . into_iter ( )
3092+ . map ( ConfirmedUtxo :: into_utxo)
3093+ . map ( |utxo| utxo. outpoint )
3094+ . collect :: < Vec < _ > > ( ) ;
3095+ assert ! (
3096+ selected_outpoints. iter( ) . all( |outpoint| !second_outpoints. contains( outpoint) ) ,
3097+ "subsequent splice coin selection must not reuse locked inputs" ,
3098+ ) ;
3099+
3100+ let cancelled_tx = Transaction {
3101+ version : bitcoin:: transaction:: Version :: TWO ,
3102+ lock_time : LockTime :: ZERO ,
3103+ input : selected_outpoints
3104+ . iter ( )
3105+ . map ( |outpoint| TxIn { previous_output : * outpoint, ..TxIn :: default ( ) } )
3106+ . collect ( ) ,
3107+ output : selection. change_output . into_iter ( ) . collect ( ) ,
3108+ } ;
3109+ reloaded. cancel_tx ( cancelled_tx) . await . unwrap ( ) ;
3110+ drop ( reloaded) ;
3111+ let reloaded = new_test_wallet ( store, true ) . await ;
3112+ assert ! (
3113+ selected_outpoints. iter( ) . all( |outpoint| !reloaded
3114+ . inner
3115+ . lock( )
3116+ . unwrap( )
3117+ . is_outpoint_locked( * outpoint) ) ,
3118+ "discarded splice inputs must be unlocked persistently" ,
3119+ ) ;
3120+ let replacement_selection =
3121+ Wallet :: select_confirmed_utxos ( & reloaded, Vec :: new ( ) , & [ payment] , fee_rate)
3122+ . await
3123+ . unwrap ( ) ;
3124+ let replacement_outpoints = replacement_selection
3125+ . confirmed_utxos
3126+ . into_iter ( )
3127+ . map ( ConfirmedUtxo :: into_utxo)
3128+ . map ( |utxo| utxo. outpoint )
3129+ . collect :: < Vec < _ > > ( ) ;
3130+ assert_eq ! ( replacement_outpoints, selected_outpoints) ;
3131+ }
3132+
30053133 fn pooled_indices ( wallet : & Wallet ) -> Vec < u32 > {
30063134 wallet. address_pool . lock ( ) . unwrap ( ) . available . iter ( ) . map ( |( index, _) | * index) . collect ( )
30073135 }
0 commit comments