askrene: fixed a timeout corner case
What changed, and why it matters
This commit fixes a bug in Core Lightning's payment routing plugin (askrene) where certain impossible or severely constrained payments would make the routing engine loop until a timeout instead of quickly giving up. The fix detects tiny or nearly-undeliverable payment paths, disables the bottleneck channel, and removes the bad path so the engine can fail fast. It is a denial-of-service/availability issue rather than theft of funds, and it only affects routing attempts that already violate constraints.
Treat as a routine bug-fix patch with minor availability impact. Reviewers should verify that remove_bottleneck() never disables a channel incorrectly when deliverable is merely small but valid, and that the 1000 msat / 20% thresholds are safe for all network sizes. Re-run test_impossible_payment and related askrene tests before release.
Security signals we found
Denial-of-service/availability: routing operation timeout due to algorithmic loop
Resource exhaustion: wasted runtime in askrene main loop
Algorithmic constraint violation: HTLC max and fee constraints made a path effectively undeliverable
New defensive guard: early removal of zero-amount and near-zero-deliverable flows
New bottleneck-disabling logic to prune impossible channels
Test re-enabled: test_impossible_payment validates the timeout corner case
Evidence from the diff
The patch adds remove_bottleneck() in plugins/askrene/refine.c, which iterates a flow’s channels, computes the effective maximum per channel as min(known_max_liquidity, htlc_max), and disables the most restrictive channel. refine_flows() is changed from a simple clamping loop to one that: (1) drops zero-amount flows, (2) drops flows whose deliverable amount is <1000 msat or <20% of the requested amount while disabling the bottleneck channel, and (3) early-exits if no flows remain. A previously skipped test, test_impossible_payment, is re-enabled. The bug caused the main MCF/refinement loop to keep retrying a path that could never satisfy HTLC max and fee constraints, burning CPU until timeout.
Changed components
plugins/askrene/refine.ctests/test_askrene.pyaskrene payment routing pluginInspect captured patch +75 / −4
diff --git a/plugins/askrene/refine.c b/plugins/askrene/refine.c
index d6802709..fa28219d 100644
--- a/plugins/askrene/refine.c
+++ b/plugins/askrene/refine.c
@@ -303,6 +303,45 @@ remove_htlc_min_violations(const tal_t *ctx, struct route_query *rq,
return error_message;
}
+/* Loop over the channels in the path and disable the one with the least
+ * permiting amount based on htlc_max and known max liquidity. */
+static const char *remove_bottleneck(const tal_t *ctx, struct route_query *rq,
+ const struct flow *flow)
+{
+ const char *error_message = NULL;
+ struct amount_msat min = AMOUNT_MSAT(-1);
+ u32 min_pos = UINT32_MAX;
+ struct amount_msat htlc_max, known_max, unused;
+ struct short_channel_id_dir scidd;
+ size_t idx;
+ for (u32 i = 0; i < tal_count(flow->path); i++) {
+ htlc_max = get_chan_htlc_max(rq, flow->path[i], flow->dirs[i]);
+ get_constraints(rq, flow->path[i], flow->dirs[i], &unused,
+ &known_max);
+ known_max = amount_msat_min(known_max, htlc_max);
+ if (amount_msat_less(known_max, min)) {
+ min = known_max;
+ min_pos = i;
+ }
+ }
+ if (min_pos >= tal_count(flow->path)) {
+ error_message = rq_log(
+ ctx, rq, LOG_BROKEN,
+ "%s: failed to find any bottleneck, flow has no hops? %s",
+ __func__, fmt_flow_full(tmpctx, rq, flow));
+ } else {
+ get_scidd(rq->gossmap, flow, min_pos, &scidd);
+ rq_log(ctx, rq, LOG_INFORM,
+ "Disabling bottleneck channel %s with "
+ "htlc_max/known_max at %s",
+ fmt_short_channel_id_dir(ctx, &scidd),
+ fmt_amount_msat(ctx, min));
+ idx = flow->dirs[min_pos] +
+ 2 * gossmap_chan_idx(rq->gossmap, flow->path[min_pos]);
+ bitmap_set_bit(rq->disabled_chans, idx);
+ }
+ return error_message;
+}
static struct amount_msat sum_all_deliver(struct flow **flows)
{
@@ -494,10 +533,43 @@ const char *refine_flows(const tal_t *ctx, struct route_query *rq,
size_t *flows_index;
/* do not deliver more than HTLC_MAX allow us */
- for (size_t i = 0; i < tal_count(*flows); i++) {
+ for (size_t i = 0; i < tal_count(*flows);) {
+ struct amount_msat try_deliver = (*flows)[i]->delivers;
+ struct amount_msat deliverable =
+ flow_max_deliverable(rq, (*flows)[i]);
+
+ /* We don't expect to have a zero flow amount here. Just report
+ * it. */
+ if (amount_msat_is_zero(try_deliver)) {
+ rq_log(ctx, rq, LOG_UNUSUAL,
+ "Tried to refine a flow with zero amount: %s",
+ fmt_flow_full(tmpctx, rq, (*flows)[i]));
+ del_flow_from_arr(flows, i);
+ continue;
+ }
+
+ /* A path with a very small deliverable amount is not worth the
+ * effort, and we don't want either a path that for fees and
+ * HTLC max constraints removes too much from the actual
+ * delivery amount. In theory the MCF already has partitioned
+ * the payment in different paths. The refinement step is not
+ * expected to change the flow by much. */
+ if (amount_msat_less(deliverable, AMOUNT_MSAT(1000)) ||
+ amount_msat_ratio(deliverable, try_deliver) < 0.2) {
+ error_message = remove_bottleneck(ctx, rq, (*flows)[i]);
+ if (error_message)
+ goto fail;
+ del_flow_from_arr(flows, i);
+ continue;
+ }
+
(*flows)[i]->delivers =
- amount_msat_min((*flows)[i]->delivers,
- flow_max_deliverable(rq, (*flows)[i]));
+ amount_msat_min(try_deliver, deliverable);
+ i++;
+ }
+ if (tal_count(*flows) == 0) {
+ /* No flows left to complete the next steps, early exit. */
+ goto fail;
}
/* remove excess from MCF granularity if any */
diff --git a/tests/test_askrene.py b/tests/test_askrene.py
index c613d2c7..15bb02b3 100644
--- a/tests/test_askrene.py
+++ b/tests/test_askrene.py
@@ -2433,7 +2433,6 @@ def test_includefees(node_factory):
)
-@unittest.skip
def test_impossible_payment(node_factory):
"""A payment that is impossible due to HTLC constraints and fees. The
constraint might cause a timeout in in askrene's main loop due to the refine
Why this scored 32/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.