Skip to content

Commit 76eb665

Browse files
committed
staticaddr: listen for spents on withdrawals
1 parent f5a018e commit 76eb665

File tree

2 files changed

+89
-55
lines changed

2 files changed

+89
-55
lines changed

staticaddr/deposit/fsm.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,8 @@ func (f *FSM) DepositStatesV0() fsm.States {
361361
},
362362
Withdrawn: fsm.State{
363363
Transitions: fsm.Transitions{
364-
OnExpiry: Expired,
364+
OnExpiry: Expired,
365+
OnWithdrawn: Withdrawn,
365366
},
366367
Action: f.FinalizeDepositAction,
367368
},

staticaddr/withdraw/manager.go

Lines changed: 87 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"reflect"
88
"strings"
9+
"sync"
910
"sync/atomic"
1011

1112
"github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -93,6 +94,9 @@ type newWithdrawalResponse struct {
9394
type Manager struct {
9495
cfg *ManagerConfig
9596

97+
// mu protects access to finalizedWithdrawalTxns.
98+
mu sync.Mutex
99+
96100
// initChan signals the daemon that the withdrawal manager has completed
97101
// its initialization.
98102
initChan chan struct{}
@@ -113,25 +117,17 @@ type Manager struct {
113117
// finalizedWithdrawalTx are the finalized withdrawal transactions that
114118
// are published to the network and re-published on block arrivals.
115119
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{}
123120
}
124121

125122
// NewManager creates a new deposit withdrawal manager.
126123
func NewManager(cfg *ManagerConfig, currentHeight uint32) *Manager {
127124
m := &Manager{
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{}),
125+
cfg: cfg,
126+
initChan: make(chan struct{}),
127+
finalizedWithdrawalTxns: make(map[chainhash.Hash]*wire.MsgTx),
128+
exitChan: make(chan struct{}),
129+
newWithdrawalRequestChan: make(chan newWithdrawalRequest),
130+
errChan: make(chan error),
135131
}
136132
m.initiationHeight.Store(currentHeight)
137133

@@ -251,14 +247,14 @@ func (m *Manager) recoverWithdrawals(ctx context.Context) error {
251247
return err
252248
}
253249

254-
err = m.handleWithdrawal(
255-
ctx, deposits, tx.TxHash(), tx.TxOut[0].PkScript,
256-
)
250+
err = m.handleWithdrawal(ctx, deposits, tx.TxHash())
257251
if err != nil {
258252
return err
259253
}
260254

255+
m.mu.Lock()
261256
m.finalizedWithdrawalTxns[tx.TxHash()] = tx
257+
m.mu.Unlock()
262258
}
263259

264260
return nil
@@ -283,9 +279,15 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
283279
"withdraw, unconfirmed deposits can't be withdrawn")
284280
}
285281

282+
var (
283+
deposits []*deposit.Deposit
284+
allDeposited bool
285+
allWithdrawing bool
286+
)
287+
286288
// Ensure that the deposits are in a state in which they can be
287289
// withdrawn.
288-
deposits, allDeposited := m.cfg.DepositManager.AllOutpointsActiveDeposits(
290+
deposits, allDeposited = m.cfg.DepositManager.AllOutpointsActiveDeposits(
289291
outpoints, deposit.Deposited,
290292
)
291293

@@ -294,7 +296,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
294296
// requesting a fee bump, if not we'll return an error as we only allow
295297
// fee bumping deposits in state Withdrawing.
296298
if !allDeposited {
297-
deposits, allWithdrawing := m.cfg.DepositManager.AllOutpointsActiveDeposits(
299+
deposits, allWithdrawing = m.cfg.DepositManager.AllOutpointsActiveDeposits(
298300
outpoints, deposit.Withdrawing,
299301
)
300302

@@ -316,6 +318,30 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
316318
"previous withdrawal tx hash")
317319
}
318320
}
321+
322+
// We also avoid that the user selects a subset of previously
323+
// clustered deposits for a fee bump. This would result in a
324+
// different transaction shape.
325+
allDeposits, err := m.cfg.DepositManager.GetActiveDepositsInState(
326+
deposit.Withdrawing,
327+
)
328+
if err != nil {
329+
return "", "", err
330+
}
331+
332+
allDepositsWithHash := make(map[chainhash.Hash][]*deposit.Deposit)
333+
for _, d := range allDeposits {
334+
if d.FinalizedWithdrawalTx.TxHash() == hash {
335+
allDepositsWithHash[hash] = append(
336+
allDepositsWithHash[hash], d,
337+
)
338+
}
339+
}
340+
341+
if len(allDepositsWithHash[hash]) != len(deposits) {
342+
return "", "", fmt.Errorf("can't bump fee for subset " +
343+
"of clustered deposits")
344+
}
319345
}
320346

