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

lightningnetwork / lnd / 15838907453

24 Jun 2025 01:26AM UTC coverage: 57.079% (-11.1%) from 68.172%
15838907453

Pull #9982

github

web-flow
Merge e42780be2 into 45c15646c
Pull Request #9982: lnwire+lnwallet: add LocalNonces field for splice nonce coordination w/ taproot channels

103 of 167 new or added lines in 5 files covered. (61.68%)

30191 existing lines in 463 files now uncovered.

96331 of 168768 relevant lines covered (57.08%)

0.6 hits per line

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

77.84
/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/watchtower/blob"
16
        "github.com/lightningnetwork/lnd/watchtower/wtdb"
17
)
18

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
130
        justiceTxn := wire.NewMsgTx(2)
1✔
131

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

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

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

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

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

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

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

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

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

204
        return justiceTxn, nil
1✔
205
}
206

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

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

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

233
        case input.P2WSHSize:
1✔
234
                weightEstimate.AddP2WSHOutput()
1✔
235

236
        default:
×
237
                return nil, ErrUnknownSweepAddrType
×
238
        }
239

240
        // Add our reward address to the weight estimate if the policy's blob
241
        // type specifies a reward output.
242
        if p.SessionInfo.Policy.BlobType.Has(blob.FlagReward) {
1✔
UNCOV
243
                weightEstimate.AddP2WKHOutput()
×
UNCOV
244
        }
×
245

246
        // Assemble the breached to-local output from the justice descriptor and
247
        // add it to our weight estimate.
248
        toLocalInput, err := p.commitToLocalInput()
1✔
249
        if err != nil {
1✔
250
                return nil, err
×
251
        }
×
252

253
        // Get the weight for the to-local witness and add that to the
254
        // estimator.
255
        toLocalWitnessSize, err := commitmentType.ToLocalWitnessSize()
1✔
256
        if err != nil {
1✔
257
                return nil, err
×
258
        }
×
259
        weightEstimate.AddWitnessInput(toLocalWitnessSize)
1✔
260

1✔
261
        sweepInputs = append(sweepInputs, toLocalInput)
1✔
262

1✔
263
        log.Debugf("Found to local witness output=%#v, stack=%v",
1✔
264
                toLocalInput.txOut, toLocalInput.witness)
1✔
265

1✔
266
        // If the justice kit specifies that we have to sweep the to-remote
1✔
267
        // output, we'll also try to assemble the output and add it to weight
1✔
268
        // estimate if successful.
1✔
269
        if p.JusticeKit.HasCommitToRemoteOutput() {
2✔
270
                toRemoteInput, err := p.commitToRemoteInput()
1✔
271
                if err != nil {
1✔
272
                        return nil, err
×
273
                }
×
274
                sweepInputs = append(sweepInputs, toRemoteInput)
1✔
275

1✔
276
                log.Debugf("Found to remote witness output=%#v, stack=%v",
1✔
277
                        toRemoteInput.txOut, toRemoteInput.witness)
1✔
278

1✔
279
                // Get the weight for the to-remote witness and add that to the
1✔
280
                // estimator.
1✔
281
                toRemoteWitnessSize, err := commitmentType.ToRemoteWitnessSize()
1✔
282
                if err != nil {
1✔
283
                        return nil, err
×
284
                }
×
285

286
                weightEstimate.AddWitnessInput(toRemoteWitnessSize)
1✔
287
        }
288

289
        // TODO(conner): sweep htlc outputs
290

291
        txWeight := weightEstimate.Weight()
1✔
292

1✔
293
        return p.assembleJusticeTxn(txWeight, sweepInputs...)
1✔
294
}
295

296
// findTxOutByPkScript searches the given transaction for an output whose
297
// pkscript matches the query. If one is found, the TxOut is returned along with
298
// the index.
299
//
300
// NOTE: The search stops after the first match is found.
301
func findTxOutByPkScript(txn *wire.MsgTx,
302
        pkScript *txscript.PkScript) (uint32, *wire.TxOut, error) {
1✔
303

1✔
304
        found, index := input.FindScriptOutputIndex(txn, pkScript.Script())
1✔
305
        if !found {
1✔
306
                return 0, nil, ErrOutputNotFound
×
307
        }
×
308

309
        return index, txn.TxOut[index], nil
1✔
310
}
311

312
// prevOutFetcher returns a txscript.MultiPrevOutFetcher for the given set
313
// of inputs.
314
func prevOutFetcher(inputs []*breachedInput) (*txscript.MultiPrevOutFetcher,
315
        error) {
1✔
316

1✔
317
        fetcher := txscript.NewMultiPrevOutFetcher(nil)
1✔
318
        for _, inp := range inputs {
2✔
319
                if inp.txOut == nil {
1✔
320
                        return nil, fmt.Errorf("missing input utxo information")
×
321
                }
×
322

323
                fetcher.AddPrevOut(inp.outPoint, inp.txOut)
1✔
324
        }
325

326
        return fetcher, nil
1✔
327
}
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