xpay: fix reported value of amount_sent_msat
What changed, and why it matters
This commit fixes a bookkeeping bug in Core Lightning's xpay plugin. Previously, when a payment was routed through the user's own node, the plugin incorrectly reported that the user had paid fees to themselves. The fix separates the amount actually injected into the network from the amount reported as sent, so self-routed payments no longer show a fake fee. This is a reporting/accounting issue, not a way for an attacker to steal funds.
No urgent security action required. This is a reporting correction. Operators relying on xpay's `amount_sent_msat` for accounting or fee analysis should upgrade to obtain accurate values for self-routed payments.
Security signals we found
Incorrect fee accounting in payment reporting
Self-payment overestimation of amount_sent_msat
Test expectation changed to reflect corrected reporting
Evidence from the diff
The patch introduces a new helper inject_amount() and renames the prior initial_sent() logic. inject_amount() is used when constructing the actual sendpay request (amount_msat field), while initial_sent() is now used only for reporting amount_sent_msat. For self-payments with no hops, both return attempt->delivers. For routed payments, initial_sent() returns the first hop’s outgoing amount (which includes fees paid to others), while inject_amount() returns the same value. The test change shows the real behavioral fix: a blinded-path self-routing scenario now reports amount_sent_msat == amount_msat instead of amount_msat + fees_msat, because the fees were to the user’s own node and should not be counted as paid. A comment is also clarified about why auto.sourcefree is used in MCF route computation.
Changed components
plugins/xpay/xpay.ctests/test_xpay.pyInspect captured patch +11 / −4
diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c
index 05d001d8..fb54d0e8 100644
--- a/plugins/xpay/xpay.c
+++ b/plugins/xpay/xpay.c
@@ -343,6 +343,13 @@ send_payment_req(struct command *aux_cmd,
/* For self-pay, we don't have hops. */
static struct amount_msat initial_sent(const struct attempt *attempt)
+{
+ if (tal_count(attempt->hops) == 0)
+ return attempt->delivers;
+ return attempt->hops[0].amount_out;
+}
+
+static struct amount_msat inject_amount(const struct attempt *attempt)
{
if (tal_count(attempt->hops) == 0)
return attempt->delivers;
@@ -1151,7 +1158,7 @@ static struct command_result *do_inject(struct command *aux_cmd,
json_add_hex_talarr(req->js, "onion", onion);
json_add_sha256(req->js, "payment_hash", &attempt->payment->payment_hash);
/* If no route, its the same as delivery (self-pay) */
- json_add_amount_msat(req->js, "amount_msat", initial_sent(attempt));
+ json_add_amount_msat(req->js, "amount_msat", inject_amount(attempt));
json_add_u32(req->js, "cltv_expiry", initial_cltv_delta(attempt) + effective_bheight);
json_add_u64(req->js, "partid", attempt->partid);
json_add_u64(req->js, "groupid", attempt->payment->group_id);
@@ -1466,7 +1473,8 @@ static struct command_result *getroutes_for(struct command *aux_cmd,
json_array_start(req->js, "layers");
/* Add local channels */
json_add_string(req->js, NULL, "auto.localchans");
- /* We don't pay fees for ourselves */
+ /* For the MCF computation we must discard the cost of routing through
+ * our own channels because we don't pay fees for that. */
json_add_string(req->js, NULL, "auto.sourcefree");
/* Add xpay global channel */
json_add_string(req->js, NULL, "xpay");
diff --git a/tests/test_xpay.py b/tests/test_xpay.py
index 77b10b51..1f9106e1 100644
--- a/tests/test_xpay.py
+++ b/tests/test_xpay.py
@@ -1099,9 +1099,8 @@ def test_blinded_path_fees(node_factory):
assert ret["failed_parts"] == 0
assert ret["successful_parts"] == 1
assert ret["amount_msat"] == AMT_MSAT
- assert ret["amount_sent_msat"] == AMT_MSAT + FEES_MSAT
+ assert ret["amount_sent_msat"] == AMT_MSAT
- # we pay fees to ourselves
htlcs = l1.rpc.listhtlcs()["htlcs"]
assert len(htlcs) == 1
assert htlcs[0]["payment_hash"] == b12_decode["invoice_payment_hash"]
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.