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

lightningnetwork / lnd / 12312390362

13 Dec 2024 08:44AM UTC coverage: 57.458% (+8.5%) from 48.92%
12312390362

Pull #9343

github

ellemouton
fn: rework the ContextGuard and add tests

In this commit, the ContextGuard struct is re-worked such that the
context that its new main WithCtx method provides is cancelled in sync
with a parent context being cancelled or with it's quit channel being
cancelled. Tests are added to assert the behaviour. In order for the
close of the quit channel to be consistent with the cancelling of the
derived context, the quit channel _must_ be contained internal to the
ContextGuard so that callers are only able to close the channel via the
exposed Quit method which will then take care to first cancel any
derived context that depend on the quit channel before returning.
Pull Request #9343: fn: expand the ContextGuard and add tests

101853 of 177264 relevant lines covered (57.46%)

24972.93 hits per line

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

68.47
/sweep/txgenerator.go
1
package sweep
2

3
import (
4
        "errors"
5
        "fmt"
6
        "sort"
7
        "strings"
8

9
        "github.com/btcsuite/btcd/blockchain"
10
        "github.com/btcsuite/btcd/btcutil"
11
        "github.com/btcsuite/btcd/txscript"
12
        "github.com/btcsuite/btcd/wire"
13
        "github.com/lightningnetwork/lnd/input"
14
        "github.com/lightningnetwork/lnd/lnwallet"
15
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
16
)
17

18
var (
19
        // DefaultMaxInputsPerTx specifies the default maximum number of inputs
20
        // allowed in a single sweep tx. If more need to be swept, multiple txes
21
        // are created and published.
22
        DefaultMaxInputsPerTx = uint32(100)
23

24
        // ErrLocktimeConflict is returned when inputs with different
25
        // transaction nLockTime values are included in the same transaction.
26
        //
27
        // NOTE: due the SINGLE|ANYONECANPAY sighash flag, which is used in the
28
        // second level success/timeout txns, only the txns sharing the same
29
        // nLockTime can exist in the same tx.
30
        ErrLocktimeConflict = errors.New("incompatible locktime")
31
)
32

