Skip to content

Commit 5e53af7

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 <rusty@rustcorp.com.au>
1 parent 0d18b82 commit 5e53af7

File tree

3 files changed

+109
-25
lines changed

3 files changed

+109
-25
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: 68 additions & 18 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
}
@@ -372,6 +388,18 @@ static struct command_result *add_blindedpaths(struct command *cmd,
372388
found_best_peer, ir);
373389
}
374390

391+
static struct command_result *cancel_invoice(struct command *cmd,
392+
struct invreq *ir)
393+
{
394+
/* We create an invoice, so we can mark the cancellation, but with
395+
* expiry 0. And we don't send it to them! */
396+
*ir->inv->invoice_relative_expiry = 0;
397+
398+
/* In case they set a reply path! */
399+
ir->reply_path = tal_free(ir->reply_path);
400+
return create_invoicereq(cmd, ir);
401+
}
402+
375403
static struct command_result *check_period(struct command *cmd,
376404
struct invreq *ir,
377405
u64 basetime)
@@ -483,6 +511,10 @@ static struct command_result *check_period(struct command *cmd,
483511
}
484512
}
485513

514+
/* If this is actually a cancel, we create an expired invoice */
515+
if (ir->invreq->invreq_recurrence_cancel)
516+
return cancel_invoice(cmd, ir);
517+
486518
return add_blindedpaths(cmd, ir);
487519
}
488520

