pytest: test for crash when enableoffer called on a used single-use offer.
What changed, and why it matters
This commit adds a test that exposes a bug in Core Lightning: if you try to re-enable a single-use offer that has already been used, the daemon hits an internal assertion and crashes. The test is marked as expected-to-fail for now, meaning the actual crash is not fixed by this commit—it only documents the failure. A user with RPC access could trigger a denial-of-service by calling enableoffer on a used single-use offer.
Treat this as a known bug needing a follow-up fix. The enableoffer path should reject or clear OFFER_STATUS_USED_F before setting OFFER_STATUS_ACTIVE_F, or explicitly forbid re-enabling used single-use offers and return the OFFER_USED_SINGLE_USE RPC error. Until fixed, restrict RPC access to trusted callers.
Security signals we found
Daemon abort/fatal assertion triggered through RPC
Denial-of-service via crafted enableoffer call
State-machine validation gap in offer status transitions
Test-only commit marking issue as expected failure
Evidence from the diff
The commit modifies tests/test_pay.py to add a regression test for wallet_offer_enable / json_enableoffer. When enableoffer is invoked on a single-use offer whose status is OFFER_STATUS_SINGLE_F|OFFER_STATUS_USED_F, the code sets OFFER_STATUS_ACTIVE_F, producing the combined status value 7. offer_status_in_db treats that combination as invalid and calls fatal(), aborting the daemon. The test asserts the RPC should instead return error code 1007 (OFFER_USED_SINGLE_USE). It is annotated with @pytest.mark.xfail(strict=True), so the patch records the bug rather than resolving it.
Changed components
lightningd/offer.c (json_enableoffer)wallet/wallet.c (wallet_offer_enable)wallet/wallet.h (offer_status_in_db)tests/test_pay.pyInspect captured patch +12 / −3
diff --git a/tests/test_pay.py b/tests/test_pay.py
index bf03297a..980f3935 100644
--- a/tests/test_pay.py
+++ b/tests/test_pay.py
@@ -4509,6 +4509,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,
@@ -4570,9 +4571,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)
@@ -4593,6 +4596,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)
Why this scored 42/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.