From d1468f3f018a288fe471a0148c68db03d2770a3d Mon Sep 17 00:00:00 2001 From: Peter Neuroth Date: Thu, 6 Nov 2025 12:49:23 +0100 Subject: [PATCH 1/2] lightningd: add override amt to invoice_check_payment Adds `expected_msat_override` to the `invoice_check_payment` check. If it's set, it will be used to override the invoice amount as the expected amount of the payment check. This enables us to charge a different amount for a payment than the amount stated on the invoice. Signed-off-by: Peter Neuroth --- lightningd/htlc_set.c | 4 ++-- lightningd/invoice.c | 11 ++++++++--- lightningd/invoice.h | 2 ++ lightningd/pay.c | 2 +- wallet/test/run-wallet.c | 1 + 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lightningd/htlc_set.c b/lightningd/htlc_set.c index 5d2531fff060..b2fbf86eed6a 100644 --- a/lightningd/htlc_set.c +++ b/lightningd/htlc_set.c @@ -150,8 +150,8 @@ void htlc_set_add_(struct lightningd *ld, * [Failure Messages](#failure-messages) * - Note: "amount paid" specified there is the `total_msat` field. */ - details = invoice_check_payment(tmpctx, ld, payment_hash, - total_msat, payment_secret, &err); + details = invoice_check_payment(tmpctx, ld, payment_hash, total_msat, + NULL, payment_secret, &err); if (!details) { log_debug(log, "payment failed: %s", err); fail(arg, take(failmsg_incorrect_or_unknown(NULL, ld, msat))); diff --git a/lightningd/invoice.c b/lightningd/invoice.c index dfdf633e45b2..198a58e1562e 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -332,6 +332,7 @@ invoice_check_payment(const tal_t *ctx, struct lightningd *ld, const struct sha256 *payment_hash, const struct amount_msat msat, + const struct amount_msat *expected_msat_override, const struct secret *payment_secret, const char **err) { @@ -408,15 +409,19 @@ invoice_check_payment(const tal_t *ctx, if (details->msat != NULL) { struct amount_msat twice; - if (amount_msat_less(msat, *details->msat)) { + /* Override the expected amount. */ + struct amount_msat expected_msat = + expected_msat_override ? *expected_msat_override : *details->msat; + + if (amount_msat_less(msat, expected_msat)) { *err = tal_fmt(ctx, "Attempt to pay %s with amount %s < %s", fmt_sha256(tmpctx, &details->rhash), fmt_amount_msat(tmpctx, msat), - fmt_amount_msat(tmpctx, *details->msat)); + fmt_amount_msat(tmpctx, expected_msat)); return tal_free(details); } - if (amount_msat_add(&twice, *details->msat, *details->msat) + if (amount_msat_add(&twice, expected_msat, expected_msat) && amount_msat_greater(msat, twice)) { *err = tal_fmt(ctx, "Attempt to pay %s with amount %s > %s", fmt_sha256(tmpctx, &details->rhash), diff --git a/lightningd/invoice.h b/lightningd/invoice.h index 43e98b2a13b8..c7b24e118d0f 100644 --- a/lightningd/invoice.h +++ b/lightningd/invoice.h @@ -50,6 +50,7 @@ struct invoice_details { * @ld: lightningd * @payment_hash: hash of preimage they want. * @msat: amount they offer to pay. + * @expected_msat_override: if set: overrides the amount we expect to be payed. * @payment_secret: they payment secret they sent, if any. * @err: error string if it returns NULL. * @@ -59,6 +60,7 @@ const struct invoice_details *invoice_check_payment(const tal_t *ctx, struct lightningd *ld, const struct sha256 *payment_hash, const struct amount_msat msat, + const struct amount_msat *expected_msat_override, const struct secret *payment_secret, const char **err); diff --git a/lightningd/pay.c b/lightningd/pay.c index a4151994af98..10432eca750c 100644 --- a/lightningd/pay.c +++ b/lightningd/pay.c @@ -1482,7 +1482,7 @@ static struct command_result *self_payment(struct lightningd *ld, local_invreq_id); /* Now, resolve the invoice */ - inv = invoice_check_payment(tmpctx, ld, rhash, msat, payment_secret, &err); + inv = invoice_check_payment(tmpctx, ld, rhash, msat, NULL, payment_secret, &err); if (!inv) { struct routing_failure *fail; wallet_payment_set_status(ld->wallet, rhash, partid, groupid, diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 2c5454b35e42..3ca09e0279c2 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -391,6 +391,7 @@ const struct invoice_details *invoice_check_payment(const tal_t *ctx UNNEEDED, struct lightningd *ld UNNEEDED, const struct sha256 *payment_hash UNNEEDED, const struct amount_msat msat UNNEEDED, + const struct amount_msat *expected_msat_override UNNEEDED, const struct secret *payment_secret UNNEEDED, const char **err UNNEEDED) { fprintf(stderr, "invoice_check_payment called!\n"); abort(); } From 5c0bb5ad51da1a101b04216f22297712c7b91357 Mon Sep 17 00:00:00 2001 From: Peter Neuroth Date: Thu, 6 Nov 2025 16:40:08 +0100 Subject: [PATCH 2/2] lightningd: add invoice_amt to the htlc_accepted hook This commit introduces a new field `invoice_msat` to the htlc_accepted hook. If this field is specified it will replace the amount of the invoice that belongs to the payment_hash of the HTLC on internal checks. This is useful in scenarios where we actually expect a smaller amount than initially specified in an invoice. Changelog-Changed: Plugins: `htlc_accepted` hook can now override the expected total amount of the invoice that belongs to the HTLC. Signed-off-by: Peter Neuroth --- .../plugin-development/hooks.md | 2 ++ lightningd/htlc_set.c | 4 ++- lightningd/htlc_set.h | 8 +++-- lightningd/pay.c | 2 +- lightningd/peer_htlcs.c | 27 ++++++++++++++++- tests/plugins/override_invoice_msat.py | 29 +++++++++++++++++++ tests/test_pay.py | 29 +++++++++++++++++++ wallet/test/run-wallet.c | 1 + 8 files changed, 96 insertions(+), 6 deletions(-) create mode 100755 tests/plugins/override_invoice_msat.py diff --git a/doc/developers-guide/plugin-development/hooks.md b/doc/developers-guide/plugin-development/hooks.md index b8443387d050..9b3bacd9cd40 100644 --- a/doc/developers-guide/plugin-development/hooks.md +++ b/doc/developers-guide/plugin-development/hooks.md @@ -462,6 +462,8 @@ It can also specify `forward_to` in the response, replacing the destination. Th Also, it can specify `extra_tlvs` in the response. This will replace the TLV-stream `update_add_htlc_tlvs` in the `update_add_htlc` message for forwarded htlcs. +If the node is the final destination, the plugin can also replace the amount of the invoice that belongs to the `payment_hash` by specifying `invoice_msat`. + ```json { "result": "fail", diff --git a/lightningd/htlc_set.c b/lightningd/htlc_set.c index b2fbf86eed6a..4c3d34084310 100644 --- a/lightningd/htlc_set.c +++ b/lightningd/htlc_set.c @@ -133,6 +133,7 @@ void htlc_set_add_(struct lightningd *ld, struct logger *log, struct amount_msat msat, struct amount_msat total_msat, + const struct amount_msat *invoice_msat_override, const struct sha256 *payment_hash, const struct secret *payment_secret, void (*fail)(void *, const u8 *), @@ -151,7 +152,8 @@ void htlc_set_add_(struct lightningd *ld, * - Note: "amount paid" specified there is the `total_msat` field. */ details = invoice_check_payment(tmpctx, ld, payment_hash, total_msat, - NULL, payment_secret, &err); + invoice_msat_override, payment_secret, + &err); if (!details) { log_debug(log, "payment failed: %s", err); fail(arg, take(failmsg_incorrect_or_unknown(NULL, ld, msat))); diff --git a/lightningd/htlc_set.h b/lightningd/htlc_set.h index bb0119dc3ca8..d6de03292435 100644 --- a/lightningd/htlc_set.h +++ b/lightningd/htlc_set.h @@ -60,15 +60,17 @@ void htlc_set_add_(struct lightningd *ld, struct logger *log, struct amount_msat msat, struct amount_msat total_msat, + const struct amount_msat *invoice_msat_override, const struct sha256 *payment_hash, const struct secret *payment_secret, void (*fail)(void *, const u8 *), void (*succeeded)(void *, const struct preimage *), void *arg); -#define htlc_set_add(ld, log, msat, total_msat, payment_hash, payment_secret, \ - fail, succeeded, arg) \ - htlc_set_add_((ld), (log), (msat), (total_msat), (payment_hash), \ +#define htlc_set_add(ld, log, msat, total_msat, invoice_msat_override, \ + payment_hash, payment_secret, fail, succeeded, arg)\ + htlc_set_add_((ld), (log), (msat), (total_msat), \ + (invoice_msat_override), (payment_hash), \ (payment_secret), \ typesafe_cb_postargs(void, void *, \ (fail), (arg), \ diff --git a/lightningd/pay.c b/lightningd/pay.c index 10432eca750c..b38b8e1e9f15 100644 --- a/lightningd/pay.c +++ b/lightningd/pay.c @@ -1973,7 +1973,7 @@ static struct command_result *json_injectpaymentonion(struct command *cmd, * not resolve immediately */ fixme_ignore(command_still_pending(cmd)); htlc_set_add(cmd->ld, cmd->ld->log, *msat, *payload->total_msat, - payment_hash, payload->payment_secret, + NULL, payment_hash, payload->payment_secret, selfpay_mpp_fail, selfpay_mpp_succeeded, selfpay); return command_its_complicated("htlc_set_add may have immediately succeeded or failed"); diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 6dea60182548..99c287f91be3 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -3,9 +3,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -435,6 +437,7 @@ static void handle_localpay(struct htlc_in *hin, struct amount_msat amt_to_forward, u32 outgoing_cltv_value, struct amount_msat total_msat, + const struct amount_msat *invoice_msat_override, const struct secret *payment_secret, const u8 *payment_metadata) { @@ -525,6 +528,7 @@ static void handle_localpay(struct htlc_in *hin, htlc_set_add(ld, hin->key.channel->log, hin->msat, total_msat, + invoice_msat_override, &hin->payment_hash, payment_secret, local_fail_in_htlc, @@ -947,6 +951,9 @@ struct htlc_accepted_hook_payload { size_t failtlvpos; const char *failexplanation; u8 *extra_tlvs_raw; + /* Default is NULL, if NOT NULL: used to override the amount of the + * invoice this htlc belongs to in checks! */ + struct amount_msat *invoice_msat; }; static void @@ -1003,7 +1010,8 @@ static bool htlc_accepted_hook_deserialize(struct htlc_accepted_hook_payload *re struct htlc_in *hin = request->hin; struct lightningd *ld = request->ld; struct preimage payment_preimage; - const jsmntok_t *resulttok, *paykeytok, *payloadtok, *fwdtok, *extra_tlvs_tok; + const jsmntok_t *resulttok, *paykeytok, *payloadtok, *fwdtok, *extra_tlvs_tok, + *invmsattok; u8 *failonion, *raw_tlvs; if (!toks || !buffer) @@ -1018,6 +1026,17 @@ static bool htlc_accepted_hook_deserialize(struct htlc_accepted_hook_payload *re json_strdup(tmpctx, buffer, toks)); } + invmsattok = json_get_member(buffer, toks, "invoice_msat"); + if (invmsattok) { + tal_free(request->invoice_msat); + request->invoice_msat = tal(request, struct amount_msat); + if (!json_to_msat(buffer, invmsattok, request->invoice_msat)) { + fatal("Bad invoice_msat for htlc_accepted hook: %.*s", + invmsattok->end - invmsattok->start, + buffer + invmsattok->start); + } + } + extra_tlvs_tok = json_get_member(buffer, toks, "extra_tlvs"); if (extra_tlvs_tok) { size_t max; @@ -1275,6 +1294,7 @@ htlc_accepted_hook_final(struct htlc_accepted_hook_payload *request STEALS) request->payload->amt_to_forward, request->payload->outgoing_cltv, *request->payload->total_msat, + request->invoice_msat, request->payload->payment_secret, request->payload->payment_metadata); @@ -1555,6 +1575,11 @@ static bool peer_accepted_htlc(const tal_t *ctx, hook_payload->extra_tlvs_raw = NULL; } + /* We don't set the invoice amount here, if it is set during a hook + * response, it will be used to override the actual invoice amount on + * later checks. */ + hook_payload->invoice_msat = NULL; + plugin_hook_call_htlc_accepted(ld, NULL, hook_payload); /* Falling through here is ok, after all the HTLC locked */ diff --git a/tests/plugins/override_invoice_msat.py b/tests/plugins/override_invoice_msat.py new file mode 100755 index 000000000000..d3f89e892a19 --- /dev/null +++ b/tests/plugins/override_invoice_msat.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""A plugin that overrides the amount of the invoice that belongs to an HTLC.""" + +from pyln.client import Plugin + + +plugin = Plugin() + + +@plugin.hook("htlc_accepted") +def on_htlc_accepted(htlc, onion, plugin, **kwargs): + res = {"result": "continue"} + if plugin.invoice_msat: + res["invoice_msat"] = plugin.invoice_msat + return res + + +@plugin.method("setinvoicemsat") +def setinvoicemsat(plugin, msat: int): + """Sets invoice_msat for the htlc_accepted response.""" + plugin.invoice_msat = msat + + +@plugin.init() +def on_init(**kwargs): + plugin.invoice_msat = None + + +plugin.run() diff --git a/tests/test_pay.py b/tests/test_pay.py index 9f97ab82820e..3bfc289951e8 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -7133,3 +7133,32 @@ def test_htlc_tlv_crash(node_factory): l1.rpc.waitsendpay(inv1['payment_hash'], TIMEOUT) l1.rpc.waitsendpay(inv2['payment_hash'], TIMEOUT) + + +def test_invoice_amount_override(node_factory): + """Uses the htlc_accepted hook response value `invoice_msat` to override + the expected total payment amount of the invoice. + """ + plugin = os.path.join(os.path.dirname(__file__), "plugins/override_invoice_msat.py") + l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": plugin}]) + + inv = l2.rpc.invoice(10_000, "expected_amt_override", "expected_amt_override") + + route = [ + { + "amount_msat": 1_000, # Reduced amount that is below the expected amount + "id": l2.info["id"], + "delay": 10, + "channel": first_scid(l1, l2), + } + ] + + with pytest.raises(RpcError): + l1.rpc.sendpay(route, inv["payment_hash"], payment_secret=inv["payment_secret"]) + l1.rpc.waitsendpay(inv["payment_hash"]) + + # Override expected invoice amount, via htlc_accepted. + l2.rpc.setinvoicemsat(msat=1_000) + + l1.rpc.sendpay(route, inv["payment_hash"], payment_secret=inv["payment_secret"]) + assert l1.rpc.waitsendpay(inv["payment_hash"])["status"] == "complete" diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 3ca09e0279c2..5ce78e712293 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -380,6 +380,7 @@ void htlc_set_add_(struct lightningd *ld UNNEEDED, struct logger *log UNNEEDED, struct amount_msat msat UNNEEDED, struct amount_msat total_msat UNNEEDED, + const struct amount_msat *invoice_msat_override UNNEEDED, const struct sha256 *payment_hash UNNEEDED, const struct secret *payment_secret UNNEEDED, void (*fail)(void * UNNEEDED, const u8 *) UNNEEDED,