33
// createSweepTx builds a signed tx spending the inputs to the given outputs,
34
// sending any leftover change to the change script.
35
func createSweepTx(inputs []input.Input, outputs []*wire.TxOut,
36
        changePkScript []byte, currentBlockHeight uint32,
37
        feeRate, maxFeeRate chainfee.SatPerKWeight,
38
        signer input.Signer) (*wire.MsgTx, btcutil.Amount, error) {
2✔
39

2✔
40
        inputs, estimator, err := getWeightEstimate(
2✔
41
                inputs, outputs, feeRate, maxFeeRate, [][]byte{changePkScript},
2✔
42
        )
2✔
43
        if err != nil {
2✔
44
                return nil, 0, err
×
45
        }
×
46

47
        txFee := estimator.feeWithParent()
2✔
48

2✔
49
        var (
2✔
50
                // Create the sweep transaction that we will be building. We
2✔
51
                // use version 2 as it is required for CSV.
2✔
52
                sweepTx = wire.NewMsgTx(2)
2✔
53

2✔
54
                // Track whether any of the inputs require a certain locktime.
2✔
55
                locktime = int32(-1)
2✔
56

2✔
57
                // We keep track of total input amount, and required output
2✔
58
                // amount to use for calculating the change amount below.
2✔
59
                totalInput     btcutil.Amount
2✔
60
                requiredOutput btcutil.Amount
2✔
61

2✔
62
                // We'll add the inputs as we go so we know the final ordering
2✔
63
                // of inputs to sign.
2✔
64
                idxs []input.Input
2✔
65
        )
2✔
66

2✔
67
        // We start by adding all inputs that commit to an output. We do this
2✔
68
        // since the input and output index must stay the same for the
2✔
69
        // signatures to be valid.
2✔
70
        for _, o := range inputs {
5✔
71
                if o.RequiredTxOut() == nil {
6✔
72
                        continue
3✔
73
                }
74

75
                idxs = append(idxs, o)
×
76
                sweepTx.AddTxIn(&wire.TxIn{
×
77
                        PreviousOutPoint: o.OutPoint(),
×
78
                        Sequence:         o.BlocksToMaturity(),
×
79
                })
×
80
                sweepTx.AddTxOut(o.RequiredTxOut())
×
81

×
82
                if lt, ok := o.RequiredLockTime(); ok {
×
83
                        // If another input commits to a different locktime,
×
84
                        // they cannot be combined in the same transaction.
×
85
                        if locktime != -1 && locktime != int32(lt) {
×
86
                                return nil, 0, ErrLocktimeConflict
×
87
                        }
×
88

89
                        locktime = int32(lt)
×
90
                }
91

92
                totalInput += btcutil.Amount(o.SignDesc().Output.Value)
×
93
                requiredOutput += btcutil.Amount(o.RequiredTxOut().Value)
×
94
        }
95

96
        // Sum up the value contained in the remaining inputs, and add them to
97
        // the sweep transaction.
98
        for _, o := range inputs {
5✔
99
                if o.RequiredTxOut() != nil {
3✔
100
                        continue
×
101
                }
102

103
                idxs = append(idxs, o)
3✔
104
                sweepTx.AddTxIn(&wire.TxIn{
3✔
105
                        PreviousOutPoint: o.OutPoint(),
3✔
106
                        Sequence:         o.BlocksToMaturity(),
3✔
107
                })
3✔
108

3✔
109
                if lt, ok := o.RequiredLockTime(); ok {
3✔
110
                        if locktime != -1 && locktime != int32(lt) {
×
111
                                return nil, 0, ErrLocktimeConflict
×
112
                        }
×
113

114
                        locktime = int32(lt)
×
115
                }
116

117
                totalInput += btcutil.Amount(o.SignDesc().Output.Value)
3✔
118
        }
119

120
        // Add the outputs given, if any.
121
        for _, o := range outputs {
2✔
122
                sweepTx.AddTxOut(o)
×
123
                requiredOutput += btcutil.Amount(o.Value)
×
124
        }
×
125

126
        if requiredOutput+txFee > totalInput {
2✔
127
                return nil, 0, fmt.Errorf("insufficient input to create sweep "+
×
128
                        "tx: input_sum=%v, output_sum=%v", totalInput,
×
129
                        requiredOutput+txFee)
×
130
        }
×
131

132
        // The value remaining after the required output and fees, go to
133
        // change. Not that this fee is what we would have to pay in case the
134
        // sweep tx has a change output.
135
        changeAmt := totalInput - requiredOutput - txFee
2✔
136

2✔
137
        // We'll calculate the dust limit for the given changePkScript since it
2✔
138
        // is variable.
2✔
139
        changeLimit := lnwallet.DustLimitForSize(len(changePkScript))
2✔
140

2✔
141
        // The txn will sweep the amount after fees to the pkscript generated
2✔
142
        // above.
2✔
143
        if changeAmt >= changeLimit {
4✔
144
                sweepTx.AddTxOut(&wire.TxOut{
2✔
145
                        PkScript: changePkScript,
2✔
146
                        Value:    int64(changeAmt),
2✔
147
                })
2✔
148
        } else {
2✔
149
                log.Infof("Change amt %v below dustlimit %v, not adding "+
×
150
                        "change output", changeAmt, changeLimit)
×
151

×
152
                // The dust amount is added to the fee as the miner will
×
153
                // collect it.
×
154
                txFee += changeAmt
×
155
        }
×
156

157
        // We'll default to using the current block height as locktime, if none
158
        // of the inputs commits to a different locktime.
159
        sweepTx.LockTime = currentBlockHeight
2✔
160
        if locktime != -1 {
2✔
161
                sweepTx.LockTime = uint32(locktime)
×
162
        }
×
163

164
        // Before signing the transaction, check to ensure that it meets some
165
        // basic validity requirements.
166
        //
167
        // TODO(conner): add more control to sanity checks, allowing us to
168
        // delay spending "problem" outputs, e.g. possibly batching with other
169
        // classes if fees are too low.
170
        btx := btcutil.NewTx(sweepTx)
2✔
171
        if err := blockchain.CheckTransactionSanity(btx); err != nil {
2✔
172
                return nil, 0, err
×
173
        }
×
174

175
        prevInputFetcher, err := input.MultiPrevOutFetcher(inputs)
2✔
176
        if err != nil {
2✔
177
                return nil, 0, fmt.Errorf("error creating prev input fetcher "+
×
178
                        "for hash cache: %v", err)
×
179
        }
×
180
        hashCache := txscript.NewTxSigHashes(sweepTx, prevInputFetcher)
2✔
181

2✔
182
        // With all the inputs in place, use each output's unique input script
2✔
183
        // function to generate the final witness required for spending.
2✔
184
        addInputScript := func(idx int, tso input.Input) error {
5✔
185
                inputScript, err := tso.CraftInputScript(
3✔
186
                        signer, sweepTx, hashCache, prevInputFetcher, idx,
3✔
187
                )
3✔
188
                if err != nil {
3✔
189
                        return err
×
190
                }
×
191

192
                sweepTx.TxIn[idx].Witness = inputScript.Witness
3✔
193

3✔
194
                if len(inputScript.SigScript) != 0 {
3✔
195
                        sweepTx.TxIn[idx].SignatureScript =
×
196
                                inputScript.SigScript
×
197
                }
×
198

199
                return nil
3✔
200
        }
201

202
        for idx, inp := range idxs {
5✔
203
                if err := addInputScript(idx, inp); err != nil {
3✔
204
                        return nil, 0, err
×
205
                }
×
206
        }
207

208
        log.Debugf("Creating sweep transaction %v for %v inputs (%s) "+
2✔
209
                "using %v, tx_weight=%v, tx_fee=%v, parents_count=%v, "+
2✔
210
                "parents_fee=%v, parents_weight=%v, current_height=%v",
2✔
211
                sweepTx.TxHash(), len(inputs),
2✔
212
                inputTypeSummary(inputs), feeRate,
2✔
213
                estimator.weight(), txFee,
2✔
214
                len(estimator.parents), estimator.parentsFee,
2✔
215
                estimator.parentsWeight, currentBlockHeight)
2✔
216

2✔
217
        return sweepTx, txFee, nil
2✔
218
}
219

