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

lightningnetwork / lnd / 13211764208

08 Feb 2025 03:08AM UTC coverage: 49.288% (-9.5%) from 58.815%
13211764208

Pull #9489

github

calvinrzachman
itest: verify switchrpc server enforces send then track

We prevent the rpc server from allowing onion dispatches for
attempt IDs which have already been tracked by rpc clients.

This helps protect the client from leaking a duplicate onion
attempt. NOTE: This is not the only method for solving this
issue! The issue could be addressed via careful client side
programming which accounts for the uncertainty and async
nature of dispatching onions to a remote process via RPC.
This would require some lnd ChannelRouter changes for how
we intend to use these RPCs though.
Pull Request #9489: multi: add BuildOnion, SendOnion, and TrackOnion RPCs

474 of 990 new or added lines in 11 files covered. (47.88%)

27321 existing lines in 435 files now uncovered.

101192 of 205306 relevant lines covered (49.29%)

1.54 hits per line

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

79.62
/watchtower/wtclient/backup_task.go
1
package wtclient
2

3
import (
4
        "fmt"
5

6
        "github.com/btcsuite/btcd/blockchain"
7
        "github.com/btcsuite/btcd/btcutil"
8
        "github.com/btcsuite/btcd/btcutil/txsort"
9
        "github.com/btcsuite/btcd/chaincfg"
10
        "github.com/btcsuite/btcd/txscript"
11
        "github.com/btcsuite/btcd/wire"
12
        "github.com/lightningnetwork/lnd/input"
13
        "github.com/lightningnetwork/lnd/lnwallet"
14
        "github.com/lightningnetwork/lnd/watchtower/blob"
15
        "github.com/lightningnetwork/lnd/watchtower/wtdb"
16
)
17

18
// backupTask is an internal struct for computing the justice transaction for a
19
// particular revoked state. A backupTask functions as a scratch pad for storing
20
// computing values of the transaction itself, such as the final split in
21
// balance if the justice transaction will give a reward to the tower. The
22
// backup task has three primary phases:
23
//  1. Init: Determines which inputs from the breach transaction will be spent,
24
//     and the total amount contained in the inputs.
25
//  2. Bind: Asserts that the revoked state is eligible under a given session's
26
//     parameters. Certain states may be ineligible due to fee rates, too little
27
//     input amount, etc. Backup of these states can be deferred to a later time
28
//     or session with more favorable parameters. If the session is bound
29
//     successfully, the final session-dependent values to the justice
30
//     transaction are solidified.
31
//  3. Send: Once the task is bound, it will be queued to send to a specific
32
//     tower corresponding to the session in which it was bound. The justice
33
//     transaction will be assembled by examining the parameters left as a
34
//     result of the binding. After the justice transaction is signed, the
35
//     necessary components are stripped out and encrypted before being sent to
36
//     the tower in a StateUpdate.
37
type backupTask struct {
38
        id             wtdb.BackupID
39
        breachInfo     *lnwallet.BreachRetribution
40
        commitmentType blob.CommitmentType
41

42
        // state-dependent variables
43

44
        toLocalInput  input.Input
45
        toRemoteInput input.Input
46
        totalAmt      btcutil.Amount
47
        sweepPkScript []byte
48

49
        // session-dependent variables
50

51
        blobType blob.Type
52
        outputs  []*wire.TxOut
53
}
54

55
// newBackupTask initializes a new backupTask.
56
func newBackupTask(id wtdb.BackupID, sweepPkScript []byte) *backupTask {
3✔
57
        return &backupTask{
3✔
58
                id:            id,
3✔
59
                sweepPkScript: sweepPkScript,
3✔
60
        }
3✔
61
}
3✔
62

63
// inputs returns all non-dust inputs that we will attempt to spend from.
64
//
65
// NOTE: Ordering of the inputs is not critical as we sort the transaction with
66
// BIP69 in a later stage.
67
func (t *backupTask) inputs() map[wire.OutPoint]input.Input {
3✔
68
        inputs := make(map[wire.OutPoint]input.Input)
3✔
69
        if t.toLocalInput != nil {
6✔
70
                inputs[t.toLocalInput.OutPoint()] = t.toLocalInput
3✔
71
        }
3✔
72
        if t.toRemoteInput != nil {
6✔
73
                inputs[t.toRemoteInput.OutPoint()] = t.toRemoteInput
3✔
74
        }
3✔
75

76
        return inputs
3✔
77
}
78

