Skip to content

Commit

Permalink
plugins/askrene: attach getroutes call to MCF code.
Browse files Browse the repository at this point in the history
Now getroutes actually does something!

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
  • Loading branch information
rustyrussell committed Aug 4, 2024
1 parent 3d6db10 commit 52827f3
Show file tree
Hide file tree
Showing 2 changed files with 291 additions and 20 deletions.
161 changes: 142 additions & 19 deletions plugins/askrene/askrene.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
#include <common/route.h>
#include <errno.h>
#include <plugins/askrene/askrene.h>
#include <plugins/askrene/flow.h>
#include <plugins/askrene/layer.h>
#include <plugins/askrene/mcf.h>
#include <plugins/askrene/reserve.h>
#include <plugins/libplugin.h>

Expand Down Expand Up @@ -168,12 +170,22 @@ static const char *get_routes(struct command *cmd,
const struct node_id *source,
const struct node_id *dest,
struct amount_msat amount,
struct amount_msat maxfee,
u32 finalcltv,
const char **layers,
struct route ***routes)
struct route ***routes,
struct amount_msat **amounts,
double *probability)
{
struct askrene *askrene = get_askrene(cmd->plugin);
struct route_query *rq = tal(cmd, struct route_query);
struct gossmap_localmods *localmods;
struct flow **flows;
const struct gossmap_node *srcnode, *dstnode;
double delay_feefactor;
double base_fee_penalty;
u32 prob_cost_factor, mu;
const char *ret;

if (gossmap_refresh(askrene->gossmap, NULL)) {
/* FIXME: gossmap_refresh callbacks to we can update in place */
Expand Down Expand Up @@ -208,20 +220,122 @@ static const char *get_routes(struct command *cmd,
reserves_clear_capacities(askrene->reserved, askrene->gossmap, rq->capacities);

gossmap_apply_localmods(askrene->gossmap, localmods);
(void)rq;

/* FIXME: Do route here! This is a dummy, single "direct" route. */
*routes = tal_arr(cmd, struct route *, 1);
(*routes)[0]->success_prob = 1;
(*routes)[0]->hops = tal_arr((*routes)[0], struct route_hop, 1);
(*routes)[0]->hops[0].scid.u64 = 0x0000010000020003ULL;
(*routes)[0]->hops[0].direction = 0;
(*routes)[0]->hops[0].node_id = *dest;
(*routes)[0]->hops[0].amount = amount;
(*routes)[0]->hops[0].delay = 6;

srcnode = gossmap_find_node(askrene->gossmap, source);
if (!srcnode) {
ret = tal_fmt(cmd, "Unknown source node %s", fmt_node_id(tmpctx, source));
goto out;
}

dstnode = gossmap_find_node(askrene->gossmap, dest);
if (!dstnode) {
ret = tal_fmt(cmd, "Unknown destination node %s", fmt_node_id(tmpctx, dest));
goto out;
}

delay_feefactor = 1.0/1000000;
base_fee_penalty = 10.0;

/* From mcf.c: The input parameter `prob_cost_factor` in the function
* `minflow` is defined as the PPM from the delivery amount `T` we are
* *willing to pay* to increase the prob. of success by 0.1% */

/* This value is somewhat implied by our fee budget: say we would pay
* the entire budget for 100% probability, that means prob_cost_factor
* is (fee / amount) / 1000, or in PPM: (fee / amount) * 1000 */
if (amount_msat_zero(amount))
prob_cost_factor = 0;
else
prob_cost_factor = amount_msat_ratio(maxfee, amount) * 1000;

/* First up, don't care about fees. */
mu = 0;
flows = minflow(rq, rq, srcnode, dstnode, amount,
mu, delay_feefactor, base_fee_penalty, prob_cost_factor);
if (!flows) {
/* FIXME: disjktra here to see if there is any route, and
* diagnose problem (offline peers? Not enough capacity at
* our end? Not enough at theirs?) */
ret = tal_fmt(cmd, "Could not find route");
goto out;
}

/* Too much delay? */
/* BOLT #4:
* ## `max_htlc_cltv` Selection
*
* This ... value is defined as 2016 blocks, based on historical value
* deployed by Lightning implementations.
*/
/* FIXME: Typo in spec for CLTV in descripton! But it breaks our spelling check, so we omit it above */
while (finalcltv + flows_worst_delay(flows) > 2016) {
delay_feefactor *= 2;
flows = minflow(rq, rq, srcnode, dstnode, amount,
mu, delay_feefactor, base_fee_penalty, prob_cost_factor);
if (!flows || delay_feefactor > 10) {
ret = tal_fmt(cmd, "Could not find route without excessive delays");
goto out;
}
}

/* Too expensive? */
while (amount_msat_greater(flowset_fee(cmd->plugin, flows), maxfee)) {
mu += 10;
flows = minflow(rq, rq, srcnode, dstnode, amount,
mu, delay_feefactor, base_fee_penalty, prob_cost_factor);
if (!flows || mu == 100) {
ret = tal_fmt(cmd, "Could not find route without excessive cost");
goto out;
}
}

if (finalcltv + flows_worst_delay(flows) > 2016) {
ret = tal_fmt(cmd, "Could not find route without excessive cost or delays");
goto out;
}

/* Convert back into routes, with delay and other information fixed */
*routes = tal_arr(cmd, struct route *, tal_count(flows));
*amounts = tal_arr(cmd, 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);
/* FIXME: flow_probability doesn't take into account other flows! */
r->success_prob = flows[i]->success_prob;
r->hops = tal_arr(r, struct route_hop, tal_count(flows[i]->path));

/* Fill in backwards to calc amount and delay */
msat = flows[i]->amount;
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(cmd->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] = flow_delivers(flows[i]);
}

*probability = flowset_probability(flows, rq);
ret = NULL;

out:
gossmap_remove_localmods(askrene->gossmap, localmods);
return NULL;
return ret;
}

void get_constraints(const struct route_query *rq,
Expand Down Expand Up @@ -294,41 +408,50 @@ static struct command_result *json_getroutes(struct command *cmd,
{
struct node_id *dest, *source;
const char **layers;
struct amount_msat *amount;
struct amount_msat *amount, *maxfee, *amounts;
struct route **routes;
struct json_stream *response;
u32 *finalcltv;
const char *err;
double probability;

if (!param(cmd, buffer, params,
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_string_array, &layers),
p_req("maxfee_msat", param_msat, &maxfee),
p_req("finalcltv", param_u32, &finalcltv),
NULL))
return command_param_failed();

err = get_routes(cmd, source, dest, *amount, layers, &routes);
err = get_routes(cmd, source, dest, *amount, *maxfee, *finalcltv, layers,
&routes, &amounts, &probability);
if (err)
return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err);

response = jsonrpc_stream_success(cmd);
json_object_start(response, "routes");
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_array_start(response, "path");
for (size_t j = 0; j < tal_count(routes[i]->hops); j++) {
const struct route_hop *r = &routes[i]->hops[j];
json_object_start(response, NULL);
json_add_short_channel_id(response, "short_channel_id", r->scid);
json_add_u32(response, "direction", r->direction);
json_add_node_id(response, "node_id", &r->node_id);
json_add_amount_msat(response, "amount", r->amount);
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_object_end(response);
return command_finished(cmd, response);
}

Expand Down
150 changes: 149 additions & 1 deletion tests/test_askrene.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from fixtures import * # noqa: F401,F403
from pyln.client import RpcError
from utils import (
only_one, first_scid
only_one, first_scid, GenChannel, generate_gossip_store,
TEST_NETWORK
)
import os
import pytest
import time
import shutil


def test_layers(node_factory):
Expand Down Expand Up @@ -102,3 +107,146 @@ def test_layers(node_factory):
del expect['constraints'][0]
listlayers = l2.rpc.askrene_listlayers('test_layers')
assert listlayers == {'layers': [expect]}


def check_getroute_paths(node,
source,
destination,
amount_msat,
paths,
layers=[],
maxfee_msat=1000,
finalcltv=99):
"""Check that routes are as expected in result"""
getroutes = node.rpc.getroutes(source=source,
destination=destination,
amount_msat=amount_msat,
layers=layers,
maxfee_msat=maxfee_msat,
finalcltv=finalcltv)

assert getroutes['probability_ppm'] <= 1000000
# Total delivered should be amount we told it to send.
assert amount_msat == sum([r['amount_msat'] for r in getroutes['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)

for expected_path in paths:
found = False
for i in range(len(getroutes['routes'])):
route = getroutes['routes'][i]
if len(route['path']) != len(expected_path):
continue
if all(dict_subset_eq(route['path'][i], expected_path[i]) for i in range(len(expected_path))):
del getroutes['routes'][i]
found = True
break
if not found:
raise ValueError("Could not find expected_path {} in paths {}".format(expected_path, getroutes['routes']))

if getroutes['routes'] != []:
raise ValueError("Did not expect paths {}".format(getroutes['routes']))


def test_getroutes(node_factory):
"""Test getroutes call"""
l1 = node_factory.get_node(start=False)
gsfile, nodemap = generate_gossip_store([GenChannel(0, 1, forward=GenChannel.Half(propfee=10000)),
GenChannel(0, 2, capacity_sats=9000),
GenChannel(1, 3, forward=GenChannel.Half(propfee=20000)),
GenChannel(0, 2, capacity_sats=10000),
GenChannel(2, 4, forward=GenChannel.Half(delay=2000))])

# Set up l1 with this as the gossip_store
shutil.copy(gsfile.name, os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store'))
l1.start()

# Start easy
assert l1.rpc.getroutes(source=nodemap[0],
destination=nodemap[1],
amount_msat=1000,
layers=[],
maxfee_msat=1000,
finalcltv=99) == {'probability_ppm': 999999,
'routes': [{'probability_ppm': 999999,
'amount_msat': 1000,
'path': [{'short_channel_id': '0x1x0',
'direction': 1,
'next_node_id': nodemap[1],
'amount_msat': 1010,
'delay': 99 + 6}]}]}
# Two hop, still easy.
assert l1.rpc.getroutes(source=nodemap[0],
destination=nodemap[3],
amount_msat=100000,
layers=[],
maxfee_msat=5000,
finalcltv=99) == {'probability_ppm': 999798,
'routes': [{'probability_ppm': 999798,
'amount_msat': 100000,
'path': [{'short_channel_id': '0x1x0',
'direction': 1,
'next_node_id': nodemap[1],
'amount_msat': 103020,
'delay': 99 + 6 + 6},
{'short_channel_id': '1x3x2',
'direction': 1,
'next_node_id': nodemap[3],
'amount_msat': 102000,
'delay': 99 + 6}
]}]}

# Too expensive
with pytest.raises(RpcError, match="Could not find route without excessive cost"):
l1.rpc.getroutes(source=nodemap[0],
destination=nodemap[3],
amount_msat=100000,
layers=[],
maxfee_msat=100,
finalcltv=99)

# Too much delay (if final delay too great!)
l1.rpc.getroutes(source=nodemap[0],
destination=nodemap[4],
amount_msat=100000,
layers=[],
maxfee_msat=100,
finalcltv=6)
with pytest.raises(RpcError, match="Could not find route without excessive delays"):
l1.rpc.getroutes(source=nodemap[0],
destination=nodemap[4],
amount_msat=100000,
layers=[],
maxfee_msat=100,
finalcltv=99)

# Two choices, but for <= 1000 sats we choose the larger.
assert l1.rpc.getroutes(source=nodemap[0],
destination=nodemap[2],
amount_msat=1000000,
layers=[],
maxfee_msat=5000,
finalcltv=99) == {'probability_ppm': 900000,
'routes': [{'probability_ppm': 900000,
'amount_msat': 1000000,
'path': [{'short_channel_id': '0x2x3',
'direction': 1,
'next_node_id': nodemap[2],
'amount_msat': 1000001,
'delay': 99 + 6}]}]}

# For 10000 sats, we will split.
check_getroute_paths(l1,
nodemap[0],
nodemap[2],
10000000,
[[{'short_channel_id': '0x2x1',
'next_node_id': nodemap[2],
'amount_msat': 500000,
'delay': 99 + 6}],
[{'short_channel_id': '0x2x3',
'next_node_id': nodemap[2],
'amount_msat': 9500009,
'delay': 99 + 6}]])

0 comments on commit 52827f3

Please sign in to comment.