askrene: refine: disable HTLC min violations
What changed, and why it matters
This change fixes a routing bug in Core Lightning's experimental 'askrene' plugin. Previously, when the plugin planned a payment route, it could repeatedly try to use channels whose minimum payment size (htlc_min) was bigger than the amount being sent, wasting computation and potentially producing bad routes. The patch makes the plugin remember and temporarily disable those channels during the same routing query, so it doesn't hit the same constraint twice.
Treat as a routine bug-fix patch. Reviewers should verify that disabled_chans is correctly sized for both directions (2 * max_chan_idx) and that bitmap indices are never out of bounds, especially when localmods add channels. No urgent security deployment is indicated.
Security signals we found
Logic bug in route computation that could cause repeated htlc_min violations
Potential denial-of-service vector: wasted CPU cycles / path-finding loops
No memory safety, cryptographic, or remote-code-execution signals in diff
Evidence from the diff
The commit adds a per-route_query bitmap, disabled_chans, and a helper channel_is_available() that treats channels in this bitmap as unavailable. During refine_flows(), when a flow violates a channel’s htlc_min, remove_htlc_min_violations() sets the corresponding bit in disabled_chans. The MCF (minimum-cost flow) path-finding and linear-network initialization now consult channel_is_available(), so disabled channels are excluded from subsequent iterations. This prevents the same htlc_min violation from being rediscovered and improves route refinement convergence.
Changed components
plugins/askrene/askrene.cplugins/askrene/askrene.hplugins/askrene/mcf.cplugins/askrene/refine.cInspect captured patch +67 / −6
diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c
index 31126f82..ad6bd088 100644
--- a/plugins/askrene/askrene.c
+++ b/plugins/askrene/askrene.c
@@ -564,6 +564,13 @@ static struct command_result *do_getroutes(struct command *cmd,
/* we temporarily apply localmods */
gossmap_apply_localmods(askrene->gossmap, localmods);
+ /* I want to be able to disable channels while working on this query.
+ * Layers are for user interaction and cannot be used for this purpose.
+ */
+ rq->disabled_chans =
+ tal_arrz(rq, bitmap,
+ 2 * BITMAP_NWORDS(gossmap_max_chan_idx(askrene->gossmap)));
+
/* localmods can add channels, so we need to allocate biases array
* *afterwards* */
rq->biases =
diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h
index 8688835e..7f1352f6 100644
--- a/plugins/askrene/askrene.h
+++ b/plugins/askrene/askrene.h
@@ -2,6 +2,7 @@
#define LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H
#include "config.h"
#include <bitcoin/short_channel_id.h>
+#include <ccan/bitmap/bitmap.h>
#include <ccan/htable/htable_type.h>
#include <ccan/list/list.h>
#include <common/amount.h>
@@ -60,6 +61,9 @@ struct route_query {
/* Additional per-htlc cost for local channels */
const struct additional_cost_htable *additional_costs;
+
+ /* channels we disable during computation to meet constraints */
+ bitmap *disabled_chans;
};
/* Given a gossmap channel, get the current known min/max */
diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c
index 696b810c..6d50cb8b 100644
--- a/plugins/askrene/mcf.c
+++ b/plugins/askrene/mcf.c
@@ -319,6 +319,15 @@ static void set_capacity(s64 *capacity, u64 value, u64 *cap_on_capacity)
*cap_on_capacity -= *capacity;
}
+/* Helper to check whether a channel is available */
+static bool channel_is_available(const struct route_query *rq,
+ const struct gossmap_chan *chan, const int dir)
+{
+ const u32 c_idx = gossmap_chan_idx(rq->gossmap, chan);
+ return gossmap_chan_set(chan, dir) && chan->half[dir].enabled &&
+ !bitmap_test_bit(rq->disabled_chans, c_idx * 2 + dir);
+}
+
/* FIXME: unit test this */
/* The probability of forwarding a payment amount given a high and low liquidity
* bounds.
@@ -568,7 +577,7 @@ static void init_linear_network(const tal_t *ctx,
const struct gossmap_chan *c = gossmap_nth_chan(gossmap,
node, j, &half);
- if (!gossmap_chan_set(c, half) || !c->half[half].enabled)
+ if (!channel_is_available(params->rq, c, half))
continue;
/* If a channel insists on more than our total, remove it */
@@ -644,7 +653,7 @@ struct chan_flow
* */
static struct node find_path_or_cycle(
const tal_t *working_ctx,
- const struct gossmap *gossmap,
+ const struct route_query *rq,
const struct chan_flow *chan_flow,
const struct node source,
const s64 *balance,
@@ -653,6 +662,7 @@ static struct node find_path_or_cycle(
int *prev_dir,
u32 *prev_idx)
{
+ const struct gossmap *gossmap = rq->gossmap;
const size_t max_num_nodes = gossmap_max_node_idx(gossmap);
bitmap *visited =
tal_arrz(working_ctx, bitmap, BITMAP_NWORDS(max_num_nodes));
@@ -671,7 +681,7 @@ static struct node find_path_or_cycle(
const struct gossmap_chan *c =
gossmap_nth_chan(gossmap, cur, i, &dir);
- if (!gossmap_chan_set(c, dir) || !c->half[dir].enabled)
+ if (!channel_is_available(rq, c, dir))
continue;
const u32 c_idx = gossmap_chan_idx(gossmap, c);
@@ -877,7 +887,7 @@ get_flow_paths(const tal_t *ctx,
while (balance[source.idx] < 0) {
prev_chan[source.idx] = NULL;
struct node sink = find_path_or_cycle(
- working_ctx, params->rq->gossmap, chan_flow, source,
+ working_ctx, params->rq, chan_flow, source,
balance, prev_chan, prev_dir, prev_idx);
if (balance[sink.idx] > 0)
@@ -1107,8 +1117,7 @@ static void init_linear_network_single_path(
gossmap_nth_chan(gossmap, node, j, &half);
struct amount_msat mincap, maxcap;
- if (!gossmap_chan_set(c, half) ||
- !c->half[half].enabled)
+ if (!channel_is_available(params->rq, c, half))
continue;
/* If a channel cannot forward the total amount we don't
diff --git a/plugins/askrene/refine.c b/plugins/askrene/refine.c
index e93d0953..97472443 100644
--- a/plugins/askrene/refine.c
+++ b/plugins/askrene/refine.c
@@ -735,6 +735,38 @@ static struct amount_msat path_min_deliverable(struct channel_data *path)
return least_destination;
}
+static const char *
+remove_htlc_min_violations(const tal_t *ctx, struct route_query *rq,
+ const struct flow *flow,
+ const struct channel_data *channels)
+{
+ const char *error_message = NULL;
+ struct amount_msat msat = flow->delivers;
+ for (size_t i = tal_count(flow->path) - 1; i < tal_count(flow->path);
+ i--) {
+ if (amount_msat_less(msat, channels[i].htlc_min)) {
+ rq_log(
+ ctx, rq, LOG_INFORM,
+ "Sending %s across %s would violate htlc_min "
+ "(~%s), disabling this channel",
+ fmt_amount_msat(ctx, msat),
+ fmt_short_channel_id_dir(ctx, &channels[i].scidd),
+ fmt_amount_msat(ctx, channels[i].htlc_min));
+ bitmap_set_bit(rq->disabled_chans, channels[i].idx);
+ break;
+ }
+ if (!amount_msat_add_fee(
+ &msat, channels[i].fee_base_msat,
+ channels[i].fee_proportional_millionths)) {
+ error_message =
+ rq_log(ctx, rq, LOG_BROKEN,
+ "%s: Adding fee to amount", __func__);
+ break;
+ }
+ }
+ return error_message;
+}
+
static struct amount_msat sum_all_deliver(struct flow **flows,
size_t *flows_index)
{
@@ -819,6 +851,7 @@ const char *refine_flows(const tal_t *ctx, struct route_query *rq,
struct amount_msat deliver, struct flow ***flows)
{
const tal_t *working_ctx = tal(ctx, tal_t);
+ const char *error_message = NULL;
struct amount_msat *max_deliverable;
struct amount_msat *min_deliverable;
struct channel_data **channel_mpp_cache;
@@ -863,6 +896,10 @@ const char *refine_flows(const tal_t *ctx, struct route_query *rq,
}
/* htlc_min is not met for this flow */
tal_arr_remove(&flows_index, i);
+ error_message = remove_htlc_min_violations(
+ working_ctx, rq, (*flows)[k], channel_mpp_cache[k]);
+ if (error_message)
+ goto fail;
}
/* remove 0 amount flows if any */
@@ -889,4 +926,8 @@ const char *refine_flows(const tal_t *ctx, struct route_query *rq,
tal_free(working_ctx);
return NULL;
+
+fail:
+ tal_free(working_ctx);
+ return error_message;
}
Why this scored 35/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.