220
// getWeightEstimate returns a weight estimate for the given inputs.
221
// Additionally, it returns counts for the number of csv and cltv inputs.
222
func getWeightEstimate(inputs []input.Input, outputs []*wire.TxOut,
223
        feeRate, maxFeeRate chainfee.SatPerKWeight,
224
        outputPkScripts [][]byte) ([]input.Input, *weightEstimator, error) {
42✔
225

42✔
226
        // We initialize a weight estimator so we can accurately asses the
42✔
227
        // amount of fees we need to pay for this sweep transaction.
42✔
228
        //
42✔
229
        // TODO(roasbeef): can be more intelligent about buffering outputs to
42✔
230
        // be more efficient on-chain.
42✔
231
        weightEstimate := newWeightEstimator(feeRate, maxFeeRate)
42✔
232

42✔
233
        // Our sweep transaction will always pay to the given set of outputs.
42✔
234
        for _, o := range outputs {
42✔
235
                weightEstimate.addOutput(o)
×
236
        }
×
237

238
        // If there is any leftover change after paying to the given outputs
239
        // and required outputs, it will go to a single segwit p2wkh or p2tr
240
        // address. This will be our change address, so ensure it contributes
241
        // to our weight estimate. Note that if we have other outputs, we might
242
        // end up creating a sweep tx without a change output. It is okay to
243
        // add the change output to the weight estimate regardless, since the
244
        // estimated fee will just be subtracted from this already dust output,
245
        // and trimmed.
246
        for _, outputPkScript := range outputPkScripts {
84✔
247
                switch {
42✔
248
                case txscript.IsPayToTaproot(outputPkScript):
32✔
249
                        weightEstimate.addP2TROutput()
32✔
250

251
                case txscript.IsPayToWitnessScriptHash(outputPkScript):
1✔
252
                        weightEstimate.addP2WSHOutput()
1✔
253

254
                case txscript.IsPayToWitnessPubKeyHash(outputPkScript):
4✔
255
                        weightEstimate.addP2WKHOutput()
4✔
256

257
                case txscript.IsPayToPubKeyHash(outputPkScript):
1✔
258
                        weightEstimate.estimator.AddP2PKHOutput()
1✔
259

260
                case txscript.IsPayToScriptHash(outputPkScript):
1✔
261
                        weightEstimate.estimator.AddP2SHOutput()
1✔
262

263
                default:
3✔
264
                        // Unknown script type.
3✔
265
                        return nil, nil, fmt.Errorf("unknown script "+
3✔
266
                                "type: %x", outputPkScript)
3✔
267
                }
268
        }
269

270
        // For each output, use its witness type to determine the estimate
271
        // weight of its witness, and add it to the proper set of spendable
272
        // outputs.
273
        var sweepInputs []input.Input
39✔
274
        for i := range inputs {
97✔
275
                inp := inputs[i]
58✔
276

58✔
277
                err := weightEstimate.add(inp)
58✔
278
                if err != nil {
58✔
279
                        // TODO(yy): check if this is even possible? If so, we
×
280
                        // should return the error here instead of filtering!
×
281
                        log.Errorf("Failed to get weight estimate for "+
×
282
                                "input=%v, witnessType=%v: %v ", inp.OutPoint(),
×
283
                                inp.WitnessType(), err)
×
284

×
285
                        // Skip inputs for which no weight estimate can be
×
286
                        // given.
×
287
                        continue
×
288
                }
289

290
                // If this input comes with a committed output, add that as
291
                // well.
292
                if inp.RequiredTxOut() != nil {
58✔
293
                        weightEstimate.addOutput(inp.RequiredTxOut())
×
294
                }
×
295

296
                sweepInputs = append(sweepInputs, inp)
58✔
297
        }
298

299
        return sweepInputs, weightEstimate, nil
39✔
300
}
301

302
// inputSummary returns a string containing a human readable summary about the
303
// witness types of a list of inputs.
304
func inputTypeSummary(inputs []input.Input) string {
55✔
305
        // Sort inputs by witness type.
55✔
306
        sortedInputs := make([]input.Input, len(inputs))
55✔
307
        copy(sortedInputs, inputs)
55✔
308
        sort.Slice(sortedInputs, func(i, j int) bool {
59✔
309
                return sortedInputs[i].WitnessType().String() <
4✔
310
                        sortedInputs[j].WitnessType().String()
4✔
311
        })
4✔
312

313
        var parts []string
55✔
314
        for _, i := range sortedInputs {
112✔
315
                part := fmt.Sprintf("%v (%v)", i.OutPoint(), i.WitnessType())
57✔
316
                parts = append(parts, part)
57✔
317
        }
57✔
318
        return strings.Join(parts, ", ")
55✔
319
}
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