Merge pull request #3140 from wpaulino/sweeper-bumpfee

sweep+rpc: add support to bump fee of inputs/transactions
This commit is contained in:
Wilmer Paulino 2019-06-12 12:21:53 -07:00 committed by GitHub
commit 5af4022b6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 889 additions and 79 deletions

@ -1,7 +1,11 @@
package main
import (
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/lnrpc"
@ -17,6 +21,27 @@ func NewOutPointFromProto(op *lnrpc.OutPoint) OutPoint {
return OutPoint(fmt.Sprintf("%v:%d", hash, op.OutputIndex))
}
// NewProtoOutPoint parses an OutPoint into its corresponding lnrpc.OutPoint
// type.
func NewProtoOutPoint(op string) (*lnrpc.OutPoint, error) {
parts := strings.Split(op, ":")
if len(parts) != 2 {
return nil, errors.New("outpoint should be of the form txid:index")
}
txid := parts[0]
if hex.DecodedLen(len(txid)) != chainhash.HashSize {
return nil, fmt.Errorf("invalid hex-encoded txid %v", txid)
}
outputIndex, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid output index: %v", err)
}
return &lnrpc.OutPoint{
TxidStr: txid,
OutputIndex: uint32(outputIndex),
}, nil
}
// Utxo displays information about an unspent output, including its address,
// amount, pkscript, and confirmations.
type Utxo struct {

@ -4,6 +4,7 @@ package main
import (
"context"
"fmt"
"sort"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
@ -21,6 +22,7 @@ func walletCommands() []cli.Command {
Description: "",
Subcommands: []cli.Command{
pendingSweepsCommand,
bumpFeeCommand,
},
},
}
@ -81,3 +83,89 @@ func pendingSweeps(ctx *cli.Context) error {
return nil
}
var bumpFeeCommand = cli.Command{
Name: "bumpfee",
Usage: "Bumps the fee of an arbitrary input/transaction.",
ArgsUsage: "outpoint",
Description: `
This command takes a different approach than bitcoind's bumpfee command.
lnd has a central batching engine in which inputs with similar fee rates
are batched together to save on transaction fees. Due to this, we cannot
rely on bumping the fee on a specific transaction, since transactions
can change at any point with the addition of new inputs. The list of
inputs that currently exist within lnd's central batching engine can be
retrieved through lncli pendingsweeps.
When bumping the fee of an input that currently exists within lnd's
central batching engine, a higher fee transaction will be created that
replaces the lower fee transaction through the Replace-By-Fee (RBF)
policy.
This command also serves useful when wanting to perform a
Child-Pays-For-Parent (CPFP), where the child transaction pays for its
parent's fee. This can be done by specifying an outpoint within the low
fee transaction that is under the control of the wallet.
A fee preference must be provided, either through the conf_target or
sat_per_byte parameters.
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.`,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "conf_target",
Usage: "the number of blocks that the output should " +
"be swept on-chain within",
},
cli.Uint64Flag{
Name: "sat_per_byte",
Usage: "a manual fee expressed in sat/byte that " +
"should be used when sweeping the output",
},
},
Action: actionDecorator(bumpFee),
}
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 {
return cli.ShowCommandHelp(ctx, "bumpfee")
}
// Validate and parse the relevant arguments/flags.
protoOutPoint, err := NewProtoOutPoint(ctx.Args().Get(0))
if err != nil {
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,
})
if err != nil {
return err
}
printRespJSON(resp)
return nil
}

