|
| 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 | + payment := ht.SendPaymentAssertFail( |
| 61 | + alice, sendReq, |
| 62 | + lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, //nolint:ll |
| 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