lightningd: don't allow invoices with 640 byte descriptions.
What changed, and why it matters
This fix stops Core Lightning from creating invalid BOLT11 invoices when a user supplies a 640-byte payment description. Previously the software treated 640 bytes as acceptable, but the Lightning protocol only allows 639 bytes, so a 640-byte description produced a malformed invoice that other wallets might reject or fail to parse correctly. The change tightens the length check and updates an internal constant so the limit is now correctly 639 bytes.
Upgrade nodes to a release containing this commit. If running an older version, avoid creating invoices with 640-byte descriptions and consider rejecting or re-issuing any such invoices already generated.
Security signals we found
Protocol compliance bug producing malformed invoices
Off-by-one length limit in BOLT11 field encoding
Input validation gap in JSON-RPC invoice endpoint
Potential interoperability/parsing failure for recipients
Evidence from the diff
BOLT11_FIELD_BYTE_LIMIT was defined as ((1 << 10) * 5 / 8) = 640, but BOLT11 tagged fields can only carry 1023*5 bits = 639 bytes of data. The constant was therefore one greater than the true limit. The patch redefines it to 639 and changes the invoice RPC to reject descriptions longer than 639 bytes. The keysend plugin’s deschashonly fallback is also switched from >= to > so a 639-byte description is still emitted inline rather than hashed. A regression test verifies that 639 bytes succeeds, while 640 and 641 bytes fail.
Changed components
lightningd/invoice.cplugins/keysend.ccommon/bolt11.hInspect captured patch +28 / −4
diff --git a/common/bolt11.h b/common/bolt11.h
index e802fc80..b75ad55e 100644
--- a/common/bolt11.h
+++ b/common/bolt11.h
@@ -9,7 +9,7 @@
#include <secp256k1_recovery.h>
/* We only have 10 bits for the field length, meaning < 640 bytes */
-#define BOLT11_FIELD_BYTE_LIMIT ((1 << 10) * 5 / 8)
+#define BOLT11_FIELD_BYTE_LIMIT (((1 << 10) * 5 / 8) - 1)
/* BOLT #11:
* * `c` (24): `data_length` variable.
diff --git a/lightningd/invoice.c b/lightningd/invoice.c
index 198a58e1..49933a93 100644
--- a/lightningd/invoice.c
+++ b/lightningd/invoice.c
@@ -1134,8 +1134,8 @@ static struct command_result *json_invoice(struct command *cmd,
if (strlen(desc_val) > BOLT11_FIELD_BYTE_LIMIT && !*hashonly) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
- "Descriptions greater than %d bytes "
- "not yet supported "
+ "Description greater than %d bytes "
+ "invalid "
"(description length %zu)",
BOLT11_FIELD_BYTE_LIMIT,
strlen(desc_val));
diff --git a/plugins/keysend.c b/plugins/keysend.c
index 5a7071e3..58c66cbc 100644
--- a/plugins/keysend.c
+++ b/plugins/keysend.c
@@ -561,7 +561,7 @@ static struct command_result *htlc_accepted_call(struct command *cmd,
(const char *)desc_field->value);
json_add_string(req->js, "description", desc);
/* Don't exceed max possible desc length! */
- if (strlen(desc) >= BOLT11_FIELD_BYTE_LIMIT)
+ if (strlen(desc) > BOLT11_FIELD_BYTE_LIMIT)
json_add_bool(req->js, "deschashonly", true);
} else {
json_add_string(req->js, "description", "keysend");
diff --git a/tests/test_invoices.py b/tests/test_invoices.py
index 1e026041..68df91c4 100644
--- a/tests/test_invoices.py
+++ b/tests/test_invoices.py
@@ -937,3 +937,27 @@ def test_invoice_botched_migration(node_factory, chainparams):
assert ([(i['created_index'], i['label']) for i in l1.rpc.listinvoices()["invoices"]]
== [(1, "made_after_bad_migration"), (2, "label1")])
assert l1.rpc.invoice(100, "test", "test")["created_index"] == 3
+
+
+def test_invoice_maxdesc(node_factory, chainparams):
+ l1, l2 = node_factory.line_graph(2)
+
+ # BOLT #11:
+ #
+ # Note that the maximum length of a Tagged Field's `data` is constricted
+ # by the maximum value of `data_length`. This is 1023 x 5 bits, or 639
+ # bytes.
+ maxdesc = "x" * 639
+
+ # This should fail!
+ with pytest.raises(RpcError, match=r'Description greater than 639 bytes invalid \(description length 641\)'):
+ l1.rpc.invoice(123000, 'test_invoice_maxdesc', maxdesc + 'xx')
+
+ # This should also fail, but used to produce
+ # lnbcrt1230n1p5dm097sp545trjl795r3mm86mk4ln5jpjvnh04x8aryl3qadjt99vspu646zspp52hf43ln8vg0564ljwccs8d84xc70ls8n7wdmp75ygp7ll8rprqzsdqq0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rcxqyjw5qcqp99qxpqysgqr6l8swzm6jc42ehy4v7s83jrggtwa9ua39cvy46c46tmqwn97mn43ycww7e9cf4w5ws8lxnef2k3m5nfa5c34nz54jaxhzc5e72q0ccq26n9fx
+ with pytest.raises(RpcError, match=r'Description greater than 639 bytes invalid \(description length 640\)'):
+ l1.rpc.invoice(123000, 'test_invoice_maxdesc2', maxdesc + 'x')
+
+ # This should succeed.
+ inv = l1.rpc.invoice(123000, 'test_invoice_maxdesc3', maxdesc)
+ assert l1.rpc.decode(inv['bolt11'])['description'] == maxdesc
Why this scored 37/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.