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

lightningnetwork / lnd / 16177692143

09 Jul 2025 06:49PM UTC coverage: 55.317% (-2.3%) from 57.611%
16177692143

Pull #10060

github

web-flow
Merge 4aec413e3 into 0e830da9d
Pull Request #10060: sweep: fix expected spending events being missed

9 of 25 new or added lines in 1 file covered. (36.0%)

23713 existing lines in 281 files now uncovered.

108499 of 196142 relevant lines covered (55.32%)

22331.52 hits per line

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

79.14
/watchtower/lookout/justice_descriptor.go
1
package lookout
2

3
import (
4
        "errors"
5
        "fmt"
6

7
        "github.com/btcsuite/btcd/blockchain"
8
        "github.com/btcsuite/btcd/btcutil"
9
        "github.com/btcsuite/btcd/btcutil/txsort"
10
        "github.com/btcsuite/btcd/txscript"
11
        "github.com/btcsuite/btcd/wire"
12
        "github.com/davecgh/go-spew/spew"
13
        "github.com/lightningnetwork/lnd/input"
14
        "github.com/lightningnetwork/lnd/lntypes"
15
        "github.com/lightningnetwork/lnd/lnutils"
16
        "github.com/lightningnetwork/lnd/watchtower/blob"
17
        "github.com/lightningnetwork/lnd/watchtower/wtdb"
18
)
19

20
var (
21
        // ErrOutputNotFound signals that the breached output could not be found
22
        // on the commitment transaction.
23
        ErrOutputNotFound = errors.New("unable to find output on commit tx")
24

25
        // ErrUnknownSweepAddrType signals that client provided an output that
26
        // was not p2wkh or p2wsh.
27
        ErrUnknownSweepAddrType = errors.New("sweep addr is not p2wkh or p2wsh")
28
)
29

30
// JusticeDescriptor contains the information required to sweep a breached
31
// channel on behalf of a victim. It supports the ability to create the justice
32
// transaction that sweeps the commitments and recover a cut of the channel for
33
// the watcher's eternal vigilance.
34
type JusticeDescriptor struct {
35
        // BreachedCommitTx is the commitment transaction that caused the breach
36
        // to be detected.
37
        BreachedCommitTx *wire.MsgTx
38

39
        // SessionInfo contains the contract with the watchtower client and
40
        // the prenegotiated terms they agreed to.
41
        SessionInfo *wtdb.SessionInfo
42

43
        // JusticeKit contains the decrypted blob and information required to
44
        // construct the transaction scripts and witnesses.
45
        JusticeKit blob.JusticeKit
46
}
47

48
// breachedInput contains the required information to construct and spend
49
// breached outputs on a commitment transaction.
50
type breachedInput struct {
51
        txOut    *wire.TxOut
52
        outPoint wire.OutPoint
53
        witness  [][]byte
54
        sequence uint32
55
}
56

57
// commitToLocalInput extracts the information required to spend the commit
58
// to-local output.
59
func (p *JusticeDescriptor) commitToLocalInput() (*breachedInput, error) {
4✔
60
        kit := p.JusticeKit
4✔
61

4✔
62
        // Retrieve the to-local output script and witness from the justice kit.
4✔
63
        toLocalPkScript, witness, err := kit.ToLocalOutputSpendInfo()
4✔
64
        if err != nil {
4✔
65
                return nil, err
×
66
        }
×
67

68
        // Locate the to-local output on the breaching commitment transaction.
69
        toLocalIndex, toLocalTxOut, err := findTxOutByPkScript(
4✔
70
                p.BreachedCommitTx, toLocalPkScript,
4✔
71
        )
4✔
72
        if err != nil {
4✔
73
                return nil, err
×
74
        }
×
75

76
        // Construct the to-local outpoint that will be spent in the justice
77
        // transaction.
78
        toLocalOutPoint := wire.OutPoint{
4✔
79
                Hash:  p.BreachedCommitTx.TxHash(),
4✔
80
                Index: toLocalIndex,
4✔
81
        }
4✔
82

4✔
83
        return &breachedInput{
4✔
84
                txOut:    toLocalTxOut,
4✔
85
                outPoint: toLocalOutPoint,
4✔
86
                witness:  witness,
4✔
87
        }, nil
4✔
88
}
89

