Skip to content

Commit 0cd4b04

Browse files
Roasbeefguggero
authored andcommitted
sweep: add new AuxSweeper interface
In this commit, we add a new AuxSweeper interface. This'll take a set of inputs, and a change addr for the sweep transaction, then optionally return a new sweep output to be added to the sweep transaction. We also add a new NotifyBroadcast method. This'll be used to notify that we're _about_ to broadcast a sweeping transaction. The set of inputs is passed in, which allows the caller to prepare for the ultimate broadcast of the sweeping transaction. We also add ExtraTxOut to BumpRequest pass fees to NotifyBroadcast. This allows the callee to know the total fee of the sweeping transaction.
1 parent 1574a4b commit 0cd4b04

File tree

7 files changed

+138
-56
lines changed

7 files changed

+138
-56
lines changed

lnwallet/interface.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ type MessageSigner interface {
601601
type AddrWithKey struct {
602602
lnwire.DeliveryAddress
603603

604-
InternalKey fn.Option[btcec.PublicKey]
604+
InternalKey fn.Option[keychain.KeyDescriptor]
605605

606606
// TODO(roasbeef): consolidate w/ instance in chan closer
607607
}

server.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4900,12 +4900,7 @@ func newSweepPkScriptGen(
49004900

49014901
return fn.Ok(lnwallet.AddrWithKey{
49024902
DeliveryAddress: addr,
4903-
InternalKey: fn.MapOption(func(
4904-
desc keychain.KeyDescriptor) btcec.PublicKey {
4905-
4906-
return *desc.PubKey
4907-
},
4908-
)(internalKeyDesc),
4903+
InternalKey: internalKeyDesc,
49094904
})
49104905
}
49114906
}

sweep/fee_bumper.go

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,18 @@ type BumpRequest struct {
110110
DeadlineHeight int32
111111

112112
// DeliveryAddress is the script to send the change output to.
113-
DeliveryAddress []byte
113+
DeliveryAddress lnwallet.AddrWithKey
114114

115115
// MaxFeeRate is the maximum fee rate that can be used for fee bumping.
116116
MaxFeeRate chainfee.SatPerKWeight
117117

118118
// StartingFeeRate is an optional parameter that can be used to specify
119119
// the initial fee rate to use for the fee function.
120120
StartingFeeRate fn.Option[chainfee.SatPerKWeight]
121+
122+
// ExtraTxOut tracks if this bump request has an optional set of extra
123+
// outputs to add to the transaction.
124+
ExtraTxOut fn.Option[SweepOutput]
121125
}
122126

