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',