From b88f4cb854741a2cd686e972ce91da8b71662173 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:53:53 +0930 Subject: [PATCH] askrene: askrene-create-layer and askrene-remove-layer. It's generally better to be explicit with these things: currently typos would be ignored. But it's also much easier to clean up entire layers as we use them for temporary (per-payment) effects. Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 238 +++++++++++++++++- doc/Makefile | 2 + doc/index.rst | 2 + .../lightning-askrene-create-channel.json | 2 +- .../lightning-askrene-create-layer.json | 195 ++++++++++++++ .../lightning-askrene-inform-channel.json | 2 +- .../lightning-askrene-remove-layer.json | 39 +++ plugins/askrene/askrene.c | 159 +++++++----- plugins/askrene/layer.c | 10 +- plugins/askrene/layer.h | 4 +- tests/test_askrene.py | 20 +- 11 files changed, 599 insertions(+), 74 deletions(-) create mode 100644 doc/schemas/lightning-askrene-create-layer.json create mode 100644 doc/schemas/lightning-askrene-remove-layer.json diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 6caadaf47..c04ab364f 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -265,7 +265,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden. If the layer does not exist, it will be created." + "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden." ], "request": { "required": [ @@ -361,6 +361,201 @@ "Main web site: " ] }, + "lightning-askrene-create-layer.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-create-layer", + "title": "Command to create a new layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-create-layer** RPC command tells askrene to create a new, empty layer. This layer can then be populated with `askrene-create-channel` and `askrene-inform-channel`, and be used in `getroutes`." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to create." + ] + } + } + }, + "response": { + "required": [ + "layers" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "layer", + "disabled_nodes", + "disabled_channels", + "created_channels", + "constraints" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer." + ] + }, + "disabled_nodes": { + "type": "array", + "items": { + "type": "pubkey", + "description": [ + "The id of the disabled node." + ] + } + }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, + "created_channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "destination", + "short_channel_id", + "capacity_msat", + "htlc_minimum_msat", + "htlc_maximum_msat", + "fee_base_msat", + "fee_proportional_millionths", + "delay" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + }, + "htlc_minimum_msat": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_maximum_msat": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "fee_base_msat": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "fee_proportional_millionths": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + } + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id", + "direction" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + } + } + } + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-remove-layer(7)", + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, "lightning-askrene-disable-channel.json": { "$schema": "../rpc-schema-draft.json", "type": "object", @@ -469,7 +664,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the curren channel exists or not. If the layer does not exist, it will be created." + "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the current channel exists or not." ], "request": { "required": [ @@ -742,6 +937,45 @@ "Main web site: " ] }, + "lightning-askrene-remove-layer.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-remove-layer", + "title": "Command to destroy a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-remove-layer** RPC command tells askrene to forget a layer." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to remove." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-askrene-create-layer(7)", + "lightning-askrene-listlayers(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, "lightning-askrene-reserve.json": { "$schema": "../rpc-schema-draft.json", "type": "object", diff --git a/doc/Makefile b/doc/Makefile index fd59c4182..9e01a4382 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -6,6 +6,8 @@ doc-wrongdir: GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-addpsbtoutput.7 \ + doc/lightning-askrene-create-layer.7 \ + doc/lightning-askrene-remove-layer.7 \ doc/lightning-askrene-create-channel.7 \ doc/lightning-askrene-disable-node.7 \ doc/lightning-askrene-inform-channel.7 \ diff --git a/doc/index.rst b/doc/index.rst index 5515229db..682dee3e9 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,9 +15,11 @@ Core Lightning Documentation lightning-addgossip lightning-addpsbtoutput lightning-askrene-create-channel + lightning-askrene-create-layer lightning-askrene-disable-node lightning-askrene-inform-channel lightning-askrene-listlayers + lightning-askrene-remove-layer lightning-askrene-reserve lightning-askrene-unreserve lightning-autoclean-once diff --git a/doc/schemas/lightning-askrene-create-channel.json b/doc/schemas/lightning-askrene-create-channel.json index ff43fdf7e..3e76313aa 100644 --- a/doc/schemas/lightning-askrene-create-channel.json +++ b/doc/schemas/lightning-askrene-create-channel.json @@ -7,7 +7,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden. If the layer does not exist, it will be created." + "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden." ], "request": { "required": [ diff --git a/doc/schemas/lightning-askrene-create-layer.json b/doc/schemas/lightning-askrene-create-layer.json new file mode 100644 index 000000000..00518b4e4 --- /dev/null +++ b/doc/schemas/lightning-askrene-create-layer.json @@ -0,0 +1,195 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-create-layer", + "title": "Command to create a new layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-create-layer** RPC command tells askrene to create a new, empty layer. This layer can then be populated with `askrene-create-channel` and `askrene-inform-channel`, and be used in `getroutes`." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to create." + ] + } + } + }, + "response": { + "required": [ + "layers" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "layer", + "disabled_nodes", + "disabled_channels", + "created_channels", + "constraints" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer." + ] + }, + "disabled_nodes": { + "type": "array", + "items": { + "type": "pubkey", + "description": [ + "The id of the disabled node." + ] + } + }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, + "created_channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "destination", + "short_channel_id", + "capacity_msat", + "htlc_minimum_msat", + "htlc_maximum_msat", + "fee_base_msat", + "fee_proportional_millionths", + "delay" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + }, + "htlc_minimum_msat": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_maximum_msat": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "fee_base_msat": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "fee_proportional_millionths": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + } + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id", + "direction" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + } + } + } + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-remove-layer(7)", + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(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-inform-channel.json b/doc/schemas/lightning-askrene-inform-channel.json index 7deb95b77..9d63efe60 100644 --- a/doc/schemas/lightning-askrene-inform-channel.json +++ b/doc/schemas/lightning-askrene-inform-channel.json @@ -7,7 +7,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the curren channel exists or not. If the layer does not exist, it will be created." + "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the current channel exists or not." ], "request": { "required": [ diff --git a/doc/schemas/lightning-askrene-remove-layer.json b/doc/schemas/lightning-askrene-remove-layer.json new file mode 100644 index 000000000..541cf9529 --- /dev/null +++ b/doc/schemas/lightning-askrene-remove-layer.json @@ -0,0 +1,39 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-remove-layer", + "title": "Command to destroy a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-remove-layer** RPC command tells askrene to forget a layer." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to remove." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-askrene-create-layer(7)", + "lightning-askrene-listlayers(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 97d1060d0..289430118 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -62,24 +62,35 @@ static bool have_layer(const char **layers, const char *name) return false; } -/* JSON helpers */ -static struct command_result *param_string_array(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - const char ***arr) +/* Valid, known layers */ +static struct command_result *param_layer_names(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + const char ***arr) { size_t i; const jsmntok_t *t; if (tok->type != JSMN_ARRAY) - return command_fail_badparam(cmd, name, buffer, tok, "should be an array"); + return command_fail_badparam(cmd, name, buffer, tok, + "should be an array"); *arr = tal_arr(cmd, const char *, tok->size); json_for_each_arr(i, t, tok) { if (t->type != JSMN_STRING) - return command_fail_badparam(cmd, name, buffer, t, "should be a string"); + return command_fail_badparam(cmd, name, buffer, t, + "should be a string"); (*arr)[i] = json_strdup(*arr, buffer, t); + + /* Must be a known layer name */ + if (streq((*arr)[i], "auto.localchans") + || streq((*arr)[i], "auto.sourcefree")) + continue; + if (!find_layer(get_askrene(cmd->plugin), (*arr)[i])) { + return command_fail_badparam(cmd, name, buffer, t, + "unknown layer"); + } } return NULL; } @@ -285,11 +296,14 @@ static const char *get_routes(const tal_t *ctx, for (size_t i = 0; i < tal_count(layers); i++) { const struct layer *l = find_layer(askrene, layers[i]); if (!l) { - if (local_layer && streq(layers[i], "auto.localchans")) { + if (streq(layers[i], "auto.localchans")) { plugin_log(plugin, LOG_DBG, "Adding auto.localchans"); l = local_layer; - } else + } else { + /* Handled below, after other layers */ + assert(streq(layers[i], "auto.sourcefree")); continue; + } } tal_arr_expand(&rq->layers, l); @@ -652,7 +666,7 @@ static struct command_result *json_getroutes(struct command *cmd, p_req("source", param_node_id, &info->source), p_req("destination", param_node_id, &info->dest), p_req("amount_msat", param_msat, &info->amount), - p_req("layers", param_string_array, &info->layers), + p_req("layers", param_layer_names, &info->layers), p_req("maxfee_msat", param_msat, &info->maxfee), p_req("final_cltv", param_u32, &info->finalcltv), NULL)) @@ -737,25 +751,10 @@ 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) { - const char *layername; struct layer *layer; const struct local_channel *lc; struct node_id *src, *dst; @@ -765,10 +764,9 @@ static struct command_result *json_askrene_create_channel(struct command *cmd, struct amount_msat *htlc_min, *htlc_max, *base_fee; u32 *proportional_fee; u16 *delay; - struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, - p_req("layer", param_layername, &layername), + p_req("layer", param_known_layer, &layer), p_req("source", param_node_id, &src), p_req("destination", param_node_id, &dst), p_req("short_channel_id", param_short_channel_id, &scid), @@ -782,22 +780,15 @@ static struct command_result *json_askrene_create_channel(struct command *cmd, return command_param_failed(); /* If it exists, it must match */ - layer = find_layer(askrene, layername); - if (layer) { - lc = layer_find_local_channel(layer, *scid); - if (lc && !layer_check_local_channel(lc, src, dst, *capacity)) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "channel already exists with different values!"); - } - } else - lc = NULL; + lc = layer_find_local_channel(layer, *scid); + if (lc && !layer_check_local_channel(lc, src, dst, *capacity)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "channel already exists with different values!"); + } if (command_check_only(cmd)) return command_check_done(cmd); - if (!layer) - layer = new_layer(askrene, layername); - layer_update_local_channel(layer, src, dst, *scid, *capacity, *base_fee, *proportional_fee, *delay, *htlc_min, *htlc_max); @@ -811,15 +802,13 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, const jsmntok_t *params) { struct layer *layer; - const char *layername; struct short_channel_id_dir *scidd; struct json_stream *response; struct amount_msat *max, *min; const struct constraint *c; - struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, - p_req("layer", param_layername, &layername), + p_req("layer", param_known_layer, &layer), p_req("short_channel_id_dir", param_short_channel_id_dir, &scidd), p_opt("minimum_msat", param_msat, &min), p_opt("maximum_msat", param_msat, &max), @@ -834,10 +823,6 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, if (command_check_only(cmd)) return command_check_done(cmd); - layer = find_layer(askrene, layername); - if (!layer) - layer = new_layer(askrene, layername); - if (min) { c = layer_update_constraint(layer, scidd, CONSTRAINT_MIN, time_now().ts.tv_sec, *min); @@ -855,21 +840,15 @@ static struct command_result *json_askrene_disable_channel(struct command *cmd, const jsmntok_t *params) { struct short_channel_id_dir *scidd; - 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("layer", param_known_layer, &layer), p_req("short_channel_id_dir", param_short_channel_id_dir, &scidd), NULL)) return command_param_failed(); - layer = find_layer(askrene, layername); - if (!layer) - layer = new_layer(askrene, layername); - layer_add_disabled_channel(layer, scidd); response = jsonrpc_stream_success(cmd); @@ -881,21 +860,15 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, const jsmntok_t *params) { struct node_id *node; - 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("layer", param_known_layer, &layer), p_req("node", param_node_id, &node), NULL)) return command_param_failed(); - layer = find_layer(askrene, layername); - if (!layer) - layer = new_layer(askrene, layername); - /* We save this in the layer, because they want us to disable all the channels * to the node at *use* time (a new channel might be gossiped!). */ layer_add_disabled_node(layer, node); @@ -904,21 +877,71 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, return command_finished(cmd, response); } +static struct command_result *json_askrene_create_layer(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct askrene *askrene = get_askrene(cmd->plugin); + struct layer *layer; + const char *layername; + struct json_stream *response; + + if (!param_check(cmd, buffer, params, + p_req("layer", param_string, &layername), + NULL)) + return command_param_failed(); + + if (find_layer(askrene, layername)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Layer already exists"); + + if (strstarts(layername, "auto.")) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Cannot create auto layer"); + + if (command_check_only(cmd)) + return command_check_done(cmd); + + layer = new_layer(askrene, layername); + + response = jsonrpc_stream_success(cmd); + json_add_layers(response, askrene, "layers", layer); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_remove_layer(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct layer *layer; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("layer", param_known_layer, &layer), + NULL)) + return command_param_failed(); + + tal_free(layer); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + static struct command_result *json_askrene_listlayers(struct command *cmd, const char *buffer, const jsmntok_t *params) { struct askrene *askrene = get_askrene(cmd->plugin); - const char *layername; + struct layer *layer; struct json_stream *response; if (!param(cmd, buffer, params, - p_opt("layer", param_string, &layername), + p_opt("layer", param_known_layer, &layer), NULL)) return command_param_failed(); response = jsonrpc_stream_success(cmd); - json_add_layers(response, askrene, "layers", layername); + json_add_layers(response, askrene, "layers", layer); return command_finished(cmd, response); } @@ -970,6 +993,14 @@ static const struct plugin_command commands[] = { "askrene-inform-channel", json_askrene_inform_channel, }, + { + "askrene-create-layer", + json_askrene_create_layer, + }, + { + "askrene-remove-layer", + json_askrene_remove_layer, + }, { "askrene-listlayers", json_askrene_listlayers, diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index b2dd00bea..2ae5b59c1 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -103,10 +103,16 @@ struct layer *new_temp_layer(const tal_t *ctx, const char *name) return l; } +static void destroy_layer(struct layer *l, struct askrene *askrene) +{ + list_del_from(&askrene->layers, &l->list); +} + struct layer *new_layer(struct askrene *askrene, const char *name) { struct layer *l = new_temp_layer(askrene, name); list_add(&askrene->layers, &l->list); + tal_add_destructor2(l, destroy_layer, askrene); return l; } @@ -477,13 +483,13 @@ static void json_add_layer(struct json_stream *js, void json_add_layers(struct json_stream *js, struct askrene *askrene, const char *fieldname, - const char *layername) + const struct layer *layer) { struct layer *l; json_array_start(js, fieldname); list_for_each(&askrene->layers, l, list) { - if (layername && !streq(l->name, layername)) + if (layer && l != layer) continue; json_add_layer(js, NULL, l); } diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 5503644ae..8867ba701 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -106,11 +106,11 @@ void layer_add_disabled_node(struct layer *layer, const struct node_id *node); 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 */ +/* Print out a json object for this layer, or all if layer is NULL */ void json_add_layers(struct json_stream *js, struct askrene *askrene, const char *fieldname, - const char *layername); + const struct layer *layer); /* Print a single constraint */ void json_add_constraint(struct json_stream *js, diff --git a/tests/test_askrene.py b/tests/test_askrene.py index e7380fe04..b71d37af0 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -20,22 +20,27 @@ def test_layers(node_factory): l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) assert l2.rpc.askrene_listlayers() == {'layers': []} - assert l2.rpc.askrene_listlayers('test_layers') == {'layers': []} + with pytest.raises(RpcError, match="Unknown layer"): + l2.rpc.askrene_listlayers('test_layers') expect = {'layer': 'test_layers', 'disabled_nodes': [], 'disabled_channels': [], 'created_channels': [], 'constraints': []} + l2.rpc.askrene_create_layer('test_layers') l2.rpc.askrene_disable_node('test_layers', l1.info['id']) expect['disabled_nodes'].append(l1.info['id']) assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} assert l2.rpc.askrene_listlayers() == {'layers': [expect]} - assert l2.rpc.askrene_listlayers('test_layers2') == {'layers': []} + with pytest.raises(RpcError, match="Unknown layer"): + l2.rpc.askrene_listlayers('test_layers2') l2.rpc.askrene_disable_channel('test_layers', "1x2x3/0") expect['disabled_channels'].append("1x2x3/0") assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + with pytest.raises(RpcError, match="Layer already exists"): + l2.rpc.askrene_create_layer('test_layers') # Tell it l3 connects to l1! l2.rpc.askrene_create_channel('test_layers', @@ -114,6 +119,12 @@ def test_layers(node_factory): listlayers = l2.rpc.askrene_listlayers('test_layers') assert listlayers == {'layers': [expect]} + with pytest.raises(RpcError, match="Unknown layer"): + l2.rpc.askrene_remove_layer('test_layers_unknown') + + assert l2.rpc.askrene_remove_layer('test_layers') == {} + assert l2.rpc.askrene_listlayers() == {'layers': []} + def check_route_as_expected(routes, paths): """Make sure all fields in paths are match those in routes""" @@ -173,6 +184,7 @@ def test_getroutes(node_factory): l1 = node_factory.get_node(gossip_store_file=gsfile.name) # Disabling channels makes getroutes fail + l1.rpc.askrene_create_layer('chans_disabled') l1.rpc.askrene_disable_channel("chans_disabled", '0x1x0/1') with pytest.raises(RpcError, match="Could not find route"): l1.rpc.getroutes(source=nodemap[0], @@ -435,6 +447,7 @@ def test_fees_dont_exceed_constraints(node_factory): l1 = node_factory.get_node(gossip_store_file=gsfile.name) chan = only_one([c for c in l1.rpc.listchannels(source=nodemap[0])['channels'] if c['destination'] == nodemap[1]]) + l1.rpc.askrene_create_layer('test_layers') l1.rpc.askrene_inform_channel(layer='test_layers', short_channel_id_dir=f"{chan['short_channel_id']}/{chan['direction']}", maximum_msat=max_msat) @@ -460,6 +473,7 @@ def test_sourcefree_on_mods(node_factory, bitcoind): l1 = node_factory.get_node(gossip_store_file=gsfile.name) # Add a local channel from 0->l1 (we just needed a nodeid). + l1.rpc.askrene_create_layer('test_layers') l1.rpc.askrene_create_channel('test_layers', nodemap[0], l1.info['id'], @@ -580,6 +594,7 @@ def test_limits_fake_gossmap(node_factory, bitcoind): for scidd in spendable: assert scidd in [f"{c['short_channel_id']}/{c['direction']}" for c in l1.rpc.listchannels(source=nodemap[0])['channels']] + l1.rpc.askrene_create_layer('localchans') for scidd, amount in spendable.items(): l1.rpc.askrene_inform_channel(layer='localchans', short_channel_id_dir=scidd, @@ -640,6 +655,7 @@ def test_max_htlc(node_factory, bitcoind): [{'short_channel_id_dir': '0x1x1/1', 'amount_msat': 19_000_019, 'delay': 10 + 6}]]) # If we can't use channel 2, we fail. + l1.rpc.askrene_create_layer('removechan2') l1.rpc.askrene_inform_channel(layer='removechan2', short_channel_id_dir='0x1x1/1', maximum_msat=0)