diff --git a/common/bolt11.c b/common/bolt11.c index 5ec8f8f71..b33ac132e 100644 --- a/common/bolt11.c +++ b/common/bolt11.c @@ -1119,11 +1119,16 @@ char *bolt11_encode_(const tal_t *ctx, /* Thus we do built-in fields, then extras last. */ encode_p(&data, &b11->payment_hash); - if (b11->description) - encode_d(&data, b11->description); - + /* BOLT #11: + * A writer: + *... + * - MUST include either exactly one `d` or exactly one `h` field. + */ + /* We sometimes keep description around (to put in db), so prefer hash */ if (b11->description_hash) encode_h(&data, b11->description_hash); + else if (b11->description) + encode_d(&data, b11->description); if (n_field) encode_n(&data, &b11->receiver_id); diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index e6d1dc584..ed05203f5 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -828,7 +828,7 @@ class LightningRpc(UnixDomainSocketRpc): return self.call("help", payload) def invoice(self, msatoshi, label, description, expiry=None, fallbacks=None, - preimage=None, exposeprivatechannels=None, cltv=None): + preimage=None, exposeprivatechannels=None, cltv=None, deschashonly=None): """ Create an invoice for {msatoshi} with {label} and {description} with optional {expiry} seconds (default 1 week). @@ -842,6 +842,7 @@ class LightningRpc(UnixDomainSocketRpc): "preimage": preimage, "exposeprivatechannels": exposeprivatechannels, "cltv": cltv, + "deschashonly": deschashonly, } return self.call("invoice", payload) diff --git a/doc/lightning-invoice.7.md b/doc/lightning-invoice.7.md index 5347625ac..b52ed45b4 100644 --- a/doc/lightning-invoice.7.md +++ b/doc/lightning-invoice.7.md @@ -5,7 +5,7 @@ SYNOPSIS -------- **invoice** *msatoshi* *label* *description* [*expiry*] -[*fallbacks*] [*preimage*] [*exposeprivatechannels*] [*cltv*] +[*fallbacks*] [*preimage*] [*exposeprivatechannels*] [*cltv*] [*deschashonly*] DESCRIPTION ----------- @@ -29,8 +29,9 @@ of this invoice. The *description* is a short description of purpose of payment, e.g. *1 cup of coffee*. This value is encoded into the BOLT11 invoice and is -viewable by any node you send this invoice to. It must be UTF-8, and -cannot use *\\u* JSON escape codes. +viewable by any node you send this invoice to (unless *deschashonly* is +true as described below). It must be UTF-8, and cannot use *\\u* JSON +escape codes. The *expiry* is optionally the time the invoice is valid for; without a suffix it is interpreted as seconds, otherwise suffixes *s*, *m*, *h*, @@ -68,6 +69,11 @@ payment. If specified, *cltv* sets the *min_final_cltv_expiry* for the invoice. Otherwise, it's set to the parameter **cltv-final**. +If *deschash* is true (default false), then the bolt11 returned +contains a hash of the *description*, rather than the *description* +itself: this allows much longer descriptions, but they must be +communicated via some other mechanism. + RETURN VALUE ------------ diff --git a/lightningd/invoice.c b/lightningd/invoice.c index 9e205d9de..fadbb4c6f 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -1135,6 +1135,7 @@ static struct command_result *json_invoice(struct command *cmd, u32 *cltv; struct jsonrpc_request *req; struct plugin *plugin; + bool *hashonly; #if DEVELOPER const jsmntok_t *routes; #endif @@ -1153,6 +1154,7 @@ static struct command_result *json_invoice(struct command *cmd, &info->chanhints), p_opt_def("cltv", param_number, &cltv, cmd->ld->config.cltv_final), + p_opt_def("deschashonly", param_bool, &hashonly, false), #if DEVELOPER p_opt("dev-routes", param_array, &routes), #endif @@ -1165,7 +1167,7 @@ static struct command_result *json_invoice(struct command *cmd, INVOICE_MAX_LABEL_LEN); } - if (strlen(desc_val) > BOLT11_FIELD_BYTE_LIMIT) { + if (strlen(desc_val) > BOLT11_FIELD_BYTE_LIMIT && !*hashonly) { return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Descriptions greater than %d bytes " "not yet supported " @@ -1207,7 +1209,18 @@ static struct command_result *json_invoice(struct command *cmd, info->b11->min_final_cltv_expiry = *cltv; info->b11->expiry = *expiry; info->b11->description = tal_steal(info->b11, desc_val); - info->b11->description_hash = NULL; + /* BOLT #11: + * * `h` (23): `data_length` 52. 256-bit description of purpose of payment (SHA256). + *... + * A writer: + *... + * - MUST include either exactly one `d` or exactly one `h` field. + */ + if (*hashonly) { + info->b11->description_hash = tal(info->b11, struct sha256); + sha256(info->b11->description_hash, desc_val, strlen(desc_val)); + } else + info->b11->description_hash = NULL; info->b11->payment_secret = tal_dup(info->b11, struct secret, &payment_secret); info->b11->features = tal_dup_talarr(info->b11, u8, diff --git a/tests/test_invoices.py b/tests/test_invoices.py index d31fade61..6cbf960d6 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -712,3 +712,24 @@ def test_listinvoices_filter(node_factory): for q in queries: r = l1.rpc.listinvoices(**q) assert len(r['invoices']) == 0 + + +def test_invoice_deschash(node_factory, chainparams): + l1, l2 = node_factory.line_graph(2) + + # BOLT #11: + # * `h`: tagged field: hash of description + # * `p5`: `data_length` (`p` = 1, `5` = 20; 1 * 32 + 20 == 52) + # * `8yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs`: SHA256 of 'One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon' + inv = l2.rpc.invoice(42, 'label', 'One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon', deschashonly=True) + assert '8yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs' in inv['bolt11'] + + b11 = l2.rpc.decodepay(inv['bolt11']) + assert 'description' not in b11 + assert b11['description_hash'] == '3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1' + + listinv = only_one(l2.rpc.listinvoices()['invoices']) + assert listinv['description'] == 'One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon' + + # Make sure we can pay it! + l1.rpc.pay(inv['bolt11'])