askrene: add new functions to refine flows
What changed, and why it matters
This commit adds new internal helper functions to the askrene routing plugin in Core Lightning. The code is designed to refine payment routes after they are computed, trimming them so they respect per-hop HTLC maximums, liquidity limits, and exact fees. There is no indication in the commit that this fixes a security vulnerability; it reads as a normal feature or accuracy improvement to the routing algorithm.
No security action required. Treat as a routine code change. If reviewing for quality, note the embedded TODO/FIXME items about unit tests and single-channel zero-fee edge cases.
Security signals we found
No security-relevant keywords in commit title or message
No CVE, advisory, or incident references present
Code is additive (228 insertions, 0 deletions) and localized to routing refinement
Contains developer TODO/FIXME comments about unit tests and edge cases, consistent with normal development rather than an emergency fix
Evidence from the diff
The patch introduces refine_flows() plus supporting helpers (new_channel_path_cache, new_channel_mpp_cache, path_max_deliverable, remove_excess, sum_all_deliver, revcmp_flows) in plugins/askrene/refine.c/h. These helpers cache channel constraints, compute the maximum deliverable amount along each path accounting for fees and htlc_max/liquidity_max, and reduce or drop flows so the total delivery does not exceed the requested amount. The commit message frames this as a new refinement approach to reduce excess caused by MCF granularity and HTLC max constraints. No security relevance is claimed, no CVE or advisory is referenced, and no external incident details are supplied.
Changed components
plugins/askrene/refine.cplugins/askrene/refine.hInspect captured patch +228 / −0
diff --git a/plugins/askrene/refine.c b/plugins/askrene/refine.c
index 51f030ad..a44a9cd0 100644
--- a/plugins/askrene/refine.c
+++ b/plugins/askrene/refine.c
@@ -1,4 +1,5 @@
#include "config.h"
+#include <ccan/asort/asort.h>
#include <ccan/tal/str/str.h>
#include <common/gossmap.h>
#include <plugins/askrene/askrene.h>
@@ -6,6 +7,14 @@
#include <plugins/askrene/refine.h>
#include <plugins/askrene/reserve.h>
+/* Channel data for fast retrieval. */
+struct channel_data {
+ struct amount_msat htlc_min, htlc_max, liquidity_max;
+ u32 fee_base_msat, fee_proportional_millionths;
+ struct short_channel_id_dir scidd;
+ u32 idx;
+};
+
/* We (ab)use the reservation system to place temporary reservations
* on channels while we are refining each flow. This has the effect
* of making flows aware of each other. */
@@ -604,3 +613,217 @@ out:
tal_free(reservations);
return ret;
}
+
+/* Cache channel data along the path used by this flow. */
+static struct channel_data *new_channel_path_cache(const tal_t *ctx,
+ struct route_query *rq,
+ struct flow *flow)
+{
+ const size_t pathlen = tal_count(flow->path);
+ struct channel_data *path = tal_arr(ctx, struct channel_data, pathlen);
+
+ for (size_t i = 0; i < pathlen; i++) {
+ /* knowledge on liquidity bounds */
+ struct amount_msat known_min, known_max;
+ const struct half_chan *h = flow_edge(flow, i);
+ struct short_channel_id_dir scidd;
+
+ get_scidd(rq->gossmap, flow, i, &scidd);
+ get_constraints(rq, flow->path[i], flow->dirs[i], &known_min,
+ &known_max);
+
+ path[i].htlc_min = get_chan_htlc_min(rq, flow->path[i], &scidd);
+ path[i].htlc_max = get_chan_htlc_max(rq, flow->path[i], &scidd);
+ path[i].fee_base_msat = h->base_fee;
+ path[i].fee_proportional_millionths = h->proportional_fee;
+ path[i].liquidity_max = known_max;
+ path[i].scidd = scidd;
+ path[i].idx = scidd.dir +
+ 2 * gossmap_chan_idx(rq->gossmap, flow->path[i]);
+ }
+ return path;
+}
+
+/* Cache channel data along multiple paths. */
+static struct channel_data **new_channel_mpp_cache(const tal_t *ctx,
+ struct route_query *rq,
+ struct flow **flows)
+{
+ const size_t npaths = tal_count(flows);
+ struct channel_data **paths =
+ tal_arr(ctx, struct channel_data *, npaths);
+ for (size_t i = 0; i < npaths; i++) {
+ paths[i] = new_channel_path_cache(paths, rq, flows[i]);
+ }
+ return paths;
+}
+
+/* Reverse order: bigger first */
+static int revcmp_flows(const size_t *a, const size_t *b, struct flow **flows)
+{
+ if (amount_msat_eq(flows[*a]->delivers, flows[*b]->delivers))
+ return 0;
+ if (amount_msat_greater(flows[*a]->delivers, flows[*b]->delivers))
+ return -1;
+ return 1;
+}
+
+// TODO: unit test:
+// -> make a path
+// -> compute x = path_max_deliverable
+// -> check that htlc_max are all satisfied
+// -> check that (x+1) at least one htlc_max is violated
+/* Given the channel constraints, return the maximum amount that can be
+ * delivered. */
+static struct amount_msat path_max_deliverable(struct channel_data *path)
+{
+ struct amount_msat deliver = AMOUNT_MSAT(-1);
+ for (size_t i = 0; i < tal_count(path); i++) {
+ deliver =
+ amount_msat_sub_fee(deliver, path[i].fee_base_msat,
+ path[i].fee_proportional_millionths);
+ deliver = amount_msat_min(deliver, path[i].htlc_max);
+ deliver = amount_msat_min(deliver, path[i].liquidity_max);
+ }
+ return deliver;
+}
+
+static struct amount_msat sum_all_deliver(struct flow **flows,
+ size_t *flows_index)
+{
+ struct amount_msat all_deliver = AMOUNT_MSAT(0);
+ for (size_t i = 0; i < tal_count(flows_index); i++) {
+ if (!amount_msat_accumulate(&all_deliver,
+ flows[flows_index[i]]->delivers))
+ abort();
+ }
+ return all_deliver;
+}
+
+/* It reduces the amount of the flows and/or removes some flows in order to
+ * deliver no more than max_deliver. It will leave at least one flow.
+ * Returns the total delivery amount. */
+static struct amount_msat remove_excess(struct flow **flows,
+ size_t **flows_index,
+ struct amount_msat max_deliver)
+{
+ if (tal_count(flows) == 0)
+ return AMOUNT_MSAT(0);
+
+ struct amount_msat all_deliver, excess;
+ all_deliver = sum_all_deliver(flows, *flows_index);
+
+ /* early exit: there is no excess */
+ if (!amount_msat_sub(&excess, all_deliver, max_deliver) ||
+ amount_msat_is_zero(excess))
+ return all_deliver;
+
+ asort(*flows_index, tal_count(*flows_index), revcmp_flows, flows);
+
+ /* Remove the smaller parts if they deliver less than the
+ * excess. */
+ for (int i = tal_count(*flows_index) - 1; i >= 0; i--) {
+ if (!amount_msat_sub(&excess, excess,
+ flows[(*flows_index)[i]]->delivers))
+ break;
+ if (!amount_msat_sub(&all_deliver, all_deliver,
+ flows[(*flows_index)[i]]->delivers))
+ abort();
+ tal_arr_remove(flows_index, i);
+ }
+
+ /* If we still have some excess, remove it from the
+ * current flows in the same proportion every flow contributes to the
+ * total. */
+ struct amount_msat old_excess = excess;
+ struct amount_msat old_deliver = all_deliver;
+ for (size_t i = 0; i < tal_count(*flows_index); i++) {
+ double fraction = amount_msat_ratio(
+ flows[(*flows_index)[i]]->delivers, old_deliver);
+ struct amount_msat remove;
+
+ if (!amount_msat_scale(&remove, old_excess, fraction))
+ abort();
+
+ /* rounding errors: don't remove more than excess */
+ remove = amount_msat_min(remove, excess);
+
+ if (!amount_msat_sub(&excess, excess, remove))
+ abort();
+
+ if (!amount_msat_sub(&all_deliver, all_deliver, remove) ||
+ !amount_msat_sub(&flows[(*flows_index)[i]]->delivers,
+ flows[(*flows_index)[i]]->delivers,
+ remove))
+ abort();
+ }
+
+ /* any rounding error left, take it from the first */
+ assert(tal_count(*flows_index) > 0);
+ if (!amount_msat_sub(&all_deliver, all_deliver, excess) ||
+ !amount_msat_sub(&flows[(*flows_index)[0]]->delivers,
+ flows[(*flows_index)[0]]->delivers, excess))
+ abort();
+ return all_deliver;
+}
+
+/* FIXME: on failure return error message */
+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);
+ struct amount_msat *max_deliverable;
+ struct channel_data **channel_mpp_cache;
+ size_t *flows_index;
+
+ /* we might need to access this data multiple times, so we cache
+ * it */
+ channel_mpp_cache = new_channel_mpp_cache(working_ctx, rq, *flows);
+ max_deliverable = tal_arrz(working_ctx, struct amount_msat,
+ tal_count(channel_mpp_cache));
+ flows_index = tal_arrz(working_ctx, size_t, tal_count(*flows));
+ for (size_t i = 0; i < tal_count(channel_mpp_cache); i++) {
+ // FIXME: does path_max_deliverable work for a single
+ // channel with 0 fees?
+ max_deliverable[i] = path_max_deliverable(channel_mpp_cache[i]);
+ /* We use an array of indexes to keep track of the order
+ * of the flows. Likewise flows can be removed by simply
+ * shrinking the flows_index array. */
+ flows_index[i] = i;
+ }
+
+ /* do not deliver more than HTLC_MAX allow us */
+ for (size_t i = 0; i < tal_count(flows_index); i++) {
+ (*flows)[flows_index[i]]->delivers =
+ amount_msat_min((*flows)[flows_index[i]]->delivers,
+ max_deliverable[flows_index[i]]);
+ }
+
+ /* remove excess from MCF granularity if any */
+ remove_excess(*flows, &flows_index, deliver);
+
+ /* remove 0 amount flows if any */
+ asort(flows_index, tal_count(flows_index), revcmp_flows, *flows);
+ for (int i = tal_count(flows_index) - 1; i >= 0; i--) {
+ if (!amount_msat_is_zero((*flows)[flows_index[i]]->delivers))
+ break;
+ tal_arr_remove(&flows_index, i);
+ }
+
+ /* finally write the remaining flows */
+ struct flow **tmp_flows = tal_arr(working_ctx, struct flow *, 0);
+ for (size_t i = 0; i < tal_count(flows_index); i++) {
+ tal_arr_expand(&tmp_flows, (*flows)[flows_index[i]]);
+ (*flows)[flows_index[i]] = NULL;
+ }
+ for (size_t i = 0; i < tal_count(*flows); i++) {
+ (*flows)[i] = tal_free((*flows)[i]);
+ }
+ tal_resize(flows, 0);
+ for (size_t i = 0; i < tal_count(tmp_flows); i++) {
+ tal_arr_expand(flows, tmp_flows[i]);
+ }
+
+ tal_free(working_ctx);
+ return NULL;
+}
diff --git a/plugins/askrene/refine.h b/plugins/askrene/refine.h
index 66befec9..8757f736 100644
--- a/plugins/askrene/refine.h
+++ b/plugins/askrene/refine.h
@@ -23,4 +23,9 @@ refine_with_fees_and_limits(const tal_t *ctx,
struct amount_msat deliver,
struct flow ***flows,
double *flowset_probability);
+
+/* Modify flows to meet HTLC min/max requirements.
+ * It takes into account the exact value of the fees expected at each hop. */
+const char *refine_flows(const tal_t *ctx, struct route_query *rq,
+ struct amount_msat deliver, struct flow ***flows);
#endif /* LIGHTNING_PLUGINS_ASKRENE_REFINE_H */
Why this scored 11/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.