Skip to content

Commit f868520

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

File tree

2 files changed

+114
-35
lines changed

2 files changed

+114
-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: 111 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 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

@@ -450,27 +509,31 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
450509
}
451510

452511
func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
453-
tx *wire.MsgTx) error {
512+
tx *wire.MsgTx) (bool, error) {
454513

455514
if tx == nil {
456-
return errors.New("can't publish, finalized withdrawal tx is " +
457-
"nil")
515+
return false, errors.New("can't publish, finalized " +
516+
"withdrawal tx is nil")
458517
}
459518

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

462521
// Publish the withdrawal sweep transaction.
463522
err := m.cfg.WalletKit.PublishTransaction(ctx, tx, txLabel)
464-
465523
if err != nil {
466-
if !strings.Contains(err.Error(), "output already spent") {
467-
log.Errorf("%v: %v", txLabel, err)
524+
if !strings.Contains(err.Error(), "output already spent") &&
525+
!strings.Contains(err.Error(), "insufficient fee") {
526+
527+
return false, err
528+
} else {
529+
return false, nil
468530
}
531+
} else {
532+
log.Debugf("published deposit withdrawal with txid: %v",
533+
tx.TxHash())
469534
}
470535

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

476539
func (m *Manager) handleWithdrawal(ctx context.Context,
@@ -485,6 +548,13 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
485548
return err
486549
}
487550

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

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

@@ -915,7 +991,7 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
915991
continue
916992
}
917993

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

0 commit comments

Comments
 (0)