• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 12343072627

15 Dec 2024 11:09PM UTC coverage: 57.504% (-1.1%) from 58.636%
12343072627

Pull #9315

github

yyforyongyu
contractcourt: offer outgoing htlc one block earlier before its expiry

We need to offer the outgoing htlc one block earlier to make sure when
the expiry height hits, the sweeper will not miss sweeping it in the
same block. This also means the outgoing contest resolver now only does
one thing - watch for preimage spend till height expiry-1, which can
easily be moved into the timeout resolver instead in the future.
Pull Request #9315: Implement `blockbeat`

1445 of 2007 new or added lines in 26 files covered. (72.0%)

19246 existing lines in 249 files now uncovered.

102342 of 177975 relevant lines covered (57.5%)

24772.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.28
/contractcourt/htlc_outgoing_contest_resolver.go
1
package contractcourt
2

3
import (
4
        "io"
5

6
        "github.com/btcsuite/btcd/btcutil"
7
        "github.com/lightningnetwork/lnd/channeldb"
8
        "github.com/lightningnetwork/lnd/fn/v2"
9
        "github.com/lightningnetwork/lnd/lnwallet"
10
)
11

12
// htlcOutgoingContestResolver is a ContractResolver that's able to resolve an
13
// outgoing HTLC that is still contested. An HTLC is still contested, if at the
14
// time that we broadcast the commitment transaction, it isn't able to be fully
15
// resolved. In this case, we'll either wait for the HTLC to timeout, or for
16
// us to learn of the preimage.
17
type htlcOutgoingContestResolver struct {
18
        // htlcTimeoutResolver is the inner solver that this resolver may turn
19
        // into. This only happens if the HTLC expires on-chain.
20
        *htlcTimeoutResolver
21
}
22

23
// newOutgoingContestResolver instantiates a new outgoing contested htlc
24
// resolver.
25
func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution,
26
        broadcastHeight uint32, htlc channeldb.HTLC,
27
        resCfg ResolverConfig) *htlcOutgoingContestResolver {
1✔
28

1✔
29
        timeout := newTimeoutResolver(
1✔
30
                res, broadcastHeight, htlc, resCfg,
1✔
31
        )
1✔
32

1✔
33
        return &htlcOutgoingContestResolver{
1✔
34
                htlcTimeoutResolver: timeout,
1✔
35
        }
1✔
36
}
1✔
37

38
// Launch will call the inner resolver's launch method if the expiry height has
39
// been reached, otherwise it's a no-op.
40
func (h *htlcOutgoingContestResolver) Launch() error {
4✔
41
        // NOTE: we don't mark this resolver as launched as the inner resolver
4✔
42
        // will set it when it's launched.
4✔
43
        if h.isLaunched() {
4✔
NEW
44
                h.log.Tracef("already launched")
×
NEW
45
                return nil
×
NEW
46
        }
×
47

48
        h.log.Debugf("launching contest resolver...")
4✔
49

4✔
50
        _, bestHeight, err := h.ChainIO.GetBestBlock()
4✔
51
        if err != nil {
4✔
NEW
52
                return err
×
NEW
53
        }
×
54

55
        if uint32(bestHeight) < h.htlcResolution.Expiry {
8✔
56
                return nil
4✔
57
        }
4✔
58

59
        // If the current height is >= expiry, then a timeout path spend will
60
        // be valid to be included in the next block, and we can immediately
61
        // return the resolver.
NEW
62
        h.log.Infof("expired (height=%v, expiry=%v), transforming into "+
×
NEW
63
                "timeout resolver and launching it", bestHeight,
×
NEW
64
                h.htlcResolution.Expiry)
×
NEW
65

×
NEW
66
        return h.htlcTimeoutResolver.Launch()
×
67
}
68

