What changed, and why it matters
This commit adds extra safety checks to Core Lightning's 'askrene' routing plugin. After computing payment routes, it now verifies that every hop's amount respects each channel's advertised minimum and maximum HTLC limits. If a route violates those limits, it is rejected with a broken/log message. This is a defensive hardening change rather than a fix for a known active exploit.
Treat as a low-priority hardening patch. Review whether the checks cover all flow outputs and whether failures are handled gracefully by callers. No urgent deployment is indicated absent a disclosed vulnerability.
Security signals we found
Defensive validation of HTLC min/max constraints after route computation
Use of LOG_BROKEN on constraint violation, indicating an unexpected internal inconsistency
Comment framing as 'paranoid checks' and 'don't trust, verify'
No changelog entry, suggesting routine hardening rather than announced vulnerability fix
Evidence from the diff
The patch introduces two new validation functions, check_htlc_min_limits() and check_htlc_max_limits(), in plugins/askrene/mcf.c. They iterate over each computed flow’s path from the destination backward, accumulating fees, and compare each hop amount against the exact htlc_min and htlc_max values from the channel update. If either check fails, linear_routes() logs a LOG_BROKEN message, frees the flows, and returns failure. The commit title calls these ‘paranoid checks’ and the code comment says ‘don’t trust, verify’.
Changed components
plugins/askrene/mcf.clinear_routes() functionaskrene payment routing flow generationInspect captured patch +100 / −0
diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c
index 0e87e1c7..1c43efc7 100644
--- a/plugins/askrene/mcf.c
+++ b/plugins/askrene/mcf.c
@@ -1253,6 +1253,89 @@ fail:
return NULL;
}
+/* Get the scidd for the i'th hop in flow */
+static void get_scidd(const struct gossmap *gossmap, const struct flow *flow,
+ size_t i, struct short_channel_id_dir *scidd)
+{
+ scidd->scid = gossmap_chan_scid(gossmap, flow->path[i]);
+ scidd->dir = flow->dirs[i];
+}
+
+/* We use an fp16_t approximatin for htlc_max/min: this gets the exact value. */
+static struct amount_msat
+get_chan_htlc_max(const struct route_query *rq, const struct gossmap_chan *c,
+ const struct short_channel_id_dir *scidd)
+{
+ struct amount_msat htlc_max;
+
+ gossmap_chan_get_update_details(rq->gossmap, c, scidd->dir, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL,
+ &htlc_max);
+ return htlc_max;
+}
+
+static struct amount_msat
+get_chan_htlc_min(const struct route_query *rq, const struct gossmap_chan *c,
+ const struct short_channel_id_dir *scidd)
+{
+ struct amount_msat htlc_min;
+
+ gossmap_chan_get_update_details(rq->gossmap, c, scidd->dir, NULL, NULL,
+ NULL, NULL, NULL, NULL, &htlc_min,
+ NULL);
+ return htlc_min;
+}
+
+static bool check_htlc_min_limits(struct route_query *rq, struct flow **flows)
+{
+
+ for (size_t k = 0; k < tal_count(flows); k++) {
+ struct flow *flow = flows[k];
+ size_t pathlen = tal_count(flow->path);
+ struct amount_msat hop_amt = flow->delivers;
+ for (size_t i = pathlen - 1; i < pathlen; i--) {
+ const struct half_chan *h = flow_edge(flow, i);
+ struct short_channel_id_dir scidd;
+
+ get_scidd(rq->gossmap, flow, i, &scidd);
+ struct amount_msat htlc_min =
+ get_chan_htlc_min(rq, flow->path[i], &scidd);
+ if (amount_msat_less(hop_amt, htlc_min))
+ return false;
+
+ if (!amount_msat_add_fee(&hop_amt, h->base_fee,
+ h->proportional_fee))
+ abort();
+ }
+ }
+ return true;
+}
+
+static bool check_htlc_max_limits(struct route_query *rq, struct flow **flows)
+{
+
+ for (size_t k = 0; k < tal_count(flows); k++) {
+ struct flow *flow = flows[k];
+ size_t pathlen = tal_count(flow->path);
+ struct amount_msat hop_amt = flow->delivers;
+ for (size_t i = pathlen - 1; i < pathlen; i--) {
+ const struct half_chan *h = flow_edge(flow, i);
+ struct short_channel_id_dir scidd;
+
+ get_scidd(rq->gossmap, flow, i, &scidd);
+ struct amount_msat htlc_max =
+ get_chan_htlc_max(rq, flow->path[i], &scidd);
+ if (amount_msat_greater(hop_amt, htlc_max))
+ return false;
+
+ if (!amount_msat_add_fee(&hop_amt, h->base_fee,
+ h->proportional_fee))
+ abort();
+ }
+ }
+ return true;
+}
+
/* FIXME: add extra constraint maximum route length, use an activation
* probability cost for each channel. Recall that every activation cost, eg.
* base fee and activation probability can only be properly added modifying the
@@ -1471,6 +1554,23 @@ linear_routes(const tal_t *ctx, struct route_query *rq,
* hence after we freed working_ctx. */
*probability = flows_probability(ctx, rq, flows);
+ /* we should have fixed all htlc violations, "don't trust,
+ * verify" */
+ if (!check_htlc_min_limits(rq, *flows)) {
+ error_message =
+ rq_log(rq, rq, LOG_BROKEN,
+ "%s: check_htlc_min_limits failed", __func__);
+ *flows = tal_free(*flows);
+ goto fail;
+ }
+ if (!check_htlc_max_limits(rq, *flows)) {
+ error_message =
+ rq_log(rq, rq, LOG_BROKEN,
+ "%s: check_htlc_max_limits failed", __func__);
+ *flows = tal_free(*flows);
+ goto fail;
+ }
+
return NULL;
fail:
/* cleanup */
Why this scored 42/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.