Skip to content

Commit de70c70

Browse files
committed
loopd: recover confirmed htlcs with direct sweep
1 parent 4ea7d27 commit de70c70

3 files changed

Lines changed: 498 additions & 0 deletions

File tree

loopd/daemon.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,27 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
571571
}
572572
})
573573

574+
// Start the HTLC recovery worker that handles HTLC-confirmed
575+
// notifications with a direct sweep attempt.
576+
htlcConfirmedRecovery := &htlcConfirmedRecoveryManager{
577+
notificationSource: notificationManager,
578+
swapStore: swapDb,
579+
chainParams: d.lnd.ChainParams,
580+
notifier: d.lnd.ChainNotifier,
581+
wallet: d.lnd.WalletKit,
582+
signer: d.lnd.Signer,
583+
}
584+
585+
d.wg.Go(func() {
586+
debugf("Starting htlc confirmed recovery worker")
587+
defer debugf("Htlc confirmed recovery worker stopped")
588+
589+
err := htlcConfirmedRecovery.run(d.mainCtx)
590+
if shouldReportManagerErr(err) {
591+
debugf("htlc confirmed recovery worker failed: %v", err)
592+
}
593+
})
594+
574595
var (
575596
staticAddressManager *address.Manager
576597
depositManager *deposit.Manager

loopd/htlc_confirmed_recovery.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package loopd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcd/chaincfg"
8+
"github.com/btcsuite/btcd/wire"
9+
"github.com/lightninglabs/loop/looprpc"
10+
"github.com/lightninglabs/loop/swapserverrpc"
11+
)
12+
13+
const (
14+
// htlcConfirmedRecoverySatPerVByte is the fixed fee rate used by the
15+
// HTLC recovery worker.
16+
htlcConfirmedRecoverySatPerVByte = 10
17+
)
18+
19+
// htlcConfirmedSubscriber exposes the HTLC-confirmed notification stream used
20+
// by the recovery worker.
21+
type htlcConfirmedSubscriber interface {
22+
SubscribeHtlcConfirmed(ctx context.Context,
23+
) <-chan *swapserverrpc.ServerHtlcConfirmedNotification
24+
}
25+
26+
// htlcConfirmedRecoveryManager consumes HTLC-confirmed notifications and
27+
// reuses sweepHtlc to recover the notified loop-out HTLC.
28+
type htlcConfirmedRecoveryManager struct {
29+
notificationSource htlcConfirmedSubscriber
30+
swapStore loopOutStore
31+
chainParams *chaincfg.Params
32+
notifier htlcChainNotifier
33+
wallet htlcWallet
34+
signer htlcSigner
35+
}
36+
37+
// run starts the HTLC recovery worker.
38+
func (m *htlcConfirmedRecoveryManager) run(ctx context.Context) error {
39+
if m.notificationSource == nil {
40+
return nil
41+
}
42+
43+
ntfnChan := m.notificationSource.SubscribeHtlcConfirmed(ctx)
44+
45+
for {
46+
select {
47+
case <-ctx.Done():
48+
return ctx.Err()
49+
50+
case ntfn, ok := <-ntfnChan:
51+
if !ok {
52+
return nil
53+
}
54+
55+
m.handleNotification(ctx, ntfn)
56+
}
57+
}
58+
}
59+
60+
// handleNotification validates a notification and triggers a direct sweep
61+
// attempt for the notified HTLC.
62+
func (m *htlcConfirmedRecoveryManager) handleNotification(ctx context.Context,
63+
ntfn *swapserverrpc.ServerHtlcConfirmedNotification) {
64+
65+
if ntfn == nil {
66+
debugf("Ignoring nil HTLC recovery notification")
67+
68+
return
69+
}
70+
71+
// Require all dependencies up front so the optional recovery path
72+
// exits quietly when the daemon is not wired for sweeping.
73+
if m.swapStore == nil || m.chainParams == nil || m.notifier == nil ||
74+
m.wallet == nil || m.signer == nil {
75+
76+
debugf("HTLC recovery dependencies unavailable")
77+
78+
return
79+
}
80+
81+
outpoint, htlcAddress, err := m.parseNotification(ntfn)
82+
if err != nil {
83+
debugf("Ignoring HTLC recovery notification: %v", err)
84+
85+
return
86+
}
87+
88+
// Reuse sweepHtlc so the worker follows the same success-path spend
89+
// construction and destination selection as the manual sweep path.
90+
_, err = sweepHtlc(
91+
ctx, &looprpc.SweepHtlcRequest{
92+
Outpoint: outpoint.String(),
93+
HtlcAddress: htlcAddress,
94+
SatPerVbyte: htlcConfirmedRecoverySatPerVByte,
95+
Publish: true,
96+
}, m.chainParams, m.swapStore, m.notifier, m.wallet, m.signer,
97+
)
98+
if err != nil {
99+
debugf("Unable to recover HTLC outpoint %s: %v", outpoint, err)
100+
}
101+
}
102+
103+
// parseNotification parses the swap hash and outpoint from a notification.
104+
func (m *htlcConfirmedRecoveryManager) parseNotification(
105+
ntfn *swapserverrpc.ServerHtlcConfirmedNotification) (*wire.OutPoint,
106+
string, error) {
107+
108+
outpoint, err := wire.NewOutPointFromString(ntfn.HtlcOutpoint)
109+
if err != nil {
110+
return nil, "", fmt.Errorf("bad outpoint: %w", err)
111+
}
112+
113+
if ntfn.HtlcAddress == "" {
114+
return nil, "", fmt.Errorf("missing HTLC address")
115+
}
116+
117+
return outpoint, ntfn.HtlcAddress, nil
118+
}

0 commit comments

Comments
 (0)