diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index d602078afa60..075457c946a5 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -185,7 +186,8 @@ static void add_free_source(struct plugin *plugin, { const struct gossmap_node *srcnode; - /* If we're not in map, we complain later */ + /* If we're not in map, we complain later (unless we're purely + * using local channels) */ srcnode = gossmap_find_node(gossmap, source); if (!srcnode) return; @@ -221,6 +223,7 @@ static const char *get_routes(const tal_t *ctx, u32 finalcltv, const char **layers, struct gossmap_localmods *localmods, + const struct layer *local_layer, struct route ***routes, struct amount_msat **amounts, double *probability) @@ -233,6 +236,7 @@ static const char *get_routes(const tal_t *ctx, double base_fee_penalty; u32 prob_cost_factor, mu; const char *ret; + bool zero_cost; if (gossmap_refresh(askrene->gossmap, NULL)) { /* FIXME: gossmap_refresh callbacks to we can update in place */ @@ -246,21 +250,33 @@ static const char *get_routes(const tal_t *ctx, rq->layers = tal_arr(rq, const struct layer *, 0); rq->capacities = tal_dup_talarr(rq, fp16_t, askrene->capacities); + /* If we're told to zerocost local channels, then make sure that's done + * in local mods as well. */ + zero_cost = have_layer(layers, "auto.sourcefree") + && node_id_eq(source, &askrene->my_id); + /* Layers don't have to exist: they might be empty! */ for (size_t i = 0; i < tal_count(layers); i++) { - struct layer *l = find_layer(askrene, layers[i]); - if (!l) - continue; + const struct layer *l = find_layer(askrene, layers[i]); + if (!l) { + if (local_layer && streq(layers[i], "auto.localchans")) { + plugin_log(plugin, LOG_DBG, "Adding auto.localchans"); + l = local_layer; + } else + continue; + } tal_arr_expand(&rq->layers, l); /* FIXME: Implement localmods_merge, and cache this in layer? */ - layer_add_localmods(l, rq->gossmap, localmods); + layer_add_localmods(l, rq->gossmap, zero_cost, localmods); /* Clear any entries in capacities array if we * override them (incl local channels) */ layer_clear_overridden_capacities(l, askrene->gossmap, rq->capacities); } + /* This does not see local mods! If you add local channel in a layer, it won't + * have costs zeroed out here. */ if (have_layer(layers, "auto.sourcefree")) add_free_source(plugin, askrene->gossmap, localmods, source); @@ -460,6 +476,7 @@ struct getroutes_info { static struct command_result *do_getroutes(struct command *cmd, struct gossmap_localmods *localmods, + const struct layer *local_layer, const struct getroutes_info *info) { const char *err; @@ -471,7 +488,7 @@ static struct command_result *do_getroutes(struct command *cmd, err = get_routes(cmd, cmd->plugin, info->source, info->dest, *info->amount, *info->maxfee, *info->finalcltv, - info->layers, localmods, + info->layers, localmods, local_layer, &routes, &amounts, &probability); if (err) return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err); @@ -501,6 +518,55 @@ static struct command_result *do_getroutes(struct command *cmd, return command_finished(cmd, response); } +static void add_localchan(struct gossmap_localmods *mods, + const struct node_id *self, + const struct node_id *peer, + const struct short_channel_id_dir *scidd, + struct amount_msat htlcmin, + struct amount_msat htlcmax, + struct amount_msat spendable, + struct amount_msat fee_base, + u32 fee_proportional, + u32 cltv_delta, + bool enabled, + const char *buf UNUSED, + const jsmntok_t *chantok UNUSED, + struct layer *local_layer) +{ + gossmod_add_localchan(mods, self, peer, scidd, htlcmin, htlcmax, + spendable, fee_base, fee_proportional, cltv_delta, enabled, + buf, chantok, local_layer); + + /* Known capacity on local channels (ts = max) */ + layer_update_constraint(local_layer, scidd, CONSTRAINT_MIN, UINT64_MAX, spendable); + layer_update_constraint(local_layer, scidd, CONSTRAINT_MAX, UINT64_MAX, spendable); +} + +static struct command_result * +listpeerchannels_done(struct command *cmd, + const char *buffer, + const jsmntok_t *toks, + struct getroutes_info *info) +{ + struct layer *local_layer = new_temp_layer(info, "auto.localchans"); + struct gossmap_localmods *localmods; + bool zero_cost; + + /* If we're told to zerocost local channels, then make sure that's done + * in local mods as well. */ + zero_cost = have_layer(info->layers, "auto.sourcefree") + && node_id_eq(info->source, &get_askrene(cmd->plugin)->my_id); + + localmods = gossmods_from_listpeerchannels(cmd, + &get_askrene(cmd->plugin)->my_id, + buffer, toks, + zero_cost, + add_localchan, + local_layer); + + return do_getroutes(cmd, localmods, local_layer, info); +} + static struct command_result *json_getroutes(struct command *cmd, const char *buffer, const jsmntok_t *params) @@ -517,7 +583,17 @@ static struct command_result *json_getroutes(struct command *cmd, NULL)) return command_param_failed(); - return do_getroutes(cmd, gossmap_localmods_new(cmd), info); + if (have_layer(info->layers, "auto.localchans")) { + struct out_req *req; + + req = jsonrpc_request_start(cmd->plugin, cmd, + "listpeerchannels", + listpeerchannels_done, + forward_error, info); + return send_outreq(cmd->plugin, req); + } + + return do_getroutes(cmd, gossmap_localmods_new(cmd), NULL, info); } static struct command_result *json_askrene_reserve(struct command *cmd, @@ -823,6 +899,8 @@ static const char *init(struct plugin *plugin, plugin_err(plugin, "Could not load gossmap %s: %s", GOSSIP_STORE_FILENAME, strerror(errno)); askrene->capacities = get_capacities(askrene, askrene->plugin, askrene->gossmap); + rpc_scan(plugin, "getinfo", take(json_out_obj(NULL, NULL, NULL)), + "{id:%}", JSON_SCAN(json_to_node_id, &askrene->my_id)); plugin_set_data(plugin, askrene); plugin_set_memleak_handler(plugin, askrene_markmem); diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 693e70ca7cc0..76abd2eae081 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -5,6 +5,7 @@ #include #include #include +#include struct gossmap_chan; @@ -26,6 +27,8 @@ struct askrene { struct reserve_hash *reserved; /* Compact cache of gossmap capacities */ fp16_t *capacities; + /* My own id */ + struct node_id my_id; }; /* Information for a single route query. */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index ea1296bac5c1..3991716dc988 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -85,9 +85,9 @@ struct layer { struct node_id *disabled_nodes; }; -struct layer *new_layer(struct askrene *askrene, const char *name) +struct layer *new_temp_layer(const tal_t *ctx, const char *name) { - struct layer *l = tal(askrene, struct layer); + struct layer *l = tal(ctx, struct layer); l->name = tal_strdup(l, name); l->local_channels = tal(l, struct local_channel_hash); @@ -96,6 +96,12 @@ struct layer *new_layer(struct askrene *askrene, const char *name) constraint_hash_init(l->constraints); l->disabled_nodes = tal_arr(l, struct node_id, 0); + return l; +} + +struct layer *new_layer(struct askrene *askrene, const char *name) +{ + struct layer *l = new_temp_layer(askrene, name); list_add(&askrene->layers, &l->list); return l; } @@ -296,11 +302,12 @@ void layer_add_disabled_node(struct layer *layer, const struct node_id *node) tal_arr_expand(&layer->disabled_nodes, *node); } -void layer_add_localmods(struct layer *layer, +void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, + bool zero_cost, struct gossmap_localmods *localmods) { - struct local_channel *lc; + const struct local_channel *lc; struct local_channel_hash_iter lcit; /* First, disable all channels into blocked nodes (local updates @@ -337,14 +344,20 @@ void layer_add_localmods(struct layer *layer, gossmap_local_addchan(localmods, &lc->n1, &lc->n2, lc->scid, NULL); for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { + u64 base, propfee, delay; if (!lc->half[i].enabled) continue; + if (zero_cost) { + base = propfee = delay = 0; + } else { + base = lc->half[i].base_fee.millisatoshis; /* Raw: gossmap */ + propfee = lc->half[i].proportional_fee; + delay = lc->half[i].delay; + } gossmap_local_updatechan(localmods, lc->scid, lc->half[i].htlc_min, lc->half[i].htlc_max, - lc->half[i].base_fee.millisatoshis /* Raw: gossmap */, - lc->half[i].proportional_fee, - lc->half[i].delay, + base, propfee, delay, true, i); } diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 5fa45ffee7bd..7acea220c072 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -41,6 +41,9 @@ struct layer *find_layer(struct askrene *askrene, const char *name); /* Create new layer by name. */ struct layer *new_layer(struct askrene *askrene, const char *name); +/* New temporary layer (not in askrene's hash table) */ +struct layer *new_temp_layer(const tal_t *ctx, const char *name); + /* Get the name of the layer */ const char *layer_name(const struct layer *layer); @@ -87,9 +90,10 @@ const struct constraint *layer_update_constraint(struct layer *layer, u64 timestamp, struct amount_msat limit); -/* Add local channels from this layer */ -void layer_add_localmods(struct layer *layer, +/* Add local channels from this layer. zero_cost means set fees and delay to 0. */ +void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, + bool zero_cost, struct gossmap_localmods *localmods); /* Remove constraints older then cutoff: returns num removed. */ diff --git a/tests/test_askrene.py b/tests/test_askrene.py index aa00c41d13ba..a0e4f33038b0 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -362,3 +362,51 @@ def test_getroutes_auto_sourcefree(node_factory): layers=[], maxfee_msat=100, finalcltv=99) + + +def test_getroutes_auto_localchans(node_factory): + """Test getroutes call with auto.localchans layer""" + # We get bad signature warnings, since our gossip is made up! + l1, l2 = node_factory.get_nodes(2, opts={'allow_warning': True}) + gsfile, nodemap = generate_gossip_store([GenChannel(0, 1, forward=GenChannel.Half(propfee=10000)), + GenChannel(1, 2, forward=GenChannel.Half(propfee=10000))], + nodeids=[l2.info['id']]) + + # Set up l1 with this as the gossip_store + l1.stop() + shutil.copy(gsfile.name, os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store')) + l1.start() + + # Now l1 beleives l2 has an entire network behind it. + scid12, _ = l1.fundchannel(l2, 10**6, announce_channel=False) + + # Cannot find a route unless we use local hints. + with pytest.raises(RpcError, match="Unknown source node {}".format(l1.info['id'])): + l1.rpc.getroutes(source=l1.info['id'], + destination=nodemap[2], + amount_msat=100000, + layers=[], + maxfee_msat=100000, + finalcltv=99) + + # This should work + check_getroute_paths(l1, + l1.info['id'], + nodemap[2], + 100000, + maxfee_msat=100000, + layers=['auto.localchans'], + paths=[[{'short_channel_id': scid12, 'amount_msat': 102012, 'delay': 99 + 6 + 6 + 6}, + {'short_channel_id': '0x1x0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id': '1x2x1', 'amount_msat': 101000, 'delay': 99 + 6}]]) + + # This should get self-discount correct + check_getroute_paths(l1, + l1.info['id'], + nodemap[2], + 100000, + maxfee_msat=100000, + layers=['auto.localchans', 'auto.sourcefree'], + paths=[[{'short_channel_id': scid12, 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id': '0x1x0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id': '1x2x1', 'amount_msat': 101000, 'delay': 99 + 6}]])