69
// Resolve commences the resolution of this contract. As this contract hasn't
70
// yet timed out, we'll wait for one of two things to happen
71
//
72
//  1. The HTLC expires. In this case, we'll sweep the funds and send a clean
73
//     up cancel message to outside sub-systems.
74
//
75
//  2. The remote party sweeps this HTLC on-chain, in which case we'll add the
76
//     pre-image to our global cache, then send a clean up settle message
77
//     backwards.
78
//
79
// When either of these two things happens, we'll create a new resolver which
80
// is able to handle the final resolution of the contract. We're only the pivot
81
// point.
82
func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
4✔
83
        // If we're already full resolved, then we don't have anything further
4✔
84
        // to do.
4✔
85
        if h.IsResolved() {
4✔
NEW
86
                h.log.Errorf("already resolved")
×
87
                return nil, nil
×
88
        }
×
89

90
        // Otherwise, we'll watch for two external signals to decide if we'll
91
        // morph into another resolver, or fully resolve the contract.
92
        //
93
        // The output we'll be watching for is the *direct* spend from the HTLC
94
        // output. If this isn't our commitment transaction, it'll be right on
95
        // the resolution. Otherwise, we fetch this pointer from the input of
96
        // the time out transaction.
97
        outPointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
4✔
98
        if err != nil {
4✔
99
                return nil, err
×
100
        }
×
101

102
        // First, we'll register for a spend notification for this output. If
103
        // the remote party sweeps with the pre-image, we'll be notified.
104
        spendNtfn, err := h.Notifier.RegisterSpendNtfn(
4✔
105
                outPointToWatch, scriptToWatch, h.broadcastHeight,
4✔
106
        )
4✔
107
        if err != nil {
4✔
108
                return nil, err
×
109
        }
×
110

111
        // We'll quickly check to see if the output has already been spent.
112
        select {
4✔
113
        // If the output has already been spent, then we can stop early and
114
        // sweep the pre-image from the output.
115
        case commitSpend, ok := <-spendNtfn.Spend:
×
116
                if !ok {
×
117
                        return nil, errResolverShuttingDown
×
118
                }
×
119

NEW
120
                return nil, h.claimCleanUp(commitSpend)
×
121

122
        // If it hasn't, then we'll watch for both the expiration, and the
123
        // sweeping out this output.
124
        default:
4✔
125
        }
126

127
        // If we reach this point, then we can't fully act yet, so we'll await
128
        // either of our signals triggering: the HTLC expires, or we learn of
129
        // the preimage.
130
        blockEpochs, err := h.Notifier.RegisterBlockEpochNtfn(nil)
4✔
131
        if err != nil {
4✔
132
                return nil, err
×
133
        }
×
134
        defer blockEpochs.Cancel()
4✔
135

4✔
136
        for {
10✔
137
                select {
6✔
138

139
                // A new block has arrived, we'll check to see if this leads to
140
                // HTLC expiration.
141
                case newBlock, ok := <-blockEpochs.Epochs:
4✔
142
                        if !ok {
4✔
143
                                return nil, errResolverShuttingDown
×
144
                        }
×
145

146
                        // If the current height is >= expiry, then a timeout
147
                        // path spend will be valid to be included in the next
148
                        // block, and we can immediately return the resolver.
149
                        //
150
                        // NOTE: when broadcasting this transaction, btcd will
151
                        // check the timelock in `CheckTransactionStandard`,
152
                        // which requires `expiry < currentHeight+1`. If the
153
                        // check doesn't pass, error `transaction is not
154
                        // finalized` will be returned and the broadcast will
155
                        // fail.
156
                        newHeight := uint32(newBlock.Height)
4✔
157
                        expiry := h.htlcResolution.Expiry
4✔
158

4✔
159
                        // Check if the expiry height is about to be reached.
4✔
160
                        // We offer this HTLC one block earlier to make sure
4✔
161
                        // when the next block arrives, the sweeper will pick
4✔
162
                        // up this input and sweep it immediately. The sweeper
4✔
163
                        // will handle the waiting for the one last block till
4✔
164
                        // expiry.
4✔
165
                        if newHeight >= expiry-1 {
6✔
166
                                h.log.Infof("HTLC about to expire "+
2✔
167
                                        "(height=%v, expiry=%v), transforming "+
2✔
168
                                        "into timeout resolver", h,
2✔
169
                                        h.htlcResolution.ClaimOutpoint,
2✔
170
                                        newHeight, h.htlcResolution.Expiry)
2✔
171

2✔
172
                                return h.htlcTimeoutResolver, nil
2✔
173
                        }
2✔
174

175
                // The output has been spent! This means the preimage has been
176
                // revealed on-chain.
177
                case commitSpend, ok := <-spendNtfn.Spend:
1✔
178
                        if !ok {
1✔
179
                                return nil, errResolverShuttingDown
×
180
                        }
×
181

182
                        // The only way this output can be spent by the remote
183
                        // party is by revealing the preimage. So we'll perform
184
                        // our duties to clean up the contract once it has been
185
                        // claimed.
186
                        return nil, h.claimCleanUp(commitSpend)
1✔
187

188
                case <-h.quit:
1✔
189
                        return nil, errResolverShuttingDown
1✔
190
                }
191
        }