321347
var (
@@ -358,30 +384,24 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
358384
return "", "", nil
359385
}
360386

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-
)
387+
err = m.handleWithdrawal(ctx, deposits, finalizedTx.TxHash())
369388
if err != nil {
370389
return "", "", err
371390
}
372391

373392
// 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.
393+
// it isn't the same as the new withdrawal, we remove it from the
394+
// finalized withdrawals to stop republishing it, but we keep the
395+
// goroutine in handleWithdrawal running to monitor the potential
396+
// confirmation of the previous withdrawal.
376397
deposits[0].Lock()
377398
prevTx := deposits[0].FinalizedWithdrawalTx
378399
deposits[0].Unlock()
379400

380401
if prevTx != nil && prevTx.TxHash() != finalizedTx.TxHash() {
381-
quitChan := m.withdrawalHandlerQuitChans[prevTx.TxHash()]
382-
close(quitChan)
383-
delete(m.withdrawalHandlerQuitChans, prevTx.TxHash())
402+
m.mu.Lock()
384403
delete(m.finalizedWithdrawalTxns, prevTx.TxHash())
404+
m.mu.Unlock()
385405
}
386406

387407
// Attach the finalized withdrawal tx to the deposits. After a client
@@ -394,7 +414,9 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
394414
d.Unlock()
395415
}
396416

417+
m.mu.Lock()
397418
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
419+
m.mu.Unlock()
398420

399421
// Transition the deposits to the withdrawing state. This updates each
400422
// deposits withdrawal address. If a transition fails, we'll return an
@@ -540,27 +562,36 @@ func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
540562
}
541563

542564
func (m *Manager) handleWithdrawal(ctx context.Context,
543-
deposits []*deposit.Deposit, txHash chainhash.Hash,
544-
withdrawalPkScript []byte) error {
565+
deposits []*deposit.Deposit, txHash chainhash.Hash) error {
545566

546-
confChan, errChan, err := m.cfg.ChainNotifier.RegisterConfirmationsNtfn(
547-
ctx, &txHash, withdrawalPkScript, MinConfs,
548-
int32(m.initiationHeight.Load()),
567+
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
568+
if err != nil {
569+
log.Errorf("error retrieving taproot address %w", err)
570+
571+
return fmt.Errorf("withdrawal failed")
572+
}
573+
574+
address, err := btcutil.NewAddressTaproot(
575+
schnorr.SerializePubKey(staticAddress.TaprootKey),
576+
m.cfg.ChainParams,
549577
)
550578
if err != nil {
551579
return err
552580
}
553581

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]
582+
script, err := txscript.PayToAddrScript(address)
583+
if err != nil {
584+
return err
585+
}
586+
587+
d := deposits[0]
588+
spentChan, errChan, err := m.cfg.ChainNotifier.RegisterSpendNtfn(
589+
ctx, &d.OutPoint, script, int32(d.ConfirmationHeight),
590+
)
560591

561592
go func() {
562593
select {
563-
case <-confChan:
594+
case <-spentChan:
564595
err = m.cfg.DepositManager.TransitionDeposits(
565596
ctx, deposits, deposit.OnWithdrawn,
566597
deposit.Withdrawn,
@@ -570,16 +601,11 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
570601
err)
571602
}
572603

573-
// Remove the withdrawal from the active withdrawals and
574-
// remove its finalized to stop republishing it on block
575-
// arrivals.
604+
// Remove the withdrawal tx from the active withdrawals
605+
// to stop republishing it on block arrivals.
606+
m.mu.Lock()
576607
delete(m.finalizedWithdrawalTxns, txHash)
577-
578-
case <-quitChan:
579-
log.Debugf("Exiting withdrawal handler for tx %v",
580-
txHash)
581-
582-
return
608+
m.mu.Unlock()
583609

584610
case err := <-errChan:
585611
log.Errorf("Error waiting for confirmation: %v", err)
@@ -987,7 +1013,14 @@ func (m *Manager) toPrevOuts(deposits []*deposit.Deposit,
9871013
}
9881014

9891015
func (m *Manager) republishWithdrawals(ctx context.Context) error {
990-
for _, finalizedTx := range m.finalizedWithdrawalTxns {
1016+
m.mu.Lock()
1017+
txns := make([]*wire.MsgTx, 0, len(m.finalizedWithdrawalTxns))
1018+
for _, tx := range m.finalizedWithdrawalTxns {
1019+
txns = append(txns, tx)
1020+
}
1021+
m.mu.Unlock()
1022+
1023+
for _, finalizedTx := range txns {
9911024
if finalizedTx == nil {
9921025
log.Warnf("Finalized withdrawal tx is nil")
9931026
continue

0 commit comments

Comments
 (0)