66 "fmt"
77 "reflect"
88 "strings"
9+ "sync"
910 "sync/atomic"
1011
1112 "github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -26,10 +27,21 @@ import (
2627)
2728
2829var (
29- // ErrWithdrawingInactiveDeposits is returned when the user tries to
30- // withdraw inactive deposits.
31- ErrWithdrawingInactiveDeposits = errors .New ("deposits to be " +
32- "withdrawn are unknown or inactive" )
30+ // ErrWithdrawingMixedDeposits is returned when a withdrawal is
31+ // requested for deposits in different states.
32+ ErrWithdrawingMixedDeposits = errors .New ("need to withdraw deposits " +
33+ "having the same state, either all deposited or all " +
34+ "withdrawing" )
35+
36+ // ErrDiffPreviousWithdrawalTx signals that the user selected new
37+ // deposits that have different previous withdrawal transactions.
38+ ErrDiffPreviousWithdrawalTx = errors .New ("can't bump fee for " +
39+ "deposits with different previous withdrawal tx hash" )
40+
41+ // ErrMissingPreviousWithdrawn is returned when the user tries to bump
42+ // the fee for a subset of previously selected deposits to withdraw.
43+ ErrMissingPreviousWithdrawn = errors .New ("can't bump fee for subset " +
44+ "of clustered deposits" )
3345
3446 // MinConfs is the minimum number of confirmations we require for a
3547 // deposit to be considered withdrawn.
@@ -92,6 +104,9 @@ type newWithdrawalResponse struct {
92104type Manager struct {
93105 cfg * ManagerConfig
94106
107+ // mu protects access to finalizedWithdrawalTxns.
108+ mu sync.Mutex
109+
95110 // initChan signals the daemon that the withdrawal manager has completed
96111 // its initialization.
97112 initChan chan struct {}
@@ -237,19 +252,19 @@ func (m *Manager) recoverWithdrawals(ctx context.Context) error {
237252 return err
238253 }
239254
240- err = m .publishFinalizedWithdrawalTx (ctx , tx )
255+ _ , err = m .publishFinalizedWithdrawalTx (ctx , tx )
241256 if err != nil {
242257 return err
243258 }
244259
245- err = m .handleWithdrawal (
246- ctx , deposits , tx .TxHash (), tx .TxOut [0 ].PkScript ,
247- )
260+ err = m .handleWithdrawal (ctx , deposits , tx .TxHash ())
248261 if err != nil {
249262 return err
250263 }
251264
265+ m .mu .Lock ()
252266 m .finalizedWithdrawalTxns [tx .TxHash ()] = tx
267+ m .mu .Unlock ()
253268 }
254269
255270 return nil
@@ -274,14 +289,65 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
274289 "withdraw, unconfirmed deposits can't be withdrawn" )
275290 }
276291
292+ var (
293+ deposits []* deposit.Deposit
294+ allDeposited bool
295+ allWithdrawing bool
296+ )
297+
277298 // Ensure that the deposits are in a state in which they can be
278299 // withdrawn.
279- deposits , allActive : = m .cfg .DepositManager .AllOutpointsActiveDeposits (
300+ deposits , allDeposited = m .cfg .DepositManager .AllOutpointsActiveDeposits (
280301 outpoints , deposit .Deposited ,
281302 )
282303
283- if ! allActive {
284- return "" , "" , ErrWithdrawingInactiveDeposits
304+ // If not all passed outpoints are in state Deposited, we'll check if
305+ // they are all in state Withdrawing. If they are, then the user is
306+ // requesting a fee bump, if not we'll return an error as we only allow
307+ // fee bumping deposits in state Withdrawing.
308+ if ! allDeposited {
309+ deposits , allWithdrawing = m .cfg .DepositManager .AllOutpointsActiveDeposits (
310+ outpoints , deposit .Withdrawing ,
311+ )
312+
313+ if ! allWithdrawing {
314+ return "" , "" , ErrWithdrawingMixedDeposits
315+ }
316+
317+ // If republishing of an existing withdrawal is requested we
318+ // ensure that all deposits remain clustered in the context of
319+ // the same withdrawal tx. We do this by checking that they have
320+ // the same previous withdrawal tx hash. This ensures that the
321+ // shape of the transaction stays the same.
322+ hash := deposits [0 ].FinalizedWithdrawalTx .TxHash ()
323+ for i := 1 ; i < len (deposits ); i ++ {
324+ if deposits [i ].FinalizedWithdrawalTx .TxHash () != hash {
325+ return "" , "" , ErrDiffPreviousWithdrawalTx
326+ }
327+ }
328+
329+ // We also avoid that the user selects a subset of previously
330+ // clustered deposits for a fee bump. This would result in a
331+ // different transaction shape.
332+ allDeposits , err := m .cfg .DepositManager .GetActiveDepositsInState (
333+ deposit .Withdrawing ,
334+ )
335+ if err != nil {
336+ return "" , "" , err
337+ }
338+
339+ allDepositsWithHash := make (map [chainhash.Hash ][]* deposit.Deposit )
340+ for _ , d := range allDeposits {
341+ if d .FinalizedWithdrawalTx .TxHash () == hash {
342+ allDepositsWithHash [hash ] = append (
343+ allDepositsWithHash [hash ], d ,
344+ )
345+ }
346+ }
347+
348+ if len (allDepositsWithHash [hash ]) != len (deposits ) {
349+ return "" , "" , ErrMissingPreviousWithdrawn
350+ }
285351 }
286352
287353 var (
@@ -315,6 +381,39 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
315381 return "" , "" , err
316382 }
317383
384+ published , err := m .publishFinalizedWithdrawalTx (ctx , finalizedTx )
385+ if err != nil {
386+ return "" , "" , err
387+ }
388+
389+ if ! published {
390+ return "" , "" , nil
391+ }
392+
393+ // Start a goroutine that listens for the spent of the first deposit
394+ // input of the withdrawal tx. Each fee bump creates a new spent
395+ // notifier, so we can be sure to catch the confirmation of previous
396+ // withdrawal tx versions.
397+ err = m .handleWithdrawal (ctx , deposits , finalizedTx .TxHash ())
398+ if err != nil {
399+ return "" , "" , err
400+ }
401+
402+ // If a previous withdrawal existed across the selected deposits, and
403+ // it isn't the same as the new withdrawal, we remove it from the
404+ // finalized withdrawals to stop republishing it, but we keep the
405+ // previous goroutine in handleWithdrawal running to monitor the
406+ // potential confirmation of the previous withdrawal.
407+ deposits [0 ].Lock ()
408+ prevTx := deposits [0 ].FinalizedWithdrawalTx
409+ deposits [0 ].Unlock ()
410+
411+ if prevTx != nil && prevTx .TxHash () != finalizedTx .TxHash () {
412+ m .mu .Lock ()
413+ delete (m .finalizedWithdrawalTxns , prevTx .TxHash ())
414+ m .mu .Unlock ()
415+ }
416+
318417 // Attach the finalized withdrawal tx to the deposits. After a client
319418 // restart we can use this address as an indicator to republish the
320419 // withdrawal tx and continue the withdrawal.
@@ -325,37 +424,34 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
325424 d .Unlock ()
326425 }
327426
427+ // Add the new withdrawal tx to the finalized withdrawals to republish
428+ // it on block arrivals.
429+ m .mu .Lock ()
430+ m .finalizedWithdrawalTxns [finalizedTx .TxHash ()] = finalizedTx
431+ m .mu .Unlock ()
432+
328433 // Transition the deposits to the withdrawing state. This updates each
329434 // deposits withdrawal address. If a transition fails, we'll return an
330435 // error and abort the withdrawal. An error in transition is likely due
331436 // to an error in the state machine. The already transitioned deposits
332437 // should be reset to the Deposit state after a restart.
438+ // If the user fee bumped a withdrawal this results in a NOOP
439+ // transition.
333440 err = m .cfg .DepositManager .TransitionDeposits (
334441 ctx , deposits , deposit .OnWithdrawInitiated , deposit .Withdrawing ,
335442 )
336443 if err != nil {
337444 return "" , "" , err
338445 }
339446
340- err = m .publishFinalizedWithdrawalTx (ctx , finalizedTx )
341- if err != nil {
342- return "" , "" , err
343- }
344-
345- withdrawalPkScript , err := txscript .PayToAddrScript (withdrawalAddress )
346- if err != nil {
347- return "" , "" , err
348- }
349-
350- err = m .handleWithdrawal (
351- ctx , deposits , finalizedTx .TxHash (), withdrawalPkScript ,
352- )
353- if err != nil {
354- return "" , "" , err
447+ // Update the deposits in the database.
448+ for _ , d := range deposits {
449+ err = m .cfg .DepositManager .UpdateDeposit (ctx , d )
450+ if err != nil {
451+ return "" , "" , err
452+ }
355453 }
356454
357- m .finalizedWithdrawalTxns [finalizedTx .TxHash ()] = finalizedTx
358-
359455 return finalizedTx .TxID (), withdrawalAddress .String (), nil
360456}
361457
@@ -452,44 +548,66 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
452548}
453549
454550func (m * Manager ) publishFinalizedWithdrawalTx (ctx context.Context ,
455- tx * wire.MsgTx ) error {
551+ tx * wire.MsgTx ) ( bool , error ) {
456552
457553 if tx == nil {
458- return errors .New ("can't publish, finalized withdrawal tx is " +
459- "nil" )
554+ return false , errors .New ("can't publish, finalized " +
555+ "withdrawal tx is nil" )
460556 }
461557
462558 txLabel := fmt .Sprintf ("deposit-withdrawal-%v" , tx .TxHash ())
463559
464560 // Publish the withdrawal sweep transaction.
465561 err := m .cfg .WalletKit .PublishTransaction (ctx , tx , txLabel )
466-
467562 if err != nil {
468- if ! strings .Contains (err .Error (), "output already spent" ) {
469- log .Errorf ("%v: %v" , txLabel , err )
563+ if ! strings .Contains (err .Error (), "output already spent" ) &&
564+ ! strings .Contains (err .Error (), "insufficient fee" ) {
565+
566+ return false , err
567+ } else {
568+ return false , nil
470569 }
570+ } else {
571+ log .Debugf ("published deposit withdrawal with txid: %v" ,
572+ tx .TxHash ())
471573 }
472574
473- log .Debugf ("published deposit withdrawal with txid: %v" , tx .TxHash ())
474-
475- return nil
575+ return true , nil
476576}
477577
578+ // handleWithdrawal starts a goroutine that listens for the spent of the first
579+ // input of the withdrawal transaction.
478580func (m * Manager ) handleWithdrawal (ctx context.Context ,
479- deposits []* deposit.Deposit , txHash chainhash.Hash ,
480- withdrawalPkScript []byte ) error {
581+ deposits []* deposit.Deposit , txHash chainhash.Hash ) error {
582+
583+ staticAddress , err := m .cfg .AddressManager .GetStaticAddress (ctx )
584+ if err != nil {
585+ log .Errorf ("error retrieving taproot address %w" , err )
586+
587+ return fmt .Errorf ("withdrawal failed" )
588+ }
481589
482- confChan , errChan , err := m . cfg . ChainNotifier . RegisterConfirmationsNtfn (
483- ctx , & txHash , withdrawalPkScript , MinConfs ,
484- int32 ( m . initiationHeight . Load ()) ,
590+ address , err := btcutil . NewAddressTaproot (
591+ schnorr . SerializePubKey ( staticAddress . TaprootKey ) ,
592+ m . cfg . ChainParams ,
485593 )
486594 if err != nil {
487595 return err
488596 }
489597
598+ script , err := txscript .PayToAddrScript (address )
599+ if err != nil {
600+ return err
601+ }
602+
603+ d := deposits [0 ]
604+ spentChan , errChan , err := m .cfg .ChainNotifier .RegisterSpendNtfn (
605+ ctx , & d .OutPoint , script , int32 (d .ConfirmationHeight ),
606+ )
607+
490608 go func () {
491609 select {
492- case <- confChan :
610+ case <- spentChan :
493611 err = m .cfg .DepositManager .TransitionDeposits (
494612 ctx , deposits , deposit .OnWithdrawn ,
495613 deposit .Withdrawn ,
@@ -499,10 +617,11 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
499617 err )
500618 }
501619
502- // Remove the withdrawal from the active withdrawals and
503- // remove its finalized to stop republishing it on block
504- // arrivals.
620+ // Remove the withdrawal tx from the active withdrawals
621+ // to stop republishing it on block arrivals.
622+ m . mu . Lock ()
505623 delete (m .finalizedWithdrawalTxns , txHash )
624+ m .mu .Unlock ()
506625
507626 case err := <- errChan :
508627 log .Errorf ("Error waiting for confirmation: %v" , err )
@@ -910,13 +1029,20 @@ func (m *Manager) toPrevOuts(deposits []*deposit.Deposit,
9101029}
9111030
9121031func (m * Manager ) republishWithdrawals (ctx context.Context ) error {
913- for _ , finalizedTx := range m .finalizedWithdrawalTxns {
1032+ m .mu .Lock ()
1033+ txns := make ([]* wire.MsgTx , 0 , len (m .finalizedWithdrawalTxns ))
1034+ for _ , tx := range m .finalizedWithdrawalTxns {
1035+ txns = append (txns , tx )
1036+ }
1037+ m .mu .Unlock ()
1038+
1039+ for _ , finalizedTx := range txns {
9141040 if finalizedTx == nil {
9151041 log .Warnf ("Finalized withdrawal tx is nil" )
9161042 continue
9171043 }
9181044
919- err := m .publishFinalizedWithdrawalTx (ctx , finalizedTx )
1045+ _ , err := m .publishFinalizedWithdrawalTx (ctx , finalizedTx )
9201046 if err != nil {
9211047 log .Errorf ("Error republishing withdrawal: %v" , err )
9221048
0 commit comments