From 8f833e09041febf59b8d8d5f1f1f3d59362784b6 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 28 Jan 2026 15:21:40 +1030 Subject: [PATCH 1/2] pytest: test for crash when enableoffer called on a used single-use offer. Assertion happens here: newstatus = offer_status_in_db(s | OFFER_STATUS_ACTIVE_F); Since OFFER_STATUS_SINGLE_F|OFFER_STATUS_USED_F|OFFER_STATUS_ACTIVE_F is not a valid combination: ``` lightningd-3 2026-01-28T04:45:21.184Z **BROKEN** lightningd: offer_status_in_db: 7 is invalid lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: FATAL SIGNAL 6 (version v25.12-92-g7fff32d-modded) lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: common/daemon.c:83 (crashdump) 0x5a883759dbb7 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ./signal/../sysdeps/unix/sysv/linux/x86_64/libc_sigaction.c:0 ((null)) 0x79a2b0c4532f lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ./nptl/pthread_kill.c:44 (__pthread_kill_implementation) 0x79a2b0c9eb2c lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ./nptl/pthread_kill.c:78 (__pthread_kill_internal) 0x79a2b0c9eb2c lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ./nptl/pthread_kill.c:89 (__GI___pthread_kill) 0x79a2b0c9eb2c lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ../sysdeps/posix/raise.c:26 (__GI_raise) 0x79a2b0c4527d lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ./stdlib/abort.c:79 (__GI_abort) 0x79a2b0c288fe lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/log.c:1054 (fatal_vfmt) 0x5a8837509557 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/log.c:1064 (fatal) 0x5a88375095fe lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ./wallet/wallet.h:1451 (offer_status_in_db) 0x5a88375491dc lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: wallet/wallet.c:6160 (offer_status_in_db) 0x5a8837555388 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: wallet/wallet.c:6162 (wallet_offer_enable) 0x5a8837555388 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/offer.c:288 (json_enableoffer) 0x5a8837540939 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/jsonrpc.c:769 (command_exec) 0x5a8837503198 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/jsonrpc.c:910 (rpc_command_hook_final) 0x5a8837503198 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/jsonrpc.c:884 (rpc_command_hook_final) 0x5a8837503198 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/plugin_hook.c:243 (hook_done) 0x5a8837535383 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/plugin_hook.c:343 (plugin_hook_call_next) 0x5a8837535383 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/jsonrpc.c:998 (plugin_hook_call_rpc_command) 0x5a8837503c4f lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/jsonrpc.c:1123 (parse_request) 0x5a8837503c4f lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/jsonrpc.c:1217 (read_json) 0x5a8837503c4f lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ccan/ccan/io/io.c:60 (next_plan) 0x5a88375eca38 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ccan/ccan/io/io.c:422 (do_plan) 0x5a88375eca38 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ccan/ccan/io/io.c:439 (io_ready) 0x5a88375eca38 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ccan/ccan/io/poll.c:470 (io_loop) 0x5a88375eead5 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/io_loop_with_timers.c:22 (io_loop_with_timers) 0x5a8837501f8e lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: lightningd/lightningd.c:1492 (main) 0x5a88374d3c27 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ../sysdeps/nptl/libc_start_call_main.h:58 (__libc_start_call_main) 0x79a2b0c2a1c9 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: ../csu/libc-start.c:360 (__libc_start_main_impl) 0x79a2b0c2a28a lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: (null):0 ((null)) 0x5a88374d5aa4 lightningd-3 2026-01-28T04:45:21.260Z **BROKEN** lightningd: backtrace: (null):0 ((null)) 0xffffffffffffffff ``` Signed-off-by: Rusty Russell --- tests/test_pay.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_pay.py b/tests/test_pay.py index 48abcb3142ab..b7c0b024263f 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -4530,6 +4530,7 @@ def test_fetchinvoice_3hop(node_factory, bitcoind): l1.rpc.call('fetchinvoice', {'offer': offer1['bolt12']}) +@pytest.mark.xfail(strict=True) def test_fetchinvoice(node_factory, bitcoind): # We remove the conversion plugin on l3, causing it to get upset. l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True, @@ -4591,9 +4592,11 @@ def test_fetchinvoice(node_factory, bitcoind): assert 'msat' not in inv1['changes'] # Single-use invoice can be fetched multiple times, only paid once. - offer2 = l3.rpc.call('offer', {'amount': '1msat', - 'description': 'single-use test', - 'single_use': True})['bolt12'] + offer2_ret = l3.rpc.call('offer', {'amount': '1msat', + 'description': 'single-use test', + 'single_use': True}) + offer2 = offer2_ret['bolt12'] + offer2_id = offer2_ret['offer_id'] # We've done 3 onion calls: sleep now to avoid hitting ratelimit! time.sleep(1) @@ -4614,6 +4617,12 @@ def test_fetchinvoice(node_factory, bitcoind): with pytest.raises(RpcError, match='Offer no longer available'): l1.rpc.call('fetchinvoice', {'offer': offer2}) + # Can't enable it either! + OFFER_USED_SINGLE_USE = 1007 + with pytest.raises(RpcError) as excinfo: + l3.rpc.enableoffer(offer_id=offer2_id) + assert excinfo.value.error['code'] == OFFER_USED_SINGLE_USE + # Now, test amount in different currency! plugin = os.path.join(os.path.dirname(__file__), 'plugins/currencyUSDAUD5000.py') l3.rpc.plugin_start(plugin) From 81355bde7a2f70da98061f3f14f2fb7f314c90af Mon Sep 17 00:00:00 2001 From: 21M4TW <21m4tw@proton.me> Date: Wed, 28 Jan 2026 15:23:11 +1030 Subject: [PATCH 2/2] lightningd: don't allow enableoffer on single-use offer. Changelog-Fixed: enableoffer: Adding an error when trying to activate an used single use offer (don't crash!) --- common/jsonrpc_errors.h | 1 + lightningd/offer.c | 4 ++++ tests/test_pay.py | 1 - 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h index d3fe80d53997..a2ea1bf1b2e1 100644 --- a/common/jsonrpc_errors.h +++ b/common/jsonrpc_errors.h @@ -116,6 +116,7 @@ enum jsonrpc_errcode { OFFER_BAD_INVREQ_REPLY = 1004, OFFER_TIMEOUT = 1005, OFFER_ALREADY_ENABLED = 1006, + OFFER_USED_SINGLE_USE = 1007, /* Errors from datastore command */ DATASTORE_DEL_DOES_NOT_EXIST = 1200, diff --git a/lightningd/offer.c b/lightningd/offer.c index bd4357eb33af..50edd4d4f9b6 100644 --- a/lightningd/offer.c +++ b/lightningd/offer.c @@ -282,6 +282,10 @@ static struct command_result *json_enableoffer(struct command *cmd, return command_fail(cmd, OFFER_ALREADY_ENABLED, "offer already active"); + if (offer_status_single(status) && offer_status_used(status)) + return command_fail(cmd, OFFER_USED_SINGLE_USE, + "cannot activate an used single use offer"); + if (command_check_only(cmd)) return command_check_done(cmd); diff --git a/tests/test_pay.py b/tests/test_pay.py index b7c0b024263f..cf016f58ef5d 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -4530,7 +4530,6 @@ def test_fetchinvoice_3hop(node_factory, bitcoind): l1.rpc.call('fetchinvoice', {'offer': offer1['bolt12']}) -@pytest.mark.xfail(strict=True) def test_fetchinvoice(node_factory, bitcoind): # We remove the conversion plugin on l3, causing it to get upset. l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True,