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

lightningnetwork / lnd / 17101605539

20 Aug 2025 02:35PM UTC coverage: 57.321% (-9.4%) from 66.68%
17101605539

push

github

web-flow
Merge pull request #10102 from yyforyongyu/fix-UpdatesInHorizon

Catch bad gossip peer and fix `UpdatesInHorizon`

28 of 89 new or added lines in 4 files covered. (31.46%)

29163 existing lines in 459 files now uncovered.

99187 of 173038 relevant lines covered (57.32%)

1.78 hits per line

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

78.07
/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) {
3✔
60
        kit := p.JusticeKit
3✔
61

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

68
        // Locate the to-local output on the breaching commitment transaction.
69
        toLocalIndex, toLocalTxOut, err := findTxOutByPkScript(
3✔
70
                p.BreachedCommitTx, toLocalPkScript,
3✔
71
        )
3✔
72
        if err != nil {
3✔
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{
3✔
79
                Hash:  p.BreachedCommitTx.TxHash(),
3✔
80
                Index: toLocalIndex,
3✔
81
        }
3✔
82

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

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

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

102
        // Locate the to-remote output on the breaching commitment transaction.
103
        toRemoteIndex, toRemoteTxOut, err := findTxOutByPkScript(
3✔
104
                p.BreachedCommitTx, toRemotePkScript,
3✔
105
        )
3✔
106
        if err != nil {
3✔
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{
3✔
113
                Hash:  p.BreachedCommitTx.TxHash(),
3✔
114
                Index: toRemoteIndex,
3✔
115
        }
3✔
116

3✔
117
        return &breachedInput{
3✔
118
                txOut:    toRemoteTxOut,
3✔
119
                outPoint: toRemoteOutPoint,
3✔
120
                witness:  witness,
3✔
121
                sequence: seq,
3✔
122
        }, nil
3✔
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) {
3✔
130

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

3✔
133
        // First, construct add the breached inputs to our justice transaction
3✔
134
        // and compute the total amount that will be swept.
3✔
135
        var totalAmt btcutil.Amount
3✔
136
        for _, inp := range inputs {
6✔
137
                totalAmt += btcutil.Amount(inp.txOut.Value)
3✔
138
                justiceTxn.AddTxIn(&wire.TxIn{
3✔
139
                        PreviousOutPoint: inp.outPoint,
3✔
140
                        Sequence:         inp.sequence,
3✔
141
                })
3✔
142
        }
3✔
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(
3✔
150
                totalAmt, txWeight, p.JusticeKit.SweepAddress(),
3✔
151
                p.SessionInfo.RewardAddress,
3✔
152
        )
3✔
153
        if err != nil {
3✔
154
                return nil, err
×
155
        }
×
156

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

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

3✔
163
        btx := btcutil.NewTx(justiceTxn)
3✔
164
        if err := blockchain.CheckTransactionSanity(btx); err != nil {
3✔
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)
3✔
171
        for i, txIn := range justiceTxn.TxIn {
6✔
172
                inputIndex[txIn.PreviousOutPoint] = i
3✔
173
        }
3✔
174

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

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

3✔
188
                // Validate the reconstructed witnesses to ensure they are
3✔
189
                // valid for the breached inputs.
3✔
190
                vm, err := txscript.NewEngine(
3✔
191
                        inp.txOut.PkScript, justiceTxn, i,
3✔
192
                        txscript.StandardVerifyFlags,
3✔
193
                        nil, hashes, inp.txOut.Value, prevOutFetcher,
3✔
194
                )
3✔
195
                if err != nil {
3✔
196
                        return nil, err
×
197
                }
×
198
                if err := vm.Execute(); err != nil {
3✔
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
3✔
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) {
3✔
218
        var (
3✔
219
                sweepInputs    = make([]*breachedInput, 0, 2)
3✔
220
                weightEstimate input.TxWeightEstimator
3✔
221
        )
3✔
222

3✔
223
        commitmentType, err := p.SessionInfo.Policy.BlobType.CommitmentType(nil)
3✔
224
        if err != nil {
3✔
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()) {
3✔
UNCOV
231
        case input.P2WPKHSize:
×
UNCOV
232
                weightEstimate.AddP2WKHOutput()
×
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.
237
        case input.P2WSHSize:
3✔
238
                weightEstimate.AddP2WSHOutput()
3✔
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) {
3✔
UNCOV
247
                weightEstimate.AddP2WKHOutput()
×
UNCOV
248
        }
×
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()
3✔
253
        if err != nil {
3✔
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()
3✔
260
        if err != nil {
3✔
261
                return nil, err
×
262
        }
×
263
        weightEstimate.AddWitnessInput(toLocalWitnessSize)
3✔
264

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

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

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

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

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

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

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

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

3✔
299
        return p.assembleJusticeTxn(txWeight, sweepInputs...)
3✔
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) {
3✔
309

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

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

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

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

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

332
        return fetcher, nil
3✔
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