90
// commitToRemoteInput extracts the information required to spend the commit
91
// to-remote output.
92
func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
4✔
93
        kit := p.JusticeKit
4✔
94

4✔
95
        // Retrieve the to-remote output script, witness script and sequence
4✔
96
        // from the justice kit.
4✔
97
        toRemotePkScript, witness, seq, err := kit.ToRemoteOutputSpendInfo()
4✔
98
        if err != nil {
4✔
99
                return nil, err
×
100
        }
×
101

102
        // Locate the to-remote output on the breaching commitment transaction.
103
        toRemoteIndex, toRemoteTxOut, err := findTxOutByPkScript(
4✔
104
                p.BreachedCommitTx, toRemotePkScript,
4✔
105
        )
4✔
106
        if err != nil {
4✔
107
                return nil, err
×
108
        }
×
109

110
        // Construct the to-remote outpoint which will be spent in the justice
111
        // transaction.
112
        toRemoteOutPoint := wire.OutPoint{
4✔
113
                Hash:  p.BreachedCommitTx.TxHash(),
4✔
114
                Index: toRemoteIndex,
4✔
115
        }
4✔
116

4✔
117
        return &breachedInput{
4✔
118
                txOut:    toRemoteTxOut,
4✔
119
                outPoint: toRemoteOutPoint,
4✔
120
                witness:  witness,
4✔
121
                sequence: seq,
4✔
122
        }, nil
4✔
123
}
124

