diff --git a/lightningd/offer.c b/lightningd/offer.c index a79fb6d3d..9b34dd5d7 100644 --- a/lightningd/offer.c +++ b/lightningd/offer.c @@ -229,7 +229,8 @@ AUTODATA(json_command, &disableoffer_command); * but our main purpose is to fill in invreq->payer_info tweak. */ static struct command_result *prev_payment(struct command *cmd, const char *label, - struct tlv_invoice_request *invreq) + struct tlv_invoice_request *invreq, + u64 **prev_basetime) { const struct wallet_payment **payments; bool prev_paid = false; @@ -294,9 +295,15 @@ static struct command_result *prev_payment(struct command *cmd, prev_paid = true; } - if (inv->payer_info) + if (inv->payer_info) { invreq->payer_info = tal_dup_talarr(invreq, u8, inv->payer_info); + *prev_basetime = tal_dup(cmd, u64, + inv->recurrence_basetime); + } + + if (prev_paid && inv->payer_info) + break; } if (!invreq->payer_info) @@ -363,6 +370,7 @@ static struct command_result *json_createinvoicerequest(struct command *cmd, struct tlv_invoice_request *invreq; const char *label; struct json_stream *response; + u64 *prev_basetime = NULL; if (!param(cmd, buffer, params, p_req("bolt12", param_b12_invreq, &invreq), @@ -377,7 +385,8 @@ static struct command_result *json_createinvoicerequest(struct command *cmd, if (*invreq->recurrence_counter != 0) { struct command_result *err - = prev_payment(cmd, label, invreq); + = prev_payment(cmd, label, invreq, + &prev_basetime); if (err) return err; } @@ -430,6 +439,8 @@ static struct command_result *json_createinvoicerequest(struct command *cmd, if (label) json_add_escaped_string(response, "recurrence_label", take(json_escape(NULL, label))); + if (prev_basetime) + json_add_u64(response, "previous_basetime", *prev_basetime); return command_success(cmd, response); } diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c index 4d6581fbd..c4dffa45f 100644 --- a/plugins/fetchinvoice.c +++ b/plugins/fetchinvoice.c @@ -682,6 +682,70 @@ static struct command_result *invreq_done(struct command *cmd, json_tok_full(buf, t), fail); + /* Now that's given us the previous base, check this is an OK time + * to request an invoice. */ + if (sent->invreq->recurrence_counter) { + u64 *base; + const jsmntok_t *pbtok; + u64 period_idx = *sent->invreq->recurrence_counter; + + if (sent->invreq->recurrence_start) + period_idx += *sent->invreq->recurrence_start; + + /* BOLT-offers #12: + * - if the offer contained `recurrence_limit`: + * - MUST NOT send an `invoice_request` for a period greater + * than `max_period` + */ + if (sent->offer->recurrence_limit + && period_idx > *sent->offer->recurrence_limit) + return command_fail(cmd, LIGHTNINGD, + "Can't send invreq for period %" + PRIu64" (limit %u)", + period_idx, + *sent->offer->recurrence_limit); + + /* BOLT-offers #12: + * - SHOULD NOT send an `invoice_request` for a period which has + * already passed. + */ + /* If there's no recurrence_base, we need a previous payment + * for this: fortunately createinvoicerequest does that + * lookup. */ + pbtok = json_get_member(buf, result, "previous_basetime"); + if (pbtok) { + base = tal(tmpctx, u64); + json_to_u64(buf, pbtok, base); + } else if (sent->offer->recurrence_base) + base = &sent->offer->recurrence_base->basetime; + else { + /* happens with *recurrence_base == 0 */ + assert(*sent->invreq->recurrence_counter == 0); + base = NULL; + } + + if (base) { + u64 period_start, period_end, now = time_now().ts.tv_sec; + offer_period_paywindow(sent->offer->recurrence, + sent->offer->recurrence_paywindow, + sent->offer->recurrence_base, + *base, period_idx, + &period_start, &period_end); + if (now < period_start) + return command_fail(cmd, LIGHTNINGD, + "Too early: can't send until time %" + PRIu64" (in %"PRIu64" secs)", + period_start, + period_start - now); + if (now > period_end) + return command_fail(cmd, LIGHTNINGD, + "Too late: expired time %" + PRIu64" (%"PRIu64" secs ago)", + period_end, + now - period_end); + } + } + rawinvreq = tal_arr(tmpctx, u8, 0); towire_invoice_request(&rawinvreq, sent->invreq); return send_message(cmd, sent, "invoice_request", rawinvreq, @@ -826,14 +890,6 @@ static struct command_result *json_fetchinvoice(struct command *cmd, if (!rec_label) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "needs recurrence_label"); - - /* FIXME! */ - /* BOLT-offers #12: - * - SHOULD NOT send an `invoice_request` for a period which has - * already passed. - */ - /* If there's no recurrence_base, we need the initial payment - * for this... */ } else { /* BOLT-offers #12: * - otherwise: diff --git a/plugins/offers.c b/plugins/offers.c index dc835a3c0..a51ad7988 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -134,7 +134,7 @@ static const struct plugin_command commands[] = { "offer", "payment", "Create an offer", - "Create an offer for invoices of {amount} with {destination}, optional {vendor}, {quantity_min}, {quantity_max}, {absolute_expiry}, {recurrence}, {recurrence_base}, {recurrence_paywindow}, {recurrence_limit} and {single_use}", + "Create an offer for invoices of {amount} with {description}, optional {vendor}, {quantity_min}, {quantity_max}, {absolute_expiry}, {recurrence}, {recurrence_base}, {recurrence_paywindow}, {recurrence_limit} and {single_use}", json_offer }, }; diff --git a/tests/test_pay.py b/tests/test_pay.py index 48d31ecfb..1070eb03d 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -3917,7 +3917,7 @@ def test_fetchinvoice(node_factory, bitcoind): l1.rpc.pay(ret['invoice'], label='test recurrence') # Now we can, but it's too early: - with pytest.raises(RpcError, match='Remote node sent failure message.*too early'): + with pytest.raises(RpcError, match="Too early: can't send until time {}".format(period1['starttime'])): l1.rpc.call('fetchinvoice', {'offer': offer3, 'recurrence_counter': 2, 'recurrence_label': 'test recurrence'}) @@ -3935,6 +3935,31 @@ def test_fetchinvoice(node_factory, bitcoind): with pytest.raises(RpcError, match='Timeout waiting for response'): l1.rpc.call('fetchinvoice', {'offer': offer1, 'timeout': 10}) + # Now try an offer with a more complex paywindow (only 10 seconds before) + offer = l2.rpc.call('offer', {'amount': '1msat', + 'description': 'paywindow test', + 'recurrence': '20seconds', + 'recurrence_paywindow': '-10+0'})['bolt12'] + + ret = l1.rpc.call('fetchinvoice', {'offer': offer, + 'recurrence_counter': 0, + 'recurrence_label': 'test paywindow'}) + period3 = ret['next_period'] + assert period3['counter'] == 1 + assert period3['endtime'] == period3['starttime'] + 19 + assert period3['paywindow_start'] == period3['starttime'] - 10 + assert period3['paywindow_end'] == period3['starttime'] + l1.rpc.pay(ret['invoice'], label='test paywindow') + + # Wait until too late! + while int(time.time()) <= period3['paywindow_end']: + time.sleep(1) + + with pytest.raises(RpcError, match="Too late: expired time {}".format(period3['paywindow_end'])): + l1.rpc.call('fetchinvoice', {'offer': offer, + 'recurrence_counter': 1, + 'recurrence_label': 'test paywindow'}) + def test_pay_waitblockheight_timeout(node_factory, bitcoind): plugin = os.path.join(os.path.dirname(__file__), 'plugins', 'endlesswaitblockheight.py')