Skip to content

Commit 49e8c1d

Browse files
committed
staticaddr: allow rbf'ing withdrawal transactions
1 parent d2c08bb commit 49e8c1d

File tree

2 files changed

+122
-41
lines changed

2 files changed

+122
-41
lines changed

staticaddr/deposit/fsm.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,9 @@ func (f *FSM) DepositStatesV0() fsm.States {
289289
Withdrawing: fsm.State{
290290
Transitions: fsm.Transitions{
291291
OnWithdrawn: Withdrawn,
292+
// OnWithdrawInitiated is sent if a fee bump was
293+
// requested and the withdrawal was republished.
294+
OnWithdrawInitiated: Withdrawing,
292295
// Upon recovery, we go back to the Deposited
293296
// state. The deposit by then has a withdrawal
294297
// address stamped to it which will cause it to

staticaddr/withdraw/manager.go

Lines changed: 119 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ import (
2525
)
2626

2727
var (
28-
// ErrWithdrawingInactiveDeposits is returned when the user tries to
29-
// withdraw inactive deposits.
30-
ErrWithdrawingInactiveDeposits = errors.New("deposits to be " +
31-
"withdrawn are unknown or inactive")
28+
// ErrWithdrawingMixedDeposits is returned when a withdrawal is
29+
// requested for deposits in different states.
30+
ErrWithdrawingMixedDeposits = errors.New("need to withdraw deposits " +
31+
"having the same state, either all deposited or all " +
32+
"withdrawing")
3233

3334
// MinConfs is the minimum number of confirmations we require for a
3435
// deposit to be considered withdrawn.
@@ -111,17 +112,25 @@ type Manager struct {
111112
// finalizedWithdrawalTx are the finalized withdrawal transactions that
112113
// are published to the network and re-published on block arrivals.
113114
finalizedWithdrawalTxns map[chainhash.Hash]*wire.MsgTx
115+
116+
// withdrawalHandlerQuitChans is a map of quit channels for each
117+
// withdrawal transaction. The quit channels are used to stop the
118+
// withdrawal handler for a specific withdrawal transaction, e.g. if
119+
// a new rbf'd transaction has to be monitored for confirmation in
120+
// favor of the previous one.
121+
withdrawalHandlerQuitChans map[chainhash.Hash]chan struct{}
114122
}
115123

116124
// NewManager creates a new deposit withdrawal manager.
117125
func NewManager(cfg *ManagerConfig) *Manager {
118126
return &Manager{
119-
cfg: cfg,
120-
initChan: make(chan struct{}),
121-
finalizedWithdrawalTxns: make(map[chainhash.Hash]*wire.MsgTx),
122-
exitChan: make(chan struct{}),
123-
newWithdrawalRequestChan: make(chan newWithdrawalRequest),
124-
errChan: make(chan error),
127+
cfg: cfg,
128+
initChan: make(chan struct{}),
129+
finalizedWithdrawalTxns: make(map[chainhash.Hash]*wire.MsgTx),
130+
exitChan: make(chan struct{}),
131+
newWithdrawalRequestChan: make(chan newWithdrawalRequest),
132+
errChan: make(chan error),
133+
withdrawalHandlerQuitChans: make(map[chainhash.Hash]chan struct{}),
125134
}
126135
}
127136

@@ -235,7 +244,7 @@ func (m *Manager) recoverWithdrawals(ctx context.Context) error {
235244
return err
236245
}
237246

238-
err = m.publishFinalizedWithdrawalTx(ctx, tx)
247+
_, err := m.publishFinalizedWithdrawalTx(ctx, tx)
239248
if err != nil {
240249
return err
241250
}
@@ -274,12 +283,38 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
274283

275284
// Ensure that the deposits are in a state in which they can be
276285
// withdrawn.
277-
deposits, allActive := m.cfg.DepositManager.AllOutpointsActiveDeposits(
286+
deposits, allDeposited := m.cfg.DepositManager.AllOutpointsActiveDeposits(
278287
outpoints, deposit.Deposited,
279288
)
280289

281-
if !allActive {
282-
return "", "", ErrWithdrawingInactiveDeposits
290+
// If not all passed outpoints are in state Deposited, we'll check if
291+
// they are all in state Withdrawing. If they are, then the user is
292+
// requesting a fee bump, if not we'll return an error as we only allow
293+
// fee bumping deposits in state Withdrawing.
294+
if !allDeposited {
295+
deposits, allWithdrawing := m.cfg.DepositManager.AllOutpointsActiveDeposits(
296+
outpoints, deposit.Withdrawing,
297+
)
298+
299+
if !allWithdrawing {
300+
return "", "", ErrWithdrawingMixedDeposits
301+
}
302+
303+
// If a republishing of an existing withdrawal is requested we
304+
// ensure that all deposits remain clustered in the context of
305+
// the same withdrawal by checking if they have the same
306+
// previous withdrawal tx hash.
307+
// This ensures that the shape of the transaction stays the
308+
// same.
309+
hash := deposits[0].FinalizedWithdrawalTx.TxHash()
310+
for i := 1; i < len(deposits); i++ {
311+
if deposits[i].FinalizedWithdrawalTx.TxHash() != hash {
312+
return "", "", fmt.Errorf("can't bump fee " +
313+
"for deposits with different " +
314+
"previous withdrawal tx hash")
315+
}
316+
}
317+
283318
}
284319

285320
var (
@@ -313,6 +348,41 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
313348
return "", "", err
314349
}
315350

351+
published, err := m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
352+
if err != nil {
353+
return "", "", err
354+
}
355+
356+
if !published {
357+
return "", "", nil
358+
}
359+
360+
withdrawalPkScript, err := txscript.PayToAddrScript(withdrawalAddress)
361+
if err != nil {
362+
return "", "", err
363+
}
364+
365+
err = m.handleWithdrawal(
366+
ctx, deposits, finalizedTx.TxHash(), withdrawalPkScript,
367+
)
368+
if err != nil {
369+
return "", "", err
370+
}
371+
372+
// If a previous withdrawal existed across the selected deposits, and
373+
// it isn't the same as the new withdrawal, we'll stop monitoring the
374+
// previous withdrawal and remove it from the finalized withdrawals.
375+
deposits[0].Lock()
376+
prevTx := deposits[0].FinalizedWithdrawalTx
377+
deposits[0].Unlock()
378+
379+
if prevTx != nil && prevTx.TxHash() != finalizedTx.TxHash() {
380+
quitChan := m.withdrawalHandlerQuitChans[prevTx.TxHash()]
381+
close(quitChan)
382+
delete(m.withdrawalHandlerQuitChans, prevTx.TxHash())
383+
delete(m.finalizedWithdrawalTxns, prevTx.TxHash())
384+
}
385+
316386
// Attach the finalized withdrawal tx to the deposits. After a client
317387
// restart we can use this address as an indicator to republish the
318388
// withdrawal tx and continue the withdrawal.
@@ -323,6 +393,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
323393
d.Unlock()
324394
}
325395

396+
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
397+
326398
// Transition the deposits to the withdrawing state. This updates each
327399
// deposits withdrawal address. If a transition fails, we'll return an
328400
// error and abort the withdrawal. An error in transition is likely due
@@ -335,25 +407,14 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
335407
return "", "", err
336408
}
337409

338-
err = m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
339-
if err != nil {
340-
return "", "", err
341-
}
342-
343-
withdrawalPkScript, err := txscript.PayToAddrScript(withdrawalAddress)
344-
if err != nil {
345-
return "", "", err
346-
}
347-
348-
err = m.handleWithdrawal(
349-
ctx, deposits, finalizedTx.TxHash(), withdrawalPkScript,
350-
)
351-
if err != nil {
352-
return "", "", err
410+
// Update the deposits in the database.
411+
for _, d := range deposits {
412+
err = m.cfg.DepositManager.UpdateDeposit(ctx, d)
413+
if err != nil {
414+
return "", "", err
415+
}
353416
}
354417

355-
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
356-
357418
return finalizedTx.TxID(), withdrawalAddress.String(), nil
358419
}
359420

@@ -450,27 +511,31 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
450511
}
451512

452513
func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
453-
tx *wire.MsgTx) error {
514+
tx *wire.MsgTx) (bool, error) {
454515

455516
if tx == nil {
456-
return errors.New("can't publish, finalized withdrawal tx is " +
457-
"nil")
517+
return false, errors.New("can't publish, finalized " +
518+
"withdrawal tx is nil")
458519
}
459520

460521
txLabel := fmt.Sprintf("deposit-withdrawal-%v", tx.TxHash())
461522

462523
// Publish the withdrawal sweep transaction.
463524
err := m.cfg.WalletKit.PublishTransaction(ctx, tx, txLabel)
464-
465525
if err != nil {
466-
if !strings.Contains(err.Error(), "output already spent") {
467-
log.Errorf("%v: %v", txLabel, err)
526+
if !strings.Contains(err.Error(), "output already spent") &&
527+
!strings.Contains(err.Error(), "insufficient fee") {
528+
529+
return false, err
530+
} else {
531+
return false, nil
468532
}
533+
} else {
534+
log.Debugf("published deposit withdrawal with txid: %v",
535+
tx.TxHash())
469536
}
470537

471-
log.Debugf("published deposit withdrawal with txid: %v", tx.TxHash())
472-
473-
return nil
538+
return true, nil
474539
}
475540

476541
func (m *Manager) handleWithdrawal(ctx context.Context,
@@ -485,6 +550,13 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
485550
return err
486551
}
487552

553+
// Create a new quit chan for this set of deposits under the same
554+
// withdrawal tx hash. If a new withdrawal is requested the quit chan
555+
// is closed in favor of a new one, to start monitoring the new
556+
// withdrawal transaction.
557+
m.withdrawalHandlerQuitChans[txHash] = make(chan struct{})
558+
quitChan := m.withdrawalHandlerQuitChans[txHash]
559+
488560
go func() {
489561
select {
490562
case <-confChan:
@@ -502,6 +574,12 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
502574
// arrivals.
503575
delete(m.finalizedWithdrawalTxns, txHash)
504576

577+
case <-quitChan:
578+
log.Debugf("Exiting withdrawal handler for tx %v",
579+
txHash)
580+
581+
return
582+
505583
case err := <-errChan:
506584
log.Errorf("Error waiting for confirmation: %v", err)
507585

@@ -914,7 +992,7 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
914992
continue
915993
}
916994

917-
err := m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
995+
_, err := m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
918996
if err != nil {
919997
log.Errorf("Error republishing withdrawal: %v", err)
920998

0 commit comments

Comments
 (0)