diff --git a/common/bolt11.h b/common/bolt11.h index e802fc801d4a..b75ad55ef430 100644 --- a/common/bolt11.h +++ b/common/bolt11.h @@ -9,7 +9,7 @@ #include /* 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 31362ab6c7ef..97e7cc9d6eec 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -1137,8 +1137,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 6104bf607ccd..d1257e213c8a 100644 --- a/plugins/keysend.c +++ b/plugins/keysend.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -563,7 +564,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) > 1023) + 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 1e0260415de4..68df91c40e18 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 diff --git a/tests/test_pay.py b/tests/test_pay.py index ab78f76a701a..72d5d20e2201 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -11,6 +11,7 @@ tu64_encode ) import copy +import json import os import pytest import random @@ -3697,6 +3698,30 @@ def test_keysend_maxfee(node_factory): assert len(l3.rpc.listinvoices()['invoices']) == 1 +@pytest.mark.parametrize("tlv_payload_length", [638, 639, 640, 641, 1022, 1023, 1024]) +def test_keysend_description_size_limit(node_factory, tlv_payload_length): + """ + Test keysend description handling near BOLT11 field size limits. + + Checks boundary conditions where the payload length is just below, + exactly at, and just above the maximum allowed tagged-field size. + + See common/bolt11.h: BOLT11_FIELD_BYTE_LIMIT. + """ + l1, l2 = node_factory.line_graph(2, wait_for_announce=True) + amt = 10000 + prefix = 'keysend: {"message": ""}' + base_len = len(prefix) + + # Prep TLV payload with test length + body_len = tlv_payload_length - base_len + tlv_payload = json.dumps({"message": "a" * body_len}).encode().hex() + + # Send keysend payment with test payload + l1.rpc.keysend(l2.info["id"], amt, extratlvs={7629169: tlv_payload}) + assert len(l2.rpc.listinvoices()["invoices"]) == 1 + + def test_invalid_onion_channel_update(node_factory): ''' Some onion failures "should" send a `channel_update`.