Skip to content

Commit 8d37d85

Browse files
committed
staticaddr: allow rbf'ing withdrawal transactions
1 parent 10c15c0 commit 8d37d85

File tree

2 files changed

+114
-35
lines changed

2 files changed

+114
-35
lines changed

staticaddr/deposit/fsm.go

+3
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

+111-35
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 the user is requesting
291+
// a fee bump, if not we'll return an error as we only allow fee bumping
292+
// 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,40 @@ 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+
if prevTx != nil && prevTx.TxHash() != finalizedTx.TxHash() {
377+
quitChan := m.withdrawalHandlerQuitChans[prevTx.TxHash()]
378+
close(quitChan)
379+
delete(m.withdrawalHandlerQuitChans, prevTx.TxHash())
380+
delete(m.finalizedWithdrawalTxns, prevTx.TxHash())
381+
}
382+
deposits[0].Unlock()
383+
316384
// Attach the finalized withdrawal tx to the deposits. After a client
317385
// restart we can use this address as an indicator to republish the
318386
// withdrawal tx and continue the withdrawal.
@@ -323,6 +391,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
323391
d.Unlock()
324392
}
325393

394+
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
395+
326396
// Transition the deposits to the withdrawing state. This updates each
327397
// deposits withdrawal address. If a transition fails, we'll return an
328398
// error and abort the withdrawal. An error in transition is likely due
@@ -335,25 +405,14 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
335405
return "", "", err
336406
}
337407

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

355-
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
356-
357416
return finalizedTx.TxID(), withdrawalAddress.String(), nil
358417
}
359418

@@ -449,27 +508,31 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
449508
}
450509

451510
func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
452-
tx *wire.MsgTx) error {
511+
tx *wire.MsgTx) (bool, error) {
453512

454513
if tx == nil {
455-
return errors.New("can't publish, finalized withdrawal tx is " +
456-
"nil")
514+
return false, errors.New("can't publish, finalized " +
515+
"withdrawal tx is nil")
457516
}
458517

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

461520
// Publish the withdrawal sweep transaction.
462521
err := m.cfg.WalletKit.PublishTransaction(ctx, tx, txLabel)
463-
464522
if err != nil {
465-
if !strings.Contains(err.Error(), "output already spent") {
466-
log.Errorf("%v: %v", txLabel, err)
523+
if !strings.Contains(err.Error(), "output already spent") &&
524+
!strings.Contains(err.Error(), "insufficient fee") {
525+
526+
return false, err
527+
} else {
528+
return false, nil
467529
}
530+
} else {
531+
log.Debugf("published deposit withdrawal with txid: %v",
532+
tx.TxHash())
468533
}
469534

470-
log.Debugf("published deposit withdrawal with txid: %v", tx.TxHash())
471-
472-
return nil
535+
return true, nil
473536
}
474537

475538
func (m *Manager) handleWithdrawal(ctx context.Context,
@@ -484,6 +547,13 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
484547
return err
485548
}
486549

550+
// Create a new quit chan for this set of deposits under the same
551+
// withdrawal tx hash. If a new withdrawal is requested the quit chan
552+
// is closed in favor of a new one, to start monitoring the new
553+
// withdrawal transaction.
554+
m.withdrawalHandlerQuitChans[txHash] = make(chan struct{})
555+
quitChan := m.withdrawalHandlerQuitChans[txHash]
556+
487557
go func() {
488558
select {
489559
case <-confChan:
@@ -501,6 +571,12 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
501571
// arrivals.
502572
delete(m.finalizedWithdrawalTxns, txHash)
503573

574+
case <-quitChan:
575+
log.Debugf("Exiting withdrawal handler for tx %v",
576+
txHash)
577+
578+
return
579+
504580
case err := <-errChan:
505581
log.Errorf("Error waiting for confirmation: %v", err)
506582

@@ -914,7 +990,7 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
914990
continue
915991
}
916992

917-
err := m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
993+
_, err := m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
918994
if err != nil {
919995
log.Errorf("Error republishing withdrawal: %v", err)
920996

0 commit comments

Comments
 (0)