xpay: don't place global reservations on generated channels.
What changed, and why it matters
This fix corrects a bug in Core Lightning's 'xpay' plugin where temporary, made-up channel identifiers used for private payment routes (routehints and blinded paths) were being treated as real, globally-known channels. When two payments ran at the same time, they could reserve the same fake identifier and interfere with each other, potentially causing one payment to fail or be delayed. The fix marks these channels as fake and keeps their reservations private to each payment.
Upgrade to a Core Lightning version containing this commit if you use xpay with routehints or blinded paths and process concurrent payments. No immediate emergency response is indicated; the issue is a correctness/concurrency bug rather than an exploitable vulnerability.
Security signals we found
Concurrency bug causing resource reservation clash
Incorrect layer assignment for synthetic channel identifiers
Potential payment failure or delay due to fake scid collision
Fix explicitly marks fake channels and isolates their reservations
Evidence from the diff
The xpay plugin generates fake short_channel_ids (scids) for routehints and blinded paths. Previously, reservations and knowledge updates for these fake scids were placed in the global ‘xpay’ layer via askrene-inform-channel and askrene-unreserve-path calls. Because fake scids are not unique across payments, concurrent xpay attempts could clash on the same global reservation. The patch adds a fake_channel boolean to struct hop, sets it when the scid is not found in the gossmap, and routes reservations and error-learned knowledge into the payment’s private_layer instead of the global ‘xpay’ layer for fake channels.
Changed components
plugins/xpay/xpay.caskrene reservation/inform-channel layer selectionroutehint and blinded path handlingInspect captured patch +16 / −14
diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c
index aa59feae..8d2cdbe2 100644
--- a/plugins/xpay/xpay.c
+++ b/plugins/xpay/xpay.c
@@ -148,6 +148,8 @@ struct hop {
u32 cltv_value_in;
/* This is the delay, out from node. */
u32 cltv_value_out;
+ /* This is a fake channel. */
+ bool fake_channel;
};
/* Each actual payment attempt */
@@ -481,18 +483,6 @@ static void payment_give_up(struct command *aux_cmd,
cleanup(aux_cmd, payment);
}
-/* We usually add things we learned to the global layer, but not
- * if it's a fake channel */
-static const char *layer_of(const struct payment *payment,
- const struct short_channel_id_dir *scidd)
-{
- struct gossmap *gossmap = get_gossmap(xpay_of(payment->plugin));
-
- if (gossmap_find_chan(gossmap, &scidd->scid))
- return "xpay";
- return payment->private_layer;
-}
-
static void add_result_summary(struct attempt *attempt,
enum log_level level,
const char *fmt, ...)
@@ -713,8 +703,11 @@ static void update_knowledge_from_error(struct command *aux_cmd,
/* We learned something about prior nodes */
for (size_t i = 0; i < index; i++) {
req = payment_ignored_req(aux_cmd, attempt, "askrene-inform-channel");
+ /* Put what we learned in xpay, unless it's a fake channel */
json_add_string(req->js, "layer",
- layer_of(attempt->payment, &attempt->hops[i].scidd));
+ attempt->hops[i].fake_channel
+ ? attempt->payment->private_layer
+ : "xpay");
json_add_short_channel_id_dir(req->js,
"short_channel_id_dir",
attempt->hops[i].scidd);
@@ -881,8 +874,11 @@ disable_channel:
channel_capacity:
req = payment_ignored_req(aux_cmd, attempt, "askrene-inform-channel");
+ /* Put what we learned in xpay, unless it's a fake channel */
json_add_string(req->js, "layer",
- layer_of(attempt->payment, &attempt->hops[index].scidd));
+ attempt->hops[index].fake_channel
+ ? attempt->payment->private_layer
+ : "xpay");
json_add_short_channel_id_dir(req->js,
"short_channel_id_dir",
attempt->hops[index].scidd);
@@ -918,6 +914,8 @@ static struct command_result *unreserve_path(struct command *aux_cmd,
json_object_start(req->js, NULL);
json_add_short_channel_id_dir(req->js, "short_channel_id_dir", hop->scidd);
json_add_amount_msat(req->js, "amount_msat", hop->amount_out);
+ if (hop->fake_channel)
+ json_add_string(req->js, "layer", attempt->payment->private_layer);
json_object_end(req->js);
}
json_array_end(req->js);
@@ -1203,6 +1201,7 @@ static struct command_result *getroutes_done(struct command *aux_cmd,
const jsmntok_t *t, *routes;
size_t i;
struct amount_msat needs_routing, was_routing;
+ struct gossmap *gossmap = get_gossmap(xpay_of(payment->plugin));
payment_log(payment, LOG_DBG, "getroutes_done: %s",
payment->cmd ? "continuing" : "ignoring");
@@ -1270,6 +1269,7 @@ static struct command_result *getroutes_done(struct command *aux_cmd,
if (err)
plugin_err(aux_cmd->plugin, "Malformed routes: %s",
err);
+ hop->fake_channel = !gossmap_find_chan(gossmap, &hop->scidd.scid);
if (j > 0) {
hops[j-1].amount_out = hop->amount_in;
hops[j-1].cltv_value_out = hop->cltv_value_in;
@@ -1294,6 +1294,8 @@ static struct command_result *getroutes_done(struct command *aux_cmd,
json_add_short_channel_id_dir(req->js, "short_channel_id_dir",
hop->scidd);
json_add_amount_msat(req->js, "amount_msat", hop->amount_out);
+ if (hop->fake_channel)
+ json_add_string(req->js, "layer", payment->private_layer);
json_object_end(req->js);
}
json_array_end(req->js);
Why this scored 49/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.