mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-26 01:33:02 +01:00
itest: add new RBF coop close itest
The itest has both sides try to close multiple times, each time with increasing fee rates. We also test the reconnection case, bad RBF updates, and instances where the local party can't actually pay for fees.
This commit is contained in:
parent
3a18bf088c
commit
971ac5a14d
@ -678,6 +678,10 @@ var allTestCases = []*lntest.TestCase{
|
||||
Name: "access perm",
|
||||
TestFunc: testAccessPerm,
|
||||
},
|
||||
{
|
||||
Name: "rbf coop close",
|
||||
TestFunc: testCoopCloseRbf,
|
||||
},
|
||||
}
|
||||
|
||||
// appendPrefixed is used to add a prefix to each test name in the subtests
|
||||
|
133
itest/lnd_coop_close_rbf_test.go
Normal file
133
itest/lnd_coop_close_rbf_test.go
Normal file
@ -0,0 +1,133 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testCoopCloseRbf(ht *lntest.HarnessTest) {
|
||||
rbfCoopFlags := []string{"--protocol.rbf-coop-close"}
|
||||
|
||||
// Set the fee estimate to 1sat/vbyte. This ensures that our manually
|
||||
// initiated RBF attempts will always be successful.
|
||||
ht.SetFeeEstimate(250)
|
||||
ht.SetFeeEstimateWithConf(250, 6)
|
||||
|
||||
// To kick things off, we'll create two new nodes, then fund them with
|
||||
// enough coins to make a 50/50 channel.
|
||||
cfgs := [][]string{rbfCoopFlags, rbfCoopFlags}
|
||||
params := lntest.OpenChannelParams{
|
||||
Amt: btcutil.Amount(1000000),
|
||||
PushAmt: btcutil.Amount(1000000 / 2),
|
||||
}
|
||||
chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params)
|
||||
alice, bob := nodes[0], nodes[1]
|
||||
chanPoint := chanPoints[0]
|
||||
|
||||
// Now that both sides are active with a funded channel, we can kick
|
||||
// off the test.
|
||||
//
|
||||
// To start, we'll have Alice try to close the channel, with a fee rate
|
||||
// of 5 sat/byte.
|
||||
aliceFeeRate := chainfee.SatPerVByte(5)
|
||||
aliceCloseStream, aliceCloseUpdate := ht.CloseChannelAssertPending(
|
||||
alice, chanPoint, false,
|
||||
lntest.WithCoopCloseFeeRate(aliceFeeRate),
|
||||
lntest.WithLocalTxNotify(),
|
||||
)
|
||||
|
||||
// Confirm that this new update was at 5 sat/vb.
|
||||
alicePendingUpdate := aliceCloseUpdate.GetClosePending()
|
||||
require.NotNil(ht, aliceCloseUpdate)
|
||||
require.Equal(
|
||||
ht, int64(aliceFeeRate), alicePendingUpdate.FeePerVbyte,
|
||||
)
|
||||
require.True(ht, alicePendingUpdate.LocalCloseTx)
|
||||
|
||||
// Now, we'll have Bob attempt to RBF the close transaction with a
|
||||
// higher fee rate, double that of Alice's.
|
||||
bobFeeRate := aliceFeeRate * 2
|
||||
bobCloseStream, bobCloseUpdate := ht.CloseChannelAssertPending(
|
||||
bob, chanPoint, false, lntest.WithCoopCloseFeeRate(bobFeeRate),
|
||||
lntest.WithLocalTxNotify(),
|
||||
)
|
||||
|
||||
// Confirm that this new update was at 10 sat/vb.
|
||||
bobPendingUpdate := bobCloseUpdate.GetClosePending()
|
||||
require.NotNil(ht, bobCloseUpdate)
|
||||
require.Equal(ht, bobPendingUpdate.FeePerVbyte, int64(bobFeeRate))
|
||||
require.True(ht, bobPendingUpdate.LocalCloseTx)
|
||||
|
||||
var err error
|
||||
|
||||
// Alice should've also received a similar update that Bob has
|
||||
// increased the closing fee rate to 10 sat/vb with his settled funds.
|
||||
aliceCloseUpdate, err = ht.ReceiveCloseChannelUpdate(aliceCloseStream)
|
||||
require.NoError(ht, err)
|
||||
alicePendingUpdate = aliceCloseUpdate.GetClosePending()
|
||||
require.NotNil(ht, aliceCloseUpdate)
|
||||
require.Equal(ht, alicePendingUpdate.FeePerVbyte, int64(bobFeeRate))
|
||||
require.False(ht, alicePendingUpdate.LocalCloseTx)
|
||||
|
||||
// We'll now attempt to make a fee update that increases Alice's fee
|
||||
// rate by 6 sat/vb, which should be rejected as it is too small of an
|
||||
// increase for the RBF rules. The RPC API however will return the new
|
||||
// fee. We'll skip the mempool check here as it won't make it in.
|
||||
aliceRejectedFeeRate := aliceFeeRate + 1
|
||||
_, aliceCloseUpdate = ht.CloseChannelAssertPending(
|
||||
alice, chanPoint, false,
|
||||
lntest.WithCoopCloseFeeRate(aliceRejectedFeeRate),
|
||||
lntest.WithLocalTxNotify(), lntest.WithSkipMempoolCheck(),
|
||||
)
|
||||
alicePendingUpdate = aliceCloseUpdate.GetClosePending()
|
||||
require.NotNil(ht, aliceCloseUpdate)
|
||||
require.Equal(
|
||||
ht, alicePendingUpdate.FeePerVbyte,
|
||||
int64(aliceRejectedFeeRate),
|
||||
)
|
||||
require.True(ht, alicePendingUpdate.LocalCloseTx)
|
||||
|
||||
_, err = ht.ReceiveCloseChannelUpdate(bobCloseStream)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// We'll now attempt a fee update that we can't actually pay for. This
|
||||
// will actually show up as an error to the remote party.
|
||||
aliceRejectedFeeRate = 100_000
|
||||
_, _ = ht.CloseChannelAssertPending(
|
||||
alice, chanPoint, false,
|
||||
lntest.WithCoopCloseFeeRate(aliceRejectedFeeRate),
|
||||
lntest.WithLocalTxNotify(),
|
||||
lntest.WithExpectedErrString("cannot pay for fee"),
|
||||
)
|
||||
|
||||
// At this point, we'll have Alice+Bob reconnect so we can ensure that
|
||||
// we can continue to do RBF bumps even after a reconnection.
|
||||
ht.DisconnectNodes(alice, bob)
|
||||
ht.ConnectNodes(alice, bob)
|
||||
|
||||
// Next, we'll have Alice double that fee rate again to 20 sat/vb.
|
||||
aliceFeeRate = bobFeeRate * 2
|
||||
aliceCloseStream, aliceCloseUpdate = ht.CloseChannelAssertPending(
|
||||
alice, chanPoint, false,
|
||||
lntest.WithCoopCloseFeeRate(aliceFeeRate),
|
||||
lntest.WithLocalTxNotify(),
|
||||
)
|
||||
|
||||
alicePendingUpdate = aliceCloseUpdate.GetClosePending()
|
||||
require.NotNil(ht, aliceCloseUpdate)
|
||||
require.Equal(
|
||||
ht, alicePendingUpdate.FeePerVbyte, int64(aliceFeeRate),
|
||||
)
|
||||
require.True(ht, alicePendingUpdate.LocalCloseTx)
|
||||
|
||||
// To conclude, we'll mine a block which should now confirm Alice's
|
||||
// version of the coop close transaction.
|
||||
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
|
||||
// Both Alice and Bob should trigger a final close update to signal the
|
||||
// closing transaction has confirmed.
|
||||
aliceClosingTxid := ht.WaitForChannelCloseEvent(aliceCloseStream)
|
||||
ht.AssertTxInBlock(block, aliceClosingTxid)
|
||||
}
|
@ -66,7 +66,6 @@ func testCoopCloseWithHtlcs(ht *lntest.HarnessTest) {
|
||||
// channel party initiates a channel shutdown while an HTLC is still pending on
|
||||
// the channel.
|
||||
func coopCloseWithHTLCs(ht *lntest.HarnessTest, alice, bob *node.HarnessNode) {
|
||||
|
||||
ht.ConnectNodes(alice, bob)
|
||||
|
||||
// Here we set up a channel between Alice and Bob, beginning with a
|
||||
|
@ -1979,6 +1979,7 @@ func testBumpForceCloseFee(ht *lntest.HarnessTest) {
|
||||
closeTxid, err := chainhash.NewHash(pendingClose.Txid)
|
||||
require.NoError(ht, err)
|
||||
closingTx := ht.AssertTxInMempool(*closeTxid)
|
||||
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
|
||||
|
@ -1211,6 +1211,19 @@ func (h *HarnessTest) OpenChannelAssertErr(srcNode, destNode *node.HarnessNode,
|
||||
// closeChannelOpts holds the options for closing a channel.
|
||||
type closeChannelOpts struct {
|
||||
feeRate fn.Option[chainfee.SatPerVByte]
|
||||
|
||||
// localTxOnly is a boolean indicating if we should only attempt to
|
||||
// consume close pending notifications for the local transaction.
|
||||
localTxOnly bool
|
||||
|
||||
// skipMempoolCheck is a boolean indicating if we should skip the normal
|
||||
// mempool check after a coop close.
|
||||
skipMempoolCheck bool
|
||||
|
||||
// errString is an expected error. If this is non-blank, then we'll
|
||||
// assert that the coop close wasn't possible, and returns an error that
|
||||
// contains this err string.
|
||||
errString string
|
||||
}
|
||||
|
||||
// CloseChanOpt is a functional option to modify the way we close a channel.
|
||||
@ -1224,6 +1237,32 @@ func WithCoopCloseFeeRate(rate chainfee.SatPerVByte) CloseChanOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithLocalTxNotify is a functional option to indicate that we should only
|
||||
// notify for the local txn. This is useful for the RBF coop close type, as
|
||||
// it'll notify for both local and remote txns.
|
||||
func WithLocalTxNotify() CloseChanOpt {
|
||||
return func(o *closeChannelOpts) {
|
||||
o.localTxOnly = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithSkipMempoolCheck is a functional option to indicate that we should skip
|
||||
// the mempool check. This can be used when a coop close iteration may not
|
||||
// result in a newly broadcast transaction.
|
||||
func WithSkipMempoolCheck() CloseChanOpt {
|
||||
return func(o *closeChannelOpts) {
|
||||
o.skipMempoolCheck = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithExpectedErrString is a functional option that can be used to assert that
|
||||
// an error occurs during the coop close process.
|
||||
func WithExpectedErrString(errString string) CloseChanOpt {
|
||||
return func(o *closeChannelOpts) {
|
||||
o.errString = errString
|
||||
}
|
||||
}
|
||||
|
||||
// defaultCloseOpts returns the set of default close options.
|
||||
func defaultCloseOpts() *closeChannelOpts {
|
||||
return &closeChannelOpts{}
|
||||
@ -1267,23 +1306,49 @@ func (h *HarnessTest) CloseChannelAssertPending(hn *node.HarnessNode,
|
||||
_, err = h.ReceiveCloseChannelUpdate(stream)
|
||||
require.NoError(h, err, "close channel update got error: %v", err)
|
||||
|
||||
event, err = h.ReceiveCloseChannelUpdate(stream)
|
||||
if err != nil {
|
||||
h.Logf("Test: %s, close channel got error: %v",
|
||||
h.manager.currentTestCase, err)
|
||||
var closeTxid *chainhash.Hash
|
||||
for {
|
||||
event, err = h.ReceiveCloseChannelUpdate(stream)
|
||||
if err != nil {
|
||||
h.Logf("Test: %s, close channel got error: %v",
|
||||
h.manager.currentTestCase, err)
|
||||
}
|
||||
if err != nil && closeOpts.errString == "" {
|
||||
require.NoError(h, err, "retry closing channel failed")
|
||||
} else if err != nil && closeOpts.errString != "" {
|
||||
require.ErrorContains(h, err, closeOpts.errString)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pendingClose, ok := event.Update.(*lnrpc.CloseStatusUpdate_ClosePending) //nolint:ll
|
||||
require.Truef(h, ok, "expected channel close "+
|
||||
"update, instead got %v", pendingClose)
|
||||
|
||||
if !pendingClose.ClosePending.LocalCloseTx &&
|
||||
closeOpts.localTxOnly {
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
notifyRate := pendingClose.ClosePending.FeePerVbyte
|
||||
if closeOpts.localTxOnly &&
|
||||
notifyRate != int64(closeReq.SatPerVbyte) {
|
||||
continue
|
||||
}
|
||||
|
||||
closeTxid, err = chainhash.NewHash(
|
||||
pendingClose.ClosePending.Txid,
|
||||
)
|
||||
require.NoErrorf(h, err, "unable to decode closeTxid: %v",
|
||||
pendingClose.ClosePending.Txid)
|
||||
|
||||
break
|
||||
}
|
||||
require.NoError(h, err, "retry closing channel failed")
|
||||
|
||||
pendingClose, ok := event.Update.(*lnrpc.CloseStatusUpdate_ClosePending)
|
||||
require.Truef(h, ok, "expected channel close update, instead got %v",
|
||||
pendingClose)
|
||||
|
||||
closeTxid, err := chainhash.NewHash(pendingClose.ClosePending.Txid)
|
||||
require.NoErrorf(h, err, "unable to decode closeTxid: %v",
|
||||
pendingClose.ClosePending.Txid)
|
||||
|
||||
// Assert the closing tx is in the mempool.
|
||||
h.miner.AssertTxInMempool(*closeTxid)
|
||||
if !closeOpts.skipMempoolCheck {
|
||||
// Assert the closing tx is in the mempool.
|
||||
h.miner.AssertTxInMempool(*closeTxid)
|
||||
}
|
||||
|
||||
return stream, event
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user