From 225ca6aba395208614ad41ad03c7d86f06ec57d6 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sun, 26 Oct 2025 20:32:45 +1030 Subject: [PATCH 1/2] pytest: test that we correctly mark a payment part failed if we cannot queue it to the channeld for the peer. Signed-off-by: Rusty Russell --- tests/test_pay.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_pay.py b/tests/test_pay.py index 1afddd56c0fe..9820a2bf37b5 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -6755,6 +6755,50 @@ 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, + 'dev-no-reconnect': None, + 'disconnect': ['=WIRE_UPDATE_ADD_HTLC', '-WIRE_COMMITMENT_SIGNED']}, + {'may_reconnect': True, + 'dev-no-reconnect': None}]) + blockheight = l1.rpc.getinfo()['blockheight'] + + inv1 = l2.rpc.invoice(1000, "test_injectpaymentonion_peerfail", "test_injectpaymentonion_peerfail") + + # First hop for injectpaymentonion is self. + hops = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_tlv(1000, 18 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l2.info['id'], + 'payload': serialize_payload_final_tlv(1000, 18, 1000, blockheight, inv1['payment_secret']).hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata=inv1['payment_hash']) + + l1.rpc.disconnect(l2.info['id'], force=True) + with pytest.raises(RpcError, match='WIRE_TEMPORARY_CHANNEL_FAILURE'): + l1.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash=inv1['payment_hash'], + amount_msat=1000, + cltv_expiry=blockheight + 18 + 6, + partid=1, + groupid=0) + # In fact, it won't create any sendpays entry, since it fails too early. + assert l1.rpc.listsendpays() == {'payments': []} + + # This will hang, since we disconnect once committed. But provides another + # (legitimately) pending payment for our migration code to test. + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + executor.submit(l1.rpc.injectpaymentonion, + onion=onion['onion'], + payment_hash=inv1['payment_hash'], + amount_msat=1000, + cltv_expiry=blockheight + 18 + 6, + partid=2, + groupid=0) + l1.daemon.wait_for_log("dev_disconnect: =WIRE_UPDATE_ADD_HTLC") + assert [p['status'] for p in l1.rpc.listsendpays()['payments']] == ['pending'] + + def test_parallel_channels_reserve(node_factory, bitcoind): """Tests wether we are able to pay through parallel channels concurrently. To do that we need to enable strict-forwarding.""" From 725b86427d51f5c0ee9281c7171625dd3bde4ee7 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 3 Nov 2025 13:16:54 +1030 Subject: [PATCH 2/2] lightningd: fix case where injectpaymentonion failure results in listsendpays "pending". If we failed after we register (e.g. channeld not available), we don't mark it failed. We shouldn't register until we've definitely created the htlc. Changelog-Fixed: `xpay` would sometimes leave payment parts status `pending` in failure cases (as seen in listpays or listsendpays). Signed-off-by: Rusty Russell Fixes: https://github.com/ElementsProject/lightning/issues/8629 --- lightningd/pay.c | 26 ++++++++++++++------------ tests/test_pay.py | 1 - 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lightningd/pay.c b/lightningd/pay.c index c1b277185810..a4151994af98 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 9820a2bf37b5..9f97ab82820e 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,