Skip to content

Commit c38544b

Browse files
Roasbeefguggero
authored andcommitted
sweep: update sweeper to use AuxSweeper to add extra change addr
In this commit, we start to use the AuxSweeper (if present) to obtain a new extra change addr we should add to the sweeping transaction. With this, we'll take the set of inputs and our change addr, and then maybe gain a new change addr to add to the sweep transaction. The extra change addr will be treated as an extra required tx out, shared across all the relevant inputs. This'll also be used in NeedWalletInput to make sure that we add an extra input if needed to be able to pay for the change addr.
1 parent dc02260 commit c38544b

File tree

4 files changed

+139
-55
lines changed

4 files changed

+139
-55
lines changed

sweep/fee_bumper.go

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/btcsuite/btcd/txscript"
1313
"github.com/btcsuite/btcd/wire"
1414
"github.com/btcsuite/btcwallet/chain"
15+
"github.com/davecgh/go-spew/spew"
1516
"github.com/lightningnetwork/lnd/chainntnfs"
1617
"github.com/lightningnetwork/lnd/fn"
1718
"github.com/lightningnetwork/lnd/input"
@@ -131,6 +132,8 @@ type BumpRequest struct {
131132
func (r *BumpRequest) MaxFeeRateAllowed() (chainfee.SatPerKWeight, error) {
132133
// Get the size of the sweep tx, which will be used to calculate the
133134
// budget fee rate.
135+
//
136+
// TODO(roasbeef): also wants the extra change output?
134137
size, err := calcSweepTxWeight(
135138
r.Inputs, r.DeliveryAddress.DeliveryAddress,
136139
)
@@ -175,7 +178,7 @@ func calcSweepTxWeight(inputs []input.Input,
175178
// TODO(yy): we should refactor the weight estimator to not require a
176179
// fee rate and max fee rate and make it a pure tx weight calculator.
177180
_, estimator, err := getWeightEstimate(
178-
inputs, nil, feeRate, 0, outputPkScript,
181+
inputs, nil, feeRate, 0, [][]byte{outputPkScript},
179182
)
180183
if err != nil {
181184
return 0, err
@@ -1159,9 +1162,9 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input,
11591162
feeRate chainfee.SatPerKWeight) (*sweepTxCtx, error) {
11601163

11611164
// Validate and calculate the fee and change amount.
1162-
txFee, changeAmtOpt, locktimeOpt, err := prepareSweepTx(
1163-
inputs, changePkScript.DeliveryAddress, feeRate,
1164-
t.currentHeight.Load(),
1165+
txFee, changeOutputsOpt, locktimeOpt, err := prepareSweepTx(
1166+
inputs, changePkScript, feeRate, t.currentHeight.Load(),
1167+
t.cfg.AuxSweeper,
11651168
)
11661169
if err != nil {
11671170
return nil, err
@@ -1207,12 +1210,12 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input,
12071210
})
12081211
}
12091212

1210-
// If there's a change amount, add it to the transaction.
1211-
changeAmtOpt.WhenSome(func(changeAmt btcutil.Amount) {
1212-
sweepTx.AddTxOut(&wire.TxOut{
1213-
PkScript: changePkScript.DeliveryAddress,
1214-
Value: int64(changeAmt),
1215-
})
1213+
// If we have change outputs to add, then add it the sweep transaction
1214+
// here.
1215+
changeOutputsOpt.WhenSome(func(changeOuts []SweepOutput) {
1216+
for i := range changeOuts {
1217+
sweepTx.AddTxOut(&changeOuts[i].TxOut)
1218+
}
12161219
})
12171220

12181221
// We'll default to using the current block height as locktime, if none
@@ -1256,31 +1259,64 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input,
12561259
log.Debugf("Created sweep tx %v for inputs:\n%v", sweepTx.TxHash(),
12571260
inputTypeSummary(inputs))
12581261

1262+
// Try to locate the extra change output, though there might be None.
1263+
extraTxOut := fn.MapOption(func(sweepOuts []SweepOutput) fn.Option[SweepOutput] { //nolint:lll
1264+
for _, sweepOut := range sweepOuts {
1265+
if sweepOut.IsExtra {
1266+
log.Infof("Sweep produced extra_sweep_out=%v",
1267+
spew.Sdump(sweepOut))
1268+
1269+
return fn.Some(sweepOut)
1270+
}
1271+
}
1272+
1273+
return fn.None[SweepOutput]()
1274+
})(changeOutputsOpt)
1275+
12591276
return &sweepTxCtx{
1260-
tx: sweepTx,
1261-
fee: txFee,
1277+
tx: sweepTx,
1278+
fee: txFee,
1279+
extraTxOut: fn.FlattenOption(extraTxOut),
12621280
}, nil
12631281
}
12641282

1265-
// prepareSweepTx returns the tx fee, an optional change amount and an optional
1266-
// locktime after a series of validations:
1283+
// prepareSweepTx returns the tx fee, a set of optional change outputs and an
1284+
// optional locktime after a series of validations:
12671285
// 1. check the locktime has been reached.
12681286
// 2. check the locktimes are the same.
12691287
// 3. check the inputs cover the outputs.
12701288
//
12711289
// NOTE: if the change amount is below dust, it will be added to the tx fee.
1272-
func prepareSweepTx(inputs []input.Input, changePkScript []byte,
1273-
feeRate chainfee.SatPerKWeight, currentHeight int32) (
1274-
btcutil.Amount, fn.Option[btcutil.Amount], fn.Option[int32], error) {
1290+
func prepareSweepTx(inputs []input.Input, changePkScript lnwallet.AddrWithKey,
1291+
feeRate chainfee.SatPerKWeight, currentHeight int32,
1292+
auxSweeper fn.Option[AuxSweeper]) (
1293+
btcutil.Amount, fn.Option[[]SweepOutput], fn.Option[int32], error) {
12751294

1276-
noChange := fn.None[btcutil.Amount]()
1295+
noChange := fn.None[[]SweepOutput]()
12771296
noLocktime := fn.None[int32]()
12781297

1298+
// Given the set of inputs we have, if we have an aux sweeper, then
1299+
// we'll attempt to see if we have any other change outputs we'll need
1300+
// to add to the sweep transaction.
1301+
changePkScripts := [][]byte{changePkScript.DeliveryAddress}
1302+
extraChangeOut := fn.MapOptionZ(
1303+
auxSweeper,
1304+
func(aux AuxSweeper) fn.Result[SweepOutput] {
1305+
return aux.DeriveSweepAddr(inputs, changePkScript)
1306+
},
1307+
)
1308+
if err := extraChangeOut.Err(); err != nil {
1309+
return 0, noChange, noLocktime, err
1310+
}
1311+
extraChangeOut.WhenResult(func(o SweepOutput) {
1312+
changePkScripts = append(changePkScripts, o.PkScript)
1313+
})
1314+
12791315
// Creating a weight estimator with nil outputs and zero max fee rate.
12801316
// We don't allow adding customized outputs in the sweeping tx, and the
12811317
// fee rate is already being managed before we get here.
12821318
inputs, estimator, err := getWeightEstimate(
1283-
inputs, nil, feeRate, 0, changePkScript,
1319+
inputs, nil, feeRate, 0, changePkScripts,
12841320
)
12851321
if err != nil {
12861322
return 0, noChange, noLocktime, err
@@ -1298,6 +1334,12 @@ func prepareSweepTx(inputs []input.Input, changePkScript []byte,
12981334
requiredOutput btcutil.Amount
12991335
)
13001336

1337+
// If we have an extra change output, then we'll add it as a required
1338+
// output amt.
1339+
extraChangeOut.WhenResult(func(o SweepOutput) {
1340+
requiredOutput += btcutil.Amount(o.Value)
1341+
})
1342+
13011343
// Go through each input and check if the required lock times have
13021344
// reached and are the same.
13031345
for _, o := range inputs {
@@ -1345,14 +1387,21 @@ func prepareSweepTx(inputs []input.Input, changePkScript []byte,
13451387
// change output.
13461388
changeAmt := totalInput - requiredOutput - txFee
13471389

1390+
changeOuts := make([]SweepOutput, 0, 2)
1391+
1392+
extraChangeOut.WhenResult(func(o SweepOutput) {
1393+
changeOuts = append(changeOuts, o)
1394+
})
1395+
13481396
// We'll calculate the dust limit for the given changePkScript since it
13491397
// is variable.
1350-
changeFloor := lnwallet.DustLimitForSize(len(changePkScript))
1351-
1352-
changeAmtOpt := fn.Some(changeAmt)
1398+
changeFloor := lnwallet.DustLimitForSize(
1399+
len(changePkScript.DeliveryAddress),
1400+
)
13531401

1354-
// If the change amount is dust, we'll move it into the fees.
13551402
switch {
1403+
// If the change amount is dust, we'll move it into the fees, and
1404+
// ignore it.
13561405
case changeAmt < changeFloor:
13571406
log.Infof("Change amt %v below dustlimit %v, not adding "+
13581407
"change output", changeAmt, changeFloor)
@@ -1368,8 +1417,16 @@ func prepareSweepTx(inputs []input.Input, changePkScript []byte,
13681417
// The dust amount is added to the fee.
13691418
txFee += changeAmt
13701419

1371-
// Set the change amount to none.
1372-
changeAmtOpt = fn.None[btcutil.Amount]()
1420+
// Otherwise, we'll actually recognize it as a change output.
1421+
default:
1422+
changeOuts = append(changeOuts, SweepOutput{
1423+
TxOut: wire.TxOut{
1424+
Value: int64(changeAmt),
1425+
PkScript: changePkScript.DeliveryAddress,
1426+
},
1427+
IsExtra: false,
1428+
InternalKey: changePkScript.InternalKey,
1429+
})
13731430
}
13741431

13751432
// Optionally set the locktime.
@@ -1378,12 +1435,17 @@ func prepareSweepTx(inputs []input.Input, changePkScript []byte,
13781435
locktimeOpt = noLocktime
13791436
}
13801437

1438+
var changeOutsOpt fn.Option[[]SweepOutput]
1439+
if len(changeOuts) > 0 {
1440+
changeOutsOpt = fn.Some(changeOuts)
1441+
}
1442+
13811443
log.Debugf("Creating sweep tx for %v inputs (%s) using %v, "+
13821444
"tx_weight=%v, tx_fee=%v, locktime=%v, parents_count=%v, "+
13831445
"parents_fee=%v, parents_weight=%v, current_height=%v",
13841446
len(inputs), inputTypeSummary(inputs), feeRate,
13851447
estimator.weight(), txFee, locktimeOpt, len(estimator.parents),
13861448
estimator.parentsFee, estimator.parentsWeight, currentHeight)
13871449

1388-
return txFee, changeAmtOpt, locktimeOpt, nil
1450+
return txFee, changeOutsOpt, locktimeOpt, nil
13891451
}

sweep/tx_input_set.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,26 @@ func (b *BudgetInputSet) NeedWalletInput() bool {
220220
budgetBorrowable btcutil.Amount
221221
)
222222

223+
// If any of the outputs in the set have a resolution blob, then this
224+
// means we'll end up needing an extra change output. We'll tack this
225+
// on now as an extra portion of the budget.
226+
extraRequiredTxOut := fn.Any(func(i *SweeperInput) bool {
227+
// If there's a required txout, then we don't count this as
228+
// it'll be a second level HTLC.
229+
if i.RequiredTxOut() != nil {
230+
return false
231+
}
232+
233+
// Otherwise, we need one if we have a resolution blob.
234+
return i.ResolutionBlob().IsSome()
235+
}, b.inputs)
236+
237+
if extraRequiredTxOut {
238+
// TODO(roasbeef): aux sweeper ext to ask for extra output
239+
// params and value?
240+
budgetNeeded += 1_000
241+
}
242+
223243
for _, inp := range b.inputs {
224244
// If this input has a required output, we can assume it's a
225245
// second-level htlc txns input. Although this input must have

sweep/txgenerator.go

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func createSweepTx(inputs []input.Input, outputs []*wire.TxOut,
3838
signer input.Signer) (*wire.MsgTx, btcutil.Amount, error) {
3939

4040
inputs, estimator, err := getWeightEstimate(
41-
inputs, outputs, feeRate, maxFeeRate, changePkScript,
41+
inputs, outputs, feeRate, maxFeeRate, [][]byte{changePkScript},
4242
)
4343
if err != nil {
4444
return nil, 0, err
@@ -221,7 +221,7 @@ func createSweepTx(inputs []input.Input, outputs []*wire.TxOut,
221221
// Additionally, it returns counts for the number of csv and cltv inputs.
222222
func getWeightEstimate(inputs []input.Input, outputs []*wire.TxOut,
223223
feeRate, maxFeeRate chainfee.SatPerKWeight,
224-
outputPkScript []byte) ([]input.Input, *weightEstimator, error) {
224+
outputPkScripts [][]byte) ([]input.Input, *weightEstimator, error) {
225225

226226
// We initialize a weight estimator so we can accurately asses the
227227
// amount of fees we need to pay for this sweep transaction.
@@ -237,31 +237,33 @@ func getWeightEstimate(inputs []input.Input, outputs []*wire.TxOut,
237237

238238
// If there is any leftover change after paying to the given outputs
239239
// 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 to
241-
// our weight estimate. Note that if we have other outputs, we might end
242-
// up creating a sweep tx without a change output. It is okay to add the
243-
// change output to the weight estimate regardless, since the estimated
244-
// fee will just be subtracted from this already dust output, and
245-
// trimmed.
246-
switch {
247-
case txscript.IsPayToTaproot(outputPkScript):
248-
weightEstimate.addP2TROutput()
249-
250-
case txscript.IsPayToWitnessScriptHash(outputPkScript):
251-
weightEstimate.addP2WSHOutput()
252-
253-
case txscript.IsPayToWitnessPubKeyHash(outputPkScript):
254-
weightEstimate.addP2WKHOutput()
255-
256-
case txscript.IsPayToPubKeyHash(outputPkScript):
257-
weightEstimate.estimator.AddP2PKHOutput()
258-
259-
case txscript.IsPayToScriptHash(outputPkScript):
260-
weightEstimate.estimator.AddP2SHOutput()
261-
262-
default:
263-
// Unknown script type.
264-
return nil, nil, errors.New("unknown script type")
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 {
247+
switch {
248+
case txscript.IsPayToTaproot(outputPkScript):
249+
weightEstimate.addP2TROutput()
250+
251+
case txscript.IsPayToWitnessScriptHash(outputPkScript):
252+
weightEstimate.addP2WSHOutput()
253+
254+
case txscript.IsPayToWitnessPubKeyHash(outputPkScript):
255+
weightEstimate.addP2WKHOutput()
256+
257+
case txscript.IsPayToPubKeyHash(outputPkScript):
258+
weightEstimate.estimator.AddP2PKHOutput()
259+
260+
case txscript.IsPayToScriptHash(outputPkScript):
261+
weightEstimate.estimator.AddP2SHOutput()
262+
263+
default:
264+
// Unknown script type.
265+
return nil, nil, errors.New("unknown script type")
266+
}
265267
}
266268

267269
// For each output, use its witness type to determine the estimate

sweep/txgenerator_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestWeightEstimate(t *testing.T) {
5151
}
5252

5353
_, estimator, err := getWeightEstimate(
54-
inputs, nil, 0, 0, changePkScript,
54+
inputs, nil, 0, 0, [][]byte{changePkScript},
5555
)
5656
require.NoError(t, err)
5757

@@ -153,7 +153,7 @@ func testUnknownScriptInner(t *testing.T, pkscript []byte, expectFail bool) {
153153
))
154154
}
155155

156-
_, _, err := getWeightEstimate(inputs, nil, 0, 0, pkscript)
156+
_, _, err := getWeightEstimate(inputs, nil, 0, 0, [][]byte{pkscript})
157157
if expectFail {
158158
require.Error(t, err)
159159
} else {

0 commit comments

Comments
 (0)