Skip to content

Commit b8e5b12

Browse files
rustyrussellvincenzopalazzo
authored andcommitted
decode: don't fail to decode just because a bolt12 invoice has expired.
In fact, there are several places where we try to decode old invoices, and they should all work. The only place we should enforce expiration is when we're going to pay. This also revealed that xpay wasn't checking bolt11 expiries! Reported-by: hMsats Fixes: #7869 Signed-off-by: Rusty Russell <[email protected]> Changelog-Fixed: JSON-RPC: `decode` refused to decode expired bolt12 invoices.
1 parent 14cb057 commit b8e5b12

File tree

6 files changed

+40
-23
lines changed

6 files changed

+40
-23
lines changed

common/bolt12.c

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,6 @@ struct tlv_invoice *invoice_decode(const tal_t *ctx,
489489
char **fail)
490490
{
491491
struct tlv_invoice *invoice;
492-
u64 expiry, now;
493492

494493
invoice = invoice_decode_minimal(ctx, b12, b12len, our_features,
495494
must_be_chain, fail);
@@ -527,27 +526,6 @@ struct tlv_invoice *invoice_decode(const tal_t *ctx,
527526
return tal_free(invoice);
528527
}
529528

530-
/* BOLT-offers #12:
531-
* - if `invoice_relative_expiry` is present:
532-
* - MUST reject the invoice if the current time since 1970-01-01 UTC
533-
* is greater than `invoice_created_at` plus `seconds_from_creation`.
534-
* - otherwise:
535-
* - MUST reject the invoice if the current time since 1970-01-01 UTC
536-
* is greater than `invoice_created_at` plus 7200.
537-
*/
538-
if (invoice->invoice_relative_expiry)
539-
expiry = *invoice->invoice_relative_expiry;
540-
else
541-
expiry = 7200;
542-
now = time_now().ts.tv_sec;
543-
/* If it overflows, it's forever */
544-
if (!add_overflows_u64(*invoice->invoice_created_at, expiry)
545-
&& now > *invoice->invoice_created_at + expiry) {
546-
*fail = tal_fmt(ctx, "expired %"PRIu64" seconds ago",
547-
now - (*invoice->invoice_created_at + expiry));
548-
return tal_free(invoice);
549-
}
550-
551529
/* BOLT-offers #12:
552530
* - MUST reject the invoice if `invoice_paths` is not present or is
553531
* empty. */
@@ -583,6 +561,30 @@ struct tlv_invoice *invoice_decode(const tal_t *ctx,
583561
return invoice;
584562
}
585563

564+
u64 invoice_expiry(const struct tlv_invoice *invoice)
565+
{
566+
u64 expiry;
567+
568+
/* BOLT-offers #12:
569+
* - if `invoice_relative_expiry` is present:
570+
* - MUST reject the invoice if the current time since 1970-01-01 UTC
571+
* is greater than `invoice_created_at` plus `seconds_from_creation`.
572+
* - otherwise:
573+
* - MUST reject the invoice if the current time since 1970-01-01 UTC
574+
* is greater than `invoice_created_at` plus 7200.
575+
*/
576+
if (invoice->invoice_relative_expiry)
577+
expiry = *invoice->invoice_relative_expiry;
578+
else
579+
expiry = 7200;
580+
581+
/* If it overflows, it's forever */
582+
if (add_overflows_u64(*invoice->invoice_created_at, expiry))
583+
return UINT64_MAX;
584+
585+
return *invoice->invoice_created_at + expiry;
586+
}
587+
586588
static bool bolt12_has_invoice_prefix(const char *str)
587589
{
588590
return strstarts(str, "lni1") || strstarts(str, "LNI1");

common/bolt12.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,17 @@ char *invoice_encode(const tal_t *ctx, const struct tlv_invoice *bolt12_tlv);
7575
* is not expired). It also checks signature.
7676
*
7777
* Note: blinded path features need to be checked by the caller before use!
78+
* Note: expiration must be check by caller before use!
7879
*/
7980
struct tlv_invoice *invoice_decode(const tal_t *ctx,
8081
const char *b12, size_t b12len,
8182
const struct feature_set *our_features,
8283
const struct chainparams *must_be_chain,
8384
char **fail);
8485

86+
/* UINT64_MAX if no expiry. */
87+
u64 invoice_expiry(const struct tlv_invoice *invoice);
88+
8589
/* This one only checks it decides, and optionally is correct chain/features */
8690
struct tlv_invoice *invoice_decode_minimal(const tal_t *ctx,
8791
const char *b12, size_t b12len,

contrib/msggen/msggen/schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36330,6 +36330,7 @@
3633036330
"- -1: Catchall nonspecific error.",
3633136331
"- 203: Permanent failure from destination (e.g. it said it didn't recognize invoice)",
3633236332
"- 205: Couldn't find, or find a way to, the destination.",
36333+
"- 207: Invoice has expired.",
3633336334
"- 219: Invoice has already been paid.",
3633436335
"- 209: Other payment error."
3633536336
],

doc/schemas/lightning-xpay.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"- -1: Catchall nonspecific error.",
108108
"- 203: Permanent failure from destination (e.g. it said it didn't recognize invoice)",
109109
"- 205: Couldn't find, or find a way to, the destination.",
110+
"- 207: Invoice has expired.",
110111
"- 219: Invoice has already been paid.",
111112
"- 209: Other payment error."
112113
],

plugins/xpay/xpay.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,7 @@ static struct command_result *json_xpay(struct command *cmd,
14141414
struct payment *payment = tal(cmd, struct payment);
14151415
unsigned int *retryfor;
14161416
struct out_req *req;
1417+
u64 now, invexpiry;
14171418
char *err;
14181419

14191420
if (!param_check(cmd, buffer, params,
@@ -1448,6 +1449,7 @@ static struct command_result *json_xpay(struct command *cmd,
14481449
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
14491450
"Invalid bolt12 invoice: %s", err);
14501451

1452+
invexpiry = invoice_expiry(b12inv);
14511453
payment->full_amount = amount_msat(*b12inv->invoice_amount);
14521454
if (msat)
14531455
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
@@ -1517,8 +1519,16 @@ static struct command_result *json_xpay(struct command *cmd,
15171519
payment->full_amount = *b11->msat;
15181520
else
15191521
payment->full_amount = *msat;
1522+
1523+
invexpiry = b11->timestamp + b11->expiry;
15201524
}
15211525

1526+
now = time_now().ts.tv_sec;
1527+
if (now > invexpiry)
1528+
return command_fail(cmd, PAY_INVOICE_EXPIRED,
1529+
"Invoice expired %"PRIu64" seconds ago",
1530+
now - invexpiry);
1531+
15221532
if (partial) {
15231533
payment->amount = *partial;
15241534
if (amount_msat_greater(payment->amount, payment->full_amount))

tests/test_pay.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6770,7 +6770,6 @@ def test_pay_unannounced_routehint(node_factory, bitcoind):
67706770
assert result["status"] == "complete", f"pay result is {result}"
67716771

67726772

6773-
@pytest.mark.xfail(strict=True)
67746773
def test_decode_expired_bolt12(node_factory):
67756774
l1 = node_factory.get_node()
67766775

0 commit comments

Comments
 (0)