diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index c850bf9ebf4c..b6a43c80fa14 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -15587,7 +15587,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 path solution which is useful for payments for which MPP is not supported." + "There are four 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. *auto.no_mpp_support* forces getroutes to return a single path solution which is useful for payments for which MPP is not supported. And *auto.include_fees* that fixes the send amount and deducts fee from there, ie. the receiver pays for fees instead of the sender." ], "categories": [ "readonly" diff --git a/doc/schemas/getroutes.json b/doc/schemas/getroutes.json index 6fc5020b23e2..04f502e2890b 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 path solution which is useful for payments for which MPP is not supported." + "There are four 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. *auto.no_mpp_support* forces getroutes to return a single path solution which is useful for payments for which MPP is not supported. And *auto.include_fees* that fixes the send amount and deducts fee from there, ie. the receiver pays for fees instead of the sender." ], "categories": [ "readonly" diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index fe8f477fbd85..5316aa44cdd9 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -112,7 +112,8 @@ static struct command_result *param_layer_names(struct command *cmd, /* Must be a known layer name */ if (streq((*arr)[i], "auto.localchans") || streq((*arr)[i], "auto.no_mpp_support") - || streq((*arr)[i], "auto.sourcefree")) + || streq((*arr)[i], "auto.sourcefree") + || streq((*arr)[i], "auto.include_fees")) continue; if (!find_layer(get_askrene(cmd->plugin), (*arr)[i])) { return command_fail_badparam(cmd, name, buffer, t, @@ -425,6 +426,12 @@ static void apply_layers(struct askrene *askrene, struct route_query *rq, } 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 if (streq(layers[i], "auto.include_fees")) { + plugin_log(rq->plugin, LOG_DBG, + "Adding auto.include_fees"); + /* This layer takes effect when converting flows + * into routes. */ + continue; } else { assert(streq(layers[i], "auto.sourcefree")); plugin_log(rq->plugin, LOG_DBG, "Adding auto.sourcefree"); @@ -447,7 +454,8 @@ 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 amount_msat **amounts, + bool include_fees) { struct route **routes; routes = tal_arr(ctx, struct route *, tal_count(flows)); @@ -462,27 +470,70 @@ static struct route **convert_flows_to_routes(const tal_t *ctx, 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; + if (!include_fees) { + /* Fill in backwards to calc amount and delay */ + 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; + } else { + /* Fill in backwards to calc delay */ + 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); + + 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->delay = delay; + } + /* Compute fees forward */ + for (int j = 0; j < tal_count(flows[i]->path); j++) { + struct route_hop *rh = &r->hops[j]; + const struct half_chan *h = + flow_edge(flows[i], j); + + rh->amount = msat; + msat = amount_msat_sub_fee(msat, h->base_fee, + h->proportional_fee); + } + (*amounts)[i] = msat; } - (*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)); @@ -574,6 +625,7 @@ static struct command_result *do_getroutes(struct command *cmd, struct flow **flows; struct json_stream *response; const struct gossmap_node *me; + bool include_fees; /* update the gossmap */ if (gossmap_refresh(askrene->gossmap)) { @@ -709,9 +761,11 @@ static struct command_result *do_getroutes(struct command *cmd, rq_log(tmpctx, rq, LOG_DBG, "Final answer has %zu flows", tal_count(flows)); + include_fees = have_layer(info->layers, "auto.include_fees"); + /* convert flows to routes */ routes = convert_flows_to_routes(rq, rq, info->finalcltv, flows, - &amounts); + &amounts, include_fees); assert(tal_count(routes) == tal_count(flows)); assert(tal_count(amounts) == tal_count(flows)); diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 501deebb800a..beac7b3ae7a9 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -1994,3 +1994,415 @@ def test_excessive_fee_cost(node_factory): maxfee_msat=1000, final_cltv=5, ) + + +def check_getroute_routes( + node, + source, + destination, + amount_msat, + routes, + layers=[], + maxfee_msat=1000, + final_cltv=99, +): + """Check that routes are as expected in result. This is similar to + check_getroute_paths but it compares all fields inside the routes and not + just the path.""" + + def check_route_as_expected(routes, expected_routes): + """Make sure all fields in paths are match those in routes""" + + def dict_subset_eq(a, b): + """Is every key in B is the same in A?""" + return all(a.get(key) == b[key] for key in b if key != "path") + + for r in expected_routes: + found = False + for i, candidate in enumerate(routes): + pathlen = len(r["path"]) + if len(candidate["path"]) != pathlen: + continue + if dict_subset_eq(candidate, r) and all( + dict_subset_eq(candidate["path"][i], r["path"][i]) + for i in range(pathlen) + ): + del routes[i] + found = True + break + + if not found: + raise ValueError( + "Could not find route {} in routes {}".format(r, routes) + ) + + if routes != []: + raise ValueError("Did not expect paths {}".format(routes)) + + getroutes = node.rpc.getroutes( + source=source, + destination=destination, + amount_msat=amount_msat, + layers=layers, + maxfee_msat=maxfee_msat, + final_cltv=final_cltv, + ) + assert getroutes["probability_ppm"] <= 1000000 + check_route_as_expected(getroutes["routes"], routes) + + +def test_includefees(node_factory): + """Test the amounts in the hops is set correctly when we use + auto.include_fees layer.""" + gsfile, nodemap = generate_gossip_store( + [ + GenChannel( + 0, + 1, + capacity_sats=1000000, + forward=GenChannel.Half(basefee=1, propfee=10000, delay=5), + ), + GenChannel( + 1, + 2, + capacity_sats=1000000, + forward=GenChannel.Half(basefee=2, propfee=20000, delay=5), + ), + GenChannel( + 2, + 3, + capacity_sats=1000000, + forward=GenChannel.Half(basefee=3, propfee=30000, delay=5), + ), + ] + ) + l1 = node_factory.get_node(gossip_store_file=gsfile.name) + + # Check computed fees in normal mode + check_getroute_routes( + l1, + nodemap[0], + nodemap[1], + 1000, + [ + { + "amount_msat": 1000, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1011, + "delay": 99 + 5, + } + ], + } + ], + layers=[], + ) + + check_getroute_routes( + l1, + nodemap[0], + nodemap[2], + 1000, + [ + { + "amount_msat": 1000, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1033, + "delay": 99 + 5 + 5, + }, + { + "short_channel_id_dir": "1x2x1/1", + "next_node_id": nodemap[2], + "amount_msat": 1022, + "delay": 99 + 5, + }, + ], + } + ], + layers=[], + ) + + check_getroute_routes( + l1, + nodemap[0], + nodemap[3], + 1000, + [ + { + "amount_msat": 1000, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1066, + "delay": 99 + 5 + 5 + 5, + }, + { + "short_channel_id_dir": "1x2x1/1", + "next_node_id": nodemap[2], + "amount_msat": 1055, + "delay": 99 + 5 + 5, + }, + { + "short_channel_id_dir": "2x3x2/0", + "next_node_id": nodemap[3], + "amount_msat": 1033, + "delay": 99 + 5, + }, + ], + } + ], + layers=[], + ) + + # Check computed fees in include_fees mode + check_getroute_routes( + l1, + nodemap[0], + nodemap[1], + 1000, + [ + # we compute a route for 1000msat and the recepient receives 990 + { + "amount_msat": 990, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1000, + "delay": 99 + 5, + } + ], + } + ], + layers=["auto.include_fees"], + ) + + check_getroute_routes( + l1, + nodemap[0], + nodemap[2], + 1000, + [ + { + "amount_msat": 969, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1000, + "delay": 99 + 5 + 5, + }, + { + "short_channel_id_dir": "1x2x1/1", + "next_node_id": nodemap[2], + "amount_msat": 990, + "delay": 99 + 5, + }, + ], + } + ], + layers=["auto.include_fees"], + ) + + check_getroute_routes( + l1, + nodemap[0], + nodemap[3], + 1000, + [ + { + "amount_msat": 938, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1000, + "delay": 99 + 5 + 5 + 5, + }, + { + "short_channel_id_dir": "1x2x1/1", + "next_node_id": nodemap[2], + "amount_msat": 990, + "delay": 99 + 5 + 5, + }, + { + "short_channel_id_dir": "2x3x2/0", + "next_node_id": nodemap[3], + "amount_msat": 969, + "delay": 99 + 5, + }, + ], + } + ], + layers=["auto.include_fees"], + ) + + # Normal mode combined with "auto.sourcefree" + check_getroute_routes( + l1, + nodemap[0], + nodemap[1], + 1000, + [ + { + "amount_msat": 1000, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1000, + "delay": 99, + } + ], + } + ], + layers=["auto.sourcefree"], + ) + + check_getroute_routes( + l1, + nodemap[0], + nodemap[2], + 1000, + [ + { + "amount_msat": 1000, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1022, + "delay": 99 + 5, + }, + { + "short_channel_id_dir": "1x2x1/1", + "next_node_id": nodemap[2], + "amount_msat": 1022, + "delay": 99 + 5, + }, + ], + } + ], + layers=["auto.sourcefree"], + ) + + check_getroute_routes( + l1, + nodemap[0], + nodemap[3], + 1000, + [ + { + "amount_msat": 1000, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1055, + "delay": 99 + 5 + 5, + }, + { + "short_channel_id_dir": "1x2x1/1", + "next_node_id": nodemap[2], + "amount_msat": 1055, + "delay": 99 + 5 + 5, + }, + { + "short_channel_id_dir": "2x3x2/0", + "next_node_id": nodemap[3], + "amount_msat": 1033, + "delay": 99 + 5, + }, + ], + } + ], + layers=["auto.sourcefree"], + ) + + # "auto.include_fees" mode combined with "auto.sourcefree" + check_getroute_routes( + l1, + nodemap[0], + nodemap[1], + 1000, + [ + { + "amount_msat": 1000, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1000, + "delay": 99, + } + ], + } + ], + layers=["auto.sourcefree", "auto.include_fees"], + ) + + check_getroute_routes( + l1, + nodemap[0], + nodemap[2], + 1000, + [ + { + "amount_msat": 979, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1000, + "delay": 99 + 5, + }, + { + "short_channel_id_dir": "1x2x1/1", + "next_node_id": nodemap[2], + "amount_msat": 1000, + "delay": 99 + 5, + }, + ], + } + ], + layers=["auto.sourcefree", "auto.include_fees"], + ) + + check_getroute_routes( + l1, + nodemap[0], + nodemap[3], + 1000, + [ + { + "amount_msat": 948, + "path": [ + { + "short_channel_id_dir": "0x1x0/1", + "next_node_id": nodemap[1], + "amount_msat": 1000, + "delay": 99 + 5 + 5, + }, + { + "short_channel_id_dir": "1x2x1/1", + "next_node_id": nodemap[2], + "amount_msat": 1000, + "delay": 99 + 5 + 5, + }, + { + "short_channel_id_dir": "2x3x2/0", + "next_node_id": nodemap[3], + "amount_msat": 979, + "delay": 99 + 5, + }, + ], + } + ], + layers=["auto.sourcefree", "auto.include_fees"], + )