Skip to content

Commit 2d04813

Browse files
committed
itest: Add itest for bumpclosefeerate rpc.
Add an itest which will bump the close fee rate of an anchor channel which is force closed without having any HTLCs at stake.
1 parent ae28f75 commit 2d04813

File tree

4 files changed

+174
-1
lines changed

4 files changed

+174
-1
lines changed

cmd/lncli/walletrpc_active.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ func bumpForceCloseFee(ctx *cli.Context) error {
496496

497497
// `sat_per_byte` was deprecated we only use sats/vbyte now.
498498
if ctx.IsSet("sat_per_byte") {
499-
return fmt.Errorf("deprecated, use sat_per_vbyte instead.")
499+
return fmt.Errorf("deprecated, use sat_per_vbyte instead")
500500
}
501501

502502
// Retrieve pending sweeps.

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,10 @@ var allTestCases = []*lntest.TestCase{
490490
Name: "bumpfee",
491491
TestFunc: testBumpFee,
492492
},
493+
{
494+
Name: "bumpforceclosefee",
495+
TestFunc: testBumpForceCloseFee,
496+
},
493497
{
494498
Name: "taproot",
495499
TestFunc: testTaproot,

itest/lnd_sweep_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2154,3 +2154,157 @@ func runBumpFee(ht *lntest.HarnessTest, alice *node.HarnessNode) {
21542154
// Clean up the mempol.
21552155
ht.MineBlocksAndAssertNumTxes(1, 2)
21562156
}
2157+
2158+
// testBumpForceCloseFee tests that when a force close transaction, in
2159+
// particular a commitment which has no HTLCs at stake, can be bumped via the
2160+
// rpc endpoint `BumpForceCloseFee`.
2161+
//
2162+
// NOTE: This test does not check for a specific fee rate because channel force
2163+
// closures should be bumped taking a budget into account not a specific
2164+
// fee rate.
2165+
func testBumpForceCloseFee(ht *lntest.HarnessTest) {
2166+
// Skip this test for neutrino, as it's not aware of mempool
2167+
// transactions.
2168+
if ht.IsNeutrinoBackend() {
2169+
ht.Skipf("skipping BumpForceCloseFee test for neutrino backend")
2170+
}
2171+
// fundAmt is the funding amount.
2172+
fundAmt := btcutil.Amount(1_000_000)
2173+
2174+
// We add a push amount because otherwise no anchor for the counter
2175+
// party will be created which influences the commitment fee
2176+
// calculation.
2177+
pushAmt := btcutil.Amount(50_000)
2178+
2179+
openChannelParams := lntest.OpenChannelParams{
2180+
Amt: fundAmt,
2181+
PushAmt: pushAmt,
2182+
}
2183+
2184+
// Bumping the close fee rate is only possible for anchor channels.
2185+
cfg := []string{
2186+
"--protocol.anchors",
2187+
}
2188+
2189+
// Create a two hop network: Alice -> Bob.
2190+
chanPoints, nodes := createSimpleNetwork(ht, cfg, 2, openChannelParams)
2191+
2192+
// Unwrap the results.
2193+
chanPoint := chanPoints[0]
2194+
alice := nodes[0]
2195+
2196+
// We need to fund alice with 2 wallet inputs so that we can test to
2197+
// increase the fee rate of the anchor cpfp via two subsequent calls of
2198+
// the`BumpForceCloseFee` rpc cmd.
2199+
//
2200+
// TODO (ziggie): Make sure we use enough wallet inputs so that both
2201+
// anchor transactions (local, remote commitment tx) can be created and
2202+
// broadcasted. Not sure if we really need this, because we can be sure
2203+
// as soon as one anchor transactions makes it into the mempool that the
2204+
// others will fail anyways?
2205+
ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, alice)
2206+
2207+
// Alice force closes the channel which has no HTLCs at stake.
2208+
_, closingTxID := ht.CloseChannelAssertPending(alice, chanPoint, true)
2209+
require.NotNil(ht, closingTxID)
2210+
2211+
// Alice should see one waiting close channel.
2212+
ht.AssertNumWaitingClose(alice, 1)
2213+
2214+
// Alice should have 2 registered sweep inputs. The anchor of the local
2215+
// commitment tx and the anchor of the remote commitment tx.
2216+
ht.AssertNumPendingSweeps(alice, 2)
2217+
2218+
// Calculate the commitment tx fee rate.
2219+
closingTx := ht.AssertTxInMempool(closingTxID)
2220+
require.NotNil(ht, closingTx)
2221+
2222+
// The default commitment fee for anchor channels is capped at 2500
2223+
// sat/kw but there might be some inaccuracies because of the witness
2224+
// signature length therefore we calculate the exact value here.
2225+
closingFeeRate := ht.CalculateTxFeeRate(closingTx)
2226+
2227+
// We increase the fee rate of the fee function by 100% to make sure
2228+
// we trigger a cpfp-transaction.
2229+
newFeeRate := closingFeeRate * 2
2230+
2231+
// We need to make sure that the budget can cover the fees for bumping.
2232+
// However we also want to make sure that the budget is not too large
2233+
// so that the delta of the fee function does not increase the feerate
2234+
// by a single sat hence NOT rbfing the anchor sweep every time a new
2235+
// block is found and a new sweep broadcast is triggered.
2236+
//
2237+
// NOTE:
2238+
// We expect an anchor sweep with 2 inputs (anchor input + a wallet
2239+
// input) and 1 p2tr output. This transaction has a weight of approx.
2240+
// 725 wu. This info helps us to calculate the delta of the fee
2241+
// function.
2242+
// EndFeeRate: 100_000 sats/725 wu * 1000 = 137931 sat/kw
2243+
// StartingFeeRate: 5000 sat/kw
2244+
// delta = (137931-5000)/1008 = 132 sat/kw (which is lower than
2245+
// 250 sat/kw) => hence we are violating BIP 125 Rule 4, which is
2246+
// exactly what we want here to test the subsequent calling of the
2247+
// bumpclosefee rpc.
2248+
cpfpBudget := 100_000
2249+
2250+
bumpFeeReq := &walletrpc.BumpForceCloseFeeRequest{
2251+
ChanPoint: chanPoint,
2252+
StartingFeerate: uint64(newFeeRate.FeePerVByte()),
2253+
Budget: uint64(cpfpBudget),
2254+
// We use a force param to create the sweeping tx immediately.
2255+
Immediate: true,
2256+
}
2257+
alice.RPC.BumpForceCloseFee(bumpFeeReq)
2258+
2259+
// We expect the initial closing transaction and the local anchor cpfp
2260+
// transaction because alice force closed the channel.
2261+
//
2262+
// NOTE: We don't compare a feerate but only make sure that a cpfp
2263+
// transaction was triggered. The sweeper increases the fee rate
2264+
// periodically with every new incoming block and the selected fee
2265+
// function.
2266+
ht.AssertNumTxsInMempool(2)
2267+
2268+
// Identify the cpfp anchor sweep.
2269+
txns := ht.GetNumTxsFromMempool(2)
2270+
cpfpSweep1 := ht.FindSweepingTxns(txns, 1, closingTx.TxHash())[0]
2271+
2272+
// Mine an empty block and make sure the anchor cpfp is still in the
2273+
// mempool hence the new block did not let the sweeper subsystem rbf
2274+
// this anchor sweep transaction (because of the small fee delta).
2275+
ht.MineEmptyBlocks(1)
2276+
cpfpHash1 := cpfpSweep1.TxHash()
2277+
ht.AssertTxInMempool(&cpfpHash1)
2278+
2279+
// Now Bump the fee rate again with a bigger starting fee rate of the
2280+
// fee function.
2281+
newFeeRate = closingFeeRate * 3
2282+
2283+
bumpFeeReq = &walletrpc.BumpForceCloseFeeRequest{
2284+
ChanPoint: chanPoint,
2285+
StartingFeerate: uint64(newFeeRate.FeePerVByte()),
2286+
// The budget needs to be high enough to pay for the fee because
2287+
// the anchor does not have an output value high enough to pay
2288+
// for itself.
2289+
Budget: uint64(cpfpBudget),
2290+
// We use a force param to create the sweeping tx immediately.
2291+
Immediate: true,
2292+
}
2293+
alice.RPC.BumpForceCloseFee(bumpFeeReq)
2294+
2295+
// Make sure the old sweep is not in the mempool anymore, which proofs
2296+
// that a new cpfp transaction replaced the old one paying higher fees.
2297+
ht.AssertTxNotInMempool(cpfpHash1)
2298+
2299+
// Identify the new cpfp transaction.
2300+
// Both anchor sweeps result from the same closing tx (the local
2301+
// commitment) hence proofing that the remote commitment transaction
2302+
// and its cpfp transaction is invalid and not accepted into the
2303+
// mempool.
2304+
txns = ht.GetNumTxsFromMempool(2)
2305+
ht.FindSweepingTxns(txns, 1, closingTx.TxHash())
2306+
2307+
// Mine both transactions, the closing tx and the anchor cpfp tx.
2308+
// This is needed to clean up the mempool.
2309+
ht.MineBlocksAndAssertNumTxes(1, 2)
2310+
}

lntest/rpc/wallet_kit.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,21 @@ func (h *HarnessRPC) BumpFee(
254254
return resp
255255
}
256256

257+
// BumpForceCloseFee makes a RPC call to the node's WalletKitClient and asserts.
258+
//
259+
//nolint:lll
260+
func (h *HarnessRPC) BumpForceCloseFee(
261+
req *walletrpc.BumpForceCloseFeeRequest) *walletrpc.BumpForceCloseFeeResponse {
262+
263+
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
264+
defer cancel()
265+
266+
resp, err := h.WalletKit.BumpForceCloseFee(ctxt, req)
267+
h.NoError(err, "BumpForceCloseFee")
268+
269+
return resp
270+
}
271+
257272
// ListAccounts makes a RPC call to the node's WalletKitClient and asserts.
258273
func (h *HarnessRPC) ListAccounts(
259274
req *walletrpc.ListAccountsRequest) *walletrpc.ListAccountsResponse {

0 commit comments

Comments
 (0)