lightningd: don't allow enableoffer on single-use offer.
What changed, and why it matters
This commit fixes a bug in Core Lightning's offer management. A 'single-use offer' is meant to be used once and then become inactive. Previously, calling the enableoffer command on an already-used single-use offer could cause the node to crash. The fix adds a clear error message instead of crashing. It also removes an 'expected failure' marker from a related test, meaning that test now passes.
Treat as a low-severity hardening fix. Upgrade nodes that expose offer RPCs to users, and review whether any automated tooling or plugins call enableoffer on single-use offers, since they will now receive error code 1007 instead of a crash.
Security signals we found
Denial-of-service vector: unhandled state transition could crash the lightningd daemon
Input validation gap: enableoffer did not guard against re-activation of used single-use offers
New error code introduced to explicitly reject invalid state transition
Test expectation changed from expected-fail to passing
Evidence from the diff
The patch adds a new JSON-RPC error code OFFER_USED_SINGLE_USE (1007) and a guard in json_enableoffer() in lightningd/offer.c. The guard checks if the offer is both single-use and already used; if so, it returns a command failure rather than proceeding. The change prevents a crash path when re-enabling a consumed single-use offer. A pytest xfail marker is removed from test_fetchinvoice, indicating the underlying issue is now resolved.
Changed components
lightningd/offer.ccommon/jsonrpc_errors.htests/test_pay.pyInspect captured patch +5 / −1
diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h
index d3fe80d5..a2ea1bf1 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 bd4357eb..50edd4d4 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 980f3935..de53bb3c 100644
--- a/tests/test_pay.py
+++ b/tests/test_pay.py
@@ -4509,7 +4509,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,
Why this scored 44/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.