What changed, and why it matters
This change updates the SendPayment function so it accepts a caller-provided context instead of always creating a fresh background context. In plain terms, it lets the payment-sending code respect cancellation or timeout signals from the caller (for example, if an RPC client disconnects). There is no direct security bug being fixed here; it is a code-quality and resource-cleanup improvement.
No urgent action required. Treat as routine maintenance. If auditing, verify that all callers of SendPayment now pass an appropriate context and that cancellation does not leave inconsistent payment state.
Security signals we found
Context propagation refactor for request lifecycle hygiene
Potential reduction in orphaned payment attempts on RPC cancellation
No direct vulnerability or exploit mechanism visible in diff
Evidence from the diff
The commit threads a context.Context parameter through ChannelRouter.SendPayment and its call sites. Previously SendPayment internally used context.Background(), meaning payment attempts could continue even if the originating RPC context was cancelled. The patch propagates the caller’s context from rpcserver.dispatchPaymentIntent into SendPayment and updates all tests to pass t.Context(). This is a defensive refactor to honor request cancellation and avoid leaked or orphaned payment attempts.
Changed components
routing/router.go: ChannelRouter.SendPaymentrpcserver.go: dispatchPaymentIntentrouting/router_test.go: SendPayment call sitesInspect captured patch +34 / −16
diff --git a/routing/router.go b/routing/router.go
index 71dcf7d..c0bc15c 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 9bc7bdb..f1c6c38 100644
--- a/routing/router_test.go
+++ b/routing/router_test.go
@@ -328,7 +328,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)
@@ -407,7 +409,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)
@@ -638,7 +642,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)
@@ -745,7 +751,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)
@@ -871,7 +879,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)
@@ -994,7 +1004,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)
@@ -1020,7 +1032,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)
@@ -1089,7 +1103,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
@@ -1134,7 +1148,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)
@@ -1174,7 +1190,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)
@@ -1306,7 +1324,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)
@@ -1331,7 +1349,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 3909fb1..c40d973 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -6022,7 +6022,7 @@ func (r *rpcServer) dispatchPaymentIntent(ctx context.Context,
}
preImage, route, routerErr = r.server.chanRouter.SendPayment(
- payment,
+ ctx, payment,
)
} else {
var attempt *paymentsdb.HTLCAttempt
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.