Skip to content

Commit 5d23bca

Browse files
committed
plugins/offers: handle invoice_request with invreq_recurrence_cancel
In this case, we make an immediately-expiring invoice. This correctly blocks any successive requests for invoices, as per the spec requirement. This means we have to handle invoice_requests without reply_path, amounts or quantity *if* they specify invreq_recurrence_cancel. Signed-off-by: Rusty Russell <[email protected]>
1 parent ba3031d commit 5d23bca

File tree

3 files changed

+107
-23
lines changed

3 files changed

+107
-23
lines changed

plugins/offers.c

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,9 @@ static struct command_result *onion_message_recv(struct command *cmd,
295295
invreqtok = json_get_member(buf, om, "invoice_request");
296296
if (invreqtok) {
297297
const u8 *invreqbin = json_tok_bin_from_hex(tmpctx, buf, invreqtok);
298-
if (reply_path)
299-
return handle_invoice_request(cmd,
300-
invreqbin,
301-
reply_path, secret);
302-
else
303-
plugin_log(cmd->plugin, LOG_DBG,
304-
"invoice_request without reply_path");
298+
return handle_invoice_request(cmd,
299+
invreqbin,
300+
reply_path, secret);
305301
}
306302

307303
invtok = json_get_member(buf, om, "invoice");

plugins/offers_invreq_hook.c

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ fail_invreq_level(struct command *cmd,
7070
err->error = tal_dup_arr(err, char, msg, strlen(msg), 0);
7171
/* FIXME: Add suggested_value / erroneous_field! */
7272

73+
if (!invreq->reply_path)
74+
return command_hook_success(cmd);
75+
7376
payload = tlv_onionmsg_tlv_new(tmpctx);
7477
payload->invoice_error = tal_arr(payload, u8, 0);
7578
towire_tlv_invoice_error(&payload->invoice_error, err);
@@ -194,6 +197,13 @@ static struct command_result *createinvoice_done(struct command *cmd,
194197
json_tok_full(buf, t));
195198
}
196199

200+
/* BOLT-recurrence #12:
201+
* - if `invreq_recurrence_cancel` is present:
202+
* - MUST NOT send an invoice in reply.
203+
*/
204+
if (!ir->reply_path)
205+
return command_hook_success(cmd);
206+
197207
payload = tlv_onionmsg_tlv_new(tmpctx);
198208
payload->invoice = tal_steal(payload, rawinv);
199209
return send_onion_reply(cmd, ir->reply_path, payload);
@@ -206,13 +216,19 @@ static struct command_result *createinvoice_error(struct command *cmd,
206216
struct invreq *ir)
207217
{
208218
u32 code;
219+
const char *status;
209220

210221
/* If it already exists, we can reuse its bolt12 directly. */
211222
if (json_scan(tmpctx, buf, err,
212-
"{code:%}", JSON_SCAN(json_to_u32, &code)) == NULL
223+
"{code:%,data:{status:%}}",
224+
JSON_SCAN(json_to_u32, &code),
225+
JSON_SCAN_TAL(tmpctx, json_strdup, &status)) == NULL
213226
&& code == INVOICE_LABEL_ALREADY_EXISTS) {
214-
return createinvoice_done(cmd, method, buf,
215-
json_get_member(buf, err, "data"), ir);
227+
if (streq(status, "unpaid"))
228+
return createinvoice_done(cmd, method, buf,
229+
json_get_member(buf, err, "data"), ir);
230+
if (streq(status, "expired"))
231+
return fail_invreq(cmd, ir, "invoice expired (cancelled?)");
216232
}
217233
return error(cmd, method, buf, err, ir);
218234
}
@@ -359,6 +375,18 @@ static struct command_result *add_blindedpaths(struct command *cmd,
359375
found_best_peer, ir);
360376
}
361377

