From af46a4f57d6d921bf5a6ee6e55f6e4158feed6ee Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 9 Jan 2021 14:55:46 +1030 Subject: [PATCH] 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 --- plugins/fetchinvoice.c | 13 ++++++---- plugins/offers_invreq_hook.c | 46 +++++++++++++++++++++++++++--------- tests/test_pay.py | 16 ++++++++++++- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c index acd554a1b..dab08bc4e 100644 --- a/plugins/fetchinvoice.c +++ b/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, diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index 7053cf8cb..852549113 100644 --- a/plugins/offers_invreq_hook.c +++ b/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. */ diff --git a/tests/test_pay.py b/tests/test_pay.py index 4497711af..f28608eea 100644 --- a/tests/test_pay.py +++ b/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',