79
// addrType returns the type of an address after parsing it and matching it to
80
// the set of known script templates.
81
func addrType(pkScript []byte) txscript.ScriptClass {
3✔
82
        // We pass in a set of dummy chain params here as they're only needed
3✔
83
        // to make the address struct, which we're ignoring anyway (scripts are
3✔
84
        // always the same, it's addresses that change across chains).
3✔
85
        scriptClass, _, _, _ := txscript.ExtractPkScriptAddrs(
3✔
86
                pkScript, &chaincfg.MainNetParams,
3✔
87
        )
3✔
88

3✔
89
        return scriptClass
3✔
90
}
3✔
91

92
// addScriptWeight parses the passed pkScript and adds the computed weight cost
93
// were the script to be added to the justice transaction.
94
func addScriptWeight(weightEstimate *input.TxWeightEstimator,
95
        pkScript []byte) error {
3✔
96

3✔
97
        switch addrType(pkScript) {
3✔
UNCOV
98
        case txscript.WitnessV0PubKeyHashTy:
×
UNCOV
99
                weightEstimate.AddP2WKHOutput()
×
100

101
        case txscript.WitnessV0ScriptHashTy:
×
102
                weightEstimate.AddP2WSHOutput()
×
103

104
        case txscript.WitnessV1TaprootTy:
3✔
105
                weightEstimate.AddP2TROutput()
3✔
106

107
        default:
×
108
                return fmt.Errorf("invalid addr type: %v", addrType(pkScript))
×
109
        }
110

111
        return nil
3✔
112
}
113

