diff --git a/cmd/lncli/walletrpc_active.go b/cmd/lncli/walletrpc_active.go index 35dc562d67..31b14b764f 100644 --- a/cmd/lncli/walletrpc_active.go +++ b/cmd/lncli/walletrpc_active.go @@ -4,7 +4,6 @@ package main import ( "context" - "fmt" "sort" "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 on the fee preference being provided. For now, the responsibility of 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{ cli.Uint64Flag{ Name: "conf_target", @@ -125,6 +130,10 @@ var bumpFeeCommand = cli.Command{ Usage: "a manual fee expressed in sat/byte that " + "should be used when sweeping the output", }, + cli.BoolFlag{ + Name: "force", + Usage: "sweep even if the yield is negative", + }, }, Action: actionDecorator(bumpFee), } @@ -132,7 +141,7 @@ var bumpFeeCommand = cli.Command{ func bumpFee(ctx *cli.Context) error { // Display the command's help message if we do not have the expected // number of arguments/flags. - if ctx.NArg() != 1 || ctx.NumFlags() != 1 { + if ctx.NArg() != 1 { return cli.ShowCommandHelp(ctx, "bumpfee") } @@ -142,24 +151,14 @@ func bumpFee(ctx *cli.Context) error { 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) defer cleanUp() resp, err := client.BumpFee(context.Background(), &walletrpc.BumpFeeRequest{ Outpoint: protoOutPoint, - TargetConf: confTarget, - SatPerByte: satPerByte, + TargetConf: uint32(ctx.Uint64("conf_target")), + SatPerByte: uint32(ctx.Uint64("sat_per_byte")), + Force: ctx.Bool("force"), }) if err != nil { return err diff --git a/cmd/lncli/walletrpc_types.go b/cmd/lncli/walletrpc_types.go index f336e02e20..c2b5698191 100644 --- a/cmd/lncli/walletrpc_types.go +++ b/cmd/lncli/walletrpc_types.go @@ -11,6 +11,9 @@ type PendingSweep struct { SatPerByte uint32 `json:"sat_per_byte"` BroadcastAttempts uint32 `json:"broadcast_attempts"` 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 @@ -23,5 +26,8 @@ func NewPendingSweepFromProto(pendingSweep *walletrpc.PendingSweep) *PendingSwee SatPerByte: pendingSweep.SatPerByte, BroadcastAttempts: pendingSweep.BroadcastAttempts, NextBroadcastHeight: pendingSweep.NextBroadcastHeight, + RequestedSatPerByte: pendingSweep.RequestedSatPerByte, + RequestedConfTarget: pendingSweep.RequestedConfTarget, + Force: pendingSweep.Force, } } diff --git a/lnrpc/walletrpc/walletkit.pb.go b/lnrpc/walletrpc/walletkit.pb.go index 9737602c58..e2ded0cc0c 100644 --- a/lnrpc/walletrpc/walletkit.pb.go +++ b/lnrpc/walletrpc/walletkit.pb.go @@ -531,7 +531,15 @@ type PendingSweep struct { // //The next height of the chain at which we'll attempt to broadcast the //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_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -604,6 +612,27 @@ func (m *PendingSweep) GetNextBroadcastHeight() uint32 { 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 { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -684,7 +713,11 @@ type BumpFeeRequest struct { // //The fee rate, expressed in sat/byte, that should be used to spend the input //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_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -736,6 +769,13 @@ func (m *BumpFeeRequest) GetSatPerByte() uint32 { return 0 } +func (m *BumpFeeRequest) GetForce() bool { + if m != nil { + return m.Force + } + return false +} + type BumpFeeResponse struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -788,68 +828,72 @@ func init() { func init() { proto.RegisterFile("walletrpc/walletkit.proto", fileDescriptor_6cc6942ac78249e5) } var fileDescriptor_6cc6942ac78249e5 = []byte{ - // 976 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xed, 0x6e, 0xe2, 0x46, - 0x14, 0x2d, 0x21, 0x61, 0xc3, 0x05, 0x12, 0x67, 0x08, 0x89, 0x97, 0xcd, 0x6e, 0xa8, 0xfb, 0x21, - 0xd4, 0x56, 0xa0, 0x66, 0xdb, 0xaa, 0x6a, 0x7f, 0xb4, 0x59, 0x70, 0x44, 0xc4, 0x87, 0xa9, 0xed, - 0x6c, 0xba, 0x55, 0xa5, 0x91, 0x81, 0x59, 0xb0, 0x00, 0xdb, 0x3b, 0x1e, 0x0a, 0xfc, 0x6d, 0x9f, - 0xa4, 0xaf, 0xd1, 0xa7, 0xab, 0x3c, 0xb6, 0xc9, 0x18, 0x92, 0x4a, 0xfd, 0x15, 0xe7, 0x9c, 0x73, - 0xcf, 0xdc, 0xb9, 0x33, 0x73, 0x04, 0x3c, 0x5f, 0x5a, 0xb3, 0x19, 0x61, 0xd4, 0x1b, 0xd6, 0xc3, - 0xaf, 0xa9, 0xcd, 0x6a, 0x1e, 0x75, 0x99, 0x8b, 0xb2, 0x1b, 0xaa, 0x9c, 0xa5, 0xde, 0x30, 0x44, - 0xcb, 0xa7, 0xbe, 0x3d, 0x76, 0x02, 0x79, 0xf0, 0x97, 0xd0, 0x10, 0x55, 0x7e, 0x81, 0x4c, 0x9b, - 0xac, 0x75, 0xf2, 0x01, 0x55, 0x41, 0x9a, 0x92, 0x35, 0x7e, 0x6f, 0x3b, 0x63, 0x42, 0xb1, 0x47, - 0x6d, 0x87, 0xc9, 0xa9, 0x4a, 0xaa, 0x7a, 0xa0, 0x1f, 0x4d, 0xc9, 0xfa, 0x86, 0xc3, 0xfd, 0x00, - 0x45, 0x2f, 0x01, 0xb8, 0xd2, 0x9a, 0xdb, 0xb3, 0xb5, 0xbc, 0xc7, 0x35, 0xd9, 0x40, 0xc3, 0x01, - 0xa5, 0x00, 0xb9, 0xeb, 0xd1, 0x88, 0xea, 0xe4, 0xc3, 0x82, 0xf8, 0x4c, 0x51, 0x20, 0x1f, 0xfe, - 0xeb, 0x7b, 0xae, 0xe3, 0x13, 0x84, 0x60, 0xdf, 0x1a, 0x8d, 0x28, 0xf7, 0xce, 0xea, 0xfc, 0x5b, - 0xf9, 0x14, 0x72, 0x26, 0xb5, 0x1c, 0xdf, 0x1a, 0x32, 0xdb, 0x75, 0x50, 0x09, 0x32, 0x6c, 0x85, - 0x27, 0x64, 0xc5, 0x45, 0x79, 0xfd, 0x80, 0xad, 0x5a, 0x64, 0xa5, 0x7c, 0x07, 0xc7, 0xfd, 0xc5, - 0x60, 0x66, 0xfb, 0x93, 0x8d, 0xd9, 0x27, 0x50, 0xf0, 0x42, 0x08, 0x13, 0x4a, 0xdd, 0xd8, 0x35, - 0x1f, 0x81, 0x6a, 0x80, 0x29, 0xbf, 0x03, 0x32, 0x88, 0x33, 0xd2, 0x16, 0xcc, 0x5b, 0x30, 0x3f, - 0xea, 0x0b, 0x5d, 0x00, 0xf8, 0x16, 0xc3, 0x1e, 0xa1, 0x78, 0xba, 0xe4, 0x75, 0x69, 0xfd, 0xd0, - 0xb7, 0x58, 0x9f, 0xd0, 0xf6, 0x12, 0x55, 0xe1, 0x99, 0x1b, 0xea, 0xe5, 0xbd, 0x4a, 0xba, 0x9a, - 0xbb, 0x3a, 0xaa, 0x45, 0xf3, 0xab, 0x99, 0x2b, 0x6d, 0xc1, 0xf4, 0x98, 0x56, 0xbe, 0x82, 0x62, - 0xc2, 0x3d, 0xea, 0xac, 0x04, 0x19, 0x6a, 0x2d, 0x31, 0xdb, 0xec, 0x81, 0x5a, 0x4b, 0x73, 0xa5, - 0x7c, 0x0b, 0x48, 0xf5, 0x99, 0x3d, 0xb7, 0x18, 0xb9, 0x21, 0x24, 0xee, 0xe5, 0x12, 0x72, 0x43, - 0xd7, 0x79, 0x8f, 0x99, 0x45, 0xc7, 0x24, 0x1e, 0x3b, 0x04, 0x90, 0xc9, 0x11, 0xe5, 0x35, 0x14, - 0x13, 0x65, 0xd1, 0x22, 0xff, 0xb9, 0x07, 0xe5, 0xef, 0x3d, 0xc8, 0xf7, 0x89, 0x33, 0xb2, 0x9d, - 0xb1, 0xb1, 0x24, 0xc4, 0x43, 0x5f, 0xc2, 0x61, 0xd0, 0xb5, 0x1b, 0x1f, 0x6d, 0xee, 0xea, 0xb8, - 0x36, 0xe3, 0x7b, 0xd2, 0x16, 0xac, 0x1f, 0xc0, 0xfa, 0x46, 0x80, 0x7e, 0x80, 0xfc, 0xd2, 0x66, - 0x0e, 0xf1, 0x7d, 0xcc, 0xd6, 0x1e, 0xe1, 0xe7, 0x7c, 0x74, 0x75, 0x56, 0xdb, 0x5c, 0xae, 0xda, - 0x7d, 0x48, 0x9b, 0x6b, 0x8f, 0xe8, 0x09, 0x2d, 0x7a, 0x05, 0x60, 0xcd, 0xdd, 0x85, 0xc3, 0xb0, - 0x6f, 0x31, 0x39, 0x5d, 0x49, 0x55, 0x0b, 0xba, 0x80, 0x20, 0x05, 0xf2, 0x71, 0xdf, 0x83, 0x35, - 0x23, 0xf2, 0x3e, 0x57, 0x24, 0x30, 0x54, 0x03, 0x34, 0xa0, 0xae, 0x35, 0x1a, 0x5a, 0x3e, 0xc3, - 0x16, 0x63, 0x64, 0xee, 0x31, 0x5f, 0x3e, 0xe0, 0xca, 0x47, 0x18, 0xf4, 0x0d, 0x94, 0x1c, 0xb2, - 0x62, 0xf8, 0x81, 0x9a, 0x10, 0x7b, 0x3c, 0x61, 0x72, 0x86, 0x97, 0x3c, 0x4e, 0x2a, 0x67, 0x70, - 0x2a, 0x8e, 0x28, 0xbe, 0x1d, 0xca, 0xaf, 0x50, 0xda, 0xc2, 0xa3, 0x91, 0xff, 0x04, 0x47, 0x5e, - 0x48, 0x60, 0x9f, 0x33, 0x72, 0x8a, 0xdf, 0x8f, 0x73, 0x61, 0x30, 0x62, 0xa5, 0xbe, 0x25, 0x57, - 0xfe, 0x4a, 0xc1, 0xd1, 0x9b, 0xc5, 0xdc, 0x13, 0x8e, 0xff, 0x7f, 0x9d, 0x4b, 0x05, 0x72, 0xe1, - 0x35, 0xc1, 0xc1, 0xfd, 0xe0, 0xc7, 0x52, 0xd0, 0x45, 0x68, 0x67, 0xba, 0xe9, 0xdd, 0xe9, 0x2a, - 0x27, 0x70, 0xbc, 0x69, 0x22, 0xdc, 0xd9, 0x17, 0x7f, 0xa6, 0x21, 0x27, 0x1c, 0x29, 0x2a, 0xc2, - 0xf1, 0x5d, 0xaf, 0xdd, 0xd3, 0xee, 0x7b, 0xf8, 0xfe, 0xd6, 0xec, 0xa9, 0x86, 0x21, 0x7d, 0x84, - 0x64, 0x38, 0x6d, 0x68, 0xdd, 0xee, 0xad, 0xd9, 0x55, 0x7b, 0x26, 0x36, 0x6f, 0xbb, 0x2a, 0xee, - 0x68, 0x8d, 0xb6, 0x94, 0x42, 0xe7, 0x50, 0x14, 0x98, 0x9e, 0x86, 0x9b, 0x6a, 0xe7, 0xfa, 0x9d, - 0xb4, 0x87, 0x4a, 0x70, 0x22, 0x10, 0xba, 0xfa, 0x56, 0x6b, 0xab, 0x52, 0x3a, 0xd0, 0xb7, 0xcc, - 0x4e, 0x03, 0x6b, 0x37, 0x37, 0xaa, 0xae, 0x36, 0x63, 0x62, 0x3f, 0x58, 0x82, 0x13, 0xd7, 0x8d, - 0x86, 0xda, 0x37, 0x1f, 0x98, 0x03, 0xf4, 0x19, 0x7c, 0x9c, 0x28, 0x09, 0x96, 0xd7, 0xee, 0x4c, - 0x6c, 0xa8, 0x0d, 0xad, 0xd7, 0xc4, 0x1d, 0xf5, 0xad, 0xda, 0x91, 0x32, 0xe8, 0x73, 0x50, 0x92, - 0x06, 0xc6, 0x5d, 0xa3, 0xa1, 0x1a, 0x46, 0x52, 0xf7, 0x0c, 0x5d, 0xc2, 0x8b, 0xad, 0x0e, 0xba, - 0x9a, 0xa9, 0xc6, 0xae, 0xd2, 0x21, 0xaa, 0xc0, 0xc5, 0x76, 0x27, 0x5c, 0x11, 0xf9, 0x49, 0x59, - 0x74, 0x01, 0x32, 0x57, 0x88, 0xce, 0x71, 0xbf, 0x80, 0x4e, 0x41, 0x8a, 0x26, 0x87, 0xdb, 0xea, - 0x3b, 0xdc, 0xba, 0x36, 0x5a, 0x52, 0x0e, 0xbd, 0x80, 0xf3, 0x9e, 0x6a, 0x04, 0x76, 0x3b, 0x64, - 0xfe, 0xea, 0x9f, 0x7d, 0xc8, 0xde, 0xf3, 0x8b, 0xd4, 0xb6, 0x83, 0x37, 0x58, 0x68, 0x12, 0x6a, - 0xff, 0x41, 0x7a, 0x64, 0xc5, 0xda, 0x64, 0x8d, 0x4e, 0x84, 0x5b, 0x16, 0xe6, 0x76, 0xf9, 0x6c, - 0x13, 0x4c, 0x6d, 0xb2, 0x6e, 0x12, 0x7f, 0x48, 0x6d, 0x8f, 0xb9, 0x14, 0x7d, 0x0f, 0xd9, 0xb0, - 0x36, 0xa8, 0x2b, 0x8a, 0xa2, 0x8e, 0x3b, 0xb4, 0x98, 0x4b, 0x9f, 0xac, 0xfc, 0x11, 0x0e, 0x83, - 0xf5, 0x82, 0xd4, 0x46, 0xe2, 0x7b, 0x17, 0x52, 0xbd, 0x7c, 0xbe, 0x83, 0x47, 0xef, 0xa3, 0x05, - 0x28, 0x0a, 0x69, 0x31, 0xd1, 0x45, 0x1b, 0x01, 0x2f, 0x97, 0xc5, 0x57, 0xb3, 0x95, 0xed, 0x1d, - 0xc8, 0x09, 0xc1, 0x8a, 0x5e, 0x0a, 0xd2, 0xdd, 0x38, 0x2f, 0xbf, 0x7a, 0x8a, 0x7e, 0x70, 0x13, - 0x12, 0x34, 0xe1, 0xb6, 0x1b, 0xc8, 0x09, 0xb7, 0xc7, 0x82, 0x57, 0x87, 0x42, 0x22, 0x1e, 0xd0, - 0xe5, 0x13, 0xcf, 0x7f, 0xd3, 0x5f, 0xe5, 0x69, 0x41, 0xe4, 0xf9, 0x33, 0x3c, 0x8b, 0x9e, 0x24, - 0x7a, 0x2e, 0x88, 0x93, 0x59, 0x91, 0x98, 0xd8, 0xd6, 0x0b, 0x7e, 0xf3, 0xf5, 0x6f, 0xf5, 0xb1, - 0xcd, 0x26, 0x8b, 0x41, 0x6d, 0xe8, 0xce, 0xeb, 0xb3, 0x20, 0xe0, 0x1c, 0xdb, 0x19, 0x3b, 0x84, - 0x2d, 0x5d, 0x3a, 0xad, 0xcf, 0x9c, 0x51, 0x9d, 0xc7, 0x4a, 0x7d, 0x63, 0x31, 0xc8, 0xf0, 0x9f, - 0x01, 0xaf, 0xff, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xd3, 0xc7, 0x77, 0x11, 0x4f, 0x08, 0x00, 0x00, + // 1026 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0x6d, 0x6f, 0xe2, 0xc6, + 0x13, 0xff, 0x13, 0x12, 0x02, 0x03, 0x24, 0xce, 0xe6, 0xc9, 0xc7, 0xe5, 0x2e, 0xfc, 0xdd, 0x07, + 0xa1, 0xb6, 0x02, 0x35, 0xd7, 0x9e, 0xaa, 0xf6, 0x45, 0x9b, 0x23, 0x8e, 0x12, 0x41, 0x30, 0xb5, + 0x9d, 0x4b, 0xaf, 0xaa, 0xb4, 0x72, 0x60, 0x03, 0x16, 0x60, 0xfb, 0xd6, 0x4b, 0x81, 0xb7, 0xfd, + 0x16, 0xfd, 0x00, 0xfd, 0x12, 0xfd, 0x74, 0x95, 0xd7, 0x0f, 0x59, 0xf3, 0x50, 0xa9, 0xaf, 0x82, + 0x7f, 0xbf, 0xdf, 0xcc, 0xce, 0xce, 0xcc, 0xce, 0x04, 0x5e, 0xcc, 0xac, 0xf1, 0x98, 0x30, 0xea, + 0xf5, 0x1a, 0xe1, 0xaf, 0x91, 0xcd, 0xea, 0x1e, 0x75, 0x99, 0x8b, 0x0a, 0x09, 0x55, 0x29, 0x50, + 0xaf, 0x17, 0xa2, 0x95, 0x23, 0xdf, 0x1e, 0x38, 0x81, 0x3c, 0xf8, 0x4b, 0x68, 0x88, 0x2a, 0x3f, + 0x43, 0xae, 0x45, 0x16, 0x3a, 0xf9, 0x88, 0x6a, 0x20, 0x8d, 0xc8, 0x02, 0x3f, 0xd9, 0xce, 0x80, + 0x50, 0xec, 0x51, 0xdb, 0x61, 0x72, 0xa6, 0x9a, 0xa9, 0xed, 0xe8, 0x7b, 0x23, 0xb2, 0xb8, 0xe6, + 0x70, 0x37, 0x40, 0xd1, 0x2b, 0x00, 0xae, 0xb4, 0x26, 0xf6, 0x78, 0x21, 0x6f, 0x71, 0x4d, 0x21, + 0xd0, 0x70, 0x40, 0x29, 0x43, 0xf1, 0xb2, 0xdf, 0xa7, 0x3a, 0xf9, 0x38, 0x25, 0x3e, 0x53, 0x14, + 0x28, 0x85, 0x9f, 0xbe, 0xe7, 0x3a, 0x3e, 0x41, 0x08, 0xb6, 0xad, 0x7e, 0x9f, 0x72, 0xdf, 0x05, + 0x9d, 0xff, 0x56, 0x3e, 0x85, 0xa2, 0x49, 0x2d, 0xc7, 0xb7, 0x7a, 0xcc, 0x76, 0x1d, 0x74, 0x0c, + 0x39, 0x36, 0xc7, 0x43, 0x32, 0xe7, 0xa2, 0x92, 0xbe, 0xc3, 0xe6, 0x37, 0x64, 0xae, 0xbc, 0x85, + 0xfd, 0xee, 0xf4, 0x71, 0x6c, 0xfb, 0xc3, 0xc4, 0xd9, 0x27, 0x50, 0xf6, 0x42, 0x08, 0x13, 0x4a, + 0xdd, 0xd8, 0x6b, 0x29, 0x02, 0xd5, 0x00, 0x53, 0x7e, 0x03, 0x64, 0x10, 0xa7, 0xaf, 0x4d, 0x99, + 0x37, 0x65, 0x7e, 0x14, 0x17, 0x3a, 0x03, 0xf0, 0x2d, 0x86, 0x3d, 0x42, 0xf1, 0x68, 0xc6, 0xed, + 0xb2, 0x7a, 0xde, 0xb7, 0x58, 0x97, 0xd0, 0xd6, 0x0c, 0xd5, 0x60, 0xd7, 0x0d, 0xf5, 0xf2, 0x56, + 0x35, 0x5b, 0x2b, 0x5e, 0xec, 0xd5, 0xa3, 0xfc, 0xd5, 0xcd, 0xb9, 0x36, 0x65, 0x7a, 0x4c, 0x2b, + 0x5f, 0xc1, 0x61, 0xca, 0x7b, 0x14, 0xd9, 0x31, 0xe4, 0xa8, 0x35, 0xc3, 0x2c, 0xb9, 0x03, 0xb5, + 0x66, 0xe6, 0x5c, 0xf9, 0x16, 0x90, 0xea, 0x33, 0x7b, 0x62, 0x31, 0x72, 0x4d, 0x48, 0x1c, 0xcb, + 0x39, 0x14, 0x7b, 0xae, 0xf3, 0x84, 0x99, 0x45, 0x07, 0x24, 0x4e, 0x3b, 0x04, 0x90, 0xc9, 0x11, + 0xe5, 0x0d, 0x1c, 0xa6, 0xcc, 0xa2, 0x43, 0xfe, 0xf5, 0x0e, 0xca, 0x5f, 0x59, 0x28, 0x75, 0x89, + 0xd3, 0xb7, 0x9d, 0x81, 0x31, 0x23, 0xc4, 0x43, 0x5f, 0x42, 0x3e, 0x88, 0xda, 0x8d, 0x4b, 0x5b, + 0xbc, 0xd8, 0xaf, 0x8f, 0xf9, 0x9d, 0xb4, 0x29, 0xeb, 0x06, 0xb0, 0x9e, 0x08, 0xd0, 0xf7, 0x50, + 0x9a, 0xd9, 0xcc, 0x21, 0xbe, 0x8f, 0xd9, 0xc2, 0x23, 0xbc, 0xce, 0x7b, 0x17, 0x27, 0xf5, 0xa4, + 0xb9, 0xea, 0x0f, 0x21, 0x6d, 0x2e, 0x3c, 0xa2, 0xa7, 0xb4, 0xe8, 0x35, 0x80, 0x35, 0x71, 0xa7, + 0x0e, 0xc3, 0xbe, 0xc5, 0xe4, 0x6c, 0x35, 0x53, 0x2b, 0xeb, 0x02, 0x82, 0x14, 0x28, 0xc5, 0x71, + 0x3f, 0x2e, 0x18, 0x91, 0xb7, 0xb9, 0x22, 0x85, 0xa1, 0x3a, 0xa0, 0x47, 0xea, 0x5a, 0xfd, 0x9e, + 0xe5, 0x33, 0x6c, 0x31, 0x46, 0x26, 0x1e, 0xf3, 0xe5, 0x1d, 0xae, 0x5c, 0xc3, 0xa0, 0x6f, 0xe0, + 0xd8, 0x21, 0x73, 0x86, 0x9f, 0xa9, 0x21, 0xb1, 0x07, 0x43, 0x26, 0xe7, 0xb8, 0xc9, 0x7a, 0x32, + 0xb0, 0xa2, 0x61, 0x11, 0x48, 0x1f, 0x8b, 0x35, 0xc8, 0x87, 0x56, 0x6b, 0x49, 0xf4, 0x16, 0x4e, + 0x9e, 0x89, 0xd4, 0x4d, 0x0a, 0xdc, 0x6c, 0x03, 0x8b, 0x8e, 0x60, 0xe7, 0xc9, 0xa5, 0x3d, 0x22, + 0xef, 0x56, 0x33, 0xb5, 0xbc, 0x1e, 0x7e, 0x28, 0x27, 0x70, 0x24, 0x96, 0x29, 0xee, 0x50, 0xe5, + 0x17, 0x38, 0x5e, 0xc2, 0xa3, 0xb2, 0xff, 0x08, 0x7b, 0x5e, 0x48, 0x60, 0x9f, 0x33, 0x72, 0x86, + 0xf7, 0xe8, 0xa9, 0x50, 0x1c, 0xd1, 0x52, 0x5f, 0x92, 0x2b, 0x7f, 0x66, 0x60, 0xef, 0xdd, 0x74, + 0xe2, 0x09, 0x2d, 0xf8, 0x9f, 0x7a, 0xa3, 0x0a, 0xc5, 0x30, 0x13, 0x3c, 0x2b, 0xbc, 0x35, 0xca, + 0xba, 0x08, 0xad, 0x54, 0x38, 0xbb, 0xa6, 0xc2, 0x49, 0x36, 0xb6, 0xc5, 0x6c, 0x1c, 0xc0, 0x7e, + 0x12, 0x5a, 0x78, 0xdf, 0x2f, 0xfe, 0xc8, 0x42, 0x51, 0x68, 0x36, 0x74, 0x08, 0xfb, 0xf7, 0x9d, + 0x56, 0x47, 0x7b, 0xe8, 0xe0, 0x87, 0x5b, 0xb3, 0xa3, 0x1a, 0x86, 0xf4, 0x3f, 0x24, 0xc3, 0x51, + 0x53, 0xbb, 0xbb, 0xbb, 0x35, 0xef, 0xd4, 0x8e, 0x89, 0xcd, 0xdb, 0x3b, 0x15, 0xb7, 0xb5, 0x66, + 0x4b, 0xca, 0xa0, 0x53, 0x38, 0x14, 0x98, 0x8e, 0x86, 0xaf, 0xd4, 0xf6, 0xe5, 0x07, 0x69, 0x0b, + 0x1d, 0xc3, 0x81, 0x40, 0xe8, 0xea, 0x7b, 0xad, 0xa5, 0x4a, 0xd9, 0x40, 0x7f, 0x63, 0xb6, 0x9b, + 0x58, 0xbb, 0xbe, 0x56, 0x75, 0xf5, 0x2a, 0x26, 0xb6, 0x83, 0x23, 0x38, 0x71, 0xd9, 0x6c, 0xaa, + 0x5d, 0xf3, 0x99, 0xd9, 0x41, 0x9f, 0xc1, 0xff, 0x53, 0x26, 0xc1, 0xf1, 0xda, 0xbd, 0x89, 0x0d, + 0xb5, 0xa9, 0x75, 0xae, 0x70, 0x5b, 0x7d, 0xaf, 0xb6, 0xa5, 0x1c, 0xfa, 0x1c, 0x94, 0xb4, 0x03, + 0xe3, 0xbe, 0xd9, 0x54, 0x0d, 0x23, 0xad, 0xdb, 0x45, 0xe7, 0xf0, 0x72, 0x29, 0x82, 0x3b, 0xcd, + 0x54, 0x63, 0xaf, 0x52, 0x1e, 0x55, 0xe1, 0x6c, 0x39, 0x12, 0xae, 0x88, 0xfc, 0x49, 0x05, 0x74, + 0x06, 0x32, 0x57, 0x88, 0x9e, 0xe3, 0x78, 0x01, 0x1d, 0x81, 0x14, 0x65, 0x0e, 0xb7, 0xd4, 0x0f, + 0xf8, 0xe6, 0xd2, 0xb8, 0x91, 0x8a, 0xe8, 0x25, 0x9c, 0x76, 0x54, 0x23, 0x70, 0xb7, 0x42, 0x96, + 0x2e, 0xfe, 0xde, 0x86, 0xc2, 0x03, 0x6f, 0xaf, 0x96, 0x1d, 0x4c, 0x87, 0xf2, 0x15, 0xa1, 0xf6, + 0xef, 0xa4, 0x43, 0xe6, 0xac, 0x45, 0x16, 0xe8, 0x40, 0xe8, 0xbd, 0x70, 0xa3, 0x54, 0x4e, 0x92, + 0x91, 0xd9, 0x22, 0x8b, 0x2b, 0xe2, 0xf7, 0xa8, 0xed, 0x31, 0x97, 0xa2, 0xef, 0xa0, 0x10, 0xda, + 0x06, 0x76, 0x87, 0xa2, 0xa8, 0xed, 0xf6, 0x2c, 0xe6, 0xd2, 0x8d, 0x96, 0x3f, 0x40, 0x3e, 0x38, + 0x2f, 0xd8, 0x27, 0x48, 0x9c, 0x44, 0xc2, 0xbe, 0xa9, 0x9c, 0xae, 0xe0, 0xd1, 0xab, 0xb9, 0x01, + 0x14, 0xad, 0x0f, 0x71, 0xd7, 0x88, 0x6e, 0x04, 0xbc, 0x52, 0x11, 0xdf, 0xd2, 0xd2, 0xd6, 0x69, + 0x43, 0x51, 0x18, 0xf9, 0xe8, 0x95, 0x20, 0x5d, 0x5d, 0x34, 0x95, 0xd7, 0x9b, 0xe8, 0x67, 0x6f, + 0xc2, 0x6c, 0x4f, 0x79, 0x5b, 0x5d, 0x15, 0x29, 0x6f, 0xeb, 0x56, 0x82, 0x0e, 0xe5, 0xd4, 0xd0, + 0x40, 0xe7, 0x1b, 0x86, 0x42, 0x12, 0x5f, 0x75, 0xb3, 0x20, 0xf2, 0xf9, 0x13, 0xec, 0x46, 0x4f, + 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. diff --git a/lnrpc/walletrpc/walletkit.proto b/lnrpc/walletrpc/walletkit.proto index 96441025b2..446e79fe32 100644 --- a/lnrpc/walletrpc/walletkit.proto +++ b/lnrpc/walletrpc/walletkit.proto @@ -191,6 +191,18 @@ message PendingSweep { sweep transaction of the output. */ 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 { @@ -215,6 +227,12 @@ message BumpFeeRequest { with. */ 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 { diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index 53a25e7bca..42534c653e 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -404,6 +404,9 @@ func (w *WalletKit) PendingSweeps(ctx context.Context, broadcastAttempts := uint32(pendingInput.BroadcastAttempts) nextBroadcastHeight := uint32(pendingInput.NextBroadcastHeight) + requestedFee := pendingInput.Params.Fee + requestedFeeRate := uint32(requestedFee.FeeRate.FeePerKVByte() / 1000) + rpcPendingSweeps = append(rpcPendingSweeps, &PendingSweep{ Outpoint: op, WitnessType: witnessType, @@ -411,6 +414,9 @@ func (w *WalletKit) PendingSweeps(ctx context.Context, SatPerByte: satPerByte, BroadcastAttempts: broadcastAttempts, 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) // being broadcast. If it is not aware of the input however, // 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 { case nil: return &BumpFeeResponse{}, nil diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 9114cf8a45..fc65879f66 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -67,6 +67,15 @@ type Params struct { // 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. 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 @@ -146,19 +155,22 @@ type PendingInput struct { // NextBroadcastHeight is the next height of the chain at which we'll // attempt to broadcast a transaction sweeping the input. 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 -// intent to bump the fee rate of a given input. -type bumpFeeReq struct { - input wire.OutPoint - feePreference FeePreference - responseChan chan *bumpFeeResp +// updateReq is an internal message we'll use to represent an external caller's +// intent to update the sweep parameters of a given input. +type updateReq struct { + input wire.OutPoint + params Params + responseChan chan *updateResp } -// bumpFeeResp 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. -type bumpFeeResp struct { +// updateResp is an internal message we'll use to hand off the response of a +// updateReq from the UtxoSweeper's main event loop back to the caller. +type updateResp struct { resultChan chan Result err error } @@ -178,9 +190,9 @@ type UtxoSweeper struct { // UtxoSweeper is attempting to sweep. 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. - bumpFeeReqs chan *bumpFeeReq + updateReqs chan *updateReq // pendingInputs is the total set of inputs the UtxoSweeper has been // requested to sweep. @@ -287,7 +299,7 @@ func New(cfg *UtxoSweeperConfig) *UtxoSweeper { cfg: cfg, newInputs: make(chan *sweepInputMessage), spendChan: make(chan *chainntnfs.SpendDetail), - bumpFeeReqs: make(chan *bumpFeeReq), + updateReqs: make(chan *updateReq), pendingSweepsReqs: make(chan *pendingSweepsReq), quit: make(chan struct{}), 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, "+ - "time_lock=%v, amount=%v, fee_preference=%v", input.OutPoint(), - input.WitnessType(), input.BlocksToMaturity(), + "time_lock=%v, amount=%v, fee_preference=%v, force=%v", + input.OutPoint(), input.WitnessType(), input.BlocksToMaturity(), btcutil.Amount(input.SignDesc().Output.Value), - params.Fee) + params.Fee, params.Force) sweeperInput := &sweepInputMessage{ 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 // of a given input. - case req := <-s.bumpFeeReqs: - resultChan, err := s.handleBumpFeeReq(req, bestHeight) - req.responseChan <- &bumpFeeResp{ + case req := <-s.updateReqs: + resultChan, err := s.handleUpdateReq(req, bestHeight) + req.responseChan <- &updateResp{ resultChan: resultChan, err: err, } @@ -1034,34 +1046,35 @@ func (s *UtxoSweeper) handlePendingSweepsReq( LastFeeRate: pendingInput.lastFeeRate, BroadcastAttempts: pendingInput.publishAttempts, NextBroadcastHeight: uint32(pendingInput.minPublishHeight), + Params: pendingInput.params, } } return pendingInputs } -// BumpFee allows bumping the fee of an input being swept by the UtxoSweeper -// according to the provided fee preference. The new fee preference will be used -// for a new sweep transaction of the input that will act as a replacement -// transaction (RBF) of the original sweeping transaction, if any. +// UpdateParams allows updating the sweep parameters of a pending input in the +// UtxoSweeper. This function can be used to provide an updated fee preference +// that will be used for a new sweep transaction of the input that will act as a +// 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 // is actually successful. The responsibility of doing so should be handled by // the caller. -func (s *UtxoSweeper) BumpFee(input wire.OutPoint, - feePreference FeePreference) (chan Result, error) { +func (s *UtxoSweeper) UpdateParams(input wire.OutPoint, + params Params) (chan Result, error) { // 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 } - responseChan := make(chan *bumpFeeResp, 1) + responseChan := make(chan *updateResp, 1) select { - case s.bumpFeeReqs <- &bumpFeeReq{ - input: input, - feePreference: feePreference, - responseChan: responseChan, + case s.updateReqs <- &updateReq{ + input: input, + params: params, + responseChan: responseChan, }: case <-s.quit: return nil, ErrSweeperShuttingDown @@ -1075,9 +1088,9 @@ func (s *UtxoSweeper) BumpFee(input wire.OutPoint, } } -// handleBumpFeeReq handles a bump fee request by simply updating the inputs fee -// preference. Currently, no validation is done on the new fee preference to -// ensure it will properly create a replacement transaction. +// handleUpdateReq handles an update request by simply updating the sweep +// parameters of the pending input. Currently, no validation is done on the new +// fee preference to ensure it will properly create a replacement transaction. // // TODO(wilmer): // * 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 // did not exist in the original sweep transaction, resulting in an invalid // replacement transaction. -func (s *UtxoSweeper) handleBumpFeeReq(req *bumpFeeReq, - bestHeight int32) (chan Result, error) { +func (s *UtxoSweeper) handleUpdateReq(req *updateReq, bestHeight int32) ( + chan Result, error) { // 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 @@ -1099,10 +1112,10 @@ func (s *UtxoSweeper) handleBumpFeeReq(req *bumpFeeReq, return nil, lnwallet.ErrNotMine } - log.Debugf("Updating fee preference for %v from %v to %v", req.input, - pendingInput.params.Fee, req.feePreference) + log.Debugf("Updating sweep parameters for %v from %v to %v", req.input, + 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 // transaction can be created that replaces the transaction currently diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index b63129c9a7..a663c3ab51 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -1178,7 +1178,9 @@ func TestBumpFeeRBF(t *testing.T) { // 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. - bumpResult, err := ctx.sweeper.BumpFee(wire.OutPoint{}, lowFeePref) + _, err := ctx.sweeper.UpdateParams( + wire.OutPoint{}, Params{Fee: lowFeePref}, + ) if err != lnwallet.ErrNotMine { t.Fatalf("expected error lnwallet.ErrNotMine, got \"%v\"", err) } @@ -1206,12 +1208,14 @@ func TestBumpFeeRBF(t *testing.T) { ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate // 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 { 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 { t.Fatalf("unable to bump input's fee: %v", err) } diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go index a00ac6ca58..1f21f9602c 100644 --- a/sweep/tx_input_set.go +++ b/sweep/tx_input_set.go @@ -13,6 +13,23 @@ import ( "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 // on various properties of the tx. type txInputSet struct { @@ -45,6 +62,10 @@ type txInputSet struct { // wallet contains wallet functionality required by the input set to // retrieve utxos. 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. @@ -78,10 +99,12 @@ func (t *txInputSet) dustLimitReached() bool { // 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 // 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, // 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 } @@ -108,23 +131,42 @@ func (t *txInputSet) add(input input.Input, fromWallet bool) bool { // added to the set. newOutputValue := newInputTotal - fee - // If adding this input makes the total output value of the set - // decrease, this is a negative yield input. We don't add the input to - // the set and return the outcome. - if newOutputValue <= t.outputValue { - return false - } + // Initialize new wallet total with the current wallet total. This is + // updated below if this input is a wallet input. + newWalletTotal := t.walletInputTotal + + // 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 + } - // 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 // 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 // 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 // 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 // transaction. To prevent attaching and locking a big utxo for // very little benefit. - if newWalletTotal >= newOutputValue { + if !t.force && newWalletTotal >= newOutputValue { log.Debugf("Rejecting wallet input of %v, because it "+ "would make a negative yielding transaction "+ "(%v)", @@ -142,17 +184,16 @@ func (t *txInputSet) add(input input.Input, fromWallet bool) bool { 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. + // + // TODO: Return new instance? t.inputTotal = newInputTotal t.outputValue = newOutputValue t.inputs = append(t.inputs, input) t.weightEstimate = newWeightEstimate + t.walletInputTotal = newWalletTotal return true } @@ -167,11 +208,17 @@ func (t *txInputSet) add(input input.Input, fromWallet bool) bool { // whole. func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) { 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 // succeed because it wouldn't increase the output value, // return. Assuming inputs are sorted by yield, any further // inputs wouldn't increase the output value either. - if !t.add(input, false) { + if !t.add(input, constraints) { return } } @@ -202,7 +249,7 @@ func (t *txInputSet) tryAddWalletInputsIfNeeded() error { // If the wallet input isn't positively-yielding at this fee // rate, skip it. - if !t.add(input, true) { + if !t.add(input, constraintsWallet) { continue } diff --git a/sweep/tx_input_set_test.go b/sweep/tx_input_set_test.go index 2fa5018836..d9e98f7330 100644 --- a/sweep/tx_input_set_test.go +++ b/sweep/tx_input_set_test.go @@ -24,13 +24,13 @@ func TestTxInputSet(t *testing.T) { // 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 // 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") } // A 700 sat input should be accepted into the set, because it yields // positively. - if !set.add(createP2WKHInput(700), false) { + if !set.add(createP2WKHInput(700), constraintsRegular) { 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 // 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") } 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 // 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") } if set.dustLimitReached() { 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() if err != nil { t.Fatal(err) diff --git a/sweep/txgenerator.go b/sweep/txgenerator.go index 47c8d3c652..229ecf3eb4 100644 --- a/sweep/txgenerator.go +++ b/sweep/txgenerator.go @@ -67,6 +67,14 @@ func generateInputPartitionings(sweepableInputs []txInput, } 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()] > yields[*sweepableInputs[j].OutPoint()] })