Skip to content

Commit cb77f35

Browse files
committed
staticaddr: allow rbf'ing withdrawal transactions
1 parent 75ad9b4 commit cb77f35

File tree

2 files changed

+121
-41
lines changed

2 files changed

+121
-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: 118 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ import (
2626
)
2727

2828
var (
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")
29+
// ErrWithdrawingMixedDeposits is returned when a withdrawal is
30+
// requested for deposits in different states.
31+
ErrWithdrawingMixedDeposits = errors.New("need to withdraw deposits " +
32+
"having the same state, either all deposited or all " +
33+
"withdrawing")
3334

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

117125
// NewManager creates a new deposit withdrawal manager.
118126
func NewManager(cfg *ManagerConfig, currentHeight uint32) *Manager {
119127
m := &Manager{
120-
cfg: cfg,
121-
initChan: make(chan struct{}),
122-
finalizedWithdrawalTxns: make(map[chainhash.Hash]*wire.MsgTx),
123-
exitChan: make(chan struct{}),
124-
newWithdrawalRequestChan: make(chan newWithdrawalRequest),
125-
errChan: make(chan error),
128+
cfg: cfg,
129+
initChan: make(chan struct{}),
130+
finalizedWithdrawalTxns: make(map[chainhash.Hash]*wire.MsgTx),
131+
exitChan: make(chan struct{}),
132+
newWithdrawalRequestChan: make(chan newWithdrawalRequest),
133+
errChan: make(chan error),
134+
withdrawalHandlerQuitChans: make(map[chainhash.Hash]chan struct{}),
126135
}
127136
m.initiationHeight.Store(currentHeight)
128137

@@ -237,7 +246,7 @@ func (m *Manager) recoverWithdrawals(ctx context.Context) error {
237246
return err
238247
}
239248

240-
err = m.publishFinalizedWithdrawalTx(ctx, tx)
249+
_, err := m.publishFinalizedWithdrawalTx(ctx, tx)
241250
if err != nil {
242251
return err
243252
}
@@ -276,12 +285,37 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
276285

277286
// Ensure that the deposits are in a state in which they can be
278287
// withdrawn.
279-
deposits, allActive := m.cfg.DepositManager.AllOutpointsActiveDeposits(
288+
deposits, allDeposited := m.cfg.DepositManager.AllOutpointsActiveDeposits(
280289
outpoints, deposit.Deposited,
281290
)
282291

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

287321
var (
@@ -315,6 +349,41 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
315349
return "", "", err
316350
}
317351

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

397+
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
398+
328399
// Transition the deposits to the withdrawing state. This updates each
329400
// deposits withdrawal address. If a transition fails, we'll return an
330401
// error and abort the withdrawal. An error in transition is likely due
@@ -337,25 +408,14 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
337408
return "", "", err
338409
}
339410

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
411+
// Update the deposits in the database.
412+
for _, d := range deposits {
413+
err = m.cfg.DepositManager.UpdateDeposit(ctx, d)
414+
if err != nil {
415+
return "", "", err
416+
}
355417
}
356418

357-
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
358-
359419
return finalizedTx.TxID(), withdrawalAddress.String(), nil
360420
}
361421

@@ -452,27 +512,31 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
452512
}
453513

454514
func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
455-
tx *wire.MsgTx) error {
515+
tx *wire.MsgTx) (bool, error) {
456516

457517
if tx == nil {
458-
return errors.New("can't publish, finalized withdrawal tx is " +
459-
"nil")
518+
return false, errors.New("can't publish, finalized " +
519+
"withdrawal tx is nil")
460520
}
461521

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

464524
// Publish the withdrawal sweep transaction.
465525
err := m.cfg.WalletKit.PublishTransaction(ctx, tx, txLabel)
466-
467526
if err != nil {
468-
if !strings.Contains(err.Error(), "output already spent") {
469-
log.Errorf("%v: %v", txLabel, err)
527+
if !strings.Contains(err.Error(), "output already spent") &&
528+
!strings.Contains(err.Error(), "insufficient fee") {
529+
530+
return false, err
531+
} else {
532+
return false, nil
470533
}
534+
} else {
535+
log.Debugf("published deposit withdrawal with txid: %v",
536+
tx.TxHash())
471537
}
472538

473-
log.Debugf("published deposit withdrawal with txid: %v", tx.TxHash())
474-
475-
return nil
539+
return true, nil
476540
}
477541

478542
func (m *Manager) handleWithdrawal(ctx context.Context,
@@ -487,6 +551,13 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
487551
return err
488552
}
489553

554+
// Create a new quit chan for this set of deposits under the same
555+
// withdrawal tx hash. If a new withdrawal is requested the quit chan
556+
// is closed in favor of a new one, to start monitoring the new
557+
// withdrawal transaction.
558+
m.withdrawalHandlerQuitChans[txHash] = make(chan struct{})
559+
quitChan := m.withdrawalHandlerQuitChans[txHash]
560+
490561
go func() {
491562
select {
492563
case <-confChan:
@@ -504,6 +575,12 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
504575
// arrivals.
505576
delete(m.finalizedWithdrawalTxns, txHash)
506577

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

@@ -916,7 +993,7 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
916993
continue
917994
}
918995

919-
err := m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
996+
_, err := m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
920997
if err != nil {
921998
log.Errorf("Error republishing withdrawal: %v", err)
922999

0 commit comments

Comments
 (0)