From 58792cd1600acc4b1514cbe1e121bd7719438014 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 28 May 2025 07:33:55 +0100 Subject: [PATCH 1/4] askrene: refactor MCF Refactor MCF solver: remove structs linear_network and residual_network. Prefer passing raw data to the helper functions. Changelog-None Signed-off-by: Lagrang3 --- plugins/askrene/mcf.c | 206 ++++++++++++++---------------------------- 1 file changed, 66 insertions(+), 140 deletions(-) diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index cb2b1ea83075..a13104a22096 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -299,48 +299,17 @@ struct pay_parameters { double base_fee_penalty; }; -/* Representation of the linear MCF network. - * This contains the topology of the extended network (after linearization and - * addition of arc duality). - * This contains also the arc probability and linear fee cost, as well as - * capacity; these quantities remain constant during MCF execution. */ -struct linear_network -{ - struct graph *graph; - - // probability and fee cost associated to an arc - double *arc_prob_cost; - s64 *arc_fee_cost; - s64 *capacity; -}; - -/* This is the structure that keeps track of the network properties while we - * seek for a solution. */ -struct residual_network { - /* residual capacity on arcs */ - s64 *cap; - - /* some combination of prob. cost and fee cost on arcs */ - s64 *cost; - - /* potential function on nodes */ - s64 *potential; - - /* auxiliary data, the excess of flow on nodes */ - s64 *excess; -}; - /* Helper function. * Given an arc of the network (not residual) give me the flow. */ static s64 get_arc_flow( - const struct residual_network *network, + const s64 *arc_residual_capacity, const struct graph *graph, const struct arc arc) { assert(!arc_is_dual(graph, arc)); struct arc dual = arc_dual(graph, arc); - assert(dual.idx < tal_count(network->cap)); - return network->cap[dual.idx]; + assert(dual.idx < tal_count(arc_residual_capacity)); + return arc_residual_capacity[dual.idx]; } /* Set *capacity to value, up to *cap_on_capacity. Reduce cap_on_capacity */ @@ -385,49 +354,6 @@ static void linearize_channel(const struct pay_parameters *params, } } -static struct residual_network * -alloc_residual_network(const tal_t *ctx, const size_t max_num_nodes, - const size_t max_num_arcs) -{ - struct residual_network *residual_network = - tal(ctx, struct residual_network); - - residual_network->cap = tal_arrz(residual_network, s64, max_num_arcs); - residual_network->cost = tal_arrz(residual_network, s64, max_num_arcs); - residual_network->potential = - tal_arrz(residual_network, s64, max_num_nodes); - residual_network->excess = - tal_arrz(residual_network, s64, max_num_nodes); - - return residual_network; -} - -static void init_residual_network( - const struct linear_network * linear_network, - struct residual_network* residual_network) -{ - const struct graph *graph = linear_network->graph; - const size_t max_num_arcs = graph_max_num_arcs(graph); - const size_t max_num_nodes = graph_max_num_nodes(graph); - - for (struct arc arc = {.idx = 0}; arc.idx < max_num_arcs; ++arc.idx) { - if (arc_is_dual(graph, arc) || !arc_enabled(graph, arc)) - continue; - - struct arc dual = arc_dual(graph, arc); - residual_network->cap[arc.idx] = - linear_network->capacity[arc.idx]; - residual_network->cap[dual.idx] = 0; - - residual_network->cost[arc.idx] = - residual_network->cost[dual.idx] = 0; - } - for (u32 i = 0; i < max_num_nodes; ++i) { - residual_network->potential[i] = 0; - residual_network->excess[i] = 0; - } -} - static int cmp_u64(const u64 *a, const u64 *b, void *unused) { if (*a < *b) @@ -447,9 +373,10 @@ static int cmp_double(const double *a, const double *b, void *unused) } static double get_median_ratio(const tal_t *working_ctx, - const struct linear_network* linear_network) + const struct graph *graph, + const double *arc_prob_cost, + const s64 *arc_fee_cost) { - const struct graph *graph = linear_network->graph; const size_t max_num_arcs = graph_max_num_arcs(graph); u64 *u64_arr = tal_arr(working_ctx, u64, max_num_arcs); double *double_arr = tal_arr(working_ctx, double, max_num_arcs); @@ -460,8 +387,8 @@ static double get_median_ratio(const tal_t *working_ctx, if (arc_is_dual(graph, arc) || !arc_enabled(graph, arc)) continue; assert(n < max_num_arcs/2); - u64_arr[n] = linear_network->arc_fee_cost[arc.idx]; - double_arr[n] = linear_network->arc_prob_cost[arc.idx]; + u64_arr[n] = arc_fee_cost[arc.idx]; + double_arr[n] = arc_prob_cost[arc.idx]; n++; } asort(u64_arr, n, cmp_u64, NULL); @@ -475,18 +402,17 @@ static double get_median_ratio(const tal_t *working_ctx, return u64_arr[n/2] / double_arr[n/2]; } -static void combine_cost_function( - const tal_t *working_ctx, - const struct linear_network* linear_network, - struct residual_network *residual_network, - const s8 *biases, - s64 mu) +static void combine_cost_function(const tal_t *working_ctx, + const struct graph *graph, + const double *arc_prob_cost, + const s64 *arc_fee_cost, const s8 *biases, + s64 mu, s64 *arc_cost) { /* probabilty and fee costs are not directly comparable! * Scale by ratio of (positive) medians. */ - const double k = get_median_ratio(working_ctx, linear_network); + const double k = + get_median_ratio(working_ctx, graph, arc_prob_cost, arc_fee_cost); const double ln_30 = log(30); - const struct graph *graph = linear_network->graph; const size_t max_num_arcs = graph_max_num_arcs(graph); for(struct arc arc = {.idx=0};arc.idx < max_num_arcs; ++arc.idx) @@ -494,8 +420,8 @@ static void combine_cost_function( if (arc_is_dual(graph, arc) || !arc_enabled(graph, arc)) continue; - const double pcost = linear_network->arc_prob_cost[arc.idx]; - const s64 fcost = linear_network->arc_fee_cost[arc.idx]; + const double pcost = arc_prob_cost[arc.idx]; + const s64 fcost = arc_fee_cost[arc.idx]; double combined; u32 chanidx; int chandir; @@ -515,13 +441,13 @@ static void combine_cost_function( * e^(-bias / (100/ln(30))) */ double bias_factor = exp(-bias / (100 / ln_30)); - residual_network->cost[arc.idx] = combined * bias_factor; + arc_cost[arc.idx] = combined * bias_factor; } else { - residual_network->cost[arc.idx] = combined; + arc_cost[arc.idx] = combined; } /* and the respective dual */ struct arc dual = arc_dual(graph, arc); - residual_network->cost[dual.idx] = -combined; + arc_cost[dual.idx] = -combined; } } @@ -578,31 +504,26 @@ struct amount_msat linear_flow_cost(const struct flow *flow, return msat_cost; } -/* FIXME: Instead of mapping one-to-one the indexes in the gossmap, try to - * reduce the number of nodes and arcs used by taking only those that are - * enabled. We might save some cpu if the work with a pruned network. */ -static struct linear_network * -init_linear_network(const tal_t *ctx, const struct pay_parameters *params) +static void init_linear_network(const tal_t *ctx, + const struct pay_parameters *params, + struct graph **graph, double **arc_prob_cost, + s64 **arc_fee_cost, s64 **arc_capacity) { - struct linear_network * linear_network = tal(ctx, struct linear_network); const struct gossmap *gossmap = params->rq->gossmap; - const size_t max_num_chans = gossmap_max_chan_idx(gossmap); const size_t max_num_arcs = max_num_chans * ARCS_PER_CHANNEL; const size_t max_num_nodes = gossmap_max_node_idx(gossmap); - linear_network->graph = - graph_new(ctx, max_num_nodes, max_num_arcs, ARC_DUAL_BITOFF); + *graph = graph_new(ctx, max_num_nodes, max_num_arcs, ARC_DUAL_BITOFF); + *arc_prob_cost = tal_arr(ctx, double, max_num_arcs); + for (size_t i = 0; i < max_num_arcs; ++i) + (*arc_prob_cost)[i] = DBL_MAX; - linear_network->arc_prob_cost = tal_arr(linear_network,double,max_num_arcs); - for(size_t i=0;iarc_prob_cost[i]=DBL_MAX; + *arc_fee_cost = tal_arr(ctx, s64, max_num_arcs); + for (size_t i = 0; i < max_num_arcs; ++i) + (*arc_fee_cost)[i] = INT64_MAX; - linear_network->arc_fee_cost = tal_arr(linear_network,s64,max_num_arcs); - for(size_t i=0;iarc_fee_cost[i]=INFINITE; - - linear_network->capacity = tal_arrz(linear_network,s64,max_num_arcs); + *arc_capacity = tal_arrz(ctx, s64, max_num_arcs); for(struct gossmap_node *node = gossmap_first_node(gossmap); node; @@ -660,25 +581,23 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params) struct arc arc = arc_from_parts(chan_id, half, k, false); - graph_add_arc(linear_network->graph, arc, + graph_add_arc(*graph, arc, node_obj(node_id), node_obj(next_id)); - linear_network->capacity[arc.idx] = capacity[k]; - linear_network->arc_prob_cost[arc.idx] = prob_cost[k]; - linear_network->arc_fee_cost[arc.idx] = fee_cost; + (*arc_capacity)[arc.idx] = capacity[k]; + (*arc_prob_cost)[arc.idx] = prob_cost[k]; + (*arc_fee_cost)[arc.idx] = fee_cost; // + the respective dual - struct arc dual = arc_dual(linear_network->graph, arc); + struct arc dual = arc_dual(*graph, arc); - linear_network->capacity[dual.idx] = 0; - linear_network->arc_prob_cost[dual.idx] = -prob_cost[k]; - linear_network->arc_fee_cost[dual.idx] = -fee_cost; + (*arc_capacity)[dual.idx] = 0; + (*arc_prob_cost)[dual.idx] = -prob_cost[k]; + (*arc_fee_cost)[dual.idx] = -fee_cost; } } } - - return linear_network; } // flow on directed channels @@ -873,8 +792,8 @@ static struct flow ** get_flow_paths(const tal_t *ctx, const tal_t *working_ctx, const struct pay_parameters *params, - const struct linear_network *linear_network, - const struct residual_network *residual_network) + const struct graph *graph, + const s64 *arc_residual_capacity) { struct flow **flows = tal_arr(ctx,struct flow*,0); @@ -897,7 +816,6 @@ get_flow_paths(const tal_t *ctx, // Convert the arc based residual network flow into a flow in the // directed channel network. // Compute balance on the nodes. - const struct graph *graph = linear_network->graph; for (struct node n = {.idx = 0}; n.idx < max_num_nodes; n.idx++) { for(struct arc arc = node_adjacency_begin(graph,n); !node_adjacency_end(arc); @@ -906,7 +824,7 @@ get_flow_paths(const tal_t *ctx, if(arc_is_dual(graph, arc)) continue; struct node m = arc_head(graph,arc); - s64 flow = get_arc_flow(residual_network, + s64 flow = get_arc_flow(arc_residual_capacity, graph, arc); u32 chanidx; int chandir; @@ -1000,17 +918,25 @@ struct flow **minflow(const tal_t *ctx, params->base_fee_penalty = base_fee_penalty_estimate(amount); // build the uncertainty network with linearization and residual arcs - struct linear_network *linear_network= init_linear_network(working_ctx, params); - const struct graph *graph = linear_network->graph; + struct graph *graph; + double *arc_prob_cost; + s64 *arc_fee_cost; + s64 *arc_capacity; + init_linear_network(working_ctx, params, &graph, &arc_prob_cost, + &arc_fee_cost, &arc_capacity); + const size_t max_num_arcs = graph_max_num_arcs(graph); const size_t max_num_nodes = graph_max_num_nodes(graph); - struct residual_network *residual_network = - alloc_residual_network(working_ctx, max_num_nodes, max_num_arcs); + s64 *arc_cost; + s64 *node_potential; + s64 *node_excess; + arc_cost = tal_arrz(working_ctx, s64, max_num_arcs); + node_potential = tal_arrz(working_ctx, s64, max_num_nodes); + node_excess = tal_arrz(working_ctx, s64, max_num_nodes); const struct node dst = {.idx = gossmap_node_idx(rq->gossmap, target)}; const struct node src = {.idx = gossmap_node_idx(rq->gossmap, source)}; - init_residual_network(linear_network,residual_network); /* Since we have constraint accuracy, ask to find a payment solution * that can pay a bit more than the actual value rathen than undershoot it. @@ -1018,22 +944,22 @@ struct flow **minflow(const tal_t *ctx, const u64 pay_amount = amount_msat_ratio_ceil(params->amount, params->accuracy); - if (!simple_feasibleflow(working_ctx, linear_network->graph, src, dst, - residual_network->cap, pay_amount)) { + if (!simple_feasibleflow(working_ctx, graph, src, dst, + arc_capacity, pay_amount)) { rq_log(tmpctx, rq, LOG_INFORM, "%s failed: unable to find a feasible flow.", __func__); goto fail; } - combine_cost_function(working_ctx, linear_network, residual_network, - rq->biases, mu); + combine_cost_function(working_ctx, graph, arc_prob_cost, arc_fee_cost, + rq->biases, mu, arc_cost); /* We solve a linear MCF problem. */ if (!mcf_refinement(working_ctx, - linear_network->graph, - residual_network->excess, - residual_network->cap, - residual_network->cost, - residual_network->potential)) { + graph, + node_excess, + arc_capacity, + arc_cost, + node_potential)) { rq_log(tmpctx, rq, LOG_BROKEN, "%s: MCF optimization step failed", __func__); goto fail; @@ -1043,7 +969,7 @@ struct flow **minflow(const tal_t *ctx, * Actual amounts considering fees are computed for every * channel in the routes. */ flow_paths = get_flow_paths(ctx, working_ctx, params, - linear_network, residual_network); + graph, arc_capacity); if(!flow_paths){ rq_log(tmpctx, rq, LOG_BROKEN, "%s: failed to extract flow paths from the MCF solution", From 4499bbf8e3f9d79d4df175bf54ada24e9e18bd53 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 28 May 2025 08:06:33 +0100 Subject: [PATCH 2/4] askrene: add internal API for single-path routes The single path solver uses the same probability cost and fee cost estimation of minflow. Single path routes computed this way are suboptimal with respect to the MCF solution but still are optimal among any other single path. Computationally is way faster than MCF, therefore for some trivial payments it should be prefered. Changelog-None. Signed-off-by: Lagrang3 --- plugins/askrene/mcf.c | 242 ++++++++++++++++++++++++++++++++++++++++++ plugins/askrene/mcf.h | 22 ++++ 2 files changed, 264 insertions(+) diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index a13104a22096..007bb9245228 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -319,6 +319,28 @@ static void set_capacity(s64 *capacity, u64 value, u64 *cap_on_capacity) *cap_on_capacity -= *capacity; } +/* FIXME: unit test this */ +/* The probability of forwarding a payment amount given a high and low liquidity + * bounds. + * @low: the liquidity is known to be greater or equal than "low" + * @high: the liquidity is known to be less than "high" + * @amount: how much is required to forward */ +static double pickhardt_richter_probability(struct amount_msat low, + struct amount_msat high, + struct amount_msat amount) +{ + struct amount_msat all_states, good_states; + if (amount_msat_greater_eq(amount, high)) + return 0.0; + if (!amount_msat_sub(&amount, amount, low)) + return 1.0; + if (!amount_msat_sub(&all_states, high, low)) + abort(); // we expect high > low + if (!amount_msat_sub(&good_states, all_states, amount)) + abort(); // we expect high > amount + return amount_msat_ratio(good_states, all_states); +} + // TODO(eduardo): unit test this /* Split a directed channel into parts with linear cost function. */ static void linearize_channel(const struct pay_parameters *params, @@ -867,6 +889,46 @@ get_flow_paths(const tal_t *ctx, return flows; } +/* Given a single path build a flow set. */ +static struct flow ** +get_flow_singlepath(const tal_t *ctx, const struct pay_parameters *params, + const struct graph *graph, const struct gossmap *gossmap, + const struct node source, const struct node destination, + const u64 pay_amount, const struct arc *prev) +{ + struct flow **flows, *f; + flows = tal_arr(ctx, struct flow *, 1); + f = flows[0] = tal(flows, struct flow); + + size_t length = 0; + + for (u32 cur_idx = destination.idx; cur_idx != source.idx;) { + assert(cur_idx != INVALID_INDEX); + length++; + struct arc arc = prev[cur_idx]; + struct node next = arc_tail(graph, arc); + cur_idx = next.idx; + } + f->path = tal_arr(f, const struct gossmap_chan *, length); + f->dirs = tal_arr(f, int, length); + + for (u32 cur_idx = destination.idx; cur_idx != source.idx;) { + int chandir; + u32 chanidx; + struct arc arc = prev[cur_idx]; + arc_to_parts(arc, &chanidx, &chandir, NULL, NULL); + + length--; + f->path[length] = gossmap_chan_byidx(gossmap, chanidx); + f->dirs[length] = chandir; + + struct node next = arc_tail(graph, arc); + cur_idx = next.idx; + } + f->delivers = params->amount; + return flows; +} + // TODO(eduardo): choose some default values for the minflow parameters /* eduardo: I think it should be clear that this module deals with linear * flows, ie. base fees are not considered. Hence a flow along a path is @@ -1025,6 +1087,186 @@ static struct amount_msat linear_flows_cost(struct flow **flows, return total; } +/* Initialize the data vectors for the single-path solver. */ +static void init_linear_network_single_path( + const tal_t *ctx, const struct pay_parameters *params, struct graph **graph, + double **arc_prob_cost, s64 **arc_fee_cost, s64 **arc_capacity) +{ + const size_t max_num_chans = gossmap_max_chan_idx(params->rq->gossmap); + const size_t max_num_arcs = max_num_chans * ARCS_PER_CHANNEL; + const size_t max_num_nodes = gossmap_max_node_idx(params->rq->gossmap); + + *graph = graph_new(ctx, max_num_nodes, max_num_arcs, ARC_DUAL_BITOFF); + *arc_prob_cost = tal_arr(ctx, double, max_num_arcs); + for (size_t i = 0; i < max_num_arcs; ++i) + (*arc_prob_cost)[i] = DBL_MAX; + + *arc_fee_cost = tal_arr(ctx, s64, max_num_arcs); + for (size_t i = 0; i < max_num_arcs; ++i) + (*arc_fee_cost)[i] = INT64_MAX; + *arc_capacity = tal_arrz(ctx, s64, max_num_arcs); + + const struct gossmap *gossmap = params->rq->gossmap; + + for (struct gossmap_node *node = gossmap_first_node(gossmap); node; + node = gossmap_next_node(gossmap, node)) { + const u32 node_id = gossmap_node_idx(gossmap, node); + + for (size_t j = 0; j < node->num_chans; ++j) { + int half; + const struct gossmap_chan *c = + gossmap_nth_chan(gossmap, node, j, &half); + struct amount_msat mincap, maxcap; + + if (!gossmap_chan_set(c, half) || + !c->half[half].enabled) + continue; + + /* If a channel cannot forward the total amount we don't + * use it. */ + if (amount_msat_less(params->amount, + gossmap_chan_htlc_min(c, half)) || + amount_msat_greater(params->amount, + gossmap_chan_htlc_max(c, half))) + continue; + + get_constraints(params->rq, c, half, &mincap, &maxcap); + /* Assume if min > max, min is wrong */ + if (amount_msat_greater(mincap, maxcap)) + mincap = maxcap; + /* It is preferable to work on 1msat past the known + * bound. */ + if (!amount_msat_accumulate(&maxcap, amount_msat(1))) + abort(); + + /* If amount is greater than the known liquidity upper + * bound we get infinite probability cost. */ + if (amount_msat_greater_eq(params->amount, maxcap)) + continue; + + const u32 chan_id = gossmap_chan_idx(gossmap, c); + + const struct gossmap_node *next = + gossmap_nth_node(gossmap, c, !half); + + const u32 next_id = gossmap_node_idx(gossmap, next); + + /* channel to self? */ + if (node_id == next_id) + continue; + + struct arc arc = + arc_from_parts(chan_id, half, 0, false); + + graph_add_arc(*graph, arc, node_obj(node_id), + node_obj(next_id)); + + (*arc_capacity)[arc.idx] = 1; + (*arc_prob_cost)[arc.idx] = + (-1.0) * log(pickhardt_richter_probability( + mincap, maxcap, params->amount)); + + struct amount_msat fee; + if (!amount_msat_fee(&fee, params->amount, + c->half[half].base_fee, + c->half[half].proportional_fee)) + abort(); + u32 fee_msat; + if (!amount_msat_to_u32(fee, &fee_msat)) + continue; + (*arc_fee_cost)[arc.idx] = + fee_msat + + params->delay_feefactor * c->half[half].delay; + } + } +} + +/* Similar to minflow but computes routes that have a single path. */ +struct flow **single_path_flow(const tal_t *ctx, const struct route_query *rq, + const struct gossmap_node *source, + const struct gossmap_node *target, + struct amount_msat amount, u32 mu, + double delay_feefactor) +{ + struct flow **flow_paths; + /* We allocate everything off this, and free it at the end, + * as we can be called multiple times without cleaning tmpctx! */ + tal_t *working_ctx = tal(NULL, char); + struct pay_parameters *params = tal(working_ctx, struct pay_parameters); + + params->rq = rq; + params->source = source; + params->target = target; + params->amount = amount; + /* for the single-path solver the accuracy does not detriment + * performance */ + params->accuracy = amount; + params->delay_feefactor = delay_feefactor; + params->base_fee_penalty = base_fee_penalty_estimate(amount); + + struct graph *graph; + double *arc_prob_cost; + s64 *arc_fee_cost; + s64 *arc_capacity; + + init_linear_network_single_path(working_ctx, params, &graph, + &arc_prob_cost, &arc_fee_cost, + &arc_capacity); + + const struct node dst = {.idx = gossmap_node_idx(rq->gossmap, target)}; + const struct node src = {.idx = gossmap_node_idx(rq->gossmap, source)}; + + const size_t max_num_nodes = graph_max_num_nodes(graph); + const size_t max_num_arcs = graph_max_num_arcs(graph); + + s64 *potential = tal_arrz(working_ctx, s64, max_num_nodes); + s64 *distance = tal_arrz(working_ctx, s64, max_num_nodes); + s64 *arc_cost = tal_arrz(working_ctx, s64, max_num_arcs); + struct arc *prev = tal_arrz(working_ctx, struct arc, max_num_nodes); + + combine_cost_function(working_ctx, graph, arc_prob_cost, arc_fee_cost, + rq->biases, mu, arc_cost); + + /* We solve a linear cost flow problem. */ + if (!dijkstra_path(working_ctx, graph, src, dst, + /* prune = */ true, arc_capacity, + /*threshold = */ 1, arc_cost, potential, prev, + distance)) { + /* This might fail if we are unable to find a suitable route, it + * doesn't mean the plugin is broken, that's why we LOG_INFORM. */ + rq_log(tmpctx, rq, LOG_INFORM, + "%s: could not find a feasible single path", __func__); + goto fail; + } + const u64 pay_amount = + amount_msat_ratio_ceil(params->amount, params->accuracy); + + /* We dissect the flow into payment routes. + * Actual amounts considering fees are computed for every + * channel in the routes. */ + flow_paths = get_flow_singlepath(ctx, params, graph, rq->gossmap, + src, dst, pay_amount, prev); + if (!flow_paths) { + rq_log(tmpctx, rq, LOG_BROKEN, + "%s: failed to extract flow paths from the single-path " + "solution", + __func__); + goto fail; + } + if (tal_count(flow_paths) != 1) { + rq_log( + tmpctx, rq, LOG_BROKEN, + "%s: single-path solution returned a multi route solution", + __func__); + goto fail; + } + tal_free(working_ctx); + return flow_paths; + +fail: + tal_free(working_ctx); + return NULL; +} const char *default_routes(const tal_t *ctx, struct route_query *rq, const struct gossmap_node *srcnode, diff --git a/plugins/askrene/mcf.h b/plugins/askrene/mcf.h index 701a1ac5bf75..328b7445dc14 100644 --- a/plugins/askrene/mcf.h +++ b/plugins/askrene/mcf.h @@ -34,6 +34,28 @@ struct flow **minflow(const tal_t *ctx, double delay_feefactor, bool single_part); +/** + * API for min cost single path. + * @ctx: context to allocate returned flows from + * @rq: the route_query we're processing (for logging) + * @source: the source to start from + * @target: the target to pay + * @amount: the amount we want to reach @target + * @mu: 0 = corresponds to only probabilities, 100 corresponds to only fee. + * @delay_feefactor: convert 1 block delay into msat. + * + * @delay_feefactor converts 1 block delay into msat, as if it were an additional + * fee. So if a CLTV delay on a node is 5 blocks, that's treated as if it + * were a fee of 5 * @delay_feefactor. + * + * Returns an array with one flow which deliver amount to target, or NULL. + */ +struct flow **single_path_flow(const tal_t *ctx, const struct route_query *rq, + const struct gossmap_node *source, + const struct gossmap_node *target, + struct amount_msat amount, u32 mu, + double delay_feefactor); + /* To sanity check: this is the approximation mcf uses for the cost * of each channel. */ struct amount_msat linear_flow_cost(const struct flow *flow, From 5b012895f29152c1b5c29a70ae773bdda2973a3c Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 28 May 2025 08:48:49 +0100 Subject: [PATCH 3/4] askrene: add algorithm for single path routing Changelog-Added: askrene: an optimal single-path solver has been added, it can be called using the developer option --dev_algorithm=single-path or by adding the layer "auto.no_mpp_support" Signed-off-by: Lagrang3 --- plugins/askrene/askrene.c | 33 ++++++++++--- plugins/askrene/mcf.c | 81 ++++++++++++++++---------------- plugins/askrene/mcf.h | 14 ++++-- tests/test_askrene.py | 99 ++++++++++++++++++++++++++++++++------- 4 files changed, 158 insertions(+), 69 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index c9071fa3a1cf..31126f82c350 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -332,7 +332,11 @@ const char *fmt_flow_full(const tal_t *ctx, } enum algorithm { + /* Min. Cost Flow by successive shortests paths. */ ALGO_DEFAULT, + /* Algorithm that finds the optimal routing solution constrained to a + * single path. */ + ALGO_SINGLE_PATH, }; static struct command_result * @@ -343,6 +347,8 @@ param_algorithm(struct command *cmd, const char *name, const char *buffer, *algo = tal(cmd, enum algorithm); if (streq(algo_str, "default")) **algo = ALGO_DEFAULT; + else if (streq(algo_str, "single-path")) + **algo = ALGO_SINGLE_PATH; else return command_fail_badparam(cmd, name, buffer, tok, "unknown algorithm"); @@ -517,7 +523,7 @@ void get_constraints(const struct route_query *rq, static struct command_result *do_getroutes(struct command *cmd, struct gossmap_localmods *localmods, - const struct getroutes_info *info) + struct getroutes_info *info) { struct askrene *askrene = get_askrene(cmd->plugin); struct route_query *rq = tal(cmd, struct route_query); @@ -586,15 +592,28 @@ static struct command_result *do_getroutes(struct command *cmd, goto fail; } + /* auto.no_mpp_support layer overrides any choice of algorithm. */ + if (have_layer(info->layers, "auto.no_mpp_support") && + info->dev_algo != ALGO_SINGLE_PATH) { + info->dev_algo = ALGO_SINGLE_PATH; + rq_log(tmpctx, rq, LOG_DBG, + "Layer no_mpp_support is active we switch to a " + "single path algorithm."); + } + /* Compute the routes. At this point we might select between multiple * algorithms. Right now there is only one algorithm available. */ struct timemono time_start = time_mono(); - assert(info->dev_algo == ALGO_DEFAULT); - err = default_routes(rq, rq, srcnode, dstnode, info->amount, - /* only one path? = */ - have_layer(info->layers, "auto.no_mpp_support"), - info->maxfee, info->finalcltv, info->maxdelay, - &flows, &probability); + if (info->dev_algo == ALGO_SINGLE_PATH) { + err = single_path_routes(rq, rq, srcnode, dstnode, info->amount, + info->maxfee, info->finalcltv, + info->maxdelay, &flows, &probability); + } else { + assert(info->dev_algo == ALGO_DEFAULT); + err = default_routes(rq, rq, srcnode, dstnode, info->amount, + info->maxfee, info->finalcltv, + info->maxdelay, &flows, &probability); + } struct timerel time_delta = timemono_between(time_mono(), time_start); /* log the time of computation */ diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 007bb9245228..2c43ae4da78d 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -947,8 +947,7 @@ struct flow **minflow(const tal_t *ctx, const struct gossmap_node *target, struct amount_msat amount, u32 mu, - double delay_feefactor, - bool single_part) + double delay_feefactor) { struct flow **flow_paths; /* We allocate everything off this, and free it at the end, @@ -1039,31 +1038,6 @@ struct flow **minflow(const tal_t *ctx, goto fail; } tal_free(working_ctx); - - /* This is dumb, but if you don't support MPP you don't deserve any - * better. Pile it into the largest part if not already. */ - if (single_part) { - struct flow *best = flow_paths[0]; - for (size_t i = 1; i < tal_count(flow_paths); i++) { - if (amount_msat_greater(flow_paths[i]->delivers, best->delivers)) - best = flow_paths[i]; - } - for (size_t i = 0; i < tal_count(flow_paths); i++) { - if (flow_paths[i] == best) - continue; - if (!amount_msat_accumulate(&best->delivers, - flow_paths[i]->delivers)) { - rq_log(tmpctx, rq, LOG_BROKEN, - "%s: failed to extract accumulate flow paths %s+%s", - __func__, - fmt_amount_msat(tmpctx, best->delivers), - fmt_amount_msat(tmpctx, flow_paths[i]->delivers)); - goto fail; - } - } - flow_paths[0] = best; - tal_resize(&flow_paths, 1); - } return flow_paths; fail: @@ -1268,13 +1242,16 @@ struct flow **single_path_flow(const tal_t *ctx, const struct route_query *rq, return NULL; } -const char *default_routes(const tal_t *ctx, struct route_query *rq, - const struct gossmap_node *srcnode, - const struct gossmap_node *dstnode, - struct amount_msat amount, bool single_path, - struct amount_msat maxfee, u32 finalcltv, - u32 maxdelay, struct flow ***flows, - double *probability) +static const char * +linear_routes(const tal_t *ctx, struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, struct amount_msat amount, + struct amount_msat maxfee, u32 finalcltv, u32 maxdelay, + struct flow ***flows, double *probability, + struct flow **(*solver)(const tal_t *, const struct route_query *, + const struct gossmap_node *, + const struct gossmap_node *, + struct amount_msat, u32, double)) { *flows = NULL; const char *ret; @@ -1283,8 +1260,7 @@ const char *default_routes(const tal_t *ctx, struct route_query *rq, /* First up, don't care about fees (well, just enough to tiebreak!) */ u32 mu = 1; tal_free(*flows); - *flows = minflow(ctx, rq, srcnode, dstnode, amount, mu, delay_feefactor, - single_path); + *flows = solver(ctx, rq, srcnode, dstnode, amount, mu, delay_feefactor); if (!*flows) { ret = explain_failure(ctx, rq, srcnode, dstnode, amount); goto fail; @@ -1299,8 +1275,8 @@ const char *default_routes(const tal_t *ctx, struct route_query *rq, flows_worst_delay(*flows), maxdelay - finalcltv, delay_feefactor); tal_free(*flows); - *flows = minflow(ctx, rq, srcnode, dstnode, amount, mu, - delay_feefactor, single_path); + *flows = solver(ctx, rq, srcnode, dstnode, amount, mu, + delay_feefactor); if (!*flows || delay_feefactor > 10) { ret = rq_log( ctx, rq, LOG_UNUSUAL, @@ -1323,9 +1299,8 @@ const char *default_routes(const tal_t *ctx, struct route_query *rq, "retrying with mu of %u%%...", fmt_amount_msat(tmpctx, flowset_fee(rq->plugin, *flows)), fmt_amount_msat(tmpctx, maxfee), mu); - new_flows = - minflow(ctx, rq, srcnode, dstnode, amount, - mu > 100 ? 100 : mu, delay_feefactor, single_path); + new_flows = solver(ctx, rq, srcnode, dstnode, amount, + mu > 100 ? 100 : mu, delay_feefactor); if (!*flows || mu >= 100) { ret = rq_log( ctx, rq, LOG_UNUSUAL, @@ -1406,3 +1381,27 @@ const char *default_routes(const tal_t *ctx, struct route_query *rq, assert(ret != NULL); return ret; } + +const char *default_routes(const tal_t *ctx, struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, + struct amount_msat amount, struct amount_msat maxfee, + u32 finalcltv, u32 maxdelay, struct flow ***flows, + double *probability) +{ + return linear_routes(ctx, rq, srcnode, dstnode, amount, maxfee, + finalcltv, maxdelay, flows, probability, minflow); +} + +const char *single_path_routes(const tal_t *ctx, struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, + struct amount_msat amount, + struct amount_msat maxfee, u32 finalcltv, + u32 maxdelay, struct flow ***flows, + double *probability) +{ + return linear_routes(ctx, rq, srcnode, dstnode, amount, maxfee, + finalcltv, maxdelay, flows, probability, + single_path_flow); +} diff --git a/plugins/askrene/mcf.h b/plugins/askrene/mcf.h index 328b7445dc14..448aee27a40c 100644 --- a/plugins/askrene/mcf.h +++ b/plugins/askrene/mcf.h @@ -31,8 +31,7 @@ struct flow **minflow(const tal_t *ctx, const struct gossmap_node *target, struct amount_msat amount, u32 mu, - double delay_feefactor, - bool single_part); + double delay_feefactor); /** * API for min cost single path. @@ -67,9 +66,18 @@ struct amount_msat linear_flow_cost(const struct flow *flow, const char *default_routes(const tal_t *ctx, struct route_query *rq, const struct gossmap_node *srcnode, const struct gossmap_node *dstnode, - struct amount_msat amount, bool single_path, + struct amount_msat amount, struct amount_msat maxfee, u32 finalcltv, u32 maxdelay, struct flow ***flows, double *probability); +/* A wrapper to the single-path constrained solver. */ +const char *single_path_routes(const tal_t *ctx, struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, + struct amount_msat amount, + struct amount_msat maxfee, u32 finalcltv, + u32 maxdelay, struct flow ***flows, + double *probability); + #endif /* LIGHTNING_PLUGINS_ASKRENE_MCF_H */ diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 8944fc262001..c566e6fa9de9 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -568,25 +568,88 @@ def test_getroutes(node_factory): 'amount_msat': 5500005, 'delay': 99 + 6}]]) - # We realize that this is impossible in a single path: - with pytest.raises(RpcError, match="The shortest path is 0x2x1, but 0x2x1/1 marked disabled by layer auto.no_mpp_support."): - l1.rpc.getroutes(source=nodemap[0], - destination=nodemap[2], - amount_msat=10000000, - layers=['auto.no_mpp_support'], - maxfee_msat=1000, - final_cltv=99) - # But this will work. - check_getroute_paths(l1, - nodemap[0], - nodemap[2], - 9000000, - [[{'short_channel_id_dir': '0x2x3/1', - 'next_node_id': nodemap[2], - 'amount_msat': 9000009, - 'delay': 99 + 6}]], - layers=['auto.no_mpp_support']) +def test_getroutes_single_path(node_factory): + """Test getroutes generating single path payments""" + gsfile, nodemap = generate_gossip_store( + [ + GenChannel(0, 1), + GenChannel(1, 2, capacity_sats=9000), + GenChannel(1, 2, capacity_sats=10000), + ] + ) + # Set up l1 with this as the gossip_store + l1 = node_factory.get_node(gossip_store_file=gsfile.name) + + # To be able to route this amount two parts are needed, therefore a single + # pay search will fail. + # FIXME: the explanation for the failure is wrong + with pytest.raises(RpcError): + l1.rpc.getroutes( + source=nodemap[1], + destination=nodemap[2], + amount_msat=10000001, + layers=["auto.no_mpp_support"], + maxfee_msat=1000, + final_cltv=99, + ) + + # For this amount, only one solution is possible + check_getroute_paths( + l1, + nodemap[1], + nodemap[2], + 10000000, + [ + [ + { + "short_channel_id_dir": "1x2x2/1", + "next_node_id": nodemap[2], + "amount_msat": 10000010, + "delay": 99 + 6, + } + ] + ], + layers=["auto.no_mpp_support"], + ) + + # To be able to route this amount two parts are needed, therefore a single + # pay search will fail. + # FIXME: the explanation for the failure is wrong + with pytest.raises(RpcError): + l1.rpc.getroutes( + source=nodemap[0], + destination=nodemap[2], + amount_msat=10000001, + layers=["auto.no_mpp_support"], + maxfee_msat=1000, + final_cltv=99, + ) + + # For this amount, only one solution is possible + check_getroute_paths( + l1, + nodemap[0], + nodemap[2], + 10000000, + [ + [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 10000020, + "delay": 99 + 6 + 6, + }, + { + "short_channel_id_dir": "1x2x2/1", + "next_node_id": nodemap[2], + "amount_msat": 10000010, + "delay": 99 + 6, + }, + ] + ], + layers=["auto.no_mpp_support"], + ) def test_getroutes_fee_fallback(node_factory): From b0fa17c46022afbfe7c5708cf62e13db5014f470 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Thu, 29 May 2025 08:30:17 +0100 Subject: [PATCH 4/4] askrene: update the docs on auto.no_mpp_support Changelog-None Signed-off-by: Lagrang3 --- contrib/msggen/msggen/schema.json | 2 +- doc/schemas/getroutes.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 36851d45e04b..1ffc7fafbf92 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -15045,7 +15045,7 @@ "", "Layers are generally maintained by plugins, either to contain persistent information about capacities which have been discovered, or to contain transient information for this particular payment (such as blinded paths or routehints).", "", - "There are three automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities. *auto.sourcefree* overrides all channels (including those from previous layers) leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node. And *auto.no_mpp_support* forces getroutes to return a single flow, though only basic checks are done that the result is useful." + "There are three automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities. *auto.sourcefree* overrides all channels (including those from previous layers) leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node. And *auto.no_mpp_support* forces getroutes to return a single path solution which is useful for payments for which MPP is not supported." ], "categories": [ "readonly" diff --git a/doc/schemas/getroutes.json b/doc/schemas/getroutes.json index cfa2ac3a3b90..942ef99de0c6 100644 --- a/doc/schemas/getroutes.json +++ b/doc/schemas/getroutes.json @@ -11,7 +11,7 @@ "", "Layers are generally maintained by plugins, either to contain persistent information about capacities which have been discovered, or to contain transient information for this particular payment (such as blinded paths or routehints).", "", - "There are three automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities. *auto.sourcefree* overrides all channels (including those from previous layers) leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node. And *auto.no_mpp_support* forces getroutes to return a single flow, though only basic checks are done that the result is useful." + "There are three automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities. *auto.sourcefree* overrides all channels (including those from previous layers) leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node. And *auto.no_mpp_support* forces getroutes to return a single path solution which is useful for payments for which MPP is not supported." ], "categories": [ "readonly"