@@ -1371,24 +1371,26 @@ impl Wallet {
13711371 return Err ( ( ) ) ;
13721372 }
13731373
1374+ // Keep selected wallet inputs unavailable until LDK either broadcasts a transaction
1375+ // spending them or returns them through `DiscardFunding`.
1376+ for txin in unsigned_tx. input . iter ( ) . filter ( |txin| {
1377+ must_spend. iter ( ) . all ( |input| input. outpoint != txin. previous_output )
1378+ } ) {
1379+ locked_wallet. lock_outpoint ( txin. previous_output ) ;
1380+ }
1381+
13741382 let change_output = unsigned_tx
13751383 . output
13761384 . into_iter ( )
13771385 . find ( |txout| must_pay_to. iter ( ) . all ( |output| output != txout) ) ;
1378- let change_set = if change_output. is_some ( ) {
1379- Some ( locked_wallet. take_staged ( ) . unwrap_or_default ( ) )
1380- } else {
1381- None
1382- } ;
1386+ let change_set = locked_wallet. take_staged ( ) . unwrap_or_default ( ) ;
13831387
13841388 ( CoinSelection { confirmed_utxos, change_output } , change_set)
13851389 } ;
13861390
1387- if let Some ( change_set) = change_set {
1388- locked_persister. persist_changeset ( change_set) . await . map_err ( |e| {
1389- log_error ! ( self . logger, "Failed to persist wallet: {}" , e) ;
1390- } ) ?;
1391- }
1391+ locked_persister. persist_changeset ( change_set) . await . map_err ( |e| {
1392+ log_error ! ( self . logger, "Failed to persist wallet: {}" , e) ;
1393+ } ) ?;
13921394
13931395 Ok ( coin_selection)
13941396 }
@@ -2849,7 +2851,7 @@ mod tests {
28492851 use std:: sync:: atomic:: { AtomicBool , Ordering } ;
28502852 use std:: time:: Duration ;
28512853
2852- use bdk_chain:: { BlockId , ConfirmationBlockTime } ;
2854+ use bdk_chain:: { BlockId , CheckPoint , ConfirmationBlockTime , TxUpdate } ;
28532855 use bdk_wallet:: Wallet as BdkWallet ;
28542856 use bitcoin:: hashes:: Hash ;
28552857 use bitcoin:: { Network , TxIn } ;
@@ -3016,6 +3018,132 @@ mod tests {
30163018 ) )
30173019 }
30183020
3021+ #[ tokio:: test]
3022+ async fn splice_coin_selection_locks_inputs_until_cancelled ( ) {
3023+ let store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
3024+ let wallet = new_test_wallet ( Arc :: clone ( & store) , false ) . await ;
3025+ let ( funding_tx, block_id) = {
3026+ let mut locked_wallet = wallet. inner . lock ( ) . unwrap ( ) ;
3027+ let outputs = ( 0 ..2 )
3028+ . map ( |_| TxOut {
3029+ value : Amount :: from_sat ( 100_000 ) ,
3030+ script_pubkey : locked_wallet
3031+ . reveal_next_address ( KeychainKind :: External )
3032+ . address
3033+ . script_pubkey ( ) ,
3034+ } )
3035+ . collect ( ) ;
3036+ let funding_tx = Transaction {
3037+ version : bitcoin:: transaction:: Version :: TWO ,
3038+ lock_time : LockTime :: ZERO ,
3039+ input : Vec :: new ( ) ,
3040+ output : outputs,
3041+ } ;
3042+ let block_id = BlockId {
3043+ height : locked_wallet. latest_checkpoint ( ) . height ( ) + 1 ,
3044+ hash : bitcoin:: BlockHash :: from_byte_array ( [ 42 ; 32 ] ) ,
3045+ } ;
3046+ ( funding_tx, block_id)
3047+ } ;
3048+ let funding_txid = funding_tx. compute_txid ( ) ;
3049+ let mut tx_update = TxUpdate :: default ( ) ;
3050+ tx_update. txs = vec ! [ Arc :: new( funding_tx) ] ;
3051+ tx_update. anchors =
3052+ [ ( ConfirmationBlockTime { block_id, confirmation_time : 1 } , funding_txid) ] . into ( ) ;
3053+ let chain = CheckPoint :: from_block_ids ( [
3054+ wallet. inner . lock ( ) . unwrap ( ) . latest_checkpoint ( ) . block_id ( ) ,
3055+ block_id,
3056+ ] )
3057+ . unwrap ( ) ;
3058+ wallet
3059+ . apply_update ( Update { tx_update, chain : Some ( chain) , ..Default :: default ( ) } )
3060+ . await
3061+ . unwrap ( ) ;
3062+
3063+ let payment = TxOut {
3064+ value : Amount :: from_sat ( 50_000 ) ,
3065+ script_pubkey : ScriptBuf :: new_p2wpkh ( & WPubkeyHash :: from_slice ( & [ 1 ; 20 ] ) . unwrap ( ) ) ,
3066+ } ;
3067+ let fee_rate = FeeRate :: from_sat_per_kwu ( 250 ) ;
3068+ let selection =
3069+ Wallet :: select_confirmed_utxos ( & wallet, Vec :: new ( ) , & [ payment. clone ( ) ] , fee_rate)
3070+ . await
3071+ . unwrap ( ) ;
3072+ let selected_outpoints = selection
3073+ . confirmed_utxos
3074+ . iter ( )
3075+ . cloned ( )
3076+ . map ( ConfirmedUtxo :: into_utxo)
3077+ . map ( |utxo| utxo. outpoint )
3078+ . collect :: < Vec < _ > > ( ) ;
3079+ assert ! ( !selected_outpoints. is_empty( ) ) ;
3080+ assert ! (
3081+ selected_outpoints. iter( ) . all( |outpoint| wallet
3082+ . inner
3083+ . lock( )
3084+ . unwrap( )
3085+ . is_outpoint_locked( * outpoint) ) ,
3086+ "splice coin selection must lock selected wallet inputs" ,
3087+ ) ;
3088+ drop ( wallet) ;
3089+
3090+ let reloaded = new_test_wallet ( Arc :: clone ( & store) , true ) . await ;
3091+ assert ! (
3092+ selected_outpoints. iter( ) . all( |outpoint| reloaded
3093+ . inner
3094+ . lock( )
3095+ . unwrap( )
3096+ . is_outpoint_locked( * outpoint) ) ,
3097+ "splice input locks must survive a wallet reload" ,
3098+ ) ;
3099+ let second_selection =
3100+ Wallet :: select_confirmed_utxos ( & reloaded, Vec :: new ( ) , & [ payment. clone ( ) ] , fee_rate)
3101+ . await
3102+ . unwrap ( ) ;
3103+ let second_outpoints = second_selection
3104+ . confirmed_utxos
3105+ . into_iter ( )
3106+ . map ( ConfirmedUtxo :: into_utxo)
3107+ . map ( |utxo| utxo. outpoint )
3108+ . collect :: < Vec < _ > > ( ) ;
3109+ assert ! (
3110+ selected_outpoints. iter( ) . all( |outpoint| !second_outpoints. contains( outpoint) ) ,
3111+ "subsequent splice coin selection must not reuse locked inputs" ,
3112+ ) ;
3113+
3114+ let cancelled_tx = Transaction {
3115+ version : bitcoin:: transaction:: Version :: TWO ,
3116+ lock_time : LockTime :: ZERO ,
3117+ input : selected_outpoints
3118+ . iter ( )
3119+ . map ( |outpoint| TxIn { previous_output : * outpoint, ..TxIn :: default ( ) } )
3120+ . collect ( ) ,
3121+ output : selection. change_output . into_iter ( ) . collect ( ) ,
3122+ } ;
3123+ reloaded. cancel_tx ( cancelled_tx) . await . unwrap ( ) ;
3124+ drop ( reloaded) ;
3125+ let reloaded = new_test_wallet ( store, true ) . await ;
3126+ assert ! (
3127+ selected_outpoints. iter( ) . all( |outpoint| !reloaded
3128+ . inner
3129+ . lock( )
3130+ . unwrap( )
3131+ . is_outpoint_locked( * outpoint) ) ,
3132+ "discarded splice inputs must be unlocked persistently" ,
3133+ ) ;
3134+ let replacement_selection =
3135+ Wallet :: select_confirmed_utxos ( & reloaded, Vec :: new ( ) , & [ payment] , fee_rate)
3136+ . await
3137+ . unwrap ( ) ;
3138+ let replacement_outpoints = replacement_selection
3139+ . confirmed_utxos
3140+ . into_iter ( )
3141+ . map ( ConfirmedUtxo :: into_utxo)
3142+ . map ( |utxo| utxo. outpoint )
3143+ . collect :: < Vec < _ > > ( ) ;
3144+ assert_eq ! ( replacement_outpoints, selected_outpoints) ;
3145+ }
3146+
30193147 fn pooled_indices ( wallet : & Wallet ) -> Vec < u32 > {
30203148 wallet. address_pool . lock ( ) . unwrap ( ) . available . iter ( ) . map ( |( index, _) | * index) . collect ( )
30213149 }
0 commit comments