Skip to content

Commit

Permalink
invoice: add deschashonly parameter.
Browse files Browse the repository at this point in the history
LNURL wants this so they can include images etc in descriptions.

Replaces: #4892
Changelog-Added: JSON-RPC: `invoice` has a new parameter `deschashonly` to put hash of description in bolt11.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
  • Loading branch information
rustyrussell committed Mar 28, 2022
1 parent 290dfd2 commit ccaf04d
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 9 deletions.
11 changes: 8 additions & 3 deletions common/bolt11.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion contrib/pyln-client/pyln/client/lightning.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,7 @@ def help(self, command=None):
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).
Expand All @@ -842,6 +842,7 @@ def invoice(self, msatoshi, label, description, expiry=None, fallbacks=None,
"preimage": preimage,
"exposeprivatechannels": exposeprivatechannels,
"cltv": cltv,
"deschashonly": deschashonly,
}
return self.call("invoice", payload)

Expand Down
12 changes: 9 additions & 3 deletions doc/lightning-invoice.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ SYNOPSIS
--------

**invoice** *msatoshi* *label* *description* [*expiry*]
[*fallbacks*] [*preimage*] [*exposeprivatechannels*] [*cltv*]
[*fallbacks*] [*preimage*] [*exposeprivatechannels*] [*cltv*] [*deschashonly*]

DESCRIPTION
-----------
Expand All @@ -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*,
Expand Down Expand Up @@ -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
------------

Expand Down
17 changes: 15 additions & 2 deletions lightningd/invoice.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 "
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions tests/test_invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,3 +712,24 @@ def match(node, query, invoice):
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'])

0 comments on commit ccaf04d

Please sign in to comment.