Merge pull request #3809 from joostjager/anchor-sweeper

sweep: allow force sweeping of negatively yielding inputs
This commit is contained in:
Joost Jager 2020-01-23 11:46:46 +01:00 committed by GitHub
commit ae9c6faebf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 306 additions and 146 deletions

@ -4,7 +4,6 @@ package main
import ( import (
"context" "context"
"fmt"
"sort" "sort"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
@ -113,7 +112,13 @@ var bumpFeeCommand = cli.Command{
Note that this command currently doesn't perform any validation checks Note that this command currently doesn't perform any validation checks
on the fee preference being provided. For now, the responsibility of on the fee preference being provided. For now, the responsibility of
ensuring that the new fee preference is sufficient is delegated to the ensuring that the new fee preference is sufficient is delegated to the
user.`, user.
The force flag enables sweeping of inputs that are negatively yielding.
Normally it does not make sense to lose money on sweeping, unless a
parent transaction needs to get confirmed and there is only a small
output available to attach the child transaction to.
`,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.Uint64Flag{ cli.Uint64Flag{
Name: "conf_target", Name: "conf_target",
@ -125,6 +130,10 @@ var bumpFeeCommand = cli.Command{
Usage: "a manual fee expressed in sat/byte that " + Usage: "a manual fee expressed in sat/byte that " +
"should be used when sweeping the output", "should be used when sweeping the output",
}, },
cli.BoolFlag{
Name: "force",
Usage: "sweep even if the yield is negative",
},
}, },
Action: actionDecorator(bumpFee), Action: actionDecorator(bumpFee),
} }
@ -132,7 +141,7 @@ var bumpFeeCommand = cli.Command{
func bumpFee(ctx *cli.Context) error { func bumpFee(ctx *cli.Context) error {
// Display the command's help message if we do not have the expected // Display the command's help message if we do not have the expected
// number of arguments/flags. // number of arguments/flags.
if ctx.NArg() != 1 || ctx.NumFlags() != 1 { if ctx.NArg() != 1 {
return cli.ShowCommandHelp(ctx, "bumpfee") return cli.ShowCommandHelp(ctx, "bumpfee")
} }
@ -142,24 +151,14 @@ func bumpFee(ctx *cli.Context) error {
return err return err
} }
var confTarget, satPerByte uint32
switch {
case ctx.IsSet("conf_target") && ctx.IsSet("sat_per_byte"):
return fmt.Errorf("either conf_target or sat_per_byte should " +
"be set, but not both")
case ctx.IsSet("conf_target"):
confTarget = uint32(ctx.Uint64("conf_target"))
case ctx.IsSet("sat_per_byte"):
satPerByte = uint32(ctx.Uint64("sat_per_byte"))
}
client, cleanUp := getWalletClient(ctx) client, cleanUp := getWalletClient(ctx)
defer cleanUp() defer cleanUp()
resp, err := client.BumpFee(context.Background(), &walletrpc.BumpFeeRequest{ resp, err := client.BumpFee(context.Background(), &walletrpc.BumpFeeRequest{
Outpoint: protoOutPoint, Outpoint: protoOutPoint,
TargetConf: confTarget, TargetConf: uint32(ctx.Uint64("conf_target")),
SatPerByte: satPerByte, SatPerByte: uint32(ctx.Uint64("sat_per_byte")),
Force: ctx.Bool("force"),
}) })
if err != nil { if err != nil {
return err return err

@ -11,6 +11,9 @@ type PendingSweep struct {
SatPerByte uint32 `json:"sat_per_byte"` SatPerByte uint32 `json:"sat_per_byte"`
BroadcastAttempts uint32 `json:"broadcast_attempts"` BroadcastAttempts uint32 `json:"broadcast_attempts"`
NextBroadcastHeight uint32 `json:"next_broadcast_height"` NextBroadcastHeight uint32 `json:"next_broadcast_height"`
RequestedSatPerByte uint32 `json:"requested_sat_per_byte"`
RequestedConfTarget uint32 `json:"requested_conf_target"`
Force bool `json:"force"`
} }
// NewPendingSweepFromProto converts the walletrpc.PendingSweep proto type into // NewPendingSweepFromProto converts the walletrpc.PendingSweep proto type into
@ -23,5 +26,8 @@ func NewPendingSweepFromProto(pendingSweep *walletrpc.PendingSweep) *PendingSwee
SatPerByte: pendingSweep.SatPerByte, SatPerByte: pendingSweep.SatPerByte,
BroadcastAttempts: pendingSweep.BroadcastAttempts, BroadcastAttempts: pendingSweep.BroadcastAttempts,
NextBroadcastHeight: pendingSweep.NextBroadcastHeight, NextBroadcastHeight: pendingSweep.NextBroadcastHeight,
RequestedSatPerByte: pendingSweep.RequestedSatPerByte,
RequestedConfTarget: pendingSweep.RequestedConfTarget,
Force: pendingSweep.Force,
} }
} }

