diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index a9b6d1572ac4..d98e0e67dfd0 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -361,6 +361,62 @@ "Main web site: " ] }, + "lightning-askrene-disable-channel.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-disable-channel", + "title": "Command to disable a channel in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-disable-channel** RPC command tells askrene to disable a channel whenever the given layer is used. This is mainly useful to force the use of alternate paths." + ], + "request": { + "required": [ + "layer", + "short_channel_id" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The channel to disable." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction of the channel. If the direction is not specified then both directions are disabled." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, "lightning-askrene-disable-node.json": { "$schema": "../rpc-schema-draft.json", "type": "object", @@ -556,6 +612,7 @@ "required": [ "layer", "disabled_nodes", + "disabled_channels", "created_channels", "constraints" ], @@ -575,6 +632,15 @@ ] } }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The short channel id of the disabled channel." + ] + } + }, "created_channels": { "type": "array", "items": { diff --git a/doc/Makefile b/doc/Makefile index fd59c4182646..719b0527f2f7 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -8,6 +8,7 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-addpsbtoutput.7 \ doc/lightning-askrene-create-channel.7 \ doc/lightning-askrene-disable-node.7 \ + doc/lightning-askrene-disable-channel.7 \ doc/lightning-askrene-inform-channel.7 \ doc/lightning-askrene-listlayers.7 \ doc/lightning-askrene-reserve.7 \ diff --git a/doc/index.rst b/doc/index.rst index 5515229db141..75e3d863f6d1 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,6 +15,7 @@ Core Lightning Documentation lightning-addgossip lightning-addpsbtoutput lightning-askrene-create-channel + lightning-askrene-disable-channel lightning-askrene-disable-node lightning-askrene-inform-channel lightning-askrene-listlayers diff --git a/doc/schemas/lightning-askrene-disable-channel.json b/doc/schemas/lightning-askrene-disable-channel.json new file mode 100644 index 000000000000..84f3f43042f0 --- /dev/null +++ b/doc/schemas/lightning-askrene-disable-channel.json @@ -0,0 +1,56 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-disable-channel", + "title": "Command to disable a channel in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-disable-channel** RPC command tells askrene to disable a channel whenever the given layer is used. This is mainly useful to force the use of alternate paths." + ], + "request": { + "required": [ + "layer", + "short_channel_id" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The channel to disable." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction of the channel. If the direction is not specified then both directions are disabled." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-listlayers.json b/doc/schemas/lightning-askrene-listlayers.json index a0a5cba70511..d6cd487beae4 100644 --- a/doc/schemas/lightning-askrene-listlayers.json +++ b/doc/schemas/lightning-askrene-listlayers.json @@ -33,6 +33,7 @@ "required": [ "layer", "disabled_nodes", + "disabled_channels", "created_channels", "constraints" ], @@ -52,6 +53,15 @@ ] } }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The short channel id of the disabled channel." + ] + } + }, "created_channels": { "type": "array", "items": { diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 751b687416ef..6aa47e90e5d8 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -337,6 +337,12 @@ static const char *get_routes(const tal_t *ctx, gossmap_apply_localmods(askrene->gossmap, localmods); + rq->disabled = tal_get_disabled_bitmap(rq, rq); + if (!rq->disabled) { + ret = tal_fmt(ctx, "Failed creation of disabled bitmap."); + goto out; + } + srcnode = gossmap_find_node(askrene->gossmap, source); if (!srcnode) { ret = tal_fmt(ctx, "Unknown source node %s", fmt_node_id(tmpctx, source)); @@ -881,6 +887,44 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, return command_finished(cmd, response); } +static struct command_result *json_askrene_disable_channel(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct short_channel_id *scid; + int *direction; + const char *layername; + struct layer *layer; + struct json_stream *response; + struct askrene *askrene = get_askrene(cmd->plugin); + + if (!param(cmd, buffer, params, + p_req("layer", param_layername, &layername), + p_req("short_channel_id", param_short_channel_id, &scid), + p_opt("direction", param_zero_or_one, &direction), + NULL)) + return command_param_failed(); + + layer = find_layer(askrene, layername); + if (!layer) + layer = new_layer(askrene, layername); + + struct short_channel_id_dir scidd = {.scid = *scid}; + if (direction) { + scidd.dir = *direction; + layer_add_disabled_channel(layer, &scidd); + } else { + /* If no direction is provided we disable both. */ + scidd.dir = 0; + layer_add_disabled_channel(layer, &scidd); + scidd.dir = 1; + layer_add_disabled_channel(layer, &scidd); + } + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + static struct command_result *json_askrene_disable_node(struct command *cmd, const char *buffer, const jsmntok_t *params) @@ -983,6 +1027,10 @@ static const struct plugin_command commands[] = { "askrene-age", json_askrene_age, }, + { + "askrene-disable-channel", + json_askrene_disable_channel, + }, }; static void askrene_markmem(struct plugin *plugin, struct htable *memtable) diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 7fb432838c8f..892d18204e80 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -2,6 +2,7 @@ #define LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H #include "config.h" #include +#include #include #include #include @@ -41,6 +42,10 @@ struct route_query { /* This is *not* updated during a query! Has all layers applied. */ const struct gossmap *gossmap; + /* Bit is set for [idx*2+dir] if that channel is disabled or if it + * belongs to a disabled node.. */ + bitmap *disabled; + /* We need to take in-flight payments into account */ const struct reserve_htable *reserved; diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 3991716dc988..ae3c2446bcd1 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -81,8 +82,9 @@ struct layer { /* Additional info, indexed by scid+dir */ struct constraint_hash *constraints; - /* Nodes to completely disable (tal_arr) */ + /* Channels and nodes to disable (tal_arr). */ struct node_id *disabled_nodes; + struct short_channel_id_dir *disabled_chans; }; struct layer *new_temp_layer(const tal_t *ctx, const char *name) @@ -94,6 +96,7 @@ struct layer *new_temp_layer(const tal_t *ctx, const char *name) local_channel_hash_init(l->local_channels); l->constraints = tal(l, struct constraint_hash); constraint_hash_init(l->constraints); + l->disabled_chans = tal_arr(l, struct short_channel_id_dir, 0); l->disabled_nodes = tal_arr(l, struct node_id, 0); return l; @@ -302,6 +305,12 @@ void layer_add_disabled_node(struct layer *layer, const struct node_id *node) tal_arr_expand(&layer->disabled_nodes, *node); } +void layer_add_disabled_channel(struct layer *layer, + const struct short_channel_id_dir *scidd) +{ + tal_arr_expand(&layer->disabled_chans, *scidd); +} + void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, bool zero_cost, @@ -310,34 +319,6 @@ void layer_add_localmods(const struct layer *layer, const struct local_channel *lc; struct local_channel_hash_iter lcit; - /* First, disable all channels into blocked nodes (local updates - * can add new ones)! */ - for (size_t i = 0; i < tal_count(layer->disabled_nodes); i++) { - const struct gossmap_node *node; - - node = gossmap_find_node(gossmap, &layer->disabled_nodes[i]); - if (!node) - continue; - for (size_t n = 0; n < node->num_chans; n++) { - struct short_channel_id scid; - struct gossmap_chan *c; - int dir; - c = gossmap_nth_chan(gossmap, node, n, &dir); - scid = gossmap_chan_scid(gossmap, c); - - /* Disabled zero-capacity on incoming */ - gossmap_local_updatechan(localmods, - scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(0), - 0, - 0, - 0, - false, - !dir); - } - } - for (lc = local_channel_hash_first(layer->local_channels, &lcit); lc; lc = local_channel_hash_next(layer->local_channels, &lcit)) { @@ -426,6 +407,10 @@ static void json_add_layer(struct json_stream *js, for (size_t i = 0; i < tal_count(layer->disabled_nodes); i++) json_add_node_id(js, NULL, &layer->disabled_nodes[i]); json_array_end(js); + json_array_start(js, "disabled_channels"); + for (size_t i = 0; i < tal_count(layer->disabled_chans); i++) + json_add_short_channel_id_dir(js, NULL, layer->disabled_chans[i]); + json_array_end(js); json_array_start(js, "created_channels"); for (lc = local_channel_hash_first(layer->local_channels, &lcit); lc; @@ -473,3 +458,53 @@ void layer_memleak_mark(struct askrene *askrene, struct htable *memtable) memleak_scan_htable(memtable, &l->local_channels->raw); } } + +static void set_channel_bit(bitmap *bm, const struct gossmap *gossmap, + const struct short_channel_id_dir *scidd) +{ + const struct gossmap_chan *chan = + gossmap_find_chan(gossmap, &scidd->scid); + + if (!chan) + return; + + bitmap_set_bit(bm, gossmap_chan_idx(gossmap, chan) * 2 + scidd->dir); +} + +static void set_node_channels_bit(bitmap *bm, const struct gossmap *gossmap, + const struct node_id *node_id) +{ + const struct gossmap_node *node = gossmap_find_node(gossmap, node_id); + for (size_t k = 0; k < node->num_chans; k++) { + int half; + const struct gossmap_chan *chan = + gossmap_nth_chan(gossmap, node, k, &half); + + bitmap_set_bit(bm, gossmap_chan_idx(gossmap, chan) * 2 + half); + bitmap_set_bit(bm, gossmap_chan_idx(gossmap, chan) * 2 + !half); + } +} + +bitmap *tal_get_disabled_bitmap(const tal_t *ctx, struct route_query *rq) +{ + + bitmap *disabled = tal_arrz( + ctx, bitmap, 2 * BITMAP_NWORDS(gossmap_max_chan_idx(rq->gossmap))); + + if (!disabled) + return NULL; + + /* Disable every channel in the list of disabled scids. */ + for (size_t i = 0; i < tal_count(rq->layers); i++) { + const struct layer *l = rq->layers[i]; + + for (size_t j = 0; j < tal_count(l->disabled_chans); j++) + set_channel_bit(disabled, rq->gossmap, + &l->disabled_chans[j]); + + for (size_t j = 0; j < tal_count(l->disabled_nodes); j++) + set_node_channels_bit(disabled, rq->gossmap, + &l->disabled_nodes[j]); + } + return disabled; +} diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 7acea220c072..8ab655817090 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -102,6 +102,10 @@ size_t layer_trim_constraints(struct layer *layer, u64 cutoff); /* Add a disabled node to a layer. */ void layer_add_disabled_node(struct layer *layer, const struct node_id *node); +/* Add a disabled channel to a layer. */ +void layer_add_disabled_channel(struct layer *layer, + const struct short_channel_id_dir *scidd); + /* Print out a json object per layer, or all if layer is NULL */ void json_add_layers(struct json_stream *js, struct askrene *askrene, @@ -116,4 +120,9 @@ void json_add_constraint(struct json_stream *js, /* Scan for memleaks */ void layer_memleak_mark(struct askrene *askrene, struct htable *memtable); + +/* Creates a bitmap of disabled channels. It loops over all layers in the + * query searching for disabled channels and disabled nodes. All channels + * linked to a disabled node will be disabled. */ +bitmap *tal_get_disabled_bitmap(const tal_t *ctx, struct route_query *rq); #endif /* LIGHTNING_PLUGINS_ASKRENE_LAYER_H */ diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 18b62ab7570e..ee85c3753ecf 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -583,11 +583,22 @@ static s64 linear_fee_cost( return pfee + bfee* base_fee_penalty+ delay*delay_feefactor; } +static bool channel_is_available(const struct gossmap_chan *c, int dir, + const struct gossmap *gossmap, + const bitmap *disabled) +{ + if (!gossmap_chan_set(c, dir)) + return false; + const u32 chan_idx = gossmap_chan_idx(gossmap, c); + return !bitmap_test_bit(disabled, chan_idx * 2 + dir); +} + static struct linear_network * init_linear_network(const tal_t *ctx, const struct pay_parameters *params) { struct linear_network * linear_network = tal(ctx, struct linear_network); const struct gossmap *gossmap = params->rq->gossmap; + const bitmap *disabled = params->rq->disabled; const size_t max_num_chans = gossmap_max_chan_idx(gossmap); const size_t max_num_arcs = max_num_chans * ARCS_PER_CHANNEL; @@ -630,7 +641,7 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params) const struct gossmap_chan *c = gossmap_nth_chan(gossmap, node, j, &half); - if (!gossmap_chan_set(c, half)) + if (!channel_is_available(c, half, gossmap, disabled)) continue; const u32 chan_id = gossmap_chan_idx(gossmap, c); diff --git a/tests/test_askrene.py b/tests/test_askrene.py index a87ae3c6ec53..9a63d2b9dafd 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -17,6 +17,7 @@ def test_layers(node_factory): expect = {'layer': 'test_layers', 'disabled_nodes': [], + 'disabled_channels': [], 'created_channels': [], 'constraints': []} l2.rpc.askrene_disable_node('test_layers', l1.info['id']) @@ -25,6 +26,10 @@ def test_layers(node_factory): assert l2.rpc.askrene_listlayers() == {'layers': [expect]} assert l2.rpc.askrene_listlayers('test_layers2') == {'layers': []} + l2.rpc.askrene_disable_channel('test_layers', "1x2x3", 0) + expect['disabled_channels'].append("1x2x3/0") + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + # Tell it l3 connects to l1! l2.rpc.askrene_create_channel('test_layers', l3.info['id'], @@ -164,6 +169,19 @@ def test_getroutes(node_factory): # Set up l1 with this as the gossip_store l1 = node_factory.get_node(gossip_store_file=gsfile.name) + # Disabling channels makes getroutes fail + l1.rpc.askrene_disable_channel("chans_disabled", "0x1x0") + l1.rpc.askrene_disable_channel("chans_disabled", "0x2x1") + l1.rpc.askrene_disable_channel("chans_disabled", "0x2x3") + # we can also disable by mistake channels that do not exists + l1.rpc.askrene_disable_channel("chans_disabled", "111x222x333") + with pytest.raises(RpcError, match="Could not find route"): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000, + layers=["chans_disabled"], + maxfee_msat=1000, + final_cltv=99) # Start easy assert l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1],