192
}
193

194
// report returns a report on the resolution state of the contract.
UNCOV
195
func (h *htlcOutgoingContestResolver) report() *ContractReport {
×
UNCOV
196
        // No locking needed as these values are read-only.
×
UNCOV
197

×
UNCOV
198
        finalAmt := h.htlc.Amt.ToSatoshis()
×
UNCOV
199
        if h.htlcResolution.SignedTimeoutTx != nil {
×
UNCOV
200
                finalAmt = btcutil.Amount(
×
UNCOV
201
                        h.htlcResolution.SignedTimeoutTx.TxOut[0].Value,
×
UNCOV
202
                )
×
UNCOV
203
        }
×
204

UNCOV
205
        return &ContractReport{
×
UNCOV
206
                Outpoint:       h.htlcResolution.ClaimOutpoint,
×
UNCOV
207
                Type:           ReportOutputOutgoingHtlc,
×
UNCOV
208
                Amount:         finalAmt,
×
UNCOV
209
                MaturityHeight: h.htlcResolution.Expiry,
×
UNCOV
210
                LimboBalance:   finalAmt,
×
UNCOV
211
                Stage:          1,
×
UNCOV
212
        }
×
213
}
214

215
// Stop signals the resolver to cancel any current resolution processes, and
216
// suspend.
217
//
218
// NOTE: Part of the ContractResolver interface.
219
func (h *htlcOutgoingContestResolver) Stop() {
1✔
220
        h.log.Debugf("stopping...")
1✔
221
        defer h.log.Debugf("stopped")
1✔
222
        close(h.quit)
1✔
223
}
1✔
224

225
// Encode writes an encoded version of the ContractResolver into the passed
226
// Writer.
227
//
228
// NOTE: Part of the ContractResolver interface.
229
func (h *htlcOutgoingContestResolver) Encode(w io.Writer) error {
3✔
230
        return h.htlcTimeoutResolver.Encode(w)
3✔
231
}
3✔
232

233
// SupplementDeadline does nothing for an incoming htlc resolver.
234
//
235
// NOTE: Part of the htlcContractResolver interface.
236
func (h *htlcOutgoingContestResolver) SupplementDeadline(_ fn.Option[int32]) {
2✔
237
}
2✔
238

239
// newOutgoingContestResolverFromReader attempts to decode an encoded ContractResolver
240
// from the passed Reader instance, returning an active ContractResolver
241
// instance.
242
func newOutgoingContestResolverFromReader(r io.Reader, resCfg ResolverConfig) (
243
        *htlcOutgoingContestResolver, error) {
4✔
244

4✔
245
        h := &htlcOutgoingContestResolver{}
4✔
246
        timeoutResolver, err := newTimeoutResolverFromReader(r, resCfg)
4✔
247
        if err != nil {
4✔
248
                return nil, err
×
249
        }
×
250
        h.htlcTimeoutResolver = timeoutResolver
4✔
251
        return h, nil
4✔
252
}
253

254
// A compile time assertion to ensure htlcOutgoingContestResolver meets the
255
// ContractResolver interface.
256
var _ htlcContractResolver = (*htlcOutgoingContestResolver)(nil)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc