Skip to content

Commit

Permalink
plugin/askrene: add "auto.sourcefree" layer.
Browse files Browse the repository at this point in the history
This marks all channels around the source node as free (no delay, no fee).  This is normally what we want, if we are calculating a path for ourselves.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
  • Loading branch information
rustyrussell committed Aug 4, 2024
1 parent 5d38768 commit 439289b
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 3 deletions.
69 changes: 66 additions & 3 deletions plugins/askrene/askrene.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ static struct askrene *get_askrene(struct plugin *plugin)
return plugin_get_data(plugin, struct askrene);
}

static bool have_layer(const char **layers, const char *name)
{
for (size_t i = 0; i < tal_count(layers); i++) {
if (streq(layers[i], name))
return true;
}
return false;
}

/* JSON helpers */
static struct command_result *param_string_array(struct command *cmd,
const char *name,
Expand Down Expand Up @@ -165,6 +174,43 @@ static fp16_t *get_capacities(const tal_t *ctx,
return caps;
}

/* If we're the payer, we don't add delay or fee to our own outgoing
* channels. This wouldn't be right if we looped back through ourselves,
* but we won't. */
/* FIXME: We could cache this until gossmap changes... */
static void add_free_source(struct command *cmd,
struct gossmap *gossmap,
struct gossmap_localmods *localmods,
const struct node_id *source)
{
const struct gossmap_node *srcnode;

/* If we're not in map, we complain later */
srcnode = gossmap_find_node(gossmap, source);
if (!srcnode)
return;

for (size_t i = 0; i < srcnode->num_chans; i++) {
struct gossmap_chan *c;
int dir;
struct short_channel_id scid;

c = gossmap_nth_chan(gossmap, srcnode, i, &dir);
scid = gossmap_chan_scid(gossmap, c);
if (!gossmap_local_updatechan(localmods,
scid,
/* Keep min and max */
gossmap_chan_htlc_min(c, dir),
gossmap_chan_htlc_max(c, dir),
0, 0, 0,
/* Keep enabled flag */
c->half[dir].enabled,
dir))
plugin_err(cmd->plugin, "Could not zero fee on local %s",
fmt_short_channel_id(tmpctx, scid));
}
}

/* Returns an error message, or sets *routes */
static const char *get_routes(struct command *cmd,
const struct node_id *source,
Expand Down Expand Up @@ -215,6 +261,9 @@ static const char *get_routes(struct command *cmd,
layer_clear_overridden_capacities(l, askrene->gossmap, rq->capacities);
}

if (have_layer(layers, "auto.sourcefree"))
add_free_source(cmd, askrene->gossmap, localmods, source);

/* 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);
Expand Down Expand Up @@ -516,6 +565,20 @@ static struct command_result *json_askrene_unreserve(struct command *cmd,
return command_finished(cmd, response);
}

static struct command_result *param_layername(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
const char **str)
{
*str = tal_strndup(cmd, buffer + tok->start,
tok->end - tok->start);
if (strstarts(*str, "auto."))
return command_fail_badparam(cmd, name, buffer, tok,
"New layers cannot start with auto.");
return NULL;
}

static struct command_result *json_askrene_create_channel(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
Expand All @@ -533,7 +596,7 @@ static struct command_result *json_askrene_create_channel(struct command *cmd,
struct askrene *askrene = get_askrene(cmd->plugin);

if (!param_check(cmd, buffer, params,
p_req("layer", param_string, &layername),
p_req("layer", param_layername, &layername),
p_req("source", param_node_id, &src),
p_req("destination", param_node_id, &dst),
p_req("short_channel_id", param_short_channel_id, &scid),
Expand Down Expand Up @@ -586,7 +649,7 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd,
struct askrene *askrene = get_askrene(cmd->plugin);

if (!param_check(cmd, buffer, params,
p_req("layer", param_string, &layername),
p_req("layer", param_layername, &layername),
p_req("short_channel_id", param_short_channel_id, &scid),
p_req("direction", param_zero_or_one, &direction),
p_opt("minimum_msat", param_msat, &min),
Expand Down Expand Up @@ -633,7 +696,7 @@ static struct command_result *json_askrene_disable_node(struct command *cmd,
struct askrene *askrene = get_askrene(cmd->plugin);

if (!param(cmd, buffer, params,
p_req("layer", param_string, &layername),
p_req("layer", param_layername, &layername),
p_req("node", param_node_id, &node),
NULL))
return command_param_failed();
Expand Down
73 changes: 73 additions & 0 deletions tests/test_askrene.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,76 @@ def test_getroutes_fee_fallback(node_factory):
maxfee_msat=200,
paths=[[{'short_channel_id': '0x2x1'},
{'short_channel_id': '2x3x3'}]])


def test_getroutes_auto_sourcefree(node_factory):
"""Test getroutes call with auto.sourcefree layer"""
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=['auto.sourcefree'],
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': 1000,
'delay': 99}]}]}
# Two hop, still easy.
assert l1.rpc.getroutes(source=nodemap[0],
destination=nodemap[3],
amount_msat=100000,
layers=['auto.sourcefree'],
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': 102000,
'delay': 99 + 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)

0 comments on commit 439289b

Please sign in to comment.