125
// assembleJusticeTxn accepts the breached inputs recovered from state update
126
// and attempts to construct the justice transaction that sweeps the victims
127
// funds to their wallet and claims the watchtower's reward.
128
func (p *JusticeDescriptor) assembleJusticeTxn(txWeight lntypes.WeightUnit,
129
        inputs ...*breachedInput) (*wire.MsgTx, error) {
4✔
130

4✔
131
        justiceTxn := wire.NewMsgTx(2)
4✔
132

4✔
133
        // First, construct add the breached inputs to our justice transaction
4✔
134
        // and compute the total amount that will be swept.
4✔
135
        var totalAmt btcutil.Amount
4✔
136
        for _, inp := range inputs {
12✔
137
                totalAmt += btcutil.Amount(inp.txOut.Value)
8✔
138
                justiceTxn.AddTxIn(&wire.TxIn{
8✔
139
                        PreviousOutPoint: inp.outPoint,
8✔
140
                        Sequence:         inp.sequence,
8✔
141
                })
8✔
142
        }
8✔
143

144
        // Using the session's policy, compute the outputs that should be added
145
        // to the justice transaction. In the case of an altruist sweep, there
146
        // will be a single output paying back to the victim. Otherwise for a
147
        // reward sweep, there will be two outputs, one of which pays back to
148
        // the victim while the other gives a cut to the tower.
149
        outputs, err := p.SessionInfo.Policy.ComputeJusticeTxOuts(
4✔
150
                totalAmt, txWeight, p.JusticeKit.SweepAddress(),
4✔
151
                p.SessionInfo.RewardAddress,
4✔
152
        )
4✔
153
        if err != nil {
4✔
154
                return nil, err
×
155
        }
×
156

157
        // Attach the computed txouts to the justice transaction.
158
        justiceTxn.TxOut = outputs
4✔
159

4✔
160
        // Apply a BIP69 sort to the resulting transaction.
4✔
161
        txsort.InPlaceSort(justiceTxn)
4✔
162

4✔
163
        btx := btcutil.NewTx(justiceTxn)
4✔
164
        if err := blockchain.CheckTransactionSanity(btx); err != nil {
4✔
165
                return nil, err
×
166
        }
×
167

168
        // Since the transaction inputs could have been reordered as a result of the
169
        // BIP69 sort, create an index mapping each prevout to it's new index.
170
        inputIndex := make(map[wire.OutPoint]int)
4✔
171
        for i, txIn := range justiceTxn.TxIn {
12✔
172
                inputIndex[txIn.PreviousOutPoint] = i
8✔
173
        }
8✔
174

175
        // Attach each of the provided witnesses to the transaction.
176
        prevOutFetcher, err := prevOutFetcher(inputs)
4✔
177
        if err != nil {
4✔
178
                return nil, fmt.Errorf("error creating previous output "+
×
179
                        "fetcher: %v", err)
×
180
        }
×
181

182
        hashes := txscript.NewTxSigHashes(justiceTxn, prevOutFetcher)
4✔
183
        for _, inp := range inputs {
12✔
184
                // Lookup the input's new post-sort position.
8✔
185
                i := inputIndex[inp.outPoint]
8✔
186
                justiceTxn.TxIn[i].Witness = inp.witness
8✔
187

8✔
188
                // Validate the reconstructed witnesses to ensure they are
8✔
189
                // valid for the breached inputs.
8✔
190
                vm, err := txscript.NewEngine(
8✔
191
                        inp.txOut.PkScript, justiceTxn, i,
8✔
192
                        txscript.StandardVerifyFlags,
8✔
193
                        nil, hashes, inp.txOut.Value, prevOutFetcher,
8✔
194
                )
8✔
195
                if err != nil {
8✔
196
                        return nil, err
×
197
                }
×
198
                if err := vm.Execute(); err != nil {
8✔
199
                        log.Debugf("Failed to validate justice transaction: %s",
×
200
                                spew.Sdump(justiceTxn))
×
201
                        return nil, fmt.Errorf("error validating TX: %w", err)
×
202
                }
×
203
        }
204

205
        return justiceTxn, nil
4✔
206
}
207

208
// CreateJusticeTxn computes the justice transaction that sweeps a breaching
209
// commitment transaction. The justice transaction is constructed by assembling
210
// the witnesses using data provided by the client in a prior state update.
211
//
212
// NOTE: An older version of ToLocalPenaltyWitnessSize underestimated the size
213
// of the witness by one byte, which could cause the signature(s) to break if
214
// the tower is reconstructing with the newer constant because the output values
215
// might differ. This method retains that original behavior to not invalidate
216
// historical signatures.
217
func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
4✔
218
        var (
4✔
219
                sweepInputs    = make([]*breachedInput, 0, 2)
4✔
220
                weightEstimate input.TxWeightEstimator
4✔
221
        )
4✔
222

4✔
223
        commitmentType, err := p.SessionInfo.Policy.BlobType.CommitmentType(nil)
4✔
224
        if err != nil {
4✔
225
                return nil, err
×
226
        }
×
227

228
        // Add the sweep address's contribution, depending on whether it is a
229
        // p2wkh or p2wsh output.
230
        switch len(p.JusticeKit.SweepAddress()) {
4✔
231
        case input.P2WPKHSize:
4✔
232
                weightEstimate.AddP2WKHOutput()
4✔
233

234
        // NOTE: P2TR has the same size as P2WSH (34 bytes), their output sizes
235
        // are also the same (43 bytes), so here we implicitly catch the P2TR
236
        // output case.
UNCOV
237
        case input.P2WSHSize:
×
UNCOV
238
                weightEstimate.AddP2WSHOutput()
×
239

240
        default:
×
241
                return nil, ErrUnknownSweepAddrType
×
242
        }
243

244
        // Add our reward address to the weight estimate if the policy's blob
245
        // type specifies a reward output.