@@ -626,19 +658,23 @@ static struct command_result *invreq_base_amount_simple(struct command *cmd,
626658

627659
*amt = amount_msat(raw_amount);
628660
} else {
629-
/* BOLT #12:
661+
/* BOLT-recurrence #12:
630662
*
631663
* The reader:
632664
*...
633665
* - otherwise (no `offer_amount`):
634-
* - MUST reject the invoice request if it does not contain
635-
* `invreq_amount`.
666+
* - MUST reject the invoice request if `invreq_recurrence_cancel`
667+
* is not present and it does not contain `invreq_amount`.
636668
*/
637-
err = invreq_must_have(cmd, ir, invreq_amount);
638-
if (err)
639-
return err;
640-
641-
*amt = amount_msat(*ir->invreq->invreq_amount);
669+
if (!ir->invreq->invreq_recurrence_cancel) {
670+
err = invreq_must_have(cmd, ir, invreq_amount);
671+
if (err)
672+
return err;
673+
}
674+
if (ir->invreq->invreq_amount)
675+
*amt = amount_msat(*ir->invreq->invreq_amount);
676+
else
677+
*amt = AMOUNT_MSAT(0);
642678
}
643679
return NULL;
644680
}
@@ -776,6 +812,7 @@ static struct command_result *listoffers_done(struct command *cmd,
776812
bool active;
777813
struct command_result *err;
778814
struct amount_msat amt;
815+
struct tlv_invoice_request_invreq_recurrence_cancel *cancel;
779816

780817
/* BOLT #12:
781818
*
@@ -861,25 +898,29 @@ static struct command_result *listoffers_done(struct command *cmd,
861898
return fail_invreq(cmd, ir, "Offer expired");
862899
}
863900

864-
/* BOLT #12:
901+
/* BOLT-recurrence #12:
865902
* - if `offer_quantity_max` is present:
866-
* - MUST reject the invoice request if there is no `invreq_quantity` field.
903+
* - MUST reject the invoice request if `invreq_recurrence_cancel`
904+
* is not present and there is no `invreq_quantity` field.
867905
* - if `offer_quantity_max` is non-zero:
868906
* - MUST reject the invoice request if `invreq_quantity` is zero, OR greater than
869907
* `offer_quantity_max`.
870908
* - otherwise:
871909
* - MUST reject the invoice request if there is an `invreq_quantity` field.
872910
*/
873911
if (ir->invreq->offer_quantity_max) {
874-
err = invreq_must_have(cmd, ir, invreq_quantity);
875-
if (err)
876-
return err;
912+
if (!ir->invreq->invreq_recurrence_cancel) {
913+
err = invreq_must_have(cmd, ir, invreq_quantity);
914+
if (err)
915+
return err;
916+
}
877917

878-
if (*ir->invreq->invreq_quantity == 0)
918+
if (ir->invreq->invreq_quantity && *ir->invreq->invreq_quantity == 0)
879919
return fail_invreq(cmd, ir,
880920
"quantity zero invalid");
881921

882-
if (*ir->invreq->offer_quantity_max &&
922+
if (ir->invreq->invreq_quantity &&
923+
*ir->invreq->offer_quantity_max &&
883924
*ir->invreq->invreq_quantity > *ir->invreq->offer_quantity_max) {
884925
return fail_invreq(cmd, ir,
885926
"quantity %"PRIu64" > %"PRIu64,
@@ -923,13 +964,18 @@ static struct command_result *listoffers_done(struct command *cmd,
923964
* field.
924965
* - MUST reject the invoice request if there is a `invreq_recurrence_start`
925966
* field.
967+
* - MUST reject the invoice request if there is a `invreq_recurrence_cancel`
968+
* field.
926969
*/
927970
err = invreq_must_not_have(cmd, ir, invreq_recurrence_counter);
928971
if (err)
929972
return err;
930973
err = invreq_must_not_have(cmd, ir, invreq_recurrence_start);
931974
if (err)
932975
return err;
976+
err = invreq_must_not_have(cmd, ir, invreq_recurrence_cancel);
977+
if (err)
978+
return err;
933979
}
934980

935981
/* BOLT #12:
@@ -939,8 +985,12 @@ static struct command_result *listoffers_done(struct command *cmd,
939985
* - MUST copy all non-signature fields from the invoice request (including
940986
* unknown fields).
941987
*/
988+
/* But "invreq_recurrence_cancel" doesn't exist in invoices, so temporarily remove */
989+
cancel = ir->invreq->invreq_recurrence_cancel;
990+
ir->invreq->invreq_recurrence_cancel = NULL;
942991
ir->inv = invoice_for_invreq(cmd, ir->invreq);
943992
assert(ir->inv->invreq_payer_id);
993+
ir->invreq->invreq_recurrence_cancel = cancel;
944994

945995
/* BOLT #12:
946996
* - if `offer_issuer_id` is present:

tests/test_pay.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7021,6 +7021,44 @@ def pay_with_sendonion(invoice, route, groupid, partid):
70217021
assert invoice["amount_received_msat"] == Millisatoshi(total_amount)
70227022

70237023

7024+
def test_cancel_recurrence(node_factory):
7025+
"""Test handling of invoice cancellation"""
7026+
l1, l2 = node_factory.line_graph(2)
7027+
7028+
# Recurring offer.
7029+
offer = l2.rpc.offer(amount='1msat',
7030+
description='test_cancel_recurrence',
7031+
recurrence='1minutes')
7032+
7033+
# We cannot cancel if we never got the first one.
7034+
with pytest.raises(RpcError, match="recurrence_counter: Must be non-zero"):
7035+
l1.rpc.cancelrecurringinvoice(offer['bolt12'], 0, 'test_cancel_recurrence')
7036+
7037+
with pytest.raises(RpcError, match="No previous payment attempted for this label and offer"):
7038+
l1.rpc.cancelrecurringinvoice(offer['bolt12'], 1, 'test_cancel_recurrence')
7039+
7040+
# Fetch and pay first one
7041+
ret = l1.rpc.fetchinvoice(offer=offer['bolt12'],
7042+
recurrence_counter=0,
7043+
recurrence_label='test_cancel_recurrence')
7044+
l1.rpc.pay(ret['invoice'], label='test_cancel_recurrence')
7045+
7046+
# Cancel counter must be correct!
7047+
with pytest.raises(RpcError, match=r"previous invoice has not been paid \(last was 0\)"):
7048+
l1.rpc.cancelrecurringinvoice(offer['bolt12'], 2, 'test_cancel_recurrence')
7049+
7050+
# Cancel second one.
7051+
l1.rpc.cancelrecurringinvoice(offer=offer['bolt12'],
7052+
recurrence_counter=1,
7053+
recurrence_label='test_cancel_recurrence')
7054+
7055+
# Now we cannot fetch second one!
7056+
with pytest.raises(RpcError, match=r"invoice expired \(cancelled\?\)"):
7057+
l1.rpc.fetchinvoice(offer=offer['bolt12'],
7058+
recurrence_counter=1,
7059+
recurrence_label='test_cancel_recurrence')
7060+
7061+
70247062
def test_htlc_tlv_crash(node_factory):
70257063
"""Marshalling code treated an array of htlc_added as if they were tal objects, but only the head is a tal object so if we have more than one, BOOM!"""
70267064
plugin = os.path.join(os.path.dirname(__file__), 'plugins/htlc_accepted-customtlv.py')

0 commit comments

Comments
 (0)