114
// bindSession first populates all state-dependent variables of the task. Then
115
// it determines if the backupTask is compatible with the passed SessionInfo's
116
// policy. If no error is returned, the task has been bound to the session and
117
// can be queued to upload to the tower. Otherwise, the bind failed and should
118
// be rescheduled with a different session.
119
func (t *backupTask) bindSession(session *wtdb.ClientSessionBody,
120
        newBreachRetribution BreachRetributionBuilder) error {
3✔
121

3✔
122
        breachInfo, chanType, err := newBreachRetribution(
3✔
123
                t.id.ChanID, t.id.CommitHeight,
3✔
124
        )
3✔
125
        if err != nil {
3✔
126
                return err
×
127
        }
×
128

129
        commitType, err := session.Policy.BlobType.CommitmentType(&chanType)
3✔
130
        if err != nil {
3✔
131
                return err
×
132
        }
×
133

134
        // Parse the non-dust outputs from the breach transaction,
135
        // simultaneously computing the total amount contained in the inputs
136
        // present. We can't compute the exact output values at this time
137
        // since the task has not been assigned to a session, at which point
138
        // parameters such as fee rate, number of outputs, and reward rate will
139
        // be finalized.
140
        var (
3✔
141
                totalAmt      int64
3✔
142
                toLocalInput  input.Input
3✔
143
                toRemoteInput input.Input
3✔
144
        )
3✔
145

3✔
146
        // Add the sign descriptors and outputs corresponding to the to-local
3✔
147
        // and to-remote outputs, respectively, if either input amount is
3✔
148
        // non-dust. Note that the naming here seems reversed, but both are
3✔
149
        // correct. For example, the to-remote output on the remote party's
3✔
150
        // commitment is an output that pays to us. Hence the retribution refers
3✔
151
        // to that output as local, though relative to their commitment, it is
3✔
152
        // paying to-the-remote party (which is us).
3✔
153
        if breachInfo.RemoteOutputSignDesc != nil {
6✔
154
                toLocalInput, err = commitType.ToLocalInput(breachInfo)
3✔
155
                if err != nil {
3✔
156
                        return err
×
157
                }
×
158

159
                totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value
3✔
160
        }
161
        if breachInfo.LocalOutputSignDesc != nil {
6✔
162
                toRemoteInput, err = commitType.ToRemoteInput(breachInfo)
3✔
163
                if err != nil {
3✔
164
                        return err
×
165
                }
×
166

167
                totalAmt += breachInfo.LocalOutputSignDesc.Output.Value
3✔
168
        }
169

170
        t.commitmentType = commitType
3✔
171
        t.breachInfo = breachInfo
3✔
172
        t.toLocalInput = toLocalInput
3✔
173
        t.toRemoteInput = toRemoteInput
3✔
174
        t.totalAmt = btcutil.Amount(totalAmt)
3✔
175

3✔
176
        // First we'll begin by deriving a weight estimate for the justice
3✔
177
        // transaction. The final weight can be different depending on whether
3✔
178
        // the watchtower is taking a reward.
3✔
179
        var weightEstimate input.TxWeightEstimator
3✔
180

3✔
181
        // Next, add the contribution from the inputs that are present on this
3✔
182
        // breach transaction.
3✔
183
        if t.toLocalInput != nil {
6✔
184
                toLocalWitnessSize, err := commitType.ToLocalWitnessSize()
3✔
185
                if err != nil {
3✔
186
                        return err
×
187
                }
×
188

189
                weightEstimate.AddWitnessInput(toLocalWitnessSize)
3✔
190
        }
191
        if t.toRemoteInput != nil {
6✔
192
                toRemoteWitnessSize, err := commitType.ToRemoteWitnessSize()
3✔
193
                if err != nil {
3✔
194
                        return err
×
195
                }
×
196

197
                weightEstimate.AddWitnessInput(toRemoteWitnessSize)
3✔
198
        }
199

200
        // All justice transactions will either use segwit v0 (p2wkh + p2wsh)
201
        // or segwit v1 (p2tr).
202
        err = addScriptWeight(&weightEstimate, t.sweepPkScript)
3✔
203
        if err != nil {
3✔
204
                return err
×
205
        }
×
206

207
        // If the justice transaction has a reward output, add the output's
208
        // contribution to the weight estimate.
209
        if session.Policy.BlobType.Has(blob.FlagReward) {
3✔
UNCOV
210
                err := addScriptWeight(&weightEstimate, session.RewardPkScript)
×
UNCOV
211
                if err != nil {
×
212
                        return err
×
213
                }
×
214
        }
215

216
        // Now, compute the output values depending on whether FlagReward is set
217
        // in the current session's policy.
218
        outputs, err := session.Policy.ComputeJusticeTxOuts(
3✔
219
                t.totalAmt, weightEstimate.Weight(),
3✔
220
                t.sweepPkScript, session.RewardPkScript,
3✔
221
        )
3✔
222
        if err != nil {
3✔
UNCOV
223
                return err
×
UNCOV
224
        }
×
225

226
        t.blobType = session.Policy.BlobType
3✔
227
        t.outputs = outputs
3✔
228

3✔
229
        return nil
3✔
230
}
231