123127
// MaxFeeRateAllowed returns the maximum fee rate allowed for the given
@@ -127,7 +131,9 @@ type BumpRequest struct {
127131
func (r *BumpRequest) MaxFeeRateAllowed() (chainfee.SatPerKWeight, error) {
128132
// Get the size of the sweep tx, which will be used to calculate the
129133
// budget fee rate.
130-
size, err := calcSweepTxWeight(r.Inputs, r.DeliveryAddress)
134+
size, err := calcSweepTxWeight(
135+
r.Inputs, r.DeliveryAddress.DeliveryAddress,
136+
)
131137
if err != nil {
132138
return 0, err
133139
}
@@ -248,6 +254,10 @@ type TxPublisherConfig struct {
248254

249255
// Notifier is used to monitor the confirmation status of the tx.
250256
Notifier chainntnfs.ChainNotifier
257+
258+
// AuxSweeper is an optional interface that can be used to shape the
259+
// way the final sweep transaction is generated.
260+
AuxSweeper fn.Option[AuxSweeper]
251261
}
252262

253263
// TxPublisher is an implementation of the Bumper interface. It utilizes the
@@ -401,16 +411,18 @@ func (t *TxPublisher) createRBFCompliantTx(req *BumpRequest,
401411
for {
402412
// Create a new tx with the given fee rate and check its
403413
// mempool acceptance.
404-
tx, fee, err := t.createAndCheckTx(req, f)
414+
sweepCtx, err := t.createAndCheckTx(req, f)
405415

406416
switch {
407417
case err == nil:
408418
// The tx is valid, return the request ID.
409-
requestID := t.storeRecord(tx, req, f, fee)
419+
requestID := t.storeRecord(
420+
sweepCtx.tx, req, f, sweepCtx.fee,
421+
)
410422

411423
log.Infof("Created tx %v for %v inputs: feerate=%v, "+
412-
"fee=%v, inputs=%v", tx.TxHash(),
413-
len(req.Inputs), f.FeeRate(), fee,
424+
"fee=%v, inputs=%v", sweepCtx.tx.TxHash(),
425+
len(req.Inputs), f.FeeRate(), sweepCtx.fee,
414426
inputTypeSummary(req.Inputs))
415427

416428
return requestID, nil
@@ -421,8 +433,8 @@ func (t *TxPublisher) createRBFCompliantTx(req *BumpRequest,
421433
// We should at least start with a feerate above the
422434
// mempool min feerate, so if we get this error, it
423435
// means something is wrong earlier in the pipeline.
424-
log.Errorf("Current fee=%v, feerate=%v, %v", fee,
425-
f.FeeRate(), err)
436+
log.Errorf("Current fee=%v, feerate=%v, %v",
437+
sweepCtx.fee, f.FeeRate(), err)
426438

427439
fallthrough
428440

@@ -434,8 +446,8 @@ func (t *TxPublisher) createRBFCompliantTx(req *BumpRequest,
434446
// increased or maxed out.
435447
for !increased {
436448
log.Debugf("Increasing fee for next round, "+
437-
"current fee=%v, feerate=%v", fee,
438-
f.FeeRate())
449+
"current fee=%v, feerate=%v",
450+
sweepCtx.fee, f.FeeRate())
439451

440452
// If the fee function tells us that we have
441453
// used up the budget, we will return an error
@@ -484,49 +496,53 @@ func (t *TxPublisher) storeRecord(tx *wire.MsgTx, req *BumpRequest,
484496
// script, and the fee rate. In addition, it validates the tx's mempool
485497
// acceptance before returning a tx that can be published directly, along with
486498
// its fee.
487-
func (t *TxPublisher) createAndCheckTx(req *BumpRequest, f FeeFunction) (
488-
*wire.MsgTx, btcutil.Amount, error) {
499+
func (t *TxPublisher) createAndCheckTx(req *BumpRequest,
500+
f FeeFunction) (*sweepTxCtx, error) {
489501

490502
// Create the sweep tx with max fee rate of 0 as the fee function
491503
// guarantees the fee rate used here won't exceed the max fee rate.
492-
tx, fee, err := t.createSweepTx(
504+
sweepCtx, err := t.createSweepTx(
493505
req.Inputs, req.DeliveryAddress, f.FeeRate(),
494506
)
495507
if err != nil {
496-
return nil, fee, fmt.Errorf("create sweep tx: %w", err)
508+
return sweepCtx, fmt.Errorf("create sweep tx: %w", err)
497509
}
498510

499511
// Sanity check the budget still covers the fee.
500-
if fee > req.Budget {
501-
return nil, fee, fmt.Errorf("%w: budget=%v, fee=%v",
502-
ErrNotEnoughBudget, req.Budget, fee)
512+
if sweepCtx.fee > req.Budget {
513+
return sweepCtx, fmt.Errorf("%w: budget=%v, fee=%v",
514+
ErrNotEnoughBudget, req.Budget, sweepCtx.fee)
503515
}
504516

517+
// If we had an extra txOut, then we'll update the result to include
518+
// it.
519+
req.ExtraTxOut = sweepCtx.extraTxOut
520+
505521
// Validate the tx's mempool acceptance.
506-
err = t.cfg.Wallet.CheckMempoolAcceptance(tx)
522+
err = t.cfg.Wallet.CheckMempoolAcceptance(sweepCtx.tx)
507523

508524
// Exit early if the tx is valid.
509525
if err == nil {
510-
return tx, fee, nil
526+
return sweepCtx, nil
511527
}
512528

513529
// Print an error log if the chain backend doesn't support the mempool
514530
// acceptance test RPC.
515531
if errors.Is(err, rpcclient.ErrBackendVersion) {
516532
log.Errorf("TestMempoolAccept not supported by backend, " +
517533
"consider upgrading it to a newer version")
518-
return tx, fee, nil
534+
return sweepCtx, nil
519535
}
520536

521537
// We are running on a backend that doesn't implement the RPC
522538
// testmempoolaccept, eg, neutrino, so we'll skip the check.
523539
if errors.Is(err, chain.ErrUnimplemented) {
524540
log.Debug("Skipped testmempoolaccept due to not implemented")
525-
return tx, fee, nil
541+
return sweepCtx, nil
526542
}
527543

528-
return nil, fee, fmt.Errorf("tx=%v failed mempool check: %w",
529-
tx.TxHash(), err)
544+
return sweepCtx, fmt.Errorf("tx=%v failed mempool check: %w",
545+
sweepCtx.tx.TxHash(), err)
530546
}
531547

532548
// broadcast takes a monitored tx and publishes it to the network. Prior to the
@@ -547,14 +563,23 @@ func (t *TxPublisher) broadcast(requestID uint64) (*BumpResult, error) {
547563
log.Debugf("Publishing sweep tx %v, num_inputs=%v, height=%v",
548564
txid, len(tx.TxIn), t.currentHeight.Load())
549565

566+
// Before we go to broadcast, we'll notify the aux sweeper, if it's
567+
// present of this new broadcast attempt.
568+
err := fn.MapOptionZ(t.cfg.AuxSweeper, func(aux AuxSweeper) error {
569+
return aux.NotifyBroadcast(record.req, tx, record.fee)
570+
})
571+
if err != nil {
572+
return nil, fmt.Errorf("unable to notify aux sweeper: %w", err)
573+
}
574+
550575
// Set the event, and change it to TxFailed if the wallet fails to
551576
// publish it.
552577
event := TxPublished
553578

554579
// Publish the sweeping tx with customized label. If the publish fails,
555580
// this error will be saved in the `BumpResult` and it will be removed
556581
// from being monitored.
557-
err := t.cfg.Wallet.PublishTransaction(
582+
err = t.cfg.Wallet.PublishTransaction(
558583
tx, labels.MakeLabel(labels.LabelTypeSweepTransaction, nil),
559584
)
560585
if err != nil {
@@ -933,7 +958,7 @@ func (t *TxPublisher) createAndPublishTx(requestID uint64,
933958
// NOTE: The fee function is expected to have increased its returned
934959
// fee rate after calling the SkipFeeBump method. So we can use it
935960
// directly here.
936-
tx, fee, err := t.createAndCheckTx(r.req, r.feeFunction)
961+
sweepCtx, err := t.createAndCheckTx(r.req, r.feeFunction)
937962

938963
// If the error is fee related, we will return no error and let the fee
939964
// bumper retry it at next block.
@@ -980,17 +1005,17 @@ func (t *TxPublisher) createAndPublishTx(requestID uint64,
9801005
// The tx has been created without any errors, we now register a new
9811006
// record by overwriting the same requestID.
9821007
t.records.Store(requestID, &monitorRecord{
983-
tx: tx,
1008+
tx: sweepCtx.tx,
9841009
req: r.req,
9851010
feeFunction: r.feeFunction,
986-
fee: fee,
1011+
fee: sweepCtx.fee,
9871012
})
9881013

9891014
// Attempt to broadcast this new tx.
9901015
result, err := t.broadcast(requestID)
9911016
if err != nil {
9921017
log.Infof("Failed to broadcast replacement tx %v: %v",
993-
tx.TxHash(), err)
1018+
sweepCtx.tx.TxHash(), err)
9941019

9951020
return fn.None[BumpResult]()
9961021
}
@@ -1016,7 +1041,8 @@ func (t *TxPublisher) createAndPublishTx(requestID uint64,
10161041
return fn.Some(*result)
10171042
}
10181043

1019-
log.Infof("Replaced tx=%v with new tx=%v", oldTx.TxHash(), tx.TxHash())
1044+
log.Infof("Replaced tx=%v with new tx=%v", oldTx.TxHash(),
1045+
sweepCtx.tx.TxHash())
10201046

10211047
// Otherwise, it's a successful RBF, set the event and return.
10221048
result.Event = TxReplaced
@@ -1129,17 +1155,28 @@ func calcCurrentConfTarget(currentHeight, deadline int32) uint32 {
11291155
return confTarget
11301156
}
11311157

1158+
// sweepTxCtx houses a sweep transaction with additional context.
1159+
type sweepTxCtx struct {
1160+
tx *wire.MsgTx
1161+
1162+
fee btcutil.Amount
1163+
1164+
extraTxOut fn.Option[SweepOutput]
1165+
}
1166+
11321167
// createSweepTx creates a sweeping tx based on the given inputs, change
11331168
// address and fee rate.
1134-
func (t *TxPublisher) createSweepTx(inputs []input.Input, changePkScript []byte,
1135-
feeRate chainfee.SatPerKWeight) (*wire.MsgTx, btcutil.Amount, error) {
1169+
func (t *TxPublisher) createSweepTx(inputs []input.Input,
1170+
changePkScript lnwallet.AddrWithKey,
1171+
feeRate chainfee.SatPerKWeight) (*sweepTxCtx, error) {
11361172

11371173
// Validate and calculate the fee and change amount.
11381174
txFee, changeAmtOpt, locktimeOpt, err := prepareSweepTx(
1139-
inputs, changePkScript, feeRate, t.currentHeight.Load(),
1175+
inputs, changePkScript.DeliveryAddress, feeRate,
1176+
t.currentHeight.Load(),
11401177
)
11411178
if err != nil {
1142-
return nil, 0, err
1179+
return nil, err
11431180
}
11441181

11451182
var (
@@ -1185,7 +1222,7 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input, changePkScript []byte,
11851222
// If there's a change amount, add it to the transaction.
11861223
changeAmtOpt.WhenSome(func(changeAmt btcutil.Amount) {
11871224
sweepTx.AddTxOut(&wire.TxOut{
1188-
PkScript: changePkScript,
1225+
PkScript: changePkScript.DeliveryAddress,
11891226
Value: int64(changeAmt),
11901227
})
11911228
})
@@ -1196,7 +1233,7 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input, changePkScript []byte,
11961233

11971234
prevInputFetcher, err := input.MultiPrevOutFetcher(inputs)
11981235
if err != nil {
1199-
return nil, 0, fmt.Errorf("error creating prev input fetcher "+
1236+
return nil, fmt.Errorf("error creating prev input fetcher "+
12001237
"for hash cache: %v", err)
12011238
}
12021239
hashCache := txscript.NewTxSigHashes(sweepTx, prevInputFetcher)
@@ -1224,14 +1261,17 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input, changePkScript []byte,
12241261

12251262
for idx, inp := range idxs {
12261263
if err := addInputScript(idx, inp); err != nil {
1227-
return nil, 0, err
1264+
return nil, err
12281265
}
12291266
}
12301267

12311268
log.Debugf("Created sweep tx %v for inputs:\n%v", sweepTx.TxHash(),
12321269
inputTypeSummary(inputs))
12331270

1234-
return sweepTx, txFee, nil
1271+
return &sweepTxCtx{
1272+
tx: sweepTx,
1273+
fee: txFee,
1274+
}, nil
12351275
}
12361276

12371277
// prepareSweepTx returns the tx fee, an optional change amount and an optional
@@ -1323,7 +1363,8 @@ func prepareSweepTx(inputs []input.Input, changePkScript []byte,
13231363
changeFloor := lnwallet.DustLimitForSize(len(changePkScript))
13241364

13251365
// If the change amount is dust, we'll move it into the fees.
1326-
if changeAmt < changeFloor {
1366+
switch {
1367+
case changeAmt < changeFloor:
13271368
log.Infof("Change amt %v below dustlimit %v, not adding "+
13281369
"change output", changeAmt, changeFloor)
13291370

@@ -1340,6 +1381,10 @@ func prepareSweepTx(inputs []input.Input, changePkScript []byte,
13401381

13411382
// Set the change amount to none.
13421383
changeAmtOpt = fn.None[btcutil.Amount]()
1384+
1385+
// Otherwise, we'll actually recognize it as a change output.
1386+
default:
1387+
// TODO(roasbeef): Implement (later commit in this PR).
13431388
}
13441389

13451390
// Optionally set the locktime.

sweep/fee_bumper_test.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ import (
2121

2222
var (
2323
// Create a taproot change script.
24-
changePkScript = []byte{
25-
0x51, 0x20,
26-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
27-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
28-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
29-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
24+
changePkScript = lnwallet.AddrWithKey{
25+
DeliveryAddress: []byte{
26+
0x51, 0x20,
27+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
28+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
29+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
30+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
31+
},
3032
}
3133

3234
testInputCount atomic.Uint64
@@ -117,7 +119,9 @@ func TestCalcSweepTxWeight(t *testing.T) {
117119
require.Zero(t, weight)
118120

119121
// Use a correct change script to test the success case.
120-
weight, err = calcSweepTxWeight([]input.Input{&inp}, changePkScript)
122+
weight, err = calcSweepTxWeight(
123+
[]input.Input{&inp}, changePkScript.DeliveryAddress,
124+
)
121125
require.NoError(t, err)
122126

123127
// BaseTxSize 8 bytes
@@ -137,7 +141,9 @@ func TestBumpRequestMaxFeeRateAllowed(t *testing.T) {
137141
inp := createTestInput(100, input.WitnessKeyHash)
138142

139143
// The weight is 487.
140-
weight, err := calcSweepTxWeight([]input.Input{&inp}, changePkScript)
144+
weight, err := calcSweepTxWeight(
145+
[]input.Input{&inp}, changePkScript.DeliveryAddress,
146+
)
141147
require.NoError(t, err)
142148

143149
// Define a test budget and calculates its fee rate.
@@ -154,7 +160,9 @@ func TestBumpRequestMaxFeeRateAllowed(t *testing.T) {
154160
// Use a wrong change script to test the error case.
155161
name: "error calc weight",
156162
req: &BumpRequest{
157-
DeliveryAddress: []byte{1},
163+
DeliveryAddress: lnwallet.AddrWithKey{
164+
DeliveryAddress: []byte{1},
165+
},
158166
},
159167
expectedMaxFeeRate: 0,
160168
expectedErr: true,
@@ -451,7 +459,7 @@ func TestCreateAndCheckTx(t *testing.T) {
451459

452460
t.Run(tc.name, func(t *testing.T) {
453461
// Call the method under test.
454-
_, _, err := tp.createAndCheckTx(tc.req, m.feeFunc)
462+
_, err := tp.createAndCheckTx(tc.req, m.feeFunc)
455463

456464
// Check the result is as expected.
457465
require.ErrorIs(t, err, tc.expectedErr)

0 commit comments

Comments
 (0)