246
        if p.SessionInfo.Policy.BlobType.Has(blob.FlagReward) {
5✔
247
                weightEstimate.AddP2WKHOutput()
1✔
248
        }
1✔
249

250
        // Assemble the breached to-local output from the justice descriptor and
251
        // add it to our weight estimate.
252
        toLocalInput, err := p.commitToLocalInput()
4✔
253
        if err != nil {
4✔
254
                return nil, err
×
255
        }
×
256

257
        // Get the weight for the to-local witness and add that to the
258
        // estimator.
259
        toLocalWitnessSize, err := commitmentType.ToLocalWitnessSize()
4✔
260
        if err != nil {
4✔
261
                return nil, err
×
262
        }
×
263
        weightEstimate.AddWitnessInput(toLocalWitnessSize)
4✔
264

4✔
265
        sweepInputs = append(sweepInputs, toLocalInput)
4✔
266

4✔
267
        log.Debugf("Found to local witness output=%v, stack=%x",
4✔
268
                lnutils.SpewLogClosure(toLocalInput.txOut),
4✔
269
                toLocalInput.witness)
4✔
270

4✔
271
        // If the justice kit specifies that we have to sweep the to-remote
4✔
272
        // output, we'll also try to assemble the output and add it to weight
4✔
273
        // estimate if successful.
4✔
274
        if p.JusticeKit.HasCommitToRemoteOutput() {
8✔
275
                toRemoteInput, err := p.commitToRemoteInput()
4✔
276
                if err != nil {
4✔
277
                        return nil, err
×
278
                }
×
279
                sweepInputs = append(sweepInputs, toRemoteInput)
4✔
280

4✔
281
                log.Debugf("Found to remote witness output=%v, stack=%x",
4✔
282
                        lnutils.SpewLogClosure(toRemoteInput.txOut),
4✔
283
                        toRemoteInput.witness)
4✔
284

4✔
285
                // Get the weight for the to-remote witness and add that to the
4✔
286
                // estimator.
4✔
287
                toRemoteWitnessSize, err := commitmentType.ToRemoteWitnessSize()
4✔
288
                if err != nil {
4✔
289
                        return nil, err
×
290
                }
×
291

292
                weightEstimate.AddWitnessInput(toRemoteWitnessSize)
4✔
293
        }
294

295
        // TODO(conner): sweep htlc outputs
296

297
        txWeight := weightEstimate.Weight()
4✔
298

4✔
299
        return p.assembleJusticeTxn(txWeight, sweepInputs...)
4✔
300
}
301

302
// findTxOutByPkScript searches the given transaction for an output whose
303
// pkscript matches the query. If one is found, the TxOut is returned along with
304
// the index.
305
//
306
// NOTE: The search stops after the first match is found.
307
func findTxOutByPkScript(txn *wire.MsgTx,
308
        pkScript *txscript.PkScript) (uint32, *wire.TxOut, error) {
8✔
309

8✔
310
        found, index := input.FindScriptOutputIndex(txn, pkScript.Script())
8✔
311
        if !found {
8✔
312
                return 0, nil, ErrOutputNotFound
×
313
        }
×
314

315
        return index, txn.TxOut[index], nil
8✔
316
}
317

318
// prevOutFetcher returns a txscript.MultiPrevOutFetcher for the given set
319
// of inputs.
320
func prevOutFetcher(inputs []*breachedInput) (*txscript.MultiPrevOutFetcher,
321
        error) {
4✔
322

4✔
323
        fetcher := txscript.NewMultiPrevOutFetcher(nil)
4✔
324
        for _, inp := range inputs {
12✔
325
                if inp.txOut == nil {
8✔
326
                        return nil, fmt.Errorf("missing input utxo information")
×
327
                }
×
328

329
                fetcher.AddPrevOut(inp.outPoint, inp.txOut)
8✔
330
        }
331

332
        return fetcher, nil
4✔
333
}
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