From 080fa873c8e12bcf9b9826e35681ba7db39e1909 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Thu, 10 Jul 2025 23:38:06 +0930 Subject: [PATCH 1/8] askrene: refactor get_routes ... Move the feature tuning algorithm to mcf.c, ie. the loops for searching a good mu and delay_feefactor to satisfy the problem constraints. We are looking to set the stage for an execution logic that allows for multiple choices of routing algorithms, mainly for experimenting without breaking the default working code. Changelog-None Signed-off-by: Lagrang3 --- plugins/askrene/askrene.c | 128 ++---------------------------- plugins/askrene/mcf.c | 158 ++++++++++++++++++++++++++++++++++++++ plugins/askrene/mcf.h | 10 +++ 3 files changed, 175 insertions(+), 121 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index a5ea464de1e8..cbc2657cc641 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -19,11 +19,9 @@ #include #include #include -#include #include #include #include -#include #include /* "spendable" for a channel assumes a single HTLC: for additional HTLCs, @@ -333,22 +331,6 @@ const char *fmt_flow_full(const tal_t *ctx, return str; } -static struct amount_msat linear_flows_cost(struct flow **flows, - struct amount_msat total_amount, - double delay_feefactor) -{ - struct amount_msat total = AMOUNT_MSAT(0); - - for (size_t i = 0; i < tal_count(flows); i++) { - if (!amount_msat_accumulate(&total, - linear_flow_cost(flows[i], - total_amount, - delay_feefactor))) - abort(); - } - return total; -} - /* Returns an error message, or sets *routes */ static const char *get_routes(const tal_t *ctx, struct command *cmd, @@ -371,8 +353,6 @@ static const char *get_routes(const tal_t *ctx, struct route_query *rq = tal(ctx, struct route_query); struct flow **flows; const struct gossmap_node *srcnode, *dstnode; - double delay_feefactor; - u32 mu; const char *ret; struct timerel time_delta; struct timemono time_start = time_mono(); @@ -446,109 +426,15 @@ static const char *get_routes(const tal_t *ctx, goto fail; } - delay_feefactor = 1.0/1000000; - - /* First up, don't care about fees (well, just enough to tiebreak!) */ - mu = 1; - flows = minflow(rq, rq, srcnode, dstnode, amount, - mu, delay_feefactor, single_path); - if (!flows) { - ret = explain_failure(ctx, rq, srcnode, dstnode, amount); - goto fail; - } - - /* Too much delay? */ - while (finalcltv + flows_worst_delay(flows) > maxdelay) { - delay_feefactor *= 2; - rq_log(tmpctx, rq, LOG_UNUSUAL, - "The worst flow delay is %"PRIu64" (> %i), retrying with delay_feefactor %f...", - flows_worst_delay(flows), maxdelay - finalcltv, delay_feefactor); - flows = minflow(rq, rq, srcnode, dstnode, amount, - mu, delay_feefactor, single_path); - if (!flows || delay_feefactor > 10) { - ret = rq_log(ctx, rq, LOG_UNUSUAL, - "Could not find route without excessive delays"); - goto fail; - } - } - - /* Too expensive? */ -too_expensive: - while (amount_msat_greater(flowset_fee(rq->plugin, flows), maxfee)) { - struct flow **new_flows; - - if (mu == 1) - mu = 10; - else - mu += 10; - rq_log(tmpctx, rq, LOG_UNUSUAL, - "The flows had a fee of %s, greater than max of %s, retrying with mu of %u%%...", - fmt_amount_msat(tmpctx, flowset_fee(rq->plugin, flows)), - fmt_amount_msat(tmpctx, maxfee), - mu); - new_flows = minflow(rq, rq, srcnode, dstnode, amount, - mu > 100 ? 100 : mu, delay_feefactor, single_path); - if (!flows || mu >= 100) { - ret = rq_log(ctx, rq, LOG_UNUSUAL, - "Could not find route without excessive cost"); - goto fail; - } - - /* This is possible, because MCF's linear fees are not the same. */ - if (amount_msat_greater(flowset_fee(rq->plugin, new_flows), - flowset_fee(rq->plugin, flows))) { - struct amount_msat old_cost = linear_flows_cost(flows, amount, delay_feefactor); - struct amount_msat new_cost = linear_flows_cost(new_flows, amount, delay_feefactor); - if (amount_msat_greater_eq(new_cost, old_cost)) { - rq_log(tmpctx, rq, LOG_BROKEN, "Old flows cost %s:", - fmt_amount_msat(tmpctx, old_cost)); - for (size_t i = 0; i < tal_count(flows); i++) { - rq_log(tmpctx, rq, LOG_BROKEN, - "Flow %zu/%zu: %s (linear cost %s)", i, tal_count(flows), - fmt_flow_full(tmpctx, rq, flows[i]), - fmt_amount_msat(tmpctx, linear_flow_cost(flows[i], - amount, - delay_feefactor))); - } - rq_log(tmpctx, rq, LOG_BROKEN, "Old flows cost %s:", - fmt_amount_msat(tmpctx, new_cost)); - for (size_t i = 0; i < tal_count(new_flows); i++) { - rq_log(tmpctx, rq, LOG_BROKEN, - "Flow %zu/%zu: %s (linear cost %s)", i, tal_count(new_flows), - fmt_flow_full(tmpctx, rq, new_flows[i]), - fmt_amount_msat(tmpctx, linear_flow_cost(new_flows[i], - amount, - delay_feefactor))); - } - } - } - tal_free(flows); - flows = new_flows; - } - - if (finalcltv + flows_worst_delay(flows) > maxdelay) { - ret = rq_log(ctx, rq, LOG_UNUSUAL, - "Could not find route without excessive cost or delays"); - goto fail; - } - - /* The above did not take into account the extra funds to pay - * fees, so we try to adjust now. We could re-run MCF if this - * fails, but failure basically never happens where payment is - * still possible */ - ret = refine_with_fees_and_limits(ctx, rq, amount, &flows, probability); - if (ret) + /* FIXME: single_path should signal a change in algorithm. */ + ret = default_routes(rq, rq, srcnode, dstnode, amount, single_path, + maxfee, finalcltv, maxdelay, &flows, probability); + if (ret) { goto fail; - - /* Again, a tiny corner case: refine step can make us exceed maxfee */ - if (amount_msat_greater(flowset_fee(rq->plugin, flows), maxfee)) { - rq_log(tmpctx, rq, LOG_UNUSUAL, - "After final refinement, fee was excessive: retrying"); - goto too_expensive; } - - rq_log(tmpctx, rq, LOG_DBG, "Final answer has %zu flows with mu=%u", - tal_count(flows), mu); + assert(tal_count(flows) > 0); + rq_log(tmpctx, rq, LOG_DBG, "Final answer has %zu flows", + tal_count(flows)); /* Convert back into routes, with delay and other information fixed */ *routes = tal_arr(ctx, struct route *, tal_count(flows)); diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 6339510a5ce9..cb2b1ea83075 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -11,9 +11,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -1080,3 +1082,159 @@ struct flow **minflow(const tal_t *ctx, tal_free(working_ctx); return NULL; } + +static struct amount_msat linear_flows_cost(struct flow **flows, + struct amount_msat total_amount, + double delay_feefactor) +{ + struct amount_msat total = AMOUNT_MSAT(0); + + for (size_t i = 0; i < tal_count(flows); i++) { + if (!amount_msat_accumulate(&total, + linear_flow_cost(flows[i], + total_amount, + delay_feefactor))) + abort(); + } + return total; +} + + +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) +{ + *flows = NULL; + const char *ret; + double delay_feefactor = 1.0 / 1000000; + + /* 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); + if (!*flows) { + ret = explain_failure(ctx, rq, srcnode, dstnode, amount); + goto fail; + } + + /* Too much delay? */ + while (finalcltv + flows_worst_delay(*flows) > maxdelay) { + delay_feefactor *= 2; + rq_log(tmpctx, rq, LOG_UNUSUAL, + "The worst flow delay is %" PRIu64 + " (> %i), retrying with delay_feefactor %f...", + flows_worst_delay(*flows), maxdelay - finalcltv, + delay_feefactor); + tal_free(*flows); + *flows = minflow(ctx, rq, srcnode, dstnode, amount, mu, + delay_feefactor, single_path); + if (!*flows || delay_feefactor > 10) { + ret = rq_log( + ctx, rq, LOG_UNUSUAL, + "Could not find route without excessive delays"); + goto fail; + } + } + + /* Too expensive? */ +too_expensive: + while (amount_msat_greater(flowset_fee(rq->plugin, *flows), maxfee)) { + struct flow **new_flows; + + if (mu == 1) + mu = 10; + else + mu += 10; + rq_log(tmpctx, rq, LOG_UNUSUAL, + "The flows had a fee of %s, greater than max of %s, " + "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); + if (!*flows || mu >= 100) { + ret = rq_log( + ctx, rq, LOG_UNUSUAL, + "Could not find route without excessive cost"); + goto fail; + } + + /* This is possible, because MCF's linear fees are not the same. + */ + if (amount_msat_greater(flowset_fee(rq->plugin, new_flows), + flowset_fee(rq->plugin, *flows))) { + struct amount_msat old_cost = + linear_flows_cost(*flows, amount, delay_feefactor); + struct amount_msat new_cost = linear_flows_cost( + new_flows, amount, delay_feefactor); + if (amount_msat_greater_eq(new_cost, old_cost)) { + rq_log(tmpctx, rq, LOG_BROKEN, + "Old flows cost %s:", + fmt_amount_msat(tmpctx, old_cost)); + for (size_t i = 0; i < tal_count(*flows); i++) { + rq_log( + tmpctx, rq, LOG_BROKEN, + "Flow %zu/%zu: %s (linear cost %s)", + i, tal_count(*flows), + fmt_flow_full(tmpctx, rq, (*flows)[i]), + fmt_amount_msat( + tmpctx, linear_flow_cost( + (*flows)[i], amount, + delay_feefactor))); + } + rq_log(tmpctx, rq, LOG_BROKEN, + "Old flows cost %s:", + fmt_amount_msat(tmpctx, new_cost)); + for (size_t i = 0; i < tal_count(new_flows); + i++) { + rq_log( + tmpctx, rq, LOG_BROKEN, + "Flow %zu/%zu: %s (linear cost %s)", + i, tal_count(new_flows), + fmt_flow_full(tmpctx, rq, + new_flows[i]), + fmt_amount_msat( + tmpctx, + linear_flow_cost( + new_flows[i], amount, + delay_feefactor))); + } + } + } + tal_free(*flows); + *flows = new_flows; + } + + if (finalcltv + flows_worst_delay(*flows) > maxdelay) { + ret = rq_log( + ctx, rq, LOG_UNUSUAL, + "Could not find route without excessive cost or delays"); + goto fail; + } + + /* The above did not take into account the extra funds to pay + * fees, so we try to adjust now. We could re-run MCF if this + * fails, but failure basically never happens where payment is + * still possible */ + ret = refine_with_fees_and_limits(ctx, rq, amount, flows, probability); + if (ret) + goto fail; + + /* Again, a tiny corner case: refine step can make us exceed maxfee */ + if (amount_msat_greater(flowset_fee(rq->plugin, *flows), maxfee)) { + rq_log(tmpctx, rq, LOG_UNUSUAL, + "After final refinement, fee was excessive: retrying"); + goto too_expensive; + } + + return NULL; +fail: + assert(ret != NULL); + return ret; +} diff --git a/plugins/askrene/mcf.h b/plugins/askrene/mcf.h index f8100e766dd6..701a1ac5bf75 100644 --- a/plugins/askrene/mcf.h +++ b/plugins/askrene/mcf.h @@ -40,4 +40,14 @@ struct amount_msat linear_flow_cost(const struct flow *flow, struct amount_msat total_amount, double delay_feefactor); +/* A wrapper to the min. cost flow solver that actually takes into consideration + * the extra msats per channel needed to pay for fees. */ +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); + #endif /* LIGHTNING_PLUGINS_ASKRENE_MCF_H */ From 663dcbe14222fa99ca2f49e4db3dbd7f58438a42 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 10 Jul 2025 23:39:06 +0930 Subject: [PATCH 2/8] askrene: houst struct getroutes_info higher to clarify following patches. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index cbc2657cc641..d47bf132433f 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -331,6 +331,17 @@ const char *fmt_flow_full(const tal_t *ctx, return str; } +struct getroutes_info { + struct command *cmd; + struct node_id *source, *dest; + struct amount_msat *amount, *maxfee; + u32 *finalcltv, *maxdelay; + const char **layers; + struct additional_cost_htable *additional_costs; + /* Non-NULL if we are told to use "auto.localchans" */ + struct layer *local_layer; +}; + /* Returns an error message, or sets *routes */ static const char *get_routes(const tal_t *ctx, struct command *cmd, @@ -527,17 +538,6 @@ void get_constraints(const struct route_query *rq, reserve_sub(rq->reserved, &scidd, max); } -struct getroutes_info { - struct command *cmd; - struct node_id *source, *dest; - struct amount_msat *amount, *maxfee; - u32 *finalcltv, *maxdelay; - const char **layers; - struct additional_cost_htable *additional_costs; - /* Non-NULL if we are told to use "auto.localchans" */ - struct layer *local_layer; -}; - static struct command_result *do_getroutes(struct command *cmd, struct gossmap_localmods *localmods, const struct getroutes_info *info) From f8ba1a15804385d24be6be548536acc14cc4764b Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 10 Jul 2025 23:40:06 +0930 Subject: [PATCH 3/8] plugins/askrene: extract apply_layers into its own function. Simple refactoring. Stolen entirely from Eduardo. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 59 ++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index d47bf132433f..c9861d98feb3 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -342,6 +342,39 @@ struct getroutes_info { struct layer *local_layer; }; +static void apply_layers(struct askrene *askrene, struct route_query *rq, + const struct node_id *source, + struct amount_msat amount, + struct gossmap_localmods *localmods, + const char **layers, + const struct layer *local_layer) +{ + /* Layers must exist, but might be special ones! */ + for (size_t i = 0; i < tal_count(layers); i++) { + const struct layer *l = find_layer(askrene, layers[i]); + if (!l) { + if (streq(layers[i], "auto.localchans")) { + plugin_log(rq->plugin, LOG_DBG, "Adding auto.localchans"); + l = local_layer; + } else if (streq(layers[i], "auto.no_mpp_support")) { + plugin_log(rq->plugin, LOG_DBG, "Adding auto.no_mpp_support, sorry"); + l = remove_small_channel_layer(layers, askrene, amount, localmods); + } else { + assert(streq(layers[i], "auto.sourcefree")); + plugin_log(rq->plugin, LOG_DBG, "Adding auto.sourcefree"); + l = source_free_layer(layers, askrene, source, localmods); + } + } + + tal_arr_expand(&rq->layers, l); + /* FIXME: Implement localmods_merge, and cache this in layer? */ + layer_add_localmods(l, rq->gossmap, localmods); + + /* Clear any entries in capacities array if we + * override them (incl local channels) */ + layer_clear_overridden_capacities(l, askrene->gossmap, rq->capacities); + } +} /* Returns an error message, or sets *routes */ static const char *get_routes(const tal_t *ctx, struct command *cmd, @@ -382,31 +415,7 @@ static const char *get_routes(const tal_t *ctx, rq->capacities = tal_dup_talarr(rq, fp16_t, askrene->capacities); rq->additional_costs = additional_costs; - /* Layers must exist, but might be special ones! */ - for (size_t i = 0; i < tal_count(layers); i++) { - const struct layer *l = find_layer(askrene, layers[i]); - if (!l) { - if (streq(layers[i], "auto.localchans")) { - plugin_log(rq->plugin, LOG_DBG, "Adding auto.localchans"); - l = local_layer; - } else if (streq(layers[i], "auto.no_mpp_support")) { - plugin_log(rq->plugin, LOG_DBG, "Adding auto.no_mpp_support, sorry"); - l = remove_small_channel_layer(layers, askrene, amount, localmods); - } else { - assert(streq(layers[i], "auto.sourcefree")); - plugin_log(rq->plugin, LOG_DBG, "Adding auto.sourcefree"); - l = source_free_layer(layers, askrene, source, localmods); - } - } - - tal_arr_expand(&rq->layers, l); - /* FIXME: Implement localmods_merge, and cache this in layer? */ - layer_add_localmods(l, rq->gossmap, localmods); - - /* Clear any entries in capacities array if we - * override them (incl local channels) */ - layer_clear_overridden_capacities(l, askrene->gossmap, rq->capacities); - } + apply_layers(askrene, rq, source, amount, localmods, layers, local_layer); /* Clear scids with reservations, too, so we don't have to look up * all the time! */ From 4092a9dd57e40cdce08d211192d74b01f8526cab Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 10 Jul 2025 23:41:06 +0930 Subject: [PATCH 4/8] askrene: extracet convert_flows_to_routes into its own function. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 92 +++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index c9861d98feb3..0bf023fb80f8 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -375,6 +375,56 @@ static void apply_layers(struct askrene *askrene, struct route_query *rq, layer_clear_overridden_capacities(l, askrene->gossmap, rq->capacities); } } + +/* Convert back into routes, with delay and other information fixed */ +static struct route **convert_flows_to_routes(const tal_t *ctx, + struct route_query *rq, + u32 finalcltv, + struct flow **flows, + struct amount_msat **amounts) +{ + struct route **routes; + routes = tal_arr(ctx, struct route *, tal_count(flows)); + *amounts = tal_arr(ctx, struct amount_msat, tal_count(flows)); + + for (size_t i = 0; i < tal_count(flows); i++) { + struct route *r; + struct amount_msat msat; + u32 delay; + + routes[i] = r = tal(routes, struct route); + r->success_prob = flow_probability(flows[i], rq); + r->hops = tal_arr(r, struct route_hop, tal_count(flows[i]->path)); + + /* Fill in backwards to calc amount and delay */ + msat = flows[i]->delivers; + delay = finalcltv; + + for (int j = tal_count(flows[i]->path) - 1; j >= 0; j--) { + struct route_hop *rh = &r->hops[j]; + struct gossmap_node *far_end; + const struct half_chan *h = flow_edge(flows[i], j); + + if (!amount_msat_add_fee(&msat, h->base_fee, h->proportional_fee)) + plugin_err(rq->plugin, "Adding fee to amount"); + delay += h->delay; + + rh->scid = gossmap_chan_scid(rq->gossmap, flows[i]->path[j]); + rh->direction = flows[i]->dirs[j]; + far_end = gossmap_nth_node(rq->gossmap, flows[i]->path[j], !flows[i]->dirs[j]); + gossmap_node_get_id(rq->gossmap, far_end, &rh->node_id); + rh->amount = msat; + rh->delay = delay; + } + (*amounts)[i] = flows[i]->delivers; + rq_log(tmpctx, rq, LOG_INFORM, "Flow %zu/%zu: %s", + i, tal_count(flows), + fmt_route(tmpctx, r, (*amounts)[i], finalcltv)); + } + + return routes; +} + /* Returns an error message, or sets *routes */ static const char *get_routes(const tal_t *ctx, struct command *cmd, @@ -456,44 +506,12 @@ static const char *get_routes(const tal_t *ctx, rq_log(tmpctx, rq, LOG_DBG, "Final answer has %zu flows", tal_count(flows)); - /* Convert back into routes, with delay and other information fixed */ - *routes = tal_arr(ctx, struct route *, tal_count(flows)); - *amounts = tal_arr(ctx, struct amount_msat, tal_count(flows)); - for (size_t i = 0; i < tal_count(flows); i++) { - struct route *r; - struct amount_msat msat; - u32 delay; - - (*routes)[i] = r = tal(*routes, struct route); - r->success_prob = flow_probability(flows[i], rq); - r->hops = tal_arr(r, struct route_hop, tal_count(flows[i]->path)); - - /* Fill in backwards to calc amount and delay */ - msat = flows[i]->delivers; - delay = finalcltv; - - for (int j = tal_count(flows[i]->path) - 1; j >= 0; j--) { - struct route_hop *rh = &r->hops[j]; - struct gossmap_node *far_end; - const struct half_chan *h = flow_edge(flows[i], j); - - if (!amount_msat_add_fee(&msat, h->base_fee, h->proportional_fee)) - plugin_err(rq->plugin, "Adding fee to amount"); - delay += h->delay; - - rh->scid = gossmap_chan_scid(rq->gossmap, flows[i]->path[j]); - rh->direction = flows[i]->dirs[j]; - far_end = gossmap_nth_node(rq->gossmap, flows[i]->path[j], !flows[i]->dirs[j]); - gossmap_node_get_id(rq->gossmap, far_end, &rh->node_id); - rh->amount = msat; - rh->delay = delay; - } - (*amounts)[i] = flows[i]->delivers; - rq_log(tmpctx, rq, LOG_INFORM, "Flow %zu/%zu: %s", - i, tal_count(flows), - fmt_route(tmpctx, r, (*amounts)[i], finalcltv)); - } + /* convert flows to routes */ + *routes = convert_flows_to_routes(rq, rq, finalcltv, flows, amounts); + assert(tal_count(*routes) == tal_count(flows)); + assert(tal_count(*amounts) == tal_count(flows)); + /* At last we remove the localmods from the gossmap. */ gossmap_remove_localmods(askrene->gossmap, localmods); time_delta = timemono_between(time_mono(), time_start); rq_log(tmpctx, rq, LOG_DBG, "get_routes completed in %" PRIu64 " ms", From 31cdd57071902324c2be9fbe93cb63c79f9f20ef Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 10 Jul 2025 23:42:06 +0930 Subject: [PATCH 5/8] askrene: extract getroutes json formatting function. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 60 +++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 0bf023fb80f8..960ad7cbfe2f 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -425,6 +425,40 @@ static struct route **convert_flows_to_routes(const tal_t *ctx, return routes; } +static void json_add_getroutes(struct json_stream *js, + struct route **routes, + const struct amount_msat *amounts, + double probability, + u32 final_cltv) +{ + json_add_u64(js, "probability_ppm", (u64)(probability * 1000000)); + json_array_start(js, "routes"); + for (size_t i = 0; i < tal_count(routes); i++) { + json_object_start(js, NULL); + json_add_u64(js, "probability_ppm", + (u64)(routes[i]->success_prob * 1000000)); + json_add_amount_msat(js, "amount_msat", amounts[i]); + json_add_u32(js, "final_cltv", final_cltv); + json_array_start(js, "path"); + for (size_t j = 0; j < tal_count(routes[i]->hops); j++) { + struct short_channel_id_dir scidd; + const struct route_hop *r = &routes[i]->hops[j]; + json_object_start(js, NULL); + scidd.scid = r->scid; + scidd.dir = r->direction; + json_add_short_channel_id_dir( + js, "short_channel_id_dir", scidd); + json_add_node_id(js, "next_node_id", &r->node_id); + json_add_amount_msat(js, "amount_msat", r->amount); + json_add_u32(js, "delay", r->delay); + json_object_end(js); + } + json_array_end(js); + json_object_end(js); + } + json_array_end(js); +} + /* Returns an error message, or sets *routes */ static const char *get_routes(const tal_t *ctx, struct command *cmd, @@ -585,30 +619,8 @@ static struct command_result *do_getroutes(struct command *cmd, return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err); response = jsonrpc_stream_success(cmd); - json_add_u64(response, "probability_ppm", (u64)(probability * 1000000)); - json_array_start(response, "routes"); - for (size_t i = 0; i < tal_count(routes); i++) { - json_object_start(response, NULL); - json_add_u64(response, "probability_ppm", (u64)(routes[i]->success_prob * 1000000)); - json_add_amount_msat(response, "amount_msat", amounts[i]); - json_add_u32(response, "final_cltv", *info->finalcltv); - json_array_start(response, "path"); - for (size_t j = 0; j < tal_count(routes[i]->hops); j++) { - struct short_channel_id_dir scidd; - const struct route_hop *r = &routes[i]->hops[j]; - json_object_start(response, NULL); - scidd.scid = r->scid; - scidd.dir = r->direction; - json_add_short_channel_id_dir(response, "short_channel_id_dir", scidd); - json_add_node_id(response, "next_node_id", &r->node_id); - json_add_amount_msat(response, "amount_msat", r->amount); - json_add_u32(response, "delay", r->delay); - json_object_end(response); - } - json_array_end(response); - json_object_end(response); - } - json_array_end(response); + json_add_getroutes(response, routes, amounts, probability, + *info->finalcltv); return command_finished(cmd, response); } From 324eb568e6ef95f8805560d27b9724accd0fc8b3 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 10 Jul 2025 23:43:06 +0930 Subject: [PATCH 6/8] askrene: move get_routes into do_getroutes. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 209 ++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 112 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 960ad7cbfe2f..03c2d21f4ca1 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -459,110 +459,6 @@ static void json_add_getroutes(struct json_stream *js, json_array_end(js); } -/* Returns an error message, or sets *routes */ -static const char *get_routes(const tal_t *ctx, - struct command *cmd, - const struct node_id *source, - const struct node_id *dest, - struct amount_msat amount, - struct amount_msat maxfee, - u32 finalcltv, - u32 maxdelay, - const char **layers, - struct gossmap_localmods *localmods, - const struct layer *local_layer, - bool single_path, - struct route ***routes, - struct amount_msat **amounts, - const struct additional_cost_htable *additional_costs, - double *probability) -{ - struct askrene *askrene = get_askrene(cmd->plugin); - struct route_query *rq = tal(ctx, struct route_query); - struct flow **flows; - const struct gossmap_node *srcnode, *dstnode; - const char *ret; - struct timerel time_delta; - struct timemono time_start = time_mono(); - - if (gossmap_refresh(askrene->gossmap)) { - /* FIXME: gossmap_refresh callbacks to we can update in place */ - tal_free(askrene->capacities); - askrene->capacities = get_capacities(askrene, askrene->plugin, askrene->gossmap); - } - - rq->cmd = cmd; - rq->plugin = cmd->plugin; - rq->gossmap = askrene->gossmap; - rq->reserved = askrene->reserved; - rq->layers = tal_arr(rq, const struct layer *, 0); - rq->capacities = tal_dup_talarr(rq, fp16_t, askrene->capacities); - rq->additional_costs = additional_costs; - - apply_layers(askrene, rq, source, amount, localmods, layers, local_layer); - - /* Clear scids with reservations, too, so we don't have to look up - * all the time! */ - reserves_clear_capacities(askrene->reserved, askrene->gossmap, rq->capacities); - - gossmap_apply_localmods(askrene->gossmap, localmods); - - /* localmods can add channels, so we need to allocate biases array *afterwards* */ - rq->biases = tal_arrz(rq, s8, gossmap_max_chan_idx(askrene->gossmap) * 2); - - /* Note any channel biases */ - for (size_t i = 0; i < tal_count(rq->layers); i++) - layer_apply_biases(rq->layers[i], askrene->gossmap, rq->biases); - - srcnode = gossmap_find_node(askrene->gossmap, source); - if (!srcnode) { - ret = rq_log(ctx, rq, LOG_INFORM, - "Unknown source node %s", - fmt_node_id(tmpctx, source)); - goto fail; - } - - dstnode = gossmap_find_node(askrene->gossmap, dest); - if (!dstnode) { - ret = rq_log(ctx, rq, LOG_INFORM, - "Unknown destination node %s", - fmt_node_id(tmpctx, dest)); - goto fail; - } - - /* FIXME: single_path should signal a change in algorithm. */ - ret = default_routes(rq, rq, srcnode, dstnode, amount, single_path, - maxfee, finalcltv, maxdelay, &flows, probability); - if (ret) { - goto fail; - } - assert(tal_count(flows) > 0); - rq_log(tmpctx, rq, LOG_DBG, "Final answer has %zu flows", - tal_count(flows)); - - /* convert flows to routes */ - *routes = convert_flows_to_routes(rq, rq, finalcltv, flows, amounts); - assert(tal_count(*routes) == tal_count(flows)); - assert(tal_count(*amounts) == tal_count(flows)); - - /* At last we remove the localmods from the gossmap. */ - gossmap_remove_localmods(askrene->gossmap, localmods); - time_delta = timemono_between(time_mono(), time_start); - rq_log(tmpctx, rq, LOG_DBG, "get_routes completed in %" PRIu64 " ms", - time_to_msec(time_delta)); - return NULL; - - /* Explicit failure path keeps the compiler (gcc version 12.3.0 -O3) from - * warning about uninitialized variables in the caller */ -fail: - assert(ret != NULL); - gossmap_remove_localmods(askrene->gossmap, localmods); - time_delta = timemono_between(time_mono(), time_start); - rq_log(tmpctx, rq, LOG_DBG, "get_routes failed after %" PRIu64 " ms", - time_to_msec(time_delta)); - return ret; -} - void get_constraints(const struct route_query *rq, const struct gossmap_chan *chan, int dir, @@ -603,26 +499,115 @@ static struct command_result *do_getroutes(struct command *cmd, struct gossmap_localmods *localmods, const struct getroutes_info *info) { + struct askrene *askrene = get_askrene(cmd->plugin); + struct route_query *rq = tal(cmd, struct route_query); const char *err; double probability; struct amount_msat *amounts; struct route **routes; + struct flow **flows; struct json_stream *response; - err = get_routes(cmd, cmd, - info->source, info->dest, - *info->amount, *info->maxfee, *info->finalcltv, - *info->maxdelay, info->layers, localmods, info->local_layer, - have_layer(info->layers, "auto.no_mpp_support"), - &routes, &amounts, info->additional_costs, &probability); + /* update the gossmap */ + if (gossmap_refresh(askrene->gossmap)) { + /* FIXME: gossmap_refresh callbacks to we can update in place */ + tal_free(askrene->capacities); + askrene->capacities = + get_capacities(askrene, askrene->plugin, askrene->gossmap); + } + + /* build this request structure */ + rq->cmd = cmd; + rq->plugin = cmd->plugin; + rq->gossmap = askrene->gossmap; + rq->reserved = askrene->reserved; + rq->layers = tal_arr(rq, const struct layer *, 0); + rq->capacities = tal_dup_talarr(rq, fp16_t, askrene->capacities); + /* FIXME: we still need to do something useful with these */ + rq->additional_costs = info->additional_costs; + + /* apply selected layers to the localmods */ + apply_layers(askrene, rq, info->source, *info->amount, localmods, + info->layers, info->local_layer); + + /* Clear scids with reservations, too, so we don't have to look up + * all the time! */ + reserves_clear_capacities(askrene->reserved, askrene->gossmap, + rq->capacities); + + /* we temporarily apply localmods */ + gossmap_apply_localmods(askrene->gossmap, localmods); + + /* localmods can add channels, so we need to allocate biases array + * *afterwards* */ + rq->biases = + tal_arrz(rq, s8, gossmap_max_chan_idx(askrene->gossmap) * 2); + + /* Note any channel biases */ + for (size_t i = 0; i < tal_count(rq->layers); i++) + layer_apply_biases(rq->layers[i], askrene->gossmap, rq->biases); + + /* checkout the source */ + const struct gossmap_node *srcnode = + gossmap_find_node(askrene->gossmap, info->source); + if (!srcnode) { + err = rq_log(tmpctx, rq, LOG_INFORM, "Unknown source node %s", + fmt_node_id(tmpctx, info->source)); + goto fail; + } + + /* checkout the destination */ + const struct gossmap_node *dstnode = + gossmap_find_node(askrene->gossmap, info->dest); + if (!dstnode) { + err = rq_log(tmpctx, rq, LOG_INFORM, + "Unknown destination node %s", + fmt_node_id(tmpctx, info->dest)); + goto fail; + } + + /* Compute the routes. At this point we might select between multiple + * algorithms. */ + struct timemono time_start = time_mono(); + 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); + struct timerel time_delta = timemono_between(time_mono(), time_start); + + /* log the time of computation */ + rq_log(tmpctx, rq, LOG_DBG, "get_routes %s %" PRIu64 " ms", + err ? "failed after" : "completed in", + time_to_msec(time_delta)); if (err) - return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err); + goto fail; + /* otherwise we continue */ + assert(tal_count(flows) > 0); + rq_log(tmpctx, rq, LOG_DBG, "Final answer has %zu flows", + tal_count(flows)); + + /* convert flows to routes */ + routes = convert_flows_to_routes(rq, rq, *info->finalcltv, flows, + &amounts); + assert(tal_count(routes) == tal_count(flows)); + assert(tal_count(amounts) == tal_count(flows)); + + /* At last we remove the localmods from the gossmap. */ + gossmap_remove_localmods(askrene->gossmap, localmods); + + /* output the results */ response = jsonrpc_stream_success(cmd); json_add_getroutes(response, routes, amounts, probability, *info->finalcltv); return command_finished(cmd, response); -} + +fail: + assert(err); + gossmap_remove_localmods(askrene->gossmap, localmods); + return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err); + } static void add_localchan(struct gossmap_localmods *mods, const struct node_id *self, From 85b4e6d4a56c7d0347b8aefe25696859828870d5 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 10 Jul 2025 23:44:06 +0930 Subject: [PATCH 7/8] askrene: fix up getroutes_info struct. It was originally a wrapper for JSON param() results, but now it's a more generic struct. So make it clear that the fields are not optional. This means a manual assignment in the initial population of this struct, but all the users are now far clearer. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 56 +++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 03c2d21f4ca1..3b53cf51e3fb 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -333,9 +333,9 @@ const char *fmt_flow_full(const tal_t *ctx, struct getroutes_info { struct command *cmd; - struct node_id *source, *dest; - struct amount_msat *amount, *maxfee; - u32 *finalcltv, *maxdelay; + struct node_id source, dest; + struct amount_msat amount, maxfee; + u32 finalcltv, maxdelay; const char **layers; struct additional_cost_htable *additional_costs; /* Non-NULL if we are told to use "auto.localchans" */ @@ -527,7 +527,7 @@ static struct command_result *do_getroutes(struct command *cmd, rq->additional_costs = info->additional_costs; /* apply selected layers to the localmods */ - apply_layers(askrene, rq, info->source, *info->amount, localmods, + apply_layers(askrene, rq, &info->source, info->amount, localmods, info->layers, info->local_layer); /* Clear scids with reservations, too, so we don't have to look up @@ -549,30 +549,30 @@ static struct command_result *do_getroutes(struct command *cmd, /* checkout the source */ const struct gossmap_node *srcnode = - gossmap_find_node(askrene->gossmap, info->source); + gossmap_find_node(askrene->gossmap, &info->source); if (!srcnode) { err = rq_log(tmpctx, rq, LOG_INFORM, "Unknown source node %s", - fmt_node_id(tmpctx, info->source)); + fmt_node_id(tmpctx, &info->source)); goto fail; } /* checkout the destination */ const struct gossmap_node *dstnode = - gossmap_find_node(askrene->gossmap, info->dest); + gossmap_find_node(askrene->gossmap, &info->dest); if (!dstnode) { err = rq_log(tmpctx, rq, LOG_INFORM, "Unknown destination node %s", - fmt_node_id(tmpctx, info->dest)); + fmt_node_id(tmpctx, &info->dest)); goto fail; } /* Compute the routes. At this point we might select between multiple * algorithms. */ struct timemono time_start = time_mono(); - err = default_routes(rq, rq, srcnode, dstnode, *info->amount, + 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, + info->maxfee, info->finalcltv, info->maxdelay, &flows, &probability); struct timerel time_delta = timemono_between(time_mono(), time_start); @@ -589,7 +589,7 @@ static struct command_result *do_getroutes(struct command *cmd, tal_count(flows)); /* convert flows to routes */ - routes = convert_flows_to_routes(rq, rq, *info->finalcltv, flows, + routes = convert_flows_to_routes(rq, rq, info->finalcltv, flows, &amounts); assert(tal_count(routes) == tal_count(flows)); assert(tal_count(amounts) == tal_count(flows)); @@ -600,7 +600,7 @@ static struct command_result *do_getroutes(struct command *cmd, /* output the results */ response = jsonrpc_stream_success(cmd); json_add_getroutes(response, routes, amounts, probability, - *info->finalcltv); + info->finalcltv); return command_finished(cmd, response); fail: @@ -718,36 +718,46 @@ static struct command_result *json_getroutes(struct command *cmd, /* FIXME: Typo in spec for CLTV in descripton! But it breaks our spelling check, so we omit it above */ const u32 maxdelay_allowed = 2016; struct getroutes_info *info = tal(cmd, struct getroutes_info); + /* param functions require pointers */ + struct node_id *source, *dest; + struct amount_msat *amount, *maxfee; + u32 *finalcltv, *maxdelay; if (!param_check(cmd, buffer, params, - p_req("source", param_node_id, &info->source), - p_req("destination", param_node_id, &info->dest), - p_req("amount_msat", param_msat, &info->amount), + p_req("source", param_node_id, &source), + p_req("destination", param_node_id, &dest), + p_req("amount_msat", param_msat, &amount), p_req("layers", param_layer_names, &info->layers), - p_req("maxfee_msat", param_msat, &info->maxfee), - p_req("final_cltv", param_u32, &info->finalcltv), - p_opt_def("maxdelay", param_u32, &info->maxdelay, + p_req("maxfee_msat", param_msat, &maxfee), + p_req("final_cltv", param_u32, &finalcltv), + p_opt_def("maxdelay", param_u32, &maxdelay, maxdelay_allowed), NULL)) return command_param_failed(); plugin_log(cmd->plugin, LOG_TRACE, "%s called: %.*s", __func__, json_tok_full_len(params), json_tok_full(buffer, params)); - if (amount_msat_is_zero(*info->amount)) { + if (amount_msat_is_zero(*amount)) { return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "amount must be non-zero"); } - if (command_check_only(cmd)) - return command_check_done(cmd); - - if (*info->maxdelay > maxdelay_allowed) { + if (*maxdelay > maxdelay_allowed) { return command_fail(cmd, PAY_USER_ERROR, "maximum delay allowed is %d", maxdelay_allowed); } + if (command_check_only(cmd)) + return command_check_done(cmd); + info->cmd = cmd; + info->source = *source; + info->dest = *dest; + info->amount = *amount; + info->maxfee = *maxfee; + info->finalcltv = *finalcltv; + info->maxdelay = *maxdelay; info->additional_costs = tal(info, struct additional_cost_htable); additional_cost_htable_init(info->additional_costs); From cf6c33d6a83b3f30b24e1d603ae075d038336aef Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Thu, 10 Jul 2025 23:45:06 +0930 Subject: [PATCH 8/8] askrene: add a dev parameter to switch algorithm Changelog-None Signed-off-by: Lagrang3 --- plugins/askrene/askrene.c | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 3b53cf51e3fb..c9071fa3a1cf 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -331,11 +331,31 @@ const char *fmt_flow_full(const tal_t *ctx, return str; } +enum algorithm { + ALGO_DEFAULT, +}; + +static struct command_result * +param_algorithm(struct command *cmd, const char *name, const char *buffer, + const jsmntok_t *tok, enum algorithm **algo) +{ + const char *algo_str = json_strdup(cmd, buffer, tok); + *algo = tal(cmd, enum algorithm); + if (streq(algo_str, "default")) + **algo = ALGO_DEFAULT; + else + return command_fail_badparam(cmd, name, buffer, tok, + "unknown algorithm"); + return NULL; +} + struct getroutes_info { struct command *cmd; struct node_id source, dest; struct amount_msat amount, maxfee; u32 finalcltv, maxdelay; + /* algorithm selection, only dev */ + enum algorithm dev_algo; const char **layers; struct additional_cost_htable *additional_costs; /* Non-NULL if we are told to use "auto.localchans" */ @@ -567,8 +587,9 @@ static struct command_result *do_getroutes(struct command *cmd, } /* Compute the routes. At this point we might select between multiple - * algorithms. */ + * 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"), @@ -722,6 +743,7 @@ static struct command_result *json_getroutes(struct command *cmd, struct node_id *source, *dest; struct amount_msat *amount, *maxfee; u32 *finalcltv, *maxdelay; + enum algorithm *dev_algo; if (!param_check(cmd, buffer, params, p_req("source", param_node_id, &source), @@ -732,6 +754,8 @@ static struct command_result *json_getroutes(struct command *cmd, p_req("final_cltv", param_u32, &finalcltv), p_opt_def("maxdelay", param_u32, &maxdelay, maxdelay_allowed), + p_opt_dev("dev_algorithm", param_algorithm, + &dev_algo, ALGO_DEFAULT), NULL)) return command_param_failed(); plugin_log(cmd->plugin, LOG_TRACE, "%s called: %.*s", __func__, @@ -758,6 +782,7 @@ static struct command_result *json_getroutes(struct command *cmd, info->maxfee = *maxfee; info->finalcltv = *finalcltv; info->maxdelay = *maxdelay; + info->dev_algo = *dev_algo; info->additional_costs = tal(info, struct additional_cost_htable); additional_cost_htable_init(info->additional_costs);