multi: add bumpforceclosefee rpc endpoint.

Add a new bumpforceclosefee rpc endpoint to the wallet server.
Move the logic from the lncli level to the wallet server rpc level.
This is more in line with a proper client-server design.

wallet lncli: use new bumpforceclosefee endpoint.

Besides using the new bumpforceclosefee rpc endpoint we also enable the
bumping of taproot anchor channels.
This commit is contained in:
ziggie 2024-06-15 13:51:14 +01:00
parent 57a5e4912b
commit 07b18c1c86
No known key found for this signature in database
GPG Key ID: 1AFF9C4DCED6D666
12 changed files with 1233 additions and 621 deletions

View File

@ -5,7 +5,6 @@ package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
@ -21,7 +20,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
@ -431,51 +429,50 @@ var bumpForceCloseFeeCommand = cli.Command{
allows the fee of a channel force closing transaction to be increased by
using the child-pays-for-parent mechanism. It will instruct the sweeper
to sweep the anchor outputs of the closing transaction at the requested
fee rate or confirmation target. The specified fee rate will be the
effective fee rate taking the parent fee into account.
confirmation target and limit the fees to the specified budget.
`,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "conf_target",
Usage: `
The deadline in number of blocks that the input should be spent within.
When not set, for new inputs, the default value (1008) is used; for
exiting inputs, their current values will be retained.`,
The deadline in number of blocks that the anchor output should be spent
within to bump the closing transaction.`,
},
cli.Uint64Flag{
Name: "sat_per_byte",
Usage: "Deprecated, use sat_per_vbyte instead.",
Hidden: true,
},
cli.Uint64Flag{
Name: "sat_per_vbyte",
Usage: `
The starting fee rate, expressed in sat/vbyte. This value will be used
by the sweeper's fee function as its starting fee rate. When not set,
the sweeper will use the estimated fee rate using the target_conf as the
starting fee rate.`,
},
cli.BoolFlag{
Name: "force",
Usage: "Deprecated, use immediate instead.",
Hidden: true,
},
cli.Uint64Flag{
Name: "sat_per_vbyte",
Usage: `
The starting fee rate, expressed in sat/vbyte, that will be used to
spend the input with initially. This value will be used by the
sweeper's fee function as its starting fee rate. When not set, the
sweeper will use the estimated fee rate using the target_conf as the
starting fee rate.`,
},
cli.BoolFlag{
Name: "immediate",
Usage: `
Whether this input will be swept immediately. When set to true, the
sweeper will sweep this input without waiting for the next batch.`,
Whether this cpfp transaction will be triggered immediately. When set to
true, the sweeper will consider all currently pending registered sweeps
and trigger new batch transactions including the sweeping of the anchor
output related to the selected force close transaction.`,
},
cli.Uint64Flag{
Name: "budget",
Usage: `
The max amount in sats that can be used as the fees. Setting this value
greater than the input's value may result in CPFP - one or more wallet
utxos will be used to pay the fees specified by the budget. If not set,
for new inputs, by default 50% of the input's value will be treated as
the budget for fee bumping; for existing inputs, their current budgets
will be retained.`,
The max amount in sats that can be used as the fees. For already
registered anchor outputs if not set explicitly the old value will be
used. For channel force closes which have no HTLCs in their commitment
transaction this value has to be set to an appropriate amount to pay for
the cpfp transaction of the force closed channel otherwise the fee
bumping will fail.`,
},
},
Action: actionDecorator(bumpForceCloseFee),
@ -492,99 +489,44 @@ func bumpForceCloseFee(ctx *cli.Context) error {
// Validate the channel point.
channelPoint := ctx.Args().Get(0)
_, err := NewProtoOutPoint(channelPoint)
rpcChannelPoint, err := parseChanPoint(channelPoint)
if err != nil {
return err
}
// Fetch all waiting close channels.
client, cleanUp := getClient(ctx)
defer cleanUp()
// Fetch waiting close channel commitments.
commitments, err := getWaitingCloseCommitments(
ctxc, client, channelPoint,
)
if err != nil {
return err
// `sat_per_byte` was deprecated we only use sats/vbyte now.
if ctx.IsSet("sat_per_byte") {
return fmt.Errorf("deprecated, use sat_per_vbyte instead.")
}
// Retrieve pending sweeps.
walletClient, cleanUp := getWalletClient(ctx)
defer cleanUp()
sweeps, err := walletClient.PendingSweeps(
ctxc, &walletrpc.PendingSweepsRequest{},
)
if err != nil {
return err
// Parse immediate flag (force flag was deprecated).
if ctx.IsSet("immediate") && ctx.IsSet("force") {
return fmt.Errorf("cannot set immediate and force flag at " +
"the same time")
}
immediate := ctx.Bool("immediate") || ctx.Bool("force")
// Match pending sweeps with commitments of the channel for which a bump
// is requested and bump their fees.
commitSet := map[string]struct{}{
commitments.LocalTxid: {},
commitments.RemoteTxid: {},
}
if commitments.RemotePendingTxid != "" {
commitSet[commitments.RemotePendingTxid] = struct{}{}
}
for _, sweep := range sweeps.PendingSweeps {
// Only bump anchor sweeps.
if sweep.WitnessType != walletrpc.WitnessType_COMMITMENT_ANCHOR {
continue
}
// Skip unrelated sweeps.
sweepTxID, err := chainhash.NewHash(sweep.Outpoint.TxidBytes)
if err != nil {
return err
}
if _, match := commitSet[sweepTxID.String()]; !match {
continue
}
resp, err := walletClient.BumpFee(
ctxc, &walletrpc.BumpFeeRequest{
Outpoint: sweep.Outpoint,
TargetConf: uint32(ctx.Uint64("conf_target")),
resp, err := walletClient.BumpForceCloseFee(
ctxc, &walletrpc.BumpForceCloseFeeRequest{
ChanPoint: rpcChannelPoint,
DeadlineDelta: uint32(ctx.Uint64("conf_target")),
Budget: ctx.Uint64("budget"),
Immediate: ctx.Bool("immediate"),
SatPerVbyte: ctx.Uint64("sat_per_vbyte"),
Immediate: immediate,
StartingFeerate: ctx.Uint64("sat_per_vbyte"),
})
if err != nil {
return err
}
// Bump fee of the anchor sweep.
fmt.Printf("Bumping fee of %v:%v: %v\n",
sweepTxID, sweep.Outpoint.OutputIndex, resp.GetStatus())
}
fmt.Printf("BumpForceCloseFee result: %s\n", resp.Status)
return nil
}
func getWaitingCloseCommitments(ctxc context.Context,
client lnrpc.LightningClient, channelPoint string) (
*lnrpc.PendingChannelsResponse_Commitments, error) {
req := &lnrpc.PendingChannelsRequest{}
resp, err := client.PendingChannels(ctxc, req)
if err != nil {
return nil, err
}
// Lookup the channel commit tx hashes.
for _, channel := range resp.WaitingCloseChannels {
if channel.Channel.ChannelPoint == channelPoint {
return channel.Commitments, nil
}
}
return nil, errors.New("channel not found")
}
var listSweepsCommand = cli.Command{
Name: "listsweeps",
Usage: "Lists all sweeps that have been published by our node.",

View File

@ -197,6 +197,32 @@ func GetChanPointFundingTxid(chanPoint *ChannelPoint) (*chainhash.Hash, error) {
return chainhash.NewHash(txid)
}
// GetChannelOutPoint returns the outpoint of the related channel point.
func GetChannelOutPoint(chanPoint *ChannelPoint) (*OutPoint, error) {
var txid []byte
// A channel point's funding txid can be get/set as a byte slice or a
// string. In the case it is a string, decode it.
switch chanPoint.GetFundingTxid().(type) {
case *ChannelPoint_FundingTxidBytes:
txid = chanPoint.GetFundingTxidBytes()
case *ChannelPoint_FundingTxidStr:
s := chanPoint.GetFundingTxidStr()
h, err := chainhash.NewHashFromStr(s)
if err != nil {
return nil, err
}
txid = h[:]
}
return &OutPoint{
TxidBytes: txid,
OutputIndex: chanPoint.OutputIndex,
}, nil
}
// CalculateFeeRate uses either satPerByte or satPerVByte, but not both, from a
// request to calculate the fee rate. It provides compatibility for the
// deprecated field, satPerByte. Once the field is safe to be removed, the

View File

@ -6,6 +6,7 @@ package walletrpc
import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -76,4 +77,7 @@ type Config struct {
// CoinSelectionStrategy is the strategy that is used for selecting
// coins when funding a transaction.
CoinSelectionStrategy wallet.CoinSelectionStrategy
// ChanStateDB is the reference to the channel db.
ChanStateDB *channeldb.ChannelStateDB
}

File diff suppressed because it is too large Load Diff

View File

@ -774,6 +774,40 @@ func local_request_WalletKit_BumpFee_0(ctx context.Context, marshaler runtime.Ma
}
func request_WalletKit_BumpForceCloseFee_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq BumpForceCloseFeeRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.BumpForceCloseFee(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_WalletKit_BumpForceCloseFee_0(ctx context.Context, marshaler runtime.Marshaler, server WalletKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq BumpForceCloseFeeRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.BumpForceCloseFee(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_WalletKit_ListSweeps_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
@ -1458,6 +1492,29 @@ func RegisterWalletKitHandlerServer(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_WalletKit_BumpForceCloseFee_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/walletrpc.WalletKit/BumpForceCloseFee", runtime.WithHTTPPathPattern("/v2/wallet/BumpForceCloseFee"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_WalletKit_BumpForceCloseFee_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletKit_BumpForceCloseFee_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_WalletKit_ListSweeps_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -2054,6 +2111,26 @@ func RegisterWalletKitHandlerClient(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_WalletKit_BumpForceCloseFee_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/walletrpc.WalletKit/BumpForceCloseFee", runtime.WithHTTPPathPattern("/v2/wallet/BumpForceCloseFee"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_WalletKit_BumpForceCloseFee_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletKit_BumpForceCloseFee_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_WalletKit_ListSweeps_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -2202,6 +2279,8 @@ var (
pattern_WalletKit_BumpFee_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "wallet", "bumpfee"}, ""))
pattern_WalletKit_BumpForceCloseFee_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "wallet", "BumpForceCloseFee"}, ""))
pattern_WalletKit_ListSweeps_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "wallet", "sweeps"}, ""))
pattern_WalletKit_LabelTransaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "tx", "label"}, ""))
@ -2258,6 +2337,8 @@ var (
forward_WalletKit_BumpFee_0 = runtime.ForwardResponseMessage
forward_WalletKit_BumpForceCloseFee_0 = runtime.ForwardResponseMessage
forward_WalletKit_ListSweeps_0 = runtime.ForwardResponseMessage
forward_WalletKit_LabelTransaction_0 = runtime.ForwardResponseMessage

View File

@ -572,6 +572,31 @@ func RegisterWalletKitJSONCallbacks(registry map[string]func(ctx context.Context
callback(string(respBytes), nil)
}
registry["walletrpc.WalletKit.BumpForceCloseFee"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &BumpForceCloseFeeRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewWalletKitClient(conn)
resp, err := client.BumpForceCloseFee(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["walletrpc.WalletKit.ListSweeps"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {

View File

@ -273,6 +273,13 @@ service WalletKit {
*/
rpc BumpFee (BumpFeeRequest) returns (BumpFeeResponse);
/* lncli: `wallet bumpforceclosefee`
BumpForceCloseFee is an endpoint that allows users to bump the fee of a
channel force close. This only works for channels with option_anchors.
*/
rpc BumpForceCloseFee (BumpForceCloseFeeRequest)
returns (BumpForceCloseFeeResponse);
/* lncli: `wallet listsweeps`
ListSweeps returns a list of the sweep transactions our node has produced.
Note that these sweeps may not be confirmed yet, as we record sweeps on
@ -1226,6 +1233,46 @@ message BumpFeeResponse {
string status = 1;
}
message BumpForceCloseFeeRequest {
// The channel point which force close transaction we are attempting to
// bump the fee rate for.
lnrpc.ChannelPoint chan_point = 1;
// Optional. The deadline delta in number of blocks that the anchor output
// should be spent within to bump the closing transaction.
uint32 deadline_delta = 2;
/*
Optional. The starting fee rate, expressed in sat/vbyte. This value will be
used by the sweeper's fee function as its starting fee rate. When not set,
the sweeper will use the estimated fee rate using the target_conf as the
starting fee rate.
*/
uint64 starting_feerate = 3;
/*
Optional. Whether this cpfp transaction will be triggered immediately. When
set to true, the sweeper will consider all currently registered sweeps and
trigger new batch transactions including the sweeping of the anchor output
related to the selected force close transaction.
*/
bool immediate = 4;
/*
Optional. The max amount in sats that can be used as the fees. For already
registered anchor outputs if not set explicitly the old value will be used.
For channel force closes which have no HTLCs in their commitment transaction
this value has to be set to an appropriate amount to pay for the cpfp
transaction of the force closed channel otherwise the fee bumping will fail.
*/
uint64 budget = 5;
}
message BumpForceCloseFeeResponse {
// The status of the force close fee bump operation.
string status = 1;
}
message ListSweepsRequest {
/*
Retrieve the full sweep transaction details. If false, only the sweep txids

View File

@ -16,6 +16,39 @@
"application/json"
],
"paths": {
"/v2/wallet/BumpForceCloseFee": {
"post": {
"summary": "lncli: `wallet bumpforceclosefee`\nBumpForceCloseFee is an endpoint that allows users to bump the fee of a\nchannel force close. This only works for channels with option_anchors.",
"operationId": "WalletKit_BumpForceCloseFee",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/walletrpcBumpForceCloseFeeResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/walletrpcBumpForceCloseFeeRequest"
}
}
],
"tags": [
"WalletKit"
]
}
},
"/v2/wallet/accounts": {
"get": {
"summary": "lncli: `wallet accounts list`\nListAccounts retrieves all accounts belonging to the wallet by default. A\nname and key scope filter can be provided to filter through all of the\nwallet accounts and return only those matching.",
@ -949,6 +982,25 @@
"description": "- `p2wkh`: Pay to witness key hash (`WITNESS_PUBKEY_HASH` = 0)\n- `np2wkh`: Pay to nested witness key hash (`NESTED_PUBKEY_HASH` = 1)\n- `p2tr`: Pay to taproot pubkey (`TAPROOT_PUBKEY` = 4)",
"title": "`AddressType` has to be one of:"
},
"lnrpcChannelPoint": {
"type": "object",
"properties": {
"funding_txid_bytes": {
"type": "string",
"format": "byte",
"description": "Txid of the funding transaction. When using REST, this field must be\nencoded as base64."
},
"funding_txid_str": {
"type": "string",
"description": "Hex-encoded string representing the byte-reversed hash of the funding\ntransaction."
},
"output_index": {
"type": "integer",
"format": "int64",
"title": "The index of the output of the funding transaction"
}
}
},
"lnrpcCoinSelectionStrategy": {
"type": "string",
"enum": [
@ -1396,6 +1448,43 @@
}
}
},
"walletrpcBumpForceCloseFeeRequest": {
"type": "object",
"properties": {
"chan_point": {
"$ref": "#/definitions/lnrpcChannelPoint",
"description": "The channel point which force close transaction we are attempting to\nbump the fee rate for."
},
"deadline_delta": {
"type": "integer",
"format": "int64",
"description": "Optional. The deadline delta in number of blocks that the anchor output\nshould be spent within to bump the closing transaction."
},
"starting_feerate": {
"type": "string",
"format": "uint64",
"description": "Optional. The starting fee rate, expressed in sat/vbyte. This value will be\nused by the sweeper's fee function as its starting fee rate. When not set,\nthe sweeper will use the estimated fee rate using the target_conf as the\nstarting fee rate."
},
"immediate": {
"type": "boolean",
"description": "Optional. Whether this cpfp transaction will be triggered immediately. When\nset to true, the sweeper will consider all currently registered sweeps and\ntrigger new batch transactions including the sweeping of the anchor output\nrelated to the selected force close transaction."
},
"budget": {
"type": "string",
"format": "uint64",
"description": "Optional. The max amount in sats that can be used as the fees. For already\nregistered anchor outputs if not set explicitly the old value will be used.\nFor channel force closes which have no HTLCs in their commitment transaction\nthis value has to be set to an appropriate amount to pay for the cpfp\ntransaction of the force closed channel otherwise the fee bumping will fail."
}
}
},
"walletrpcBumpForceCloseFeeResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "The status of the force close fee bump operation."
}
}
},
"walletrpcChangeAddressType": {
"type": "string",
"enum": [

View File

@ -76,3 +76,6 @@ http:
- selector: walletrpc.WalletKit.RemoveTransaction
post: "/v2/wallet/removetx"
body: "*"
- selector: walletrpc.WalletKit.BumpForceCloseFee
post: "/v2/wallet/BumpForceCloseFee"
body: "*"

View File

@ -208,6 +208,10 @@ type WalletKitClient interface {
// done by specifying an outpoint within the low fee transaction that is under
// the control of the wallet.
BumpFee(ctx context.Context, in *BumpFeeRequest, opts ...grpc.CallOption) (*BumpFeeResponse, error)
// lncli: `wallet bumpforceclosefee`
// BumpForceCloseFee is an endpoint that allows users to bump the fee of a
// channel force close. This only works for channels with option_anchors.
BumpForceCloseFee(ctx context.Context, in *BumpForceCloseFeeRequest, opts ...grpc.CallOption) (*BumpForceCloseFeeResponse, error)
// lncli: `wallet listsweeps`
// ListSweeps returns a list of the sweep transactions our node has produced.
// Note that these sweeps may not be confirmed yet, as we record sweeps on
@ -482,6 +486,15 @@ func (c *walletKitClient) BumpFee(ctx context.Context, in *BumpFeeRequest, opts
return out, nil
}
func (c *walletKitClient) BumpForceCloseFee(ctx context.Context, in *BumpForceCloseFeeRequest, opts ...grpc.CallOption) (*BumpForceCloseFeeResponse, error) {
out := new(BumpForceCloseFeeResponse)
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/BumpForceCloseFee", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletKitClient) ListSweeps(ctx context.Context, in *ListSweepsRequest, opts ...grpc.CallOption) (*ListSweepsResponse, error) {
out := new(ListSweepsResponse)
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/ListSweeps", in, out, opts...)
@ -719,6 +732,10 @@ type WalletKitServer interface {
// done by specifying an outpoint within the low fee transaction that is under
// the control of the wallet.
BumpFee(context.Context, *BumpFeeRequest) (*BumpFeeResponse, error)
// lncli: `wallet bumpforceclosefee`
// BumpForceCloseFee is an endpoint that allows users to bump the fee of a
// channel force close. This only works for channels with option_anchors.
BumpForceCloseFee(context.Context, *BumpForceCloseFeeRequest) (*BumpForceCloseFeeResponse, error)
// lncli: `wallet listsweeps`
// ListSweeps returns a list of the sweep transactions our node has produced.
// Note that these sweeps may not be confirmed yet, as we record sweeps on
@ -858,6 +875,9 @@ func (UnimplementedWalletKitServer) PendingSweeps(context.Context, *PendingSweep
func (UnimplementedWalletKitServer) BumpFee(context.Context, *BumpFeeRequest) (*BumpFeeResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BumpFee not implemented")
}
func (UnimplementedWalletKitServer) BumpForceCloseFee(context.Context, *BumpForceCloseFeeRequest) (*BumpForceCloseFeeResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BumpForceCloseFee not implemented")
}
func (UnimplementedWalletKitServer) ListSweeps(context.Context, *ListSweepsRequest) (*ListSweepsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListSweeps not implemented")
}
@ -1282,6 +1302,24 @@ func _WalletKit_BumpFee_Handler(srv interface{}, ctx context.Context, dec func(i
return interceptor(ctx, in, info, handler)
}
func _WalletKit_BumpForceCloseFee_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BumpForceCloseFeeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletKitServer).BumpForceCloseFee(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/walletrpc.WalletKit/BumpForceCloseFee",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletKitServer).BumpForceCloseFee(ctx, req.(*BumpForceCloseFeeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletKit_ListSweeps_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListSweepsRequest)
if err := dec(in); err != nil {
@ -1467,6 +1505,10 @@ var WalletKit_ServiceDesc = grpc.ServiceDesc{
MethodName: "BumpFee",
Handler: _WalletKit_BumpFee_Handler,
},
{
MethodName: "BumpForceCloseFee",
Handler: _WalletKit_BumpForceCloseFee_Handler,
},
{
MethodName: "ListSweeps",
Handler: _WalletKit_ListSweeps_Handler,

View File

@ -29,6 +29,7 @@ import (
base "github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/contractcourt"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input"
@ -42,6 +43,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/sweep"
"golang.org/x/exp/maps"
"google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery"
)
@ -106,6 +108,10 @@ var (
Entity: "onchain",
Action: "write",
}},
"/walletrpc.WalletKit/BumpForceCloseFee": {{
Entity: "onchain",
Action: "write",
}},
"/walletrpc.WalletKit/ListSweeps": {{
Entity: "onchain",
Action: "read",
@ -1125,6 +1131,154 @@ func (w *WalletKit) BumpFee(ctx context.Context,
}, nil
}
// getWaitingCloseChannel returns the waiting close channel in case it does
// exist in the underlying channel state database.
func (w *WalletKit) getWaitingCloseChannel(
chanPoint wire.OutPoint) (*channeldb.OpenChannel, error) {
// Fetch all channels, which still have their commitment transaction not
// confirmed (waiting close channels).
chans, err := w.cfg.ChanStateDB.FetchWaitingCloseChannels()
if err != nil {
return nil, err
}
channel := fn.Find(func(c *channeldb.OpenChannel) bool {
return c.FundingOutpoint == chanPoint
}, chans)
return channel.UnwrapOrErr(errors.New("channel not found"))
}
// BumpForceCloseFee bumps the fee rate of an unconfirmed anchor channel. It
// updates the new fee rate parameters with the sweeper subsystem. Additionally
// it will try to create anchor cpfp transactions for all possible commitment
// transactions (local, remote, remote-dangling) so depending on which
// commitment is in the local mempool only one of them will succeed in being
// broadcasted.
func (w *WalletKit) BumpForceCloseFee(_ context.Context,
in *BumpForceCloseFeeRequest) (*BumpForceCloseFeeResponse, error) {
if in.ChanPoint == nil {
return nil, fmt.Errorf("no chan_point provided")
}
lnrpcOutpoint, err := lnrpc.GetChannelOutPoint(in.ChanPoint)
if err != nil {
return nil, err
}
outPoint, err := UnmarshallOutPoint(lnrpcOutpoint)
if err != nil {
return nil, err
}
// Get the relevant channel if it is in the waiting close state.
channel, err := w.getWaitingCloseChannel(*outPoint)
if err != nil {
return nil, err
}
// Match pending sweeps with commitments of the channel for which a bump
// is requested. Depending on the commitment state when force closing
// the channel we might have up to 3 commitments to consider when
// bumping the fee.
commitSet := fn.NewSet[chainhash.Hash]()
if channel.LocalCommitment.CommitTx != nil {
localTxID := channel.LocalCommitment.CommitTx.TxHash()
commitSet.Add(localTxID)
}
if channel.RemoteCommitment.CommitTx != nil {
remoteTxID := channel.RemoteCommitment.CommitTx.TxHash()
commitSet.Add(remoteTxID)
}
// Check whether there was a dangling commitment at the time the channel
// was force closed.
remoteCommitDiff, err := channel.RemoteCommitChainTip()
if err != nil && !errors.Is(err, channeldb.ErrNoPendingCommit) {
return nil, err
}
if remoteCommitDiff != nil {
hash := remoteCommitDiff.Commitment.CommitTx.TxHash()
commitSet.Add(hash)
}
// Retrieve all of the outputs the UtxoSweeper is currently trying to
// sweep.
inputsMap, err := w.cfg.Sweeper.PendingInputs()
if err != nil {
return nil, err
}
// Get the current height so we can calculate the deadline height.
_, currentHeight, err := w.cfg.Chain.GetBestBlock()
if err != nil {
return nil, fmt.Errorf("unable to retrieve current height: %w",
err)
}
pendingSweeps := maps.Values(inputsMap)
// Discard everything except for the anchor sweeps.
anchors := fn.Filter(func(sweep *sweep.PendingInputResponse) bool {
// Only filter for anchor inputs because these are the only
// inputs which can be used to bump a closed unconfirmed
// commitment transaction.
if sweep.WitnessType != input.CommitmentAnchor &&
sweep.WitnessType != input.TaprootAnchorSweepSpend {
return false
}
return commitSet.Contains(sweep.OutPoint.Hash)
}, pendingSweeps)
// Filter all relevant anchor sweeps and update the sweep request.
for _, anchor := range anchors {
// Anchor cpfp bump request are predictable because they are
// swept separately hence not batched with other sweeps (they
// are marked with the exclusive group flag). Bumping the fee
// rate does not create any conflicting fee bump conditions.
// Either the rbf requirements are met or the bump is rejected
// by the mempool rules.
params, existing, err := w.prepareSweepParams(
&BumpFeeRequest{
Outpoint: lnrpcOutpoint,
TargetConf: in.DeadlineDelta,
SatPerVbyte: in.StartingFeerate,
Immediate: in.Immediate,
Budget: in.Budget,
}, anchor.OutPoint, currentHeight,
)
if err != nil {
return nil, err
}
// There might be the case when an anchor sweep is confirmed
// between fetching the pending sweeps and preparing the sweep
// params. We log this case and proceed.
if !existing {
log.Errorf("Sweep anchor input(%v) not known to the " +
"sweeper subsystem")
continue
}
_, err = w.cfg.Sweeper.UpdateParams(anchor.OutPoint, params)
if err != nil {
return nil, err
}
}
return &BumpForceCloseFeeResponse{
Status: "Successfully registered anchor-cpfp transaction to" +
"bump channel force close transaction",
}, nil
}
// sweepNewInput handles the case where an input is seen the first time by the
// sweeper. It will fetch the output from the wallet and construct an input and
// offer it to the sweeper.

View File

@ -202,6 +202,9 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
cc.Wallet.Cfg.CoinSelectionStrategy,
),
)
subCfgValue.FieldByName("ChanStateDB").Set(
reflect.ValueOf(chanStateDB),
)
case *autopilotrpc.Config:
subCfgValue := extractReflectValue(subCfg)