mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-04-25 07:51:46 +02:00
In this commit, we test all the combinations of rbf close and taproot chans. This ensures that the downgrade logic works properly. Along the way we refactor the tests slightly, and also split them up, as running all the combos back to back mines more than 50 blocks in a test, which triggers an error in the itest sanity checks.
404 lines
13 KiB
Go
404 lines
13 KiB
Go
package itest
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
|
"github.com/lightningnetwork/lnd/lntest"
|
|
"github.com/lightningnetwork/lnd/lntest/node"
|
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type flagCombo struct {
|
|
isRbf bool
|
|
isTaproot bool
|
|
testName string
|
|
flags []string
|
|
commitType lnrpc.CommitmentType
|
|
private bool
|
|
}
|
|
|
|
// createFlagCombos generates a slice of flagCombo structs representing
|
|
// different test configurations for RBF and Taproot channels.
|
|
func createFlagCombos() []flagCombo {
|
|
var testCases []flagCombo
|
|
for _, isRbf := range []bool{true, false} {
|
|
for _, isTaproot := range []bool{true, false} {
|
|
flagCombo := flagCombo{
|
|
isRbf: isRbf,
|
|
isTaproot: isTaproot,
|
|
}
|
|
|
|
var flags []string
|
|
|
|
if isRbf {
|
|
flags = append(flags, node.CfgRbfClose...)
|
|
}
|
|
|
|
if isTaproot {
|
|
flags = append(flags, node.CfgSimpleTaproot...)
|
|
flagCombo.commitType = lnrpc.CommitmentType_SIMPLE_TAPROOT //nolint:ll
|
|
flagCombo.private = true
|
|
}
|
|
|
|
flagCombo.flags = flags
|
|
flagCombo.testName = fmt.Sprintf(
|
|
"is_rbf=%v is_taproot=%v", isRbf, isTaproot,
|
|
)
|
|
|
|
testCases = append(testCases, flagCombo)
|
|
}
|
|
}
|
|
|
|
return testCases
|
|
}
|
|
|
|
// testCoopCloseWithHtlcs tests whether we can successfully issue a coop close
|
|
// request while there are still active htlcs on the link. In this test, we
|
|
// will set up an HODL invoice to suspend settlement. Then we will attempt to
|
|
// close the channel which should appear as a noop for the time being. Then we
|
|
// will have the receiver settle the invoice and observe that the channel gets
|
|
// torn down after settlement. This test covers scenarios without node restarts.
|
|
func testCoopCloseWithHtlcs(ht *lntest.HarnessTest) {
|
|
testCases := createFlagCombos()
|
|
|
|
for _, testCase := range testCases {
|
|
testCase := testCase // Capture range variable.
|
|
ht.Run(testCase.testName, func(t *testing.T) {
|
|
tt := ht.Subtest(t)
|
|
|
|
alice := ht.NewNodeWithCoins("Alice", testCase.flags)
|
|
bob := ht.NewNodeWithCoins("bob", testCase.flags)
|
|
|
|
runCoopCloseWithHTLCsScenario(tt, alice, bob, testCase)
|
|
})
|
|
}
|
|
}
|
|
|
|
// testCoopCloseWithHtlcsWithRestart tests whether we can successfully issue a
|
|
// coop close request while there are still active htlcs on the link, even
|
|
// after a node restart. In this test, we will set up an HODL invoice to
|
|
// suspend settlement. Then we will attempt to close the channel, restart the
|
|
// nodes, and then have the receiver settle the invoice, observing that the
|
|
// channel gets torn down after settlement and restart.
|
|
func testCoopCloseWithHtlcsWithRestart(ht *lntest.HarnessTest) {
|
|
testCases := createFlagCombos()
|
|
|
|
for _, testCase := range testCases {
|
|
testCase := testCase // Capture range variable.
|
|
ht.Run(testCase.testName, func(t *testing.T) {
|
|
tt := ht.Subtest(t)
|
|
|
|
alice := ht.NewNodeWithCoins("Alice", testCase.flags)
|
|
bob := ht.NewNodeWithCoins("bob", testCase.flags)
|
|
|
|
runCoopCloseWithHTLCsWithRestartScenario(
|
|
tt, alice, bob, testCase,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// runCoopCloseWithHTLCsScenario tests the basic coop close scenario which
|
|
// occurs when one channel party initiates a channel shutdown while an HTLC is
|
|
// still pending on the channel.
|
|
func runCoopCloseWithHTLCsScenario(ht *lntest.HarnessTest, alice,
|
|
bob *node.HarnessNode, testCase flagCombo) {
|
|
|
|
ht.ConnectNodes(alice, bob)
|
|
|
|
// Here we set up a channel between Alice and Bob, beginning with a
|
|
// balance on Bob's side.
|
|
chanPoint := ht.OpenChannel(bob, alice, lntest.OpenChannelParams{
|
|
Amt: btcutil.Amount(1000000),
|
|
CommitmentType: testCase.commitType,
|
|
Private: testCase.private,
|
|
})
|
|
|
|
// Wait for Bob to understand that the channel is ready to use.
|
|
ht.AssertChannelInGraph(bob, chanPoint)
|
|
|
|
// Here we set things up so that Alice generates a HODL invoice so we
|
|
// can test whether the shutdown is deferred until the settlement of
|
|
// that invoice.
|
|
payAmt := btcutil.Amount(4)
|
|
var preimage lntypes.Preimage
|
|
copy(preimage[:], ht.Random32Bytes())
|
|
payHash := preimage.Hash()
|
|
|
|
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
|
|
Memo: "testing close",
|
|
Value: int64(payAmt),
|
|
Hash: payHash[:],
|
|
}
|
|
resp := alice.RPC.AddHoldInvoice(invoiceReq)
|
|
invoiceStream := alice.RPC.SubscribeSingleInvoice(payHash[:])
|
|
|
|
// Here we wait for the invoice to be open and payable.
|
|
ht.AssertInvoiceState(invoiceStream, lnrpc.Invoice_OPEN)
|
|
|
|
// Now that the invoice is ready to be paid, let's have Bob open an
|
|
// HTLC for it.
|
|
req := &routerrpc.SendPaymentRequest{
|
|
PaymentRequest: resp.PaymentRequest,
|
|
FeeLimitSat: 1000000,
|
|
}
|
|
ht.SendPaymentAndAssertStatus(bob, req, lnrpc.Payment_IN_FLIGHT)
|
|
ht.AssertNumActiveHtlcs(bob, 1)
|
|
|
|
// Assert at this point that the HTLC is open but not yet settled.
|
|
ht.AssertInvoiceState(invoiceStream, lnrpc.Invoice_ACCEPTED)
|
|
|
|
// Have alice attempt to close the channel.
|
|
closeClient := alice.RPC.CloseChannel(&lnrpc.CloseChannelRequest{
|
|
ChannelPoint: chanPoint,
|
|
NoWait: true,
|
|
TargetConf: 6,
|
|
})
|
|
ht.AssertChannelInactive(bob, chanPoint)
|
|
|
|
// Now that the channel is inactive we can be certain that the deferred
|
|
// closure is set up. Let's settle the invoice.
|
|
alice.RPC.SettleInvoice(preimage[:])
|
|
|
|
// Pull the instant update off the wire and make sure the number of
|
|
// pending HTLCs is as expected.
|
|
update, err := closeClient.Recv()
|
|
require.NoError(ht, err)
|
|
closeInstant := update.GetCloseInstant()
|
|
require.NotNil(ht, closeInstant)
|
|
require.Equal(ht, closeInstant.NumPendingHtlcs, int32(1))
|
|
|
|
// Wait for the next channel closure update. Now that we have settled
|
|
// the only HTLC this should be imminent.
|
|
update, err = closeClient.Recv()
|
|
require.NoError(ht, err)
|
|
|
|
// This next update should be a GetClosePending as it should be the
|
|
// negotiation of the coop close tx.
|
|
closePending := update.GetClosePending()
|
|
require.NotNil(ht, closePending)
|
|
|
|
// Convert the txid we get from the PendingUpdate to a Hash so we can
|
|
// wait for it to be mined.
|
|
var closeTxid chainhash.Hash
|
|
require.NoError(
|
|
ht, closeTxid.SetBytes(closePending.Txid),
|
|
"invalid closing txid",
|
|
)
|
|
|
|
// Wait for the close tx to be in the Mempool.
|
|
ht.AssertTxInMempool(closeTxid)
|
|
|
|
// Wait for it to get mined and finish tearing down.
|
|
ht.AssertStreamChannelCoopClosed(alice, chanPoint, false, closeClient)
|
|
}
|
|
|
|
// runCoopCloseWithHTLCsWithRestartScenario also tests the coop close flow when
|
|
// an HTLC is still pending on the channel but this time it ensures that the
|
|
// shutdown process continues as expected even if a channel re-establish
|
|
// happens after one party has already initiated the shutdown.
|
|
func runCoopCloseWithHTLCsWithRestartScenario(ht *lntest.HarnessTest, alice,
|
|
bob *node.HarnessNode, testCase flagCombo) {
|
|
|
|
ht.ConnectNodes(alice, bob)
|
|
|
|
// Open a channel between Alice and Bob with the balance split equally.
|
|
// We do this to ensure that the close transaction will have 2 outputs
|
|
// so that we can assert that the correct delivery address gets used by
|
|
// the channel close initiator.
|
|
chanPoint := ht.OpenChannel(bob, alice, lntest.OpenChannelParams{
|
|
Amt: btcutil.Amount(1000000),
|
|
PushAmt: btcutil.Amount(1000000 / 2),
|
|
CommitmentType: testCase.commitType,
|
|
Private: testCase.private,
|
|
})
|
|
|
|
// Wait for Bob to understand that the channel is ready to use.
|
|
ht.AssertChannelInGraph(bob, chanPoint)
|
|
|
|
// Set up a HODL invoice so that we can be sure that an HTLC is pending
|
|
// on the channel at the time that shutdown is requested.
|
|
var preimage lntypes.Preimage
|
|
copy(preimage[:], ht.Random32Bytes())
|
|
payHash := preimage.Hash()
|
|
|
|
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
|
|
Memo: "testing close",
|
|
Value: 400,
|
|
Hash: payHash[:],
|
|
}
|
|
resp := alice.RPC.AddHoldInvoice(invoiceReq)
|
|
invoiceStream := alice.RPC.SubscribeSingleInvoice(payHash[:])
|
|
|
|
// Wait for the invoice to be ready and payable.
|
|
ht.AssertInvoiceState(invoiceStream, lnrpc.Invoice_OPEN)
|
|
|
|
// Now that the invoice is ready to be paid, let's have Bob open an HTLC
|
|
// for it.
|
|
req := &routerrpc.SendPaymentRequest{
|
|
PaymentRequest: resp.PaymentRequest,
|
|
FeeLimitSat: 1000000,
|
|
}
|
|
ht.SendPaymentAndAssertStatus(bob, req, lnrpc.Payment_IN_FLIGHT)
|
|
ht.AssertNumActiveHtlcs(bob, 1)
|
|
|
|
// Assert at this point that the HTLC is open but not yet settled.
|
|
ht.AssertInvoiceState(invoiceStream, lnrpc.Invoice_ACCEPTED)
|
|
|
|
// We will now let Alice initiate the closure of the channel. We will
|
|
// also let her specify a specific delivery address to be used since we
|
|
// want to test that this same address is used in the Shutdown message
|
|
// on reconnection.
|
|
newAddr := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
|
|
Type: AddrTypeWitnessPubkeyHash,
|
|
})
|
|
|
|
_ = alice.RPC.CloseChannel(&lnrpc.CloseChannelRequest{
|
|
ChannelPoint: chanPoint,
|
|
NoWait: true,
|
|
DeliveryAddress: newAddr.Address,
|
|
TargetConf: 6,
|
|
})
|
|
|
|
// Assert that both nodes see the channel as waiting for close.
|
|
ht.AssertChannelInactive(bob, chanPoint)
|
|
ht.AssertChannelInactive(alice, chanPoint)
|
|
|
|
// Shutdown both Alice and Bob.
|
|
restartAlice := ht.SuspendNode(alice)
|
|
restartBob := ht.SuspendNode(bob)
|
|
|
|
// Once shutdown, restart and connect them.
|
|
require.NoError(ht, restartAlice())
|
|
require.NoError(ht, restartBob())
|
|
ht.EnsureConnected(alice, bob)
|
|
|
|
// Show that both nodes still see the channel as waiting for close after
|
|
// the restart.
|
|
ht.AssertChannelInactive(bob, chanPoint)
|
|
ht.AssertChannelInactive(alice, chanPoint)
|
|
|
|
// Settle the invoice.
|
|
alice.RPC.SettleInvoice(preimage[:])
|
|
|
|
// Wait for the channel to appear in the waiting closed list.
|
|
err := wait.Predicate(func() bool {
|
|
pendingChansResp := alice.RPC.PendingChannels()
|
|
waitingClosed := pendingChansResp.WaitingCloseChannels
|
|
|
|
return len(waitingClosed) == 1
|
|
}, defaultTimeout)
|
|
require.NoError(ht, err)
|
|
|
|
// Wait for the close tx to be in the Mempool and then mine 6 blocks to
|
|
// confirm the close.
|
|
closingTx := ht.AssertClosingTxInMempool(
|
|
chanPoint, lnrpc.CommitmentType_LEGACY,
|
|
)
|
|
ht.MineBlocksAndAssertNumTxes(6, 1)
|
|
|
|
// Finally, we inspect the closing transaction here to show that the
|
|
// delivery address that Alice specified in her original close request
|
|
// is the one that ended up being used in the final closing transaction.
|
|
tx := alice.RPC.GetTransaction(&walletrpc.GetTransactionRequest{
|
|
Txid: closingTx.TxHash().String(),
|
|
})
|
|
require.Len(ht, tx.OutputDetails, 2)
|
|
|
|
// Find Alice's output in the coop-close transaction.
|
|
var outputDetail *lnrpc.OutputDetail
|
|
for _, output := range tx.OutputDetails {
|
|
if output.IsOurAddress {
|
|
outputDetail = output
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(ht, outputDetail)
|
|
|
|
// Show that the address used is the one she requested.
|
|
require.Equal(ht, outputDetail.Address, newAddr.Address)
|
|
}
|
|
|
|
// testCoopCloseExceedsMaxFee tests that we fail the coop close process if
|
|
// the max fee rate exceeds the expected fee rate for the initial closing fee
|
|
// proposal.
|
|
func testCoopCloseExceedsMaxFee(ht *lntest.HarnessTest) {
|
|
const chanAmt = 1000000
|
|
|
|
// Create a channel Alice->Bob.
|
|
chanPoints, nodes := ht.CreateSimpleNetwork(
|
|
[][]string{nil, nil}, lntest.OpenChannelParams{
|
|
Amt: chanAmt,
|
|
},
|
|
)
|
|
|
|
alice, _ := nodes[0], nodes[1]
|
|
chanPoint := chanPoints[0]
|
|
|
|
// Set the fee estimate for one block to 10 sat/vbyte.
|
|
ht.SetFeeEstimateWithConf(chainfee.SatPerVByte(10).FeePerKWeight(), 1)
|
|
|
|
// Have alice attempt to close the channel. We expect the initial fee
|
|
// rate to exceed the max fee rate for the closing transaction so we
|
|
// fail the closing process.
|
|
req := &lnrpc.CloseChannelRequest{
|
|
ChannelPoint: chanPoint,
|
|
MaxFeePerVbyte: 5,
|
|
NoWait: true,
|
|
TargetConf: 1,
|
|
}
|
|
err := ht.CloseChannelAssertErr(alice, req)
|
|
require.Contains(ht, err.Error(), "max_fee_per_vbyte (1250 sat/kw) is "+
|
|
"less than the required fee rate (2500 sat/kw)")
|
|
|
|
// Now close the channel with a appropriate max fee rate.
|
|
closeClient := alice.RPC.CloseChannel(&lnrpc.CloseChannelRequest{
|
|
ChannelPoint: chanPoint,
|
|
NoWait: true,
|
|
TargetConf: 1,
|
|
MaxFeePerVbyte: 10,
|
|
})
|
|
|
|
// Pull the instant update off the wire to clear the path for the
|
|
// close pending update. Moreover confirm that there are no pending
|
|
// HTLCs on the channel.
|
|
update, err := closeClient.Recv()
|
|
require.NoError(ht, err)
|
|
closeInstant := update.GetCloseInstant()
|
|
require.NotNil(ht, closeInstant)
|
|
require.Equal(ht, closeInstant.NumPendingHtlcs, int32(0))
|
|
|
|
// Wait for the channel to be closed.
|
|
update, err = closeClient.Recv()
|
|
require.NoError(ht, err)
|
|
|
|
// This next update should be a GetClosePending as it should be the
|
|
// negotiation of the coop close tx.
|
|
closePending := update.GetClosePending()
|
|
require.NotNil(ht, closePending)
|
|
|
|
// Convert the txid we get from the PendingUpdate to a Hash so we can
|
|
// wait for it to be mined.
|
|
var closeTxid chainhash.Hash
|
|
require.NoError(
|
|
ht, closeTxid.SetBytes(closePending.Txid),
|
|
"invalid closing txid",
|
|
)
|
|
|
|
// Wait for the close tx to be in the Mempool.
|
|
ht.AssertTxInMempool(closeTxid)
|
|
|
|
// Wait for it to get mined and finish tearing down.
|
|
ht.AssertStreamChannelCoopClosed(alice, chanPoint, false, closeClient)
|
|
}
|