Browse Source

fetchinvoice: allow amounts to be specified.

As per lastest revision of the spec, we can specify amounts in invoice
requests even if the offer already specifies it, as long as we exceed
the amount given.  This allows for tipping, and amount obfuscation.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
ppa
Rusty Russell 4 years ago
committed by Christian Decker
parent
commit
af46a4f57d
  1. 13
      plugins/fetchinvoice.c
  2. 46
      plugins/offers_invreq_hook.c
  3. 16
      tests/test_pay.py

13
plugins/fetchinvoice.c

@ -818,12 +818,17 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
* lightning-payable unit (e.g. milli-satoshis for bitcoin) for the
* first `chains` entry.
* - otherwise:
* - MUST NOT set `amount`
* - MAY omit `amount`.
* - if it sets `amount`:
* - MUST specify `amount`.`msat` as greater or equal to amount
* expected by the offer (before any proportional period amount).
*/
if (sent->offer->amount) {
if (msat)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"msatoshi parameter unnecessary");
/* FIXME: Check after quantity? */
if (msat) {
invreq->amount = tal_dup(invreq, u64,
&msat->millisatoshis); /* Raw: tu64 */
}
} else {
if (!msat)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,

46
plugins/offers_invreq_hook.c

@ -410,19 +410,11 @@ static struct command_result *invreq_amount_by_quantity(struct command *cmd,
const struct invreq *ir,
u64 *raw_amt)
{
struct command_result *err;
assert(ir->offer->amount);
/* BOLT-offers #12:
*
* - if the offer included `amount`:
* - MUST fail the request if it contains `amount`.
* - MUST calculate the *base invoice amount* using the offer `amount`:
*/
err = invreq_must_not_have(cmd, ir, amount);
if (err)
return err;
*raw_amt = *ir->offer->amount;
/* BOLT-offers #12:
@ -476,10 +468,42 @@ static struct command_result *invreq_base_amount_simple(struct command *cmd,
static struct command_result *handle_amount_and_recurrence(struct command *cmd,
struct invreq *ir,
struct amount_msat amount)
struct amount_msat base_inv_amount)
{
/* BOLT-offers #12:
* - if the offer included `amount`:
*...
* - if the request contains `amount`:
* - MUST fail the request if its `amount` is less than the
* *base invoice amount*.
*/
if (ir->offer->amount && ir->invreq->amount) {
if (amount_msat_less(amount_msat(*ir->invreq->amount), base_inv_amount)) {
return fail_invreq(cmd, ir, "Amount must be at least %s",
type_to_string(tmpctx, struct amount_msat,
&base_inv_amount));
}
/* BOLT-offers #12:
* - MAY fail the request if its `amount` is much greater than
* the *base invoice amount*.
*/
/* Much == 5? Easier to divide and compare, than multiply. */
if (amount_msat_greater(amount_msat_div(amount_msat(*ir->invreq->amount), 5),
base_inv_amount)) {
return fail_invreq(cmd, ir, "Amount vastly exceeds %s",
type_to_string(tmpctx, struct amount_msat,
&base_inv_amount));
}
/* BOLT-offers #12:
* - MUST use the request's `amount` as the *base invoice
* amount*.
*/
base_inv_amount = amount_msat(*ir->invreq->amount);
}
/* This may be adjusted by recurrence if proportional_amount set */
ir->inv->amount = tal_dup(ir->inv, u64,
&amount.millisatoshis); /* Raw: wire protocol */
&base_inv_amount.millisatoshis); /* Raw: wire protocol */
/* Last of all, we handle recurrence details, which often requires
* further lookups. */

16
tests/test_pay.py

@ -3859,7 +3859,7 @@ def test_fetchinvoice(node_factory, bitcoind):
{'allow_broken_log': True}])
# Simple offer first.
offer1 = l3.rpc.call('offer', {'amount': '1msat',
offer1 = l3.rpc.call('offer', {'amount': '2msat',
'description': 'simple test'})['bolt12']
inv1 = l1.rpc.call('fetchinvoice', {'offer': offer1})
@ -3870,6 +3870,20 @@ def test_fetchinvoice(node_factory, bitcoind):
l1.rpc.pay(inv1['invoice'])
l1.rpc.pay(inv2['invoice'])
# We can also set the amount explicitly, to tip.
inv1 = l1.rpc.call('fetchinvoice', {'offer': offer1, 'msatoshi': 3})
assert l1.rpc.call('decode', [inv1['invoice']])['amount_msat'] == 3
l1.rpc.pay(inv1['invoice'])
# More than ~5x expected is rejected as absurd (it's actually a divide test,
# which means we need 15 here, not 11).
with pytest.raises(RpcError, match="Remote node sent failure message.*Amount vastly exceeds 2msat"):
l1.rpc.call('fetchinvoice', {'offer': offer1, 'msatoshi': 15})
# Underpay is rejected.
with pytest.raises(RpcError, match="Remote node sent failure message.*Amount must be at least 2msat"):
l1.rpc.call('fetchinvoice', {'offer': offer1, 'msatoshi': 1})
# Single-use invoice can be fetched multiple times, only paid once.
offer2 = l3.rpc.call('offer', {'amount': '1msat',
'description': 'single-use test',

Loading…
Cancel
Save