@ -676,6 +676,97 @@ func (m *PendingSweepsResponse) GetPendingSweeps() []*PendingSweep {
return nil
}
type BumpFeeRequest struct {
// The input we're attempting to bump the fee of.
Outpoint *lnrpc.OutPoint `protobuf:"bytes,1,opt,name=outpoint,proto3" json:"outpoint,omitempty"`
// The target number of blocks that the input should be spent within.
TargetConf uint32 `protobuf:"varint,2,opt,name=target_conf,proto3" json:"target_conf,omitempty"`
//
//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"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *BumpFeeRequest) Reset() { *m = BumpFeeRequest{} }
func (m *BumpFeeRequest) String() string { return proto.CompactTextString(m) }
func (*BumpFeeRequest) ProtoMessage() {}
func (*BumpFeeRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_6cc6942ac78249e5, []int{12}
}
func (m *BumpFeeRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_BumpFeeRequest.Unmarshal(m, b)
}
func (m *BumpFeeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_BumpFeeRequest.Marshal(b, m, deterministic)
}
func (m *BumpFeeRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_BumpFeeRequest.Merge(m, src)
}
func (m *BumpFeeRequest) XXX_Size() int {
return xxx_messageInfo_BumpFeeRequest.Size(m)
}
func (m *BumpFeeRequest) XXX_DiscardUnknown() {
xxx_messageInfo_BumpFeeRequest.DiscardUnknown(m)
}
var xxx_messageInfo_BumpFeeRequest proto.InternalMessageInfo
func (m *BumpFeeRequest) GetOutpoint() *lnrpc.OutPoint {
if m != nil {
return m.Outpoint
}
return nil
}
func (m *BumpFeeRequest) GetTargetConf() uint32 {
if m != nil {
return m.TargetConf
}
return 0
}
func (m *BumpFeeRequest) GetSatPerByte() uint32 {
if m != nil {
return m.SatPerByte
}
return 0
}
type BumpFeeResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *BumpFeeResponse) Reset() { *m = BumpFeeResponse{} }
func (m *BumpFeeResponse) String() string { return proto.CompactTextString(m) }
func (*BumpFeeResponse) ProtoMessage() {}
func (*BumpFeeResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_6cc6942ac78249e5, []int{13}
}
func (m *BumpFeeResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_BumpFeeResponse.Unmarshal(m, b)
}
func (m *BumpFeeResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_BumpFeeResponse.Marshal(b, m, deterministic)
}
func (m *BumpFeeResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_BumpFeeResponse.Merge(m, src)
}
func (m *BumpFeeResponse) XXX_Size() int {
return xxx_messageInfo_BumpFeeResponse.Size(m)
}
func (m *BumpFeeResponse) XXX_DiscardUnknown() {
xxx_messageInfo_BumpFeeResponse.DiscardUnknown(m)
}
var xxx_messageInfo_BumpFeeResponse proto.InternalMessageInfo
func init() {
proto.RegisterEnum("walletrpc.WitnessType", WitnessType_name, WitnessType_value)
proto.RegisterType((*KeyReq)(nil), "walletrpc.KeyReq")
@ -690,70 +781,75 @@ func init() {
proto.RegisterType((*PendingSweep)(nil), "walletrpc.PendingSweep")
proto.RegisterType((*PendingSweepsRequest)(nil), "walletrpc.PendingSweepsRequest")
proto.RegisterType((*PendingSweepsResponse)(nil), "walletrpc.PendingSweepsResponse")
proto.RegisterType((*BumpFeeRequest)(nil), "walletrpc.BumpFeeRequest")
proto.RegisterType((*BumpFeeResponse)(nil), "walletrpc.BumpFeeResponse")
}
func init() { proto.RegisterFile("walletrpc/walletkit.proto", fileDescriptor_6cc6942ac78249e5) }
var fileDescriptor_6cc6942ac78249e5 = []byte{
// 918 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x55, 0x5d, 0x6f, 0xe2, 0x46,
0x14, 0x2d, 0x21, 0x61, 0xc3, 0x05, 0x12, 0xef, 0x10, 0x12, 0x97, 0xcd, 0x6e, 0xa8, 0xfb, 0x21,
0xd4, 0x56, 0xa0, 0x66, 0xdb, 0xaa, 0x6a, 0x1f, 0xaa, 0x14, 0x1c, 0x11, 0xf1, 0x61, 0x6a, 0x3b,
0x9b, 0x6e, 0x55, 0x69, 0x64, 0x60, 0x16, 0x2c, 0xc0, 0x76, 0xc6, 0x43, 0xc1, 0xaf, 0xfd, 0x27,
0xfd, 0x97, 0x7d, 0xac, 0x3c, 0xb6, 0xc9, 0x98, 0x24, 0xfb, 0x14, 0xe7, 0x9c, 0x73, 0xcf, 0xdc,
0xb9, 0x33, 0x73, 0x80, 0x4f, 0xd7, 0xd6, 0x62, 0x41, 0x18, 0xf5, 0xc6, 0xcd, 0xe8, 0x6b, 0x6e,
0xb3, 0x86, 0x47, 0x5d, 0xe6, 0xa2, 0xfc, 0x96, 0xaa, 0xe6, 0xa9, 0x37, 0x8e, 0xd0, 0xea, 0x89,
0x6f, 0x4f, 0x9d, 0x50, 0x1e, 0xfe, 0x25, 0x34, 0x42, 0x95, 0xdf, 0x21, 0xd7, 0x25, 0x81, 0x4e,
0xee, 0x51, 0x1d, 0xa4, 0x39, 0x09, 0xf0, 0x07, 0xdb, 0x99, 0x12, 0x8a, 0x3d, 0x6a, 0x3b, 0x4c,
0xce, 0xd4, 0x32, 0xf5, 0x03, 0xfd, 0x68, 0x4e, 0x82, 0x6b, 0x0e, 0x0f, 0x43, 0x14, 0xbd, 0x06,
0xe0, 0x4a, 0x6b, 0x69, 0x2f, 0x02, 0x79, 0x8f, 0x6b, 0xf2, 0xa1, 0x86, 0x03, 0x4a, 0x09, 0x0a,
0x57, 0x93, 0x09, 0xd5, 0xc9, 0xfd, 0x8a, 0xf8, 0x4c, 0x51, 0xa0, 0x18, 0xfd, 0xeb, 0x7b, 0xae,
0xe3, 0x13, 0x84, 0x60, 0xdf, 0x9a, 0x4c, 0x28, 0xf7, 0xce, 0xeb, 0xfc, 0x5b, 0xf9, 0x02, 0x0a,
0x26, 0xb5, 0x1c, 0xdf, 0x1a, 0x33, 0xdb, 0x75, 0x50, 0x05, 0x72, 0x6c, 0x83, 0x67, 0x64, 0xc3,
0x45, 0x45, 0xfd, 0x80, 0x6d, 0x3a, 0x64, 0xa3, 0xfc, 0x08, 0xc7, 0xc3, 0xd5, 0x68, 0x61, 0xfb,
0xb3, 0xad, 0xd9, 0xe7, 0x50, 0xf2, 0x22, 0x08, 0x13, 0x4a, 0xdd, 0xc4, 0xb5, 0x18, 0x83, 0x6a,
0x88, 0x29, 0x7f, 0x01, 0x32, 0x88, 0x33, 0xd1, 0x56, 0xcc, 0x5b, 0x31, 0x3f, 0xee, 0x0b, 0x9d,
0x03, 0xf8, 0x16, 0xc3, 0x1e, 0xa1, 0x78, 0xbe, 0xe6, 0x75, 0x59, 0xfd, 0xd0, 0xb7, 0xd8, 0x90,
0xd0, 0xee, 0x1a, 0xd5, 0xe1, 0x85, 0x1b, 0xe9, 0xe5, 0xbd, 0x5a, 0xb6, 0x5e, 0xb8, 0x3c, 0x6a,
0xc4, 0xf3, 0x6b, 0x98, 0x1b, 0x6d, 0xc5, 0xf4, 0x84, 0x56, 0xbe, 0x85, 0x72, 0xca, 0x3d, 0xee,
0xac, 0x02, 0x39, 0x6a, 0xad, 0x31, 0xdb, 0xee, 0x81, 0x5a, 0x6b, 0x73, 0xa3, 0xfc, 0x00, 0x48,
0xf5, 0x99, 0xbd, 0xb4, 0x18, 0xb9, 0x26, 0x24, 0xe9, 0xe5, 0x02, 0x0a, 0x63, 0xd7, 0xf9, 0x80,
0x99, 0x45, 0xa7, 0x24, 0x19, 0x3b, 0x84, 0x90, 0xc9, 0x11, 0xe5, 0x2d, 0x94, 0x53, 0x65, 0xf1,
0x22, 0x1f, 0xdd, 0x83, 0xf2, 0xef, 0x1e, 0x14, 0x87, 0xc4, 0x99, 0xd8, 0xce, 0xd4, 0x58, 0x13,
0xe2, 0xa1, 0x6f, 0xe0, 0x30, 0xec, 0xda, 0x4d, 0x8e, 0xb6, 0x70, 0x79, 0xdc, 0x58, 0xf0, 0x3d,
0x69, 0x2b, 0x36, 0x0c, 0x61, 0x7d, 0x2b, 0x40, 0x3f, 0x43, 0x71, 0x6d, 0x33, 0x87, 0xf8, 0x3e,
0x66, 0x81, 0x47, 0xf8, 0x39, 0x1f, 0x5d, 0x9e, 0x36, 0xb6, 0x97, 0xab, 0x71, 0x17, 0xd1, 0x66,
0xe0, 0x11, 0x3d, 0xa5, 0x45, 0x6f, 0x00, 0xac, 0xa5, 0xbb, 0x72, 0x18, 0xf6, 0x2d, 0x26, 0x67,
0x6b, 0x99, 0x7a, 0x49, 0x17, 0x10, 0xa4, 0x40, 0x31, 0xe9, 0x7b, 0x14, 0x30, 0x22, 0xef, 0x73,
0x45, 0x0a, 0x43, 0x0d, 0x40, 0x23, 0xea, 0x5a, 0x93, 0xb1, 0xe5, 0x33, 0x6c, 0x31, 0x46, 0x96,
0x1e, 0xf3, 0xe5, 0x03, 0xae, 0x7c, 0x82, 0x41, 0xdf, 0x43, 0xc5, 0x21, 0x1b, 0x86, 0x1f, 0xa8,
0x19, 0xb1, 0xa7, 0x33, 0x26, 0xe7, 0x78, 0xc9, 0xd3, 0xa4, 0x72, 0x0a, 0x27, 0xe2, 0x88, 0x92,
0xdb, 0xa1, 0xfc, 0x01, 0x95, 0x1d, 0x3c, 0x1e, 0xf9, 0xaf, 0x70, 0xe4, 0x45, 0x04, 0xf6, 0x39,
0x23, 0x67, 0xf8, 0xfd, 0x38, 0x13, 0x06, 0x23, 0x56, 0xea, 0x3b, 0xf2, 0xaf, 0xff, 0xc9, 0x42,
0x41, 0x98, 0x1c, 0x2a, 0xc3, 0xf1, 0xed, 0xa0, 0x3b, 0xd0, 0xee, 0x06, 0xf8, 0xee, 0xc6, 0x1c,
0xa8, 0x86, 0x21, 0x7d, 0x82, 0x64, 0x38, 0x69, 0x69, 0xfd, 0xfe, 0x8d, 0xd9, 0x57, 0x07, 0x26,
0x36, 0x6f, 0xfa, 0x2a, 0xee, 0x69, 0xad, 0xae, 0x94, 0x41, 0x67, 0x50, 0x16, 0x98, 0x81, 0x86,
0xdb, 0x6a, 0xef, 0xea, 0xbd, 0xb4, 0x87, 0x2a, 0xf0, 0x52, 0x20, 0x74, 0xf5, 0x9d, 0xd6, 0x55,
0xa5, 0x6c, 0xa8, 0xef, 0x98, 0xbd, 0x16, 0xd6, 0xae, 0xaf, 0x55, 0x5d, 0x6d, 0x27, 0xc4, 0x7e,
0xb8, 0x04, 0x27, 0xae, 0x5a, 0x2d, 0x75, 0x68, 0x3e, 0x30, 0x07, 0xe8, 0x4b, 0xf8, 0x2c, 0x55,
0x12, 0x2e, 0xaf, 0xdd, 0x9a, 0xd8, 0x50, 0x5b, 0xda, 0xa0, 0x8d, 0x7b, 0xea, 0x3b, 0xb5, 0x27,
0xe5, 0xd0, 0x57, 0xa0, 0xa4, 0x0d, 0x8c, 0xdb, 0x56, 0x4b, 0x35, 0x8c, 0xb4, 0xee, 0x05, 0xba,
0x80, 0x57, 0x3b, 0x1d, 0xf4, 0x35, 0x53, 0x4d, 0x5c, 0xa5, 0x43, 0x54, 0x83, 0xf3, 0xdd, 0x4e,
0xb8, 0x22, 0xf6, 0x93, 0xf2, 0xe8, 0x1c, 0x64, 0xae, 0x10, 0x9d, 0x93, 0x7e, 0x01, 0x9d, 0x80,
0x14, 0x4f, 0x0e, 0x77, 0xd5, 0xf7, 0xb8, 0x73, 0x65, 0x74, 0xa4, 0x02, 0x7a, 0x05, 0x67, 0x03,
0xd5, 0x08, 0xed, 0x1e, 0x91, 0xc5, 0xcb, 0xff, 0xb2, 0x90, 0xbf, 0xe3, 0xe7, 0xd5, 0xb5, 0xc3,
0xab, 0x5e, 0x6a, 0x13, 0x6a, 0xff, 0x4d, 0x06, 0x64, 0xc3, 0xba, 0x24, 0x40, 0x2f, 0x85, 0xc3,
0x8c, 0xe2, 0xb1, 0x7a, 0xba, 0x7d, 0xff, 0x5d, 0x12, 0xb4, 0x89, 0x3f, 0xa6, 0xb6, 0xc7, 0x5c,
0x8a, 0x7e, 0x82, 0x7c, 0x54, 0x1b, 0xd6, 0x95, 0x45, 0x51, 0xcf, 0x1d, 0x5b, 0xcc, 0xa5, 0xcf,
0x56, 0xfe, 0x02, 0x87, 0xe1, 0x7a, 0x61, 0x38, 0x22, 0xf1, 0x59, 0x09, 0xe1, 0x59, 0x3d, 0x7b,
0x84, 0xc7, 0xd7, 0xb0, 0x03, 0x28, 0xce, 0x42, 0x31, 0x38, 0x45, 0x1b, 0x01, 0xaf, 0x56, 0xc5,
0xcb, 0xb9, 0x13, 0xa1, 0x3d, 0x28, 0x08, 0xf9, 0x85, 0x5e, 0x0b, 0xd2, 0xc7, 0xa9, 0x59, 0x7d,
0xf3, 0x1c, 0xfd, 0xe0, 0x26, 0x04, 0x55, 0xca, 0xed, 0x71, 0xee, 0xa5, 0xdc, 0x9e, 0xca, 0x37,
0x1d, 0x4a, 0xa9, 0x57, 0x88, 0x2e, 0x9e, 0x79, 0x65, 0xdb, 0xfe, 0x6a, 0xcf, 0x0b, 0x22, 0xcf,
0xdf, 0xbe, 0xfb, 0xb3, 0x39, 0xb5, 0xd9, 0x6c, 0x35, 0x6a, 0x8c, 0xdd, 0x65, 0x73, 0x11, 0xa6,
0x80, 0x63, 0x3b, 0x53, 0x87, 0xb0, 0xb5, 0x4b, 0xe7, 0xcd, 0x85, 0x33, 0x69, 0xf2, 0x4c, 0x6c,
0x6e, 0x8d, 0x46, 0x39, 0xfe, 0x5b, 0xf9, 0xf6, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbf, 0xc4,
0xea, 0x93, 0x74, 0x07, 0x00, 0x00,
// 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,
}
// Reference imports to suppress errors if they are not otherwise used.
@ -806,6 +902,33 @@ type WalletKitClient interface {
//remain supported. This is an advanced API that depends on the internals of
//the UtxoSweeper, so things may change.
PendingSweeps(ctx context.Context, in *PendingSweepsRequest, opts ...grpc.CallOption) (*PendingSweepsResponse, error)
//
//Bump the fee of an arbitrary input within a transaction. This RPC takes a
//different approach than bitcoind's bumpfee command. lnd has a central
//batching engine in which inputs with similar fee rates are batched together
//to save on transaction fees. Due to this, we cannot rely on bumping the fee
//on a specific transaction, since transactions can change at any point with
//the addition of new inputs. The list of inputs that currently exist within
//lnd's central batching engine can be retrieved through the PendingSweeps
//RPC.
//
//When bumping the fee of an input that currently exists within lnd's central
//batching engine, a higher fee transaction will be created that replaces the
//lower fee transaction through the Replace-By-Fee (RBF) policy. If it
//
//This RPC also serves useful when wanting to perform a Child-Pays-For-Parent
//(CPFP), where the child transaction pays for its parent's fee. This can be
//done by specifying an outpoint within the low fee transaction that is under
//the control of the wallet.
//
//The fee preference can be expressed either as a specific fee rate or a delta
//of blocks in which the output should be swept on-chain within. If a fee
//preference is not explicitly specified, then an error is returned.
//
//Note that this RPC 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.
BumpFee(ctx context.Context, in *BumpFeeRequest, opts ...grpc.CallOption) (*BumpFeeResponse, error)
}
type walletKitClient struct {
@ -879,6 +1002,15 @@ func (c *walletKitClient) PendingSweeps(ctx context.Context, in *PendingSweepsRe
return out, nil
}
func (c *walletKitClient) BumpFee(ctx context.Context, in *BumpFeeRequest, opts ...grpc.CallOption) (*BumpFeeResponse, error) {
out := new(BumpFeeResponse)
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/BumpFee", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// WalletKitServer is the server API for WalletKit service.
type WalletKitServer interface {
//*
@ -919,6 +1051,33 @@ type WalletKitServer interface {
//remain supported. This is an advanced API that depends on the internals of
//the UtxoSweeper, so things may change.
PendingSweeps(context.Context, *PendingSweepsRequest) (*PendingSweepsResponse, error)
//
//Bump the fee of an arbitrary input within a transaction. This RPC takes a
//different approach than bitcoind's bumpfee command. lnd has a central
//batching engine in which inputs with similar fee rates are batched together
//to save on transaction fees. Due to this, we cannot rely on bumping the fee
//on a specific transaction, since transactions can change at any point with
//the addition of new inputs. The list of inputs that currently exist within
//lnd's central batching engine can be retrieved through the PendingSweeps
//RPC.
//
//When bumping the fee of an input that currently exists within lnd's central
//batching engine, a higher fee transaction will be created that replaces the
//lower fee transaction through the Replace-By-Fee (RBF) policy. If it
//
//This RPC also serves useful when wanting to perform a Child-Pays-For-Parent
//(CPFP), where the child transaction pays for its parent's fee. This can be
//done by specifying an outpoint within the low fee transaction that is under
//the control of the wallet.
//
//The fee preference can be expressed either as a specific fee rate or a delta
//of blocks in which the output should be swept on-chain within. If a fee
//preference is not explicitly specified, then an error is returned.
//
//Note that this RPC 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.
BumpFee(context.Context, *BumpFeeRequest) (*BumpFeeResponse, error)
}
func RegisterWalletKitServer(s *grpc.Server, srv WalletKitServer) {
@ -1051,6 +1210,24 @@ func _WalletKit_PendingSweeps_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _WalletKit_BumpFee_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BumpFeeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletKitServer).BumpFee(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/walletrpc.WalletKit/BumpFee",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletKitServer).BumpFee(ctx, req.(*BumpFeeRequest))
}
return interceptor(ctx, in, info, handler)
}
var _WalletKit_serviceDesc = grpc.ServiceDesc{
ServiceName: "walletrpc.WalletKit",
HandlerType: (*WalletKitServer)(nil),
@ -1083,6 +1260,10 @@ var _WalletKit_serviceDesc = grpc.ServiceDesc{
MethodName: "PendingSweeps",
Handler: _WalletKit_PendingSweeps_Handler,
},
{
MethodName: "BumpFee",
Handler: _WalletKit_BumpFee_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "walletrpc/walletkit.proto",

@ -203,6 +203,23 @@ message PendingSweepsResponse {
repeated PendingSweep pending_sweeps = 1 [json_name = "pending_sweeps"];
}
message BumpFeeRequest {
// The input we're attempting to bump the fee of.
lnrpc.OutPoint outpoint = 1 [json_name = "outpoint"];
// The target number of blocks that the input should be spent within.
uint32 target_conf = 2 [json_name = "target_conf"];
/*
The fee rate, expressed in sat/byte, that should be used to spend the input
with.
*/
uint32 sat_per_byte = 3 [json_name = "sat_per_byte"];
}
message BumpFeeResponse {
}
service WalletKit {
/**
DeriveNextKey attempts to derive the *next* key within the key family
@ -255,4 +272,33 @@ service WalletKit {
the UtxoSweeper, so things may change.
*/
rpc PendingSweeps(PendingSweepsRequest) returns (PendingSweepsResponse);
/*
Bump the fee of an arbitrary input within a transaction. This RPC takes a
different approach than bitcoind's bumpfee command. lnd has a central
batching engine in which inputs with similar fee rates are batched together
to save on transaction fees. Due to this, we cannot rely on bumping the fee
on a specific transaction, since transactions can change at any point with
the addition of new inputs. The list of inputs that currently exist within
lnd's central batching engine can be retrieved through the PendingSweeps
RPC.
When bumping the fee of an input that currently exists within lnd's central
batching engine, a higher fee transaction will be created that replaces the
lower fee transaction through the Replace-By-Fee (RBF) policy. If it
This RPC also serves useful when wanting to perform a Child-Pays-For-Parent
(CPFP), where the child transaction pays for its parent's fee. This can be
done by specifying an outpoint within the low fee transaction that is under
the control of the wallet.
The fee preference can be expressed either as a specific fee rate or a delta
of blocks in which the output should be swept on-chain within. If a fee
preference is not explicitly specified, then an error is returned.
Note that this RPC 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.
*/
rpc BumpFee(BumpFeeRequest) returns (BumpFeeResponse);
}

@ -9,12 +9,15 @@ import (
"os"
"path/filepath"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/sweep"
"golang.org/x/net/context"
"google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery"
@ -79,6 +82,10 @@ var (
Entity: "onchain",
Action: "read",
}},
"/walletrpc.WalletKit/BumpFee": {{
Entity: "onchain",
Action: "write",
}},
}
// DefaultWalletKitMacFilename is the default name of the wallet kit
@ -409,3 +416,106 @@ func (w *WalletKit) PendingSweeps(ctx context.Context,
PendingSweeps: rpcPendingSweeps,
}, nil
}
// unmarshallOutPoint converts an outpoint from its lnrpc type to its canonical
// type.
func unmarshallOutPoint(op *lnrpc.OutPoint) (*wire.OutPoint, error) {
var hash chainhash.Hash
switch {
case len(op.TxidBytes) == 0 && len(op.TxidStr) == 0:
fallthrough
case len(op.TxidBytes) != 0 && len(op.TxidStr) != 0:
return nil, fmt.Errorf("either TxidBytes or TxidStr must be " +
"specified, but not both")
// The hash was provided as raw bytes.
case len(op.TxidBytes) != 0:
copy(hash[:], op.TxidBytes)
// The hash was provided as a hex-encoded string.
case len(op.TxidStr) != 0:
h, err := chainhash.NewHashFromStr(op.TxidStr)
if err != nil {
return nil, err
}
hash = *h
}
return &wire.OutPoint{
Hash: hash,
Index: op.OutputIndex,
}, nil
}
// BumpFee allows bumping the fee rate of an arbitrary input. A fee preference
// can be expressed either as a specific fee rate or a delta of blocks in which
// the output should be swept on-chain within. If a fee preference is not
// explicitly specified, then an error is returned. The status of the input
// sweep can be checked through the PendingSweeps RPC.
func (w *WalletKit) BumpFee(ctx context.Context,
in *BumpFeeRequest) (*BumpFeeResponse, error) {
// Parse the outpoint from the request.
op, err := unmarshallOutPoint(in.Outpoint)
if err != nil {
return nil, err
}
// Construct the request's fee preference.
satPerKw := lnwallet.SatPerKVByte(in.SatPerByte * 1000).FeePerKWeight()
feePreference := sweep.FeePreference{
ConfTarget: uint32(in.TargetConf),
FeeRate: satPerKw,
}
// We'll attempt to bump the fee of the input through the UtxoSweeper.
// If it is currently attempting to sweep the input, then it'll simply
// 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)
switch err {
case nil:
return &BumpFeeResponse{}, nil
case lnwallet.ErrNotMine:
break
default:
return nil, err
}
// Since we're unable to perform a bump through RBF, we'll assume the
// user is attempting to bump an unconfirmed transaction's fee rate by
// sweeping an output within it under control of the wallet with a
// higher fee rate, essentially performing a Child-Pays-For-Parent
// (CPFP).
//
// We'll gather all of the information required by the UtxoSweeper in
// order to sweep the output.
txOut, err := w.cfg.Wallet.FetchInputInfo(op)
if err != nil {
return nil, err
}
var witnessType input.WitnessType
switch {
case txscript.IsPayToWitnessPubKeyHash(txOut.PkScript):
witnessType = input.WitnessKeyHash
case txscript.IsPayToScriptHash(txOut.PkScript):
witnessType = input.NestedWitnessKeyHash
default:
return nil, fmt.Errorf("unknown input witness %v", op)
}
signDesc := &input.SignDescriptor{
Output: txOut,
HashType: txscript.SigHashAll,
}
input := input.NewBaseInput(op, witnessType, signDesc, 0)
if _, err = w.cfg.Sweeper.SweepInput(input, feePreference); err != nil {
return nil, err
}
return &BumpFeeResponse{}, nil
}

@ -13733,6 +13733,10 @@ var testsCases = []*testCase{
name: "hold invoice sender persistence",
test: testHoldInvoicePersistence,
},
{
name: "cpfp",
test: testCPFP,
},
}
// TestLightningNetworkDaemon performs a series of integration tests amongst a

160
lntest/itest/onchain.go Normal file

@ -0,0 +1,160 @@
// +build rpctest
package itest
import (
"bytes"
"context"
"fmt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/sweep"
)
// testCPFP ensures that the daemon can bump an unconfirmed transaction's fee
// rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction.
//
// TODO(wilmer): Add RBF case once btcd supports it.
func testCPFP(net *lntest.NetworkHarness, t *harnessTest) {
// Skip this test for neutrino, as it's not aware of mempool
// transactions.
if net.BackendCfg.Name() == "neutrino" {
t.Skipf("skipping reorg test for neutrino backend")
}
// We'll start the test by sending Alice some coins, which she'll use to
// send to Bob.
ctxb := context.Background()
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
err := net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, net.Alice)
if err != nil {
t.Fatalf("unable to send coins to alice: %v", err)
}
// Create an address for Bob to send the coins to.
addrReq := &lnrpc.NewAddressRequest{
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
resp, err := net.Bob.NewAddress(ctxt, addrReq)
if err != nil {
t.Fatalf("unable to get new address for bob: %v", err)
}
// Send the coins from Alice to Bob. We should expect a transaction to
// be broadcast and seen in the mempool.
sendReq := &lnrpc.SendCoinsRequest{
Addr: resp.Address,
Amount: btcutil.SatoshiPerBitcoin,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
if _, err = net.Alice.SendCoins(ctxt, sendReq); err != nil {
t.Fatalf("unable to send coins to bob: %v", err)
}
txid, err := waitForTxInMempool(net.Miner.Node, minerMempoolTimeout)
if err != nil {
t.Fatalf("expected one mempool transaction: %v", err)
}
// We'll then extract the raw transaction from the mempool in order to
// determine the index of Bob's output.
tx, err := net.Miner.Node.GetRawTransaction(txid)
if err != nil {
t.Fatalf("unable to extract raw transaction from mempool: %v",
err)
}
bobOutputIdx := -1
for i, txOut := range tx.MsgTx().TxOut {
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
txOut.PkScript, net.Miner.ActiveNet,
)
if err != nil {
t.Fatalf("unable to extract address from pkScript=%x: "+
"%v", txOut.PkScript, err)
}
if addrs[0].String() == resp.Address {
bobOutputIdx = i
}
}
if bobOutputIdx == -1 {
t.Fatalf("bob's output was not found within the transaction")
}
// We'll attempt to bump the fee of this transaction by performing a
// CPFP from Alice's point of view.
op := &lnrpc.OutPoint{
TxidBytes: txid[:],
OutputIndex: uint32(bobOutputIdx),
}
bumpFeeReq := &walletrpc.BumpFeeRequest{
Outpoint: op,
SatPerByte: uint32(sweep.DefaultMaxFeeRate.FeePerKVByte() / 1000),
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
_, err = net.Bob.WalletKitClient.BumpFee(ctxt, bumpFeeReq)
if err != nil {
t.Fatalf("unable to bump fee: %v", err)
}
// We should now expect to see two transactions within the mempool, a
// parent and its child.
_, err = waitForNTxsInMempool(net.Miner.Node, 2, minerMempoolTimeout)
if err != nil {
t.Fatalf("expected two mempool transactions: %v", err)
}
// We should also expect to see the output being swept by the
// UtxoSweeper. We'll ensure it's using the fee rate specified.
pendingSweepsReq := &walletrpc.PendingSweepsRequest{}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
pendingSweepsResp, err := net.Bob.WalletKitClient.PendingSweeps(
ctxt, pendingSweepsReq,
)
if err != nil {
t.Fatalf("unable to retrieve pending sweeps: %v", err)
}
if len(pendingSweepsResp.PendingSweeps) != 1 {
t.Fatalf("expected to find %v pending sweep(s), found %v", 1,
len(pendingSweepsResp.PendingSweeps))
}
pendingSweep := pendingSweepsResp.PendingSweeps[0]
if !bytes.Equal(pendingSweep.Outpoint.TxidBytes, op.TxidBytes) {
t.Fatalf("expected output txid %x, got %x", op.TxidBytes,
pendingSweep.Outpoint.TxidBytes)
}
if pendingSweep.Outpoint.OutputIndex != op.OutputIndex {
t.Fatalf("expected output index %v, got %v", op.OutputIndex,
pendingSweep.Outpoint.OutputIndex)
}
if pendingSweep.SatPerByte != bumpFeeReq.SatPerByte {
t.Fatalf("expected sweep sat per byte %v, got %v",
bumpFeeReq.SatPerByte, pendingSweep.SatPerByte)
}
// Mine a block to clean up the unconfirmed transactions.
mineBlocks(t, net, 1, 2)
// The input used to CPFP should no longer be pending.
err = lntest.WaitNoError(func() error {
req := &walletrpc.PendingSweepsRequest{}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
resp, err := net.Bob.WalletKitClient.PendingSweeps(ctxt, req)
if err != nil {
return fmt.Errorf("unable to retrieve bob's pending "+
"sweeps: %v", err)
}
if len(resp.PendingSweeps) != 0 {
return fmt.Errorf("expected 0 pending sweeps, found %d",
len(resp.PendingSweeps))
}
return nil
}, defaultTimeout)
if err != nil {
t.Fatalf(err.Error())
}
}

@ -15,11 +15,6 @@ import (
"sync"
"time"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
macaroon "gopkg.in/macaroon.v2"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
@ -29,7 +24,12 @@ import (
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/macaroons"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
macaroon "gopkg.in/macaroon.v2"
)
var (
@ -250,9 +250,10 @@ type HarnessNode struct {
invoicesrpc.InvoicesClient
// RouterClient cannot be embedded, because a name collision would occur
// on the main rpc SendPayment.
RouterClient routerrpc.RouterClient
// RouterClient and WalletKitClient cannot be embedded, because a name
// collision would occur with LightningClient.
RouterClient routerrpc.RouterClient
WalletKitClient walletrpc.WalletKitClient
}
// Assert *HarnessNode implements the lnrpc.LightningClient interface.
@ -503,6 +504,7 @@ func (hn *HarnessNode) initLightningClient(conn *grpc.ClientConn) error {
hn.LightningClient = lnrpc.NewLightningClient(conn)
hn.InvoicesClient = invoicesrpc.NewInvoicesClient(conn)
hn.RouterClient = routerrpc.NewRouterClient(conn)
hn.WalletKitClient = walletrpc.NewWalletKitClient(conn)
// Set the harness node's pubkey to what the node claims in GetInfo.
err := hn.FetchNodeInfo()

@ -22,7 +22,7 @@ const (
// DefaultMaxFeeRate is the default maximum fee rate allowed within the
// UtxoSweeper. The current value is equivalent to a fee rate of 10,000
// sat/vbyte.
DefaultMaxFeeRate lnwallet.SatPerKWeight = 250 * 1e4
DefaultMaxFeeRate = lnwallet.FeePerKwFloor * 1e4
// DefaultFeeRateBucketSize is the default size of fee rate buckets
// we'll use when clustering inputs into buckets with similar fee rates
@ -46,6 +46,10 @@ var (
// for the configured max number of attempts.
ErrTooManyAttempts = errors.New("sweep failed after max attempts")
// ErrNoFeePreference is returned when we attempt to satisfy a sweep
// request from a client whom did not specify a fee preference.
ErrNoFeePreference = errors.New("no fee preference specified")
// ErrSweeperShuttingDown is an error returned when a client attempts to
// make a request to the UtxoSweeper, but it is unable to handle it as
// it is/has already been stoppepd.
@ -133,6 +137,21 @@ type PendingInput struct {
NextBroadcastHeight uint32
}
// 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
}
// 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 {
resultChan chan Result
err error
}
// UtxoSweeper is responsible for sweeping outputs back into the wallet
type UtxoSweeper struct {
started uint32 // To be used atomically.
@ -148,6 +167,10 @@ type UtxoSweeper struct {
// UtxoSweeper is attempting to sweep.
pendingSweepsReqs chan *pendingSweepsReq
// bumpFeeReqs 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
// pendingInputs is the total set of inputs the UtxoSweeper has been
// requested to sweep.
pendingInputs pendingInputs
@ -257,6 +280,7 @@ func New(cfg *UtxoSweeperConfig) *UtxoSweeper {
cfg: cfg,
newInputs: make(chan *sweepInputMessage),
spendChan: make(chan *chainntnfs.SpendDetail),
bumpFeeReqs: make(chan *bumpFeeReq),
pendingSweepsReqs: make(chan *pendingSweepsReq),
quit: make(chan struct{}),
pendingInputs: make(pendingInputs),
@ -351,7 +375,10 @@ func (s *UtxoSweeper) Stop() error {
// SweepInput sweeps inputs back into the wallet. The inputs will be batched and
// swept after the batch time window ends. A custom fee preference can be
// provided, otherwise the UtxoSweeper's default will be used.
// provided to determine what fee rate should be used for the input. Note that
// the input may not always be swept with this exact value, as its possible for
// it to be batched under the same transaction with other similar fee rate
// inputs.
//
// NOTE: Extreme care needs to be taken that input isn't changed externally.
// Because it is an interface and we don't know what is exactly behind it, we
@ -394,6 +421,12 @@ func (s *UtxoSweeper) SweepInput(input input.Input,
func (s *UtxoSweeper) feeRateForPreference(
feePreference FeePreference) (lnwallet.SatPerKWeight, error) {
// Ensure a type of fee preference is specified to prevent using a
// default below.
if feePreference.FeeRate == 0 && feePreference.ConfTarget == 0 {
return 0, ErrNoFeePreference
}
feeRate, err := DetermineFeePerKw(s.cfg.FeeEstimator, feePreference)
if err != nil {
return 0, err
@ -532,6 +565,15 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch,
case req := <-s.pendingSweepsReqs:
req.respChan <- s.handlePendingSweepsReq(req)
// 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{
resultChan: resultChan,
err: err,
}
// The timer expires and we are going to (re)sweep.
case <-s.timer:
log.Debugf("Sweep timer expired")
@ -979,6 +1021,92 @@ func (s *UtxoSweeper) handlePendingSweepsReq(
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.
//
// 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) {
// Ensure the client provided a sane fee preference.
if _, err := s.feeRateForPreference(feePreference); err != nil {
return nil, err
}
responseChan := make(chan *bumpFeeResp, 1)
select {
case s.bumpFeeReqs <- &bumpFeeReq{
input: input,
feePreference: feePreference,
responseChan: responseChan,
}:
case <-s.quit:
return nil, ErrSweeperShuttingDown
}
select {
case response := <-responseChan:
return response.resultChan, response.err
case <-s.quit:
return nil, ErrSweeperShuttingDown
}
}
// 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.
//
// TODO(wilmer):
// * Validate fee preference to ensure we'll create a valid replacement
// transaction to allow the new fee rate to propagate throughout the
// network.
// * 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) {
// 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
// batched with others which also have a similar fee rate, creating a
// higher fee rate transaction that replaces the original input's
// sweeping transaction.
pendingInput, ok := s.pendingInputs[req.input]
if !ok {
return nil, lnwallet.ErrNotMine
}
log.Debugf("Updating fee preference for %v from %v to %v", req.input,
pendingInput.feePreference, req.feePreference)
pendingInput.feePreference = req.feePreference
// We'll reset the input's publish height to the current so that a new
// transaction can be created that replaces the transaction currently
// spending the input. We only do this for inputs that have been
// broadcast at least once to ensure we don't spend an input before its
// maturity height.
//
// NOTE: The UtxoSweeper is not yet offered time-locked inputs, so the
// check for broadcast attempts is redundant at the moment.
if pendingInput.publishAttempts > 0 {
pendingInput.minPublishHeight = bestHeight
}
if err := s.scheduleSweep(bestHeight); err != nil {
log.Errorf("Unable to schedule sweep: %v", err)
}
resultChan := make(chan Result, 1)
pendingInput.listeners = append(pendingInput.listeners, resultChan)
return resultChan, nil
}
// CreateSweepTx accepts a list of inputs and signs and generates a txn that
// spends from them. This method also makes an accurate fee estimate before
// generating the required witnesses.

@ -372,6 +372,12 @@ func assertTxFeeRate(t *testing.T, tx *wire.MsgTx,
func TestSuccess(t *testing.T) {
ctx := createSweeperTestContext(t)
// Sweeping an input without a fee preference should result in an error.
_, err := ctx.sweeper.SweepInput(spendableInputs[0], FeePreference{})
if err != ErrNoFeePreference {
t.Fatalf("expected ErrNoFeePreference, got %v", err)
}
resultChan, err := ctx.sweeper.SweepInput(
spendableInputs[0], defaultFeePref,
)
@ -1007,14 +1013,13 @@ func TestDifferentFeePreferences(t *testing.T) {
// with the higher fee preference, and the last with the lower. We do
// this to ensure the sweeper can broadcast distinct transactions for
// each sweep with a different fee preference.
lowFeePref := FeePreference{
ConfTarget: 12,
}
ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = 5000
highFeePref := FeePreference{
ConfTarget: 6,
}
ctx.estimator.blocksToFee[highFeePref.ConfTarget] = 10000
lowFeePref := FeePreference{ConfTarget: 12}
lowFeeRate := lnwallet.SatPerKWeight(5000)
ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate
highFeePref := FeePreference{ConfTarget: 6}
highFeeRate := lnwallet.SatPerKWeight(10000)
ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate
input1 := spendableInputs[0]
resultChan1, err := ctx.sweeper.SweepInput(input1, highFeePref)
@ -1039,11 +1044,11 @@ func TestDifferentFeePreferences(t *testing.T) {
// The first transaction broadcast should be the one spending the higher
// fee rate inputs.
sweepTx1 := ctx.receiveTx()
assertTxSweepsInputs(t, &sweepTx1, input1, input2)
assertTxFeeRate(t, &sweepTx1, highFeeRate, input1, input2)
// The second should be the one spending the lower fee rate inputs.
sweepTx2 := ctx.receiveTx()
assertTxSweepsInputs(t, &sweepTx2, input3)
assertTxFeeRate(t, &sweepTx2, lowFeeRate, input3)
// With the transactions broadcast, we'll mine a block to so that the
// result is delivered to each respective client.
@ -1123,3 +1128,64 @@ func TestPendingInputs(t *testing.T) {
ctx.finish(1)
}
// TestBumpFeeRBF ensures that the UtxoSweeper can properly handle a fee bump
// request for an input it is currently attempting to sweep. When sweeping the
// input with the higher fee rate, a replacement transaction is created.
func TestBumpFeeRBF(t *testing.T) {
ctx := createSweeperTestContext(t)
lowFeePref := FeePreference{ConfTarget: 144}
lowFeeRate := lnwallet.FeePerKwFloor
ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate
// 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)
if err != lnwallet.ErrNotMine {
t.Fatalf("expected error lnwallet.ErrNotMine, got \"%v\"", err)
}
// We'll then attempt to sweep an input, which we'll use to bump its fee
// later on.
input := createTestInput(
btcutil.SatoshiPerBitcoin, input.CommitmentTimeLock,
)
sweepResult, err := ctx.sweeper.SweepInput(&input, lowFeePref)
if err != nil {
t.Fatal(err)
}
// Ensure that a transaction is broadcast with the lower fee preference.
ctx.tick()
lowFeeTx := ctx.receiveTx()
assertTxFeeRate(t, &lowFeeTx, lowFeeRate, &input)
// We'll then attempt to bump its fee rate.
highFeePref := FeePreference{ConfTarget: 6}
highFeeRate := DefaultMaxFeeRate
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{})
if err != ErrNoFeePreference {
t.Fatalf("expected ErrNoFeePreference, got %v", err)
}
bumpResult, err = ctx.sweeper.BumpFee(*input.OutPoint(), highFeePref)
if err != nil {
t.Fatalf("unable to bump input's fee: %v", err)
}
// A higher fee rate transaction should be immediately broadcast.
ctx.tick()
highFeeTx := ctx.receiveTx()
assertTxFeeRate(t, &highFeeTx, highFeeRate, &input)
// We'll finish our test by mining the sweep transaction.
ctx.backend.mine()
ctx.expectResult(sweepResult, nil)
ctx.expectResult(bumpResult, nil)
ctx.finish(1)
}