askrene: improve errors we return.
What changed, and why it matters
This change improves the error messages returned by Core Lightning's routing plugin (askrene). Previously, almost all routing failures were reported with a single generic 'route not found' error code. Now the plugin distinguishes cases such as 'route too expensive' or 'timed out' and returns the appropriate error code. This is a quality-of-life improvement for callers, not a fix for an exploitable vulnerability.
No security action required; treat as a normal bugfix/improvement. Reviewers may want to verify that the child reply format change is backward-compatible with any other consumers of the askrene child protocol, though the parent is updated accordingly.
Security signals we found
Error-code precision improvement reduces caller ambiguity
Child-to-parent IPC now prefixes replies with a raw enum value and includes a truncation check
No memory-safety, authentication, authorization, or cryptographic changes observed
Evidence from the diff
The askrene plugin’s child process previously returned only a string on failure, and the parent always mapped failures to PAY_ROUTE_NOT_FOUND. The patch threads an enum jsonrpc_errcode through default_routes/single_path_routes/linear_routes, sets distinct codes (PAY_ROUTE_TOO_EXPENSIVE, PAY_ROUTE_NOT_FOUND, PAY_STOPPED_RETRYING, LIGHTNINGD) for different failure modes, and prefixes the child’s error reply with that code so the parent can return it via command_fail. Schema documentation is updated to list error codes -1, 205, and 206.
Changed components
plugins/askrene/askrene.cplugins/askrene/child/child.cplugins/askrene/child/mcf.cplugins/askrene/child/mcf.hdoc/schemas/getroutes.jsoncontrib/msggen/msggen/schema.jsonInspect captured patch +59 / −11
diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json
index e79cb442..f424c408 100644
--- a/contrib/msggen/msggen/schema.json
+++ b/contrib/msggen/msggen/schema.json
@@ -16740,6 +16740,13 @@
}
}
},
+ "errors": [
+ "The following error codes may occur:",
+ "",
+ "- -1: Catchall nonspecific error.",
+ "- 205: Unable to find a route.",
+ "- 206: Route too expensive. Either the max_delay or maxfee_msat was exceeded."
+ ],
"author": [
"[lagrang3@protonmail.com](mailto:lagrang3@protonmail.com) wrote the minimum-cost-flow solver, Rusty Russell [rusty@rustcorp.com.au](mailto:rusty@rustcorp.com.au) wrote the API and this documentation."
],
diff --git a/doc/schemas/getroutes.json b/doc/schemas/getroutes.json
index 04f502e2..7860aeb0 100644
--- a/doc/schemas/getroutes.json
+++ b/doc/schemas/getroutes.json
@@ -174,6 +174,13 @@
}
}
},
+ "errors": [
+ "The following error codes may occur:",
+ "",
+ "- -1: Catchall nonspecific error.",
+ "- 205: Unable to find a route.",
+ "- 206: Route too expensive. Either the max_delay or maxfee_msat was exceeded."
+ ],
"author": [
"[lagrang3@protonmail.com](mailto:lagrang3@protonmail.com) wrote the minimum-cost-flow solver, Rusty Russell [rusty@rustcorp.com.au](mailto:rusty@rustcorp.com.au) wrote the API and this documentation."
],
diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c
index 5b087cca..f31043fb 100644
--- a/plugins/askrene/askrene.c
+++ b/plugins/askrene/askrene.c
@@ -413,6 +413,7 @@ static struct command_result *reap_child(struct router_child *child)
int child_status;
struct timerel time_delta;
const char *err;
+ enum jsonrpc_errcode ecode;
waitpid(child->pid, &child_status, 0);
time_delta = timemono_between(time_mono(), child->start);
@@ -430,7 +431,17 @@ static struct command_result *reap_child(struct router_child *child)
/* This is how it indicates an error message */
if (WEXITSTATUS(child_status) != 0 && child->reply_bytes) {
- err = tal_strndup(child, child->reply_buf, child->reply_bytes);
+ if (child->reply_bytes <= sizeof(ecode)) {
+ plugin_log(child->cmd->plugin, LOG_BROKEN, "Truncated child reply (%zu) bytes, exited %i",
+ child->reply_bytes, WEXITSTATUS(child_status));
+ ecode = LIGHTNINGD;
+ err = "Truncated child result";
+ } else {
+ memcpy(&ecode, child->reply_buf, sizeof(ecode));
+ err = tal_strndup(child,
+ child->reply_buf + sizeof(ecode),
+ child->reply_bytes - sizeof(ecode));
+ }
goto fail;
}
if (child->reply_bytes == 0) {
@@ -445,10 +456,11 @@ static struct command_result *reap_child(struct router_child *child)
fail_broken:
plugin_log(child->cmd->plugin, LOG_BROKEN, "%s", err);
+ ecode = LIGHTNINGD;
fail:
assert(err);
/* Frees child, since it's a child of cmd */
- return command_fail(child->cmd, PAY_ROUTE_NOT_FOUND, "%s", err);
+ return command_fail(child->cmd, ecode, "%s", err);
}
/* Last one out finalizes */
diff --git a/plugins/askrene/child/child.c b/plugins/askrene/child/child.c
index 8a4363d5..c0779096 100644
--- a/plugins/askrene/child/child.c
+++ b/plugins/askrene/child/child.c
@@ -217,6 +217,7 @@ void run_child(const struct gossmap *gossmap,
const char *err, *p;
size_t len;
struct route_query *rq;
+ enum jsonrpc_errcode ecode;
/* We exit below, so we don't bother freeing this */
rq = new_route_query(NULL, gossmap, cmd_id, layers,
@@ -225,13 +226,14 @@ void run_child(const struct gossmap *gossmap,
if (single_path) {
err = single_path_routes(rq, rq, deadline, srcnode, dstnode,
amount, maxfee, finalcltv,
- maxdelay, &flows, &probability);
+ maxdelay, &flows, &probability, &ecode);
} else {
err = default_routes(rq, rq, deadline, srcnode, dstnode,
amount, maxfee, finalcltv, maxdelay,
- maxparts, &flows, &probability);
+ maxparts, &flows, &probability, &ecode);
}
if (err) {
+ write_all(replyfd, &ecode, sizeof(ecode));
write_all(replyfd, err, strlen(err));
/* Non-zero exit tells parent this is an error string. */
exit(1);
diff --git a/plugins/askrene/child/mcf.c b/plugins/askrene/child/mcf.c
index 41594c50..dde97f02 100644
--- a/plugins/askrene/child/mcf.c
+++ b/plugins/askrene/child/mcf.c
@@ -1343,6 +1343,7 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
struct amount_msat maxfee, u32 finalcltv, u32 maxdelay,
size_t maxparts,
struct flow ***flows, double *probability,
+ enum jsonrpc_errcode *ecode,
struct flow **(*solver)(const tal_t *, const struct route_query *,
const struct gossmap_node *,
const struct gossmap_node *,
@@ -1373,6 +1374,7 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
error_message = child_log(ctx, LOG_BROKEN,
"%s: timed out after deadline",
__func__);
+ *ecode = PAY_STOPPED_RETRYING;
goto fail;
}
@@ -1400,13 +1402,16 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
if (!new_flows) {
error_message = explain_failure(
ctx, rq, srcnode, dstnode, amount_to_deliver);
+ *ecode = PAY_ROUTE_NOT_FOUND;
goto fail;
}
error_message =
refine_flows(ctx, rq, amount_to_deliver, &new_flows);
- if (error_message)
+ if (error_message) {
+ *ecode = PAY_ROUTE_NOT_FOUND;
goto fail;
+ }
/* we finished removing flows and excess */
all_deliver = flowset_delivers(new_flows);
@@ -1450,6 +1455,7 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
"fraction (%lf)",
__func__, fmt_amount_msat(tmpctx, feebudget),
deliver_fraction);
+ *ecode = PAY_ROUTE_NOT_FOUND;
goto fail;
}
if (amount_msat_greater(all_fees, partial_feebudget)) {
@@ -1476,6 +1482,7 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
child_log(ctx, LOG_UNUSUAL,
"Could not find route without "
"excessive cost");
+ *ecode = PAY_ROUTE_TOO_EXPENSIVE;
goto fail;
} else {
/* mu cannot be increased but at least all_fees
@@ -1499,6 +1506,7 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
child_log(ctx, LOG_UNUSUAL,
"Could not find route without "
"excessive delays");
+ *ecode = PAY_ROUTE_TOO_EXPENSIVE;
goto fail;
}
@@ -1538,6 +1546,7 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
"%s: unexpected arithmetic operation "
"failure on amount_msat",
__func__);
+ *ecode = PAY_ROUTE_NOT_FOUND;
goto fail;
}
}
@@ -1558,6 +1567,7 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
error_message = reduce_num_flows(rq, rq, flows, amount, maxparts);
if (error_message) {
*flows = tal_free(*flows);
+ *ecode = PAY_ROUTE_NOT_FOUND;
return error_message;
}
@@ -1572,6 +1582,7 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
fmt_amount_msat(tmpctx, fee),
fmt_amount_msat(tmpctx, maxfee));
*flows = tal_free(*flows);
+ *ecode = PAY_ROUTE_TOO_EXPENSIVE;
return error_message;
}
}
@@ -1588,16 +1599,19 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
child_log(rq, LOG_BROKEN,
"%s: check_htlc_min_limits failed", __func__);
*flows = tal_free(*flows);
+ *ecode = PAY_ROUTE_NOT_FOUND;
return error_message;
}
if (!check_htlc_max_limits(rq, *flows)) {
*flows = tal_free(*flows);
+ *ecode = PAY_ROUTE_NOT_FOUND;
return child_log(rq, LOG_BROKEN,
"%s: check_htlc_max_limits failed", __func__);
}
if (tal_count(*flows) > maxparts) {
size_t num_flows = tal_count(*flows);
*flows = tal_free(*flows);
+ *ecode = PAY_ROUTE_NOT_FOUND;
return child_log(rq, LOG_BROKEN,
"%s: the number of flows (%zu) exceeds the limit set "
"on payment parts (%zu), please submit a bug report",
@@ -1620,10 +1634,12 @@ const char *default_routes(const tal_t *ctx, struct route_query *rq,
struct amount_msat amount, struct amount_msat maxfee,
u32 finalcltv, u32 maxdelay, size_t maxparts,
struct flow ***flows,
- double *probability)
+ double *probability,
+ enum jsonrpc_errcode *ecode)
{
return linear_routes(ctx, rq, deadline, srcnode, dstnode, amount, maxfee,
- finalcltv, maxdelay, maxparts, flows, probability, minflow);
+ finalcltv, maxdelay, maxparts, flows, probability, ecode,
+ minflow);
}
const char *single_path_routes(const tal_t *ctx, struct route_query *rq,
@@ -1633,9 +1649,10 @@ const char *single_path_routes(const tal_t *ctx, struct route_query *rq,
struct amount_msat amount,
struct amount_msat maxfee, u32 finalcltv,
u32 maxdelay, struct flow ***flows,
- double *probability)
+ double *probability,
+ enum jsonrpc_errcode *ecode)
{
return linear_routes(ctx, rq, deadline, srcnode, dstnode, amount, maxfee,
- finalcltv, maxdelay, 1, flows, probability,
+ finalcltv, maxdelay, 1, flows, probability, ecode,
single_path_flow);
}
diff --git a/plugins/askrene/child/mcf.h b/plugins/askrene/child/mcf.h
index 27538c52..9cceac32 100644
--- a/plugins/askrene/child/mcf.h
+++ b/plugins/askrene/child/mcf.h
@@ -6,6 +6,7 @@
#include <ccan/time/time.h>
#include <common/amount.h>
#include <common/gossmap.h>
+#include <common/jsonrpc_errors.h>
struct route_query;
@@ -18,7 +19,8 @@ const char *default_routes(const tal_t *ctx, struct route_query *rq,
struct amount_msat amount,
struct amount_msat maxfee, u32 finalcltv,
u32 maxdelay, size_t maxparts, struct flow ***flows,
- double *probability);
+ double *probability,
+ enum jsonrpc_errcode *ecode);
/* A wrapper to the single-path constrained solver. */
const char *single_path_routes(const tal_t *ctx, struct route_query *rq,
@@ -28,6 +30,7 @@ const char *single_path_routes(const tal_t *ctx, struct route_query *rq,
struct amount_msat amount,
struct amount_msat maxfee, u32 finalcltv,
u32 maxdelay, struct flow ***flows,
- double *probability);
+ double *probability,
+ enum jsonrpc_errcode *ecode);
#endif /* LIGHTNING_PLUGINS_ASKRENE_CHILD_MCF_H */
Why this scored 19/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.