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,41 @@ 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+ // If this is the first time this cluster of deposits is withdrawn, we
394+ // start a goroutine that listens for the spent of the first input of
395+ // the withdrawal transaction.
396+ // Since we ensure above that the same ensemble of deposits is
397+ // republished in case of a fee bump, it suffices if only one spent
398+ // notifier is run, since if the first input is spent.
399+ if allDeposited {
400+ err = m .handleWithdrawal (ctx , deposits , finalizedTx .TxHash ())
401+ if err != nil {
402+ return "" , "" , err
403+ }
404+ }
405+
406+ // If a previous withdrawal existed across the selected deposits, and
407+ // it isn't the same as the new withdrawal, we remove it from the
408+ // finalized withdrawals to stop republishing it on block arrivals.
409+ deposits [0 ].Lock ()
410+ prevTx := deposits [0 ].FinalizedWithdrawalTx
411+ deposits [0 ].Unlock ()
412+
413+ if prevTx != nil && prevTx .TxHash () != finalizedTx .TxHash () {
414+ m .mu .Lock ()
415+ delete (m .finalizedWithdrawalTxns , prevTx .TxHash ())
416+ m .mu .Unlock ()
417+ }
418+
318419 // Attach the finalized withdrawal tx to the deposits. After a client
319420 // restart we can use this address as an indicator to republish the
320421 // withdrawal tx and continue the withdrawal.
@@ -325,37 +426,29 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
325426 d .Unlock ()
326427 }
327428
328- // Transition the deposits to the withdrawing state. This updates each
329- // deposits withdrawal address. If a transition fails, we'll return an
330- // error and abort the withdrawal. An error in transition is likely due
331- // to an error in the state machine. The already transitioned deposits
332- // should be reset to the Deposit state after a restart.
429+ // Add the new withdrawal tx to the finalized withdrawals to republish
430+ // it on block arrivals.
431+ m .mu .Lock ()
432+ m .finalizedWithdrawalTxns [finalizedTx .TxHash ()] = finalizedTx
433+ m .mu .Unlock ()
434+
435+ // Transition the deposits to the withdrawing state. If the user fee
436+ // bumped a withdrawal this results in a NOOP transition.
333437 err = m .cfg .DepositManager .TransitionDeposits (
334438 ctx , deposits , deposit .OnWithdrawInitiated , deposit .Withdrawing ,
335439 )
336440 if err != nil {
337441 return "" , "" , err
338442 }
339443
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
444+ // Update the deposits in the database.
445+ for _ , d := range deposits {
446+ err = m .cfg .DepositManager .UpdateDeposit (ctx , d )
447+ if err != nil {
448+ return "" , "" , err
449+ }
355450 }
356451
357- m .finalizedWithdrawalTxns [finalizedTx .TxHash ()] = finalizedTx
358-
359452 return finalizedTx .TxID (), withdrawalAddress .String (), nil
360453}
361454
@@ -452,44 +545,66 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
452545}
453546
454547func (m * Manager ) publishFinalizedWithdrawalTx (ctx context.Context ,
455- tx * wire.MsgTx ) error {
548+ tx * wire.MsgTx ) ( bool , error ) {
456549
457550 if tx == nil {
458- return errors .New ("can't publish, finalized withdrawal tx is " +
459- "nil" )
551+ return false , errors .New ("can't publish, finalized " +
552+ "withdrawal tx is nil" )
460553 }
461554
462555 txLabel := fmt .Sprintf ("deposit-withdrawal-%v" , tx .TxHash ())
463556
464557 // Publish the withdrawal sweep transaction.
465558 err := m .cfg .WalletKit .PublishTransaction (ctx , tx , txLabel )
466-
467559 if err != nil {
468- if ! strings .Contains (err .Error (), "output already spent" ) {
469- log .Errorf ("%v: %v" , txLabel , err )
560+ if ! strings .Contains (err .Error (), "output already spent" ) &&
561+ ! strings .Contains (err .Error (), "insufficient fee" ) {
562+
563+ return false , err
564+ } else {
565+ return false , nil
470566 }
567+ } else {
568+ log .Debugf ("published deposit withdrawal with txid: %v" ,
569+ tx .TxHash ())
471570 }
472571
473- log .Debugf ("published deposit withdrawal with txid: %v" , tx .TxHash ())
474-
475- return nil
572+ return true , nil
476573}
477574
575+ // handleWithdrawal starts a goroutine that listens for the spent of the first
576+ // input of the withdrawal transaction.
478577func (m * Manager ) handleWithdrawal (ctx context.Context ,
479- deposits []* deposit.Deposit , txHash chainhash.Hash ,
480- withdrawalPkScript []byte ) error {
578+ deposits []* deposit.Deposit , txHash chainhash.Hash ) error {
579+
580+ staticAddress , err := m .cfg .AddressManager .GetStaticAddress (ctx )
581+ if err != nil {
582+ log .Errorf ("error retrieving taproot address %w" , err )
583+
584+ return fmt .Errorf ("withdrawal failed" )
585+ }
481586
482- confChan , errChan , err := m . cfg . ChainNotifier . RegisterConfirmationsNtfn (
483- ctx , & txHash , withdrawalPkScript , MinConfs ,
484- int32 ( m . initiationHeight . Load ()) ,
587+ address , err := btcutil . NewAddressTaproot (
588+ schnorr . SerializePubKey ( staticAddress . TaprootKey ) ,
589+ m . cfg . ChainParams ,
485590 )
486591 if err != nil {
487592 return err
488593 }
489594
595+ script , err := txscript .PayToAddrScript (address )
596+ if err != nil {
597+ return err
598+ }
599+
600+ d := deposits [0 ]
601+ spentChan , errChan , err := m .cfg .ChainNotifier .RegisterSpendNtfn (
602+ ctx , & d .OutPoint , script , int32 (d .ConfirmationHeight ),
603+ )
604+
490605 go func () {
491606 select {
492- case <- confChan :
607+ case <- spentChan :
493608 err = m .cfg .DepositManager .TransitionDeposits (
494609 ctx , deposits , deposit .OnWithdrawn ,
495610 deposit .Withdrawn ,
@@ -499,10 +614,11 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
499614 err )
500615 }
501616
502- // Remove the withdrawal from the active withdrawals and
503- // remove its finalized to stop republishing it on block
504- // arrivals.
617+ // Remove the withdrawal tx from the active withdrawals
618+ // to stop republishing it on block arrivals.
619+ m . mu . Lock ()
505620 delete (m .finalizedWithdrawalTxns , txHash )
621+ m .mu .Unlock ()
506622
507623 case err := <- errChan :
508624 log .Errorf ("Error waiting for confirmation: %v" , err )
@@ -910,13 +1026,20 @@ func (m *Manager) toPrevOuts(deposits []*deposit.Deposit,
9101026}
9111027
9121028func (m * Manager ) republishWithdrawals (ctx context.Context ) error {
913- for _ , finalizedTx := range m .finalizedWithdrawalTxns {
1029+ m .mu .Lock ()
1030+ txns := make ([]* wire.MsgTx , 0 , len (m .finalizedWithdrawalTxns ))
1031+ for _ , tx := range m .finalizedWithdrawalTxns {
1032+ txns = append (txns , tx )
1033+ }
1034+ m .mu .Unlock ()
1035+
1036+ for _ , finalizedTx := range txns {
9141037 if finalizedTx == nil {
9151038 log .Warnf ("Finalized withdrawal tx is nil" )
9161039 continue
9171040 }
9181041
919- err := m .publishFinalizedWithdrawalTx (ctx , finalizedTx )
1042+ _ , err := m .publishFinalizedWithdrawalTx (ctx , finalizedTx )
9201043 if err != nil {
9211044 log .Errorf ("Error republishing withdrawal: %v" , err )
9221045
0 commit comments