From d5c736fe8668c81f59e230604aebeac8d74c36c9 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 2 Apr 2022 13:03:05 +1030 Subject: [PATCH] pay: require description if bolt11 only has hash. Signed-off-by: Rusty Russell Changelog-Added: JSON-RPC: `pay` has `description` parameter, will be required if bolt11 only has a hash. Changelog-Deprecated: JSON-RPC: `pay` for a bolt11 which uses a `description_hash`, without setting `description`. --- contrib/pyln-client/pyln/client/lightning.py | 3 ++- doc/lightning-pay.7.md | 7 +++++- doc/schemas/pay.request.json | 3 +++ plugins/pay.c | 23 ++++++++++++++++++-- tests/test_invoices.py | 10 +++++++-- tests/test_plugin.py | 2 +- 6 files changed, 41 insertions(+), 7 deletions(-) diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 4996c81fb..c0c649db4 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -998,7 +998,7 @@ class LightningRpc(UnixDomainSocketRpc): def pay(self, bolt11, msatoshi=None, label=None, riskfactor=None, maxfeepercent=None, retry_for=None, maxdelay=None, exemptfee=None, localofferid=None, exclude=None, - maxfee=None): + maxfee=None, description=None): """ Send payment specified by {bolt11} with {msatoshi} (ignored if {bolt11} has an amount), optional {label} @@ -1016,6 +1016,7 @@ class LightningRpc(UnixDomainSocketRpc): "localofferid": localofferid, "exclude": exclude, "maxfee": maxfee, + "description": description, } return self.call("pay", payload) diff --git a/doc/lightning-pay.7.md b/doc/lightning-pay.7.md index dd1ff3ad2..223bfb086 100644 --- a/doc/lightning-pay.7.md +++ b/doc/lightning-pay.7.md @@ -6,7 +6,7 @@ SYNOPSIS **pay** *bolt11* [*msatoshi*] [*label*] [*riskfactor*] [*maxfeepercent*] [*retry_for*] [*maxdelay*] [*exemptfee*] -[*localofferid*] [*exclude*] [*maxfee*] +[*localofferid*] [*exclude*] [*maxfee*] [*description*] DESCRIPTION ----------- @@ -43,6 +43,11 @@ creates an absolute limit on what fee we will pay. This allows you to implement your own heuristics rather than the primitive ones used here. +*description* is only required for bolt11 invoices which do not +contain a description themselves, but contain a description hash. +*description* is then checked against the hash inside the invoice +before it will be paid. + The response will occur when the payment fails or succeeds. Once a payment has succeeded, calls to **pay** with the same *bolt11* will succeed immediately. diff --git a/doc/schemas/pay.request.json b/doc/schemas/pay.request.json index b6dc27a1a..a02750911 100644 --- a/doc/schemas/pay.request.json +++ b/doc/schemas/pay.request.json @@ -48,6 +48,9 @@ }, "maxfee": { "type": "msat" + }, + "description": { + "type": "string" } } } diff --git a/plugins/pay.c b/plugins/pay.c index 7bc0eb92c..7cebac54c 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -926,7 +926,7 @@ static struct command_result *json_pay(struct command *cmd, u64 *maxfee_pct_millionths; u32 *maxdelay; struct amount_msat *exemptfee, *msat, *maxfee; - const char *label; + const char *label, *description; unsigned int *retryfor; u64 *riskfactor_millionths; struct shadow_route_data *shadow_route; @@ -959,6 +959,7 @@ static struct command_result *json_pay(struct command *cmd, p_opt("localofferid", param_sha256, &local_offer_id), p_opt("exclude", param_route_exclusion_array, &exclusions), p_opt("maxfee", param_msat, &maxfee), + p_opt("description", param_string, &description), #if DEVELOPER p_opt_def("use_shadow", param_bool, &use_shadow, true), #endif @@ -971,7 +972,7 @@ static struct command_result *json_pay(struct command *cmd, if (!bolt12_has_prefix(b11str)) { b11 = bolt11_decode(tmpctx, b11str, plugin_feature_set(cmd->plugin), - NULL, chainparams, &b11_fail); + description, chainparams, &b11_fail); if (b11 == NULL) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Invalid bolt11: %s", b11_fail); @@ -999,6 +1000,24 @@ static struct command_result *json_pay(struct command *cmd, cmd, JSONRPC2_INVALID_PARAMS, "Invalid bolt11:" " sets feature var_onion with no secret"); + + /* BOLT #11: + * A reader: + *... + * - MUST check that the SHA2 256-bit hash in the `h` field + * exactly matches the hashed description. + */ + if (!b11->description && !deprecated_apis) { + if (!b11->description_hash) { + return command_fail(cmd, + JSONRPC2_INVALID_PARAMS, + "Invalid bolt11: missing description"); + } + if (!description) + return command_fail(cmd, + JSONRPC2_INVALID_PARAMS, + "bolt11 uses description_hash, but you did not provide description parameter"); + } } else { b12 = invoice_decode(tmpctx, b11str, strlen(b11str), plugin_feature_set(cmd->plugin), diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 1dec61081..ba66a2efe 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -707,8 +707,14 @@ def test_invoice_deschash(node_factory, chainparams): 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']) + # To pay it we need to provide the (correct!) description. + with pytest.raises(RpcError, match=r'you did not provide description parameter'): + l1.rpc.pay(inv['bolt11']) + + with pytest.raises(RpcError, match=r'does not match description'): + l1.rpc.pay(inv['bolt11'], description=listinv['description'][:-1]) + + l1.rpc.pay(inv['bolt11'], description=listinv['description']) # Try removing description. l2.rpc.delinvoice('label', "paid", desconly=True) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 01b90631d..5cb9e8f81 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -398,7 +398,7 @@ def test_pay_plugin(node_factory): # Make sure usage messages are present. msg = 'pay bolt11 [msatoshi] [label] [riskfactor] [maxfeepercent] '\ '[retry_for] [maxdelay] [exemptfee] [localofferid] [exclude] '\ - '[maxfee]' + '[maxfee] [description]' if DEVELOPER: msg += ' [use_shadow]' assert only_one(l1.rpc.help('pay')['help'])['command'] == msg