@ -532,6 +532,14 @@ type PendingSweep struct {
//The next height of the chain at which we'll attempt to broadcast the //The next height of the chain at which we'll attempt to broadcast the
//sweep transaction of the output. //sweep transaction of the output.
NextBroadcastHeight uint32 `protobuf:"varint,6,opt,name=next_broadcast_height,proto3" json:"next_broadcast_height,omitempty"` NextBroadcastHeight uint32 `protobuf:"varint,6,opt,name=next_broadcast_height,proto3" json:"next_broadcast_height,omitempty"`
// The requested confirmation target for this output.
RequestedConfTarget uint32 `protobuf:"varint,8,opt,name=requested_conf_target,proto3" json:"requested_conf_target,omitempty"`
// The requested fee rate, expressed in sat/byte, for this output.
RequestedSatPerByte uint32 `protobuf:"varint,9,opt,name=requested_sat_per_byte,proto3" json:"requested_sat_per_byte,omitempty"`
//*
//Whether this input must be force-swept. This means that it is swept even
//if it has a negative yield.
Force bool `protobuf:"varint,7,opt,name=force,proto3" json:"force,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_sizecache int32 `json:"-"`
@ -604,6 +612,27 @@ func (m *PendingSweep) GetNextBroadcastHeight() uint32 {
return 0 return 0
} }
func (m *PendingSweep) GetRequestedConfTarget() uint32 {
if m != nil {
return m.RequestedConfTarget
}
return 0
}
func (m *PendingSweep) GetRequestedSatPerByte() uint32 {
if m != nil {
return m.RequestedSatPerByte
}
return 0
}
func (m *PendingSweep) GetForce() bool {
if m != nil {
return m.Force
}
return false
}
type PendingSweepsRequest struct { type PendingSweepsRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
@ -685,6 +714,10 @@ type BumpFeeRequest struct {
//The fee rate, expressed in sat/byte, that should be used to spend the input //The fee rate, expressed in sat/byte, that should be used to spend the input
//with. //with.
SatPerByte uint32 `protobuf:"varint,3,opt,name=sat_per_byte,proto3" json:"sat_per_byte,omitempty"` SatPerByte uint32 `protobuf:"varint,3,opt,name=sat_per_byte,proto3" json:"sat_per_byte,omitempty"`
//*
//Whether this input must be force-swept. This means that it is swept even
//if it has a negative yield.
Force bool `protobuf:"varint,4,opt,name=force,proto3" json:"force,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_sizecache int32 `json:"-"`
@ -736,6 +769,13 @@ func (m *BumpFeeRequest) GetSatPerByte() uint32 {
return 0 return 0
} }
func (m *BumpFeeRequest) GetForce() bool {
if m != nil {
return m.Force
}
return false
}
type BumpFeeResponse struct { type BumpFeeResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
@ -788,68 +828,72 @@ func init() {
func init() { proto.RegisterFile("walletrpc/walletkit.proto", fileDescriptor_6cc6942ac78249e5) } func init() { proto.RegisterFile("walletrpc/walletkit.proto", fileDescriptor_6cc6942ac78249e5) }
var fileDescriptor_6cc6942ac78249e5 = []byte{ var fileDescriptor_6cc6942ac78249e5 = []byte{
// 976 bytes of a gzipped FileDescriptorProto // 1026 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xed, 0x6e, 0xe2, 0x46, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0x6d, 0x6f, 0xe2, 0xc6,
0x14, 0x2d, 0x21, 0x61, 0xc3, 0x05, 0x12, 0x67, 0x08, 0x89, 0x97, 0xcd, 0x6e, 0xa8, 0xfb, 0x21, 0x13, 0xff, 0x13, 0x12, 0x02, 0x03, 0x24, 0xce, 0xe6, 0xc9, 0xc7, 0xe5, 0x2e, 0xfc, 0xdd, 0x07,
0xd4, 0x56, 0xa0, 0x66, 0xdb, 0xaa, 0x6a, 0x7f, 0xb4, 0x59, 0x70, 0x44, 0xc4, 0x87, 0xa9, 0xed, 0xa1, 0xb6, 0x02, 0x35, 0xd7, 0x9e, 0xaa, 0xf6, 0x45, 0x9b, 0x23, 0x8e, 0x12, 0x41, 0x30, 0xb5,
0x6c, 0xba, 0x55, 0xa5, 0x91, 0x81, 0x59, 0xb0, 0x00, 0xdb, 0x3b, 0x1e, 0x0a, 0xfc, 0x6d, 0x9f, 0x9d, 0x4b, 0xaf, 0xaa, 0xb4, 0x72, 0x60, 0x03, 0x16, 0x60, 0xfb, 0xd6, 0x4b, 0x81, 0xb7, 0xfd,
0xa4, 0xaf, 0xd1, 0xa7, 0xab, 0x3c, 0xb6, 0xc9, 0x18, 0x92, 0x4a, 0xfd, 0x15, 0xe7, 0x9c, 0x73, 0x16, 0xfd, 0x00, 0xfd, 0x12, 0xfd, 0x74, 0x95, 0xd7, 0x0f, 0x59, 0xf3, 0x50, 0xa9, 0xaf, 0x82,
0xcf, 0xdc, 0xb9, 0x33, 0x73, 0x04, 0x3c, 0x5f, 0x5a, 0xb3, 0x19, 0x61, 0xd4, 0x1b, 0xd6, 0xc3, 0x7f, 0xbf, 0xdf, 0xcc, 0xce, 0xce, 0xcc, 0xce, 0x04, 0x5e, 0xcc, 0xac, 0xf1, 0x98, 0x30, 0xea,
0xaf, 0xa9, 0xcd, 0x6a, 0x1e, 0x75, 0x99, 0x8b, 0xb2, 0x1b, 0xaa, 0x9c, 0xa5, 0xde, 0x30, 0x44, 0xf5, 0x1a, 0xe1, 0xaf, 0x91, 0xcd, 0xea, 0x1e, 0x75, 0x99, 0x8b, 0x0a, 0x09, 0x55, 0x29, 0x50,
0xcb, 0xa7, 0xbe, 0x3d, 0x76, 0x02, 0x79, 0xf0, 0x97, 0xd0, 0x10, 0x55, 0x7e, 0x81, 0x4c, 0x9b, 0xaf, 0x17, 0xa2, 0x95, 0x23, 0xdf, 0x1e, 0x38, 0x81, 0x3c, 0xf8, 0x4b, 0x68, 0x88, 0x2a, 0x3f,
0xac, 0x75, 0xf2, 0x01, 0x55, 0x41, 0x9a, 0x92, 0x35, 0x7e, 0x6f, 0x3b, 0x63, 0x42, 0xb1, 0x47, 0x43, 0xae, 0x45, 0x16, 0x3a, 0xf9, 0x88, 0x6a, 0x20, 0x8d, 0xc8, 0x02, 0x3f, 0xd9, 0xce, 0x80,
0x6d, 0x87, 0xc9, 0xa9, 0x4a, 0xaa, 0x7a, 0xa0, 0x1f, 0x4d, 0xc9, 0xfa, 0x86, 0xc3, 0xfd, 0x00, 0x50, 0xec, 0x51, 0xdb, 0x61, 0x72, 0xa6, 0x9a, 0xa9, 0xed, 0xe8, 0x7b, 0x23, 0xb2, 0xb8, 0xe6,
0x45, 0x2f, 0x01, 0xb8, 0xd2, 0x9a, 0xdb, 0xb3, 0xb5, 0xbc, 0xc7, 0x35, 0xd9, 0x40, 0xc3, 0x01, 0x70, 0x37, 0x40, 0xd1, 0x2b, 0x00, 0xae, 0xb4, 0x26, 0xf6, 0x78, 0x21, 0x6f, 0x71, 0x4d, 0x21,
0xa5, 0x00, 0xb9, 0xeb, 0xd1, 0x88, 0xea, 0xe4, 0xc3, 0x82, 0xf8, 0x4c, 0x51, 0x20, 0x1f, 0xfe, 0xd0, 0x70, 0x40, 0x29, 0x43, 0xf1, 0xb2, 0xdf, 0xa7, 0x3a, 0xf9, 0x38, 0x25, 0x3e, 0x53, 0x14,
0xeb, 0x7b, 0xae, 0xe3, 0x13, 0x84, 0x60, 0xdf, 0x1a, 0x8d, 0x28, 0xf7, 0xce, 0xea, 0xfc, 0x5b, 0x28, 0x85, 0x9f, 0xbe, 0xe7, 0x3a, 0x3e, 0x41, 0x08, 0xb6, 0xad, 0x7e, 0x9f, 0x72, 0xdf, 0x05,
0xf9, 0x14, 0x72, 0x26, 0xb5, 0x1c, 0xdf, 0x1a, 0x32, 0xdb, 0x75, 0x50, 0x09, 0x32, 0x6c, 0x85, 0x9d, 0xff, 0x56, 0x3e, 0x85, 0xa2, 0x49, 0x2d, 0xc7, 0xb7, 0x7a, 0xcc, 0x76, 0x1d, 0x74, 0x0c,
0x27, 0x64, 0xc5, 0x45, 0x79, 0xfd, 0x80, 0xad, 0x5a, 0x64, 0xa5, 0x7c, 0x07, 0xc7, 0xfd, 0xc5, 0x39, 0x36, 0xc7, 0x43, 0x32, 0xe7, 0xa2, 0x92, 0xbe, 0xc3, 0xe6, 0x37, 0x64, 0xae, 0xbc, 0x85,
0x60, 0x66, 0xfb, 0x93, 0x8d, 0xd9, 0x27, 0x50, 0xf0, 0x42, 0x08, 0x13, 0x4a, 0xdd, 0xd8, 0x35, 0xfd, 0xee, 0xf4, 0x71, 0x6c, 0xfb, 0xc3, 0xc4, 0xd9, 0x27, 0x50, 0xf6, 0x42, 0x08, 0x13, 0x4a,
0x1f, 0x81, 0x6a, 0x80, 0x29, 0xbf, 0x03, 0x32, 0x88, 0x33, 0xd2, 0x16, 0xcc, 0x5b, 0x30, 0x3f, 0xdd, 0xd8, 0x6b, 0x29, 0x02, 0xd5, 0x00, 0x53, 0x7e, 0x03, 0x64, 0x10, 0xa7, 0xaf, 0x4d, 0x99,
0xea, 0x0b, 0x5d, 0x00, 0xf8, 0x16, 0xc3, 0x1e, 0xa1, 0x78, 0xba, 0xe4, 0x75, 0x69, 0xfd, 0xd0, 0x37, 0x65, 0x7e, 0x14, 0x17, 0x3a, 0x03, 0xf0, 0x2d, 0x86, 0x3d, 0x42, 0xf1, 0x68, 0xc6, 0xed,
0xb7, 0x58, 0x9f, 0xd0, 0xf6, 0x12, 0x55, 0xe1, 0x99, 0x1b, 0xea, 0xe5, 0xbd, 0x4a, 0xba, 0x9a, 0xb2, 0x7a, 0xde, 0xb7, 0x58, 0x97, 0xd0, 0xd6, 0x0c, 0xd5, 0x60, 0xd7, 0x0d, 0xf5, 0xf2, 0x56,
0xbb, 0x3a, 0xaa, 0x45, 0xf3, 0xab, 0x99, 0x2b, 0x6d, 0xc1, 0xf4, 0x98, 0x56, 0xbe, 0x82, 0x62, 0x35, 0x5b, 0x2b, 0x5e, 0xec, 0xd5, 0xa3, 0xfc, 0xd5, 0xcd, 0xb9, 0x36, 0x65, 0x7a, 0x4c, 0x2b,
0xc2, 0x3d, 0xea, 0xac, 0x04, 0x19, 0x6a, 0x2d, 0x31, 0xdb, 0xec, 0x81, 0x5a, 0x4b, 0x73, 0xa5, 0x5f, 0xc1, 0x61, 0xca, 0x7b, 0x14, 0xd9, 0x31, 0xe4, 0xa8, 0x35, 0xc3, 0x2c, 0xb9, 0x03, 0xb5,
0x7c, 0x0b, 0x48, 0xf5, 0x99, 0x3d, 0xb7, 0x18, 0xb9, 0x21, 0x24, 0xee, 0xe5, 0x12, 0x72, 0x43, 0x66, 0xe6, 0x5c, 0xf9, 0x16, 0x90, 0xea, 0x33, 0x7b, 0x62, 0x31, 0x72, 0x4d, 0x48, 0x1c, 0xcb,
0xd7, 0x79, 0x8f, 0x99, 0x45, 0xc7, 0x24, 0x1e, 0x3b, 0x04, 0x90, 0xc9, 0x11, 0xe5, 0x35, 0x14, 0x39, 0x14, 0x7b, 0xae, 0xf3, 0x84, 0x99, 0x45, 0x07, 0x24, 0x4e, 0x3b, 0x04, 0x90, 0xc9, 0x11,
0x13, 0x65, 0xd1, 0x22, 0xff, 0xb9, 0x07, 0xe5, 0xef, 0x3d, 0xc8, 0xf7, 0x89, 0x33, 0xb2, 0x9d, 0xe5, 0x0d, 0x1c, 0xa6, 0xcc, 0xa2, 0x43, 0xfe, 0xf5, 0x0e, 0xca, 0x5f, 0x59, 0x28, 0x75, 0x89,
0xb1, 0xb1, 0x24, 0xc4, 0x43, 0x5f, 0xc2, 0x61, 0xd0, 0xb5, 0x1b, 0x1f, 0x6d, 0xee, 0xea, 0xb8, 0xd3, 0xb7, 0x9d, 0x81, 0x31, 0x23, 0xc4, 0x43, 0x5f, 0x42, 0x3e, 0x88, 0xda, 0x8d, 0x4b, 0x5b,
0x36, 0xe3, 0x7b, 0xd2, 0x16, 0xac, 0x1f, 0xc0, 0xfa, 0x46, 0x80, 0x7e, 0x80, 0xfc, 0xd2, 0x66, 0xbc, 0xd8, 0xaf, 0x8f, 0xf9, 0x9d, 0xb4, 0x29, 0xeb, 0x06, 0xb0, 0x9e, 0x08, 0xd0, 0xf7, 0x50,
0x0e, 0xf1, 0x7d, 0xcc, 0xd6, 0x1e, 0xe1, 0xe7, 0x7c, 0x74, 0x75, 0x56, 0xdb, 0x5c, 0xae, 0xda, 0x9a, 0xd9, 0xcc, 0x21, 0xbe, 0x8f, 0xd9, 0xc2, 0x23, 0xbc, 0xce, 0x7b, 0x17, 0x27, 0xf5, 0xa4,
0x7d, 0x48, 0x9b, 0x6b, 0x8f, 0xe8, 0x09, 0x2d, 0x7a, 0x05, 0x60, 0xcd, 0xdd, 0x85, 0xc3, 0xb0, 0xb9, 0xea, 0x0f, 0x21, 0x6d, 0x2e, 0x3c, 0xa2, 0xa7, 0xb4, 0xe8, 0x35, 0x80, 0x35, 0x71, 0xa7,
0x6f, 0x31, 0x39, 0x5d, 0x49, 0x55, 0x0b, 0xba, 0x80, 0x20, 0x05, 0xf2, 0x71, 0xdf, 0x83, 0x35, 0x0e, 0xc3, 0xbe, 0xc5, 0xe4, 0x6c, 0x35, 0x53, 0x2b, 0xeb, 0x02, 0x82, 0x14, 0x28, 0xc5, 0x71,
0x23, 0xf2, 0x3e, 0x57, 0x24, 0x30, 0x54, 0x03, 0x34, 0xa0, 0xae, 0x35, 0x1a, 0x5a, 0x3e, 0xc3, 0x3f, 0x2e, 0x18, 0x91, 0xb7, 0xb9, 0x22, 0x85, 0xa1, 0x3a, 0xa0, 0x47, 0xea, 0x5a, 0xfd, 0x9e,
0x16, 0x63, 0x64, 0xee, 0x31, 0x5f, 0x3e, 0xe0, 0xca, 0x47, 0x18, 0xf4, 0x0d, 0x94, 0x1c, 0xb2, 0xe5, 0x33, 0x6c, 0x31, 0x46, 0x26, 0x1e, 0xf3, 0xe5, 0x1d, 0xae, 0x5c, 0xc3, 0xa0, 0x6f, 0xe0,
0x62, 0xf8, 0x81, 0x9a, 0x10, 0x7b, 0x3c, 0x61, 0x72, 0x86, 0x97, 0x3c, 0x4e, 0x2a, 0x67, 0x70, 0xd8, 0x21, 0x73, 0x86, 0x9f, 0xa9, 0x21, 0xb1, 0x07, 0x43, 0x26, 0xe7, 0xb8, 0xc9, 0x7a, 0x32,
0x2a, 0x8e, 0x28, 0xbe, 0x1d, 0xca, 0xaf, 0x50, 0xda, 0xc2, 0xa3, 0x91, 0xff, 0x04, 0x47, 0x5e, 0xb0, 0xa2, 0x61, 0x11, 0x48, 0x1f, 0x8b, 0x35, 0xc8, 0x87, 0x56, 0x6b, 0x49, 0xf4, 0x16, 0x4e,
0x48, 0x60, 0x9f, 0x33, 0x72, 0x8a, 0xdf, 0x8f, 0x73, 0x61, 0x30, 0x62, 0xa5, 0xbe, 0x25, 0x57, 0x9e, 0x89, 0xd4, 0x4d, 0x0a, 0xdc, 0x6c, 0x03, 0x8b, 0x8e, 0x60, 0xe7, 0xc9, 0xa5, 0x3d, 0x22,
0xfe, 0x4a, 0xc1, 0xd1, 0x9b, 0xc5, 0xdc, 0x13, 0x8e, 0xff, 0x7f, 0x9d, 0x4b, 0x05, 0x72, 0xe1, 0xef, 0x56, 0x33, 0xb5, 0xbc, 0x1e, 0x7e, 0x28, 0x27, 0x70, 0x24, 0x96, 0x29, 0xee, 0x50, 0xe5,
0x35, 0xc1, 0xc1, 0xfd, 0xe0, 0xc7, 0x52, 0xd0, 0x45, 0x68, 0x67, 0xba, 0xe9, 0xdd, 0xe9, 0x2a, 0x17, 0x38, 0x5e, 0xc2, 0xa3, 0xb2, 0xff, 0x08, 0x7b, 0x5e, 0x48, 0x60, 0x9f, 0x33, 0x72, 0x86,
0x27, 0x70, 0xbc, 0x69, 0x22, 0xdc, 0xd9, 0x17, 0x7f, 0xa6, 0x21, 0x27, 0x1c, 0x29, 0x2a, 0xc2, 0xf7, 0xe8, 0xa9, 0x50, 0x1c, 0xd1, 0x52, 0x5f, 0x92, 0x2b, 0x7f, 0x66, 0x60, 0xef, 0xdd, 0x74,
0xf1, 0x5d, 0xaf, 0xdd, 0xd3, 0xee, 0x7b, 0xf8, 0xfe, 0xd6, 0xec, 0xa9, 0x86, 0x21, 0x7d, 0x84, 0xe2, 0x09, 0x2d, 0xf8, 0x9f, 0x7a, 0xa3, 0x0a, 0xc5, 0x30, 0x13, 0x3c, 0x2b, 0xbc, 0x35, 0xca,
0x64, 0x38, 0x6d, 0x68, 0xdd, 0xee, 0xad, 0xd9, 0x55, 0x7b, 0x26, 0x36, 0x6f, 0xbb, 0x2a, 0xee, 0xba, 0x08, 0xad, 0x54, 0x38, 0xbb, 0xa6, 0xc2, 0x49, 0x36, 0xb6, 0xc5, 0x6c, 0x1c, 0xc0, 0x7e,
0x68, 0x8d, 0xb6, 0x94, 0x42, 0xe7, 0x50, 0x14, 0x98, 0x9e, 0x86, 0x9b, 0x6a, 0xe7, 0xfa, 0x9d, 0x12, 0x5a, 0x78, 0xdf, 0x2f, 0xfe, 0xc8, 0x42, 0x51, 0x68, 0x36, 0x74, 0x08, 0xfb, 0xf7, 0x9d,
0xb4, 0x87, 0x4a, 0x70, 0x22, 0x10, 0xba, 0xfa, 0x56, 0x6b, 0xab, 0x52, 0x3a, 0xd0, 0xb7, 0xcc, 0x56, 0x47, 0x7b, 0xe8, 0xe0, 0x87, 0x5b, 0xb3, 0xa3, 0x1a, 0x86, 0xf4, 0x3f, 0x24, 0xc3, 0x51,
0x4e, 0x03, 0x6b, 0x37, 0x37, 0xaa, 0xae, 0x36, 0x63, 0x62, 0x3f, 0x58, 0x82, 0x13, 0xd7, 0x8d, 0x53, 0xbb, 0xbb, 0xbb, 0x35, 0xef, 0xd4, 0x8e, 0x89, 0xcd, 0xdb, 0x3b, 0x15, 0xb7, 0xb5, 0x66,
0x86, 0xda, 0x37, 0x1f, 0x98, 0x03, 0xf4, 0x19, 0x7c, 0x9c, 0x28, 0x09, 0x96, 0xd7, 0xee, 0x4c, 0x4b, 0xca, 0xa0, 0x53, 0x38, 0x14, 0x98, 0x8e, 0x86, 0xaf, 0xd4, 0xf6, 0xe5, 0x07, 0x69, 0x0b,
0x6c, 0xa8, 0x0d, 0xad, 0xd7, 0xc4, 0x1d, 0xf5, 0xad, 0xda, 0x91, 0x32, 0xe8, 0x73, 0x50, 0x92, 0x1d, 0xc3, 0x81, 0x40, 0xe8, 0xea, 0x7b, 0xad, 0xa5, 0x4a, 0xd9, 0x40, 0x7f, 0x63, 0xb6, 0x9b,
0x06, 0xc6, 0x5d, 0xa3, 0xa1, 0x1a, 0x46, 0x52, 0xf7, 0x0c, 0x5d, 0xc2, 0x8b, 0xad, 0x0e, 0xba, 0x58, 0xbb, 0xbe, 0x56, 0x75, 0xf5, 0x2a, 0x26, 0xb6, 0x83, 0x23, 0x38, 0x71, 0xd9, 0x6c, 0xaa,
0x9a, 0xa9, 0xc6, 0xae, 0xd2, 0x21, 0xaa, 0xc0, 0xc5, 0x76, 0x27, 0x5c, 0x11, 0xf9, 0x49, 0x59, 0x5d, 0xf3, 0x99, 0xd9, 0x41, 0x9f, 0xc1, 0xff, 0x53, 0x26, 0xc1, 0xf1, 0xda, 0xbd, 0x89, 0x0d,
0x74, 0x01, 0x32, 0x57, 0x88, 0xce, 0x71, 0xbf, 0x80, 0x4e, 0x41, 0x8a, 0x26, 0x87, 0xdb, 0xea, 0xb5, 0xa9, 0x75, 0xae, 0x70, 0x5b, 0x7d, 0xaf, 0xb6, 0xa5, 0x1c, 0xfa, 0x1c, 0x94, 0xb4, 0x03,
0x3b, 0xdc, 0xba, 0x36, 0x5a, 0x52, 0x0e, 0xbd, 0x80, 0xf3, 0x9e, 0x6a, 0x04, 0x76, 0x3b, 0x64, 0xe3, 0xbe, 0xd9, 0x54, 0x0d, 0x23, 0xad, 0xdb, 0x45, 0xe7, 0xf0, 0x72, 0x29, 0x82, 0x3b, 0xcd,
0xfe, 0xea, 0x9f, 0x7d, 0xc8, 0xde, 0xf3, 0x8b, 0xd4, 0xb6, 0x83, 0x37, 0x58, 0x68, 0x12, 0x6a, 0x54, 0x63, 0xaf, 0x52, 0x1e, 0x55, 0xe1, 0x6c, 0x39, 0x12, 0xae, 0x88, 0xfc, 0x49, 0x05, 0x74,
0xff, 0x41, 0x7a, 0x64, 0xc5, 0xda, 0x64, 0x8d, 0x4e, 0x84, 0x5b, 0x16, 0xe6, 0x76, 0xf9, 0x6c, 0x06, 0x32, 0x57, 0x88, 0x9e, 0xe3, 0x78, 0x01, 0x1d, 0x81, 0x14, 0x65, 0x0e, 0xb7, 0xd4, 0x0f,
0x13, 0x4c, 0x6d, 0xb2, 0x6e, 0x12, 0x7f, 0x48, 0x6d, 0x8f, 0xb9, 0x14, 0x7d, 0x0f, 0xd9, 0xb0, 0xf8, 0xe6, 0xd2, 0xb8, 0x91, 0x8a, 0xe8, 0x25, 0x9c, 0x76, 0x54, 0x23, 0x70, 0xb7, 0x42, 0x96,
0x36, 0xa8, 0x2b, 0x8a, 0xa2, 0x8e, 0x3b, 0xb4, 0x98, 0x4b, 0x9f, 0xac, 0xfc, 0x11, 0x0e, 0x83, 0x2e, 0xfe, 0xde, 0x86, 0xc2, 0x03, 0x6f, 0xaf, 0x96, 0x1d, 0x4c, 0x87, 0xf2, 0x15, 0xa1, 0xf6,
0xf5, 0x82, 0xd4, 0x46, 0xe2, 0x7b, 0x17, 0x52, 0xbd, 0x7c, 0xbe, 0x83, 0x47, 0xef, 0xa3, 0x05, 0xef, 0xa4, 0x43, 0xe6, 0xac, 0x45, 0x16, 0xe8, 0x40, 0xe8, 0xbd, 0x70, 0xa3, 0x54, 0x4e, 0x92,
0x28, 0x0a, 0x69, 0x31, 0xd1, 0x45, 0x1b, 0x01, 0x2f, 0x97, 0xc5, 0x57, 0xb3, 0x95, 0xed, 0x1d, 0x91, 0xd9, 0x22, 0x8b, 0x2b, 0xe2, 0xf7, 0xa8, 0xed, 0x31, 0x97, 0xa2, 0xef, 0xa0, 0x10, 0xda,
0xc8, 0x09, 0xc1, 0x8a, 0x5e, 0x0a, 0xd2, 0xdd, 0x38, 0x2f, 0xbf, 0x7a, 0x8a, 0x7e, 0x70, 0x13, 0x06, 0x76, 0x87, 0xa2, 0xa8, 0xed, 0xf6, 0x2c, 0xe6, 0xd2, 0x8d, 0x96, 0x3f, 0x40, 0x3e, 0x38,
0x12, 0x34, 0xe1, 0xb6, 0x1b, 0xc8, 0x09, 0xb7, 0xc7, 0x82, 0x57, 0x87, 0x42, 0x22, 0x1e, 0xd0, 0x2f, 0xd8, 0x27, 0x48, 0x9c, 0x44, 0xc2, 0xbe, 0xa9, 0x9c, 0xae, 0xe0, 0xd1, 0xab, 0xb9, 0x01,
0xe5, 0x13, 0xcf, 0x7f, 0xd3, 0x5f, 0xe5, 0x69, 0x41, 0xe4, 0xf9, 0x33, 0x3c, 0x8b, 0x9e, 0x24, 0x14, 0xad, 0x0f, 0x71, 0xd7, 0x88, 0x6e, 0x04, 0xbc, 0x52, 0x11, 0xdf, 0xd2, 0xd2, 0xd6, 0x69,
0x7a, 0x2e, 0x88, 0x93, 0x59, 0x91, 0x98, 0xd8, 0xd6, 0x0b, 0x7e, 0xf3, 0xf5, 0x6f, 0xf5, 0xb1, 0x43, 0x51, 0x18, 0xf9, 0xe8, 0x95, 0x20, 0x5d, 0x5d, 0x34, 0x95, 0xd7, 0x9b, 0xe8, 0x67, 0x6f,
0xcd, 0x26, 0x8b, 0x41, 0x6d, 0xe8, 0xce, 0xeb, 0xb3, 0x20, 0xe0, 0x1c, 0xdb, 0x19, 0x3b, 0x84, 0xc2, 0x6c, 0x4f, 0x79, 0x5b, 0x5d, 0x15, 0x29, 0x6f, 0xeb, 0x56, 0x82, 0x0e, 0xe5, 0xd4, 0xd0,
0x2d, 0x5d, 0x3a, 0xad, 0xcf, 0x9c, 0x51, 0x9d, 0xc7, 0x4a, 0x7d, 0x63, 0x31, 0xc8, 0xf0, 0x9f, 0x40, 0xe7, 0x1b, 0x86, 0x42, 0x12, 0x5f, 0x75, 0xb3, 0x20, 0xf2, 0xf9, 0x13, 0xec, 0x46, 0x4f,
0x01, 0xaf, 0xff, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xd3, 0xc7, 0x77, 0x11, 0x4f, 0x08, 0x00, 0x00, 0x12, 0xbd, 0x10, 0xc4, 0xe9, 0x09, 0x92, 0xca, 0xd8, 0xd2, 0x0b, 0x7e, 0xf7, 0xf5, 0xaf, 0x8d,
0x81, 0xcd, 0x86, 0xd3, 0xc7, 0x7a, 0xcf, 0x9d, 0x34, 0xc6, 0xc1, 0xe8, 0x75, 0x6c, 0x67, 0xe0,
0x10, 0x36, 0x73, 0xe9, 0xa8, 0x31, 0x76, 0xfa, 0x0d, 0x3e, 0x6c, 0x1a, 0x89, 0x8b, 0xc7, 0x1c,
0xff, 0x07, 0xe5, 0xcd, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x5a, 0x4e, 0xaf, 0xaf, 0xe9, 0x08,
0x00, 0x00,
} }
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.

@ -191,6 +191,18 @@ message PendingSweep {
sweep transaction of the output. sweep transaction of the output.
*/ */
uint32 next_broadcast_height = 6 [json_name = "next_broadcast_height"]; uint32 next_broadcast_height = 6 [json_name = "next_broadcast_height"];
// The requested confirmation target for this output.
uint32 requested_conf_target = 8 [json_name = "requested_conf_target"];
// The requested fee rate, expressed in sat/byte, for this output.
uint32 requested_sat_per_byte = 9 [json_name = "requested_sat_per_byte"];
/**
Whether this input must be force-swept. This means that it is swept even
if it has a negative yield.
*/
bool force = 7 [json_name = "force"];
} }
message PendingSweepsRequest { message PendingSweepsRequest {
@ -215,6 +227,12 @@ message BumpFeeRequest {
with. with.
*/ */
uint32 sat_per_byte = 3 [json_name = "sat_per_byte"]; uint32 sat_per_byte = 3 [json_name = "sat_per_byte"];
/**
Whether this input must be force-swept. This means that it is swept even
if it has a negative yield.
*/
bool force = 4 [json_name = "force"];
} }
message BumpFeeResponse { message BumpFeeResponse {

@ -404,6 +404,9 @@ func (w *WalletKit) PendingSweeps(ctx context.Context,
broadcastAttempts := uint32(pendingInput.BroadcastAttempts) broadcastAttempts := uint32(pendingInput.BroadcastAttempts)
nextBroadcastHeight := uint32(pendingInput.NextBroadcastHeight) nextBroadcastHeight := uint32(pendingInput.NextBroadcastHeight)
requestedFee := pendingInput.Params.Fee
requestedFeeRate := uint32(requestedFee.FeeRate.FeePerKVByte() / 1000)
rpcPendingSweeps = append(rpcPendingSweeps, &PendingSweep{ rpcPendingSweeps = append(rpcPendingSweeps, &PendingSweep{
Outpoint: op, Outpoint: op,
WitnessType: witnessType, WitnessType: witnessType,
@ -411,6 +414,9 @@ func (w *WalletKit) PendingSweeps(ctx context.Context,
SatPerByte: satPerByte, SatPerByte: satPerByte,
BroadcastAttempts: broadcastAttempts, BroadcastAttempts: broadcastAttempts,
NextBroadcastHeight: nextBroadcastHeight, NextBroadcastHeight: nextBroadcastHeight,
RequestedSatPerByte: requestedFeeRate,
RequestedConfTarget: requestedFee.ConfTarget,
Force: pendingInput.Params.Force,
}) })
} }
@ -480,7 +486,12 @@ func (w *WalletKit) BumpFee(ctx context.Context,
// bump its fee, which will result in a replacement transaction (RBF) // bump its fee, which will result in a replacement transaction (RBF)
// being broadcast. If it is not aware of the input however, // being broadcast. If it is not aware of the input however,
// lnwallet.ErrNotMine is returned. // lnwallet.ErrNotMine is returned.
_, err = w.cfg.Sweeper.BumpFee(*op, feePreference) params := sweep.Params{
Fee: feePreference,
Force: in.Force,
}
_, err = w.cfg.Sweeper.UpdateParams(*op, params)
switch err { switch err {
case nil: case nil:
return &BumpFeeResponse{}, nil return &BumpFeeResponse{}, nil

@ -67,6 +67,15 @@ type Params struct {
// swept. If a confirmation target is specified, then we'll map it into // swept. If a confirmation target is specified, then we'll map it into
// a fee rate whenever we attempt to cluster inputs for a sweep. // a fee rate whenever we attempt to cluster inputs for a sweep.
Fee FeePreference Fee FeePreference
// Force indicates whether the input should be swept regardless of
// whether it is economical to do so.
Force bool
}
// String returns a human readable interpretation of the sweep parameters.
func (p Params) String() string {
return fmt.Sprintf("fee=%v, force=%v", p.Fee, p.Force)
} }
// pendingInput is created when an input reaches the main loop for the first // pendingInput is created when an input reaches the main loop for the first
@ -146,19 +155,22 @@ type PendingInput struct {
// NextBroadcastHeight is the next height of the chain at which we'll // NextBroadcastHeight is the next height of the chain at which we'll
// attempt to broadcast a transaction sweeping the input. // attempt to broadcast a transaction sweeping the input.
NextBroadcastHeight uint32 NextBroadcastHeight uint32
// Params contains the sweep parameters for this pending request.
Params Params
} }
// bumpFeeReq is an internal message we'll use to represent an external caller's // updateReq is an internal message we'll use to represent an external caller's
// intent to bump the fee rate of a given input. // intent to update the sweep parameters of a given input.
type bumpFeeReq struct { type updateReq struct {
input wire.OutPoint input wire.OutPoint
feePreference FeePreference params Params
responseChan chan *bumpFeeResp responseChan chan *updateResp
} }
// bumpFeeResp is an internal message we'll use to hand off the response of a // updateResp is an internal message we'll use to hand off the response of a
// bumpFeeReq from the UtxoSweeper's main event loop back to the caller. // updateReq from the UtxoSweeper's main event loop back to the caller.
type bumpFeeResp struct { type updateResp struct {
resultChan chan Result resultChan chan Result
err error err error
} }
@ -178,9 +190,9 @@ type UtxoSweeper struct {
// UtxoSweeper is attempting to sweep. // UtxoSweeper is attempting to sweep.
pendingSweepsReqs chan *pendingSweepsReq pendingSweepsReqs chan *pendingSweepsReq
// bumpFeeReqs is a channel that will be sent requests by external // updateReqs is a channel that will be sent requests by external
// callers who wish to bump the fee rate of a given input. // callers who wish to bump the fee rate of a given input.
bumpFeeReqs chan *bumpFeeReq updateReqs chan *updateReq
// pendingInputs is the total set of inputs the UtxoSweeper has been // pendingInputs is the total set of inputs the UtxoSweeper has been
// requested to sweep. // requested to sweep.
@ -287,7 +299,7 @@ func New(cfg *UtxoSweeperConfig) *UtxoSweeper {
cfg: cfg, cfg: cfg,
newInputs: make(chan *sweepInputMessage), newInputs: make(chan *sweepInputMessage),
spendChan: make(chan *chainntnfs.SpendDetail), spendChan: make(chan *chainntnfs.SpendDetail),
bumpFeeReqs: make(chan *bumpFeeReq), updateReqs: make(chan *updateReq),
pendingSweepsReqs: make(chan *pendingSweepsReq), pendingSweepsReqs: make(chan *pendingSweepsReq),
quit: make(chan struct{}), quit: make(chan struct{}),
pendingInputs: make(pendingInputs), pendingInputs: make(pendingInputs),
@ -390,10 +402,10 @@ func (s *UtxoSweeper) SweepInput(input input.Input,
} }
log.Infof("Sweep request received: out_point=%v, witness_type=%v, "+ log.Infof("Sweep request received: out_point=%v, witness_type=%v, "+
"time_lock=%v, amount=%v, fee_preference=%v", input.OutPoint(), "time_lock=%v, amount=%v, fee_preference=%v, force=%v",
input.WitnessType(), input.BlocksToMaturity(), input.OutPoint(), input.WitnessType(), input.BlocksToMaturity(),
btcutil.Amount(input.SignDesc().Output.Value), btcutil.Amount(input.SignDesc().Output.Value),
params.Fee) params.Fee, params.Force)
sweeperInput := &sweepInputMessage{ sweeperInput := &sweepInputMessage{
input: input, input: input,
@ -572,9 +584,9 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) {
// A new external request has been received to bump the fee rate // A new external request has been received to bump the fee rate
// of a given input. // of a given input.
case req := <-s.bumpFeeReqs: case req := <-s.updateReqs:
resultChan, err := s.handleBumpFeeReq(req, bestHeight) resultChan, err := s.handleUpdateReq(req, bestHeight)
req.responseChan <- &bumpFeeResp{ req.responseChan <- &updateResp{
resultChan: resultChan, resultChan: resultChan,
err: err, err: err,
} }
@ -1034,33 +1046,34 @@ func (s *UtxoSweeper) handlePendingSweepsReq(
LastFeeRate: pendingInput.lastFeeRate, LastFeeRate: pendingInput.lastFeeRate,
BroadcastAttempts: pendingInput.publishAttempts, BroadcastAttempts: pendingInput.publishAttempts,
NextBroadcastHeight: uint32(pendingInput.minPublishHeight), NextBroadcastHeight: uint32(pendingInput.minPublishHeight),
Params: pendingInput.params,
} }
} }
return pendingInputs return pendingInputs
} }
// BumpFee allows bumping the fee of an input being swept by the UtxoSweeper // UpdateParams allows updating the sweep parameters of a pending input in the
// according to the provided fee preference. The new fee preference will be used // UtxoSweeper. This function can be used to provide an updated fee preference
// for a new sweep transaction of the input that will act as a replacement // that will be used for a new sweep transaction of the input that will act as a
// transaction (RBF) of the original sweeping transaction, if any. // replacement transaction (RBF) of the original sweeping transaction, if any.
// //
// NOTE: This currently doesn't do any fee rate validation to ensure that a bump // NOTE: This currently doesn't do any fee rate validation to ensure that a bump
// is actually successful. The responsibility of doing so should be handled by // is actually successful. The responsibility of doing so should be handled by
// the caller. // the caller.
func (s *UtxoSweeper) BumpFee(input wire.OutPoint, func (s *UtxoSweeper) UpdateParams(input wire.OutPoint,
feePreference FeePreference) (chan Result, error) { params Params) (chan Result, error) {
// Ensure the client provided a sane fee preference. // Ensure the client provided a sane fee preference.
if _, err := s.feeRateForPreference(feePreference); err != nil { if _, err := s.feeRateForPreference(params.Fee); err != nil {
return nil, err return nil, err
} }
responseChan := make(chan *bumpFeeResp, 1) responseChan := make(chan *updateResp, 1)
select { select {
case s.bumpFeeReqs <- &bumpFeeReq{ case s.updateReqs <- &updateReq{
input: input, input: input,
feePreference: feePreference, params: params,
responseChan: responseChan, responseChan: responseChan,
}: }:
case <-s.quit: case <-s.quit:
@ -1075,9 +1088,9 @@ func (s *UtxoSweeper) BumpFee(input wire.OutPoint,
} }
} }
// handleBumpFeeReq handles a bump fee request by simply updating the inputs fee // handleUpdateReq handles an update request by simply updating the sweep
// preference. Currently, no validation is done on the new fee preference to // parameters of the pending input. Currently, no validation is done on the new
// ensure it will properly create a replacement transaction. // fee preference to ensure it will properly create a replacement transaction.
// //
// TODO(wilmer): // TODO(wilmer):
// * Validate fee preference to ensure we'll create a valid replacement // * Validate fee preference to ensure we'll create a valid replacement
@ -1086,8 +1099,8 @@ func (s *UtxoSweeper) BumpFee(input wire.OutPoint,
// * Ensure we don't combine this input with any other unconfirmed inputs that // * Ensure we don't combine this input with any other unconfirmed inputs that
// did not exist in the original sweep transaction, resulting in an invalid // did not exist in the original sweep transaction, resulting in an invalid
// replacement transaction. // replacement transaction.
func (s *UtxoSweeper) handleBumpFeeReq(req *bumpFeeReq, func (s *UtxoSweeper) handleUpdateReq(req *updateReq, bestHeight int32) (
bestHeight int32) (chan Result, error) { chan Result, error) {
// If the UtxoSweeper is already trying to sweep this input, then we can // If the UtxoSweeper is already trying to sweep this input, then we can
// simply just increase its fee rate. This will allow the input to be // simply just increase its fee rate. This will allow the input to be
@ -1099,10 +1112,10 @@ func (s *UtxoSweeper) handleBumpFeeReq(req *bumpFeeReq,
return nil, lnwallet.ErrNotMine return nil, lnwallet.ErrNotMine
} }
log.Debugf("Updating fee preference for %v from %v to %v", req.input, log.Debugf("Updating sweep parameters for %v from %v to %v", req.input,
pendingInput.params.Fee, req.feePreference) pendingInput.params, req.params)
pendingInput.params.Fee = req.feePreference pendingInput.params = req.params
// We'll reset the input's publish height to the current so that a new // We'll reset the input's publish height to the current so that a new
// transaction can be created that replaces the transaction currently // transaction can be created that replaces the transaction currently

@ -1178,7 +1178,9 @@ func TestBumpFeeRBF(t *testing.T) {
// We'll first try to bump the fee of an output currently unknown to the // We'll first try to bump the fee of an output currently unknown to the
// UtxoSweeper. Doing so should result in a lnwallet.ErrNotMine error. // UtxoSweeper. Doing so should result in a lnwallet.ErrNotMine error.
bumpResult, err := ctx.sweeper.BumpFee(wire.OutPoint{}, lowFeePref) _, err := ctx.sweeper.UpdateParams(
wire.OutPoint{}, Params{Fee: lowFeePref},
)
if err != lnwallet.ErrNotMine { if err != lnwallet.ErrNotMine {
t.Fatalf("expected error lnwallet.ErrNotMine, got \"%v\"", err) t.Fatalf("expected error lnwallet.ErrNotMine, got \"%v\"", err)
} }
@ -1206,12 +1208,14 @@ func TestBumpFeeRBF(t *testing.T) {
ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate
// We should expect to see an error if a fee preference isn't provided. // We should expect to see an error if a fee preference isn't provided.
_, err = ctx.sweeper.BumpFee(*input.OutPoint(), FeePreference{}) _, err = ctx.sweeper.UpdateParams(*input.OutPoint(), Params{})
if err != ErrNoFeePreference { if err != ErrNoFeePreference {
t.Fatalf("expected ErrNoFeePreference, got %v", err) t.Fatalf("expected ErrNoFeePreference, got %v", err)
} }
bumpResult, err = ctx.sweeper.BumpFee(*input.OutPoint(), highFeePref) bumpResult, err := ctx.sweeper.UpdateParams(
*input.OutPoint(), Params{Fee: highFeePref},
)
if err != nil { if err != nil {
t.Fatalf("unable to bump input's fee: %v", err) t.Fatalf("unable to bump input's fee: %v", err)
} }

@ -13,6 +13,23 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
// addConstraints defines the constraints to apply when adding an input.
type addConstraints uint8
const (
// constraintsRegular is for regular input sweeps that should have a positive
// yield.
constraintsRegular addConstraints = iota
// constraintsWallet is for wallet inputs that are only added to bring up the tx
// output value.
constraintsWallet
// constraintsForce is for inputs that should be swept even with a negative
// yield at the set fee rate.
constraintsForce
)
// txInputSet is an object that accumulates tx inputs and keeps running counters // txInputSet is an object that accumulates tx inputs and keeps running counters
// on various properties of the tx. // on various properties of the tx.
type txInputSet struct { type txInputSet struct {
@ -45,6 +62,10 @@ type txInputSet struct {
// wallet contains wallet functionality required by the input set to // wallet contains wallet functionality required by the input set to
// retrieve utxos. // retrieve utxos.
wallet Wallet wallet Wallet
// force indicates that this set must be swept even if the total yield
// is negative.
force bool
} }
// newTxInputSet constructs a new, empty input set. // newTxInputSet constructs a new, empty input set.
@ -78,10 +99,12 @@ func (t *txInputSet) dustLimitReached() bool {
// add adds a new input to the set. It returns a bool indicating whether the // add adds a new input to the set. It returns a bool indicating whether the
// input was added to the set. An input is rejected if it decreases the tx // input was added to the set. An input is rejected if it decreases the tx
// output value after paying fees. // output value after paying fees.
func (t *txInputSet) add(input input.Input, fromWallet bool) bool { func (t *txInputSet) add(input input.Input, constraints addConstraints) bool {
// Stop if max inputs is reached. Do not count additional wallet inputs, // Stop if max inputs is reached. Do not count additional wallet inputs,
// because we don't know in advance how many we may need. // because we don't know in advance how many we may need.
if !fromWallet && len(t.inputs) >= t.maxInputs { if constraints != constraintsWallet &&
len(t.inputs) >= t.maxInputs {
return false return false
} }
@ -108,23 +131,42 @@ func (t *txInputSet) add(input input.Input, fromWallet bool) bool {
// added to the set. // added to the set.
newOutputValue := newInputTotal - fee newOutputValue := newInputTotal - fee
// If adding this input makes the total output value of the set // Initialize new wallet total with the current wallet total. This is
// decrease, this is a negative yield input. We don't add the input to // updated below if this input is a wallet input.
// the set and return the outcome. newWalletTotal := t.walletInputTotal
if newOutputValue <= t.outputValue {
// Calculate the yield of this input from the change in tx output value.
inputYield := newOutputValue - t.outputValue
switch constraints {
// Don't sweep inputs that cost us more to sweep than they give us.
case constraintsRegular:
if inputYield <= 0 {
return false
}
// For force adds, no further constraints apply.
case constraintsForce:
t.force = true
// We are attaching a wallet input to raise the tx output value above
// the dust limit.
case constraintsWallet:
// Skip this wallet input if adding it would lower the output
// value.
if inputYield <= 0 {
return false return false
} }
// If this input comes from the wallet, verify that we still gain
// something with this transaction.
if fromWallet {
// Calculate the total value that we spend in this tx from the // Calculate the total value that we spend in this tx from the
// wallet if we'd add this wallet input. // wallet if we'd add this wallet input.
newWalletTotal := t.walletInputTotal + value newWalletTotal += value
// In any case, we don't want to lose money by sweeping. If we // In any case, we don't want to lose money by sweeping. If we
// don't get more out of the tx then we put in ourselves, do not // don't get more out of the tx then we put in ourselves, do not
// add this wallet input. // add this wallet input. If there is at least one force sweep
// in the set, this does no longer apply.
// //
// We should only add wallet inputs to get the tx output value // We should only add wallet inputs to get the tx output value
// above the dust limit, otherwise we'd only burn into fees. // above the dust limit, otherwise we'd only burn into fees.
@ -134,7 +176,7 @@ func (t *txInputSet) add(input input.Input, fromWallet bool) bool {
// value of the wallet input and what we get out of this // value of the wallet input and what we get out of this
// transaction. To prevent attaching and locking a big utxo for // transaction. To prevent attaching and locking a big utxo for
// very little benefit. // very little benefit.
if newWalletTotal >= newOutputValue { if !t.force && newWalletTotal >= newOutputValue {
log.Debugf("Rejecting wallet input of %v, because it "+ log.Debugf("Rejecting wallet input of %v, because it "+
"would make a negative yielding transaction "+ "would make a negative yielding transaction "+
"(%v)", "(%v)",
@ -142,17 +184,16 @@ func (t *txInputSet) add(input input.Input, fromWallet bool) bool {
return false return false
} }
// We've decided to add the wallet input. Increment the total
// wallet funds that go into this tx.
t.walletInputTotal = newWalletTotal
} }
// Update running values. // Update running values.
//
// TODO: Return new instance?
t.inputTotal = newInputTotal t.inputTotal = newInputTotal
t.outputValue = newOutputValue t.outputValue = newOutputValue
t.inputs = append(t.inputs, input) t.inputs = append(t.inputs, input)
t.weightEstimate = newWeightEstimate t.weightEstimate = newWeightEstimate
t.walletInputTotal = newWalletTotal
return true return true
} }
@ -167,11 +208,17 @@ func (t *txInputSet) add(input input.Input, fromWallet bool) bool {
// whole. // whole.
func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) { func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) {
for _, input := range sweepableInputs { for _, input := range sweepableInputs {
// Apply relaxed constraints for force sweeps.
constraints := constraintsRegular
if input.parameters().Force {
constraints = constraintsForce
}
// Try to add the input to the transaction. If that doesn't // Try to add the input to the transaction. If that doesn't
// succeed because it wouldn't increase the output value, // succeed because it wouldn't increase the output value,
// return. Assuming inputs are sorted by yield, any further // return. Assuming inputs are sorted by yield, any further
// inputs wouldn't increase the output value either. // inputs wouldn't increase the output value either.
if !t.add(input, false) { if !t.add(input, constraints) {
return return
} }
} }
@ -202,7 +249,7 @@ func (t *txInputSet) tryAddWalletInputsIfNeeded() error {
// If the wallet input isn't positively-yielding at this fee // If the wallet input isn't positively-yielding at this fee
// rate, skip it. // rate, skip it.
if !t.add(input, true) { if !t.add(input, constraintsWallet) {
continue continue
} }

@ -24,13 +24,13 @@ func TestTxInputSet(t *testing.T) {
// Create a 300 sat input. The fee to sweep this input to a P2WKH output // Create a 300 sat input. The fee to sweep this input to a P2WKH output
// is 439 sats. That means that this input yields -139 sats and we // is 439 sats. That means that this input yields -139 sats and we
// expect it not to be added. // expect it not to be added.
if set.add(createP2WKHInput(300), false) { if set.add(createP2WKHInput(300), constraintsRegular) {
t.Fatal("expected add of negatively yielding input to fail") t.Fatal("expected add of negatively yielding input to fail")
} }
// A 700 sat input should be accepted into the set, because it yields // A 700 sat input should be accepted into the set, because it yields
// positively. // positively.
if !set.add(createP2WKHInput(700), false) { if !set.add(createP2WKHInput(700), constraintsRegular) {
t.Fatal("expected add of positively yielding input to succeed") t.Fatal("expected add of positively yielding input to succeed")
} }
@ -45,7 +45,7 @@ func TestTxInputSet(t *testing.T) {
// Add a 1000 sat input. This increases the tx fee to 712 sats. The tx // Add a 1000 sat input. This increases the tx fee to 712 sats. The tx
// output should now be 1000+700 - 712 = 988 sats. // output should now be 1000+700 - 712 = 988 sats.
if !set.add(createP2WKHInput(1000), false) { if !set.add(createP2WKHInput(1000), constraintsRegular) {
t.Fatal("expected add of positively yielding input to succeed") t.Fatal("expected add of positively yielding input to succeed")
} }
if set.outputValue != 988 { if set.outputValue != 988 {
@ -70,13 +70,23 @@ func TestTxInputSetFromWallet(t *testing.T) {
// Add a 700 sat input to the set. It yields positively, but doesn't // Add a 700 sat input to the set. It yields positively, but doesn't
// reach the output dust limit. // reach the output dust limit.
if !set.add(createP2WKHInput(700), false) { if !set.add(createP2WKHInput(700), constraintsRegular) {
t.Fatal("expected add of positively yielding input to succeed") t.Fatal("expected add of positively yielding input to succeed")
} }
if set.dustLimitReached() { if set.dustLimitReached() {
t.Fatal("expected dust limit not yet to be reached") t.Fatal("expected dust limit not yet to be reached")
} }
// Expect that adding a negative yield input fails.
if set.add(createP2WKHInput(50), constraintsRegular) {
t.Fatal("expected negative yield input add to fail")
}
// Force add the negative yield input. It should succeed.
if !set.add(createP2WKHInput(50), constraintsForce) {
t.Fatal("expected forced add to succeed")
}
err := set.tryAddWalletInputsIfNeeded() err := set.tryAddWalletInputsIfNeeded()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

@ -67,6 +67,14 @@ func generateInputPartitionings(sweepableInputs []txInput,
} }
sort.Slice(sweepableInputs, func(i, j int) bool { sort.Slice(sweepableInputs, func(i, j int) bool {
// Because of the specific ordering and termination condition
// that is described above, we place force sweeps at the start
// of the list. Otherwise we can't be sure that they will be
// included in an input set.
if sweepableInputs[i].parameters().Force {
return true
}
return yields[*sweepableInputs[i].OutPoint()] > return yields[*sweepableInputs[i].OutPoint()] >
yields[*sweepableInputs[j].OutPoint()] yields[*sweepableInputs[j].OutPoint()]
}) })