378+
static struct command_result *cancel_invoice(struct command *cmd,
379+
struct invreq *ir)
380+
{
381+
/* We create an invoice, so we can mark the cancellation, but with
382+
* expiry 0. And we don't send it to them! */
383+
*ir->inv->invoice_relative_expiry = 0;
384+
385+
/* In case they set a reply path! */
386+
ir->reply_path = tal_free(ir->reply_path);
387+
return create_invoicereq(cmd, ir);
388+
}
389+
362390
static struct command_result *check_period(struct command *cmd,
363391
struct invreq *ir,
364392
u64 basetime)
@@ -470,6 +498,10 @@ static struct command_result *check_period(struct command *cmd,
470498
}
471499
}
472500

501+
/* If this is actually a cancel, we create an expired invoice */
502+
if (ir->invreq->invreq_recurrence_cancel)
503+
return cancel_invoice(cmd, ir);
504+
473505
return add_blindedpaths(cmd, ir);
474506
}
475507

@@ -618,14 +650,18 @@ static struct command_result *invreq_base_amount_simple(struct command *cmd,
618650
* The reader:
619651
*...
620652
* - otherwise (no `offer_amount`):
621-
* - MUST reject the invoice request if it does not contain
622-
* `invreq_amount`.
653+
* - MUST reject the invoice request if `invreq_recurrence_cancel`
654+
* is not present and it does not contain `invreq_amount`.
623655
*/
624-
err = invreq_must_have(cmd, ir, invreq_amount);
625-
if (err)
626-
return err;
627-
628-
*amt = amount_msat(*ir->invreq->invreq_amount);
656+
if (!ir->invreq->invreq_recurrence_cancel) {
657+
err = invreq_must_have(cmd, ir, invreq_amount);
658+
if (err)
659+
return err;
660+
}
661+
if (ir->invreq->invreq_amount)
662+
*amt = amount_msat(*ir->invreq->invreq_amount);
663+
else
664+
*amt = AMOUNT_MSAT(0);
629665
}
630666
return NULL;
631667
}
@@ -763,6 +799,7 @@ static struct command_result *listoffers_done(struct command *cmd,
763799
bool active;
764800
struct command_result *err;
765801
struct amount_msat amt;
802+
struct tlv_invoice_request_invreq_recurrence_cancel *cancel;
766803

767804
/* BOLT #12:
768805
*
@@ -850,23 +887,27 @@ static struct command_result *listoffers_done(struct command *cmd,
850887

851888
/* BOLT #12:
852889
* - if `offer_quantity_max` is present:
853-
* - MUST reject the invoice request if there is no `invreq_quantity` field.
890+
* - MUST reject the invoice request if `invreq_recurrence_cancel`
891+
* is not present and there is no `invreq_quantity` field.
854892
* - if `offer_quantity_max` is non-zero:
855893
* - MUST reject the invoice request if `invreq_quantity` is zero, OR greater than
856894
* `offer_quantity_max`.
857895
* - otherwise:
858896
* - MUST reject the invoice request if there is an `invreq_quantity` field.
859897
*/
860898
if (ir->invreq->offer_quantity_max) {
861-
err = invreq_must_have(cmd, ir, invreq_quantity);
862-
if (err)
863-
return err;
899+
if (!ir->invreq->invreq_recurrence_cancel) {
900+
err = invreq_must_have(cmd, ir, invreq_quantity);
901+
if (err)
902+
return err;
903+
}
864904

865-
if (*ir->invreq->invreq_quantity == 0)
905+
if (ir->invreq->invreq_quantity && *ir->invreq->invreq_quantity == 0)
866906
return fail_invreq(cmd, ir,
867907
"quantity zero invalid");
868908

869-
if (*ir->invreq->offer_quantity_max &&
909+
if (ir->invreq->invreq_quantity &&
910+
*ir->invreq->offer_quantity_max &&
870911
*ir->invreq->invreq_quantity > *ir->invreq->offer_quantity_max) {
871912
return fail_invreq(cmd, ir,
872913
"quantity %"PRIu64" > %"PRIu64,
@@ -910,13 +951,18 @@ static struct command_result *listoffers_done(struct command *cmd,
910951
* field.
911952
* - MUST reject the invoice request if there is a `invreq_recurrence_start`
912953
* field.
954+
* - MUST reject the invoice request if there is a `invreq_recurrence_cancel`
955+
* field.
913956
*/
914957
err = invreq_must_not_have(cmd, ir, invreq_recurrence_counter);
915958
if (err)
916959
return err;
917960
err = invreq_must_not_have(cmd, ir, invreq_recurrence_start);
918961
if (err)
919962
return err;
963+
err = invreq_must_not_have(cmd, ir, invreq_recurrence_cancel);
964+
if (err)
965+
return err;
920966
}
921967

922968
/* BOLT #12:
@@ -926,8 +972,12 @@ static struct command_result *listoffers_done(struct command *cmd,
926972
* - MUST copy all non-signature fields from the invoice request (including
927973
* unknown fields).
928974
*/
975+
/* But "invreq_recurrence_cancel" doesn't exist in invoices, so temporarily remove */
976+
cancel = ir->invreq->invreq_recurrence_cancel;
977+
ir->invreq->invreq_recurrence_cancel = NULL;
929978
ir->inv = invoice_for_invreq(cmd, ir->invreq);
930979
assert(ir->inv->invreq_payer_id);
980+
ir->invreq->invreq_recurrence_cancel = cancel;
931981

932982
/* BOLT #12:
933983
* - if `offer_issuer_id` is present:

tests/test_pay.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6949,3 +6949,41 @@ def pay_with_sendonion(invoice, route, groupid, partid):
69496949
invoice = only_one(l3.rpc.listinvoices("inv")["invoices"])
69506950
# the receive amount should be exact
69516951
assert invoice["amount_received_msat"] == Millisatoshi(total_amount)
6952+
6953+
6954+
def test_cancel_recurrence(node_factory):
6955+
"""Test handling of invoice cancellation"""
6956+
l1, l2 = node_factory.line_graph(2)
6957+
6958+
# Recurring offer.
6959+
offer = l2.rpc.offer(amount='1msat',
6960+
description='test_cancel_recurrence',
6961+
recurrence='1minutes')
6962+
6963+
# We cannot cancel if we never got the first one.
6964+
with pytest.raises(RpcError, match="recurrence_counter: Must be non-zero"):
6965+
l1.rpc.cancelrecurringinvoice(offer['bolt12'], 0, 'test_cancel_recurrence')
6966+
6967+
with pytest.raises(RpcError, match="No previous payment attempted for this label and offer"):
6968+
l1.rpc.cancelrecurringinvoice(offer['bolt12'], 1, 'test_cancel_recurrence')
6969+
6970+
# Fetch and pay first one
6971+
ret = l1.rpc.fetchinvoice(offer=offer['bolt12'],
6972+
recurrence_counter=0,
6973+
recurrence_label='test_cancel_recurrence')
6974+
l1.rpc.pay(ret['invoice'], label='test_cancel_recurrence')
6975+
6976+
# Cancel counter must be correct!
6977+
with pytest.raises(RpcError, match=r"previous invoice has not been paid \(last was 0\)"):
6978+
l1.rpc.cancelrecurringinvoice(offer['bolt12'], 2, 'test_cancel_recurrence')
6979+
6980+
# Cancel second one.
6981+
l1.rpc.cancelrecurringinvoice(offer=offer['bolt12'],
6982+
recurrence_counter=1,
6983+
recurrence_label='test_cancel_recurrence')
6984+
6985+
# Now we cannot fetch second one!
6986+
with pytest.raises(RpcError, match=r"invoice expired \(cancelled\?\)"):
6987+
l1.rpc.fetchinvoice(offer=offer['bolt12'],
6988+
recurrence_counter=1,
6989+
recurrence_label='test_cancel_recurrence')

0 commit comments

Comments
 (0)