From 2c2a4e60ecd5efb79696e0d1554013129783f994 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Fri, 20 Dec 2024 20:50:51 +0100 Subject: [PATCH] askrene: add expiration parameter to create-layer This could be useful if we want to create one-time use layers on the flight and not worry about memory leaks (one time used layers that are not removed constitute a memory leak in a sense). Changelog-Added: askrene: add an optional self-destruction timer (expiration parameter) to new layers. Signed-off-by: Lagrang3 --- .../lightning-askrene-create-layer.json | 7 ++++ plugins/askrene/askrene.c | 35 +++++++++++++++++++ tests/test_askrene.py | 25 +++++++++++++ 3 files changed, 67 insertions(+) diff --git a/doc/schemas/lightning-askrene-create-layer.json b/doc/schemas/lightning-askrene-create-layer.json index d0b650d5fddb..61fe8370a2d2 100644 --- a/doc/schemas/lightning-askrene-create-layer.json +++ b/doc/schemas/lightning-askrene-create-layer.json @@ -25,6 +25,13 @@ "description": [ "True if askrene should save and restore this layer. As a side-effect, create-layer also succeeds if the layer already exists and persistent is true." ] + }, + "expiration": { + "type": "u64", + "description": [ + "Sets the number of seconds until this layer expires. If this number is specified the layer will be automatically deleted at expiration. A layer cannot be persistent and have an expiration time." + ], + "added": "v25.02" } } }, diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 413eedddb8c1..d499426881f2 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -623,6 +624,7 @@ static struct command_result *do_getroutes(struct command *cmd, *info->amount, *info->maxfee, *info->finalcltv, info->layers, localmods, info->local_layer, &routes, &amounts, info->additional_costs, &probability); + if (err) return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err); @@ -1060,6 +1062,27 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, return command_finished(cmd, response); } +static struct command_result *expire_layer_done(struct command *timer_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + void *unused) +{ + return timer_complete(timer_cmd); +} + +static struct command_result *expire_layer(struct command *timer_cmd, + struct layer *l) +{ + struct out_req *req; + req = jsonrpc_request_start(timer_cmd, "askrene-remove-layer", + expire_layer_done, plugin_broken_cb, NULL); + json_add_string(req->js, "layer", layer_name(l)); + plugin_log(timer_cmd->plugin, LOG_DBG, "removing expired layer '%s'", + layer_name(l)); + return send_outreq(req); +} + static struct command_result *json_askrene_create_layer(struct command *cmd, const char *buffer, const jsmntok_t *params) @@ -1069,10 +1092,12 @@ static struct command_result *json_askrene_create_layer(struct command *cmd, const char *layername; struct json_stream *response; bool *persistent; + u64 *expire_seconds; if (!param_check(cmd, buffer, params, p_req("layer", param_string, &layername), p_opt_def("persistent", param_bool, &persistent, false), + p_opt("expiration", param_u64, &expire_seconds), NULL)) return command_param_failed(); @@ -1086,6 +1111,11 @@ static struct command_result *json_askrene_create_layer(struct command *cmd, return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Layer already exists"); } + if (persistent && expire_seconds && *persistent == true) { + return command_fail( + cmd, JSONRPC2_INVALID_PARAMS, + "A persistent layer cannot have an expiration time."); + } if (command_check_only(cmd)) return command_check_done(cmd); @@ -1093,6 +1123,11 @@ static struct command_result *json_askrene_create_layer(struct command *cmd, if (!layer) layer = new_layer(askrene, layername, *persistent); + if (expire_seconds) + notleak(global_timer(cmd->plugin, + time_from_sec(*expire_seconds), + expire_layer, layer)); + response = jsonrpc_stream_success(cmd); json_add_layers(response, askrene, "layers", layer); return command_finished(cmd, response); diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 580215a70653..231e81b5dfc1 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -367,6 +367,31 @@ def test_layer_persistence(node_factory): assert l1.rpc.askrene_listlayers() == {'layers': []} +def test_layer_expiration(node_factory): + l1 = node_factory.get_nodes(1, opts={"disable-plugin": "cln-xpay"})[0] + expect = { + "layer": "tmp_layer", + "persistent": False, + "disabled_nodes": [], + "created_channels": [], + "channel_updates": [], + "constraints": [], + "biases": [], + } + + assert l1.rpc.askrene_listlayers() == {"layers": []} + # Add a self-destructing layer + l1.rpc.askrene_create_layer(layer="tmp_layer", expiration=5) + assert l1.rpc.askrene_listlayers() == {"layers": [expect]} + time.sleep(6) + assert l1.rpc.askrene_listlayers() == {"layers": []} + + with pytest.raises( + RpcError, match="A persistent layer cannot have an expiration time." + ): + l1.rpc.askrene_create_layer(layer="pers_layer", persistent=True, expiration=5) + + def check_route_as_expected(routes, paths): """Make sure all fields in paths are match those in routes""" def dict_subset_eq(a, b):