Merge pull request #8843 from ziggie1984/bumpforceclosefee-rpc

bumpforceclosefee rpc
This commit is contained in:
Oliver Gugger 2024-08-12 06:04:53 -06:00 committed by GitHub
commit 2f2efc7824
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1479 additions and 657 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{},
)
// 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")
resp, err := walletClient.BumpForceCloseFee(
ctxc, &walletrpc.BumpForceCloseFeeRequest{
ChanPoint: rpcChannelPoint,
DeadlineDelta: uint32(ctx.Uint64("conf_target")),
Budget: ctx.Uint64("budget"),
Immediate: immediate,
StartingFeerate: ctx.Uint64("sat_per_vbyte"),
})
if err != nil {
return err
}
// 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")),
Budget: ctx.Uint64("budget"),
Immediate: ctx.Bool("immediate"),
SatPerVbyte: 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

@ -0,0 +1,56 @@
# Release Notes
- [Bug Fixes](#bug-fixes)
- [New Features](#new-features)
- [Functional Enhancements](#functional-enhancements)
- [RPC Additions](#rpc-additions)
- [lncli Additions](#lncli-additions)
- [Improvements](#improvements)
- [Functional Updates](#functional-updates)
- [RPC Updates](#rpc-updates)
- [lncli Updates](#lncli-updates)
- [Breaking Changes](#breaking-changes)
- [Performance Improvements](#performance-improvements)
- [Technical and Architectural Updates](#technical-and-architectural-updates)
- [BOLT Spec Updates](#bolt-spec-updates)
- [Testing](#testing)
- [Database](#database)
- [Code Health](#code-health)
- [Tooling and Documentation](#tooling-and-documentation)
# Bug Fixes
# New Features
## Functional Enhancements
## RPC Additions
* [Add a new rpc endpoint](https://github.com/lightningnetwork/lnd/pull/8843)
`BumpForceCloseFee` which moves the functionality soley available in the
`lncli` to LND hence making it more universal.
## lncli Additions
# Improvements
## Functional Updates
## RPC Updates
## lncli Updates
## Code Health
## Breaking Changes
## Performance Improvements
# Technical and Architectural Updates
## BOLT Spec Updates
## Testing
## Database
## Code Health
## Tooling and Documentation
# Contributors (Alphabetical Order)
* Ziggie

View File

@ -490,6 +490,10 @@ var allTestCases = []*lntest.TestCase{
Name: "bumpfee",
TestFunc: testBumpFee,
},
{
Name: "bumpforceclosefee",
TestFunc: testBumpForceCloseFee,
},
{
Name: "taproot",
TestFunc: testTaproot,

View File

@ -1936,17 +1936,6 @@ func runMultiHopHtlcAggregation(ht *lntest.HarnessTest,
expectedTxes := 0
switch c {
// With the closing transaction confirmed, we should expect Bob's HTLC
// timeout transactions to be broadcast due to the expiry being reached.
// We will also expect the success transactions, since he learnt the
// preimages from Alice. We also expect Carol to sweep her commitment
// output.
case lnrpc.CommitmentType_LEGACY:
ht.AssertNumPendingSweeps(bob, numInvoices*2+1)
ht.AssertNumPendingSweeps(carol, 1)
expectedTxes = 2*numInvoices + 1
// In case of anchors, all success transactions will be aggregated into
// one, the same is the case for the timeout transactions. In this case
// Carol will also sweep her commitment and anchor output in a single
@ -2068,12 +2057,6 @@ func runMultiHopHtlcAggregation(ht *lntest.HarnessTest,
}
switch c {
// In case this is a non-anchor channel type, we must mine 2 blocks, as
// the nursery waits an extra block before sweeping. Before the blocks
// are mined, we should expect to see Bob's commit sweep in the mempool.
case lnrpc.CommitmentType_LEGACY:
ht.MineBlocksAndAssertNumTxes(2, 1)
// Mining one additional block, Bob's second level tx is mature, and he
// can sweep the output. Before the blocks are mined, we should expect
// to see Bob's commit sweep in the mempool.
@ -2478,11 +2461,6 @@ func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest,
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED)
switch c {
// For non-anchor channel type, we should expect to see Bob's commit
// sweep in the mempool.
case lnrpc.CommitmentType_LEGACY:
numTxesMempool++
// For anchor channel type, we should expect to see Bob's commit output
// and his anchor output be swept in a single tx in the mempool.
case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT:
@ -2665,10 +2643,6 @@ func runExtraPreimageFromLocalCommit(ht *lntest.HarnessTest,
// - Bob's local output sweep tx, if this is NOT script enforced lease.
// - Carol's anchor sweep tx cannot be broadcast as it's uneconomical.
switch c {
case lnrpc.CommitmentType_LEGACY:
htlcOutpoint.Index = 0
ht.AssertNumTxsInMempool(2)
case lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT:
htlcOutpoint.Index = 2
ht.AssertNumTxsInMempool(2)

View File

@ -2154,3 +2154,157 @@ func runBumpFee(ht *lntest.HarnessTest, alice *node.HarnessNode) {
// Clean up the mempol.
ht.MineBlocksAndAssertNumTxes(1, 2)
}
// testBumpForceCloseFee tests that when a force close transaction, in
// particular a commitment which has no HTLCs at stake, can be bumped via the
// rpc endpoint `BumpForceCloseFee`.
//
// NOTE: This test does not check for a specific fee rate because channel force
// closures should be bumped taking a budget into account not a specific
// fee rate.
func testBumpForceCloseFee(ht *lntest.HarnessTest) {
// Skip this test for neutrino, as it's not aware of mempool
// transactions.
if ht.IsNeutrinoBackend() {
ht.Skipf("skipping BumpForceCloseFee test for neutrino backend")
}
// fundAmt is the funding amount.
fundAmt := btcutil.Amount(1_000_000)
// We add a push amount because otherwise no anchor for the counter
// party will be created which influences the commitment fee
// calculation.
pushAmt := btcutil.Amount(50_000)
openChannelParams := lntest.OpenChannelParams{
Amt: fundAmt,
PushAmt: pushAmt,
}
// Bumping the close fee rate is only possible for anchor channels.
cfg := []string{
"--protocol.anchors",
}
// Create a two hop network: Alice -> Bob.
chanPoints, nodes := createSimpleNetwork(ht, cfg, 2, openChannelParams)
// Unwrap the results.
chanPoint := chanPoints[0]
alice := nodes[0]
// We need to fund alice with 2 wallet inputs so that we can test to
// increase the fee rate of the anchor cpfp via two subsequent calls of
// the`BumpForceCloseFee` rpc cmd.
//
// TODO (ziggie): Make sure we use enough wallet inputs so that both
// anchor transactions (local, remote commitment tx) can be created and
// broadcasted. Not sure if we really need this, because we can be sure
// as soon as one anchor transactions makes it into the mempool that the
// others will fail anyways?
ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, alice)
// Alice force closes the channel which has no HTLCs at stake.
_, closingTxID := ht.CloseChannelAssertPending(alice, chanPoint, true)
require.NotNil(ht, closingTxID)
// Alice should see one waiting close channel.
ht.AssertNumWaitingClose(alice, 1)
// Alice should have 2 registered sweep inputs. The anchor of the local
// commitment tx and the anchor of the remote commitment tx.
ht.AssertNumPendingSweeps(alice, 2)
// Calculate the commitment tx fee rate.
closingTx := ht.AssertTxInMempool(closingTxID)
require.NotNil(ht, closingTx)
// The default commitment fee for anchor channels is capped at 2500
// sat/kw but there might be some inaccuracies because of the witness
// signature length therefore we calculate the exact value here.
closingFeeRate := ht.CalculateTxFeeRate(closingTx)
// We increase the fee rate of the fee function by 100% to make sure
// we trigger a cpfp-transaction.
newFeeRate := closingFeeRate * 2
// We need to make sure that the budget can cover the fees for bumping.
// However we also want to make sure that the budget is not too large
// so that the delta of the fee function does not increase the feerate
// by a single sat hence NOT rbfing the anchor sweep every time a new
// block is found and a new sweep broadcast is triggered.
//
// NOTE:
// We expect an anchor sweep with 2 inputs (anchor input + a wallet
// input) and 1 p2tr output. This transaction has a weight of approx.
// 725 wu. This info helps us to calculate the delta of the fee
// function.
// EndFeeRate: 100_000 sats/725 wu * 1000 = 137931 sat/kw
// StartingFeeRate: 5000 sat/kw
// delta = (137931-5000)/1008 = 132 sat/kw (which is lower than
// 250 sat/kw) => hence we are violating BIP 125 Rule 4, which is
// exactly what we want here to test the subsequent calling of the
// bumpclosefee rpc.
cpfpBudget := 100_000
bumpFeeReq := &walletrpc.BumpForceCloseFeeRequest{
ChanPoint: chanPoint,
StartingFeerate: uint64(newFeeRate.FeePerVByte()),
Budget: uint64(cpfpBudget),
// We use a force param to create the sweeping tx immediately.
Immediate: true,
}
alice.RPC.BumpForceCloseFee(bumpFeeReq)
// We expect the initial closing transaction and the local anchor cpfp
// transaction because alice force closed the channel.
//
// NOTE: We don't compare a feerate but only make sure that a cpfp
// transaction was triggered. The sweeper increases the fee rate
// periodically with every new incoming block and the selected fee
// function.
ht.AssertNumTxsInMempool(2)
// Identify the cpfp anchor sweep.
txns := ht.GetNumTxsFromMempool(2)
cpfpSweep1 := ht.FindSweepingTxns(txns, 1, closingTx.TxHash())[0]
// Mine an empty block and make sure the anchor cpfp is still in the
// mempool hence the new block did not let the sweeper subsystem rbf
// this anchor sweep transaction (because of the small fee delta).
ht.MineEmptyBlocks(1)
cpfpHash1 := cpfpSweep1.TxHash()
ht.AssertTxInMempool(&cpfpHash1)
// Now Bump the fee rate again with a bigger starting fee rate of the
// fee function.
newFeeRate = closingFeeRate * 3
bumpFeeReq = &walletrpc.BumpForceCloseFeeRequest{
ChanPoint: chanPoint,
StartingFeerate: uint64(newFeeRate.FeePerVByte()),
// The budget needs to be high enough to pay for the fee because
// the anchor does not have an output value high enough to pay
// for itself.
Budget: uint64(cpfpBudget),
// We use a force param to create the sweeping tx immediately.
Immediate: true,
}
alice.RPC.BumpForceCloseFee(bumpFeeReq)
// Make sure the old sweep is not in the mempool anymore, which proofs
// that a new cpfp transaction replaced the old one paying higher fees.
ht.AssertTxNotInMempool(cpfpHash1)
// Identify the new cpfp transaction.
// Both anchor sweeps result from the same closing tx (the local
// commitment) hence proofing that the remote commitment transaction
// and its cpfp transaction is invalid and not accepted into the
// mempool.
txns = ht.GetNumTxsFromMempool(2)
ht.FindSweepingTxns(txns, 1, closingTx.TxHash())
// Mine both transactions, the closing tx and the anchor cpfp tx.
// This is needed to clean up the mempool.
ht.MineBlocksAndAssertNumTxes(1, 2)
}

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

@ -17,6 +17,7 @@ import (
"github.com/btcsuite/btcd/integration/rpctest"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/stretchr/testify/require"
@ -281,8 +282,6 @@ func (h *HarnessMiner) GetRawTransactionVerbose(
// AssertTxInMempool asserts a given transaction can be found in the mempool.
func (h *HarnessMiner) AssertTxInMempool(txid *chainhash.Hash) *wire.MsgTx {
var msgTx *wire.MsgTx
err := wait.NoError(func() error {
// We require the RPC call to be succeeded and won't wait for
// it as it's an unexpected behavior.
@ -292,20 +291,22 @@ func (h *HarnessMiner) AssertTxInMempool(txid *chainhash.Hash) *wire.MsgTx {
return fmt.Errorf("empty mempool")
}
for _, memTx := range mempool {
// Check the values are equal.
if *memTx == *txid {
return nil
}
isEqual := func(memTx *chainhash.Hash) bool {
return *memTx == *txid
}
result := fn.Find(isEqual, mempool)
if result.IsNone() {
return fmt.Errorf("txid %v not found in "+
"mempool: %v", txid, mempool)
}
return fmt.Errorf("txid %v not found in mempool: %v", txid,
mempool)
return nil
}, wait.MinerMempoolTimeout)
require.NoError(h, err, "timeout checking mempool")
return msgTx
return h.GetRawTransaction(txid).MsgTx()
}
// AssertTxNotInMempool asserts a given transaction cannot be found in the

View File

@ -254,6 +254,21 @@ func (h *HarnessRPC) BumpFee(
return resp
}
// BumpForceCloseFee makes a RPC call to the node's WalletKitClient and asserts.
//
//nolint:lll
func (h *HarnessRPC) BumpForceCloseFee(
req *walletrpc.BumpForceCloseFeeRequest) *walletrpc.BumpForceCloseFeeResponse {
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
defer cancel()
resp, err := h.WalletKit.BumpForceCloseFee(ctxt, req)
h.NoError(err, "BumpForceCloseFee")
return resp
}
// ListAccounts makes a RPC call to the node's WalletKitClient and asserts.
func (h *HarnessRPC) ListAccounts(
req *walletrpc.ListAccountsRequest) *walletrpc.ListAccountsResponse {

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)

View File

@ -645,6 +645,12 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) {
// If this input is forced, we perform an sweep
// immediately.
//
// TODO(ziggie): Make sure when `immediate` is selected
// as a parameter that we only trigger the sweeping of
// this specific input rather than triggering the sweeps
// of all current pending inputs registered with the
// sweeper.
if input.params.Immediate {
inputs := s.updateSweeperInputs()
s.sweepPendingInputs(inputs)