Skip to content

Commit 47b0f67

Browse files
Lagrang3rustyrussell
authored andcommitted
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 <[email protected]>
1 parent 2a6eab2 commit 47b0f67

File tree

2 files changed

+264
-0
lines changed

2 files changed

+264
-0
lines changed

plugins/askrene/mcf.c

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,28 @@ static void set_capacity(s64 *capacity, u64 value, u64 *cap_on_capacity)
319319
*cap_on_capacity -= *capacity;
320320
}
321321

322+
/* FIXME: unit test this */
323+
/* The probability of forwarding a payment amount given a high and low liquidity
324+
* bounds.
325+
* @low: the liquidity is known to be greater or equal than "low"
326+
* @high: the liquidity is known to be less than "high"
327+
* @amount: how much is required to forward */
328+
static double pickhardt_richter_probability(struct amount_msat low,
329+
struct amount_msat high,
330+
struct amount_msat amount)
331+
{
332+
struct amount_msat all_states, good_states;
333+
if (amount_msat_greater_eq(amount, high))
334+
return 0.0;
335+
if (!amount_msat_sub(&amount, amount, low))
336+
return 1.0;
337+
if (!amount_msat_sub(&all_states, high, low))
338+
abort(); // we expect high > low
339+
if (!amount_msat_sub(&good_states, all_states, amount))
340+
abort(); // we expect high > amount
341+
return amount_msat_ratio(good_states, all_states);
342+
}
343+
322344
// TODO(eduardo): unit test this
323345
/* Split a directed channel into parts with linear cost function. */
324346
static void linearize_channel(const struct pay_parameters *params,
@@ -867,6 +889,46 @@ get_flow_paths(const tal_t *ctx,
867889
return flows;
868890
}
869891

892+
/* Given a single path build a flow set. */
893+
static struct flow **
894+
get_flow_singlepath(const tal_t *ctx, const struct pay_parameters *params,
895+
const struct graph *graph, const struct gossmap *gossmap,
896+
const struct node source, const struct node destination,
897+
const u64 pay_amount, const struct arc *prev)
898+
{
899+
struct flow **flows, *f;
900+
flows = tal_arr(ctx, struct flow *, 1);
901+
f = flows[0] = tal(flows, struct flow);
902+
903+
size_t length = 0;
904+
905+
for (u32 cur_idx = destination.idx; cur_idx != source.idx;) {
906+
assert(cur_idx != INVALID_INDEX);
907+
length++;
908+
struct arc arc = prev[cur_idx];
909+
struct node next = arc_tail(graph, arc);
910+
cur_idx = next.idx;
911+
}
912+
f->path = tal_arr(f, const struct gossmap_chan *, length);
913+
f->dirs = tal_arr(f, int, length);
914+
915+
for (u32 cur_idx = destination.idx; cur_idx != source.idx;) {
916+
int chandir;
917+
u32 chanidx;
918+
struct arc arc = prev[cur_idx];
919+
arc_to_parts(arc, &chanidx, &chandir, NULL, NULL);
920+
921+
length--;
922+
f->path[length] = gossmap_chan_byidx(gossmap, chanidx);
923+
f->dirs[length] = chandir;
924+
925+
struct node next = arc_tail(graph, arc);
926+
cur_idx = next.idx;
927+
}
928+
f->delivers = params->amount;
929+
return flows;
930+
}
931+
870932
// TODO(eduardo): choose some default values for the minflow parameters
871933
/* eduardo: I think it should be clear that this module deals with linear
872934
* 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,
10251087
return total;
10261088
}
10271089

1090+
/* Initialize the data vectors for the single-path solver. */
1091+
static void init_linear_network_single_path(
1092+
const tal_t *ctx, const struct pay_parameters *params, struct graph **graph,
1093+
double **arc_prob_cost, s64 **arc_fee_cost, s64 **arc_capacity)
1094+
{
1095+
const size_t max_num_chans = gossmap_max_chan_idx(params->rq->gossmap);
1096+
const size_t max_num_arcs = max_num_chans * ARCS_PER_CHANNEL;
1097+
const size_t max_num_nodes = gossmap_max_node_idx(params->rq->gossmap);
1098+
1099+
*graph = graph_new(ctx, max_num_nodes, max_num_arcs, ARC_DUAL_BITOFF);
1100+
*arc_prob_cost = tal_arr(ctx, double, max_num_arcs);
1101+
for (size_t i = 0; i < max_num_arcs; ++i)
1102+
(*arc_prob_cost)[i] = DBL_MAX;
1103+
1104+
*arc_fee_cost = tal_arr(ctx, s64, max_num_arcs);
1105+
for (size_t i = 0; i < max_num_arcs; ++i)
1106+
(*arc_fee_cost)[i] = INT64_MAX;
1107+
*arc_capacity = tal_arrz(ctx, s64, max_num_arcs);
1108+
1109+
const struct gossmap *gossmap = params->rq->gossmap;
1110+
1111+
for (struct gossmap_node *node = gossmap_first_node(gossmap); node;
1112+
node = gossmap_next_node(gossmap, node)) {
1113+
const u32 node_id = gossmap_node_idx(gossmap, node);
1114+
1115+
for (size_t j = 0; j < node->num_chans; ++j) {
1116+
int half;
1117+
const struct gossmap_chan *c =
1118+
gossmap_nth_chan(gossmap, node, j, &half);
1119+
struct amount_msat mincap, maxcap;
1120+
1121+
if (!gossmap_chan_set(c, half) ||
1122+
!c->half[half].enabled)
1123+
continue;
1124+
1125+
/* If a channel cannot forward the total amount we don't
1126+
* use it. */
1127+
if (amount_msat_less(params->amount,
1128+
gossmap_chan_htlc_min(c, half)) ||
1129+
amount_msat_greater(params->amount,
1130+
gossmap_chan_htlc_max(c, half)))
1131+
continue;
1132+
1133+
get_constraints(params->rq, c, half, &mincap, &maxcap);
1134+
/* Assume if min > max, min is wrong */
1135+
if (amount_msat_greater(mincap, maxcap))
1136+
mincap = maxcap;
1137+
/* It is preferable to work on 1msat past the known
1138+
* bound. */
1139+
if (!amount_msat_accumulate(&maxcap, amount_msat(1)))
1140+
abort();
1141+
1142+
/* If amount is greater than the known liquidity upper
1143+
* bound we get infinite probability cost. */
1144+
if (amount_msat_greater_eq(params->amount, maxcap))
1145+
continue;
1146+
1147+
const u32 chan_id = gossmap_chan_idx(gossmap, c);
1148+
1149+
const struct gossmap_node *next =
1150+
gossmap_nth_node(gossmap, c, !half);
1151+
1152+
const u32 next_id = gossmap_node_idx(gossmap, next);
1153+
1154+
/* channel to self? */
1155+
if (node_id == next_id)
1156+
continue;
1157+
1158+
struct arc arc =
1159+
arc_from_parts(chan_id, half, 0, false);
1160+
1161+
graph_add_arc(*graph, arc, node_obj(node_id),
1162+
node_obj(next_id));
1163+
1164+
(*arc_capacity)[arc.idx] = 1;
1165+
(*arc_prob_cost)[arc.idx] =
1166+
(-1.0) * log(pickhardt_richter_probability(
1167+
mincap, maxcap, params->amount));
1168+
1169+
struct amount_msat fee;
1170+
if (!amount_msat_fee(&fee, params->amount,
1171+
c->half[half].base_fee,
1172+
c->half[half].proportional_fee))
1173+
abort();
1174+
u32 fee_msat;
1175+
if (!amount_msat_to_u32(fee, &fee_msat))
1176+
continue;
1177+
(*arc_fee_cost)[arc.idx] =
1178+
fee_msat +
1179+
params->delay_feefactor * c->half[half].delay;
1180+
}
1181+
}
1182+
}
1183+
1184+
/* Similar to minflow but computes routes that have a single path. */
1185+
struct flow **single_path_flow(const tal_t *ctx, const struct route_query *rq,
1186+
const struct gossmap_node *source,
1187+
const struct gossmap_node *target,
1188+
struct amount_msat amount, u32 mu,
1189+
double delay_feefactor)
1190+
{
1191+
struct flow **flow_paths;
1192+
/* We allocate everything off this, and free it at the end,
1193+
* as we can be called multiple times without cleaning tmpctx! */
1194+
tal_t *working_ctx = tal(NULL, char);
1195+
struct pay_parameters *params = tal(working_ctx, struct pay_parameters);
1196+
1197+
params->rq = rq;
1198+
params->source = source;
1199+
params->target = target;
1200+
params->amount = amount;
1201+
/* for the single-path solver the accuracy does not detriment
1202+
* performance */
1203+
params->accuracy = amount;
1204+
params->delay_feefactor = delay_feefactor;
1205+
params->base_fee_penalty = base_fee_penalty_estimate(amount);
1206+
1207+
struct graph *graph;
1208+
double *arc_prob_cost;
1209+
s64 *arc_fee_cost;
1210+
s64 *arc_capacity;
1211+
1212+
init_linear_network_single_path(working_ctx, params, &graph,
1213+
&arc_prob_cost, &arc_fee_cost,
1214+
&arc_capacity);
1215+
1216+
const struct node dst = {.idx = gossmap_node_idx(rq->gossmap, target)};
1217+
const struct node src = {.idx = gossmap_node_idx(rq->gossmap, source)};
1218+
1219+
const size_t max_num_nodes = graph_max_num_nodes(graph);
1220+
const size_t max_num_arcs = graph_max_num_arcs(graph);
1221+
1222+
s64 *potential = tal_arrz(working_ctx, s64, max_num_nodes);
1223+
s64 *distance = tal_arrz(working_ctx, s64, max_num_nodes);
1224+
s64 *arc_cost = tal_arrz(working_ctx, s64, max_num_arcs);
1225+
struct arc *prev = tal_arrz(working_ctx, struct arc, max_num_nodes);
1226+
1227+
combine_cost_function(working_ctx, graph, arc_prob_cost, arc_fee_cost,
1228+
rq->biases, mu, arc_cost);
1229+
1230+
/* We solve a linear cost flow problem. */
1231+
if (!dijkstra_path(working_ctx, graph, src, dst,
1232+
/* prune = */ true, arc_capacity,
1233+
/*threshold = */ 1, arc_cost, potential, prev,
1234+
distance)) {
1235+
/* This might fail if we are unable to find a suitable route, it
1236+
* doesn't mean the plugin is broken, that's why we LOG_INFORM. */
1237+
rq_log(tmpctx, rq, LOG_INFORM,
1238+
"%s: could not find a feasible single path", __func__);
1239+
goto fail;
1240+
}
1241+
const u64 pay_amount =
1242+
amount_msat_ratio_ceil(params->amount, params->accuracy);
1243+
1244+
/* We dissect the flow into payment routes.
1245+
* Actual amounts considering fees are computed for every
1246+
* channel in the routes. */
1247+
flow_paths = get_flow_singlepath(ctx, params, graph, rq->gossmap,
1248+
src, dst, pay_amount, prev);
1249+
if (!flow_paths) {
1250+
rq_log(tmpctx, rq, LOG_BROKEN,
1251+
"%s: failed to extract flow paths from the single-path "
1252+
"solution",
1253+
__func__);
1254+
goto fail;
1255+
}
1256+
if (tal_count(flow_paths) != 1) {
1257+
rq_log(
1258+
tmpctx, rq, LOG_BROKEN,
1259+
"%s: single-path solution returned a multi route solution",
1260+
__func__);
1261+
goto fail;
1262+
}
1263+
tal_free(working_ctx);
1264+
return flow_paths;
1265+
1266+
fail:
1267+
tal_free(working_ctx);
1268+
return NULL;
1269+
}
10281270

10291271
const char *default_routes(const tal_t *ctx, struct route_query *rq,
10301272
const struct gossmap_node *srcnode,

plugins/askrene/mcf.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ struct flow **minflow(const tal_t *ctx,
3434
double delay_feefactor,
3535
bool single_part);
3636

37+
/**
38+
* API for min cost single path.
39+
* @ctx: context to allocate returned flows from
40+
* @rq: the route_query we're processing (for logging)
41+
* @source: the source to start from
42+
* @target: the target to pay
43+
* @amount: the amount we want to reach @target
44+
* @mu: 0 = corresponds to only probabilities, 100 corresponds to only fee.
45+
* @delay_feefactor: convert 1 block delay into msat.
46+
*
47+
* @delay_feefactor converts 1 block delay into msat, as if it were an additional
48+
* fee. So if a CLTV delay on a node is 5 blocks, that's treated as if it
49+
* were a fee of 5 * @delay_feefactor.
50+
*
51+
* Returns an array with one flow which deliver amount to target, or NULL.
52+
*/
53+
struct flow **single_path_flow(const tal_t *ctx, const struct route_query *rq,
54+
const struct gossmap_node *source,
55+
const struct gossmap_node *target,
56+
struct amount_msat amount, u32 mu,
57+
double delay_feefactor);
58+
3759
/* To sanity check: this is the approximation mcf uses for the cost
3860
* of each channel. */
3961
struct amount_msat linear_flow_cost(const struct flow *flow,

0 commit comments

Comments
 (0)