232
// craftSessionPayload is the final stage for a backupTask, and generates the
233
// encrypted payload and breach hint that should be sent to the tower. This
234
// method computes the final justice transaction using the bound
235
// session-dependent variables, and signs the resulting transaction. The
236
// required pieces from signatures, witness scripts, etc are then packaged into
237
// a JusticeKit and encrypted using the breach transaction's key.
238
func (t *backupTask) craftSessionPayload(
239
        signer input.Signer) (blob.BreachHint, []byte, error) {
3✔
240

3✔
241
        var hint blob.BreachHint
3✔
242

3✔
243
        justiceKit, err := t.commitmentType.NewJusticeKit(
3✔
244
                t.sweepPkScript, t.breachInfo, t.toRemoteInput != nil,
3✔
245
        )
3✔
246
        if err != nil {
3✔
247
                return hint, nil, err
×
248
        }
×
249

250
        // Now, begin construction of the justice transaction. We'll start with
251
        // a version 2 transaction.
252
        justiceTxn := wire.NewMsgTx(2)
3✔
253

3✔
254
        // Next, add the non-dust inputs that were derived from the breach
3✔
255
        // information. This will either be contain both the to-local and
3✔
256
        // to-remote outputs, or only be the to-local output.
3✔
257
        inputs := t.inputs()
3✔
258
        prevOutputFetcher := txscript.NewMultiPrevOutFetcher(nil)
3✔
259
        for prevOutPoint, inp := range inputs {
6✔
260
                prevOutputFetcher.AddPrevOut(
3✔
261
                        prevOutPoint, inp.SignDesc().Output,
3✔
262
                )
3✔
263
                justiceTxn.AddTxIn(&wire.TxIn{
3✔
264
                        PreviousOutPoint: prevOutPoint,
3✔
265
                        Sequence:         inp.BlocksToMaturity(),
3✔
266
                })
3✔
267
        }
3✔
268

269
        // Add the sweep output paying directly to the user and possibly a
270
        // reward output, using the outputs computed when the task was bound.
271
        justiceTxn.TxOut = t.outputs
3✔
272

3✔
273
        // Sort the justice transaction according to BIP69.
3✔
274
        txsort.InPlaceSort(justiceTxn)
3✔
275

3✔
276
        // Check that the justice transaction meets basic validity requirements
3✔
277
        // before attempting to attach the witnesses.
3✔
278
        btx := btcutil.NewTx(justiceTxn)
3✔
279
        if err := blockchain.CheckTransactionSanity(btx); err != nil {
3✔
280
                return hint, nil, err
×
281
        }
×
282

283
        // Construct a sighash cache to improve signing performance.
284
        hashCache := txscript.NewTxSigHashes(justiceTxn, prevOutputFetcher)
3✔
285

3✔
286
        // Since the transaction inputs could have been reordered as a result of
3✔
287
        // the BIP69 sort, create an index mapping each prevout to it's new
3✔
288
        // index.
3✔
289
        inputIndex := make(map[wire.OutPoint]int)
3✔
290
        for i, txIn := range justiceTxn.TxIn {
6✔
291
                inputIndex[txIn.PreviousOutPoint] = i
3✔
292
        }
3✔
293

294
        // Now, iterate through the list of inputs that were initially added to
295
        // the transaction and store the computed witness within the justice
296
        // kit.
297
        commitType := t.commitmentType
3✔
298
        for _, inp := range inputs {
6✔
299
                // Lookup the input's new post-sort position.
3✔
300
                i := inputIndex[inp.OutPoint()]
3✔
301

3✔
302
                // Construct the full witness required to spend this input.
3✔
303
                inputScript, err := inp.CraftInputScript(
3✔
304
                        signer, justiceTxn, hashCache, prevOutputFetcher, i,
3✔
305
                )
3✔
306
                if err != nil {
3✔
307
                        return hint, nil, err
×
308
                }
×
309

310
                signature, err := commitType.ParseRawSig(inputScript.Witness)
3✔
311
                if err != nil {
3✔
312
                        return hint, nil, err
×
313
                }
×
314

315
                toLocalWitnessType, err := commitType.ToLocalWitnessType()
3✔
316
                if err != nil {
3✔
317
                        return hint, nil, err
×
318
                }
×
319

320
                toRemoteWitnessType, err := commitType.ToRemoteWitnessType()
3✔
321
                if err != nil {
3✔
322
                        return hint, nil, err
×
323
                }
×
324

325
                // Finally, copy the serialized signature into the justice kit,
326
                // using the input's witness type to select the appropriate
327
                // field
328
                switch inp.WitnessType() {
3✔
329
                case toLocalWitnessType:
3✔
330
                        justiceKit.AddToLocalSig(signature)
3✔
331

332
                case toRemoteWitnessType:
3✔
333
                        justiceKit.AddToRemoteSig(signature)
3✔
334
                default:
×
335
                        return hint, nil, fmt.Errorf("invalid witness type: %v",
×
336
                                inp.WitnessType())
×
337
                }
338
        }
339

340
        breachTxID := t.breachInfo.BreachTxHash
3✔
341

3✔
342
        // Compute the breach hint as SHA256(txid)[:16] and breach key as
3✔
343
        // SHA256(txid || txid).
3✔
344
        hint, key := blob.NewBreachHintAndKeyFromHash(&breachTxID)
3✔
345

3✔
346
        // Then, we'll encrypt the computed justice kit using the full breach
3✔
347
        // transaction id, which will allow the tower to recover the contents
3✔
348
        // after the transaction is seen in the chain or mempool.
3✔
349
        encBlob, err := blob.Encrypt(justiceKit, key)
3✔
350
        if err != nil {
3✔
351
                return hint, nil, err
×
352
        }
×
353

354
        return hint, encBlob, nil
3✔
355
}
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