Skip to content

Commit 6b1f478

Browse files
committed
staticaddr: allow rbf'ing withdrawal transactions
1 parent d2c08bb commit 6b1f478

File tree

3 files changed

+123
-42
lines changed

3 files changed

+123
-42
lines changed

looprpc/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module github.com/lightninglabs/loop/looprpc
22

3-
go 1.22.3
3+
go 1.23.0
4+
45
toolchain go1.23.7
56

67
require (

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
@@ -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,37 @@ 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+
}
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

@@ -914,7 +991,7 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
914991
continue
915992
}
916993

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

0 commit comments

Comments
 (0)