From ae9c31f6e3e4bef3d4d4b19893c7b970db626586 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 10:52:30 +0200 Subject: [PATCH 1/8] routing: Add context to requestRoute --- routing/payment_lifecycle.go | 6 ++---- routing/payment_lifecycle_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 6405e850687..c8a59a32331 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -288,7 +288,7 @@ lifecycle: } // Now request a route to be used to create our HTLC attempt. - rt, err := p.requestRoute(ps) + rt, err := p.requestRoute(cleanupCtx, ps) if err != nil { return exitWithErr(err) } @@ -399,11 +399,9 @@ func (p *paymentLifecycle) checkContext(ctx context.Context) error { // requestRoute is responsible for finding a route to be used to create an HTLC // attempt. -func (p *paymentLifecycle) requestRoute( +func (p *paymentLifecycle) requestRoute(ctx context.Context, ps *paymentsdb.MPPaymentState) (*route.Route, error) { - ctx := context.TODO() - remainingFees := p.calcFeeBudget(ps.FeesPaid) // Query our payment session to construct a route. diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go index 7e94315a7dc..a03218bfed8 100644 --- a/routing/payment_lifecycle_test.go +++ b/routing/payment_lifecycle_test.go @@ -393,7 +393,7 @@ func TestRequestRouteSucceed(t *testing.T) { mock.Anything, ).Return(dummyRoute, nil) - result, err := p.requestRoute(ps) + result, err := p.requestRoute(t.Context(), ps) require.NoError(t, err, "expect no error") require.Equal(t, dummyRoute, result, "returned route not matched") @@ -430,7 +430,7 @@ func TestRequestRouteHandleCriticalErr(t *testing.T) { mock.Anything, ).Return(nil, errDummy) - result, err := p.requestRoute(ps) + result, err := p.requestRoute(t.Context(), ps) // Expect an error is returned since it's critical. require.ErrorIs(t, err, errDummy, "error not matched") @@ -470,7 +470,7 @@ func TestRequestRouteHandleNoRouteErr(t *testing.T) { p.identifier, paymentsdb.FailureReasonNoRoute, ).Return(nil).Once() - result, err := p.requestRoute(ps) + result, err := p.requestRoute(t.Context(), ps) // Expect no error is returned since it's not critical. require.NoError(t, err, "expected no error") @@ -513,7 +513,7 @@ func TestRequestRouteFailPaymentError(t *testing.T) { mock.Anything, ).Return(nil, errNoTlvPayload) - result, err := p.requestRoute(ps) + result, err := p.requestRoute(t.Context(), ps) // Expect an error is returned. require.ErrorIs(t, err, errDummy, "error not matched") From d11b7621948e345c0b939e987ff377df6e12a4c2 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 11:30:07 +0200 Subject: [PATCH 2/8] multi: thread context through payment lifecyle functions --- lnrpc/routerrpc/router_server.go | 4 ++-- routing/payment_lifecycle.go | 30 +++++++++++++++----------- routing/payment_lifecycle_test.go | 32 +++++++++++++++------------- routing/router.go | 16 +++++++++----- routing/router_test.go | 28 +++++++++++++++++-------- rpcserver.go | 35 ++++++++++++++++++++----------- 6 files changed, 91 insertions(+), 54 deletions(-) diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index 1dbc19e47f9..5db1fb9584a 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -936,11 +936,11 @@ func (s *Server) SendToRouteV2(ctx context.Context, // db. if req.SkipTempErr { attempt, err = s.cfg.Router.SendToRouteSkipTempErr( - hash, route, firstHopRecords, + ctx, hash, route, firstHopRecords, ) } else { attempt, err = s.cfg.Router.SendToRoute( - hash, route, firstHopRecords, + ctx, hash, route, firstHopRecords, ) } if attempt != nil { diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index c8a59a32331..43d56331dde 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -128,7 +128,7 @@ const ( // results is sent back. then process its result here. When there's no need to // wait for results, the method will exit with `stepExit` such that the payment // lifecycle loop will terminate. -func (p *paymentLifecycle) decideNextStep( +func (p *paymentLifecycle) decideNextStep(ctx context.Context, payment paymentsdb.DBMPPayment) (stateStep, error) { // Check whether we could make new HTLC attempts. @@ -168,7 +168,7 @@ func (p *paymentLifecycle) decideNextStep( // stepSkip and move to the next lifecycle iteration, which will // refresh the payment and wait for the next attempt result, if // any. - _, err := p.handleAttemptResult(r.attempt, r.result) + _, err := p.handleAttemptResult(ctx, r.attempt, r.result) // We would only get a DB-related error here, which will cause // us to abort the payment flow. @@ -192,6 +192,13 @@ func (p *paymentLifecycle) resumePayment(ctx context.Context) ([32]byte, // We need to make sure we can still do db operations after the context // is cancelled. + // + // TODO(ziggie): This is a workaround to avoid a greater refactor of the + // payment lifecycle. We can currently not rely on the parent context + // because this method is also collecting the results of inflight HTLCs + // after the context is cancelled. So we need to make sure we only use + // the current context to stop creating new attempts but use this + // cleanupCtx to do all the db operations. cleanupCtx := context.WithoutCancel(ctx) // When the payment lifecycle loop exits, we make sure to signal any @@ -264,7 +271,7 @@ lifecycle: // // Now decide the next step of the current lifecycle. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(cleanupCtx, payment) if err != nil { return exitWithErr(err) } @@ -307,7 +314,9 @@ lifecycle: log.Tracef("Found route: %s", lnutils.SpewLogClosure(rt.Hops)) // We found a route to try, create a new HTLC attempt to try. - attempt, err := p.registerAttempt(rt, ps.RemainingAmt) + attempt, err := p.registerAttempt( + cleanupCtx, rt, ps.RemainingAmt, + ) if err != nil { return exitWithErr(err) } @@ -596,11 +605,9 @@ func (p *paymentLifecycle) collectResult( // registerAttempt is responsible for creating and saving an HTLC attempt in db // by using the route info provided. The `remainingAmt` is used to decide // whether this is the last attempt. -func (p *paymentLifecycle) registerAttempt(rt *route.Route, +func (p *paymentLifecycle) registerAttempt(ctx context.Context, rt *route.Route, remainingAmt lnwire.MilliSatoshi) (*paymentsdb.HTLCAttempt, error) { - ctx := context.TODO() - // If this route will consume the last remaining amount to send // to the receiver, this will be our last shard (for now). isLastAttempt := rt.ReceiverAmt() == remainingAmt @@ -1184,11 +1191,10 @@ func (p *paymentLifecycle) reloadPayment() (paymentsdb.DBMPPayment, // handleAttemptResult processes the result of an HTLC attempt returned from // the htlcswitch. -func (p *paymentLifecycle) handleAttemptResult(attempt *paymentsdb.HTLCAttempt, +func (p *paymentLifecycle) handleAttemptResult(ctx context.Context, + attempt *paymentsdb.HTLCAttempt, result *htlcswitch.PaymentResult) (*attemptResult, error) { - ctx := context.TODO() - // If the result has an error, we need to further process it by failing // the attempt and maybe fail the payment. if result.Error != nil { @@ -1235,7 +1241,7 @@ func (p *paymentLifecycle) handleAttemptResult(attempt *paymentsdb.HTLCAttempt, // available from the Switch, then records the attempt outcome with the control // tower. An attemptResult is returned, indicating the final outcome of this // HTLC attempt. -func (p *paymentLifecycle) collectAndHandleResult( +func (p *paymentLifecycle) collectAndHandleResult(ctx context.Context, attempt *paymentsdb.HTLCAttempt) (*attemptResult, error) { result, err := p.collectResult(attempt) @@ -1243,5 +1249,5 @@ func (p *paymentLifecycle) collectAndHandleResult( return nil, err } - return p.handleAttemptResult(attempt, result) + return p.handleAttemptResult(ctx, attempt, result) } diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go index a03218bfed8..61ae83a31fc 100644 --- a/routing/payment_lifecycle_test.go +++ b/routing/payment_lifecycle_test.go @@ -599,7 +599,7 @@ func TestDecideNextStep(t *testing.T) { // Once the setup is finished, run the test cases. t.Run(tc.name, func(t *testing.T) { - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) require.Equal(t, tc.expectedStep, step) require.ErrorIs(t, tc.expectedErr, err) }) @@ -628,7 +628,7 @@ func TestDecideNextStepOnRouterQuit(t *testing.T) { close(p.router.quit) // Call the method under test. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) // We expect stepExit and an error to be returned. require.Equal(t, stepExit, step) @@ -657,7 +657,7 @@ func TestDecideNextStepOnLifecycleQuit(t *testing.T) { close(p.quit) // Call the method under test. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) // We expect stepExit and an error to be returned. require.Equal(t, stepExit, step) @@ -716,7 +716,7 @@ func TestDecideNextStepHandleAttemptResultSucceed(t *testing.T) { mock.Anything).Return(attempt, nil).Once() // Call the method under test. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) // We expect stepSkip and no error to be returned. require.Equal(t, stepSkip, step) @@ -774,7 +774,7 @@ func TestDecideNextStepHandleAttemptResultFail(t *testing.T) { mock.Anything).Return(attempt, errDummy).Once() // Call the method under test. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) // We expect stepExit and the above error to be returned. require.Equal(t, stepExit, step) @@ -1467,7 +1467,7 @@ func TestCollectResultExitOnErr(t *testing.T) { m.clock.On("Now").Return(time.Now()) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, errDummy, "expected dummy error") require.Nil(t, result, "expected nil attempt") } @@ -1513,7 +1513,7 @@ func TestCollectResultExitOnResultErr(t *testing.T) { m.clock.On("Now").Return(time.Now()) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, errDummy, "expected dummy error") require.Nil(t, result, "expected nil attempt") } @@ -1539,7 +1539,7 @@ func TestCollectResultExitOnSwitchQuit(t *testing.T) { }) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, htlcswitch.ErrSwitchExiting, "expected switch exit") require.Nil(t, result, "expected nil attempt") @@ -1566,7 +1566,7 @@ func TestCollectResultExitOnRouterQuit(t *testing.T) { }) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, ErrRouterShuttingDown, "expected router exit") require.Nil(t, result, "expected nil attempt") } @@ -1592,7 +1592,7 @@ func TestCollectResultExitOnLifecycleQuit(t *testing.T) { }) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, ErrPaymentLifecycleExiting, "expected lifecycle exit") require.Nil(t, result, "expected nil attempt") @@ -1636,7 +1636,7 @@ func TestCollectResultExitOnSettleErr(t *testing.T) { m.clock.On("Now").Return(time.Now()) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, errDummy, "expected settle error") require.Nil(t, result, "expected nil attempt") } @@ -1678,7 +1678,7 @@ func TestCollectResultSuccess(t *testing.T) { m.clock.On("Now").Return(time.Now()) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.NoError(t, err, "expected no error") require.Equal(t, preimage, result.attempt.Settle.Preimage, "preimage mismatch") @@ -1762,7 +1762,9 @@ func TestHandleAttemptResultWithError(t *testing.T) { // Call the method under test and expect the dummy error to be // returned. - attemptResult, err := p.handleAttemptResult(attempt, result) + attemptResult, err := p.handleAttemptResult( + t.Context(), attempt, result, + ) require.ErrorIs(t, err, errDummy, "expected fail error") require.Nil(t, attemptResult, "expected nil attempt result") } @@ -1800,7 +1802,9 @@ func TestHandleAttemptResultSuccess(t *testing.T) { // Call the method under test and expect the dummy error to be // returned. - attemptResult, err := p.handleAttemptResult(attempt, result) + attemptResult, err := p.handleAttemptResult( + t.Context(), attempt, result, + ) require.NoError(t, err, "expected no error") require.Equal(t, attempt, attemptResult.attempt) } diff --git a/routing/router.go b/routing/router.go index acd572e6380..c722aebd1ef 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1038,7 +1038,8 @@ func (r *ChannelRouter) PreparePayment(payment *LightningPayment) ( // SendToRoute sends a payment using the provided route and fails the payment // when an error is returned from the attempt. -func (r *ChannelRouter) SendToRoute(htlcHash lntypes.Hash, rt *route.Route, +func (r *ChannelRouter) SendToRoute(_ context.Context, htlcHash lntypes.Hash, + rt *route.Route, firstHopCustomRecords lnwire.CustomRecords) (*paymentsdb.HTLCAttempt, error) { @@ -1047,8 +1048,8 @@ func (r *ChannelRouter) SendToRoute(htlcHash lntypes.Hash, rt *route.Route, // SendToRouteSkipTempErr sends a payment using the provided route and fails // the payment ONLY when a terminal error is returned from the attempt. -func (r *ChannelRouter) SendToRouteSkipTempErr(htlcHash lntypes.Hash, - rt *route.Route, +func (r *ChannelRouter) SendToRouteSkipTempErr(_ context.Context, + htlcHash lntypes.Hash, rt *route.Route, firstHopCustomRecords lnwire.CustomRecords) (*paymentsdb.HTLCAttempt, error) { @@ -1066,6 +1067,11 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, firstHopCustomRecords lnwire.CustomRecords) (*paymentsdb.HTLCAttempt, error) { + // TODO(ziggie): We cannot easily thread the context from the caller + // of this method because the payment lifecycle depends on the context + // to update the db. The Sending and Receiving of results is currently + // not cleanly separated which is the reason that we cannot easily + // cancel the context and therefore cancel the ongoing payment. ctx := context.TODO() // Helper function to fail a payment. It makes sure the payment is only @@ -1179,7 +1185,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, // NOTE: we use zero `remainingAmt` here to simulate the same effect of // setting the lastShard to be false, which is used by previous // implementation. - attempt, err := p.registerAttempt(rt, 0) + attempt, err := p.registerAttempt(ctx, rt, 0) if err != nil { return nil, err } @@ -1216,7 +1222,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, // The attempt was successfully sent, wait for the result to be // available. - result, err = p.collectAndHandleResult(attempt) + result, err = p.collectAndHandleResult(ctx, attempt) if err != nil { return nil, err } diff --git a/routing/router_test.go b/routing/router_test.go index a20b1b75dd2..5ec8ff8c330 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -518,7 +518,7 @@ func TestChannelUpdateValidation(t *testing.T) { // Send off the payment request to the router. The specified route // should be attempted and the channel update should be received by // graph and ignored because it is missing a valid signature. - _, err = ctx.router.SendToRoute(payment, rt, nil) + _, err = ctx.router.SendToRoute(t.Context(), payment, rt, nil) require.Error(t, err, "expected route to fail with channel update") _, e1, e2, err = ctx.graph.FetchChannelEdgesByID( @@ -538,7 +538,7 @@ func TestChannelUpdateValidation(t *testing.T) { ctx.graphBuilder.setNextReject(false) // Retry the payment using the same route as before. - _, err = ctx.router.SendToRoute(payment, rt, nil) + _, err = ctx.router.SendToRoute(t.Context(), payment, rt, nil) require.Error(t, err, "expected route to fail with channel update") // This time a valid signature was supplied and the policy change should @@ -1423,7 +1423,9 @@ func TestSendToRouteStructuredError(t *testing.T) { // update should be received by router and ignored // because it is missing a valid // signature. - _, err = ctx.router.SendToRoute(payment, rt, nil) + _, err = ctx.router.SendToRoute( + t.Context(), payment, rt, nil, + ) fErr, ok := err.(*htlcswitch.ForwardingError) require.True( @@ -1502,7 +1504,7 @@ func TestSendToRouteMaxHops(t *testing.T) { // Send off the payment request to the router. We expect an error back // indicating that the route is too long. var payHash lntypes.Hash - _, err = ctx.router.SendToRoute(payHash, rt, nil) + _, err = ctx.router.SendToRoute(t.Context(), payHash, rt, nil) if err != route.ErrMaxRouteHopsExceeded { t.Fatalf("expected ErrMaxRouteHopsExceeded, but got %v", err) } @@ -2217,7 +2219,9 @@ func TestSendToRouteSkipTempErrSuccess(t *testing.T) { ).Return(nil) // Expect a successful send to route. - attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil) + attempt, err := router.SendToRouteSkipTempErr( + t.Context(), payHash, rt, nil, + ) require.NoError(t, err) require.Equal(t, testAttempt, attempt) @@ -2272,7 +2276,9 @@ func TestSendToRouteSkipTempErrNonMPP(t *testing.T) { }} // Expect an error to be returned. - attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil) + attempt, err := router.SendToRouteSkipTempErr( + t.Context(), payHash, rt, nil, + ) require.ErrorIs(t, ErrSkipTempErr, err) require.Nil(t, attempt) @@ -2352,7 +2358,9 @@ func TestSendToRouteSkipTempErrTempFailure(t *testing.T) { ).Return(nil, nil) // Expect a failed send to route. - attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil) + attempt, err := router.SendToRouteSkipTempErr( + t.Context(), payHash, rt, nil, + ) require.Equal(t, tempErr, err) require.Equal(t, testAttempt, attempt) @@ -2436,7 +2444,9 @@ func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) { ).Return(&failureReason, nil) // Expect a failed send to route. - attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil) + attempt, err := router.SendToRouteSkipTempErr( + t.Context(), payHash, rt, nil, + ) require.Equal(t, permErr, err) require.Equal(t, testAttempt, attempt) @@ -2525,7 +2535,7 @@ func TestSendToRouteTempFailure(t *testing.T) { ).Return(nil, nil) // Expect a failed send to route. - attempt, err := router.SendToRoute(payHash, rt, nil) + attempt, err := router.SendToRoute(t.Context(), payHash, rt, nil) require.Equal(t, tempErr, err) require.Equal(t, testAttempt, attempt) diff --git a/rpcserver.go b/rpcserver.go index e75e0dcd15a..0fc51fc92e4 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -5519,8 +5519,9 @@ func (r *rpcServer) SubscribeChannelEvents(req *lnrpc.ChannelEventSubscription, // execute sendPayment. We use this struct as a sort of bridge to enable code // re-use between SendPayment and SendToRoute. type paymentStream struct { - recv func() (*rpcPaymentRequest, error) - send func(*lnrpc.SendResponse) error + getCtx func() context.Context + recv func() (*rpcPaymentRequest, error) + send func(*lnrpc.SendResponse) error } // rpcPaymentRequest wraps lnrpc.SendRequest so that routes from @@ -5534,10 +5535,13 @@ type rpcPaymentRequest struct { // through the Lightning Network. A single RPC invocation creates a persistent // bi-directional stream allowing clients to rapidly send payments through the // Lightning Network with a single persistent connection. -func (r *rpcServer) SendPayment(stream lnrpc.Lightning_SendPaymentServer) error { +func (r *rpcServer) SendPayment( + stream lnrpc.Lightning_SendPaymentServer) error { + var lock sync.Mutex return r.sendPayment(&paymentStream{ + getCtx: stream.Context, recv: func() (*rpcPaymentRequest, error) { req, err := stream.Recv() if err != nil { @@ -5562,10 +5566,13 @@ func (r *rpcServer) SendPayment(stream lnrpc.Lightning_SendPaymentServer) error // invocation creates a persistent bi-directional stream allowing clients to // rapidly send payments through the Lightning Network with a single persistent // connection. -func (r *rpcServer) SendToRoute(stream lnrpc.Lightning_SendToRouteServer) error { +func (r *rpcServer) SendToRoute( + stream lnrpc.Lightning_SendToRouteServer) error { + var lock sync.Mutex return r.sendPayment(&paymentStream{ + getCtx: stream.Context, recv: func() (*rpcPaymentRequest, error) { req, err := stream.Recv() if err != nil { @@ -5635,7 +5642,11 @@ type rpcPaymentIntent struct { // dispatch a client from the information presented by an RPC client. There are // three ways a client can specify their payment details: a payment request, // via manual details, or via a complete route. -func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error) { +// +//nolint:funlen +func (r *rpcServer) extractPaymentIntent( + rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error) { + payIntent := rpcPaymentIntent{} // If a route was specified, then we can use that directly. @@ -5907,7 +5918,7 @@ type paymentIntentResponse struct { // pre-built route. The first error this method returns denotes if we were // unable to save the payment. The second error returned denotes if the payment // didn't succeed. -func (r *rpcServer) dispatchPaymentIntent( +func (r *rpcServer) dispatchPaymentIntent(ctx context.Context, payIntent *rpcPaymentIntent) (*paymentIntentResponse, error) { // Construct a payment request to send to the channel router. If the @@ -5954,7 +5965,7 @@ func (r *rpcServer) dispatchPaymentIntent( } else { var attempt *paymentsdb.HTLCAttempt attempt, routerErr = r.server.chanRouter.SendToRoute( - payIntent.rHash, payIntent.route, nil, + ctx, payIntent.rHash, payIntent.route, nil, ) if routerErr == nil { @@ -6127,7 +6138,7 @@ sendLoop: }() resp, saveErr := r.dispatchPaymentIntent( - payIntent, + stream.getCtx(), payIntent, ) switch { @@ -6205,7 +6216,7 @@ sendLoop: func (r *rpcServer) SendPaymentSync(ctx context.Context, nextPayment *lnrpc.SendRequest) (*lnrpc.SendResponse, error) { - return r.sendPaymentSync(&rpcPaymentRequest{ + return r.sendPaymentSync(ctx, &rpcPaymentRequest{ SendRequest: nextPayment, }) } @@ -6226,12 +6237,12 @@ func (r *rpcServer) SendToRouteSync(ctx context.Context, return nil, err } - return r.sendPaymentSync(paymentRequest) + return r.sendPaymentSync(ctx, paymentRequest) } // sendPaymentSync is the synchronous variant of sendPayment. It will block and // wait until the payment has been fully completed. -func (r *rpcServer) sendPaymentSync( +func (r *rpcServer) sendPaymentSync(ctx context.Context, nextPayment *rpcPaymentRequest) (*lnrpc.SendResponse, error) { // We don't allow payments to be sent while the daemon itself is still @@ -6250,7 +6261,7 @@ func (r *rpcServer) sendPaymentSync( // With the payment validated, we'll now attempt to dispatch the // payment. - resp, saveErr := r.dispatchPaymentIntent(&payIntent) + resp, saveErr := r.dispatchPaymentIntent(ctx, &payIntent) switch { case saveErr != nil: return nil, saveErr From 91fd00a8ebf8d91adf8ec2cc8a43e0189b9d301e Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 11:39:39 +0200 Subject: [PATCH 3/8] routing: Thread context through failPaymentAndAttempt A context is added to failPaymentAndAttempt and its dependant function calls. --- routing/payment_lifecycle.go | 21 ++++++++++----------- routing/router.go | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 43d56331dde..b5df24379ef 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -322,7 +322,7 @@ lifecycle: } // Once the attempt is created, send it to the htlcswitch. - result, err := p.sendAttempt(attempt) + result, err := p.sendAttempt(cleanupCtx, attempt) if err != nil { return exitWithErr(err) } @@ -681,7 +681,7 @@ func (p *paymentLifecycle) createNewPaymentAttempt(rt *route.Route, // sendAttempt attempts to send the current attempt to the switch to complete // the payment. If this attempt fails, then we'll continue on to the next // available route. -func (p *paymentLifecycle) sendAttempt( +func (p *paymentLifecycle) sendAttempt(ctx context.Context, attempt *paymentsdb.HTLCAttempt) (*attemptResult, error) { log.Debugf("Sending HTLC attempt(id=%v, total_amt=%v, first_hop_amt=%d"+ @@ -727,7 +727,7 @@ func (p *paymentLifecycle) sendAttempt( log.Errorf("Failed sending attempt %d for payment %v to "+ "switch: %v", attempt.AttemptID, p.identifier, err) - return p.handleSwitchErr(attempt, err) + return p.handleSwitchErr(ctx, attempt, err) } log.Debugf("Attempt %v for payment %v successfully sent to switch, "+ @@ -818,12 +818,10 @@ func (p *paymentLifecycle) amendFirstHopData(rt *route.Route) error { // failAttemptAndPayment fails both the payment and its attempt via the // router's control tower, which marks the payment as failed in db. -func (p *paymentLifecycle) failPaymentAndAttempt( +func (p *paymentLifecycle) failPaymentAndAttempt(ctx context.Context, attemptID uint64, reason *paymentsdb.FailureReason, sendErr error) (*attemptResult, error) { - ctx := context.TODO() - log.Errorf("Payment %v failed: final_outcome=%v, raw_err=%v", p.identifier, *reason, sendErr) @@ -852,7 +850,8 @@ func (p *paymentLifecycle) failPaymentAndAttempt( // the error type, the error is either the final outcome of the payment or we // need to continue with an alternative route. A final outcome is indicated by // a non-nil reason value. -func (p *paymentLifecycle) handleSwitchErr(attempt *paymentsdb.HTLCAttempt, +func (p *paymentLifecycle) handleSwitchErr(ctx context.Context, + attempt *paymentsdb.HTLCAttempt, sendErr error) (*attemptResult, error) { internalErrorReason := paymentsdb.FailureReasonError @@ -883,7 +882,7 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *paymentsdb.HTLCAttempt, } // Otherwise fail both the payment and the attempt. - return p.failPaymentAndAttempt(attemptID, reason, sendErr) + return p.failPaymentAndAttempt(ctx, attemptID, reason, sendErr) } // If this attempt ID is unknown to the Switch, it means it was never @@ -916,7 +915,7 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *paymentsdb.HTLCAttempt, ok := errors.As(sendErr, &rtErr) if !ok { return p.failPaymentAndAttempt( - attemptID, &internalErrorReason, sendErr, + ctx, attemptID, &internalErrorReason, sendErr, ) } @@ -942,7 +941,7 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *paymentsdb.HTLCAttempt, ) if err != nil { return p.failPaymentAndAttempt( - attemptID, &internalErrorReason, sendErr, + ctx, attemptID, &internalErrorReason, sendErr, ) } @@ -1198,7 +1197,7 @@ func (p *paymentLifecycle) handleAttemptResult(ctx context.Context, // If the result has an error, we need to further process it by failing // the attempt and maybe fail the payment. if result.Error != nil { - return p.handleSwitchErr(attempt, result.Error) + return p.handleSwitchErr(ctx, attempt, result.Error) } // We got an attempt settled result back from the switch. diff --git a/routing/router.go b/routing/router.go index c722aebd1ef..3af6c793305 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1194,7 +1194,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, // the `err` returned here has already been processed by // `handleSwitchErr`, which means if there's a terminal failure, the // payment has been failed. - result, err := p.sendAttempt(attempt) + result, err := p.sendAttempt(ctx, attempt) if err != nil { return nil, err } From ea337268f138e1289f1083fbca124ce3e18f3aa7 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 11:42:35 +0200 Subject: [PATCH 4/8] routing: add context to failAttempt --- routing/payment_lifecycle.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index b5df24379ef..c6f6154c105 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -713,7 +713,7 @@ func (p *paymentLifecycle) sendAttempt(ctx context.Context, "payment=%v, err:%v", attempt.AttemptID, p.identifier, err) - return p.failAttempt(attempt.AttemptID, err) + return p.failAttempt(ctx, attempt.AttemptID, err) } htlcAdd.OnionBlob = onionBlob @@ -839,7 +839,7 @@ func (p *paymentLifecycle) failPaymentAndAttempt(ctx context.Context, } // Fail the attempt. - return p.failAttempt(attemptID, sendErr) + return p.failAttempt(ctx, attemptID, sendErr) } // handleSwitchErr inspects the given error from the Switch and determines @@ -878,7 +878,7 @@ func (p *paymentLifecycle) handleSwitchErr(ctx context.Context, // Fail the attempt only if there's no reason. if reason == nil { // Fail the attempt. - return p.failAttempt(attemptID, sendErr) + return p.failAttempt(ctx, attemptID, sendErr) } // Otherwise fail both the payment and the attempt. @@ -893,7 +893,7 @@ func (p *paymentLifecycle) handleSwitchErr(ctx context.Context, log.Warnf("Failing attempt=%v for payment=%v as it's not "+ "found in the Switch", attempt.AttemptID, p.identifier) - return p.failAttempt(attemptID, sendErr) + return p.failAttempt(ctx, attemptID, sendErr) } if errors.Is(sendErr, htlcswitch.ErrUnreadableFailureMessage) { @@ -1025,11 +1025,9 @@ func (p *paymentLifecycle) handleFailureMessage(rt *route.Route, } // failAttempt calls control tower to fail the current payment attempt. -func (p *paymentLifecycle) failAttempt(attemptID uint64, +func (p *paymentLifecycle) failAttempt(ctx context.Context, attemptID uint64, sendError error) (*attemptResult, error) { - ctx := context.TODO() - log.Warnf("Attempt %v for payment %v failed: %v", attemptID, p.identifier, sendError) From abcd83447a111220413f42a2dc241bf07cdbd15e Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 11:44:35 +0200 Subject: [PATCH 5/8] routing: add context to reloadInflightAttempts --- routing/payment_lifecycle.go | 8 +++----- routing/payment_lifecycle_test.go | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index c6f6154c105..763cf43ab7f 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -209,7 +209,7 @@ func (p *paymentLifecycle) resumePayment(ctx context.Context) ([32]byte, // If we had any existing attempts outstanding, we'll start by spinning // up goroutines that'll collect their results and deliver them to the // lifecycle loop below. - payment, err := p.reloadInflightAttempts() + payment, err := p.reloadInflightAttempts(ctx) if err != nil { return [32]byte{}, nil, err } @@ -1138,10 +1138,8 @@ func (p *paymentLifecycle) patchLegacyPaymentHash( // reloadInflightAttempts is called when the payment lifecycle is resumed after // a restart. It reloads all inflight attempts from the control tower and // collects the results of the attempts that have been sent before. -func (p *paymentLifecycle) reloadInflightAttempts() (paymentsdb.DBMPPayment, - error) { - - ctx := context.TODO() +func (p *paymentLifecycle) reloadInflightAttempts( + ctx context.Context) (paymentsdb.DBMPPayment, error) { payment, err := p.router.cfg.Control.FetchPayment(ctx, p.identifier) if err != nil { diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go index 61ae83a31fc..82e2f800d8f 100644 --- a/routing/payment_lifecycle_test.go +++ b/routing/payment_lifecycle_test.go @@ -1850,7 +1850,7 @@ func TestReloadInflightAttemptsLegacy(t *testing.T) { }) // Now call the method under test. - payment, err := p.reloadInflightAttempts() + payment, err := p.reloadInflightAttempts(t.Context()) require.NoError(t, err) require.Equal(t, m.payment, payment) From 6aefe5bae74506e251feac32c0120e9aa6ccb650 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 11:46:35 +0200 Subject: [PATCH 6/8] routing: add context to reloadPayment method --- routing/payment_lifecycle.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 763cf43ab7f..9be86dc1bea 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -250,7 +250,7 @@ lifecycle: } // We update the payment state on every iteration. - currentPayment, ps, err := p.reloadPayment() + currentPayment, ps, err := p.reloadPayment(cleanupCtx) if err != nil { return exitWithErr(err) } @@ -1163,11 +1163,10 @@ func (p *paymentLifecycle) reloadInflightAttempts( } // reloadPayment returns the latest payment found in the db (control tower). -func (p *paymentLifecycle) reloadPayment() (paymentsdb.DBMPPayment, +func (p *paymentLifecycle) reloadPayment( + ctx context.Context) (paymentsdb.DBMPPayment, *paymentsdb.MPPaymentState, error) { - ctx := context.TODO() - // Read the db to get the latest state of the payment. payment, err := p.router.cfg.Control.FetchPayment(ctx, p.identifier) if err != nil { From b60e6988744e69897888547f8c0e0b8d0d084c76 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 17 Nov 2025 14:51:23 +0100 Subject: [PATCH 7/8] multi: thread context through SendPayment --- routing/router.go | 6 +++--- routing/router_test.go | 42 ++++++++++++++++++++++++++++++------------ rpcserver.go | 2 +- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/routing/router.go b/routing/router.go index 3af6c793305..ad922eca943 100644 --- a/routing/router.go +++ b/routing/router.go @@ -896,8 +896,8 @@ func (l *LightningPayment) Identifier() [32]byte { // will be returned which describes the path the successful payment traversed // within the network to reach the destination. Additionally, the payment // preimage will also be returned. -func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, - *route.Route, error) { +func (r *ChannelRouter) SendPayment(ctx context.Context, + payment *LightningPayment) ([32]byte, *route.Route, error) { paySession, shardTracker, err := r.PreparePayment(payment) if err != nil { @@ -908,7 +908,7 @@ func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, spewPayment(payment)) return r.sendPayment( - context.Background(), payment.FeeLimit, payment.Identifier(), + ctx, payment.FeeLimit, payment.Identifier(), payment.PayAttemptTimeout, paySession, shardTracker, payment.FirstHopCustomRecords, ) diff --git a/routing/router_test.go b/routing/router_test.go index 5ec8ff8c330..576bb35f6b6 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -324,7 +324,9 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) { // Send off the payment request to the router, route through pham nuwen // should've been selected as a fall back and succeeded correctly. - paymentPreImage, route, err := ctx.router.SendPayment(payment) + paymentPreImage, route, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -403,7 +405,9 @@ func TestSendPaymentRouteInfiniteLoopWithBadHopHint(t *testing.T) { // Send off the payment request to the router, should succeed // ignoring the bad channel id hint. - paymentPreImage, route, paymentErr := ctx.router.SendPayment(payment) + paymentPreImage, route, paymentErr := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, paymentErr, "unable to send payment: %v", payment.paymentHash) @@ -634,7 +638,9 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { // Send off the payment request to the router, route through phamnuwen // should've been selected as a fall back and succeeded correctly. - paymentPreImage, route, err := ctx.router.SendPayment(payment) + paymentPreImage, route, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -741,7 +747,9 @@ func TestSendPaymentErrorFeeInsufficientPrivateEdge(t *testing.T) { // Send off the payment request to the router, route through son // goku and then across the private channel to elst. - paymentPreImage, route, err := ctx.router.SendPayment(payment) + paymentPreImage, route, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -867,7 +875,9 @@ func TestSendPaymentPrivateEdgeUpdateFeeExceedsLimit(t *testing.T) { // Send off the payment request to the router, route through son // goku and then across the private channel to elst. - paymentPreImage, route, err := ctx.router.SendPayment(payment) + paymentPreImage, route, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -990,7 +1000,9 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) { // Send off the payment request to the router, this payment should // succeed as we should actually go through Pham Nuwen in order to get // to Sophon, even though he has higher fees. - paymentPreImage, rt, err := ctx.router.SendPayment(payment) + paymentPreImage, rt, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1016,7 +1028,9 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) { // w.r.t to the block height, and instead go through Pham Nuwen. We // flip a bit in the payment hash to allow resending this payment. payment.paymentHash[1] ^= 1 - paymentPreImage, rt, err = ctx.router.SendPayment(payment) + paymentPreImage, rt, err = ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1085,7 +1099,7 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { // When we try to dispatch that payment, we should receive an error as // both attempts should fail and cause both routes to be pruned. - _, _, err = ctx.router.SendPayment(payment) + _, _, err = ctx.router.SendPayment(t.Context(), payment) require.Error(t, err, "payment didn't return error") // The final error returned should also indicate that the peer wasn't @@ -1130,7 +1144,9 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { // This shouldn't return an error, as we'll make a payment attempt via // the pham nuwen channel based on the assumption that there might be an // intermittent issue with the songoku <-> sophon channel. - paymentPreImage, rt, err := ctx.router.SendPayment(payment) + paymentPreImage, rt, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1170,7 +1186,9 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { // We flip a bit in the payment hash to allow resending this payment. payment.paymentHash[1] ^= 1 - paymentPreImage, rt, err = ctx.router.SendPayment(payment) + paymentPreImage, rt, err = ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1302,7 +1320,7 @@ func TestUnknownErrorSource(t *testing.T) { // the route a->b->c is tried first. An unreadable faiure is returned // which should pruning the channel a->b. We expect the payment to // succeed via a->d. - _, _, err = ctx.router.SendPayment(payment) + _, _, err = ctx.router.SendPayment(t.Context(), payment) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1327,7 +1345,7 @@ func TestUnknownErrorSource(t *testing.T) { // Send off the payment request to the router. We expect the payment to // fail because both routes have been pruned. payment.paymentHash[1] ^= 1 - _, _, err = ctx.router.SendPayment(payment) + _, _, err = ctx.router.SendPayment(t.Context(), payment) if err == nil { t.Fatalf("expected payment to fail") } diff --git a/rpcserver.go b/rpcserver.go index 0fc51fc92e4..cbf45a6c56b 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -5960,7 +5960,7 @@ func (r *rpcServer) dispatchPaymentIntent(ctx context.Context, } preImage, route, routerErr = r.server.chanRouter.SendPayment( - payment, + ctx, payment, ) } else { var attempt *paymentsdb.HTLCAttempt From aae5a33411a935da2c53bba3d2389e18dc172113 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 21 Oct 2025 11:51:02 +0200 Subject: [PATCH 8/8] docs: add release-notes --- docs/release-notes/release-notes-0.21.0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 1057ccc47e4..752fdbf859e 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -85,6 +85,8 @@ for SQL backend](https://github.com/lightningnetwork/lnd/pull/10292) * [Thread context through payment db functions Part 1](https://github.com/lightningnetwork/lnd/pull/10307) + * [Thread context through payment + db functions Part 2](https://github.com/lightningnetwork/lnd/pull/10308) ## Code Health