lnd/itest/lnd_multi-hop-error-propagation_test.go
ziggie a1678fa9b7
itest: multi part payments test fix.
Because we need to account for an addtional fee buffer we need to
increase channel capacities for the multi payment tests.
2024-01-08 16:47:47 +01:00

375 lines
12 KiB
Go

package itest
import (
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
func testHtlcErrorPropagation(ht *lntest.HarnessTest) {
// In this test we wish to exercise the daemon's correct parsing,
// handling, and propagation of errors that occur while processing a
// multi-hop payment.
const chanAmt = funding.MaxBtcFundingAmount
alice, bob := ht.Alice, ht.Bob
// Since we'd like to test some multi-hop failure scenarios, we'll
// introduce another node into our test network: Carol.
carol := ht.NewNode("Carol", nil)
ht.ConnectNodes(bob, carol)
// Before we start sending payments, subscribe to htlc events for each
// node.
aliceEvents := alice.RPC.SubscribeHtlcEvents()
bobEvents := bob.RPC.SubscribeHtlcEvents()
carolEvents := carol.RPC.SubscribeHtlcEvents()
// Once subscribed, the first event will be UNKNOWN.
ht.AssertHtlcEventType(aliceEvents, routerrpc.HtlcEvent_UNKNOWN)
ht.AssertHtlcEventType(bobEvents, routerrpc.HtlcEvent_UNKNOWN)
ht.AssertHtlcEventType(carolEvents, routerrpc.HtlcEvent_UNKNOWN)
// First establish a channel with a capacity of 0.5 BTC between Alice
// and Bob.
chanPointAlice := ht.OpenChannel(
alice, bob,
lntest.OpenChannelParams{Amt: chanAmt},
)
// Next, we'll create a connection from Bob to Carol, and open a
// channel between them so we have the topology: Alice -> Bob -> Carol.
// The channel created will be of lower capacity that the one created
// above.
chanPointBob := ht.OpenChannel(
bob, carol, lntest.OpenChannelParams{Amt: chanAmt},
)
// Ensure that Alice has Carol in her routing table before proceeding.
ht.AssertTopologyChannelOpen(alice, chanPointBob)
cType := ht.GetChannelCommitType(alice, chanPointAlice)
commitFee := lntest.CalcStaticFee(cType, 0)
assertBaseBalance := func() {
// Alice has opened a channel with Bob with zero push amount,
// so it's remote balance is zero.
expBalanceAlice := &lnrpc.ChannelBalanceResponse{
LocalBalance: &lnrpc.Amount{
Sat: uint64(chanAmt - commitFee),
Msat: uint64(lnwire.NewMSatFromSatoshis(
chanAmt - commitFee,
)),
},
RemoteBalance: &lnrpc.Amount{},
UnsettledLocalBalance: &lnrpc.Amount{},
UnsettledRemoteBalance: &lnrpc.Amount{},
PendingOpenLocalBalance: &lnrpc.Amount{},
PendingOpenRemoteBalance: &lnrpc.Amount{},
// Deprecated fields.
Balance: int64(chanAmt - commitFee),
}
ht.AssertChannelBalanceResp(alice, expBalanceAlice)
// Bob has a channel with Alice and another with Carol, so it's
// local and remote balances are both chanAmt - commitFee.
expBalanceBob := &lnrpc.ChannelBalanceResponse{
LocalBalance: &lnrpc.Amount{
Sat: uint64(chanAmt - commitFee),
Msat: uint64(lnwire.NewMSatFromSatoshis(
chanAmt - commitFee,
)),
},
RemoteBalance: &lnrpc.Amount{
Sat: uint64(chanAmt - commitFee),
Msat: uint64(lnwire.NewMSatFromSatoshis(
chanAmt - commitFee,
)),
},
UnsettledLocalBalance: &lnrpc.Amount{},
UnsettledRemoteBalance: &lnrpc.Amount{},
PendingOpenLocalBalance: &lnrpc.Amount{},
PendingOpenRemoteBalance: &lnrpc.Amount{},
// Deprecated fields.
Balance: int64(chanAmt - commitFee),
}
ht.AssertChannelBalanceResp(bob, expBalanceBob)
}
// assertLinkFailure checks that the stream provided has a single link
// failure the failure detail provided.
assertLinkFailure := func(event *routerrpc.HtlcEvent,
failureDetail routerrpc.FailureDetail) {
linkFail, ok := event.Event.(*routerrpc.HtlcEvent_LinkFailEvent)
require.Truef(ht, ok, "expected forwarding failure, got: %T",
linkFail)
require.Equal(ht, failureDetail,
linkFail.LinkFailEvent.FailureDetail,
"wrong link fail detail")
}
// With the channels, open we can now start to test our multi-hop error
// scenarios. First, we'll generate an invoice from carol that we'll
// use to test some error cases.
const payAmt = 10000
invoiceReq := &lnrpc.Invoice{
Memo: "kek99",
Value: payAmt,
}
carolInvoice := carol.RPC.AddInvoice(invoiceReq)
carolPayReq := carol.RPC.DecodePayReq(carolInvoice.PaymentRequest)
// For the first scenario, we'll test the cancellation of an HTLC with
// an unknown payment hash.
sendReq := &routerrpc.SendPaymentRequest{
PaymentHash: ht.Random32Bytes(),
Dest: carol.PubKey[:],
Amt: payAmt,
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
}
ht.SendPaymentAssertFail(
alice, sendReq,
lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, //nolint:lll
)
ht.AssertLastHTLCError(
alice, lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
)
// assertAliceAndBob is a helper closure that asserts Alice and Bob
// each has one forward and one forward fail event, and Bob has the
// final htlc fail event.
assertAliceAndBob := func() {
ht.AssertHtlcEventTypes(
aliceEvents, routerrpc.HtlcEvent_SEND,
lntest.HtlcEventForward,
)
ht.AssertHtlcEventTypes(
aliceEvents, routerrpc.HtlcEvent_SEND,
lntest.HtlcEventForwardFail,
)
ht.AssertHtlcEventTypes(
bobEvents, routerrpc.HtlcEvent_FORWARD,
lntest.HtlcEventForward,
)
ht.AssertHtlcEventTypes(
bobEvents, routerrpc.HtlcEvent_FORWARD,
lntest.HtlcEventForwardFail,
)
ht.AssertHtlcEventTypes(
bobEvents, routerrpc.HtlcEvent_UNKNOWN,
lntest.HtlcEventFinal,
)
}
// We expect alice and bob to each have one forward and one forward
// fail event at this stage.
assertAliceAndBob()
// Carol should have a link failure because the htlc failed on her
// incoming link.
event := ht.AssertHtlcEventType(
carolEvents, routerrpc.HtlcEvent_RECEIVE,
)
assertLinkFailure(event, routerrpc.FailureDetail_UNKNOWN_INVOICE)
// There's also a final htlc event that gives the final outcome of the
// htlc.
ht.AssertHtlcEventTypes(
carolEvents, routerrpc.HtlcEvent_UNKNOWN, lntest.HtlcEventFinal,
)
// The balances of all parties should be the same as initially since
// the HTLC was canceled.
assertBaseBalance()
// Next, we'll test the case of a recognized payHash but, an incorrect
// value on the extended HTLC.
htlcAmt := lnwire.NewMSatFromSatoshis(1000)
sendReq = &routerrpc.SendPaymentRequest{
PaymentHash: carolInvoice.RHash,
Dest: carol.PubKey[:],
// 10k satoshis are expected.
Amt: int64(htlcAmt.ToSatoshis()),
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
}
ht.SendPaymentAssertFail(
alice, sendReq,
lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, //nolint:lll
)
ht.AssertLastHTLCError(
alice, lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
)
// We expect alice and bob to each have one forward and one forward
// fail event at this stage.
assertAliceAndBob()
// Carol should have a link failure because the htlc failed on her
// incoming link.
event = ht.AssertHtlcEventType(carolEvents, routerrpc.HtlcEvent_RECEIVE)
assertLinkFailure(event, routerrpc.FailureDetail_INVOICE_UNDERPAID)
// There's also a final htlc event that gives the final outcome of the
// htlc.
ht.AssertHtlcEventTypes(
carolEvents, routerrpc.HtlcEvent_UNKNOWN, lntest.HtlcEventFinal,
)
// The balances of all parties should be the same as initially since
// the HTLC was canceled.
assertBaseBalance()
// Next we'll test an error that occurs mid-route due to an outgoing
// link having insufficient capacity. In order to do so, we'll first
// need to unbalance the link connecting Bob<->Carol.
//
// To do so, we'll push most of the funds in the channel over to
// Alice's side, leaving on 10k satoshis of available balance for bob.
chanReserve := int64(chanAmt / 100)
feeBuffer := lntest.CalcStaticFeeBuffer(cType, 0)
amtToSend := int64(chanAmt) - chanReserve - int64(feeBuffer) - 10000
invoiceReq = &lnrpc.Invoice{
Value: amtToSend,
}
carolInvoice2 := carol.RPC.AddInvoice(invoiceReq)
req := &routerrpc.SendPaymentRequest{
PaymentRequest: carolInvoice2.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
}
ht.SendPaymentAndAssertStatus(bob, req, lnrpc.Payment_SUCCEEDED)
// We need to check that bob has a forward and settle event for his
// send, and carol has a settle event and a final htlc event for her
// receive.
ht.AssertHtlcEventTypes(
bobEvents, routerrpc.HtlcEvent_SEND,
lntest.HtlcEventForward,
)
ht.AssertHtlcEventTypes(
bobEvents, routerrpc.HtlcEvent_SEND,
lntest.HtlcEventSettle,
)
ht.AssertHtlcEventTypes(
carolEvents, routerrpc.HtlcEvent_RECEIVE,
lntest.HtlcEventSettle,
)
ht.AssertHtlcEventTypes(
carolEvents, routerrpc.HtlcEvent_UNKNOWN,
lntest.HtlcEventFinal,
)
// At this point, Alice has 50mil satoshis on her side of the channel,
// but Bob only has 10k available on his side of the channel. So a
// payment from Alice to Carol worth 100k satoshis should fail.
invoiceReq = &lnrpc.Invoice{
Value: 100000,
}
carolInvoice3 := carol.RPC.AddInvoice(invoiceReq)
sendReq = &routerrpc.SendPaymentRequest{
PaymentRequest: carolInvoice3.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
}
ht.SendPaymentAssertFail(
alice, sendReq,
lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE,
)
ht.AssertLastHTLCError(
alice, lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE,
)
// Alice should have a forwarding event and a forwarding failure.
ht.AssertHtlcEventTypes(
aliceEvents, routerrpc.HtlcEvent_SEND,
lntest.HtlcEventForward,
)
ht.AssertHtlcEventTypes(
aliceEvents, routerrpc.HtlcEvent_SEND,
lntest.HtlcEventForwardFail,
)
// Bob should have a link failure because the htlc failed on his
// outgoing link.
event = ht.AssertHtlcEventType(bobEvents, routerrpc.HtlcEvent_FORWARD)
assertLinkFailure(event, routerrpc.FailureDetail_INSUFFICIENT_BALANCE)
// There's also a final htlc event that gives the final outcome of the
// htlc.
ht.AssertHtlcEventTypes(
bobEvents, routerrpc.HtlcEvent_UNKNOWN, lntest.HtlcEventFinal,
)
// Generate new invoice to not pay same invoice twice.
carolInvoice = carol.RPC.AddInvoice(invoiceReq)
// For our final test, we'll ensure that if a target link isn't
// available for what ever reason then the payment fails accordingly.
//
// We'll attempt to complete the original invoice we created with Carol
// above, but before we do so, Carol will go offline, resulting in a
// failed payment.
ht.Shutdown(carol)
// Reset mission control to forget the temporary channel failure above.
alice.RPC.ResetMissionControl()
req = &routerrpc.SendPaymentRequest{
PaymentRequest: carolInvoice.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
}
ht.SendPaymentAssertFail(
alice, req, lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE,
)
ht.AssertLastHTLCError(alice, lnrpc.Failure_UNKNOWN_NEXT_PEER)
// Alice should have a forwarding event and subsequent fail.
ht.AssertHtlcEventTypes(
aliceEvents, routerrpc.HtlcEvent_SEND,
lntest.HtlcEventForward,
)
ht.AssertHtlcEventTypes(
aliceEvents, routerrpc.HtlcEvent_SEND,
lntest.HtlcEventForwardFail,
)
// Bob should have a link failure because he could not find the next
// peer.
event = ht.AssertHtlcEventType(bobEvents, routerrpc.HtlcEvent_FORWARD)
assertLinkFailure(event, routerrpc.FailureDetail_NO_DETAIL)
// There's also a final htlc event that gives the final outcome of the
// htlc.
ht.AssertHtlcEventTypes(
bobEvents, routerrpc.HtlcEvent_UNKNOWN, lntest.HtlcEventFinal,
)
// Finally, immediately close the channel. This function will also
// block until the channel is closed and will additionally assert the
// relevant channel closing post conditions.
ht.CloseChannel(alice, chanPointAlice)
// Force close Bob's final channel.
ht.ForceCloseChannel(bob, chanPointBob)
}