Skip to content

Commit 4031671

Browse files
committed
staticaddr: allow rbf'ing withdrawal transactions
1 parent 800f0e0 commit 4031671

File tree

2 files changed

+115
-35
lines changed

2 files changed

+115
-35
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: 112 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,25 @@ type Manager struct {
111111
// finalizedWithdrawalTx are the finalized withdrawal transactions that
112112
// are published to the network and re-published on block arrivals.
113113
finalizedWithdrawalTxns map[chainhash.Hash]*wire.MsgTx
114+
115+
// withdrawalHandlerQuitChans is a map of quit channels for each
116+
// withdrawal transaction. The quit channels are used to stop the
117+
// withdrawal handler for a specific withdrawal transaction, e.g. if
118+
// a new rbf'd transaction has to be monitored for confirmation in
119+
// favor of the previous one.
120+
withdrawalHandlerQuitChans map[chainhash.Hash]chan struct{}
114121
}
115122

116123
// NewManager creates a new deposit withdrawal manager.
117124
func NewManager(cfg *ManagerConfig) *Manager {
118125
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),
126+
cfg: cfg,
127+
initChan: make(chan struct{}),
128+
finalizedWithdrawalTxns: make(map[chainhash.Hash]*wire.MsgTx),
129+
exitChan: make(chan struct{}),
130+
newWithdrawalRequestChan: make(chan newWithdrawalRequest),
131+
errChan: make(chan error),
132+
withdrawalHandlerQuitChans: make(map[chainhash.Hash]chan struct{}),
125133
}
126134
}
127135

@@ -235,7 +243,7 @@ func (m *Manager) recoverWithdrawals(ctx context.Context) error {
235243
return err
236244
}
237245

238-
err = m.publishFinalizedWithdrawalTx(ctx, tx)
246+
_, err := m.publishFinalizedWithdrawalTx(ctx, tx)
239247
if err != nil {
240248
return err
241249
}
@@ -278,8 +286,34 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
278286
outpoints, deposit.Deposited,
279287
)
280288

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

285319
var (
@@ -313,6 +347,41 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
313347
return "", "", err
314348
}
315349

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

395+
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
396+
326397
// Transition the deposits to the withdrawing state. This updates each
327398
// deposits withdrawal address. If a transition fails, we'll return an
328399
// error and abort the withdrawal. An error in transition is likely due
@@ -335,25 +406,14 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
335406
return "", "", err
336407
}
337408

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

355-
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
356-
357417
return finalizedTx.TxID(), withdrawalAddress.String(), nil
358418
}
359419

@@ -450,27 +510,31 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
450510
}
451511

452512
func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
453-
tx *wire.MsgTx) error {
513+
tx *wire.MsgTx) (bool, error) {
454514

455515
if tx == nil {
456-
return errors.New("can't publish, finalized withdrawal tx is " +
457-
"nil")
516+
return false, errors.New("can't publish, finalized " +
517+
"withdrawal tx is nil")
458518
}
459519

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

462522
// Publish the withdrawal sweep transaction.
463523
err := m.cfg.WalletKit.PublishTransaction(ctx, tx, txLabel)
464-
465524
if err != nil {
466-
if !strings.Contains(err.Error(), "output already spent") {
467-
log.Errorf("%v: %v", txLabel, err)
525+
if !strings.Contains(err.Error(), "output already spent") &&
526+
!strings.Contains(err.Error(), "insufficient fee") {
527+
528+
return false, err
529+
} else {
530+
return false, nil
468531
}
532+
} else {
533+
log.Debugf("published deposit withdrawal with txid: %v",
534+
tx.TxHash())
469535
}
470536

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

476540
func (m *Manager) handleWithdrawal(ctx context.Context,
@@ -485,6 +549,13 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
485549
return err
486550
}
487551

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

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

@@ -915,7 +992,7 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
915992
continue
916993
}
917994

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

0 commit comments

Comments
 (0)