lightningd: fix case where injectpaymentonion failure results in listsendpays "pending".
What changed, and why it matters
This commit fixes a bookkeeping bug in Core Lightning's payment handling. When a developer/admin command called `injectpaymentonion` failed to actually send the payment (for example, because the peer's channel daemon wasn't reachable), the software had already marked the payment as 'pending' in its records. The fix moves the registration step to after the actual HTLC (payment packet) is successfully created, so failed attempts no longer show up as stuck pending payments in `listsendpays` or `listpays`.
Apply the patch. It is a low-risk reordering fix with an enabled regression test. Operators using `xpay` or `injectpaymentonion` should upgrade to avoid stale pending payment entries.
Security signals we found
State inconsistency between actual HTLC transmission and payment database registration
Failure path leaves payment marked pending, causing incorrect accounting/UI state
Fix reorders registration to occur only after successful HTLC creation
Evidence from the diff
In json_injectpaymentonion() in lightningd/pay.c, register_payment_and_waiter() was called before send_htlc_out(). If send_htlc_out() returned a failure message (e.g., peer/channeld unavailable), the payment was already registered as pending and would remain visible as pending in listsendpays/listpays. The patch reorders the logic so the payment is only registered after the HTLC is successfully sent. A test that was previously expected to fail (xfail) is now enabled to verify the corrected behavior.
Changed components
lightningd/pay.ctests/test_pay.pyxpay plugin (per changelog)listsendpays / listpays RPC outputInspect captured patch +14 / −13
diff --git a/lightningd/pay.c b/lightningd/pay.c
index c1b27718..a4151994 100644
--- a/lightningd/pay.c
+++ b/lightningd/pay.c
@@ -2076,19 +2076,11 @@ static struct command_result *json_injectpaymentonion(struct command *cmd,
if (command_check_only(cmd))
return command_check_done(cmd);
- register_payment_and_waiter(cmd,
- payment_hash,
- *partid, *groupid,
- *destination_msat, *msat, AMOUNT_MSAT(0),
- label, invstring, local_invreq_id,
- &shared_secret,
- destination);
-
- /* If unknown, we set this equal (so accounting logs 0 fees) */
- if (amount_msat_eq(*destination_msat, AMOUNT_MSAT(0)))
- *destination_msat = *msat;
failmsg = send_htlc_out(tmpctx, next, *msat,
- *cltv, *destination_msat,
+ *cltv,
+ /* If unknown, we set this equal (so accounting logs 0 fees) */
+ amount_msat_eq(*destination_msat, AMOUNT_MSAT(0))
+ ? *msat : *destination_msat,
payment_hash,
next_path_key, NULL, *partid, *groupid,
serialize_onionpacket(tmpctx, rs->next),
@@ -2098,6 +2090,16 @@ static struct command_result *json_injectpaymentonion(struct command *cmd,
"Could not send to first peer: %s",
onion_wire_name(fromwire_peektype(failmsg)));
}
+
+ /* Now HTLC is created, we can add the payment as pending */
+ register_payment_and_waiter(cmd,
+ payment_hash,
+ *partid, *groupid,
+ *destination_msat, *msat, AMOUNT_MSAT(0),
+ label, invstring, local_invreq_id,
+ &shared_secret,
+ destination);
+
return command_still_pending(cmd);
}
diff --git a/tests/test_pay.py b/tests/test_pay.py
index 9820a2bf..9f97ab82 100644
--- a/tests/test_pay.py
+++ b/tests/test_pay.py
@@ -6755,7 +6755,6 @@ def test_injectpaymentonion_failures(node_factory, executor):
assert 'onionreply' in err.value.error['data']
-@pytest.mark.xfail(strict=True)
def test_injectpaymentonion_peerfail(node_factory, executor):
l1, l2 = node_factory.line_graph(2,
opts=[{'may_reconnect': True,
Why this scored 33/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.