Skip to content

Commit 832693a

Browse files
committed
itest: verify attributable failure hold times
Add an integration test that sets up Alice -> Bob -> Carol, triggers a payment failure at Carol (unknown payment hash), and verifies that Alice receives hold times in the RPC failure response. The test checks: - hold_times has one entry per route hop (1:1 correspondence) - failure source index matches Carol (index 2) - each hold time is within a reasonable bound
1 parent a343129 commit 832693a

2 files changed

Lines changed: 107 additions & 0 deletions

File tree

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,10 @@ var allTestCases = []*lntest.TestCase{
715715
Name: "experimental accountability",
716716
TestFunc: testExperimentalAccountability,
717717
},
718+
{
719+
Name: "attributable failure hold times",
720+
TestFunc: testAttributableFailureHoldTimes,
721+
},
718722
{
719723
Name: "quiescence",
720724
TestFunc: testQuiescence,
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package itest
2+
3+
import (
4+
"github.com/lightningnetwork/lnd/funding"
5+
"github.com/lightningnetwork/lnd/lnrpc"
6+
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
7+
"github.com/lightningnetwork/lnd/lntest"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// testAttributableFailureHoldTimes verifies that when a payment fails at a
12+
// downstream node, the sender receives hold times in the failure response via
13+
// attributable errors. It sets up Alice -> Bob -> Carol, triggers a failure at
14+
// Carol (unknown payment hash), and checks that Alice's payment result includes
15+
// hold times that correctly correspond to the route hops.
16+
func testAttributableFailureHoldTimes(ht *lntest.HarnessTest) {
17+
const chanAmt = funding.MaxBtcFundingAmount
18+
19+
alice := ht.NewNodeWithCoins("Alice", nil)
20+
bob := ht.NewNodeWithCoins("Bob", nil)
21+
carol := ht.NewNode("Carol", nil)
22+
23+
ht.EnsureConnected(alice, bob)
24+
ht.ConnectNodes(bob, carol)
25+
26+
// Open channels: Alice -> Bob -> Carol.
27+
chanPointAB := ht.OpenChannel(
28+
alice, bob,
29+
lntest.OpenChannelParams{Amt: chanAmt},
30+
)
31+
chanPointBC := ht.OpenChannel(
32+
bob, carol,
33+
lntest.OpenChannelParams{Amt: chanAmt},
34+
)
35+
36+
// Wait for Alice to see both channels.
37+
ht.AssertChannelInGraph(alice, chanPointAB)
38+
ht.AssertChannelInGraph(alice, chanPointBC)
39+
40+
// Create an invoice from Carol to get valid route parameters.
41+
carolInvoice := carol.RPC.AddInvoice(&lnrpc.Invoice{
42+
Memo: "hold-time-test",
43+
Value: 10_000,
44+
})
45+
carolPayReq := carol.RPC.DecodePayReq(carolInvoice.PaymentRequest)
46+
47+
// Send a payment with a random (wrong) payment hash so Carol rejects
48+
// it with INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS. This ensures the error
49+
// originates at Carol (the final hop) and propagates back through Bob
50+
// to Alice, with each hop adding its hold time.
51+
sendReq := &routerrpc.SendPaymentRequest{
52+
PaymentHash: ht.Random32Bytes(),
53+
Dest: carol.PubKey[:],
54+
Amt: 10_000,
55+
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
56+
FeeLimitMsat: noFeeLimitMsat,
57+
MaxParts: 1,
58+
}
59+
60+
failReason := lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS //nolint:ll
61+
payment := ht.SendPaymentAssertFail(
62+
alice, sendReq, failReason,
63+
)
64+
65+
// Verify we got at least one HTLC attempt with failure info.
66+
require.NotEmpty(ht, payment.Htlcs, "expected at least one HTLC")
67+
htlcAttempt := payment.Htlcs[len(payment.Htlcs)-1]
68+
require.NotNil(ht, htlcAttempt.Failure, "expected failure info")
69+
70+
// The route should have 2 hops: Alice->Bob->Carol.
71+
require.Len(ht, htlcAttempt.Route.Hops, 2,
72+
"expected 2-hop route (Bob, Carol)")
73+
74+
// Verify the failure code and source. Carol is at index 2 (0=Alice,
75+
// 1=Bob, 2=Carol).
76+
require.Equal(ht,
77+
lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
78+
htlcAttempt.Failure.Code,
79+
)
80+
require.EqualValues(ht, 2, htlcAttempt.Failure.FailureSourceIndex,
81+
"failure should originate from Carol (index 2)")
82+
83+
// Verify hold times. With attributable errors, we expect one hold time
84+
// entry per hop in the route. hold_times[0] corresponds to
85+
// route.hops[0] (Bob) and hold_times[1] to route.hops[1] (Carol).
86+
holdTimes := htlcAttempt.Failure.HoldTimes
87+
require.Len(ht, holdTimes, len(htlcAttempt.Route.Hops),
88+
"hold_times should have one entry per route hop")
89+
90+
// Hold times are in 100ms units. In a test environment with local
91+
// nodes, processing should be nearly instant (likely 0-2 units).
92+
// We verify each value is within a reasonable upper bound (< 10s)
93+
// to catch any corruption or mis-encoding.
94+
const maxReasonableHoldTime = uint32(100) // 10 seconds
95+
for i, holdTime := range holdTimes {
96+
require.LessOrEqual(ht, holdTime, maxReasonableHoldTime,
97+
"hold time for hop %d (%s) unreasonably large: "+
98+
"%d (= %dms)",
99+
i, htlcAttempt.Route.Hops[i].PubKey,
100+
holdTime, holdTime*100,
101+
)
102+
}
103+
}

0 commit comments

Comments
 (0)