itest: add testSendToRouteFailHTLCTimeout to check SendToRouteV2

This commit is contained in:
yyforyongyu 2023-11-13 15:50:29 +08:00 committed by yyforyongyu
parent 21112cfdf8
commit d28d5d7a47
No known key found for this signature in database
GPG Key ID: 9BCD95C4FF296868
2 changed files with 221 additions and 0 deletions

View File

@ -662,4 +662,8 @@ var allTestCases = []*lntest.TestCase{
Name: "payment succeeded htlc remote swept",
TestFunc: testPaymentSucceededHTLCRemoteSwept,
},
{
Name: "send to route failed htlc timeout",
TestFunc: testSendToRouteFailHTLCTimeout,
},
}

View File

@ -19,6 +19,7 @@ import (
"github.com/lightningnetwork/lnd/lntest/rpc"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
@ -1211,3 +1212,219 @@ func sendPaymentInterceptAndCancel(ht *lntest.HarnessTest,
// Cancel the context, which will disconnect the above interceptor.
cancelInterceptor()
}
// testSendToRouteFailHTLCTimeout is similar to
// testPaymentFailedHTLCLocalSwept. The only difference is the `SendPayment` is
// replaced with `SendToRouteV2`. It checks that when an outgoing HTLC is timed
// out and claimed onchain via the timeout path, the payment will be marked as
// failed. This test creates a topology from Alice -> Bob, and let Alice send
// payments to Bob. Bob then goes offline, such that Alice's outgoing HTLC will
// time out. Alice will also be restarted to make sure resumed payments are
// also marked as failed.
func testSendToRouteFailHTLCTimeout(ht *lntest.HarnessTest) {
success := ht.Run("fail payment", func(t *testing.T) {
st := ht.Subtest(t)
runSendToRouteFailHTLCTimeout(st, false)
})
if !success {
return
}
ht.Run("fail resumed payment", func(t *testing.T) {
st := ht.Subtest(t)
runTestPaymentHTLCTimeout(st, true)
})
}
// runSendToRouteFailHTLCTimeout is the helper function that actually runs the
// testSendToRouteFailHTLCTimeout.
func runSendToRouteFailHTLCTimeout(ht *lntest.HarnessTest, restartAlice bool) {
// Set the feerate to be 10 sat/vb.
ht.SetFeeEstimate(2500)
// Open a channel with 100k satoshis between Alice and Bob with Alice
// being the sole funder of the channel.
chanAmt := btcutil.Amount(100_000)
openChannelParams := lntest.OpenChannelParams{
Amt: chanAmt,
}
// Create a two hop network: Alice -> Bob.
chanPoints, nodes := createSimpleNetwork(ht, nil, 2, openChannelParams)
chanPoint := chanPoints[0]
alice, bob := nodes[0], nodes[1]
// We now create two payments, one above dust and the other below dust,
// and we should see different behavior in terms of when the payment
// will be marked as failed due to the HTLC timeout.
//
// First, create random preimages.
preimage := ht.RandomPreimage()
dustPreimage := ht.RandomPreimage()
// Get the preimage hashes.
payHash := preimage.Hash()
dustPayHash := dustPreimage.Hash()
// Create an hold invoice for Bob which expects a payment of 10k
// satoshis from Alice.
const paymentAmt = 20_000
req := &invoicesrpc.AddHoldInvoiceRequest{
Value: paymentAmt,
Hash: payHash[:],
// Use a small CLTV value so we can mine fewer blocks.
CltvExpiry: finalCltvDelta,
}
invoice := bob.RPC.AddHoldInvoice(req)
// Create another hold invoice for Bob which expects a payment of 1k
// satoshis from Alice.
const dustAmt = 1000
req = &invoicesrpc.AddHoldInvoiceRequest{
Value: dustAmt,
Hash: dustPayHash[:],
// Use a small CLTV value so we can mine fewer blocks.
CltvExpiry: finalCltvDelta,
}
dustInvoice := bob.RPC.AddHoldInvoice(req)
// Construct a route to send the non-dust payment.
go func() {
// Query the route to send the payment.
routesReq := &lnrpc.QueryRoutesRequest{
PubKey: bob.PubKeyStr,
Amt: paymentAmt,
FinalCltvDelta: finalCltvDelta,
}
routes := alice.RPC.QueryRoutes(routesReq)
require.Len(ht, routes.Routes, 1)
route := routes.Routes[0]
require.Len(ht, route.Hops, 1)
// Modify the hop to include MPP info.
route.Hops[0].MppRecord = &lnrpc.MPPRecord{
PaymentAddr: invoice.PaymentAddr,
TotalAmtMsat: int64(
lnwire.NewMSatFromSatoshis(paymentAmt),
),
}
// Send the payment with the modified value.
req := &routerrpc.SendToRouteRequest{
PaymentHash: payHash[:],
Route: route,
}
// Send the payment and expect no error.
attempt := alice.RPC.SendToRouteV2(req)
require.Equal(ht, lnrpc.HTLCAttempt_FAILED, attempt.Status)
}()
// Check that the payment is in-flight.
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_IN_FLIGHT)
// Construct a route to send the dust payment.
go func() {
// Query the route to send the payment.
routesReq := &lnrpc.QueryRoutesRequest{
PubKey: bob.PubKeyStr,
Amt: dustAmt,
FinalCltvDelta: finalCltvDelta,
}
routes := alice.RPC.QueryRoutes(routesReq)
require.Len(ht, routes.Routes, 1)
route := routes.Routes[0]
require.Len(ht, route.Hops, 1)
// Modify the hop to include MPP info.
route.Hops[0].MppRecord = &lnrpc.MPPRecord{
PaymentAddr: dustInvoice.PaymentAddr,
TotalAmtMsat: int64(
lnwire.NewMSatFromSatoshis(dustAmt),
),
}
// Send the payment with the modified value.
req := &routerrpc.SendToRouteRequest{
PaymentHash: dustPayHash[:],
Route: route,
}
// Send the payment and expect no error.
attempt := alice.RPC.SendToRouteV2(req)
require.Equal(ht, lnrpc.HTLCAttempt_FAILED, attempt.Status)
}()
// Check that the dust payment is in-flight.
ht.AssertPaymentStatus(alice, dustPreimage, lnrpc.Payment_IN_FLIGHT)
// Bob should have two incoming HTLC.
ht.AssertIncomingHTLCActive(bob, chanPoint, payHash[:])
ht.AssertIncomingHTLCActive(bob, chanPoint, dustPayHash[:])
// Alice should have two outgoing HTLCs.
ht.AssertOutgoingHTLCActive(alice, chanPoint, payHash[:])
ht.AssertOutgoingHTLCActive(alice, chanPoint, dustPayHash[:])
// Let Bob go offline.
ht.Shutdown(bob)
// We'll now mine enough blocks to trigger Alice to broadcast her
// commitment transaction due to the fact that the HTLC is about to
// timeout. With the default outgoing broadcast delta of zero, this
// will be the same height as the htlc expiry height.
numBlocks := padCLTV(
uint32(req.CltvExpiry - lncfg.DefaultOutgoingBroadcastDelta),
)
ht.MineEmptyBlocks(int(numBlocks - 1))
// Restart Alice if requested.
if restartAlice {
// Restart Alice to test the resumed payment is canceled.
ht.RestartNode(alice)
}
// We now subscribe to the payment status.
payStream := alice.RPC.TrackPaymentV2(payHash[:])
dustPayStream := alice.RPC.TrackPaymentV2(dustPayHash[:])
// Mine a block to confirm Alice's closing transaction.
ht.MineBlocksAndAssertNumTxes(1, 1)
// Now the channel is closed, we expect different behaviors based on
// whether the HTLC is a dust. For dust payment, it should be failed
// now as the HTLC won't go onchain. For non-dust payment, it should
// still be inflight. It won't be marked as failed unless the outgoing
// HTLC is resolved onchain.
//
// Check that the dust payment is failed in both the stream and DB.
ht.AssertPaymentStatus(alice, dustPreimage, lnrpc.Payment_FAILED)
ht.AssertPaymentStatusFromStream(dustPayStream, lnrpc.Payment_FAILED)
// Check that the non-dust payment is still in-flight.
//
// NOTE: we don't check the payment status from the stream here as
// there's no new status being sent.
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_IN_FLIGHT)
// We now have two possible cases for the non-dust payment:
// - Bob stays offline, and Alice will sweep her outgoing HTLC, which
// makes the payment failed.
// - Bob comes back online, and claims the HTLC on Alice's commitment
// via direct preimage spend, hence racing against Alice onchain. If
// he succeeds, Alice should mark the payment as succeeded.
//
// TODO(yy): test the second case once we have the RPC to clean
// mempool.
// Since Alice's force close transaction has been confirmed, she should
// sweep her outgoing HTLC in next block.
ht.MineBlocksAndAssertNumTxes(2, 1)
// We expect the non-dust payment to marked as failed in Alice's
// database and also from her stream.
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_FAILED)
ht.AssertPaymentStatusFromStream(payStream, lnrpc.Payment_FAILED)
}