From e88e05115c84bd2a1849ba7d218ea7939bd0c21e Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:48:53 +0930 Subject: [PATCH 01/24] gossmap: don't crash on localmods on non-existant channels. We allow adding them, but crash when we remove the localmods. Yet this could theoretically happen if a channel we modified was removed from the gossmap, anyway. Reported-by: Lagrang3 Signed-off-by: Rusty Russell --- common/gossmap.c | 4 ++++ common/test/run-gossmap_local.c | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/common/gossmap.c b/common/gossmap.c index 068a25c41566..5427c5e1cd4d 100644 --- a/common/gossmap.c +++ b/common/gossmap.c @@ -980,6 +980,10 @@ void gossmap_remove_localmods(struct gossmap *map, const struct localmod *mod = &localmods->mods[i]; struct gossmap_chan *chan = gossmap_find_chan(map, &mod->scid); + /* If there was no channel, ignore */ + if (!chan) + continue; + /* If that's a local channel, remove it now. */ if (chan->cann_off >= map->map_size) { gossmap_remove_chan(map, chan); diff --git a/common/test/run-gossmap_local.c b/common/test/run-gossmap_local.c index 4debfc47c418..376e4025e12f 100644 --- a/common/test/run-gossmap_local.c +++ b/common/test/run-gossmap_local.c @@ -333,7 +333,7 @@ int main(int argc, char *argv[]) char *gossfile; struct gossmap *map; struct node_id l1, l2, l3, l4; - struct short_channel_id scid23, scid12, scid_local; + struct short_channel_id scid23, scid12, scid_local, scid_nonexisting; struct gossmap_chan *chan; struct gossmap_localmods *mods; struct amount_sat capacity; @@ -498,6 +498,13 @@ int main(int argc, char *argv[]) AMOUNT_MSAT(100), 101, 102, 103, true, 0); + /* We can "update" a channel which doesn't exist, and it's a noop */ + scid_nonexisting.u64 = 1; + gossmap_local_updatechan(mods, scid_nonexisting, + AMOUNT_MSAT(1), + AMOUNT_MSAT(100000), + 2, 3, 4, false, 0); + gossmap_apply_localmods(map, mods); chan = gossmap_find_chan(map, &scid_local); assert(gossmap_chan_set(chan, 0)); @@ -510,6 +517,8 @@ int main(int argc, char *argv[]) assert(chan->half[0].proportional_fee == 3); assert(chan->half[0].delay == 4); + assert(!gossmap_find_chan(map, &scid_nonexisting)); + chan = gossmap_find_chan(map, &scid23); assert(chan->half[0].enabled); assert(chan->half[0].htlc_min == u64_to_fp16(99, false)); From 8146177ba469cd8d4173f64cc4647df236d33dbd Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:49:53 +0930 Subject: [PATCH 02/24] askrene: add support for disabled channels in layers. Based-on-the-patch-by: Lagrang3 Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 10 +++++++ doc/schemas/lightning-askrene-listlayers.json | 10 +++++++ plugins/askrene/layer.c | 26 +++++++++++++++++++ plugins/askrene/layer.h | 4 +++ 4 files changed, 50 insertions(+) diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index a9b6d1572ac4..1dead107c5c6 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -556,6 +556,7 @@ "required": [ "layer", "disabled_nodes", + "disabled_channels", "created_channels", "constraints" ], @@ -575,6 +576,15 @@ ] } }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, "created_channels": { "type": "array", "items": { diff --git a/doc/schemas/lightning-askrene-listlayers.json b/doc/schemas/lightning-askrene-listlayers.json index a0a5cba70511..20d579539e71 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 channel and direction which is disabled." + ] + } + }, "created_channels": { "type": "array", "items": { diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 3991716dc988..9e8efe54cea4 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -83,6 +83,9 @@ struct layer { /* Nodes to completely disable (tal_arr) */ struct node_id *disabled_nodes; + + /* Channels to completely disable (tal_arr) */ + struct short_channel_id_dir *disabled_channels; }; struct layer *new_temp_layer(const tal_t *ctx, const char *name) @@ -95,6 +98,7 @@ struct layer *new_temp_layer(const tal_t *ctx, const char *name) l->constraints = tal(l, struct constraint_hash); constraint_hash_init(l->constraints); l->disabled_nodes = tal_arr(l, struct node_id, 0); + l->disabled_channels = tal_arr(l, struct short_channel_id_dir, 0); return l; } @@ -302,6 +306,11 @@ 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_channels, *scidd); +} + void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, bool zero_cost, @@ -362,6 +371,19 @@ void layer_add_localmods(const struct layer *layer, i); } } + + /* Now disable any channels they asked us to */ + for (size_t i = 0; i < tal_count(layer->disabled_channels); i++) { + gossmap_local_updatechan(localmods, + layer->disabled_channels[i].scid, + AMOUNT_MSAT(0), + AMOUNT_MSAT(0), + 0, + 0, + 0, + false, + layer->disabled_channels[i].dir); + } } static void json_add_local_channel(struct json_stream *response, @@ -426,6 +448,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_channels); i++) + json_add_short_channel_id_dir(js, NULL, layer->disabled_channels[i]); + json_array_end(js); json_array_start(js, "created_channels"); for (lc = local_channel_hash_first(layer->local_channels, &lcit); lc; diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 7acea220c072..5503644ae0c7 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, From a657d158670df84ee9b1195629fae59afdca5fa8 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:50:53 +0930 Subject: [PATCH 03/24] common: round out the short_channel_id_dir JSON routines. Signed-off-by: Rusty Russell --- common/json_param.c | 13 +++++++++++++ common/json_param.h | 6 ++++++ common/json_parse.c | 15 ++++++++++++--- common/json_parse.h | 7 +++++++ common/test/run-json_filter.c | 4 ++++ common/test/run-json_remove.c | 4 ++++ plugins/askrene/askrene.c | 11 ----------- 7 files changed, 46 insertions(+), 14 deletions(-) diff --git a/common/json_param.c b/common/json_param.c index 3a25e1684b2f..216451271803 100644 --- a/common/json_param.c +++ b/common/json_param.c @@ -642,6 +642,19 @@ struct command_result *param_short_channel_id(struct command *cmd, "should be a short_channel_id of form NxNxN"); } +struct command_result *param_short_channel_id_dir(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct short_channel_id_dir **scidd) +{ + *scidd = tal(cmd, struct short_channel_id_dir); + if (!json_to_short_channel_id_dir(buffer, tok, *scidd)) + return command_fail_badparam(cmd, name, buffer, tok, + "should be a short_channel_id_dir of form NxNxN/N"); + return NULL; +} + struct command_result *param_secret(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, struct secret **secret) diff --git a/common/json_param.h b/common/json_param.h index 83e4f3b3a70a..07db22f3d2cf 100644 --- a/common/json_param.h +++ b/common/json_param.h @@ -275,6 +275,12 @@ struct command_result *param_short_channel_id(struct command *cmd, const jsmntok_t *tok, struct short_channel_id **scid); +struct command_result *param_short_channel_id_dir(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct short_channel_id_dir **scidd); + /* Ignore the token. Not usually used. */ struct command_result *param_ignore(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, diff --git a/common/json_parse.c b/common/json_parse.c index 639c35b2e70b..1d272b1f0c6c 100644 --- a/common/json_parse.c +++ b/common/json_parse.c @@ -109,6 +109,17 @@ bool json_to_int(const char *buffer, const jsmntok_t *tok, int *num) return true; } +bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num) +{ + u32 v32; + if (!json_to_u32(buffer, tok, &v32)) + return false; + if (v32 != 0 && v32 != 1) + return false; + *num = v32; + return true; +} + bool json_to_jsonrpc_errcode(const char *buffer, const jsmntok_t *tok, enum jsonrpc_errcode *errcode) { @@ -577,7 +588,6 @@ bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok, struct short_channel_id_dir *scidd) { jsmntok_t scidtok, numtok; - u32 dir; if (!split_tok(buffer, tok, '/', &scidtok, &numtok)) return false; @@ -585,10 +595,9 @@ bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok, if (!json_to_short_channel_id(buffer, &scidtok, &scidd->scid)) return false; - if (!json_to_u32(buffer, &numtok, &dir) || (dir > 1)) + if (!json_to_zero_or_one(buffer, &numtok, &scidd->dir)) return false; - scidd->dir = dir; return true; } diff --git a/common/json_parse.h b/common/json_parse.h index 7eca7284c2d6..4bd23cbddcd7 100644 --- a/common/json_parse.h +++ b/common/json_parse.h @@ -31,6 +31,9 @@ u8 *json_tok_bin_from_hex(const tal_t *ctx, const char *buffer, const jsmntok_t bool json_to_number(const char *buffer, const jsmntok_t *tok, unsigned int *num); +/* Extract 0/1 from this */ +bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num); + /* Extract signed 64 bit integer from this (may be a string, or a number literal) */ bool json_to_s64(const char *buffer, const jsmntok_t *tok, s64 *num); @@ -86,6 +89,10 @@ bool json_to_bitcoin_amount(const char *buffer, const jsmntok_t *tok, bool json_to_short_channel_id(const char *buffer, const jsmntok_t *tok, struct short_channel_id *scid); +/* Extract a short_channel_id_dir from this */ +bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok, + struct short_channel_id_dir *scidd); + /* Extract a satoshis amount from this */ bool json_to_sat(const char *buffer, const jsmntok_t *tok, struct amount_sat *sat); diff --git a/common/test/run-json_filter.c b/common/test/run-json_filter.c index d4e68801a637..54138cbe6999 100644 --- a/common/test/run-json_filter.c +++ b/common/test/run-json_filter.c @@ -120,6 +120,10 @@ bool json_to_pubkey(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, bool json_to_short_channel_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct short_channel_id *scid UNNEEDED) { fprintf(stderr, "json_to_short_channel_id called!\n"); abort(); } +/* Generated stub for json_to_short_channel_id_dir */ +bool json_to_short_channel_id_dir(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct short_channel_id_dir *scidd UNNEEDED) +{ fprintf(stderr, "json_to_short_channel_id_dir called!\n"); abort(); } /* Generated stub for json_to_txid */ bool json_to_txid(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct bitcoin_txid *txid UNNEEDED) diff --git a/common/test/run-json_remove.c b/common/test/run-json_remove.c index 93c844143415..f1c16a28cbe4 100644 --- a/common/test/run-json_remove.c +++ b/common/test/run-json_remove.c @@ -146,6 +146,10 @@ bool json_to_pubkey(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, bool json_to_short_channel_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct short_channel_id *scid UNNEEDED) { fprintf(stderr, "json_to_short_channel_id called!\n"); abort(); } +/* Generated stub for json_to_short_channel_id_dir */ +bool json_to_short_channel_id_dir(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct short_channel_id_dir *scidd UNNEEDED) +{ fprintf(stderr, "json_to_short_channel_id_dir called!\n"); abort(); } /* Generated stub for json_to_txid */ bool json_to_txid(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct bitcoin_txid *txid UNNEEDED) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 751b687416ef..3b79da24c943 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -102,17 +102,6 @@ static struct command_result *param_known_layer(struct command *cmd, return NULL; } -static bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num) -{ - u32 v32; - if (!json_to_u32(buffer, tok, &v32)) - return false; - if (v32 != 0 && v32 != 1) - return false; - *num = v32; - return true; -} - static struct command_result *param_zero_or_one(struct command *cmd, const char *name, const char *buffer, From 1a76fb9cb12b832357a2e66416f97cb23ff7416f Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Fri, 4 Oct 2024 08:51:53 +0930 Subject: [PATCH 04/24] add askrene-disable-channel Changelog-EXPERIMENTAL: askrene: add askrene-disable-channel RPC Signed-off-by: Lagrang3 --- contrib/msggen/msggen/schema.json | 50 +++++++++++++++++++ .../lightning-askrene-disable-channel.json | 50 +++++++++++++++++++ plugins/askrene/askrene.c | 30 +++++++++++ tests/test_askrene.py | 19 +++++++ 4 files changed, 149 insertions(+) create mode 100644 doc/schemas/lightning-askrene-disable-channel.json diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 1dead107c5c6..08f23654c7c7 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -361,6 +361,56 @@ "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_dir" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction to disable." + ] + } + } + }, + "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", diff --git a/doc/schemas/lightning-askrene-disable-channel.json b/doc/schemas/lightning-askrene-disable-channel.json new file mode 100644 index 000000000000..09d5b2c4fd0b --- /dev/null +++ b/doc/schemas/lightning-askrene-disable-channel.json @@ -0,0 +1,50 @@ +{ + "$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_dir" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction to disable." + ] + } + } + }, + "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/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 3b79da24c943..ade22c2f6aed 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -870,6 +870,32 @@ 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_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("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); + return command_finished(cmd, response); +} + static struct command_result *json_askrene_disable_node(struct command *cmd, const char *buffer, const jsmntok_t *params) @@ -972,6 +998,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/tests/test_askrene.py b/tests/test_askrene.py index a87ae3c6ec53..2af1834f437b 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,20 @@ def test_getroutes(node_factory): # Set up l1 with this as the gossip_store l1 = node_factory.get_node(gossip_store_file=gsfile.name) + def direction(nodemap, src, dst): + if nodemap[src] < nodemap[dst]: + return 0 + return 1 + + # Disabling channels makes getroutes fail + l1.rpc.askrene_disable_channel("chans_disabled", f"0x1x0/{direction(nodemap, 0, 1)}") + 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], From c6350f9341586cae4cb6384eaba06eaf53bd1bac Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:52:53 +0930 Subject: [PATCH 05/24] askrene: use short_channel_id_dir in API. It's generally much more convenient, and it's already present in other APIs. Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 90 ++++---------- .../lightning-askrene-inform-channel.json | 30 ++--- doc/schemas/lightning-askrene-listlayers.json | 15 +-- doc/schemas/lightning-askrene-reserve.json | 15 +-- doc/schemas/lightning-askrene-unreserve.json | 15 +-- doc/schemas/lightning-getroutes.json | 15 +-- plugins/askrene/askrene.c | 40 ++---- plugins/askrene/layer.c | 3 +- tests/test_askrene.py | 114 ++++++++---------- 9 files changed, 109 insertions(+), 228 deletions(-) diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 08f23654c7c7..6caadaf47cb7 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -474,8 +474,7 @@ "request": { "required": [ "layer", - "short_channel_id", - "direction" + "short_channel_id_dir" ], "properties": { "layer": { @@ -484,16 +483,10 @@ "The name of the layer to apply this change to." ] }, - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The short channel id to apply this change to." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The direction to apply this change to." + "The short channel id and direction to apply this change to." ] }, "minimum_msat": { @@ -518,21 +511,14 @@ "constraint": { "type": "object", "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "timestamp" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The *short_channel_id* specified." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The *direction* specified." + "The *short_channel_id* and *direction* specified." ] }, "timestamp": { @@ -713,20 +699,13 @@ "items": { "type": "object", "required": [ - "short_channel_id", - "direction" + "short_channel_id_dir" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The short channel id." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The direction." + "The short channel id and direction" ] }, "maximum_msat": { @@ -787,21 +766,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -859,21 +831,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -14435,23 +14400,16 @@ "type": "object", "additionalProperties": false, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "next_node_id", "amount_msat", "delay" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { diff --git a/doc/schemas/lightning-askrene-inform-channel.json b/doc/schemas/lightning-askrene-inform-channel.json index 8ba0f90f62e3..7deb95b77785 100644 --- a/doc/schemas/lightning-askrene-inform-channel.json +++ b/doc/schemas/lightning-askrene-inform-channel.json @@ -12,8 +12,7 @@ "request": { "required": [ "layer", - "short_channel_id", - "direction" + "short_channel_id_dir" ], "properties": { "layer": { @@ -22,16 +21,10 @@ "The name of the layer to apply this change to." ] }, - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The short channel id to apply this change to." - ] - }, - "direction": { - "type": "u32", - "description": [ - "The direction to apply this change to." + "The short channel id and direction to apply this change to." ] }, "minimum_msat": { @@ -56,21 +49,14 @@ "constraint": { "type": "object", "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "timestamp" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The *short_channel_id* specified." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The *direction* specified." + "The *short_channel_id* and *direction* specified." ] }, "timestamp": { diff --git a/doc/schemas/lightning-askrene-listlayers.json b/doc/schemas/lightning-askrene-listlayers.json index 20d579539e71..7b6eed74d029 100644 --- a/doc/schemas/lightning-askrene-listlayers.json +++ b/doc/schemas/lightning-askrene-listlayers.json @@ -140,20 +140,13 @@ "items": { "type": "object", "required": [ - "short_channel_id", - "direction" + "short_channel_id_dir" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The short channel id." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The direction." + "The short channel id and direction" ] }, "maximum_msat": { diff --git a/doc/schemas/lightning-askrene-reserve.json b/doc/schemas/lightning-askrene-reserve.json index 7b91e3800998..070920021f20 100644 --- a/doc/schemas/lightning-askrene-reserve.json +++ b/doc/schemas/lightning-askrene-reserve.json @@ -22,21 +22,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { diff --git a/doc/schemas/lightning-askrene-unreserve.json b/doc/schemas/lightning-askrene-unreserve.json index 377595a5caa5..eeadf8058fb3 100644 --- a/doc/schemas/lightning-askrene-unreserve.json +++ b/doc/schemas/lightning-askrene-unreserve.json @@ -22,21 +22,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { diff --git a/doc/schemas/lightning-getroutes.json b/doc/schemas/lightning-getroutes.json index 5b09bda28c57..aeaeb2216b8a 100644 --- a/doc/schemas/lightning-getroutes.json +++ b/doc/schemas/lightning-getroutes.json @@ -121,23 +121,16 @@ "type": "object", "additionalProperties": false, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "next_node_id", "amount_msat", "delay" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index ade22c2f6aed..97d1060d0978 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -102,20 +102,6 @@ static struct command_result *param_known_layer(struct command *cmd, return NULL; } -static struct command_result *param_zero_or_one(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - int **num) -{ - *num = tal(cmd, int); - if (json_to_zero_or_one(buffer, tok, *num)) - return NULL; - - return command_fail_badparam(cmd, name, buffer, tok, - "should be 0 or 1"); -} - struct reserve_path { struct short_channel_id_dir *scidds; struct amount_msat *amounts; @@ -130,9 +116,8 @@ static struct command_result *parse_reserve_path(struct command *cmd, { const char *err; - err = json_scan(tmpctx, buffer, tok, "{short_channel_id:%,direction:%,amount_msat:%}", - JSON_SCAN(json_to_short_channel_id, &scidd->scid), - JSON_SCAN(json_to_zero_or_one, &scidd->dir), + err = json_scan(tmpctx, buffer, tok, "{short_channel_id_dir:%,amount_msat:%}", + JSON_SCAN(json_to_short_channel_id_dir, scidd), JSON_SCAN(json_to_msat, amount)); if (err) return command_fail_badparam(cmd, name, buffer, tok, err); @@ -553,10 +538,12 @@ static struct command_result *do_getroutes(struct command *cmd, json_add_u32(response, "final_cltv", *info->finalcltv); json_array_start(response, "path"); for (size_t j = 0; j < tal_count(routes[i]->hops); j++) { + struct short_channel_id_dir scidd; const struct route_hop *r = &routes[i]->hops[j]; json_object_start(response, NULL); - json_add_short_channel_id(response, "short_channel_id", r->scid); - json_add_u32(response, "direction", r->direction); + scidd.scid = r->scid; + scidd.dir = r->direction; + json_add_short_channel_id_dir(response, "short_channel_id_dir", scidd); json_add_node_id(response, "next_node_id", &r->node_id); json_add_amount_msat(response, "amount_msat", r->amount); json_add_u32(response, "delay", r->delay); @@ -825,18 +812,15 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, { struct layer *layer; const char *layername; - struct short_channel_id *scid; - int *direction; + struct short_channel_id_dir *scidd; struct json_stream *response; struct amount_msat *max, *min; const struct constraint *c; - struct short_channel_id_dir scidd; struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, 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_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), NULL)) @@ -854,15 +838,11 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, if (!layer) layer = new_layer(askrene, layername); - /* Calls expect a convenient short_channel_id_dir struct */ - scidd.scid = *scid; - scidd.dir = *direction; - if (min) { - c = layer_update_constraint(layer, &scidd, CONSTRAINT_MIN, + c = layer_update_constraint(layer, scidd, CONSTRAINT_MIN, time_now().ts.tv_sec, *min); } else { - c = layer_update_constraint(layer, &scidd, CONSTRAINT_MAX, + c = layer_update_constraint(layer, scidd, CONSTRAINT_MAX, time_now().ts.tv_sec, *max); } response = jsonrpc_stream_success(cmd); diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 9e8efe54cea4..b2dd00bead5b 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -419,8 +419,7 @@ void json_add_constraint(struct json_stream *js, json_object_start(js, fieldname); if (layer) json_add_string(js, "layer", layer->name); - json_add_short_channel_id(js, "short_channel_id", c->key.scidd.scid); - json_add_u32(js, "direction", c->key.scidd.dir); + json_add_short_channel_id_dir(js, "short_channel_id_dir", c->key.scidd); json_add_u64(js, "timestamp", c->timestamp); switch (c->key.type) { case CONSTRAINT_MIN: diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 2af1834f437b..e7380fe046e8 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -8,6 +8,13 @@ import time +def direction(src, dst): + """BOLT 7 direction: 0 means from lesser encoded id""" + if src < dst: + return 0 + return 1 + + def test_layers(node_factory): """Test manipulating information in layers""" l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) @@ -52,12 +59,10 @@ def test_layers(node_factory): # We can tell it about made up channels... first_timestamp = int(time.time()) l2.rpc.askrene_inform_channel('test_layers', - '0x0x1', - 1, + '0x0x1/1', 100000) last_timestamp = int(time.time()) + 1 - expect['constraints'].append({'short_channel_id': '0x0x1', - 'direction': 1, + expect['constraints'].append({'short_channel_id_dir': '0x0x1/1', 'minimum_msat': 100000}) # Check timestamp first. listlayers = l2.rpc.askrene_listlayers('test_layers') @@ -72,25 +77,23 @@ def test_layers(node_factory): # We can tell it about existing channels... scid12 = first_scid(l1, l2) first_timestamp = int(time.time()) + scid12dir = f"{scid12}/{direction(l2.info['id'], l1.info['id'])}" l2.rpc.askrene_inform_channel(layer='test_layers', - short_channel_id=scid12, - # This is l2 -> l1 - direction=0, + short_channel_id_dir=scid12dir, maximum_msat=12341234) last_timestamp = int(time.time()) + 1 - expect['constraints'].append({'short_channel_id': scid12, - 'direction': 0, + expect['constraints'].append({'short_channel_id_dir': scid12dir, 'timestamp': first_timestamp, 'maximum_msat': 12341234}) # Check timestamp first. listlayers = l2.rpc.askrene_listlayers('test_layers') - ts2 = only_one([c['timestamp'] for c in only_one(listlayers['layers'])['constraints'] if c['short_channel_id'] == scid12]) + ts2 = only_one([c['timestamp'] for c in only_one(listlayers['layers'])['constraints'] if c['short_channel_id_dir'] == scid12dir]) assert first_timestamp <= ts2 <= last_timestamp expect['constraints'][1]['timestamp'] = ts2 # Could be either order! actual = expect.copy() - if only_one(listlayers['layers'])['constraints'][0]['short_channel_id'] == scid12: + if only_one(listlayers['layers'])['constraints'][0]['short_channel_id_dir'] == scid12dir: actual['constraints'] = [expect['constraints'][1], expect['constraints'][0]] assert listlayers == {'layers': [actual]} @@ -169,13 +172,8 @@ def test_getroutes(node_factory): # Set up l1 with this as the gossip_store l1 = node_factory.get_node(gossip_store_file=gsfile.name) - def direction(nodemap, src, dst): - if nodemap[src] < nodemap[dst]: - return 0 - return 1 - # Disabling channels makes getroutes fail - l1.rpc.askrene_disable_channel("chans_disabled", f"0x1x0/{direction(nodemap, 0, 1)}") + l1.rpc.askrene_disable_channel("chans_disabled", '0x1x0/1') with pytest.raises(RpcError, match="Could not find route"): l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], @@ -193,8 +191,7 @@ def direction(nodemap, src, dst): 'routes': [{'probability_ppm': 999999, 'final_cltv': 99, 'amount_msat': 1000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 1010, 'delay': 99 + 6}]}]} @@ -208,13 +205,11 @@ def direction(nodemap, src, dst): 'routes': [{'probability_ppm': 999798, 'final_cltv': 99, 'amount_msat': 100000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 103020, 'delay': 99 + 6 + 6}, - {'short_channel_id': '1x3x2', - 'direction': 1, + {'short_channel_id_dir': '1x3x2/1', 'next_node_id': nodemap[3], 'amount_msat': 102000, 'delay': 99 + 6} @@ -254,8 +249,7 @@ def direction(nodemap, src, dst): 'routes': [{'probability_ppm': 900000, 'final_cltv': 99, 'amount_msat': 1000000, - 'path': [{'short_channel_id': '0x2x3', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x2x3/1', 'next_node_id': nodemap[2], 'amount_msat': 1000001, 'delay': 99 + 6}]}]} @@ -265,11 +259,11 @@ def direction(nodemap, src, dst): nodemap[0], nodemap[2], 10000000, - [[{'short_channel_id': '0x2x1', + [[{'short_channel_id_dir': '0x2x1/1', 'next_node_id': nodemap[2], 'amount_msat': 500000, 'delay': 99 + 6}], - [{'short_channel_id': '0x2x3', + [{'short_channel_id_dir': '0x2x3/1', 'next_node_id': nodemap[2], 'amount_msat': 9500009, 'delay': 99 + 6}]]) @@ -299,8 +293,8 @@ def test_getroutes_fee_fallback(node_factory): nodemap[3], 10000, maxfee_msat=201, - paths=[[{'short_channel_id': '0x1x0'}, - {'short_channel_id': '1x3x2'}]]) + paths=[[{'short_channel_id_dir': '0x1x0/1'}, + {'short_channel_id_dir': '1x3x2/1'}]]) # maxfee exceeded? lower prob path. check_getroute_paths(l1, @@ -308,8 +302,8 @@ def test_getroutes_fee_fallback(node_factory): nodemap[3], 10000, maxfee_msat=200, - paths=[[{'short_channel_id': '0x2x1'}, - {'short_channel_id': '2x3x3'}]]) + paths=[[{'short_channel_id_dir': '0x2x1/1'}, + {'short_channel_id_dir': '2x3x3/0'}]]) def test_getroutes_auto_sourcefree(node_factory): @@ -333,8 +327,7 @@ def test_getroutes_auto_sourcefree(node_factory): 'routes': [{'probability_ppm': 999999, 'final_cltv': 99, 'amount_msat': 1000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 1000, 'delay': 99}]}]} @@ -348,13 +341,11 @@ def test_getroutes_auto_sourcefree(node_factory): 'routes': [{'probability_ppm': 999798, 'final_cltv': 99, 'amount_msat': 100000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 102000, 'delay': 99 + 6}, - {'short_channel_id': '1x3x2', - 'direction': 1, + {'short_channel_id_dir': '1x3x2/1', 'next_node_id': nodemap[3], 'amount_msat': 102000, 'delay': 99 + 6} @@ -408,15 +399,16 @@ def test_getroutes_auto_localchans(node_factory): final_cltv=99) # This should work + scid21dir = f"{scid12}/{direction(l2.info['id'], l1.info['id'])}" check_getroute_paths(l2, l2.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}]]) + paths=[[{'short_channel_id_dir': scid21dir, 'amount_msat': 102012, 'delay': 99 + 6 + 6 + 6}, + {'short_channel_id_dir': '0x1x0/0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id_dir': '1x2x1/1', 'amount_msat': 101000, 'delay': 99 + 6}]]) # This should get self-discount correct check_getroute_paths(l2, @@ -425,9 +417,9 @@ def test_getroutes_auto_localchans(node_factory): 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}]]) + paths=[[{'short_channel_id_dir': scid21dir, 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id_dir': '0x1x0/0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id_dir': '1x2x1/1', 'amount_msat': 101000, 'delay': 99 + 6}]]) def test_fees_dont_exceed_constraints(node_factory): @@ -444,8 +436,7 @@ def test_fees_dont_exceed_constraints(node_factory): chan = only_one([c for c in l1.rpc.listchannels(source=nodemap[0])['channels'] if c['destination'] == nodemap[1]]) l1.rpc.askrene_inform_channel(layer='test_layers', - short_channel_id=chan['short_channel_id'], - direction=chan['direction'], + short_channel_id_dir=f"{chan['short_channel_id']}/{chan['direction']}", maximum_msat=max_msat) routes = l1.rpc.getroutes(source=nodemap[0], @@ -456,7 +447,7 @@ def test_fees_dont_exceed_constraints(node_factory): final_cltv=99)['routes'] assert len(routes) == 2 for hop in routes[0]['path'] + routes[1]['path']: - if hop['short_channel_id'] == chan['short_channel_id']: + if hop['short_channel_id_dir'] == f"{chan['short_channel_id']}/{chan['direction']}": amount = hop['amount_msat'] assert amount <= max_msat @@ -483,7 +474,7 @@ def test_sourcefree_on_mods(node_factory, bitcoind): maxfee_msat=100000, final_cltv=99)['routes'] # Expect no fee. - check_route_as_expected(routes, [[{'short_channel_id': '0x3x3', + check_route_as_expected(routes, [[{'short_channel_id_dir': '0x3x3/1', 'amount_msat': 1000000, 'delay': 99}]]) # Same if we specify layers in the other order! @@ -494,7 +485,7 @@ def test_sourcefree_on_mods(node_factory, bitcoind): maxfee_msat=100000, final_cltv=99)['routes'] # Expect no fee. - check_route_as_expected(routes, [[{'short_channel_id': '0x3x3', + check_route_as_expected(routes, [[{'short_channel_id_dir': '0x3x3/1', 'amount_msat': 1000000, 'delay': 99}]]) @@ -536,9 +527,7 @@ def test_live_spendable(node_factory, bitcoind): path_total = {} num_htlcs = {} for r in routes["routes"]: - key = "{}/{}".format( - r["path"][0]["short_channel_id"], r["path"][0]["direction"] - ) + key = r["path"][0]["short_channel_id_dir"] path_total[key] = path_total.get(key, 0) + r["path"][0]["amount_msat"] num_htlcs[key] = num_htlcs.get(key, 0) + 1 @@ -557,9 +546,9 @@ def test_live_spendable(node_factory, bitcoind): # No duplicate paths! for i in range(0, len(routes["routes"])): - path_i = [(p['short_channel_id'], p['direction']) for p in routes["routes"][i]['path']] + path_i = [p['short_channel_id_dir'] for p in routes["routes"][i]['path']] for j in range(i + 1, len(routes["routes"])): - path_j = [(p['short_channel_id'], p['direction']) for p in routes["routes"][j]['path']] + path_j = [p['short_channel_id_dir'] for p in routes["routes"][j]['path']] assert path_i != path_j # Must deliver exact amount. @@ -592,12 +581,11 @@ def test_limits_fake_gossmap(node_factory, bitcoind): assert scidd in [f"{c['short_channel_id']}/{c['direction']}" for c in l1.rpc.listchannels(source=nodemap[0])['channels']] for scidd, amount in spendable.items(): - chan, direction = scidd.split('/') l1.rpc.askrene_inform_channel(layer='localchans', - short_channel_id=chan, direction=int(direction), + short_channel_id_dir=scidd, minimum_msat=amount) l1.rpc.askrene_inform_channel(layer='localchans', - short_channel_id=chan, direction=int(direction), + short_channel_id_dir=scidd, maximum_msat=amount) routes = l1.rpc.getroutes( @@ -611,9 +599,7 @@ def test_limits_fake_gossmap(node_factory, bitcoind): path_total = {} for r in routes["routes"]: - key = "{}/{}".format( - r["path"][0]["short_channel_id"], r["path"][0]["direction"] - ) + key = r["path"][0]["short_channel_id_dir"] path_total[key] = path_total.get(key, 0) + r["path"][0]["amount_msat"] exceeded = {} @@ -626,9 +612,9 @@ def test_limits_fake_gossmap(node_factory, bitcoind): # No duplicate paths! for i in range(0, len(routes["routes"])): - path_i = [(p['short_channel_id'], p['direction']) for p in routes["routes"][i]['path']] + path_i = [p['short_channel_id_dir'] for p in routes["routes"][i]['path']] for j in range(i + 1, len(routes["routes"])): - path_j = [(p['short_channel_id'], p['direction']) for p in routes["routes"][j]['path']] + path_j = [p['short_channel_id_dir'] for p in routes["routes"][j]['path']] assert path_i != path_j # Must deliver exact amount. @@ -650,12 +636,12 @@ def test_max_htlc(node_factory, bitcoind): final_cltv=10) check_route_as_expected(routes['routes'], - [[{'short_channel_id': '0x1x0', 'amount_msat': 1_000_001, 'delay': 10 + 6}], - [{'short_channel_id': '0x1x1', 'amount_msat': 19_000_019, 'delay': 10 + 6}]]) + [[{'short_channel_id_dir': '0x1x0/1', 'amount_msat': 1_000_001, 'delay': 10 + 6}], + [{'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_inform_channel(layer='removechan2', - short_channel_id='0x1x1', direction=1, + short_channel_id_dir='0x1x1/1', maximum_msat=0) # FIXME: Better diag! From 1cc30c387a694f669046f03dc91fb18505f2a651 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:53:53 +0930 Subject: [PATCH 06/24] 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 6caadaf47cb7..c04ab364fb46 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 fd59c4182646..9e01a4382ac0 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 5515229db141..682dee3e909c 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 ff43fdf7eb99..3e76313aaf58 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 000000000000..00518b4e4bb6 --- /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 7deb95b77785..9d63efe60ce1 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 000000000000..541cf9529002 --- /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 97d1060d0978..289430118519 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 b2dd00bead5b..2ae5b59c1276 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 5503644ae0c7..8867ba701138 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 e7380fe046e8..b71d37af0c0e 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) From c099b2dcbc3b585638e683d62e322f8278535736 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:54:53 +0930 Subject: [PATCH 07/24] askrene: make `route_query` contain pointer to the command. This is important for errors and feedback. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 17 +++++++++-------- plugins/askrene/askrene.h | 3 +++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 289430118519..4115675c3d5a 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -256,7 +256,7 @@ struct amount_msat get_additional_per_htlc_cost(const struct route_query *rq, /* Returns an error message, or sets *routes */ static const char *get_routes(const tal_t *ctx, - struct plugin *plugin, + struct command *cmd, const struct node_id *source, const struct node_id *dest, struct amount_msat amount, @@ -270,7 +270,7 @@ static const char *get_routes(const tal_t *ctx, const struct additional_cost_htable *additional_costs, double *probability) { - struct askrene *askrene = get_askrene(plugin); + struct askrene *askrene = get_askrene(cmd->plugin); struct route_query *rq = tal(ctx, struct route_query); struct flow **flows; const struct gossmap_node *srcnode, *dstnode; @@ -285,7 +285,8 @@ static const char *get_routes(const tal_t *ctx, askrene->capacities = get_capacities(askrene, askrene->plugin, askrene->gossmap); } - rq->plugin = plugin; + rq->cmd = cmd; + rq->plugin = cmd->plugin; rq->gossmap = askrene->gossmap; rq->reserved = askrene->reserved; rq->layers = tal_arr(rq, const struct layer *, 0); @@ -297,7 +298,7 @@ static const char *get_routes(const tal_t *ctx, const struct layer *l = find_layer(askrene, layers[i]); if (!l) { if (streq(layers[i], "auto.localchans")) { - plugin_log(plugin, LOG_DBG, "Adding auto.localchans"); + plugin_log(rq->plugin, LOG_DBG, "Adding auto.localchans"); l = local_layer; } else { /* Handled below, after other layers */ @@ -317,7 +318,7 @@ static const char *get_routes(const tal_t *ctx, /* This also looks into localmods, to zero them */ if (have_layer(layers, "auto.sourcefree")) - add_free_source(plugin, askrene->gossmap, localmods, source); + add_free_source(rq->plugin, askrene->gossmap, localmods, source); /* Clear scids with reservations, too, so we don't have to look up * all the time! */ @@ -383,7 +384,7 @@ static const char *get_routes(const tal_t *ctx, } /* Too expensive? */ - while (amount_msat_greater(flowset_fee(plugin, flows), maxfee)) { + while (amount_msat_greater(flowset_fee(rq->plugin, flows), maxfee)) { mu += 10; flows = minflow(rq, rq, srcnode, dstnode, amount, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); @@ -428,7 +429,7 @@ static const char *get_routes(const tal_t *ctx, const struct half_chan *h = flow_edge(flows[i], j); if (!amount_msat_add_fee(&msat, h->base_fee, h->proportional_fee)) - plugin_err(plugin, "Adding fee to amount"); + plugin_err(rq->plugin, "Adding fee to amount"); delay += h->delay; rh->scid = gossmap_chan_scid(rq->gossmap, flows[i]->path[j]); @@ -534,7 +535,7 @@ static struct command_result *do_getroutes(struct command *cmd, struct route **routes; struct json_stream *response; - err = get_routes(cmd, cmd->plugin, + err = get_routes(cmd, cmd, info->source, info->dest, *info->amount, *info->maxfee, *info->finalcltv, info->layers, localmods, info->local_layer, diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 7fb432838c8f..29c2271242fd 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -35,6 +35,9 @@ struct askrene { /* Information for a single route query. */ struct route_query { + /* Command pointer, mainly for command id. */ + struct command *cmd; + /* Plugin pointer, for logging mainly */ struct plugin *plugin; From eda4bdc3292a2fd9c9f5dc25ca752b2cf093e186 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:55:53 +0930 Subject: [PATCH 08/24] askrene: clean up reserve array handling. I got confused, as we had a struct containing two arrays. Simply expose the reserve_hop struct and use arrays directly. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 56 +++++++++++++++------------------------ plugins/askrene/refine.c | 48 ++++++++++++++------------------- plugins/askrene/reserve.c | 20 +++++++------- plugins/askrene/reserve.h | 12 ++++++--- 4 files changed, 59 insertions(+), 77 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 4115675c3d5a..9b319a88c7e7 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -113,23 +113,17 @@ static struct command_result *param_known_layer(struct command *cmd, return NULL; } -struct reserve_path { - struct short_channel_id_dir *scidds; - struct amount_msat *amounts; -}; - -static struct command_result *parse_reserve_path(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - struct short_channel_id_dir *scidd, - struct amount_msat *amount) +static struct command_result *parse_reserve_hop(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct reserve_hop *rhop) { const char *err; err = json_scan(tmpctx, buffer, tok, "{short_channel_id_dir:%,amount_msat:%}", - JSON_SCAN(json_to_short_channel_id_dir, scidd), - JSON_SCAN(json_to_msat, amount)); + JSON_SCAN(json_to_short_channel_id_dir, &rhop->scidd), + JSON_SCAN(json_to_msat, &rhop->amount)); if (err) return command_fail_badparam(cmd, name, buffer, tok, err); return NULL; @@ -139,7 +133,7 @@ static struct command_result *param_reserve_path(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, - struct reserve_path **path) + struct reserve_hop **path) { size_t i; const jsmntok_t *t; @@ -147,15 +141,11 @@ static struct command_result *param_reserve_path(struct command *cmd, if (tok->type != JSMN_ARRAY) return command_fail_badparam(cmd, name, buffer, tok, "should be an array"); - *path = tal(cmd, struct reserve_path); - (*path)->scidds = tal_arr(cmd, struct short_channel_id_dir, tok->size); - (*path)->amounts = tal_arr(cmd, struct amount_msat, tok->size); + *path = tal_arr(cmd, struct reserve_hop, tok->size); json_for_each_arr(i, t, tok) { struct command_result *ret; - ret = parse_reserve_path(cmd, name, buffer, t, - &(*path)->scidds[i], - &(*path)->amounts[i]); + ret = parse_reserve_hop(cmd, name, buffer, t, &(*path)[i]); if (ret) return ret; } @@ -695,7 +685,7 @@ static struct command_result *json_askrene_reserve(struct command *cmd, const char *buffer, const jsmntok_t *params) { - struct reserve_path *path; + struct reserve_hop *path; struct json_stream *response; size_t num; struct askrene *askrene = get_askrene(cmd->plugin); @@ -705,15 +695,14 @@ static struct command_result *json_askrene_reserve(struct command *cmd, NULL)) return command_param_failed(); - num = reserves_add(askrene->reserved, path->scidds, path->amounts, - tal_count(path->scidds)); - if (num != tal_count(path->scidds)) { - const struct reserve *r = find_reserve(askrene->reserved, &path->scidds[num]); + num = reserves_add(askrene->reserved, path, tal_count(path)); + if (num != tal_count(path)) { + const struct reserve *r = find_reserve(askrene->reserved, &path[num].scidd); return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Overflow reserving %zu: %s amount %s (%s reserved already)", num, - fmt_short_channel_id_dir(tmpctx, &path->scidds[num]), - fmt_amount_msat(tmpctx, path->amounts[num]), + fmt_short_channel_id_dir(tmpctx, &path[num].scidd), + fmt_amount_msat(tmpctx, path[num].amount), r ? fmt_amount_msat(tmpctx, r->amount) : "none"); } @@ -725,7 +714,7 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, const char *buffer, const jsmntok_t *params) { - struct reserve_path *path; + struct reserve_hop *path; struct json_stream *response; size_t num; struct askrene *askrene = get_askrene(cmd->plugin); @@ -735,15 +724,14 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, NULL)) return command_param_failed(); - num = reserves_remove(askrene->reserved, path->scidds, path->amounts, - tal_count(path->scidds)); - if (num != tal_count(path->scidds)) { - const struct reserve *r = find_reserve(askrene->reserved, &path->scidds[num]); + num = reserves_remove(askrene->reserved, path, tal_count(path)); + if (num != tal_count(path)) { + const struct reserve *r = find_reserve(askrene->reserved, &path[num].scidd); return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Underflow unreserving %zu: %s amount %s (%zu reserved, amount %s)", num, - fmt_short_channel_id_dir(tmpctx, &path->scidds[num]), - fmt_amount_msat(tmpctx, path->amounts[num]), + fmt_short_channel_id_dir(tmpctx, &path[num].scidd), + fmt_amount_msat(tmpctx, path[num].amount), r ? r->num_htlcs : 0, r ? fmt_amount_msat(tmpctx, r->amount) : "none"); } diff --git a/plugins/askrene/refine.c b/plugins/askrene/refine.c index e78eebf7e571..317832dcfbb0 100644 --- a/plugins/askrene/refine.c +++ b/plugins/askrene/refine.c @@ -9,10 +9,6 @@ /* We (ab)use the reservation system to place temporary reservations * on channels while we are refining each flow. This has the effect * of making flows aware of each other. */ -struct reservations { - struct short_channel_id_dir *scidds; - struct amount_msat *amounts; -}; /* Get the scidd for the i'th hop in flow */ static void get_scidd(const struct gossmap *gossmap, @@ -24,48 +20,45 @@ static void get_scidd(const struct gossmap *gossmap, scidd->dir = flow->dirs[i]; } -static void destroy_reservations(struct reservations *r, struct askrene *askrene) +static void destroy_reservations(struct reserve_hop *rhops, struct askrene *askrene) { - assert(tal_count(r->scidds) == tal_count(r->amounts)); - if (reserves_remove(askrene->reserved, - r->scidds, r->amounts, - tal_count(r->scidds)) != tal_count(r->scidds)) { + if (reserves_remove(askrene->reserved, rhops, tal_count(rhops)) + != tal_count(rhops)) { plugin_err(askrene->plugin, "Failed to remove reservations?"); } } -static struct reservations *new_reservations(const tal_t *ctx, - struct route_query *rq) +static struct reserve_hop *new_reservations(const tal_t *ctx, + const struct route_query *rq) { - struct reservations *r = tal(ctx, struct reservations); - r->scidds = tal_arr(r, struct short_channel_id_dir, 0); - r->amounts = tal_arr(r, struct amount_msat, 0); + struct reserve_hop *rhops = tal_arr(ctx, struct reserve_hop, 0); /* Unreserve on free */ - tal_add_destructor2(r, destroy_reservations, get_askrene(rq->plugin)); - return r; + tal_add_destructor2(rhops, destroy_reservations, get_askrene(rq->plugin)); + return rhops; } /* Add reservation: we (ab)use this to temporarily avoid over-usage as * we refine. */ -static void add_reservation(struct reservations *r, +static void add_reservation(struct reserve_hop **reservations, struct route_query *rq, const struct flow *flow, size_t i, struct amount_msat amt) { - struct short_channel_id_dir scidd; + struct reserve_hop rhop; struct askrene *askrene = get_askrene(rq->plugin); size_t idx; - get_scidd(rq->gossmap, flow, i, &scidd); + get_scidd(rq->gossmap, flow, i, &rhop.scidd); + rhop.amount = amt; /* This should not happen, but simply don't reserve if it does */ - if (!reserves_add(askrene->reserved, &scidd, &amt, 1)) { + if (!reserves_add(askrene->reserved, &rhop, 1)) { plugin_log(rq->plugin, LOG_BROKEN, "Failed to reserve %s in %s", fmt_amount_msat(tmpctx, amt), - fmt_short_channel_id_dir(tmpctx, &scidd)); + fmt_short_channel_id_dir(tmpctx, &rhop.scidd)); return; } @@ -75,8 +68,7 @@ static void add_reservation(struct reservations *r, rq->capacities[idx] = 0; /* Record so destructor will unreserve */ - tal_arr_expand(&r->scidds, scidd); - tal_arr_expand(&r->amounts, amt); + tal_arr_expand(reservations, rhop); } /* We have a basic set of flows, but we need to add fees, and take into @@ -93,7 +85,7 @@ static void add_reservation(struct reservations *r, static const char *constrain_flow(const tal_t *ctx, struct route_query *rq, struct flow *flow, - struct reservations *reservations) + struct reserve_hop **reservations) { struct amount_msat msat; int decreased = -1; @@ -187,7 +179,7 @@ static const char *constrain_flow(const tal_t *ctx, /* Flow is now delivering `extra` additional msat, so modify reservations */ static void add_to_flow(struct flow *flow, struct route_query *rq, - struct reservations *reservations, + struct reserve_hop **reservations, struct amount_msat extra) { struct amount_msat orig, updated; @@ -299,7 +291,7 @@ refine_with_fees_and_limits(const tal_t *ctx, struct amount_msat deliver, struct flow ***flows) { - struct reservations *reservations = new_reservations(NULL, rq); + struct reserve_hop *reservations = new_reservations(NULL, rq); struct amount_msat more_to_deliver; const char *flow_constraint_error = NULL; const char *ret; @@ -317,7 +309,7 @@ refine_with_fees_and_limits(const tal_t *ctx, fmt_amount_msat(tmpctx, max)); } - flow_constraint_error = constrain_flow(tmpctx, rq, flow, reservations); + flow_constraint_error = constrain_flow(tmpctx, rq, flow, &reservations); if (!flow_constraint_error) { i++; continue; @@ -398,7 +390,7 @@ refine_with_fees_and_limits(const tal_t *ctx, } /* Make this flow deliver +extra, and modify reservations */ - add_to_flow(f, rq, reservations, extra); + add_to_flow(f, rq, &reservations, extra); /* Should not happen, since extra comes from div... */ if (!amount_msat_sub(&more_to_deliver, more_to_deliver, extra)) diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c index fb822143c068..1ae96b5fe1b7 100644 --- a/plugins/askrene/reserve.c +++ b/plugins/askrene/reserve.c @@ -86,16 +86,15 @@ static bool remove(struct reserve *r, struct amount_msat amount) /* Atomically add to reserves, or fail. * Returns offset of failure, or num on success */ size_t reserves_add(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, + const struct reserve_hop *hops, size_t num) { for (size_t i = 0; i < num; i++) { - struct reserve *r = reserve_htable_get(reserved, &scidds[i]); + struct reserve *r = reserve_htable_get(reserved, &hops[i].scidd); if (!r) - r = new_reserve(reserved, &scidds[i]); - if (!add(r, amounts[i])) { - reserves_remove(reserved, scidds, amounts, i); + r = new_reserve(reserved, &hops[i].scidd); + if (!add(r, hops[i].amount)) { + reserves_remove(reserved, hops, i); return i; } } @@ -105,14 +104,13 @@ size_t reserves_add(struct reserve_htable *reserved, /* Atomically remove from reserves, to fail. * Returns offset of failure or tal_count(scidds) */ size_t reserves_remove(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, + const struct reserve_hop *hops, size_t num) { for (size_t i = 0; i < num; i++) { - struct reserve *r = reserve_htable_get(reserved, &scidds[i]); - if (!r || !remove(r, amounts[i])) { - reserves_add(reserved, scidds, amounts, i); + struct reserve *r = reserve_htable_get(reserved, &hops[i].scidd); + if (!r || !remove(r, hops[i].amount)) { + reserves_add(reserved, hops, i); return i; } if (r->num_htlcs == 0) diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h index 7da617229c56..67fc805c885f 100644 --- a/plugins/askrene/reserve.h +++ b/plugins/askrene/reserve.h @@ -22,18 +22,22 @@ struct reserve_htable *new_reserve_htable(const tal_t *ctx); const struct reserve *find_reserve(const struct reserve_htable *reserved, const struct short_channel_id_dir *scidd); + +struct reserve_hop { + struct short_channel_id_dir scidd; + struct amount_msat amount; +}; + /* Atomically add to reserves, or fail. * Returns offset of failure, or num on success */ size_t reserves_add(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, + const struct reserve_hop *hops, size_t num); /* Atomically remove from reserves, to fail. * Returns offset of failure or tal_count(scidds) */ size_t reserves_remove(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, + const struct reserve_hop *hops, size_t num); /* Clear capacities array where we have reserves */ From 05517f2e1f2dfd66e80e4c7a3e3cce6a4712b3f7 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:56:53 +0930 Subject: [PATCH 09/24] askrene: remember individual reservations, for better debugging. Suggested-by: Lagrang3 Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 44 ++++---------- plugins/askrene/refine.c | 15 +---- plugins/askrene/reserve.c | 125 +++++++++++++++----------------------- plugins/askrene/reserve.h | 34 ++++------- 4 files changed, 77 insertions(+), 141 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 9b319a88c7e7..da68997c6127 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -447,7 +447,6 @@ void get_constraints(const struct route_query *rq, struct amount_msat *max) { struct short_channel_id_dir scidd; - const struct reserve *reserve; size_t idx = gossmap_chan_idx(rq->gossmap, chan); *min = AMOUNT_MSAT(0); @@ -494,14 +493,8 @@ void get_constraints(const struct route_query *rq, } /* Finally, if any is in use, subtract that! */ - reserve = find_reserve(rq->reserved, &scidd); - if (reserve) { - /* They can definitely *try* to push too much through a channel! */ - if (!amount_msat_sub(min, *min, reserve->amount)) - *min = AMOUNT_MSAT(0); - if (!amount_msat_sub(max, *max, reserve->amount)) - *max = AMOUNT_MSAT(0); - } + reserve_sub(rq->reserved, &scidd, min); + reserve_sub(rq->reserved, &scidd, max); } struct getroutes_info { @@ -687,7 +680,6 @@ static struct command_result *json_askrene_reserve(struct command *cmd, { struct reserve_hop *path; struct json_stream *response; - size_t num; struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, @@ -695,16 +687,8 @@ static struct command_result *json_askrene_reserve(struct command *cmd, NULL)) return command_param_failed(); - num = reserves_add(askrene->reserved, path, tal_count(path)); - if (num != tal_count(path)) { - const struct reserve *r = find_reserve(askrene->reserved, &path[num].scidd); - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Overflow reserving %zu: %s amount %s (%s reserved already)", - num, - fmt_short_channel_id_dir(tmpctx, &path[num].scidd), - fmt_amount_msat(tmpctx, path[num].amount), - r ? fmt_amount_msat(tmpctx, r->amount) : "none"); - } + for (size_t i = 0; i < tal_count(path); i++) + reserve_add(askrene->reserved, &path[i], cmd->id); response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); @@ -716,7 +700,6 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, { struct reserve_hop *path; struct json_stream *response; - size_t num; struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, @@ -724,17 +707,14 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, NULL)) return command_param_failed(); - num = reserves_remove(askrene->reserved, path, tal_count(path)); - if (num != tal_count(path)) { - const struct reserve *r = find_reserve(askrene->reserved, &path[num].scidd); - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Underflow unreserving %zu: %s amount %s (%zu reserved, amount %s)", - num, - fmt_short_channel_id_dir(tmpctx, &path[num].scidd), - fmt_amount_msat(tmpctx, path[num].amount), - r ? r->num_htlcs : 0, - r ? fmt_amount_msat(tmpctx, r->amount) : "none"); - } + for (size_t i = 0; i < tal_count(path); i++) { + if (!reserve_remove(askrene->reserved, &path[i])) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Unknown reservation for %s", + fmt_short_channel_id_dir(tmpctx, + &path[i].scidd)); + } + } response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); diff --git a/plugins/askrene/refine.c b/plugins/askrene/refine.c index 317832dcfbb0..18596281594f 100644 --- a/plugins/askrene/refine.c +++ b/plugins/askrene/refine.c @@ -22,10 +22,8 @@ static void get_scidd(const struct gossmap *gossmap, static void destroy_reservations(struct reserve_hop *rhops, struct askrene *askrene) { - if (reserves_remove(askrene->reserved, rhops, tal_count(rhops)) - != tal_count(rhops)) { - plugin_err(askrene->plugin, "Failed to remove reservations?"); - } + for (size_t i = 0; i < tal_count(rhops); i++) + reserve_remove(askrene->reserved, &rhops[i]); } static struct reserve_hop *new_reservations(const tal_t *ctx, @@ -53,14 +51,7 @@ static void add_reservation(struct reserve_hop **reservations, get_scidd(rq->gossmap, flow, i, &rhop.scidd); rhop.amount = amt; - /* This should not happen, but simply don't reserve if it does */ - if (!reserves_add(askrene->reserved, &rhop, 1)) { - plugin_log(rq->plugin, LOG_BROKEN, - "Failed to reserve %s in %s", - fmt_amount_msat(tmpctx, amt), - fmt_short_channel_id_dir(tmpctx, &rhop.scidd)); - return; - } + reserve_add(askrene->reserved, &rhop, rq->cmd->id); /* Set capacities entry to 0 so it get_constraints() looks in reserve. */ idx = gossmap_chan_idx(rq->gossmap, flow->path[i]); diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c index 1ae96b5fe1b7..9b825fc8dc63 100644 --- a/plugins/askrene/reserve.c +++ b/plugins/askrene/reserve.c @@ -1,16 +1,27 @@ #include "config.h" #include #include +#include #include #include #include #include +/* Note! We can have multiple of these! */ +struct reserve { + /* What */ + struct reserve_hop rhop; + /* When */ + struct timemono timestamp; + /* ID of command which reserved it */ + const char *cmd_id; +}; + /* Hash table for reservations */ static const struct short_channel_id_dir * reserve_scidd(const struct reserve *r) { - return &r->scidd; + return &r->rhop.scidd; } static size_t hash_scidd(const struct short_channel_id_dir *scidd) @@ -22,7 +33,7 @@ static size_t hash_scidd(const struct short_channel_id_dir *scidd) static bool reserve_eq_scidd(const struct reserve *r, const struct short_channel_id_dir *scidd) { - return short_channel_id_dir_eq(scidd, &r->scidd); + return short_channel_id_dir_eq(scidd, &r->rhop.scidd); } HTABLE_DEFINE_TYPE(struct reserve, reserve_scidd, hash_scidd, @@ -35,88 +46,37 @@ struct reserve_htable *new_reserve_htable(const tal_t *ctx) return reserved; } -/* Find a reservation for this scidd (if any!) */ -const struct reserve *find_reserve(const struct reserve_htable *reserved, - const struct short_channel_id_dir *scidd) -{ - return reserve_htable_get(reserved, scidd); -} - -/* Create a new (empty) reservation */ -static struct reserve *new_reserve(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidd) +void reserve_add(struct reserve_htable *reserved, + const struct reserve_hop *rhop, + const char *cmd_id TAKES) { struct reserve *r = tal(reserved, struct reserve); - - r->num_htlcs = 0; - r->amount = AMOUNT_MSAT(0); - r->scidd = *scidd; + r->rhop = *rhop; + r->timestamp = time_mono(); + r->cmd_id = tal_strdup(r, cmd_id); reserve_htable_add(reserved, r); - return r; } -static void del_reserve(struct reserve_htable *reserved, struct reserve *r) +bool reserve_remove(struct reserve_htable *reserved, + const struct reserve_hop *rhop) { - assert(r->num_htlcs == 0); - - reserve_htable_del(reserved, r); - tal_free(r); -} - -/* Add to existing reservation (false if would overflow). */ -static bool add(struct reserve *r, struct amount_msat amount) -{ - if (!amount_msat_accumulate(&r->amount, amount)) - return false; - r->num_htlcs++; - return true; -} - -static bool remove(struct reserve *r, struct amount_msat amount) -{ - if (r->num_htlcs == 0) - return false; - if (!amount_msat_sub(&r->amount, r->amount, amount)) - return false; - r->num_htlcs--; - return true; -} + struct reserve *r; + struct reserve_htable_iter rit; -/* Atomically add to reserves, or fail. - * Returns offset of failure, or num on success */ -size_t reserves_add(struct reserve_htable *reserved, - const struct reserve_hop *hops, - size_t num) -{ - for (size_t i = 0; i < num; i++) { - struct reserve *r = reserve_htable_get(reserved, &hops[i].scidd); - if (!r) - r = new_reserve(reserved, &hops[i].scidd); - if (!add(r, hops[i].amount)) { - reserves_remove(reserved, hops, i); - return i; - } - } - return num; -} + /* Note! This may remove the "wrong" one, but since they're only + * differentiated for debugging, that's OK */ + for (r = reserve_htable_getfirst(reserved, &rhop->scidd, &rit); + r; + r = reserve_htable_getnext(reserved, &rhop->scidd, &rit)) { + if (!amount_msat_eq(r->rhop.amount, rhop->amount)) + continue; -/* Atomically remove from reserves, to fail. - * Returns offset of failure or tal_count(scidds) */ -size_t reserves_remove(struct reserve_htable *reserved, - const struct reserve_hop *hops, - size_t num) -{ - for (size_t i = 0; i < num; i++) { - struct reserve *r = reserve_htable_get(reserved, &hops[i].scidd); - if (!r || !remove(r, hops[i].amount)) { - reserves_add(reserved, hops, i); - return i; - } - if (r->num_htlcs == 0) - del_reserve(reserved, r); + reserve_htable_del(reserved, r); + tal_free(r); + return true; } - return num; + return false; } void reserves_clear_capacities(struct reserve_htable *reserved, @@ -129,7 +89,7 @@ void reserves_clear_capacities(struct reserve_htable *reserved, for (r = reserve_htable_first(reserved, &rit); r; r = reserve_htable_next(reserved, &rit)) { - struct gossmap_chan *c = gossmap_find_chan(gossmap, &r->scidd.scid); + struct gossmap_chan *c = gossmap_find_chan(gossmap, &r->rhop.scidd.scid); size_t idx; if (!c) continue; @@ -139,6 +99,21 @@ void reserves_clear_capacities(struct reserve_htable *reserved, } } +void reserve_sub(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount) +{ + struct reserve *r; + struct reserve_htable_iter rit; + + for (r = reserve_htable_getfirst(reserved, scidd, &rit); + r; + r = reserve_htable_getnext(reserved, scidd, &rit)) { + if (!amount_msat_sub(amount, *amount, r->rhop.amount)) + *amount = AMOUNT_MSAT(0); + } +} + void reserve_memleak_mark(struct askrene *askrene, struct htable *memtable) { memleak_scan_htable(memtable, &askrene->reserved->raw); diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h index 67fc805c885f..cca4157364f0 100644 --- a/plugins/askrene/reserve.h +++ b/plugins/askrene/reserve.h @@ -8,43 +8,33 @@ #include #include -/* We reserve a path being used. This records how many and how much */ -struct reserve { - size_t num_htlcs; - struct short_channel_id_dir scidd; - struct amount_msat amount; -}; - /* Initialize hash table for reservations */ struct reserve_htable *new_reserve_htable(const tal_t *ctx); -/* Find a reservation for this scidd (if any!) */ -const struct reserve *find_reserve(const struct reserve_htable *reserved, - const struct short_channel_id_dir *scidd); - - struct reserve_hop { struct short_channel_id_dir scidd; struct amount_msat amount; }; -/* Atomically add to reserves, or fail. - * Returns offset of failure, or num on success */ -size_t reserves_add(struct reserve_htable *reserved, - const struct reserve_hop *hops, - size_t num); +/* Add a reservation */ +void reserve_add(struct reserve_htable *reserved, + const struct reserve_hop *rhop, + const char *cmd_id TAKES); -/* Atomically remove from reserves, to fail. - * Returns offset of failure or tal_count(scidds) */ -size_t reserves_remove(struct reserve_htable *reserved, - const struct reserve_hop *hops, - size_t num); +/* Try to remove a reservation, if it exists. */ +bool reserve_remove(struct reserve_htable *reserved, + const struct reserve_hop *rhop); /* Clear capacities array where we have reserves */ void reserves_clear_capacities(struct reserve_htable *reserved, const struct gossmap *gossmap, fp16_t *capacities); +/* Subtract any reserves for scidd from this amount */ +void reserve_sub(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount); + /* Scan for memleaks */ void reserve_memleak_mark(struct askrene *askrene, struct htable *memtable); #endif /* LIGHTNING_PLUGINS_ASKRENE_RESERVE_H */ From 6671c6d370567deeb2fb56ec125689f4aacd70ab Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:57:53 +0930 Subject: [PATCH 10/24] askrene: implement listreservations And actually write tests! Suggested-by: Lagrang3 Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 84 +++++++++++++++-- doc/Makefile | 1 + doc/index.rst | 1 + .../lightning-askrene-listreservations.json | 72 ++++++++++++++ doc/schemas/lightning-askrene-reserve.json | 6 +- doc/schemas/lightning-askrene-unreserve.json | 6 +- plugins/askrene/askrene.c | 20 ++++ plugins/askrene/reserve.c | 27 ++++++ plugins/askrene/reserve.h | 5 + tests/test_askrene.py | 93 +++++++++++++++++++ 10 files changed, 295 insertions(+), 20 deletions(-) create mode 100644 doc/schemas/lightning-askrene-listreservations.json diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index c04ab364fb46..9dfcbc440f67 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -937,6 +937,78 @@ "Main web site: " ] }, + "lightning-askrene-listreservations.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-listreservations", + "title": "Command to display information about reservations (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-reservations** RPC command reports outstanding reservations made with `askrene-reserve`, mainly for debugging." + ], + "request": { + "required": [], + "properties": {} + }, + "response": { + "required": [ + "reservations" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "short_channel_id_dir", + "amount_msat", + "age_in_seconds", + "command_id" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction that is reserved." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount reserved." + ] + }, + "age_in_seconds": { + "type": "u64", + "description": [ + "The age of this reservation." + ] + }, + "command_id": { + "type": "string", + "description": [ + "The JSON id of the command used to make the reservation." + ] + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-reserve(7)", + "lightning-askrene-unreserve(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, "lightning-askrene-remove-layer.json": { "$schema": "../rpc-schema-draft.json", "type": "object", @@ -1028,11 +1100,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-unreserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." @@ -1093,11 +1161,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-reserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." diff --git a/doc/Makefile b/doc/Makefile index 9e01a4382ac0..afef62d8d0a5 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -12,6 +12,7 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-askrene-disable-node.7 \ doc/lightning-askrene-inform-channel.7 \ doc/lightning-askrene-listlayers.7 \ + doc/lightning-askrene-listreservations.7 \ doc/lightning-askrene-reserve.7 \ doc/lightning-askrene-unreserve.7 \ doc/lightning-autoclean-once.7 \ diff --git a/doc/index.rst b/doc/index.rst index 682dee3e909c..a28fc8f425a3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,6 +19,7 @@ Core Lightning Documentation lightning-askrene-disable-node lightning-askrene-inform-channel lightning-askrene-listlayers + lightning-askrene-listreservations lightning-askrene-remove-layer lightning-askrene-reserve lightning-askrene-unreserve diff --git a/doc/schemas/lightning-askrene-listreservations.json b/doc/schemas/lightning-askrene-listreservations.json new file mode 100644 index 000000000000..1886d9e1c19e --- /dev/null +++ b/doc/schemas/lightning-askrene-listreservations.json @@ -0,0 +1,72 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-listreservations", + "title": "Command to display information about reservations (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-reservations** RPC command reports outstanding reservations made with `askrene-reserve`, mainly for debugging." + ], + "request": { + "required": [], + "properties": {} + }, + "response": { + "required": [ + "reservations" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "short_channel_id_dir", + "amount_msat", + "age_in_seconds", + "command_id" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction that is reserved." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount reserved." + ] + }, + "age_in_seconds": { + "type": "u64", + "description": [ + "The age of this reservation." + ] + }, + "command_id": { + "type": "string", + "description": [ + "The JSON id of the command used to make the reservation." + ] + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-reserve(7)", + "lightning-askrene-unreserve(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-reserve.json b/doc/schemas/lightning-askrene-reserve.json index 070920021f20..d2790ba5a41a 100644 --- a/doc/schemas/lightning-askrene-reserve.json +++ b/doc/schemas/lightning-askrene-reserve.json @@ -50,11 +50,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-unreserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." diff --git a/doc/schemas/lightning-askrene-unreserve.json b/doc/schemas/lightning-askrene-unreserve.json index eeadf8058fb3..c214e56ddc75 100644 --- a/doc/schemas/lightning-askrene-unreserve.json +++ b/doc/schemas/lightning-askrene-unreserve.json @@ -50,11 +50,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-reserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index da68997c6127..d0c0913b6e57 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -720,6 +720,22 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, return command_finished(cmd, response); } +static struct command_result *json_askrene_listreservations(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct askrene *askrene = get_askrene(cmd->plugin); + struct json_stream *response; + + if (!param(cmd, buffer, params, + NULL)) + return command_param_failed(); + + response = jsonrpc_stream_success(cmd); + json_add_reservations(response, askrene->reserved, "reservations"); + return command_finished(cmd, response); +} + static struct command_result *json_askrene_create_channel(struct command *cmd, const char *buffer, const jsmntok_t *params) @@ -942,6 +958,10 @@ static const struct plugin_command commands[] = { "getroutes", json_getroutes, }, + { + "askrene-listreservations", + json_askrene_listreservations, + }, { "askrene-reserve", json_askrene_reserve, diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c index 9b825fc8dc63..56bac106e3be 100644 --- a/plugins/askrene/reserve.c +++ b/plugins/askrene/reserve.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -114,6 +115,32 @@ void reserve_sub(const struct reserve_htable *reserved, } } +void json_add_reservations(struct json_stream *js, + const struct reserve_htable *reserved, + const char *fieldname) +{ + struct reserve *r; + struct reserve_htable_iter rit; + + json_array_start(js, fieldname); + for (r = reserve_htable_first(reserved, &rit); + r; + r = reserve_htable_next(reserved, &rit)) { + json_object_start(js, NULL); + json_add_short_channel_id_dir(js, + "short_channel_id_dir", + r->rhop.scidd); + json_add_amount_msat(js, + "amount_msat", + r->rhop.amount); + json_add_u64(js, "age_in_seconds", + timemono_between(time_mono(), r->timestamp).ts.tv_sec); + json_add_string(js, "command_id", r->cmd_id); + json_object_end(js); + } + json_array_end(js); +} + void reserve_memleak_mark(struct askrene *askrene, struct htable *memtable) { memleak_scan_htable(memtable, &askrene->reserved->raw); diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h index cca4157364f0..ffffe91c2cb0 100644 --- a/plugins/askrene/reserve.h +++ b/plugins/askrene/reserve.h @@ -35,6 +35,11 @@ void reserve_sub(const struct reserve_htable *reserved, const struct short_channel_id_dir *scidd, struct amount_msat *amount); +/* Print out a json object for all reservations */ +void json_add_reservations(struct json_stream *js, + const struct reserve_htable *reserved, + const char *fieldname); + /* Scan for memleaks */ void reserve_memleak_mark(struct askrene *askrene, struct htable *memtable); #endif /* LIGHTNING_PLUGINS_ASKRENE_RESERVE_H */ diff --git a/tests/test_askrene.py b/tests/test_askrene.py index b71d37af0c0e..683f548b146a 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -15,6 +15,99 @@ def direction(src, dst): return 1 +def test_reserve(node_factory): + """Test reserving channels""" + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + assert l1.rpc.askrene_listreservations() == {'reservations': []} + scid12dir = f"{first_scid(l1, l2)}/{direction(l1.info['id'], l2.info['id'])}" + scid23dir = f"{first_scid(l2, l3)}/{direction(l2.info['id'], l3.info['id'])}" + + initial_prob = l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] + + # Reserve 1000 sats on path. This should reduce probability! + l1.rpc.askrene_reserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_001}]) + listres = l1.rpc.askrene_listreservations()['reservations'] + if listres[0]['short_channel_id_dir'] == scid12dir: + assert listres[0]['amount_msat'] == 1000_000 + assert listres[1]['short_channel_id_dir'] == scid23dir + assert listres[1]['amount_msat'] == 1000_001 + else: + assert listres[0]['short_channel_id_dir'] == scid23dir + assert listres[0]['amount_msat'] == 1000_001 + assert listres[1]['short_channel_id_dir'] == scid12dir + assert listres[1]['amount_msat'] == 1000_000 + assert len(listres) == 2 + + assert l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] < initial_prob + + # Now reserve so much there's nothing left. + l1.rpc.askrene_reserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000_000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_000_000_000}]) + + # FIXME: better error! + with pytest.raises(RpcError, match="Could not find route"): + l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] + + # Can't remove wrong amounts: that's user error + with pytest.raises(RpcError, match="Unknown reservation"): + l1.rpc.askrene_unreserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_001}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_000}]) + + # Remove, it's all ok. + l1.rpc.askrene_unreserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_001}]) + l1.rpc.askrene_unreserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000_000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_000_000_000}]) + assert l1.rpc.askrene_listreservations() == {'reservations': []} + assert l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] == initial_prob + + # Reserving in reverse makes no difference! + scid12rev = f"{first_scid(l1, l2)}/{direction(l2.info['id'], l1.info['id'])}" + scid23rev = f"{first_scid(l2, l3)}/{direction(l3.info['id'], l2.info['id'])}" + l1.rpc.askrene_reserve(path=[{'short_channel_id_dir': scid12rev, + 'amount_msat': 1000_000_000_000}, + {'short_channel_id_dir': scid23rev, + 'amount_msat': 1000_000_000_000}]) + assert l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] == initial_prob + + def test_layers(node_factory): """Test manipulating information in layers""" l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) From f63727a95e4d26f621d57e4dcdca59978bb8f2a5 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:58:53 +0930 Subject: [PATCH 11/24] askrene: change inform interface, take into account reserve. Lagrang3 points out that if we hit a maximum, we should take into account the reserve. This is true, but it's hard for the caller to do, so change the API to be slightly higher level. Tell "inform" what happened, and it adjust the constraints appropriately. This makes the least assumptions possible (a reserve does *not* mean that the capacity was actually used at that time). We also add a mode to say "this succeeded": for now this does nothing, but it could reduce both min/max capacities, and add capacity in the other direction. This is useful for future payments, but not as useful for the current one. Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 81 ++++++++++--------- .../lightning-askrene-inform-channel.json | 79 +++++++++--------- doc/schemas/lightning-askrene-reserve.json | 2 +- plugins/askrene/askrene.c | 79 ++++++++++++++---- plugins/askrene/reserve.c | 16 ++++ plugins/askrene/reserve.h | 5 ++ tests/test_askrene.py | 20 +++-- 7 files changed, 181 insertions(+), 101 deletions(-) diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 9dfcbc440f67..95db709fad26 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -664,12 +664,14 @@ "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 current channel exists or not." + "The **askrene-inform-channel** RPC command tells askrene about channels we used so it can update its capacity estimates. For most accuracy, you should remove your own reservations before calling this. It can be applied whether the current channel exists or not." ], "request": { "required": [ "layer", - "short_channel_id_dir" + "short_channel_id_dir", + "amount_msat", + "inform" ], "properties": { "layer": { @@ -684,55 +686,56 @@ "The short channel id and direction to apply this change to." ] }, - "minimum_msat": { + "amount_msat": { "type": "msat", "description": [ - "The minumum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "The amount we used on the channel" ] }, - "maximum_msat": { - "type": "msat", + "inform": { + "type": "string", + "enum": [ + "constrained", + "unconstrained", + "succeeded" + ], "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "Whether this payment passed (implying capacity of at least that amount), failed (implying maximum capacity of one msat less), or succeeded (implying capacity has been reduced in this direction)" ] } } }, "response": { "required": [ - "constraint" + "constraints" ], "properties": { - "constraint": { - "type": "object", - "required": [ - "short_channel_id_dir", - "timestamp" - ], - "properties": { - "short_channel_id_dir": { - "type": "short_channel_id_dir", - "description": [ - "The *short_channel_id* and *direction* specified." - ] - }, - "timestamp": { - "type": "u64", - "description": [ - "The UNIX time (seconds since 1970) this was created." - ] - }, - "maximum_msat": { - "type": "msat", - "description": [ - "The *minimum_msat* (if specified)" - ] - }, - "minimum_msat": { - "type": "msat", - "description": [ - "The *maximum_msat* (if specified)" - ] + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The short channel id and 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." + ] + } } } } @@ -1057,7 +1060,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call **askrene-unreserve** after the attempt has completed.", + "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call *askrene-unreserve* after the attempt has completed (and before calling *askrene-inform*).", "", "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." ], diff --git a/doc/schemas/lightning-askrene-inform-channel.json b/doc/schemas/lightning-askrene-inform-channel.json index 9d63efe60ce1..3a07bb74f7c7 100644 --- a/doc/schemas/lightning-askrene-inform-channel.json +++ b/doc/schemas/lightning-askrene-inform-channel.json @@ -7,12 +7,14 @@ "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 current channel exists or not." + "The **askrene-inform-channel** RPC command tells askrene about channels we used so it can update its capacity estimates. For most accuracy, you should remove your own reservations before calling this. It can be applied whether the current channel exists or not." ], "request": { "required": [ "layer", - "short_channel_id_dir" + "short_channel_id_dir", + "amount_msat", + "inform" ], "properties": { "layer": { @@ -27,55 +29,56 @@ "The short channel id and direction to apply this change to." ] }, - "minimum_msat": { + "amount_msat": { "type": "msat", "description": [ - "The minumum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "The amount we used on the channel" ] }, - "maximum_msat": { - "type": "msat", + "inform": { + "type": "string", + "enum": [ + "constrained", + "unconstrained", + "succeeded" + ], "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "Whether this payment passed (implying capacity of at least that amount), failed (implying maximum capacity of one msat less), or succeeded (implying capacity has been reduced in this direction)" ] } } }, "response": { "required": [ - "constraint" + "constraints" ], "properties": { - "constraint": { - "type": "object", - "required": [ - "short_channel_id_dir", - "timestamp" - ], - "properties": { - "short_channel_id_dir": { - "type": "short_channel_id_dir", - "description": [ - "The *short_channel_id* and *direction* specified." - ] - }, - "timestamp": { - "type": "u64", - "description": [ - "The UNIX time (seconds since 1970) this was created." - ] - }, - "maximum_msat": { - "type": "msat", - "description": [ - "The *minimum_msat* (if specified)" - ] - }, - "minimum_msat": { - "type": "msat", - "description": [ - "The *maximum_msat* (if specified)" - ] + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The short channel id and 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." + ] + } } } } diff --git a/doc/schemas/lightning-askrene-reserve.json b/doc/schemas/lightning-askrene-reserve.json index d2790ba5a41a..880eff109dfd 100644 --- a/doc/schemas/lightning-askrene-reserve.json +++ b/doc/schemas/lightning-askrene-reserve.json @@ -7,7 +7,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call **askrene-unreserve** after the attempt has completed.", + "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call *askrene-unreserve* after the attempt has completed (and before calling *askrene-inform*).", "", "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." ], diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index d0c0913b6e57..e8629f5351e5 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -782,41 +782,86 @@ static struct command_result *json_askrene_create_channel(struct command *cmd, return command_finished(cmd, response); } +enum inform { + INFORM_CONSTRAINED, + INFORM_UNCONSTRAINED, + INFORM_SUCCEEDED, +}; + +static struct command_result *param_inform(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + enum inform **inform) +{ + *inform = tal(cmd, enum inform); + if (json_tok_streq(buffer, tok, "constrained")) + **inform = INFORM_CONSTRAINED; + else if (json_tok_streq(buffer, tok, "unconstrained")) + **inform = INFORM_UNCONSTRAINED; + else if (json_tok_streq(buffer, tok, "succeeded")) + **inform = INFORM_SUCCEEDED; + else + command_fail_badparam(cmd, name, buffer, tok, + "must be constrained/unconstrained/succeeded"); + return NULL; +} + static struct command_result *json_askrene_inform_channel(struct command *cmd, const char *buffer, const jsmntok_t *params) { + struct askrene *askrene = get_askrene(cmd->plugin); struct layer *layer; struct short_channel_id_dir *scidd; struct json_stream *response; - struct amount_msat *max, *min; + struct amount_msat *amount; + enum inform *inform; const struct constraint *c; if (!param_check(cmd, buffer, params, 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), + p_req("amount_msat", param_msat, &amount), + p_req("inform", param_inform, &inform), NULL)) return command_param_failed(); - if ((!min && !max) || (min && max)) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Must specify exactly one of maximum_msat/minimum_msat"); - } - - if (command_check_only(cmd)) - return command_check_done(cmd); - - if (min) { - c = layer_update_constraint(layer, scidd, CONSTRAINT_MIN, - time_now().ts.tv_sec, *min); - } else { + switch (*inform) { + case INFORM_CONSTRAINED: + /* It didn't pass, so minimal assumption is that reserve was all used + * then there we were one msat short. */ + if (!amount_msat_sub(amount, *amount, AMOUNT_MSAT(1))) + *amount = AMOUNT_MSAT(0); + if (!reserve_accumulate(askrene->reserved, scidd, amount)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Amount overflow with reserves"); + if (command_check_only(cmd)) + return command_check_done(cmd); c = layer_update_constraint(layer, scidd, CONSTRAINT_MAX, - time_now().ts.tv_sec, *max); + time_now().ts.tv_sec, *amount); + goto output; + case INFORM_UNCONSTRAINED: + /* It passed, so the capacity is at least this much (minimal assumption is + * that no reserves were used) */ + if (command_check_only(cmd)) + return command_check_done(cmd); + c = layer_update_constraint(layer, scidd, CONSTRAINT_MIN, + time_now().ts.tv_sec, *amount); + goto output; + case INFORM_SUCCEEDED: + /* FIXME: We could do something useful here! */ + c = NULL; + goto output; } + abort(); + +output: response = jsonrpc_stream_success(cmd); - json_add_constraint(response, "constraint", c, layer); + json_array_start(response, "constraints"); + if (c) + json_add_constraint(response, NULL, c, layer); + json_array_end(response); return command_finished(cmd, response); } diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c index 56bac106e3be..7c8e49395529 100644 --- a/plugins/askrene/reserve.c +++ b/plugins/askrene/reserve.c @@ -115,6 +115,22 @@ void reserve_sub(const struct reserve_htable *reserved, } } +bool reserve_accumulate(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount) +{ + struct reserve *r; + struct reserve_htable_iter rit; + + for (r = reserve_htable_getfirst(reserved, scidd, &rit); + r; + r = reserve_htable_getnext(reserved, scidd, &rit)) { + if (!amount_msat_add(amount, *amount, r->rhop.amount)) + return false; + } + return true; +} + void json_add_reservations(struct json_stream *js, const struct reserve_htable *reserved, const char *fieldname) diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h index ffffe91c2cb0..103ed11083cd 100644 --- a/plugins/askrene/reserve.h +++ b/plugins/askrene/reserve.h @@ -35,6 +35,11 @@ void reserve_sub(const struct reserve_htable *reserved, const struct short_channel_id_dir *scidd, struct amount_msat *amount); +/* Add any reserves for scidd to this amount */ +bool reserve_accumulate(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount); + /* Print out a json object for all reservations */ void json_add_reservations(struct json_stream *js, const struct reserve_htable *reserved, diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 683f548b146a..0b8bbab10074 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -158,7 +158,8 @@ def test_layers(node_factory): first_timestamp = int(time.time()) l2.rpc.askrene_inform_channel('test_layers', '0x0x1/1', - 100000) + 100000, + 'unconstrained') last_timestamp = int(time.time()) + 1 expect['constraints'].append({'short_channel_id_dir': '0x0x1/1', 'minimum_msat': 100000}) @@ -178,7 +179,8 @@ def test_layers(node_factory): scid12dir = f"{scid12}/{direction(l2.info['id'], l1.info['id'])}" l2.rpc.askrene_inform_channel(layer='test_layers', short_channel_id_dir=scid12dir, - maximum_msat=12341234) + amount_msat=12341235, + inform='constrained') last_timestamp = int(time.time()) + 1 expect['constraints'].append({'short_channel_id_dir': scid12dir, 'timestamp': first_timestamp, @@ -543,7 +545,8 @@ def test_fees_dont_exceed_constraints(node_factory): 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) + amount_msat=max_msat + 1, + inform='constrained') routes = l1.rpc.getroutes(source=nodemap[0], destination=nodemap[3], @@ -687,14 +690,18 @@ 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']] + # We tell it we could get through amount, but not amount + 1. + # This makes min == max, just like we do for auto.localchans spendable. l1.rpc.askrene_create_layer('localchans') for scidd, amount in spendable.items(): l1.rpc.askrene_inform_channel(layer='localchans', short_channel_id_dir=scidd, - minimum_msat=amount) + amount_msat=amount, + inform='unconstrained') l1.rpc.askrene_inform_channel(layer='localchans', short_channel_id_dir=scidd, - maximum_msat=amount) + amount_msat=amount + 1, + inform='constrained') routes = l1.rpc.getroutes( source=nodemap[0], @@ -751,7 +758,8 @@ def test_max_htlc(node_factory, bitcoind): l1.rpc.askrene_create_layer('removechan2') l1.rpc.askrene_inform_channel(layer='removechan2', short_channel_id_dir='0x1x1/1', - maximum_msat=0) + amount_msat=1, + inform='constrained') # FIXME: Better diag! with pytest.raises(RpcError, match="Could not find route"): From 96ac58070d5b138ca5f7d69671af6f60b8fca965 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 08:59:53 +0930 Subject: [PATCH 12/24] askrene: rework constraints to exist in pairs. This is a bit more efficient, but moreover the JSONRPC API is more logical this way. Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 8 +- .../lightning-askrene-inform-channel.json | 4 +- doc/schemas/lightning-askrene-listlayers.json | 4 +- plugins/askrene/askrene.c | 35 ++++---- plugins/askrene/askrene.h | 7 ++ plugins/askrene/layer.c | 86 ++++++++----------- plugins/askrene/layer.h | 26 ++---- plugins/askrene/reserve.c | 6 -- tests/test_askrene.py | 2 + 9 files changed, 75 insertions(+), 103 deletions(-) diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 95db709fad26..900e785fb140 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -727,13 +727,13 @@ "maximum_msat": { "type": "msat", "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The maximum value which this channel could pass." ] }, "minimum_msat": { "type": "msat", "description": [ - "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The minimum value which this channel could pass." ] } } @@ -909,13 +909,13 @@ "maximum_msat": { "type": "msat", "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The maximum value which this channel could pass." ] }, "minimum_msat": { "type": "msat", "description": [ - "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The minimum value which this channel could pass." ] } } diff --git a/doc/schemas/lightning-askrene-inform-channel.json b/doc/schemas/lightning-askrene-inform-channel.json index 3a07bb74f7c7..b12de57fef3a 100644 --- a/doc/schemas/lightning-askrene-inform-channel.json +++ b/doc/schemas/lightning-askrene-inform-channel.json @@ -70,13 +70,13 @@ "maximum_msat": { "type": "msat", "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The maximum value which this channel could pass." ] }, "minimum_msat": { "type": "msat", "description": [ - "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The minimum value which this channel could pass." ] } } diff --git a/doc/schemas/lightning-askrene-listlayers.json b/doc/schemas/lightning-askrene-listlayers.json index 7b6eed74d029..f3c3f6b89dd7 100644 --- a/doc/schemas/lightning-askrene-listlayers.json +++ b/doc/schemas/lightning-askrene-listlayers.json @@ -152,13 +152,13 @@ "maximum_msat": { "type": "msat", "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The maximum value which this channel could pass." ] }, "minimum_msat": { "type": "msat", "description": [ - "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The minimum value which this channel could pass." ] } } diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index e8629f5351e5..f3a0e5c8a4db 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -35,12 +35,6 @@ per_htlc_cost_key(const struct per_htlc_cost *phc) return &phc->scidd; } -static size_t hash_scidd(const struct short_channel_id_dir *scidd) -{ - /* scids cost money to generate, so simple hash works here */ - return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 << 1) ^ scidd->dir; -} - static inline bool per_htlc_cost_eq_key(const struct per_htlc_cost *phc, const struct short_channel_id_dir *scidd) { @@ -462,15 +456,17 @@ void get_constraints(const struct route_query *rq, scidd.dir = dir; *max = AMOUNT_MSAT(-1ULL); - /* Look through layers for any constraints */ + /* Look through layers for any constraints (might be dummy + * ones, for created channels!) */ for (size_t i = 0; i < tal_count(rq->layers); i++) { - const struct constraint *cmin, *cmax; - cmin = layer_find_constraint(rq->layers[i], &scidd, CONSTRAINT_MIN); - if (cmin && amount_msat_greater(cmin->limit, *min)) - *min = cmin->limit; - cmax = layer_find_constraint(rq->layers[i], &scidd, CONSTRAINT_MAX); - if (cmax && amount_msat_less(cmax->limit, *max)) - *max = cmax->limit; + const struct constraint *c; + c = layer_find_constraint(rq->layers[i], &scidd); + if (c) { + if (amount_msat_greater(c->min, *min)) + *min = c->min; + if (amount_msat_less(c->max, *max)) + *max = c->max; + } } /* Might be here because it's reserved, but capacity is normal. */ @@ -617,8 +613,7 @@ static void add_localchan(struct gossmap_localmods *mods, } /* Known capacity on local channels (ts = max) */ - layer_update_constraint(info->local_layer, scidd, CONSTRAINT_MIN, UINT64_MAX, spendable); - layer_update_constraint(info->local_layer, scidd, CONSTRAINT_MAX, UINT64_MAX, spendable); + layer_update_constraint(info->local_layer, scidd, UINT64_MAX, &spendable, &spendable); } static struct command_result * @@ -838,16 +833,16 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, "Amount overflow with reserves"); if (command_check_only(cmd)) return command_check_done(cmd); - c = layer_update_constraint(layer, scidd, CONSTRAINT_MAX, - time_now().ts.tv_sec, *amount); + c = layer_update_constraint(layer, scidd, time_now().ts.tv_sec, + NULL, amount); goto output; case INFORM_UNCONSTRAINED: /* It passed, so the capacity is at least this much (minimal assumption is * that no reserves were used) */ if (command_check_only(cmd)) return command_check_done(cmd); - c = layer_update_constraint(layer, scidd, CONSTRAINT_MIN, - time_now().ts.tv_sec, *amount); + c = layer_update_constraint(layer, scidd, time_now().ts.tv_sec, + amount, NULL); goto output; case INFORM_SUCCEEDED: /* FIXME: We could do something useful here! */ diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 29c2271242fd..5ae9a4511ace 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -74,4 +74,11 @@ static inline struct askrene *get_askrene(struct plugin *plugin) return plugin_get_data(plugin, struct askrene); } +/* Convenience routine for hash tables */ +static inline size_t hash_scidd(const struct short_channel_id_dir *scidd) +{ + /* scids cost money to generate, so simple hash works here */ + return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 >> 16) ^ (scidd->scid.u64 << 1) ^ scidd->dir; +} + #endif /* LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 2ae5b59c1276..2761694769ba 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -25,27 +25,20 @@ struct local_channel { } half[2]; }; -static const struct constraint_key * -constraint_key(const struct constraint *c) +static const struct short_channel_id_dir * +constraint_scidd(const struct constraint *c) { - return &c->key; + return &c->scidd; } -static size_t hash_constraint_key(const struct constraint_key *key) +static inline bool constraint_eq_scidd(const struct constraint *c, + const struct short_channel_id_dir *scidd) { - /* scids cost money to generate, so simple hash works here */ - return (key->scidd.scid.u64 >> 32) ^ (key->scidd.scid.u64) - ^ (key->scidd.dir << 1) ^ (key->type); + return short_channel_id_dir_eq(scidd, &c->scidd); } -static inline bool constraint_eq_key(const struct constraint *c, - const struct constraint_key *key) -{ - return short_channel_id_dir_eq(&key->scidd, &c->key.scidd) && key->type == c->key.type; -} - -HTABLE_DEFINE_TYPE(struct constraint, constraint_key, hash_constraint_key, - constraint_eq_key, constraint_hash); +HTABLE_DEFINE_TYPE(struct constraint, constraint_scidd, hash_scidd, + constraint_eq_scidd, constraint_hash); static struct short_channel_id local_channel_scid(const struct local_channel *lc) @@ -208,7 +201,7 @@ void layer_update_local_channel(struct layer *layer, * lookups. You can tell it's a fake one by the timestamp. */ scidd.scid = scid; scidd.dir = dir; - layer_update_constraint(layer, &scidd, CONSTRAINT_MAX, UINT64_MAX, capacity); + layer_update_constraint(layer, &scidd, UINT64_MAX, NULL, &capacity); } struct amount_msat local_channel_capacity(const struct local_channel *lc) @@ -223,48 +216,41 @@ const struct local_channel *layer_find_local_channel(const struct layer *layer, } static struct constraint *layer_find_constraint_nonconst(const struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type) + const struct short_channel_id_dir *scidd) { - struct constraint_key k = { *scidd, type }; - return constraint_hash_get(layer->constraints, &k); + return constraint_hash_get(layer->constraints, scidd); } /* Public one returns const */ const struct constraint *layer_find_constraint(const struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type) + const struct short_channel_id_dir *scidd) { - return layer_find_constraint_nonconst(layer, scidd, type); + return layer_find_constraint_nonconst(layer, scidd); } const struct constraint *layer_update_constraint(struct layer *layer, const struct short_channel_id_dir *scidd, - enum constraint_type type, u64 timestamp, - struct amount_msat limit) + const struct amount_msat *min, + const struct amount_msat *max) { - struct constraint *c = layer_find_constraint_nonconst(layer, scidd, type); + struct constraint *c = layer_find_constraint_nonconst(layer, scidd); if (!c) { c = tal(layer, struct constraint); - c->key.scidd = *scidd; - c->key.type = type; - c->limit = limit; + c->scidd = *scidd; + c->min = AMOUNT_MSAT(0); + c->max = AMOUNT_MSAT(UINT64_MAX); constraint_hash_add(layer->constraints, c); - } else { - switch (type) { - case CONSTRAINT_MIN: - /* Increase minimum? */ - if (amount_msat_greater(limit, c->limit)) - c->limit = limit; - break; - case CONSTRAINT_MAX: - /* Decrease maximum? */ - if (amount_msat_less(limit, c->limit)) - c->limit = limit; - break; - } } + + /* Increase minimum? */ + if (min && amount_msat_greater(*min, c->min)) + c->min = *min; + + /* Decrease maximum? */ + if (max && amount_msat_less(*max, c->max)) + c->max = *max; + c->timestamp = timestamp; return c; } @@ -279,7 +265,7 @@ void layer_clear_overridden_capacities(const struct layer *layer, for (con = constraint_hash_first(layer->constraints, &conit); con; con = constraint_hash_next(layer->constraints, &conit)) { - struct gossmap_chan *c = gossmap_find_chan(gossmap, &con->key.scidd.scid); + struct gossmap_chan *c = gossmap_find_chan(gossmap, &con->scidd.scid); size_t idx; if (!c) continue; @@ -425,16 +411,12 @@ void json_add_constraint(struct json_stream *js, json_object_start(js, fieldname); if (layer) json_add_string(js, "layer", layer->name); - json_add_short_channel_id_dir(js, "short_channel_id_dir", c->key.scidd); + json_add_short_channel_id_dir(js, "short_channel_id_dir", c->scidd); json_add_u64(js, "timestamp", c->timestamp); - switch (c->key.type) { - case CONSTRAINT_MIN: - json_add_amount_msat(js, "minimum_msat", c->limit); - break; - case CONSTRAINT_MAX: - json_add_amount_msat(js, "maximum_msat", c->limit); - break; - } + if (!amount_msat_is_zero(c->min)) + json_add_amount_msat(js, "minimum_msat", c->min); + if (!amount_msat_eq(c->max, AMOUNT_MSAT(UINT64_MAX))) + json_add_amount_msat(js, "maximum_msat", c->max); json_object_end(js); } diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 8867ba701138..70fbbcf114c9 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -17,22 +17,15 @@ struct askrene; struct layer; struct json_stream; -enum constraint_type { - CONSTRAINT_MIN, - CONSTRAINT_MAX, -}; - -struct constraint_key { - struct short_channel_id_dir scidd; - enum constraint_type type; -}; - /* A constraint reflects something we learned about a channel */ struct constraint { - struct constraint_key key; + struct short_channel_id_dir scidd; /* Time this constraint was last updated */ u64 timestamp; - struct amount_msat limit; + /* Non-zero means set */ + struct amount_msat min; + /* Non-0xFFFFF.... means set */ + struct amount_msat max; }; /* Look up a layer by name. */ @@ -80,15 +73,14 @@ void layer_clear_overridden_capacities(const struct layer *layer, /* Find a constraint in a layer. */ const struct constraint *layer_find_constraint(const struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type); + const struct short_channel_id_dir *scidd); -/* Add/update a constraint on a layer. */ +/* Add/update one or more constraints on a layer. */ const struct constraint *layer_update_constraint(struct layer *layer, const struct short_channel_id_dir *scidd, - enum constraint_type type, u64 timestamp, - struct amount_msat limit); + const struct amount_msat *min, + const struct amount_msat *max); /* Add local channels from this layer. zero_cost means set fees and delay to 0. */ void layer_add_localmods(const struct layer *layer, diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c index 7c8e49395529..a58bc6d5f24e 100644 --- a/plugins/askrene/reserve.c +++ b/plugins/askrene/reserve.c @@ -25,12 +25,6 @@ reserve_scidd(const struct reserve *r) return &r->rhop.scidd; } -static size_t hash_scidd(const struct short_channel_id_dir *scidd) -{ - /* scids cost money to generate, so simple hash works here */ - return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 >> 16) ^ (scidd->scid.u64 << 1) ^ scidd->dir; -} - static bool reserve_eq_scidd(const struct reserve *r, const struct short_channel_id_dir *scidd) { diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 0b8bbab10074..7924c02e1b3f 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -161,7 +161,9 @@ def test_layers(node_factory): 100000, 'unconstrained') last_timestamp = int(time.time()) + 1 + # Maximum for created channels is the real capacity. expect['constraints'].append({'short_channel_id_dir': '0x0x1/1', + 'maximum_msat': 1000000000, 'minimum_msat': 100000}) # Check timestamp first. listlayers = l2.rpc.askrene_listlayers('test_layers') From 2b08b25f1a6c1767a08f646dc620907a1d0270e6 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:00:53 +0930 Subject: [PATCH 13/24] askrene: remove unused parameter in layer_add_localmods. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 2 +- plugins/askrene/layer.c | 13 +++---------- plugins/askrene/layer.h | 3 +-- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index f3a0e5c8a4db..f98c78d82632 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -293,7 +293,7 @@ static const char *get_routes(const tal_t *ctx, tal_arr_expand(&rq->layers, l); /* FIXME: Implement localmods_merge, and cache this in layer? */ - layer_add_localmods(l, rq->gossmap, false, localmods); + layer_add_localmods(l, rq->gossmap, localmods); /* Clear any entries in capacities array if we * override them (incl local channels) */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 2761694769ba..fa7a372f62e3 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -305,7 +305,6 @@ void layer_add_disabled_channel(struct layer *layer, const struct short_channel_ void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, - bool zero_cost, struct gossmap_localmods *localmods) { const struct local_channel *lc; @@ -345,20 +344,14 @@ void layer_add_localmods(const 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, - base, propfee, delay, + lc->half[i].base_fee.millisatoshis, /* Raw: gossmap */ + lc->half[i].proportional_fee, + lc->half[i].delay, true, i); } diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 70fbbcf114c9..be5c4cddcfee 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -82,10 +82,9 @@ const struct constraint *layer_update_constraint(struct layer *layer, const struct amount_msat *min, const struct amount_msat *max); -/* Add local channels from this layer. zero_cost means set fees and delay to 0. */ +/* Add local channels from this layer. */ 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. */ From 51294e645ae193db72535709101f2ca3ba661be4 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:01:53 +0930 Subject: [PATCH 14/24] gossmods_from_listpeerchannels: use correct type for cltv_delta. Doesn't matter now, but will with the next change where we want to pass a pointer. Signed-off-by: Rusty Russell --- common/gossmods_listpeerchannels.c | 4 ++-- common/gossmods_listpeerchannels.h | 6 +++--- plugins/askrene/askrene.c | 2 +- plugins/renepay/mods.c | 2 +- plugins/test/run-route-calc.c | 4 ++-- plugins/test/run-route-overlong.c | 4 ++-- plugins/topology.c | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/common/gossmods_listpeerchannels.c b/common/gossmods_listpeerchannels.c index 73864241302e..52b30c8bc9f5 100644 --- a/common/gossmods_listpeerchannels.c +++ b/common/gossmods_listpeerchannels.c @@ -14,7 +14,7 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf UNUSED, const jsmntok_t *chantok UNUSED, @@ -51,7 +51,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, struct amount_msat sr_able, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf, const jsmntok_t *chantok, diff --git a/common/gossmods_listpeerchannels.h b/common/gossmods_listpeerchannels.h index a9847c882645..37272a43439c 100644 --- a/common/gossmods_listpeerchannels.h +++ b/common/gossmods_listpeerchannels.h @@ -35,7 +35,7 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf_, const jsmntok_t *chantok, @@ -54,7 +54,7 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx, struct amount_msat, \ struct amount_msat, \ u32, \ - u32, \ + u16, \ bool, \ const char *, \ const jsmntok_t *), \ @@ -70,7 +70,7 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf UNUSED, const jsmntok_t *chantok UNUSED, diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index f98c78d82632..2b34f1cf996a 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -559,7 +559,7 @@ static void add_localchan(struct gossmap_localmods *mods, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf, const jsmntok_t *chantok, diff --git a/plugins/renepay/mods.c b/plugins/renepay/mods.c index 49b420f66e9b..11dca910d5f7 100644 --- a/plugins/renepay/mods.c +++ b/plugins/renepay/mods.c @@ -389,7 +389,7 @@ static void gossmod_cb(struct gossmap_localmods *mods, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf, const jsmntok_t *chantok, diff --git a/plugins/test/run-route-calc.c b/plugins/test/run-route-calc.c index da73c25a9a8b..194984128a69 100644 --- a/plugins/test/run-route-calc.c +++ b/plugins/test/run-route-calc.c @@ -47,7 +47,7 @@ void gossmod_add_localchan(struct gossmap_localmods *mods UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf UNUSED UNNEEDED, const jsmntok_t *chantok UNUSED UNNEEDED, @@ -68,7 +68,7 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx UNNEE struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf_ UNNEEDED, const jsmntok_t *chantok UNNEEDED, diff --git a/plugins/test/run-route-overlong.c b/plugins/test/run-route-overlong.c index 4a7d94b3f7d2..ce9d7a3e1715 100644 --- a/plugins/test/run-route-overlong.c +++ b/plugins/test/run-route-overlong.c @@ -44,7 +44,7 @@ void gossmod_add_localchan(struct gossmap_localmods *mods UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf UNUSED UNNEEDED, const jsmntok_t *chantok UNUSED UNNEEDED, @@ -65,7 +65,7 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx UNNEE struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf_ UNNEEDED, const jsmntok_t *chantok UNNEEDED, diff --git a/plugins/topology.c b/plugins/topology.c index 972853e3826b..755fa84e3dd7 100644 --- a/plugins/topology.c +++ b/plugins/topology.c @@ -367,7 +367,7 @@ static void gossmod_add_unknown_localchan(struct gossmap_localmods *mods, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf UNUSED, const jsmntok_t *chantok UNUSED, From c24491aab1bca1c2f9c904b3f17e6804bb0e9b1f Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:02:53 +0930 Subject: [PATCH 15/24] pytest: enhance test_getroutes_auto_sourcefree with same case *without* auto.sourcefree. Signed-off-by: Rusty Russell --- tests/test_askrene.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 7924c02e1b3f..40edd3cced78 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -426,6 +426,21 @@ def test_getroutes_auto_sourcefree(node_factory): # Set up l1 with this as the gossip_store l1 = node_factory.get_node(gossip_store_file=gsfile.name) + # Without sourcefree: + assert l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000, + layers=[], + maxfee_msat=1000, + final_cltv=99) == {'probability_ppm': 999999, + 'routes': [{'probability_ppm': 999999, + 'final_cltv': 99, + 'amount_msat': 1000, + 'path': [{'short_channel_id_dir': '0x1x0/1', + 'next_node_id': nodemap[1], + 'amount_msat': 1010, + 'delay': 105}]}]} + # Start easy assert l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], From 792525bcf2644c0b84576349c6065d34c2de439d Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:03:53 +0930 Subject: [PATCH 16/24] askrene: ignore disabled channels for min-cost-flow. We also set htlc_max to 0 when disabling, so the tests worked, but this is correct. Signed-off-by: Rusty Russell --- plugins/askrene/mcf.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 8a72c6a229fe..aaa914b03afe 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -626,7 +626,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 (!gossmap_chan_set(c, half) || !c->half[half].enabled) continue; const u32 chan_id = gossmap_chan_idx(gossmap, c); From 0239ad960475837e40670b3338dc5e1997577aad Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:04:53 +0930 Subject: [PATCH 17/24] gossmap: implement partial updates. This is actually what we want in several places: to only override one or two fields in a channel_update. We add a gossmap_local_setchan() with a similar API to the old gossmap_local_updatechan(), for the case where we want to set every field. Signed-off-by: Rusty Russell --- common/gossmap.c | 189 ++++++++++++++++++------- common/gossmap.h | 30 ++-- common/gossmods_listpeerchannels.c | 12 +- common/test/run-gossmap_local.c | 24 ++-- common/test/run-route-infloop.c | 19 +-- plugins/askrene/askrene.c | 45 +++--- plugins/askrene/layer.c | 52 ++++--- plugins/establish_onion_path.c | 14 +- plugins/renepay/mods.c | 30 ++-- plugins/renepay/test/run-mcf-diamond.c | 52 +++---- plugins/renepay/test/run-mcf.c | 12 +- 11 files changed, 281 insertions(+), 198 deletions(-) diff --git a/common/gossmap.c b/common/gossmap.c index 5427c5e1cd4d..8b7e0de35370 100644 --- a/common/gossmap.c +++ b/common/gossmap.c @@ -760,24 +760,34 @@ static void destroy_map(struct gossmap *map) } /* Local modifications. We only expect a few, so we use a simple - * array. */ + * array. If this changes, use a hashtable and a storage area for all + * those pointers to avoid dynamic allocation overhead! */ struct localmod { struct short_channel_id scid; /* If this is an entirely-local channel, here's its offset. * Otherwise, 0xFFFFFFFF. */ u32 local_off; - /* Are updates in either direction set? */ - bool updates_set[2]; - /* hc[n] defined if updates_set[n]. */ - struct half_chan hc[2]; - /* orig[n] defined if updates_set[n] and local_off == 0xFFFFFFFF */ + /* Non-NULL values mean change existing ones */ + struct localmod_changes { + const bool *enabled; + const fp16_t *htlc_min, *htlc_max; + const u32 *base_fee, *proportional_fee; + const u16 *delay; + } changes[2]; + + /* orig[n] defined if local_off == 0xFFFFFFFF */ struct half_chan orig[2]; /* Original update offsets */ u32 orig_cupdate_off[2]; }; +static bool localmod_is_local_chan(const struct localmod *mod) +{ + return mod->local_off != 0xFFFFFFFF; +} + struct gossmap_localmods { struct localmod *mods; /* This is the local array to be used by the gossmap */ @@ -840,7 +850,7 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, return gossmap_local_addchan(localmods, n2, n1, scid, features); mod.scid = scid; - mod.updates_set[0] = mod.updates_set[1] = false; + memset(&mod.changes, 0, sizeof(mod.changes)); /* We create fake local channel_announcement. */ off = insert_local_space(localmods, @@ -885,53 +895,100 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, return true; }; -/* Insert a local-only channel_update. */ +/* Insert a local-only channel_update: false if can't represent. */ bool gossmap_local_updatechan(struct gossmap_localmods *localmods, - struct short_channel_id scid, - struct amount_msat htlc_min, - struct amount_msat htlc_max, - u32 base_fee, - u32 proportional_fee, - u16 delay, - bool enabled, - int dir) + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay) { struct localmod *mod; + struct localmod_changes *lc; + struct half_chan test; + + /* Check fit before making any changes. */ + if (base_fee) { + test.base_fee = base_fee->millisatoshis /* Raw: localmod */; + if (!amount_msat_eq(amount_msat(test.base_fee), *base_fee)) + return false; + } + if (proportional_fee) { + test.proportional_fee = *proportional_fee; + if (test.proportional_fee != *proportional_fee) + return false; + } + if (delay) { + test.delay = *delay; + if (test.delay != *delay) + return false; + } - mod = find_localmod(localmods, scid); + mod = find_localmod(localmods, scidd->scid); if (!mod) { /* Create new reference to (presumably) existing channel. */ size_t nmods = tal_count(localmods->mods); tal_resize(&localmods->mods, nmods + 1); mod = &localmods->mods[nmods]; - mod->scid = scid; - mod->updates_set[0] = mod->updates_set[1] = false; + mod->scid = scidd->scid; + memset(&mod->changes, 0, sizeof(mod->changes)); mod->local_off = 0xFFFFFFFF; } - assert(dir == 0 || dir == 1); - mod->updates_set[dir] = true; - mod->hc[dir].enabled = enabled; - /* node_idx needs to be set once we're in the gossmap. */ - mod->hc[dir].htlc_min - = u64_to_fp16(htlc_min.millisatoshis, /* Raw: to fp16 */ - false); - mod->hc[dir].htlc_max - = u64_to_fp16(htlc_max.millisatoshis, /* Raw: to fp16 */ - true); - mod->hc[dir].base_fee = base_fee; - mod->hc[dir].proportional_fee = proportional_fee; - mod->hc[dir].delay = delay; - - /* Check they fit */ - if (mod->hc[dir].base_fee != base_fee - || mod->hc[dir].proportional_fee != proportional_fee - || mod->hc[dir].delay != delay) - return false; + lc = &mod->changes[scidd->dir]; + if (enabled) { + tal_free(lc->enabled); + lc->enabled = tal_dup(localmods, bool, enabled); + } + if (htlc_min) { + fp16_t min = u64_to_fp16(htlc_min->millisatoshis, /* Raw: to fp16 */ + false); + tal_free(lc->htlc_min); + lc->htlc_min = tal_dup(localmods, fp16_t, &min); + } + if (htlc_max) { + fp16_t max = u64_to_fp16(htlc_max->millisatoshis, /* Raw: to fp16 */ + true); + tal_free(lc->htlc_max); + lc->htlc_max = tal_dup(localmods, fp16_t, &max); + } + if (base_fee) { + u32 base_as_u32 = base_fee->millisatoshis; /* Raw: localmod */ + tal_free(lc->base_fee); + lc->base_fee = tal_dup(localmods, u32, &base_as_u32); + } + if (proportional_fee) { + tal_free(lc->proportional_fee); + lc->proportional_fee = tal_dup(localmods, u32, proportional_fee); + } + if (delay) { + tal_free(lc->delay); + lc->delay = tal_dup(localmods, u16, delay); + } return true; } +bool gossmap_local_setchan(struct gossmap_localmods *localmods, + struct short_channel_id scid, + struct amount_msat htlc_min, + struct amount_msat htlc_max, + struct amount_msat base_fee, + u32 proportional_fee, + u16 delay, + bool enabled, + int dir) +{ + struct short_channel_id_dir scidd = {scid, dir}; + return gossmap_local_updatechan(localmods, &scidd, + &enabled, + &htlc_min, &htlc_max, + &base_fee, &proportional_fee, + &delay); +} + /* Apply localmods to this map */ void gossmap_apply_localmods(struct gossmap *map, struct gossmap_localmods *localmods) @@ -949,22 +1006,56 @@ void gossmap_apply_localmods(struct gossmap *map, chan = gossmap_find_chan(map, &mod->scid); /* If it doesn't exist, are we supposed to create a local one? */ if (!chan) { - if (mod->local_off == 0xFFFFFFFF) + if (!localmod_is_local_chan(mod)) continue; /* Create new channel, pointing into local. */ chan = add_channel(map, map->map_size + mod->local_off, 0); } - /* Save old, overwrite (keep nodeidx) */ + /* Save old, update any fields they wanted to change */ for (size_t h = 0; h < 2; h++) { - if (!mod->updates_set[h]) - continue; + bool was_set, all_changed; + const struct localmod_changes *c = &mod->changes[h]; + /* Save existing versions */ mod->orig[h] = chan->half[h]; mod->orig_cupdate_off[h] = chan->cupdate_off[h]; - chan->half[h] = mod->hc[h]; - chan->half[h].nodeidx = mod->orig[h].nodeidx; - chan->cupdate_off[h] = 0xFFFFFFFF; + + was_set = gossmap_chan_set(chan, h); + + /* Override specified fields. */ + all_changed = true; + if (c->enabled) + chan->half[h].enabled = *c->enabled; + else + all_changed = false; + if (c->htlc_min) + chan->half[h].htlc_min = *c->htlc_min; + else + all_changed = false; + if (c->htlc_max) + chan->half[h].htlc_max = *c->htlc_max; + else + all_changed = false; + if (c->base_fee) + chan->half[h].base_fee = *c->base_fee; + else + all_changed = false; + if (c->proportional_fee) + chan->half[h].proportional_fee = *c->proportional_fee; + else + all_changed = false; + if (c->delay) + chan->half[h].delay = *c->delay; + else + all_changed = false; + + /* Is it all defined? + * This controls gossmap_chan_set(chan, h); */ + if (was_set || all_changed) + chan->cupdate_off[h] = 0xFFFFFFFF; + else + chan->cupdate_off[h] = 0; } } } @@ -988,15 +1079,9 @@ void gossmap_remove_localmods(struct gossmap *map, if (chan->cann_off >= map->map_size) { gossmap_remove_chan(map, chan); } else { - /* Restore (keep nodeidx). */ + /* Restore. */ for (size_t h = 0; h < 2; h++) { - u32 nodeidx; - if (!mod->updates_set[h]) - continue; - - nodeidx = chan->half[h].nodeidx; chan->half[h] = mod->orig[h]; - chan->half[h].nodeidx = nodeidx; chan->cupdate_off[h] = mod->orig_cupdate_off[h]; } } diff --git a/common/gossmap.h b/common/gossmap.h index ade384f16465..ad45ec19ee5b 100644 --- a/common/gossmap.h +++ b/common/gossmap.h @@ -99,17 +99,27 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, /* Create a local-only channel_update: can apply to lcoal-only or * normal channels. Returns false if amounts don't fit in our - * internal representation (implies channel unusable anyway). */ + * internal representation (implies channel unusable anyway). Any + * NULL arguments mean "leave as is". */ bool gossmap_local_updatechan(struct gossmap_localmods *localmods, - struct short_channel_id scid, - struct amount_msat htlc_min, - struct amount_msat htlc_max, - u32 base_fee, - u32 proportional_fee, - u16 delay, - bool enabled, - int dir) - NO_NULL_ARGS; + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay); + +/* Convenience version which sets everything (older API) */ +bool gossmap_local_setchan(struct gossmap_localmods *localmods, + struct short_channel_id scid, + struct amount_msat htlc_min, + struct amount_msat htlc_max, + struct amount_msat base_fee, + u32 proportional_fee, + u16 delay, + bool enabled, + int dir); /* Apply localmods to this map */ void gossmap_apply_localmods(struct gossmap *map, diff --git a/common/gossmods_listpeerchannels.c b/common/gossmods_listpeerchannels.c index 52b30c8bc9f5..d54d292d5dbe 100644 --- a/common/gossmods_listpeerchannels.c +++ b/common/gossmods_listpeerchannels.c @@ -28,12 +28,12 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, /* FIXME: features? */ gossmap_local_addchan(mods, self, peer, scidd->scid, NULL); - gossmap_local_updatechan(mods, scidd->scid, min, max, - fee_base.millisatoshis, /* Raw: gossmap */ - fee_proportional, - cltv_delta, - enabled, - scidd->dir); + gossmap_local_updatechan(mods, scidd, + &enabled, + &min, &max, + &fee_base, + &fee_proportional, + &cltv_delta); } struct gossmap_localmods * diff --git a/common/test/run-gossmap_local.c b/common/test/run-gossmap_local.c index 376e4025e12f..5ae8d47d4fdb 100644 --- a/common/test/run-gossmap_local.c +++ b/common/test/run-gossmap_local.c @@ -485,25 +485,25 @@ int main(int argc, char *argv[]) assert(!gossmap_find_node(map, &l4)); /* Now update it both local, and an existing one. */ - gossmap_local_updatechan(mods, scid_local, - AMOUNT_MSAT(1), - AMOUNT_MSAT(100000), - 2, 3, 4, true, 0); + gossmap_local_setchan(mods, scid_local, + AMOUNT_MSAT(1), + AMOUNT_MSAT(100000), + AMOUNT_MSAT(2), 3, 4, true, 0); /* Adding an existing channel is a noop. */ assert(gossmap_local_addchan(mods, &l2, &l3, scid23, NULL)); - gossmap_local_updatechan(mods, scid23, - AMOUNT_MSAT(99), - AMOUNT_MSAT(100), - 101, 102, 103, true, 0); + gossmap_local_setchan(mods, scid23, + AMOUNT_MSAT(99), + AMOUNT_MSAT(100), + AMOUNT_MSAT(101), 102, 103, true, 0); /* We can "update" a channel which doesn't exist, and it's a noop */ scid_nonexisting.u64 = 1; - gossmap_local_updatechan(mods, scid_nonexisting, - AMOUNT_MSAT(1), - AMOUNT_MSAT(100000), - 2, 3, 4, false, 0); + gossmap_local_setchan(mods, scid_nonexisting, + AMOUNT_MSAT(1), + AMOUNT_MSAT(100000), + AMOUNT_MSAT(2), 3, 4, false, 0); gossmap_apply_localmods(map, mods); chan = gossmap_find_chan(map, &scid_local); diff --git a/common/test/run-route-infloop.c b/common/test/run-route-infloop.c index f4c3f9071f7a..0bd3cb04abc6 100644 --- a/common/test/run-route-infloop.c +++ b/common/test/run-route-infloop.c @@ -138,17 +138,18 @@ int main(int argc, char *argv[]) /* We overlay our own channels as zero fee & delay, since we don't pay fees */ struct gossmap_localmods *localmods = gossmap_localmods_new(gossmap); for (size_t i = 0; i < me->num_chans; i++) { - int dir; - struct short_channel_id scid; - struct gossmap_chan *c = gossmap_nth_chan(gossmap, me, i, &dir); + struct short_channel_id_dir scidd; + const struct amount_msat base_fee = AMOUNT_MSAT(0); + const u32 proportional_fee = 0; + struct gossmap_chan *c = gossmap_nth_chan(gossmap, me, i, &scidd.dir); - if (!c->half[dir].enabled) + if (!c->half[scidd.dir].enabled) continue; - scid = gossmap_chan_scid(gossmap, c); - assert(gossmap_local_updatechan(localmods, scid, - amount_msat(fp16_to_u64(c->half[dir].htlc_min)), - amount_msat(fp16_to_u64(c->half[dir].htlc_max)), - 0, 0, 0, true, dir)); + scidd.scid = gossmap_chan_scid(gossmap, c); + assert(gossmap_local_updatechan(localmods, &scidd, + NULL, NULL, NULL, + &base_fee, &proportional_fee, + NULL)); } gossmap_apply_localmods(gossmap, localmods); diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 2b34f1cf996a..c97586201994 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -183,11 +183,11 @@ static void add_free_source(struct plugin *plugin, /* We apply existing localmods, save up mods we want, then append * them: it's not safe to modify localmods while they are applied! */ const struct gossmap_node *srcnode; - struct mod { - struct short_channel_id_dir scidd; - fp16_t htlc_min, htlc_max; - bool enabled; - } *mods = tal_arr(tmpctx, struct mod, 0); + const struct amount_msat zero_base_fee = AMOUNT_MSAT(0); + const u16 zero_delay = 0; + const u32 zero_prop_fee = 0; + struct short_channel_id_dir *scidds + = tal_arr(tmpctx, struct short_channel_id_dir, 0); gossmap_apply_localmods(gossmap, localmods); @@ -195,35 +195,24 @@ static void add_free_source(struct plugin *plugin, srcnode = gossmap_find_node(gossmap, source); for (size_t i = 0; srcnode && i < srcnode->num_chans; i++) { + struct short_channel_id_dir scidd; const struct gossmap_chan *c; - const struct half_chan *h; - struct mod mod; - - c = gossmap_nth_chan(gossmap, srcnode, i, &mod.scidd.dir); - h = &c->half[mod.scidd.dir]; - mod.scidd.scid = gossmap_chan_scid(gossmap, c); - mod.htlc_min = h->htlc_min; - mod.htlc_max = h->htlc_max; - mod.enabled = h->enabled; - tal_arr_expand(&mods, mod); + c = gossmap_nth_chan(gossmap, srcnode, i, &scidd.dir); + scidd.scid = gossmap_chan_scid(gossmap, c); + tal_arr_expand(&scidds, scidd); } gossmap_remove_localmods(gossmap, localmods); - /* Now we can update localmods */ - for (size_t i = 0; i < tal_count(mods); i++) { + /* Now we can update localmods: we only change fee levels and delay */ + for (size_t i = 0; i < tal_count(scidds); i++) { if (!gossmap_local_updatechan(localmods, - mods[i].scidd.scid, - /* Keep min and max */ - /* FIXME: lossy conversion! */ - amount_msat(fp16_to_u64(mods[i].htlc_min)), - amount_msat(fp16_to_u64(mods[i].htlc_max)), - 0, 0, 0, - /* Keep enabled flag */ - mods[i].enabled, - mods[i].scidd.dir)) - plugin_err(plugin, "Could not zero fee on %s", - fmt_short_channel_id_dir(tmpctx, &mods[i].scidd)); + &scidds[i], + NULL, NULL, NULL, + &zero_base_fee, &zero_prop_fee, + &zero_delay)) + plugin_err(plugin, "Could not zero fee/delay on %s", + fmt_short_channel_id_dir(tmpctx, &scidds[i])); } } diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index fa7a372f62e3..1e21370c3c78 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -319,22 +319,19 @@ void layer_add_localmods(const struct layer *layer, if (!node) continue; for (size_t n = 0; n < node->num_chans; n++) { - struct short_channel_id scid; + struct short_channel_id_dir scidd; struct gossmap_chan *c; - int dir; - c = gossmap_nth_chan(gossmap, node, n, &dir); - scid = gossmap_chan_scid(gossmap, c); + bool enabled = false; + struct amount_msat zero = AMOUNT_MSAT(0); + c = gossmap_nth_chan(gossmap, node, n, &scidd.dir); + scidd.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); + &scidd, + &enabled, + &zero, &zero, + NULL, NULL, NULL); } } @@ -344,30 +341,29 @@ void layer_add_localmods(const 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++) { + struct short_channel_id_dir scidd; + bool enabled = true; if (!lc->half[i].enabled) continue; - 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, - true, - i); + scidd.scid = lc->scid; + scidd.dir = i; + gossmap_local_updatechan(localmods, &scidd, + &enabled, + &lc->half[i].htlc_min, + &lc->half[i].htlc_max, + &lc->half[i].base_fee, + &lc->half[i].proportional_fee, + &lc->half[i].delay); } } /* Now disable any channels they asked us to */ for (size_t i = 0; i < tal_count(layer->disabled_channels); i++) { + bool enabled = false; gossmap_local_updatechan(localmods, - layer->disabled_channels[i].scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(0), - 0, - 0, - 0, - false, - layer->disabled_channels[i].dir); + &layer->disabled_channels[i], + &enabled, + NULL, NULL, NULL, NULL, NULL); } } diff --git a/plugins/establish_onion_path.c b/plugins/establish_onion_path.c index 9906a55ea28f..1b5cb732076b 100644 --- a/plugins/establish_onion_path.c +++ b/plugins/establish_onion_path.c @@ -95,7 +95,8 @@ gossmods_from_listpeers(const tal_t *ctx, struct node_id peer_id; const char *err; u8 *features = NULL; - struct short_channel_id fake_scid; + struct short_channel_id_dir fake_scidd; + bool enabled = true; err = json_scan(tmpctx, buf, peer, "{connected:%," @@ -112,13 +113,12 @@ gossmods_from_listpeers(const tal_t *ctx, continue; /* Add a fake channel */ - fake_scid.u64 = i; + fake_scidd.scid.u64 = i; + fake_scidd.dir = node_id_idx(self, &peer_id); - gossmap_local_addchan(mods, self, &peer_id, fake_scid, NULL); - gossmap_local_updatechan(mods, fake_scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(0), - 0, 0, 0, true, node_id_idx(self, &peer_id)); + gossmap_local_addchan(mods, self, &peer_id, fake_scidd.scid, NULL); + gossmap_local_updatechan(mods, &fake_scidd, &enabled, + NULL, NULL, NULL, NULL, NULL); } return mods; } diff --git a/plugins/renepay/mods.c b/plugins/renepay/mods.c index 11dca910d5f7..b132d5231362 100644 --- a/plugins/renepay/mods.c +++ b/plugins/renepay/mods.c @@ -409,13 +409,10 @@ static void gossmod_cb(struct gossmap_localmods *mods, /* FIXME: features? */ gossmap_local_addchan(mods, self, peer, scidd->scid, NULL); - - gossmap_local_updatechan(mods, scidd->scid, min, max, - fee_base.millisatoshis, /* Raw: gossmap */ - fee_proportional, - cltv_delta, - enabled, - scidd->dir); + gossmap_local_updatechan(mods, scidd, + &enabled, + &min, &max, + &fee_base, &fee_proportional, &cltv_delta); /* Is it disabled? */ if (!enabled) @@ -516,13 +513,19 @@ static void add_hintchan(struct payment *payment, const struct node_id *src, assert(payment); assert(payment->local_gossmods); - int dir = node_id_idx(src, dst); - const char *errmsg; const struct chan_extra *ce = uncertainty_find_channel(pay_plugin->uncertainty, scid); if (!ce) { + struct short_channel_id_dir scidd; + /* We assume any HTLC is allowed */ + struct amount_msat htlc_min = AMOUNT_MSAT(0), htlc_max = MAX_CAPACITY; + struct amount_msat fee_base = amount_msat(fee_base_msat); + bool enabled = true; + scidd.scid = scid; + scidd.dir = node_id_idx(src, dst); + /* This channel is not public, we don't know his capacity One possible solution is set the capacity to MAX_CAP and the state to [0,MAX_CAP]. Alternatively we could @@ -542,11 +545,10 @@ static void add_hintchan(struct payment *payment, const struct node_id *src, if (!gossmap_local_addchan(payment->local_gossmods, src, dst, scid, NULL) || !gossmap_local_updatechan( - payment->local_gossmods, scid, - /* We assume any HTLC is allowed */ - AMOUNT_MSAT(0), MAX_CAPACITY, fee_base_msat, - fee_proportional_millionths, cltv_expiry_delta, true, - dir)) { + payment->local_gossmods, &scidd, + &enabled, &htlc_min, &htlc_max, + &fee_base, &fee_proportional_millionths, + &cltv_expiry_delta)) { errmsg = tal_fmt( tmpctx, "Failed to update scid=%s in the local_gossmods.", diff --git a/plugins/renepay/test/run-mcf-diamond.c b/plugins/renepay/test/run-mcf-diamond.c index 57e90db1dca2..85ceb6888346 100644 --- a/plugins/renepay/test/run-mcf-diamond.c +++ b/plugins/renepay/test/run-mcf-diamond.c @@ -134,35 +134,35 @@ int main(int argc, char *argv[]) /* 1->2->4 has capacity 10k sat, 1->3->4 has capacity 5k sat (lower fee!) */ assert(gossmap_local_addchan(mods, &l1, &l2, scid12, NULL)); - assert(gossmap_local_updatechan(mods, scid12, - /*htlc_min=*/ AMOUNT_MSAT(0), - /*htlc_max=*/ AMOUNT_MSAT(10000000), - /*base_fee=*/ 0, - /*ppm_fee =*/ 1001, - /* delay =*/ 5, - /* enabled=*/ true, - /* dir =*/ 0)); + assert(gossmap_local_setchan(mods, scid12, + /*htlc_min=*/ AMOUNT_MSAT(0), + /*htlc_max=*/ AMOUNT_MSAT(10000000), + /*base_fee=*/ AMOUNT_MSAT(0), + /*ppm_fee =*/ 1001, + /* delay =*/ 5, + /* enabled=*/ true, + /* dir =*/ 0)); assert(gossmap_local_addchan(mods, &l2, &l4, scid24, NULL)); - assert(gossmap_local_updatechan(mods, scid24, - AMOUNT_MSAT(0), - AMOUNT_MSAT(10000000), - 0, 1002, 5, - true, - 0)); + assert(gossmap_local_setchan(mods, scid24, + AMOUNT_MSAT(0), + AMOUNT_MSAT(10000000), + AMOUNT_MSAT(0), 1002, 5, + true, + 0)); assert(gossmap_local_addchan(mods, &l1, &l3, scid13, NULL)); - assert(gossmap_local_updatechan(mods, scid13, - AMOUNT_MSAT(0), - AMOUNT_MSAT(5000000), - 0, 503, 5, - true, - 0)); + assert(gossmap_local_setchan(mods, scid13, + AMOUNT_MSAT(0), + AMOUNT_MSAT(5000000), + AMOUNT_MSAT(0), 503, 5, + true, + 0)); assert(gossmap_local_addchan(mods, &l3, &l4, scid34, NULL)); - assert(gossmap_local_updatechan(mods, scid34, - AMOUNT_MSAT(0), - AMOUNT_MSAT(5000000), - 0, 504, 5, - true, - 0)); + assert(gossmap_local_setchan(mods, scid34, + AMOUNT_MSAT(0), + AMOUNT_MSAT(5000000), + AMOUNT_MSAT(0), 504, 5, + true, + 0)); gossmap_apply_localmods(gossmap, mods); chan_extra_map = tal(tmpctx, struct chan_extra_map); diff --git a/plugins/renepay/test/run-mcf.c b/plugins/renepay/test/run-mcf.c index 79c291fe92c4..510c12bf726b 100644 --- a/plugins/renepay/test/run-mcf.c +++ b/plugins/renepay/test/run-mcf.c @@ -481,12 +481,12 @@ int main(int argc, char *argv[]) /* 400,000sat channel from 1->3, basefee 0, ppm 1000, delay 5 */ assert(gossmap_local_addchan(mods, &l1, &l3, scid13, NULL)); - assert(gossmap_local_updatechan(mods, scid13, - AMOUNT_MSAT(0), - AMOUNT_MSAT(400000000), - 0, 1000, 5, - true, - 0)); + assert(gossmap_local_setchan(mods, scid13, + AMOUNT_MSAT(0), + AMOUNT_MSAT(400000000), + AMOUNT_MSAT(0), 1000, 5, + true, + 0)); /* Apply changes, check they work. */ gossmap_apply_localmods(gossmap, mods); From 2e6771b350b1deee9491341aa15c3a5c02d167e2 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:05:53 +0930 Subject: [PATCH 18/24] common/gossmods_listpeerchannels: include channel capacity in callback. Signed-off-by: Rusty Russell --- common/gossmods_listpeerchannels.c | 12 +++++++++--- common/gossmods_listpeerchannels.h | 3 +++ plugins/askrene/askrene.c | 3 ++- plugins/renepay/mods.c | 1 + plugins/test/run-route-calc.c | 2 ++ plugins/test/run-route-overlong.c | 2 ++ plugins/topology.c | 4 +++- 7 files changed, 22 insertions(+), 5 deletions(-) diff --git a/common/gossmods_listpeerchannels.c b/common/gossmods_listpeerchannels.c index d54d292d5dbe..4ecbe536506e 100644 --- a/common/gossmods_listpeerchannels.c +++ b/common/gossmods_listpeerchannels.c @@ -9,6 +9,7 @@ void gossmod_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 capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, @@ -46,6 +47,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat sr_able, @@ -68,7 +70,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, struct short_channel_id alias; bool enabled; struct node_id dst; - struct amount_msat spendable, receivable, fee_base[NUM_SIDES], htlc_min[NUM_SIDES], htlc_max[NUM_SIDES]; + struct amount_msat capacity_msat, spendable, receivable, fee_base[NUM_SIDES], htlc_min[NUM_SIDES], htlc_max[NUM_SIDES]; u32 fee_proportional[NUM_SIDES], cltv_delta[NUM_SIDES]; const char *state, *err; @@ -87,6 +89,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, "peer_connected:%," "state:%," "peer_id:%," + "total_msat?:%," "updates?:{" "local" ":{fee_base_msat:%," @@ -108,6 +111,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, JSON_SCAN(json_to_bool, &enabled), JSON_SCAN_TAL(tmpctx, json_strdup, &state), JSON_SCAN(json_to_node_id, &dst), + JSON_SCAN(json_to_msat, &capacity_msat), JSON_SCAN(json_to_msat, &fee_base[LOCAL]), JSON_SCAN(json_to_u32, &fee_proportional[LOCAL]), JSON_SCAN(json_to_msat, &htlc_min[LOCAL]), @@ -148,7 +152,8 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, } /* We add both directions */ - cb(mods, self, &dst, &scidd, htlc_min[LOCAL], htlc_max[LOCAL], + cb(mods, self, &dst, &scidd, capacity_msat, + htlc_min[LOCAL], htlc_max[LOCAL], spendable, fee_base[LOCAL], fee_proportional[LOCAL], cltv_delta[LOCAL], enabled, buf, channel, cbarg); @@ -158,7 +163,8 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, scidd.dir = !scidd.dir; - cb(mods, self, &dst, &scidd, htlc_min[REMOTE], htlc_max[REMOTE], + cb(mods, self, &dst, &scidd, capacity_msat, + htlc_min[REMOTE], htlc_max[REMOTE], receivable, fee_base[REMOTE], fee_proportional[REMOTE], cltv_delta[REMOTE], enabled, buf, channel, cbarg); } diff --git a/common/gossmods_listpeerchannels.h b/common/gossmods_listpeerchannels.h index 37272a43439c..22eafdd3d143 100644 --- a/common/gossmods_listpeerchannels.h +++ b/common/gossmods_listpeerchannels.h @@ -30,6 +30,7 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx, const struct node_id *self_, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, @@ -53,6 +54,7 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx, struct amount_msat, \ struct amount_msat, \ struct amount_msat, \ + struct amount_msat, \ u32, \ u16, \ bool, \ @@ -65,6 +67,7 @@ void gossmod_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 capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index c97586201994..3bb915ce063a 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -543,6 +543,7 @@ 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 capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, @@ -558,7 +559,7 @@ static void add_localchan(struct gossmap_localmods *mods, const char *opener; const char *err; - gossmod_add_localchan(mods, self, peer, scidd, htlcmin, htlcmax, + gossmod_add_localchan(mods, self, peer, scidd, capacity_msat, htlcmin, htlcmax, spendable, fee_base, fee_proportional, cltv_delta, enabled, buf, chantok, info->local_layer); diff --git a/plugins/renepay/mods.c b/plugins/renepay/mods.c index b132d5231362..91ac7adf6a30 100644 --- a/plugins/renepay/mods.c +++ b/plugins/renepay/mods.c @@ -384,6 +384,7 @@ static void gossmod_cb(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, diff --git a/plugins/test/run-route-calc.c b/plugins/test/run-route-calc.c index 194984128a69..7a8d23a18fbf 100644 --- a/plugins/test/run-route-calc.c +++ b/plugins/test/run-route-calc.c @@ -42,6 +42,7 @@ void gossmod_add_localchan(struct gossmap_localmods *mods UNNEEDED, const struct node_id *self UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, @@ -63,6 +64,7 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx UNNEE const struct node_id *self_ UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, diff --git a/plugins/test/run-route-overlong.c b/plugins/test/run-route-overlong.c index ce9d7a3e1715..5aa1bb093d77 100644 --- a/plugins/test/run-route-overlong.c +++ b/plugins/test/run-route-overlong.c @@ -39,6 +39,7 @@ void gossmod_add_localchan(struct gossmap_localmods *mods UNNEEDED, const struct node_id *self UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, @@ -60,6 +61,7 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx UNNEE const struct node_id *self_ UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, diff --git a/plugins/topology.c b/plugins/topology.c index 755fa84e3dd7..b3da4c885e76 100644 --- a/plugins/topology.c +++ b/plugins/topology.c @@ -362,6 +362,7 @@ static void gossmod_add_unknown_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 capacity_msat, struct amount_msat min, struct amount_msat max, struct amount_msat spendable, @@ -376,7 +377,8 @@ static void gossmod_add_unknown_localchan(struct gossmap_localmods *mods, if (gossmap_find_chan(gossmap, &scidd->scid)) return; - gossmod_add_localchan(mods, self, peer, scidd, min, max, spendable, + gossmod_add_localchan(mods, self, peer, scidd, capacity_msat, + min, max, spendable, fee_base, fee_proportional, cltv_delta, enabled, buf, chantok, gossmap); } From b60de7679aa4dd220cb54bf2b74836b7ee0b0701 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:06:53 +0930 Subject: [PATCH 19/24] gossmap: keep capacity for locally-generated channels as well. It was weird not to have a capacity associated with localmods channels, and fixing it has some very nice side effects. Now the gossmap_chan_get_capacity() call never fails (we prevented reading of channels from gossmap in the partially-written case already), so we make it return the capacity. We do this in msat, because that's what all the callers want. Signed-off-by: Rusty Russell --- common/gossmap.c | 54 ++++--- common/gossmap.h | 8 +- common/gossmods_listpeerchannels.c | 3 +- common/test/run-gossmap_canned.c | 7 +- common/test/run-gossmap_local.c | 20 +-- common/test/run-route-infloop.c | 14 +- devtools/gossmap-compress.c | 15 +- plugins/askrene/askrene.c | 29 +--- plugins/askrene/layer.c | 3 +- plugins/establish_onion_path.c | 3 +- plugins/libplugin-pay.c | 5 +- plugins/renepay/chan_extra.c | 9 +- plugins/renepay/mods.c | 5 +- plugins/renepay/test/common.h | 9 +- plugins/renepay/test/run-bottleneck.c | 27 ++-- plugins/renepay/test/run-mcf-diamond.c | 8 +- plugins/renepay/test/run-mcf.c | 4 +- plugins/renepay/test/run-missingcapacity.c | 179 --------------------- plugins/renepay/test/run-testflow.c | 42 ++--- plugins/renepay/uncertainty.c | 6 +- plugins/test/Makefile | 3 +- plugins/test/run-route-overlong.c | 3 + plugins/topology.c | 8 +- 23 files changed, 132 insertions(+), 332 deletions(-) delete mode 100644 plugins/renepay/test/run-missingcapacity.c diff --git a/common/gossmap.c b/common/gossmap.c index 8b7e0de35370..990cb421cbca 100644 --- a/common/gossmap.c +++ b/common/gossmap.c @@ -474,8 +474,8 @@ static struct gossmap_chan *add_channel(struct gossmap *map, return NULL; } - /* gossipd writes WIRE_GOSSIP_STORE_CHANNEL_AMOUNT after this (not for - * local channels), so ignore channel_announcement until that appears */ + /* gossipd writes WIRE_GOSSIP_STORE_CHANNEL_AMOUNT after this, + * so ignore channel_announcement until that appears */ if (msglen && (map->map_size < cannounce_off + msglen + sizeof(struct gossip_hdr) + sizeof(u16) + sizeof(u64))) return NULL; @@ -828,6 +828,7 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, const struct node_id *n1, const struct node_id *n2, struct short_channel_id scid, + struct amount_msat capacity, const u8 *features) { be16 be16; @@ -847,15 +848,23 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, * compressed keys sorted in ascending lexicographic order. */ if (node_id_cmp(n1, n2) > 0) - return gossmap_local_addchan(localmods, n2, n1, scid, features); + return gossmap_local_addchan(localmods, n2, n1, scid, capacity, + features); mod.scid = scid; memset(&mod.changes, 0, sizeof(mod.changes)); - /* We create fake local channel_announcement. */ + /* We create amount, then fake local channel_announcement */ off = insert_local_space(localmods, - 2 + 64 * 4 + 2 + tal_bytelen(features) + 8 + 2 + 64 * 4 + 2 + tal_bytelen(features) + 32 + 8 + 33 + 33); + + /* Write amount */ + be64 = be64_to_cpu(capacity.millisatoshis / 1000 /* Raw: gossmap */); + memcpy(localmods->local + off, &be64, sizeof(be64)); + off += sizeof(be64); + + /* From here is a channel-announcment, with only the fields we use */ mod.local_off = off; /* Set type to be kosher. */ @@ -1224,34 +1233,39 @@ bool gossmap_chan_is_dying(const struct gossmap *map, return ghdr.flags & CPU_TO_BE16(GOSSIP_STORE_DYING_BIT); } -bool gossmap_chan_get_capacity(const struct gossmap *map, - const struct gossmap_chan *c, - struct amount_sat *amount) +struct amount_msat gossmap_chan_get_capacity(const struct gossmap *map, + const struct gossmap_chan *c) { struct gossip_hdr ghdr; size_t off; u16 type; + struct amount_sat sat; + struct amount_msat msat; - /* Fail for local channels */ - if (gossmap_chan_is_localmod(map, c)) - return false; + if (gossmap_chan_is_localmod(map, c)) { + /* Amount is *before* c->cann_off */ + off = c->cann_off - sizeof(u64); + goto get_amount; + } /* Skip over this record to next; expect a gossip_store_channel_amount */ off = c->cann_off - sizeof(ghdr); map_copy(map, off, &ghdr, sizeof(ghdr)); off += sizeof(ghdr) + be16_to_cpu(ghdr.len); - /* Partial write, this can happen. */ - if (off + sizeof(ghdr) + sizeof(u16) + sizeof(u64) > map->map_size) - return false; - - /* Get type of next field. */ + /* We don't allow loading a channel unless it has capacity field! */ type = map_be16(map, off + sizeof(ghdr)); - if (type != WIRE_GOSSIP_STORE_CHANNEL_AMOUNT) - return false; + assert(type == WIRE_GOSSIP_STORE_CHANNEL_AMOUNT); - *amount = amount_sat(map_be64(map, off + sizeof(ghdr) + sizeof(be16))); - return true; + off += sizeof(ghdr) + sizeof(be16); + +get_amount: + /* Shouldn't overflow */ + sat = amount_sat(map_be64(map, off)); + if (!amount_sat_to_msat(&msat, sat)) + errx(1, "invalid capacity %s", fmt_amount_sat(tmpctx, sat)); + + return msat; } struct gossmap_chan *gossmap_nth_chan(const struct gossmap *map, diff --git a/common/gossmap.h b/common/gossmap.h index ad45ec19ee5b..6a4a5308a924 100644 --- a/common/gossmap.h +++ b/common/gossmap.h @@ -94,6 +94,7 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, const struct node_id *n1, const struct node_id *n2, struct short_channel_id scid, + struct amount_msat capacity, const u8 *features) NON_NULL_ARGS(1,2,3); @@ -171,10 +172,9 @@ static inline bool gossmap_chan_set(const struct gossmap_chan *chan, int dir) return chan->cupdate_off[dir] != 0; } -/* Return capacity if it's known (fails on a local mod) */ -bool gossmap_chan_get_capacity(const struct gossmap *map, - const struct gossmap_chan *c, - struct amount_sat *amount); +/* Return capacity (in msat). */ +struct amount_msat gossmap_chan_get_capacity(const struct gossmap *map, + const struct gossmap_chan *c); /* Get the announcement msg which created this chan (NULL for localmods) */ u8 *gossmap_chan_get_announce(const tal_t *ctx, diff --git a/common/gossmods_listpeerchannels.c b/common/gossmods_listpeerchannels.c index 4ecbe536506e..df0b97133132 100644 --- a/common/gossmods_listpeerchannels.c +++ b/common/gossmods_listpeerchannels.c @@ -27,7 +27,8 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, max = spendable; /* FIXME: features? */ - gossmap_local_addchan(mods, self, peer, scidd->scid, NULL); + gossmap_local_addchan(mods, self, peer, scidd->scid, capacity_msat, + NULL); gossmap_local_updatechan(mods, scidd, &enabled, diff --git a/common/test/run-gossmap_canned.c b/common/test/run-gossmap_canned.c index 1b267f65737d..e7f0662284cd 100644 --- a/common/test/run-gossmap_canned.c +++ b/common/test/run-gossmap_canned.c @@ -314,7 +314,7 @@ int main(int argc, char *argv[]) struct gossmap *map; struct node_id l1, l2; struct short_channel_id scid12; - struct amount_sat capacity; + struct amount_msat capacity; u32 timestamp, fee_base_msat, fee_proportional_millionths; u8 message_flags, channel_flags; struct amount_msat htlc_minimum_msat, htlc_maximum_msat; @@ -337,9 +337,8 @@ int main(int argc, char *argv[]) assert(short_channel_id_from_str("110x1x0", 7, &scid12)); assert(gossmap_find_chan(map, &scid12)); assert(!gossmap_chan_is_localmod(map, gossmap_find_chan(map, &scid12))); - assert(gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12), - &capacity)); - assert(amount_sat_eq(capacity, AMOUNT_SAT(1000000))); + capacity = gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12)); + assert(amount_msat_eq_sat(capacity, AMOUNT_SAT(1000000))); gossmap_chan_get_update_details(map, gossmap_find_chan(map, &scid12), 0, diff --git a/common/test/run-gossmap_local.c b/common/test/run-gossmap_local.c index 5ae8d47d4fdb..b583495c0439 100644 --- a/common/test/run-gossmap_local.c +++ b/common/test/run-gossmap_local.c @@ -336,7 +336,7 @@ int main(int argc, char *argv[]) struct short_channel_id scid23, scid12, scid_local, scid_nonexisting; struct gossmap_chan *chan; struct gossmap_localmods *mods; - struct amount_sat capacity; + struct amount_msat capacity; u32 timestamp, fee_base_msat, fee_proportional_millionths; u8 message_flags, channel_flags; struct amount_msat htlc_minimum_msat, htlc_maximum_msat; @@ -365,12 +365,10 @@ int main(int argc, char *argv[]) assert(!gossmap_chan_is_localmod(map, gossmap_find_chan(map, &scid23))); assert(gossmap_find_chan(map, &scid12)); assert(!gossmap_chan_is_localmod(map, gossmap_find_chan(map, &scid12))); - assert(gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid23), - &capacity)); - assert(amount_sat_eq(capacity, AMOUNT_SAT(1000000))); - assert(gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12), - &capacity)); - assert(amount_sat_eq(capacity, AMOUNT_SAT(1000000))); + capacity = gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid23)); + assert(amount_msat_eq_sat(capacity, AMOUNT_SAT(1000000))); + capacity = gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12)); + assert(amount_msat_eq_sat(capacity, AMOUNT_SAT(1000000))); gossmap_chan_get_update_details(map, gossmap_find_chan(map, &scid23), 0, @@ -467,7 +465,7 @@ int main(int argc, char *argv[]) assert(node_id_from_hexstr("0382ce59ebf18be7d84677c2e35f23294b9992ceca95491fcf8a56c6cb2d9de199", 66, &l4)); assert(short_channel_id_from_str("111x1x1", 7, &scid_local)); - assert(gossmap_local_addchan(mods, &l1, &l4, scid_local, NULL)); + assert(gossmap_local_addchan(mods, &l1, &l4, scid_local, AMOUNT_MSAT(100000), NULL)); /* Apply changes, check they work. */ gossmap_apply_localmods(map, mods); @@ -478,6 +476,10 @@ int main(int argc, char *argv[]) assert(!gossmap_chan_set(chan, 0)); assert(!gossmap_chan_set(chan, 1)); + /* Capacity is correct */ + assert(amount_msat_eq(gossmap_chan_get_capacity(map, chan), + AMOUNT_MSAT(100000))); + /* Remove, no longer can find. */ gossmap_remove_localmods(map, mods); @@ -491,7 +493,7 @@ int main(int argc, char *argv[]) AMOUNT_MSAT(2), 3, 4, true, 0); /* Adding an existing channel is a noop. */ - assert(gossmap_local_addchan(mods, &l2, &l3, scid23, NULL)); + assert(gossmap_local_addchan(mods, &l2, &l3, scid23, AMOUNT_MSAT(100000), NULL)); gossmap_local_setchan(mods, scid23, AMOUNT_MSAT(99), diff --git a/common/test/run-route-infloop.c b/common/test/run-route-infloop.c index 0bd3cb04abc6..bec98e232486 100644 --- a/common/test/run-route-infloop.c +++ b/common/test/run-route-infloop.c @@ -56,15 +56,13 @@ static double capacity_bias(const struct gossmap *map, int dir, struct amount_msat amount) { - struct amount_sat capacity; + struct amount_msat msat; u64 amtmsat = amount.millisatoshis; /* Raw: lengthy math */ double capmsat; - /* Can fail in theory if gossmap changed underneath. */ - if (!gossmap_chan_get_capacity(map, c, &capacity)) - return 0; + msat = gossmap_chan_get_capacity(map, c); - capmsat = (double)capacity.satoshis * 1000; /* Raw: lengthy math */ + capmsat = (double)msat.millisatoshis; /* Raw: log */ return -log((capmsat + 1 - amtmsat) / (capmsat + 1)); } @@ -174,13 +172,13 @@ int main(int argc, char *argv[]) } else { double probability = 1; for (size_t j = 0; j < tal_count(r); j++) { - struct amount_sat capacity_sat; + struct amount_msat msat; u64 cap_msat; struct gossmap_chan *c = gossmap_find_chan(gossmap, &r[j].scid); assert(c); - assert(gossmap_chan_get_capacity(gossmap, c, &capacity_sat)); + msat = gossmap_chan_get_capacity(gossmap, c); - cap_msat = capacity_sat.satoshis * 1000; + cap_msat = msat.millisatoshis; /* Assume linear distribution, implying probability depends on * amount we would leave in channel */ assert(cap_msat >= r[0].amount.millisatoshis); diff --git a/devtools/gossmap-compress.c b/devtools/gossmap-compress.c index 0aaf4046ed35..d41672a54b25 100644 --- a/devtools/gossmap-compress.c +++ b/devtools/gossmap-compress.c @@ -234,18 +234,17 @@ static u64 get_htlc_max(struct gossmap *gossmap, int dir) { struct amount_msat msat, capacity_msat; - struct amount_sat capacity_sats; - gossmap_chan_get_capacity(gossmap, chan, &capacity_sats); + + capacity_msat = gossmap_chan_get_capacity(gossmap, chan); gossmap_chan_get_update_details(gossmap, chan, dir, NULL, NULL, NULL, NULL, NULL, NULL, &msat); /* Special value for the common case of "max_htlc == capacity" */ - if (amount_msat_eq_sat(msat, capacity_sats)) { + if (amount_msat_eq(msat, capacity_msat)) { return 0; } /* Other common case: "max_htlc == 99% capacity" */ - if (amount_sat_to_msat(&capacity_msat, capacity_sats) - && amount_msat_scale(&capacity_msat, capacity_msat, 0.99) + if (amount_msat_scale(&capacity_msat, capacity_msat, 0.99) && amount_msat_eq(msat, capacity_msat)) { return 1; } @@ -675,9 +674,9 @@ int main(int argc, char *argv[]) /* := {capacity_count} {capacity_count}*{capacity} */ u64 *vals = tal_arr(chans, u64, channel_count); for (size_t i = 0; i < channel_count; i++) { - struct amount_sat sats; - gossmap_chan_get_capacity(gossmap, chans[i], &sats); - vals[i] = sats.satoshis; /* Raw: compression format */ + struct amount_msat cap; + cap = gossmap_chan_get_capacity(gossmap, chans[i]); + vals[i] = cap.millisatoshis / 1000; /* Raw: compression format */ } write_template_and_values(outf, vals, "capacities"); diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 3bb915ce063a..2d45b67324e4 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -157,16 +157,12 @@ static fp16_t *get_capacities(const tal_t *ctx, for (c = gossmap_first_chan(gossmap); c; c = gossmap_next_chan(gossmap, c)) { - struct amount_sat cap; + struct amount_msat cap; - if (!gossmap_chan_get_capacity(gossmap, c, &cap)) { - plugin_log(plugin, LOG_BROKEN, - "get_capacity failed for channel?"); - cap = AMOUNT_SAT(0); - } + cap = gossmap_chan_get_capacity(gossmap, c); /* Pessimistic: round down! */ caps[gossmap_chan_idx(gossmap, c)] - = u64_to_fp16(cap.satoshis, false); /* Raw: fp16 */ + = u64_to_fp16(cap.millisatoshis/1000, false); /* Raw: fp16 */ } return caps; } @@ -459,23 +455,8 @@ void get_constraints(const struct route_query *rq, } /* Might be here because it's reserved, but capacity is normal. */ - if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL))) { - struct amount_sat cap; - if (gossmap_chan_get_capacity(rq->gossmap, chan, &cap)) { - /* Shouldn't happen! */ - if (!amount_sat_to_msat(max, cap)) { - plugin_log(rq->plugin, LOG_BROKEN, - "Local channel %s with capacity %s?", - fmt_short_channel_id(tmpctx, scidd.scid), - fmt_amount_sat(tmpctx, cap)); - } - } else { - /* Shouldn't happen: local channels have explicit constraints */ - plugin_log(rq->plugin, LOG_BROKEN, - "Channel %s without capacity?", - fmt_short_channel_id(tmpctx, scidd.scid)); - } - } + if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL))) + *max = gossmap_chan_get_capacity(rq->gossmap, chan); /* Finally, if any is in use, subtract that! */ reserve_sub(rq->reserved, &scidd, min); diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 1e21370c3c78..33734ccfab27 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -339,7 +339,8 @@ void layer_add_localmods(const struct layer *layer, lc; lc = local_channel_hash_next(layer->local_channels, &lcit)) { gossmap_local_addchan(localmods, - &lc->n1, &lc->n2, lc->scid, NULL); + &lc->n1, &lc->n2, lc->scid, lc->capacity, + NULL); for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { struct short_channel_id_dir scidd; bool enabled = true; diff --git a/plugins/establish_onion_path.c b/plugins/establish_onion_path.c index 1b5cb732076b..5aa723fb3093 100644 --- a/plugins/establish_onion_path.c +++ b/plugins/establish_onion_path.c @@ -116,7 +116,8 @@ gossmods_from_listpeers(const tal_t *ctx, fake_scidd.scid.u64 = i; fake_scidd.dir = node_id_idx(self, &peer_id); - gossmap_local_addchan(mods, self, &peer_id, fake_scidd.scid, NULL); + gossmap_local_addchan(mods, self, &peer_id, fake_scidd.scid, + AMOUNT_MSAT(1000), NULL); gossmap_local_updatechan(mods, &fake_scidd, &enabled, NULL, NULL, NULL, NULL, NULL); } diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c index dd953661a019..572fcc9e3751 100644 --- a/plugins/libplugin-pay.c +++ b/plugins/libplugin-pay.c @@ -856,15 +856,12 @@ static double capacity_bias(const struct gossmap *map, int dir, struct amount_msat amount) { - struct amount_sat capacity; u64 amtmsat = amount.millisatoshis; /* Raw: lengthy math */ double capmsat; /* Can fail in theory if gossmap changed underneath. */ - if (!gossmap_chan_get_capacity(map, c, &capacity)) - return 0; + capmsat = (double)gossmap_chan_get_capacity(map, c).millisatoshis; /* Raw: log */ - capmsat = (double)capacity.satoshis * 1000; /* Raw: lengthy math */ return -log((capmsat + 1 - amtmsat) / (capmsat + 1)); } diff --git a/plugins/renepay/chan_extra.c b/plugins/renepay/chan_extra.c index 57183227ea3b..931463ca53d2 100644 --- a/plugins/renepay/chan_extra.c +++ b/plugins/renepay/chan_extra.c @@ -559,13 +559,8 @@ get_chan_extra_half_by_chan_verify(const struct gossmap *gossmap, struct chan_extra_half *h = get_chan_extra_half_by_scid(chan_extra_map, &scidd); if (!h) { - struct amount_sat cap; - struct amount_msat cap_msat; - - if (!gossmap_chan_get_capacity(gossmap, chan, &cap) || - !amount_sat_to_msat(&cap_msat, cap)) { - return NULL; - } + struct amount_msat cap_msat + = gossmap_chan_get_capacity(gossmap, chan); h = &new_chan_extra(chan_extra_map, scidd.scid, cap_msat) ->half[scidd.dir]; } diff --git a/plugins/renepay/mods.c b/plugins/renepay/mods.c index 91ac7adf6a30..3d410cbce0d5 100644 --- a/plugins/renepay/mods.c +++ b/plugins/renepay/mods.c @@ -409,7 +409,8 @@ static void gossmod_cb(struct gossmap_localmods *mods, } /* FIXME: features? */ - gossmap_local_addchan(mods, self, peer, scidd->scid, NULL); + gossmap_local_addchan(mods, self, peer, scidd->scid, capacity_msat, + NULL); gossmap_local_updatechan(mods, scidd, &enabled, &min, &max, @@ -544,7 +545,7 @@ static void add_hintchan(struct payment *payment, const struct node_id *src, } /* FIXME: features? */ if (!gossmap_local_addchan(payment->local_gossmods, src, dst, - scid, NULL) || + scid, MAX_CAPACITY, NULL) || !gossmap_local_updatechan( payment->local_gossmods, &scidd, &enabled, &htlc_min, &htlc_max, diff --git a/plugins/renepay/test/common.h b/plugins/renepay/test/common.h index 3368bdb49e54..75d3b1f3aeb8 100644 --- a/plugins/renepay/test/common.h +++ b/plugins/renepay/test/common.h @@ -49,8 +49,7 @@ static void add_connection(int store_fd, struct amount_msat max, u32 base_fee, s32 proportional_fee, u32 delay, - struct amount_sat capacity, - bool add_capacity) + struct amount_sat capacity) { secp256k1_ecdsa_signature dummy_sig; struct secret not_a_secret; @@ -79,10 +78,8 @@ static void add_connection(int store_fd, &dummy_key, &dummy_key); write_to_store(store_fd, msg); - if (add_capacity) { - msg = towire_gossip_store_channel_amount(tmpctx, capacity); - write_to_store(store_fd, msg); - } + msg = towire_gossip_store_channel_amount(tmpctx, capacity); + write_to_store(store_fd, msg); u8 flags = node_id_idx(from, to); diff --git a/plugins/renepay/test/run-bottleneck.c b/plugins/renepay/test/run-bottleneck.c index 510ddebde5e7..2173ebd54392 100644 --- a/plugins/renepay/test/run-bottleneck.c +++ b/plugins/renepay/test/run-bottleneck.c @@ -113,32 +113,28 @@ int main(int argc, char *argv[]) AMOUNT_MSAT(0), AMOUNT_MSAT(60 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(60 * 1000), - true); + AMOUNT_SAT(60 * 1000)); assert(mk_short_channel_id(&scid, 1, 3, 0)); add_connection(fd, &nodes[0], &nodes[2], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(60 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(60 * 1000), - true); + AMOUNT_SAT(60 * 1000)); assert(mk_short_channel_id(&scid, 2, 4, 0)); add_connection(fd, &nodes[1], &nodes[3], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 3, 4, 0)); add_connection(fd, &nodes[2], &nodes[3], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 4, 5, 0)); add_connection(fd, &nodes[3], &nodes[4], scid, @@ -148,40 +144,35 @@ int main(int argc, char *argv[]) * through this channel. */ AMOUNT_MSAT(106 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(110 * 1000), - true); + AMOUNT_SAT(110 * 1000)); assert(mk_short_channel_id(&scid, 5, 6, 0)); add_connection(fd, &nodes[4], &nodes[5], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 100 * 1000 /* 10% */, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 5, 7, 0)); add_connection(fd, &nodes[4], &nodes[6], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 100 * 1000 /* 10% */, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 6, 8, 0)); add_connection(fd, &nodes[5], &nodes[7], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 7, 8, 0)); add_connection(fd, &nodes[6], &nodes[7], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(gossmap_refresh(gossmap, NULL)); struct uncertainty *uncertainty = uncertainty_new(tmpctx); diff --git a/plugins/renepay/test/run-mcf-diamond.c b/plugins/renepay/test/run-mcf-diamond.c index 85ceb6888346..2db20d6e876a 100644 --- a/plugins/renepay/test/run-mcf-diamond.c +++ b/plugins/renepay/test/run-mcf-diamond.c @@ -133,7 +133,7 @@ int main(int argc, char *argv[]) mods = gossmap_localmods_new(tmpctx); /* 1->2->4 has capacity 10k sat, 1->3->4 has capacity 5k sat (lower fee!) */ - assert(gossmap_local_addchan(mods, &l1, &l2, scid12, NULL)); + assert(gossmap_local_addchan(mods, &l1, &l2, scid12, AMOUNT_MSAT(10000000), NULL)); assert(gossmap_local_setchan(mods, scid12, /*htlc_min=*/ AMOUNT_MSAT(0), /*htlc_max=*/ AMOUNT_MSAT(10000000), @@ -142,21 +142,21 @@ int main(int argc, char *argv[]) /* delay =*/ 5, /* enabled=*/ true, /* dir =*/ 0)); - assert(gossmap_local_addchan(mods, &l2, &l4, scid24, NULL)); + assert(gossmap_local_addchan(mods, &l2, &l4, scid24, AMOUNT_MSAT(10000000), NULL)); assert(gossmap_local_setchan(mods, scid24, AMOUNT_MSAT(0), AMOUNT_MSAT(10000000), AMOUNT_MSAT(0), 1002, 5, true, 0)); - assert(gossmap_local_addchan(mods, &l1, &l3, scid13, NULL)); + assert(gossmap_local_addchan(mods, &l1, &l3, scid13, AMOUNT_MSAT(5000000), NULL)); assert(gossmap_local_setchan(mods, scid13, AMOUNT_MSAT(0), AMOUNT_MSAT(5000000), AMOUNT_MSAT(0), 503, 5, true, 0)); - assert(gossmap_local_addchan(mods, &l3, &l4, scid34, NULL)); + assert(gossmap_local_addchan(mods, &l3, &l4, scid34, AMOUNT_MSAT(5000000), NULL)); assert(gossmap_local_setchan(mods, scid34, AMOUNT_MSAT(0), AMOUNT_MSAT(5000000), diff --git a/plugins/renepay/test/run-mcf.c b/plugins/renepay/test/run-mcf.c index 510c12bf726b..487304918580 100644 --- a/plugins/renepay/test/run-mcf.c +++ b/plugins/renepay/test/run-mcf.c @@ -480,7 +480,8 @@ int main(int argc, char *argv[]) assert(short_channel_id_from_str("111x1x1", 7, &scid13)); /* 400,000sat channel from 1->3, basefee 0, ppm 1000, delay 5 */ - assert(gossmap_local_addchan(mods, &l1, &l3, scid13, NULL)); + assert(gossmap_local_addchan(mods, &l1, &l3, scid13, + AMOUNT_MSAT(400000000), NULL)); assert(gossmap_local_setchan(mods, scid13, AMOUNT_MSAT(0), AMOUNT_MSAT(400000000), @@ -494,6 +495,7 @@ int main(int argc, char *argv[]) assert(local_chan); /* The local chans have no "capacity", so set it manually. */ + /* FIXME: They do now! */ uncertainty_add_channel(uncertainty, scid13, AMOUNT_MSAT(400000000)); // flows = minflow(tmpctx, gossmap, diff --git a/plugins/renepay/test/run-missingcapacity.c b/plugins/renepay/test/run-missingcapacity.c deleted file mode 100644 index 4dc53ec4825f..000000000000 --- a/plugins/renepay/test/run-missingcapacity.c +++ /dev/null @@ -1,179 +0,0 @@ -/* Checks that uncertainty_update and get_routes can handle a gossmap where the - * capacity of some channels are missing. - * - * */ -#include "config.h" - -#include "../disabledmap.c" -#include "../errorcodes.c" -#include "../flow.c" -#include "../mcf.c" -#include "../route.c" -#include "../routebuilder.c" -#include "../uncertainty.c" -#include "common.h" - -#include -#include -#include -#include -#include -#include - -/* AUTOGENERATED MOCKS START */ -/* Generated stub for sciddir_or_pubkey_from_node_id */ -bool sciddir_or_pubkey_from_node_id(struct sciddir_or_pubkey *sciddpk UNNEEDED, - const struct node_id *node_id UNNEEDED) -{ fprintf(stderr, "sciddir_or_pubkey_from_node_id called!\n"); abort(); } -/* AUTOGENERATED MOCKS END */ - -static u8 empty_map[] = {10}; - -#define NUM_NODES 4 - -static void remove_file(char *fname) { assert(!remove(fname)); } - -int main(int argc, char *argv[]) -{ - int fd; - char *gossfile; - struct gossmap *gossmap; - struct node_id nodes[NUM_NODES]; - - common_setup(argv[0]); - chainparams = chainparams_for_network("regtest"); - - fd = tmpdir_mkstemp(tmpctx, "run-missingcapacity.XXXXXX", &gossfile); - tal_add_destructor(gossfile, remove_file); - assert(write(fd, empty_map, sizeof(empty_map)) == sizeof(empty_map)); - - gossmap = gossmap_load(tmpctx, gossfile, NULL); - assert(gossmap); - - for (size_t i = 0; i < NUM_NODES; i++) { - struct privkey tmp; - memset(&tmp, i+1, sizeof(tmp)); - node_id_from_privkey(&tmp, &nodes[i]); - } - - /* We will try a payment from 1 to 4. - * There are two possible routes 1->2->4 or 1->3->4. - * However, we will simulate that we don't have channel 3->4's capacity - * in the gossmap (see #7194). We expect that 3->4 it's simply ignored - * and only route through 1->2->4 is used. - * - * +--2--+ - * | | - * 1 4 - * | | - * +--3--+ - * - * */ - struct short_channel_id scid; - - assert(mk_short_channel_id(&scid, 1, 2, 0)); - add_connection(fd, &nodes[0], &nodes[1], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ true); - - assert(mk_short_channel_id(&scid, 2, 4, 0)); - add_connection(fd, &nodes[1], &nodes[3], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ true); - - assert(mk_short_channel_id(&scid, 1, 3, 0)); - add_connection(fd, &nodes[0], &nodes[2], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ true); - - assert(mk_short_channel_id(&scid, 3, 4, 0)); - add_connection(fd, &nodes[2], &nodes[3], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ false); - - assert(gossmap_refresh(gossmap, NULL)); - struct uncertainty *uncertainty = uncertainty_new(tmpctx); - int skipped_count = - uncertainty_update(uncertainty, gossmap); - assert(skipped_count==1); - - struct preimage preimage; - - struct amount_msat maxfee = AMOUNT_MSAT(20*1000); - struct payment_info pinfo; - pinfo.invstr = NULL; - pinfo.label = NULL; - pinfo.description = NULL; - pinfo.payment_secret = NULL; - pinfo.payment_metadata = NULL; - pinfo.routehints = NULL; - pinfo.destination = nodes[3]; - pinfo.amount = AMOUNT_MSAT(100 * 1000 * 1000); - - assert(amount_msat_add(&pinfo.maxspend, maxfee, pinfo.amount)); - pinfo.maxdelay = 100; - pinfo.final_cltv = 5; - - pinfo.start_time = time_now(); - pinfo.stop_time = timeabs_add(pinfo.start_time, time_from_sec(10000)); - - pinfo.base_fee_penalty = 1e-5; - pinfo.prob_cost_factor = 1e-5; - pinfo.delay_feefactor = 1e-6; - pinfo.min_prob_success = 0.9; - pinfo.base_prob_success = 1.0; - pinfo.use_shadow = false; - - randombytes_buf(&preimage, sizeof(preimage)); - sha256(&pinfo.payment_hash, &preimage, sizeof(preimage)); - - struct disabledmap *disabledmap = disabledmap_new(tmpctx); - - enum jsonrpc_errcode errcode; - const char *err_msg; - - u64 groupid = 1; - u64 next_partid=1; - - struct route **routes = get_routes( - /* ctx */tmpctx, - /* payment */&pinfo, - /* source */&nodes[0], - /* destination */&nodes[3], - /* gossmap */gossmap, - /* uncertainty */uncertainty, - disabledmap, - /* amount */ pinfo.amount, - /* feebudget */maxfee, - &next_partid, - groupid, - &errcode, - &err_msg); - - assert(routes); - - if (!routes) { - printf("get_route failed with error %d: %s", errcode, err_msg); - } else { - printf("get_routes: %s\n", print_routes(tmpctx, routes)); - assert(tal_count(routes) == 1); - assert(tal_count(routes[0]->hops) == 2); - assert(node_id_eq(&routes[0]->hops[0].node_id, &nodes[1])); - assert(node_id_eq(&routes[0]->hops[1].node_id, &nodes[3])); - } - - common_shutdown(); -} - diff --git a/plugins/renepay/test/run-testflow.c b/plugins/renepay/test/run-testflow.c index 5d907bae1547..a570fd9eb7d0 100644 --- a/plugins/renepay/test/run-testflow.c +++ b/plugins/renepay/test/run-testflow.c @@ -578,12 +578,12 @@ static void test_flow_to_route(void) struct chan_extra_half *h0,*h1; struct gossmap_chan *c; - struct amount_sat cap; + struct amount_msat cap; struct amount_msat sum_min1_max0,sum_min0_max1; // check the bounds channel 1--2 c = gossmap_find_chan(gossmap,&scid12); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h0 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,0); assert(h0); @@ -596,17 +596,17 @@ static void test_flow_to_route(void) h1->known_max = AMOUNT_MSAT(1000000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); // check the bounds channel 2--3 c = gossmap_find_chan(gossmap,&scid23); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h1 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,1); assert(h1); @@ -619,17 +619,17 @@ static void test_flow_to_route(void) h0->known_max = AMOUNT_MSAT(2000000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); // check the bounds channel 3--4 c = gossmap_find_chan(gossmap,&scid34); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h0 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,0); assert(h0); @@ -642,17 +642,17 @@ static void test_flow_to_route(void) h1->known_max = AMOUNT_MSAT(500000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); // check the bounds channel 4--5 c = gossmap_find_chan(gossmap,&scid45); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h0 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,0); assert(h0); @@ -665,13 +665,13 @@ static void test_flow_to_route(void) h1->known_max = AMOUNT_MSAT(2000000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); struct flow *F; struct route *route; diff --git a/plugins/renepay/uncertainty.c b/plugins/renepay/uncertainty.c index 00c50ebc1134..d151471229a0 100644 --- a/plugins/renepay/uncertainty.c +++ b/plugins/renepay/uncertainty.c @@ -104,12 +104,10 @@ int uncertainty_update(struct uncertainty *uncertainty, struct gossmap *gossmap) struct chan_extra *ce = chan_extra_map_get(chan_extra_map, scid); if (!ce) { - struct amount_sat cap; struct amount_msat cap_msat; - if (!gossmap_chan_get_capacity(gossmap, chan, &cap) || - !amount_sat_to_msat(&cap_msat, cap) || - !new_chan_extra(chan_extra_map, scid, + cap_msat = gossmap_chan_get_capacity(gossmap, chan); + if (!new_chan_extra(chan_extra_map, scid, cap_msat)) { /* If the new chan_extra cannot be created we * skip this channel. */ diff --git a/plugins/test/Makefile b/plugins/test/Makefile index 0165ee8c646c..2ecba481c65a 100644 --- a/plugins/test/Makefile +++ b/plugins/test/Makefile @@ -19,7 +19,8 @@ plugins/test/run-route-overlong: \ common/fp16.o \ common/gossmap.o \ common/node_id.o \ - common/route.o + common/route.o \ + gossipd/gossip_store_wiregen.o plugins/test/run-route-calc: \ common/fp16.o \ diff --git a/plugins/test/run-route-overlong.c b/plugins/test/run-route-overlong.c index 5aa1bb093d77..9655104413ed 100644 --- a/plugins/test/run-route-overlong.c +++ b/plugins/test/run-route-overlong.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -383,6 +384,8 @@ static void add_connection(int store_fd, ids[0], ids[1], &dummy_key, &dummy_key); write_to_store(store_fd, msg); + msg = towire_gossip_store_channel_amount(tmpctx, AMOUNT_SAT(10000)); + write_to_store(store_fd, msg); update_connection(store_fd, from, to, scid, min, max, base_fee, proportional_fee, diff --git a/plugins/topology.c b/plugins/topology.c index b3da4c885e76..5cc00aa894b2 100644 --- a/plugins/topology.c +++ b/plugins/topology.c @@ -224,7 +224,7 @@ static void json_add_halfchan(struct json_stream *response, struct short_channel_id scid; struct node_id node_id[2]; const u8 *chanfeatures; - struct amount_sat capacity; + struct amount_msat capacity_msat; bool local_disable; /* These are channel (not per-direction) properties */ @@ -234,9 +234,7 @@ static void json_add_halfchan(struct json_stream *response, gossmap_node_get_id(gossmap, gossmap_nth_node(gossmap, c, i), &node_id[i]); - /* This can theoretically happen on partial write races. */ - if (!gossmap_chan_get_capacity(gossmap, c, &capacity)) - capacity = AMOUNT_SAT(0); + capacity_msat = gossmap_chan_get_capacity(gossmap, c); /* Deprecated: local channels are not "active" unless peer is connected. */ if (connected && node_id_eq(&node_id[0], &local_id)) @@ -287,7 +285,7 @@ static void json_add_halfchan(struct json_stream *response, &htlc_maximum_msat); } - json_add_amount_sat_msat(response, "amount_msat", capacity); + json_add_amount_msat(response, "amount_msat", capacity_msat); json_add_num(response, "message_flags", message_flags); json_add_num(response, "channel_flags", channel_flags); From 8ea49711aee52d641fb79f6a7f3da76c9ec30143 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:07:53 +0930 Subject: [PATCH 20/24] askrene: split askrene-create-channel into create-channel and update-channel. This allows for explicit partial updates to channels (e.g. just change fees, or just disable) without haveing to set the other fields. This generalizes askrene-disable-channel, which is removed. We also take the chance to use the proper BOLT 7 terms in the API: - htlc_minimum_msat - htlc_maximum_msat - cltv_expiry_delta - fee_base_msat - fee_proportional_millionths Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 213 +++++++------- doc/Makefile | 1 + doc/index.rst | 1 + .../lightning-askrene-create-channel.json | 43 +-- .../lightning-askrene-create-layer.json | 22 +- .../lightning-askrene-disable-channel.json | 50 ---- doc/schemas/lightning-askrene-listlayers.json | 22 +- .../lightning-askrene-update-channel.json | 76 +++++ plugins/askrene/askrene.c | 80 +++--- plugins/askrene/layer.c | 263 ++++++++++-------- plugins/askrene/layer.h | 31 ++- tests/test_askrene.py | 73 +++-- 12 files changed, 471 insertions(+), 404 deletions(-) delete mode 100644 doc/schemas/lightning-askrene-disable-channel.json create mode 100644 doc/schemas/lightning-askrene-update-channel.json diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 900e785fb140..7db584550643 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." + "The **askrene-create-channel** RPC command tells askrene create a channel in the given layer. To actually populate the channel use *askrene-update-channel* in each direction." ], "request": { "required": [ @@ -273,12 +273,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_min", - "htlc_max", - "base_fee", - "proportional_fee", - "delay" + "capacity_msat" ], "properties": { "layer": { @@ -308,37 +303,8 @@ "capacity_msat": { "type": "msat", "description": [ - "The capacity (onchain size) of the channel." - ] - }, - "htlc_min": { - "type": "msat", - "description": [ - "The minimum value allowed in this direction." - ] - }, - "htlc_max": { - "type": "msat", - "description": [ - "The maximum value allowed in this direction." - ] - }, - "base_fee": { - "type": "msat", - "description": [ - "The base fee to apply to use the channel in this direction." - ] - }, - "proportional_fee": { - "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." + "The capacity (onchain size) of the channel.", + "NOTE: this is in millisatoshis!" ] } } @@ -350,6 +316,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-disable-node(7)", + "lightning-askrene-update-channel(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" @@ -398,8 +365,8 @@ "required": [ "layer", "disabled_nodes", - "disabled_channels", "created_channels", + "channel_updates", "constraints" ], "properties": { @@ -435,12 +402,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_minimum_msat", - "htlc_maximum_msat", - "fee_base_msat", - "fee_proportional_millionths", - "delay" + "capacity_msat" ], "properties": { "source": { @@ -466,7 +428,18 @@ "description": [ "The capacity (onchain size) of the channel." ] - }, + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { "htlc_minimum_msat": { "type": "msat", "description": [ @@ -556,56 +529,6 @@ "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_dir" - ], - "properties": { - "layer": { - "type": "string", - "description": [ - "The name of the layer to apply this change to." - ] - }, - "short_channel_id_dir": { - "type": "short_channel_id_dir", - "description": [ - "The channel and direction to disable." - ] - } - } - }, - "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", @@ -790,8 +713,8 @@ "required": [ "layer", "disabled_nodes", - "disabled_channels", "created_channels", + "channel_updates", "constraints" ], "properties": { @@ -827,12 +750,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_minimum_msat", - "htlc_maximum_msat", - "fee_base_msat", - "fee_proportional_millionths", - "delay" + "capacity_msat" ], "properties": { "source": { @@ -858,7 +776,18 @@ "description": [ "The capacity (onchain size) of the channel." ] - }, + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { "htlc_minimum_msat": { "type": "msat", "description": [ @@ -1173,6 +1102,82 @@ "Main web site: " ] }, + "lightning-askrene-update-channel.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-update-channel", + "title": "Command to manipulate channel in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-update-channel** RPC command overrides updates for an existing channel when the layer is applied." + ], + "request": { + "required": [ + "layer", + "short_channel_id_dir" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction to apply the change to." + ] + }, + "htlc_min": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_max": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "base_fee": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "proportional_fee": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "cltv_expiry_delta": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, "lightning-autoclean-once.json": { "$schema": "../rpc-schema-draft.json", "type": "object", diff --git a/doc/Makefile b/doc/Makefile index afef62d8d0a5..b7b7648edcab 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -9,6 +9,7 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-askrene-create-layer.7 \ doc/lightning-askrene-remove-layer.7 \ doc/lightning-askrene-create-channel.7 \ + doc/lightning-askrene-update-channel.7 \ doc/lightning-askrene-disable-node.7 \ doc/lightning-askrene-inform-channel.7 \ doc/lightning-askrene-listlayers.7 \ diff --git a/doc/index.rst b/doc/index.rst index a28fc8f425a3..edee2e109cdd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -23,6 +23,7 @@ Core Lightning Documentation lightning-askrene-remove-layer lightning-askrene-reserve lightning-askrene-unreserve + lightning-askrene-update-channel lightning-autoclean-once lightning-autoclean-status lightning-batching diff --git a/doc/schemas/lightning-askrene-create-channel.json b/doc/schemas/lightning-askrene-create-channel.json index 3e76313aaf58..f1acd0c8299b 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." + "The **askrene-create-channel** RPC command tells askrene create a channel in the given layer. To actually populate the channel use *askrene-update-channel* in each direction." ], "request": { "required": [ @@ -15,12 +15,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_min", - "htlc_max", - "base_fee", - "proportional_fee", - "delay" + "capacity_msat" ], "properties": { "layer": { @@ -50,37 +45,8 @@ "capacity_msat": { "type": "msat", "description": [ - "The capacity (onchain size) of the channel." - ] - }, - "htlc_min": { - "type": "msat", - "description": [ - "The minimum value allowed in this direction." - ] - }, - "htlc_max": { - "type": "msat", - "description": [ - "The maximum value allowed in this direction." - ] - }, - "base_fee": { - "type": "msat", - "description": [ - "The base fee to apply to use the channel in this direction." - ] - }, - "proportional_fee": { - "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." + "The capacity (onchain size) of the channel.", + "NOTE: this is in millisatoshis!" ] } } @@ -92,6 +58,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-disable-node(7)", + "lightning-askrene-update-channel(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" diff --git a/doc/schemas/lightning-askrene-create-layer.json b/doc/schemas/lightning-askrene-create-layer.json index 00518b4e4bb6..566a2a1e947a 100644 --- a/doc/schemas/lightning-askrene-create-layer.json +++ b/doc/schemas/lightning-askrene-create-layer.json @@ -35,8 +35,8 @@ "required": [ "layer", "disabled_nodes", - "disabled_channels", "created_channels", + "channel_updates", "constraints" ], "properties": { @@ -72,12 +72,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_minimum_msat", - "htlc_maximum_msat", - "fee_base_msat", - "fee_proportional_millionths", - "delay" + "capacity_msat" ], "properties": { "source": { @@ -103,7 +98,18 @@ "description": [ "The capacity (onchain size) of the channel." ] - }, + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { "htlc_minimum_msat": { "type": "msat", "description": [ diff --git a/doc/schemas/lightning-askrene-disable-channel.json b/doc/schemas/lightning-askrene-disable-channel.json deleted file mode 100644 index 09d5b2c4fd0b..000000000000 --- a/doc/schemas/lightning-askrene-disable-channel.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "$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_dir" - ], - "properties": { - "layer": { - "type": "string", - "description": [ - "The name of the layer to apply this change to." - ] - }, - "short_channel_id_dir": { - "type": "short_channel_id_dir", - "description": [ - "The channel and direction to disable." - ] - } - } - }, - "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 f3c3f6b89dd7..c206f8fe60be 100644 --- a/doc/schemas/lightning-askrene-listlayers.json +++ b/doc/schemas/lightning-askrene-listlayers.json @@ -33,8 +33,8 @@ "required": [ "layer", "disabled_nodes", - "disabled_channels", "created_channels", + "channel_updates", "constraints" ], "properties": { @@ -70,12 +70,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_minimum_msat", - "htlc_maximum_msat", - "fee_base_msat", - "fee_proportional_millionths", - "delay" + "capacity_msat" ], "properties": { "source": { @@ -101,7 +96,18 @@ "description": [ "The capacity (onchain size) of the channel." ] - }, + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { "htlc_minimum_msat": { "type": "msat", "description": [ diff --git a/doc/schemas/lightning-askrene-update-channel.json b/doc/schemas/lightning-askrene-update-channel.json new file mode 100644 index 000000000000..c0f82dcdee0f --- /dev/null +++ b/doc/schemas/lightning-askrene-update-channel.json @@ -0,0 +1,76 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-update-channel", + "title": "Command to manipulate channel in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-update-channel** RPC command overrides updates for an existing channel when the layer is applied." + ], + "request": { + "required": [ + "layer", + "short_channel_id_dir" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction to apply the change to." + ] + }, + "htlc_min": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_max": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "base_fee": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "proportional_fee": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "cltv_expiry_delta": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 2d45b67324e4..7d71e1728c28 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -707,14 +707,10 @@ static struct command_result *json_askrene_create_channel(struct command *cmd, const jsmntok_t *params) { struct layer *layer; - const struct local_channel *lc; struct node_id *src, *dst; struct short_channel_id *scid; struct amount_msat *capacity; struct json_stream *response; - struct amount_msat *htlc_min, *htlc_max, *base_fee; - u32 *proportional_fee; - u16 *delay; if (!param_check(cmd, buffer, params, p_req("layer", param_known_layer, &layer), @@ -722,27 +718,51 @@ static struct command_result *json_askrene_create_channel(struct command *cmd, p_req("destination", param_node_id, &dst), p_req("short_channel_id", param_short_channel_id, &scid), p_req("capacity_msat", param_msat, &capacity), - p_req("htlc_minimum_msat", param_msat, &htlc_min), - p_req("htlc_maximum_msat", param_msat, &htlc_max), - p_req("fee_base_msat", param_msat, &base_fee), - p_req("fee_proportional_millionths", param_u32, &proportional_fee), - p_req("delay", param_u16, &delay), NULL)) return command_param_failed(); - /* If it exists, it must match */ - lc = layer_find_local_channel(layer, *scid); - if (lc && !layer_check_local_channel(lc, src, dst, *capacity)) { + if (layer_find_local_channel(layer, *scid)) { return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "channel already exists with different values!"); + "channel already exists"); } if (command_check_only(cmd)) return command_check_done(cmd); - layer_update_local_channel(layer, src, dst, *scid, *capacity, - *base_fee, *proportional_fee, *delay, - *htlc_min, *htlc_max); + layer_add_local_channel(layer, src, dst, *scid, *capacity); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_update_channel(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct layer *layer; + struct short_channel_id_dir *scidd; + bool *enabled; + struct amount_msat *htlc_min, *htlc_max, *base_fee; + u32 *proportional_fee; + u16 *delay; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("layer", param_known_layer, &layer), + p_req("short_channel_id_dir", param_short_channel_id_dir, &scidd), + p_opt("enabled", param_bool, &enabled), + p_opt("htlc_minimum_msat", param_msat, &htlc_min), + p_opt("htlc_maximum_msat", param_msat, &htlc_max), + p_opt("fee_base_msat", param_msat, &base_fee), + p_opt("fee_proportional_millionths", param_u32, &proportional_fee), + p_opt("cltv_expiry_delta", param_u16, &delay), + NULL)) + return command_param_failed(); + + layer_add_update_channel(layer, scidd, + enabled, + htlc_min, htlc_max, + base_fee, proportional_fee, delay); response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); @@ -831,26 +851,6 @@ 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_dir *scidd; - struct layer *layer; - struct json_stream *response; - - if (!param(cmd, buffer, params, - 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_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) @@ -989,6 +989,10 @@ static const struct plugin_command commands[] = { "askrene-create-channel", json_askrene_create_channel, }, + { + "askrene-update-channel", + json_askrene_update_channel, + }, { "askrene-inform-channel", json_askrene_inform_channel, @@ -1009,10 +1013,6 @@ 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/layer.c b/plugins/askrene/layer.c index 33734ccfab27..4a6e3ffb7591 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -14,15 +14,17 @@ struct local_channel { struct node_id n1, n2; struct short_channel_id scid; struct amount_msat capacity; +}; + +struct local_update { + struct short_channel_id_dir scidd; - struct added_channel_half { - /* Other fields only valid if this is true */ - bool enabled; - u16 delay; - u32 proportional_fee; - struct amount_msat base_fee; - struct amount_msat htlc_min, htlc_max; - } half[2]; + /* Non-null fields apply. */ + const bool *enabled; + const u16 *delay; + const u32 *proportional_fee; + const struct amount_msat *base_fee; + const struct amount_msat *htlc_min, *htlc_max; }; static const struct short_channel_id_dir * @@ -61,6 +63,21 @@ static inline bool local_channel_eq_scid(const struct local_channel *lc, HTABLE_DEFINE_TYPE(struct local_channel, local_channel_scid, hash_scid, local_channel_eq_scid, local_channel_hash); +static const struct short_channel_id_dir * +local_update_scidd(const struct local_update *lu) +{ + return &lu->scidd; +} + +static inline bool local_update_eq_scidd(const struct local_update *lu, + const struct short_channel_id_dir *scidd) +{ + return short_channel_id_dir_eq(scidd, &lu->scidd); +} + +HTABLE_DEFINE_TYPE(struct local_update, local_update_scidd, hash_scidd, + local_update_eq_scidd, local_update_hash); + struct layer { /* Inside global list of layers */ struct list_node list; @@ -71,14 +88,14 @@ struct layer { /* Completely made up local additions, indexed by scid */ struct local_channel_hash *local_channels; + /* Modifications to channels, indexed by scidd */ + struct local_update_hash *local_updates; + /* Additional info, indexed by scid+dir */ struct constraint_hash *constraints; /* Nodes to completely disable (tal_arr) */ struct node_id *disabled_nodes; - - /* Channels to completely disable (tal_arr) */ - struct short_channel_id_dir *disabled_channels; }; struct layer *new_temp_layer(const tal_t *ctx, const char *name) @@ -88,10 +105,11 @@ struct layer *new_temp_layer(const tal_t *ctx, const char *name) l->name = tal_strdup(l, name); l->local_channels = tal(l, struct local_channel_hash); local_channel_hash_init(l->local_channels); + l->local_updates = tal(l, struct local_update_hash); + local_update_hash_init(l->local_updates); l->constraints = tal(l, struct constraint_hash); constraint_hash_init(l->constraints); l->disabled_nodes = tal_arr(l, struct node_id, 0); - l->disabled_channels = tal_arr(l, struct short_channel_id_dir, 0); return l; } @@ -109,20 +127,6 @@ struct layer *new_layer(struct askrene *askrene, const char *name) return l; } -/* Swap if necessary to make into BOLT-7 order. Return direction. */ -static int canonicalize_node_order(const struct node_id **n1, - const struct node_id **n2) -{ - const struct node_id *tmp; - - if (node_id_cmp(*n1, *n2) < 0) - return 0; - tmp = *n2; - *n2 = *n1; - *n1 = tmp; - return 1; -} - struct layer *find_layer(struct askrene *askrene, const char *name) { struct layer *l; @@ -145,63 +149,77 @@ static struct local_channel *new_local_channel(struct layer *layer, struct amount_msat capacity) { struct local_channel *lc = tal(layer, struct local_channel); - lc->n1 = *n1; - lc->n2 = *n2; + + /* Swap if necessary to make into BOLT-7 order. */ + if (node_id_cmp(n1, n2) < 0) { + lc->n1 = *n1; + lc->n2 = *n2; + } else { + lc->n1 = *n2; + lc->n2 = *n1; + } lc->scid = scid; lc->capacity = capacity; - for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) - lc->half[i].enabled = false; - local_channel_hash_add(layer->local_channels, lc); return lc; } -bool layer_check_local_channel(const struct local_channel *lc, - const struct node_id *n1, - const struct node_id *n2, - struct amount_msat capacity) +void layer_add_local_channel(struct layer *layer, + const struct node_id *src, + const struct node_id *dst, + struct short_channel_id scid, + struct amount_msat capacity) { - canonicalize_node_order(&n1, &n2); - return node_id_eq(&lc->n1, n1) - && node_id_eq(&lc->n2, n2) - && amount_msat_eq(lc->capacity, capacity); + assert(!local_channel_hash_get(layer->local_channels, scid)); + new_local_channel(layer, src, dst, scid, capacity); } -/* Update a local channel to a layer: fails if you try to change capacity or nodes! */ -void layer_update_local_channel(struct layer *layer, - const struct node_id *src, - const struct node_id *dst, - struct short_channel_id scid, - struct amount_msat capacity, - struct amount_msat base_fee, - u32 proportional_fee, - u16 delay, - struct amount_msat htlc_min, - struct amount_msat htlc_max) +void layer_add_update_channel(struct layer *layer, + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay) { - struct local_channel *lc = local_channel_hash_get(layer->local_channels, scid); - int dir = canonicalize_node_order(&src, &dst); - struct short_channel_id_dir scidd; - - if (lc) { - assert(layer_check_local_channel(lc, src, dst, capacity)); - } else { - lc = new_local_channel(layer, src, dst, scid, capacity); + struct local_update *lu; + + lu = local_update_hash_get(layer->local_updates, scidd); + if (!lu) { + lu = tal(layer, struct local_update); + lu->scidd = *scidd; + lu->enabled = NULL; + lu->delay = NULL; + lu->proportional_fee = NULL; + lu->base_fee = lu->htlc_min = lu->htlc_max = NULL; + local_update_hash_add(layer->local_updates, lu); + } + if (enabled) { + tal_free(lu->enabled); + lu->enabled = tal_dup(lu, bool, enabled); + } + if (htlc_min) { + tal_free(lu->htlc_min); + lu->htlc_min = tal_dup(lu, struct amount_msat, htlc_min); + } + if (htlc_max) { + tal_free(lu->htlc_max); + lu->htlc_max = tal_dup(lu, struct amount_msat, htlc_max); + } + if (base_fee) { + tal_free(lu->base_fee); + lu->base_fee = tal_dup(lu, struct amount_msat, base_fee); + } + if (proportional_fee) { + tal_free(lu->proportional_fee); + lu->proportional_fee = tal_dup(lu, u32, proportional_fee); + } + if (delay) { + tal_free(lu->delay); + lu->delay = tal_dup(lu, u16, delay); } - - lc->half[dir].enabled = true; - lc->half[dir].htlc_min = htlc_min; - lc->half[dir].htlc_max = htlc_max; - lc->half[dir].base_fee = base_fee; - lc->half[dir].proportional_fee = proportional_fee; - lc->half[dir].delay = delay; - - /* We always add an explicit constraint for local channels, to simplify - * lookups. You can tell it's a fake one by the timestamp. */ - scidd.scid = scid; - scidd.dir = dir; - layer_update_constraint(layer, &scidd, UINT64_MAX, NULL, &capacity); } struct amount_msat local_channel_capacity(const struct local_channel *lc) @@ -298,17 +316,14 @@ 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_channels, *scidd); -} - void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, struct gossmap_localmods *localmods) { const struct local_channel *lc; struct local_channel_hash_iter lcit; + const struct local_update *lu; + struct local_update_hash_iter luit; /* First, disable all channels into blocked nodes (local updates * can add new ones)! */ @@ -335,61 +350,65 @@ void layer_add_localmods(const struct layer *layer, } } + /* Now create new channels */ for (lc = local_channel_hash_first(layer->local_channels, &lcit); lc; lc = local_channel_hash_next(layer->local_channels, &lcit)) { gossmap_local_addchan(localmods, &lc->n1, &lc->n2, lc->scid, lc->capacity, NULL); - for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { - struct short_channel_id_dir scidd; - bool enabled = true; - if (!lc->half[i].enabled) - continue; - scidd.scid = lc->scid; - scidd.dir = i; - gossmap_local_updatechan(localmods, &scidd, - &enabled, - &lc->half[i].htlc_min, - &lc->half[i].htlc_max, - &lc->half[i].base_fee, - &lc->half[i].proportional_fee, - &lc->half[i].delay); - } } - /* Now disable any channels they asked us to */ - for (size_t i = 0; i < tal_count(layer->disabled_channels); i++) { - bool enabled = false; - gossmap_local_updatechan(localmods, - &layer->disabled_channels[i], - &enabled, - NULL, NULL, NULL, NULL, NULL); + /* Now update channels */ + /* Now modify channels, if they exist */ + for (lu = local_update_hash_first(layer->local_updates, &luit); + lu; + lu = local_update_hash_next(layer->local_updates, &luit)) { + gossmap_local_updatechan(localmods, &lu->scidd, + lu->enabled, + lu->htlc_min, + lu->htlc_max, + lu->base_fee, + lu->proportional_fee, + lu->delay); } } static void json_add_local_channel(struct json_stream *response, const char *fieldname, - const struct local_channel *lc, - int dir) + const struct local_channel *lc) { json_object_start(response, fieldname); - - if (dir == 0) { - json_add_node_id(response, "source", &lc->n1); - json_add_node_id(response, "destination", &lc->n2); - } else { - json_add_node_id(response, "source", &lc->n2); - json_add_node_id(response, "destination", &lc->n1); - } + json_add_node_id(response, "source", &lc->n1); + json_add_node_id(response, "destination", &lc->n2); json_add_short_channel_id(response, "short_channel_id", lc->scid); json_add_amount_msat(response, "capacity_msat", lc->capacity); - json_add_amount_msat(response, "htlc_minimum_msat", lc->half[dir].htlc_min); - json_add_amount_msat(response, "htlc_maximum_msat", lc->half[dir].htlc_max); - json_add_amount_msat(response, "fee_base_msat", lc->half[dir].base_fee); - json_add_u32(response, "fee_proportional_millionths", lc->half[dir].proportional_fee); - json_add_u32(response, "delay", lc->half[dir].delay); + json_object_end(response); +} +static void json_add_local_update(struct json_stream *response, + const char *fieldname, + const struct local_update *lu) +{ + json_object_start(response, fieldname); + json_add_short_channel_id_dir(response, "short_channel_id_dir", + lu->scidd); + if (lu->enabled) + json_add_bool(response, "enabled", *lu->enabled); + if (lu->htlc_min) + json_add_amount_msat(response, + "htlc_minimum_msat", *lu->htlc_min); + if (lu->htlc_max) + json_add_amount_msat(response, + "htlc_maximum_msat", *lu->htlc_max); + if (lu->base_fee) + json_add_amount_msat(response, "fee_base_msat", *lu->base_fee); + if (lu->proportional_fee) + json_add_u32(response, + "fee_proportional_millionths", + *lu->proportional_fee); + if (lu->delay) + json_add_u32(response, "cltv_expiry_delta", *lu->delay); json_object_end(response); } @@ -416,6 +435,8 @@ static void json_add_layer(struct json_stream *js, { struct local_channel_hash_iter lcit; const struct local_channel *lc; + const struct local_update *lu; + struct local_update_hash_iter luit; struct constraint_hash_iter conit; const struct constraint *c; @@ -425,18 +446,18 @@ 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_channels); i++) - json_add_short_channel_id_dir(js, NULL, layer->disabled_channels[i]); - json_array_end(js); json_array_start(js, "created_channels"); for (lc = local_channel_hash_first(layer->local_channels, &lcit); lc; lc = local_channel_hash_next(layer->local_channels, &lcit)) { - for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { - if (lc->half[i].enabled) - json_add_local_channel(js, NULL, lc, i); - } + json_add_local_channel(js, NULL, lc); + } + json_array_end(js); + json_array_start(js, "channel_updates"); + for (lu = local_update_hash_first(layer->local_updates, &luit); + lu; + lu = local_update_hash_next(layer->local_updates, &luit)) { + json_add_local_update(js, NULL, lu); } json_array_end(js); json_array_start(js, "constraints"); diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index be5c4cddcfee..32fda7af5ceb 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -53,17 +53,22 @@ bool layer_check_local_channel(const struct local_channel *lc, const struct node_id *n2, struct amount_msat capacity); -/* Update a local channel to a layer: fails if you try to change capacity or nodes! */ -void layer_update_local_channel(struct layer *layer, - const struct node_id *src, - const struct node_id *dst, - struct short_channel_id scid, - struct amount_msat capacity, - struct amount_msat base_fee, - u32 proportional_fee, - u16 delay, - struct amount_msat htlc_min, - struct amount_msat htlc_max); +/* Add a local channel to a layer! */ +void layer_add_local_channel(struct layer *layer, + const struct node_id *src, + const struct node_id *dst, + struct short_channel_id scid, + struct amount_msat capacity); + +/* Update details on a channel (could be in this layer, or another) */ +void layer_add_update_channel(struct layer *layer, + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay); /* If any capacities of channels are limited, unset the corresponding element in * the capacities[] array */ @@ -93,10 +98,6 @@ 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 for this layer, or all if layer is NULL */ void json_add_layers(struct json_stream *js, struct askrene *askrene, diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 40edd3cced78..fe7711b797d1 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -118,8 +118,8 @@ def test_layers(node_factory): expect = {'layer': 'test_layers', 'disabled_nodes': [], - 'disabled_channels': [], 'created_channels': [], + 'channel_updates': [], 'constraints': []} l2.rpc.askrene_create_layer('test_layers') l2.rpc.askrene_disable_node('test_layers', l1.info['id']) @@ -129,8 +129,9 @@ def test_layers(node_factory): 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") + l2.rpc.askrene_update_channel('test_layers', "0x0x1/0", False) + expect['channel_updates'].append({'short_channel_id_dir': "0x0x1/0", + 'enabled': False}) assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} with pytest.raises(RpcError, match="Layer already exists"): l2.rpc.askrene_create_layer('test_layers') @@ -140,18 +141,44 @@ def test_layers(node_factory): l3.info['id'], l1.info['id'], '0x0x1', - '1000000sat', - 100, '900000sat', - 1, 2, 18) - expect['created_channels'].append({'source': l3.info['id'], - 'destination': l1.info['id'], + '1000000sat') + # src/dst gets turned into BOLT 7 order + expect['created_channels'].append({'source': l1.info['id'], + 'destination': l3.info['id'], 'short_channel_id': '0x0x1', - 'capacity_msat': 1000000000, - 'htlc_minimum_msat': 100, - 'htlc_maximum_msat': 900000000, - 'fee_base_msat': 1, - 'fee_proportional_millionths': 2, - 'delay': 18}) + 'capacity_msat': 1000000000}) + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + + # And give details. + l2.rpc.askrene_update_channel(layer='test_layers', + short_channel_id_dir='0x0x1/0', + htlc_minimum_msat=100, + htlc_maximum_msat=900000000, + fee_base_msat=1, + fee_proportional_millionths=2, + cltv_expiry_delta=18) + # This is *still* disabled, since we disabled it above! + expect['channel_updates'] = [{'short_channel_id_dir': '0x0x1/0', + 'enabled': False, + 'htlc_minimum_msat': 100, + 'htlc_maximum_msat': 900000000, + 'fee_base_msat': 1, + 'fee_proportional_millionths': 2, + 'cltv_expiry_delta': 18}] + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + + # Now enable (and change another value for good measure! + l2.rpc.askrene_update_channel(layer='test_layers', + short_channel_id_dir='0x0x1/0', + enabled=True, + cltv_expiry_delta=19) + expect['channel_updates'] = [{'short_channel_id_dir': '0x0x1/0', + 'enabled': True, + 'htlc_minimum_msat': 100, + 'htlc_maximum_msat': 900000000, + 'fee_base_msat': 1, + 'fee_proportional_millionths': 2, + 'cltv_expiry_delta': 19}] assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} # We can tell it about made up channels... @@ -161,9 +188,7 @@ def test_layers(node_factory): 100000, 'unconstrained') last_timestamp = int(time.time()) + 1 - # Maximum for created channels is the real capacity. expect['constraints'].append({'short_channel_id_dir': '0x0x1/1', - 'maximum_msat': 1000000000, 'minimum_msat': 100000}) # Check timestamp first. listlayers = l2.rpc.askrene_listlayers('test_layers') @@ -282,7 +307,9 @@ def test_getroutes(node_factory): # Disabling channels makes getroutes fail l1.rpc.askrene_create_layer('chans_disabled') - l1.rpc.askrene_disable_channel("chans_disabled", '0x1x0/1') + l1.rpc.askrene_update_channel(layer="chans_disabled", + short_channel_id_dir='0x1x0/1', + enabled=False) with pytest.raises(RpcError, match="Could not find route"): l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], @@ -591,9 +618,15 @@ def test_sourcefree_on_mods(node_factory, bitcoind): nodemap[0], l1.info['id'], '0x3x3', - '1000000sat', - 100, '900000sat', - 1000, 2000, 18) + '1000000sat') + l1.rpc.askrene_update_channel(layer='test_layers', + short_channel_id_dir=f'0x3x3/{direction(nodemap[0], l1.info["id"])}', + enabled=True, + htlc_minimum_msat=100, + htlc_maximum_msat='900000sat', + fee_base_msat=1000, + fee_proportional_millionths=2000, + cltv_expiry_delta=18) routes = l1.rpc.getroutes(source=nodemap[0], destination=l1.info['id'], amount_msat=1000000, From d2f64d7b63ba477e462ea914d68d811053b09340 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:08:53 +0930 Subject: [PATCH 21/24] askrene: don't replace constraints, simply accumulate. Lagrang3 points out it's less useful (when we time them out), and probably a premature optimization anyway. Suggested-by: Lagrang3 Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 22 ++++--------- plugins/askrene/layer.c | 69 +++++++++++++++++++++++---------------- plugins/askrene/layer.h | 34 ++++++++----------- 3 files changed, 61 insertions(+), 64 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 7d71e1728c28..759004cfc18d 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -443,16 +443,8 @@ void get_constraints(const struct route_query *rq, /* Look through layers for any constraints (might be dummy * ones, for created channels!) */ - for (size_t i = 0; i < tal_count(rq->layers); i++) { - const struct constraint *c; - c = layer_find_constraint(rq->layers[i], &scidd); - if (c) { - if (amount_msat_greater(c->min, *min)) - *min = c->min; - if (amount_msat_less(c->max, *max)) - *max = c->max; - } - } + for (size_t i = 0; i < tal_count(rq->layers); i++) + layer_apply_constraints(rq->layers[i], &scidd, min, max); /* Might be here because it's reserved, but capacity is normal. */ if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL))) @@ -584,7 +576,7 @@ static void add_localchan(struct gossmap_localmods *mods, } /* Known capacity on local channels (ts = max) */ - layer_update_constraint(info->local_layer, scidd, UINT64_MAX, &spendable, &spendable); + layer_add_constraint(info->local_layer, scidd, UINT64_MAX, &spendable, &spendable); } static struct command_result * @@ -824,16 +816,16 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, "Amount overflow with reserves"); if (command_check_only(cmd)) return command_check_done(cmd); - c = layer_update_constraint(layer, scidd, time_now().ts.tv_sec, - NULL, amount); + c = layer_add_constraint(layer, scidd, time_now().ts.tv_sec, + NULL, amount); goto output; case INFORM_UNCONSTRAINED: /* It passed, so the capacity is at least this much (minimal assumption is * that no reserves were used) */ if (command_check_only(cmd)) return command_check_done(cmd); - c = layer_update_constraint(layer, scidd, time_now().ts.tv_sec, - amount, NULL); + c = layer_add_constraint(layer, scidd, time_now().ts.tv_sec, + amount, NULL); goto output; case INFORM_SUCCEEDED: /* FIXME: We could do something useful here! */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 4a6e3ffb7591..d70fff4fea2e 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -27,6 +27,17 @@ struct local_update { const struct amount_msat *htlc_min, *htlc_max; }; +/* A constraint reflects something we learned about a channel */ +struct constraint { + struct short_channel_id_dir scidd; + /* Time this constraint was last updated */ + u64 timestamp; + /* Non-zero means set */ + struct amount_msat min; + /* Non-0xFFFFF.... means set */ + struct amount_msat max; +}; + static const struct short_channel_id_dir * constraint_scidd(const struct constraint *c) { @@ -233,43 +244,45 @@ const struct local_channel *layer_find_local_channel(const struct layer *layer, return local_channel_hash_get(layer->local_channels, scid); } -static struct constraint *layer_find_constraint_nonconst(const struct layer *layer, - const struct short_channel_id_dir *scidd) +void layer_apply_constraints(const struct layer *layer, + const struct short_channel_id_dir *scidd, + struct amount_msat *min, + struct amount_msat *max) { - return constraint_hash_get(layer->constraints, scidd); -} + struct constraint *c; + struct constraint_hash_iter cit; -/* Public one returns const */ -const struct constraint *layer_find_constraint(const struct layer *layer, - const struct short_channel_id_dir *scidd) -{ - return layer_find_constraint_nonconst(layer, scidd); + /* We can have more than one: apply them all! */ + for (c = constraint_hash_getfirst(layer->constraints, scidd, &cit); + c; + c = constraint_hash_getnext(layer->constraints, scidd, &cit)) { + if (amount_msat_greater(c->min, *min)) + *min = c->min; + if (amount_msat_less(c->max, *max)) + *max = c->max; + } } -const struct constraint *layer_update_constraint(struct layer *layer, - const struct short_channel_id_dir *scidd, - u64 timestamp, - const struct amount_msat *min, - const struct amount_msat *max) +const struct constraint *layer_add_constraint(struct layer *layer, + const struct short_channel_id_dir *scidd, + u64 timestamp, + const struct amount_msat *min, + const struct amount_msat *max) { - struct constraint *c = layer_find_constraint_nonconst(layer, scidd); - if (!c) { - c = tal(layer, struct constraint); - c->scidd = *scidd; - c->min = AMOUNT_MSAT(0); - c->max = AMOUNT_MSAT(UINT64_MAX); - constraint_hash_add(layer->constraints, c); - } + struct constraint *c = tal(layer, struct constraint); + c->scidd = *scidd; - /* Increase minimum? */ - if (min && amount_msat_greater(*min, c->min)) + if (min) c->min = *min; - - /* Decrease maximum? */ - if (max && amount_msat_less(*max, c->max)) + else + c->min = AMOUNT_MSAT(0); + if (max) c->max = *max; - + else + c->max = AMOUNT_MSAT(UINT64_MAX); c->timestamp = timestamp; + + constraint_hash_add(layer->constraints, c); return c; } diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 32fda7af5ceb..468dd7326c79 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -17,17 +17,6 @@ struct askrene; struct layer; struct json_stream; -/* A constraint reflects something we learned about a channel */ -struct constraint { - struct short_channel_id_dir scidd; - /* Time this constraint was last updated */ - u64 timestamp; - /* Non-zero means set */ - struct amount_msat min; - /* Non-0xFFFFF.... means set */ - struct amount_msat max; -}; - /* Look up a layer by name. */ struct layer *find_layer(struct askrene *askrene, const char *name); @@ -76,16 +65,19 @@ void layer_clear_overridden_capacities(const struct layer *layer, const struct gossmap *gossmap, fp16_t *capacities); -/* Find a constraint in a layer. */ -const struct constraint *layer_find_constraint(const struct layer *layer, - const struct short_channel_id_dir *scidd); - -/* Add/update one or more constraints on a layer. */ -const struct constraint *layer_update_constraint(struct layer *layer, - const struct short_channel_id_dir *scidd, - u64 timestamp, - const struct amount_msat *min, - const struct amount_msat *max); +/* Apply constraints from a layer (reduce min, increase max). */ +void layer_apply_constraints(const struct layer *layer, + const struct short_channel_id_dir *scidd, + struct amount_msat *min, + struct amount_msat *max) + NO_NULL_ARGS; + +/* Add one or more constraints on a layer. */ +const struct constraint *layer_add_constraint(struct layer *layer, + const struct short_channel_id_dir *scidd, + u64 timestamp, + const struct amount_msat *min, + const struct amount_msat *max); /* Add local channels from this layer. */ void layer_add_localmods(const struct layer *layer, From c55e2d112e0b180c534e62bf191ebbf72b4f8730 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:09:53 +0930 Subject: [PATCH 22/24] askrene: give better feedback when we can't find a suitable route. This turns out to be critical for users: also stops them from bothering us when their node is offline or has insufficient capacity! Signed-off-by: Rusty Russell --- plugins/askrene/Makefile | 6 +- plugins/askrene/askrene.c | 8 +- plugins/askrene/explain_failure.c | 290 ++++++++++++++++++++++++++++++ plugins/askrene/explain_failure.h | 16 ++ plugins/askrene/layer.c | 15 ++ plugins/askrene/layer.h | 6 + plugins/askrene/reserve.c | 26 +++ plugins/askrene/reserve.h | 5 + tests/test_askrene.py | 44 ++++- 9 files changed, 404 insertions(+), 12 deletions(-) create mode 100644 plugins/askrene/explain_failure.c create mode 100644 plugins/askrene/explain_failure.h diff --git a/plugins/askrene/Makefile b/plugins/askrene/Makefile index a55136f75753..989905aee192 100644 --- a/plugins/askrene/Makefile +++ b/plugins/askrene/Makefile @@ -1,5 +1,5 @@ -PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c plugins/askrene/mcf.c plugins/askrene/dijkstra.c plugins/askrene/flow.c plugins/askrene/refine.c -PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h plugins/askrene/mcf.h plugins/askrene/dijkstra.h plugins/askrene/flow.h plugins/askrene/refine.h +PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c plugins/askrene/mcf.c plugins/askrene/dijkstra.c plugins/askrene/flow.c plugins/askrene/refine.c plugins/askrene/explain_failure.c +PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h plugins/askrene/mcf.h plugins/askrene/dijkstra.h plugins/askrene/flow.h plugins/askrene/refine.h plugins/askrene/explain_failure.h PLUGIN_ASKRENE_OBJS := $(PLUGIN_ASKRENE_SRC:.c=.o) $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) @@ -7,4 +7,4 @@ $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) ALL_C_SOURCES += $(PLUGIN_ASKRENE_SRC) ALL_C_HEADERS += $(PLUGIN_ASKRENE_HEADER) -plugins/cln-askrene: $(PLUGIN_ASKRENE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) bitcoin/chainparams.o common/gossmap.o common/sciddir_or_pubkey.o common/gossmods_listpeerchannels.o common/fp16.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o wire/bolt12_wiregen.o wire/onion_wiregen.o +plugins/cln-askrene: $(PLUGIN_ASKRENE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) bitcoin/chainparams.o common/gossmap.o common/sciddir_or_pubkey.o common/gossmods_listpeerchannels.o common/fp16.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o wire/bolt12_wiregen.o wire/onion_wiregen.o common/route.o diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 759004cfc18d..ab33b9f57bee 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -9,6 +9,7 @@ #include "config.h" #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -223,6 +225,7 @@ struct amount_msat get_additional_per_htlc_cost(const struct route_query *rq, return AMOUNT_MSAT(0); } + /* Returns an error message, or sets *routes */ static const char *get_routes(const tal_t *ctx, struct command *cmd, @@ -327,10 +330,7 @@ static const char *get_routes(const tal_t *ctx, flows = minflow(rq, rq, srcnode, dstnode, amount, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows) { - /* FIXME: disjktra here to see if there is any route, and - * diagnose problem (offline peers? Not enough capacity at - * our end? Not enough at theirs?) */ - ret = tal_fmt(ctx, "Could not find route"); + ret = explain_failure(ctx, rq, srcnode, dstnode, amount); goto out; } diff --git a/plugins/askrene/explain_failure.c b/plugins/askrene/explain_failure.c new file mode 100644 index 000000000000..1369a88d2ea1 --- /dev/null +++ b/plugins/askrene/explain_failure.c @@ -0,0 +1,290 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define NO_USABLE_PATHS_STRING "We could not find a usable set of paths." + +/* Dijkstra, reduced to ignore anything but connectivity */ +static bool always_true(const struct gossmap *map, + const struct gossmap_chan *c, + int dir, + struct amount_msat amount, + void *unused) +{ + return true; +} + +static u64 route_score_one(struct amount_msat fee UNUSED, + struct amount_msat risk UNUSED, + struct amount_msat total UNUSED, + int dir UNUSED, + const struct gossmap_chan *c UNUSED) +{ + return 1; +} + +/* This mirrors get_constraints() */ +static const char *why_max_constrained(const tal_t *ctx, + const struct route_query *rq, + struct short_channel_id_dir *scidd, + struct amount_msat amount) +{ + char *ret = NULL; + const char *reservations; + const struct layer *constrains = NULL; + struct amount_msat max = amount; + + /* Figure out the layer that constrains us (most) */ + for (size_t i = 0; i < tal_count(rq->layers); i++) { + struct amount_msat min = AMOUNT_MSAT(0), new_max = max; + + layer_apply_constraints(rq->layers[i], scidd, &min, &new_max); + if (!amount_msat_eq(new_max, max)) + constrains = rq->layers[i]; + max = new_max; + } + + if (constrains) { + if (!ret) + ret = tal_strdup(ctx, ""); + else + tal_append_fmt(&ret, ", "); + tal_append_fmt(&ret, "layer %s says max is %s", + layer_name(constrains), + fmt_amount_msat(tmpctx, max)); + } + + reservations = fmt_reservations(tmpctx, rq->reserved, scidd); + if (reservations) { + if (!ret) + ret = tal_strdup(ctx, ""); + else + tal_append_fmt(&ret, " and "); + tal_append_fmt(&ret, "already reserved %s", reservations); + } + + /* If that doesn't explain it, perhaps it violates htlc_max? */ + if (!ret) { + struct gossmap_chan *c = gossmap_find_chan(rq->gossmap, &scidd->scid); + fp16_t htlc_max = c->half[scidd->dir].htlc_max; + if (amount_msat_greater_fp16(amount, htlc_max)) + ret = tal_fmt(ctx, "exceeds htlc_maximum_msat ~%s", + fmt_amount_msat(tmpctx, + amount_msat(fp16_to_u64(htlc_max)))); + } + + /* This seems unlikely, but don't return NULL. */ + if (!ret) + ret = tal_fmt(ctx, "is constrained"); + return ret; +} + +struct stat { + size_t num_channels; + struct amount_msat capacity; +}; + +struct node_stats { + struct stat total, gossip_known, enabled; +}; + +enum node_direction { + INTO_NODE, + OUT_OF_NODE, +}; + +static void add_stat(struct stat *stat, + struct amount_msat amount) +{ + stat->num_channels++; + if (!amount_msat_accumulate(&stat->capacity, amount)) + abort(); +} + +static void node_stats(const struct route_query *rq, + const struct gossmap_node *node, + enum node_direction node_direction, + struct node_stats *stats) +{ + memset(stats, 0, sizeof(*stats)); + for (size_t i = 0; i < node->num_chans; i++) { + int dir; + struct gossmap_chan *c; + struct amount_msat cap_msat; + + c = gossmap_nth_chan(rq->gossmap, node, i, &dir); + cap_msat = gossmap_chan_get_capacity(rq->gossmap, c); + + if (node_direction == INTO_NODE) + dir = !dir; + + add_stat(&stats->total, cap_msat); + if (gossmap_chan_set(c, dir)) + add_stat(&stats->gossip_known, cap_msat); + if (c->half[dir].enabled) + add_stat(&stats->enabled, cap_msat); + } +} + +static const char *check_capacity(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_node *node, + enum node_direction node_direction, + struct amount_msat amount, + const char *name) +{ + struct node_stats stats; + + node_stats(rq, node, node_direction, &stats); + if (amount_msat_greater(amount, stats.total.capacity)) { + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " Total %s capacity is only %s" + " (in %zu channels).", + name, + fmt_amount_msat(tmpctx, stats.total.capacity), + stats.total.num_channels); + } + if (amount_msat_greater(amount, stats.gossip_known.capacity)) { + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " Missing gossip for %s: only known %zu/%zu channels, leaving capacity only %s of %s.", + name, + stats.gossip_known.num_channels, + stats.total.num_channels, + fmt_amount_msat(tmpctx, stats.gossip_known.capacity), + fmt_amount_msat(tmpctx, stats.total.capacity)); + } + if (amount_msat_greater(amount, stats.enabled.capacity)) { + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " The %s has disabled %zu of %zu channels, leaving capacity only %s of %s.", + name, + stats.total.num_channels - stats.enabled.num_channels, + stats.total.num_channels, + fmt_amount_msat(tmpctx, stats.enabled.capacity), + fmt_amount_msat(tmpctx, stats.total.capacity)); + } + return NULL; +} + +/* Return description of why scidd is disabled scidd */ +static const char *describe_disabled(const tal_t *ctx, + const struct route_query *rq, + const struct short_channel_id_dir *scidd) +{ + for (int i = tal_count(rq->layers) - 1; i >= 0; i--) { + if (layer_disables(rq->layers[i], scidd)) { + return tal_fmt(ctx, "marked disabled by layer %s.", + layer_name(rq->layers[i])); + } + } + + return tal_fmt(ctx, "marked disabled by gossip message."); +} + +static const char *describe_capacity(const tal_t *ctx, + const struct route_query *rq, + const struct short_channel_id_dir *scidd, + struct amount_msat amount) +{ + for (int i = tal_count(rq->layers) - 1; i >= 0; i--) { + if (layer_created(rq->layers[i], scidd->scid)) { + return tal_fmt(ctx, " (created by layer %s) isn't big enough to carry %s.", + layer_name(rq->layers[i]), + fmt_amount_msat(tmpctx, amount)); + } + } + + return tal_fmt(ctx, "isn't big enough to carry %s.", + fmt_amount_msat(tmpctx, amount)); +} + +/* We failed to find a flow at all. Why? */ +const char *explain_failure(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, + struct amount_msat amount) +{ + const struct route_hop *hops; + const struct dijkstra *dij; + char *path; + const char *cap_check; + + /* Do we have enough funds? */ + cap_check = check_capacity(ctx, rq, srcnode, OUT_OF_NODE, + amount, "source"); + if (cap_check) + return cap_check; + + /* Does destination have enough capacity? */ + cap_check = check_capacity(ctx, rq, dstnode, INTO_NODE, + amount, "destination"); + if (cap_check) + return cap_check; + + /* OK, fall back to telling them why didn't shortest path + * work. This covers the "but I have a direct channel!" + * case. */ + dij = dijkstra(tmpctx, rq->gossmap, dstnode, AMOUNT_MSAT(0), 0, + always_true, route_score_one, NULL); + hops = route_from_dijkstra(tmpctx, rq->gossmap, dij, srcnode, + AMOUNT_MSAT(0), 0); + if (!hops) + return tal_fmt(ctx, "There is no connection between source and destination at all"); + + /* Description of shortest path */ + path = tal_strdup(tmpctx, ""); + for (size_t i = 0; i < tal_count(hops); i++) { + tal_append_fmt(&path, "%s%s", + i > 0 ? "->" : "", + fmt_short_channel_id(tmpctx, hops[i].scid)); + } + + /* Now walk through this: is it disabled? Insuff capacity? */ + for (size_t i = 0; i < tal_count(hops); i++) { + const char *explanation; + struct short_channel_id_dir scidd; + struct gossmap_chan *c; + struct amount_msat cap_msat; + + scidd.scid = hops[i].scid; + scidd.dir = hops[i].direction; + c = gossmap_find_chan(rq->gossmap, &scidd.scid); + cap_msat = gossmap_chan_get_capacity(rq->gossmap, c); + if (!gossmap_chan_set(c, scidd.dir)) + explanation = "has no gossip"; + else if (!c->half[scidd.dir].enabled) + explanation = describe_disabled(tmpctx, rq, &scidd); + else if (amount_msat_greater(amount, cap_msat)) + explanation = describe_capacity(tmpctx, rq, &scidd, amount); + else { + struct amount_msat min, max; + get_constraints(rq, c, scidd.dir, &min, &max); + if (amount_msat_less(max, amount)) { + explanation = why_max_constrained(tmpctx, rq, + &scidd, amount); + } else + continue; + } + + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " The shortest path is %s, but %s %s", + path, + fmt_short_channel_id_dir(tmpctx, &scidd), + explanation); + } + + return tal_fmt(ctx, + "Actually, I'm not sure why we didn't find the" + " obvious route %s: perhaps this is a bug?", + path); +} diff --git a/plugins/askrene/explain_failure.h b/plugins/askrene/explain_failure.h new file mode 100644 index 000000000000..ed470f8298dc --- /dev/null +++ b/plugins/askrene/explain_failure.h @@ -0,0 +1,16 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_EXPLAIN_FAILURE_H +#define LIGHTNING_PLUGINS_ASKRENE_EXPLAIN_FAILURE_H +#include "config.h" +#include + +struct route_query; +struct gossmap_node; + +/* When MCF returns nothing, try to explain why */ +const char *explain_failure(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, + struct amount_msat amount); + +#endif /* LIGHTNING_PLUGINS_ASKRENE_EXPLAIN_FAILURE_H */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index d70fff4fea2e..26d602853812 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -502,6 +502,21 @@ void json_add_layers(struct json_stream *js, json_array_end(js); } +bool layer_created(const struct layer *layer, struct short_channel_id scid) +{ + return local_channel_hash_get(layer->local_channels, scid); +} + +bool layer_disables(const struct layer *layer, + const struct short_channel_id_dir *scidd) +{ + const struct local_update *lu; + + lu = local_update_hash_get(layer->local_updates, scidd); + + return (lu && lu->enabled && *lu->enabled == false); +} + void layer_memleak_mark(struct askrene *askrene, struct htable *memtable) { struct layer *l; diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 468dd7326c79..b8e700fdc46e 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -102,6 +102,12 @@ void json_add_constraint(struct json_stream *js, const struct constraint *c, const struct layer *layer); +/* For explain_failure: did this layer create this scid? */ +bool layer_created(const struct layer *layer, struct short_channel_id scid); + +/* For explain_failure: did this layer disable this channel? */ +bool layer_disables(const struct layer *layer, const struct short_channel_id_dir *scidd); + /* Scan for memleaks */ void layer_memleak_mark(struct askrene *askrene, struct htable *memtable); #endif /* LIGHTNING_PLUGINS_ASKRENE_LAYER_H */ diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c index a58bc6d5f24e..82c9096d3c27 100644 --- a/plugins/askrene/reserve.c +++ b/plugins/askrene/reserve.c @@ -151,6 +151,32 @@ void json_add_reservations(struct json_stream *js, json_array_end(js); } +const char *fmt_reservations(const tal_t *ctx, + const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd) +{ + struct reserve *r; + struct reserve_htable_iter rit; + char *ret = NULL; + + for (r = reserve_htable_getfirst(reserved, scidd, &rit); + r; + r = reserve_htable_getnext(reserved, scidd, &rit)) { + u64 seconds; + if (!ret) + ret = tal_strdup(ctx, ""); + else + tal_append_fmt(&ret, ", "); + tal_append_fmt(&ret, "%s by command %s", + fmt_amount_msat(tmpctx, r->rhop.amount), r->cmd_id); + seconds = timemono_between(time_mono(), r->timestamp).ts.tv_sec; + /* Add a note if it's old */ + if (seconds > 0) + tal_append_fmt(&ret, " (%"PRIu64" seconds ago)", seconds); + } + return ret; +} + void reserve_memleak_mark(struct askrene *askrene, struct htable *memtable) { memleak_scan_htable(memtable, &askrene->reserved->raw); diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h index 103ed11083cd..1c9e71eaaf60 100644 --- a/plugins/askrene/reserve.h +++ b/plugins/askrene/reserve.h @@ -40,6 +40,11 @@ bool reserve_accumulate(const struct reserve_htable *reserved, const struct short_channel_id_dir *scidd, struct amount_msat *amount); +/* To explain why we couldn't route */ +const char *fmt_reservations(const tal_t *ctx, + const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd); + /* Print out a json object for all reservations */ void json_add_reservations(struct json_stream *js, const struct reserve_htable *reserved, diff --git a/tests/test_askrene.py b/tests/test_askrene.py index fe7711b797d1..fe065c1e0e5b 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -20,8 +20,10 @@ def test_reserve(node_factory): l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) assert l1.rpc.askrene_listreservations() == {'reservations': []} - scid12dir = f"{first_scid(l1, l2)}/{direction(l1.info['id'], l2.info['id'])}" - scid23dir = f"{first_scid(l2, l3)}/{direction(l2.info['id'], l3.info['id'])}" + scid12 = first_scid(l1, l2) + scid23 = first_scid(l2, l3) + scid12dir = f"{scid12}/{direction(l1.info['id'], l2.info['id'])}" + scid23dir = f"{scid23}/{direction(l2.info['id'], l3.info['id'])}" initial_prob = l1.rpc.getroutes(source=l1.info['id'], destination=l3.info['id'], @@ -60,8 +62,12 @@ def test_reserve(node_factory): {'short_channel_id_dir': scid23dir, 'amount_msat': 1000_000_000_000}]) - # FIXME: better error! - with pytest.raises(RpcError, match="Could not find route"): + # Keep it consistent: the below will mention a time if >= 1 seconds old, + # which might happen without the sleep on slow machines. + time.sleep(2) + + # Reservations can be in either order. + with pytest.raises(RpcError, match=rf'We could not find a usable set of paths. The shortest path is {scid12}->{scid23}, but {scid12dir} already reserved 10000000*msat by command ".*" \([0-9]* seconds ago\), 10000000*msat by command ".*" \([0-9]* seconds ago\)'): l1.rpc.getroutes(source=l1.info['id'], destination=l3.info['id'], amount_msat=1000000, @@ -305,18 +311,46 @@ def test_getroutes(node_factory): # Set up l1 with this as the gossip_store l1 = node_factory.get_node(gossip_store_file=gsfile.name) + # Too much should give a decent explanation. + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. The shortest path is 0x1x0, but 0x1x0/1 isn't big enough to carry 1000000001msat\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000000001, + layers=[], + maxfee_msat=100000000, + final_cltv=99) + + # This should tell us source doesn't have enough. + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. Total source capacity is only 1019000000msat \(in 3 channels\)\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=2000000001, + layers=[], + maxfee_msat=20000000, + final_cltv=99) + + # This should tell us dest doesn't have enough. + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. Total destination capacity is only 1000000000msat \(in 1 channels\)\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[4], + amount_msat=1000000001, + layers=[], + maxfee_msat=30000000, + final_cltv=99) + # Disabling channels makes getroutes fail l1.rpc.askrene_create_layer('chans_disabled') l1.rpc.askrene_update_channel(layer="chans_disabled", short_channel_id_dir='0x1x0/1', enabled=False) - with pytest.raises(RpcError, match="Could not find route"): + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. The shortest path is 0x1x0, but 0x1x0/1 marked disabled by layer chans_disabled\."): 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], From 4d473f1bfff50d61d6623d6a288adfc93831717c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:10:53 +0930 Subject: [PATCH 23/24] askrene: trivial changes to avoid -O3 compiler warnings. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The code is a bit too complex for gcc to track it: ``` In file included from ccan/ccan/tal/str/str.h:7, from plugins/askrene/askrene.c:11: plugins/askrene/askrene.c: In function ‘do_getroutes’: ccan/ccan/tal/tal.h:324:23: error: ‘routes’ may be used uninitialized in this function [-Werror=maybe-uninitialized] 324 | #define tal_count(p) (tal_bytelen(p) / sizeof(*p)) | ^~~~~~~~~~~ plugins/askrene/askrene.c:476:24: note: ‘routes’ was declared here 476 | struct route **routes; | ^~~~~~ plugins/askrene/askrene.c:475:29: error: ‘amounts’ may be used uninitialized in this function [-Werror=maybe-uninitialized] 475 | struct amount_msat *amounts; | ^~~~~~~ plugins/askrene/askrene.c:488:69: error: ‘probability’ may be used uninitialized in this function [-Werror=maybe-uninitialized] 488 | json_add_u64(response, "probability_ppm", (u64)(probability * 1000000)); | ~~~~~~~~~~~~~^~~~~~~~~~ cc plugins/askrene/dijkstra.c cc1: all warnings being treated as errors ``` On my local machine, it also warns in param_dev_channel, so I fixed that too. Signed-off-by: Rusty Russell --- lightningd/peer_control.c | 2 +- plugins/askrene/askrene.c | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index d514f59364cb..755a06f9d9d3 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -3167,7 +3167,7 @@ static struct command_result *param_dev_channel(struct command *cmd, const jsmntok_t *tok, struct channel **channel) { - struct peer *peer; + struct peer *peer COMPILER_WANTS_INIT("gcc version 12.3.0 -O3"); struct command_result *res; bool more_than_one; diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index ab33b9f57bee..db720f0d96ba 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -301,13 +301,13 @@ static const char *get_routes(const tal_t *ctx, srcnode = gossmap_find_node(askrene->gossmap, source); if (!srcnode) { ret = tal_fmt(ctx, "Unknown source node %s", fmt_node_id(tmpctx, source)); - goto out; + goto fail; } dstnode = gossmap_find_node(askrene->gossmap, dest); if (!dstnode) { ret = tal_fmt(ctx, "Unknown destination node %s", fmt_node_id(tmpctx, dest)); - goto out; + goto fail; } delay_feefactor = 1.0/1000000; @@ -331,7 +331,7 @@ static const char *get_routes(const tal_t *ctx, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows) { ret = explain_failure(ctx, rq, srcnode, dstnode, amount); - goto out; + goto fail; } /* Too much delay? */ @@ -348,7 +348,7 @@ static const char *get_routes(const tal_t *ctx, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows || delay_feefactor > 10) { ret = tal_fmt(ctx, "Could not find route without excessive delays"); - goto out; + goto fail; } } @@ -359,13 +359,13 @@ static const char *get_routes(const tal_t *ctx, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows || mu == 100) { ret = tal_fmt(ctx, "Could not find route without excessive cost"); - goto out; + goto fail; } } if (finalcltv + flows_worst_delay(flows) > 2016) { ret = tal_fmt(ctx, "Could not find route without excessive cost or delays"); - goto out; + goto fail; } /* The above did not take into account the extra funds to pay @@ -374,7 +374,7 @@ static const char *get_routes(const tal_t *ctx, * still possible */ ret = refine_with_fees_and_limits(ctx, rq, amount, &flows); if (ret) - goto out; + goto fail; /* Convert back into routes, with delay and other information fixed */ *routes = tal_arr(ctx, struct route *, tal_count(flows)); @@ -412,9 +412,13 @@ static const char *get_routes(const tal_t *ctx, } *probability = flowset_probability(flows, rq); - ret = NULL; + gossmap_remove_localmods(askrene->gossmap, localmods); + return NULL; -out: + /* Explicit failure path keeps the compiler (gcc version 12.3.0 -O3) from + * warning about uninitialized variables in the caller */ +fail: + assert(ret != NULL); gossmap_remove_localmods(askrene->gossmap, localmods); return ret; } From a74686fa2b114100ffc4f49d52dba52b20ff10df Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 4 Oct 2024 09:17:12 +0930 Subject: [PATCH 24/24] askrene: more code tweaks on feedback from Lagrang3. 1. describe_disabled should point out if node itself is disabled. 2. Hoist constraint check for neater if branching. 3. Use amount_msat_max/min for greater clarity. 4. Simply disable channels, don't zero htlc_min/max when node disabled. I also fixed the diagnostic of htlc_max correctly, which removes a FIXME. Signed-off-by: Rusty Russell --- plugins/askrene/explain_failure.c | 49 ++++++++++++++++--------------- plugins/askrene/layer.c | 24 +++++++++------ plugins/askrene/layer.h | 5 +++- tests/test_askrene.py | 3 +- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/plugins/askrene/explain_failure.c b/plugins/askrene/explain_failure.c index 1369a88d2ea1..b8ac907d132d 100644 --- a/plugins/askrene/explain_failure.c +++ b/plugins/askrene/explain_failure.c @@ -69,16 +69,6 @@ static const char *why_max_constrained(const tal_t *ctx, tal_append_fmt(&ret, "already reserved %s", reservations); } - /* If that doesn't explain it, perhaps it violates htlc_max? */ - if (!ret) { - struct gossmap_chan *c = gossmap_find_chan(rq->gossmap, &scidd->scid); - fp16_t htlc_max = c->half[scidd->dir].htlc_max; - if (amount_msat_greater_fp16(amount, htlc_max)) - ret = tal_fmt(ctx, "exceeds htlc_maximum_msat ~%s", - fmt_amount_msat(tmpctx, - amount_msat(fp16_to_u64(htlc_max)))); - } - /* This seems unlikely, but don't return NULL. */ if (!ret) ret = tal_fmt(ctx, "is constrained"); @@ -176,11 +166,19 @@ static const char *check_capacity(const tal_t *ctx, /* Return description of why scidd is disabled scidd */ static const char *describe_disabled(const tal_t *ctx, - const struct route_query *rq, - const struct short_channel_id_dir *scidd) + const struct route_query *rq, + const struct gossmap_chan *c, + const struct short_channel_id_dir *scidd) { for (int i = tal_count(rq->layers) - 1; i >= 0; i--) { - if (layer_disables(rq->layers[i], scidd)) { + struct gossmap_node *dst = gossmap_nth_node(rq->gossmap, c, !scidd->dir); + struct node_id dstid; + + gossmap_node_get_id(rq->gossmap, dst, &dstid); + if (layer_disables_node(rq->layers[i], &dstid)) + return tal_fmt(ctx, "leads to node disabled by layer %s.", + layer_name(rq->layers[i])); + else if (layer_disables_chan(rq->layers[i], scidd)) { return tal_fmt(ctx, "marked disabled by layer %s.", layer_name(rq->layers[i])); } @@ -253,27 +251,30 @@ const char *explain_failure(const tal_t *ctx, const char *explanation; struct short_channel_id_dir scidd; struct gossmap_chan *c; - struct amount_msat cap_msat; + struct amount_msat cap_msat, min, max, htlc_max; scidd.scid = hops[i].scid; scidd.dir = hops[i].direction; c = gossmap_find_chan(rq->gossmap, &scidd.scid); cap_msat = gossmap_chan_get_capacity(rq->gossmap, c); + get_constraints(rq, c, scidd.dir, &min, &max); + htlc_max = amount_msat(fp16_to_u64(c->half[scidd.dir].htlc_max)); + if (!gossmap_chan_set(c, scidd.dir)) explanation = "has no gossip"; else if (!c->half[scidd.dir].enabled) - explanation = describe_disabled(tmpctx, rq, &scidd); + explanation = describe_disabled(tmpctx, rq, c, &scidd); else if (amount_msat_greater(amount, cap_msat)) explanation = describe_capacity(tmpctx, rq, &scidd, amount); - else { - struct amount_msat min, max; - get_constraints(rq, c, scidd.dir, &min, &max); - if (amount_msat_less(max, amount)) { - explanation = why_max_constrained(tmpctx, rq, - &scidd, amount); - } else - continue; - } + else if (amount_msat_greater(amount, max)) + explanation = why_max_constrained(tmpctx, rq, + &scidd, amount); + else if (amount_msat_greater(amount, htlc_max)) + explanation = tal_fmt(tmpctx, + "exceeds htlc_maximum_msat ~%s", + fmt_amount_msat(tmpctx, htlc_max)); + else + continue; return tal_fmt(ctx, NO_USABLE_PATHS_STRING diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 26d602853812..17cadc8e8983 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -256,10 +256,8 @@ void layer_apply_constraints(const struct layer *layer, for (c = constraint_hash_getfirst(layer->constraints, scidd, &cit); c; c = constraint_hash_getnext(layer->constraints, scidd, &cit)) { - if (amount_msat_greater(c->min, *min)) - *min = c->min; - if (amount_msat_less(c->max, *max)) - *max = c->max; + *min = amount_msat_max(*min, c->min); + *max = amount_msat_min(*max, c->max); } } @@ -350,7 +348,6 @@ void layer_add_localmods(const struct layer *layer, struct short_channel_id_dir scidd; struct gossmap_chan *c; bool enabled = false; - struct amount_msat zero = AMOUNT_MSAT(0); c = gossmap_nth_chan(gossmap, node, n, &scidd.dir); scidd.scid = gossmap_chan_scid(gossmap, c); @@ -358,8 +355,7 @@ void layer_add_localmods(const struct layer *layer, gossmap_local_updatechan(localmods, &scidd, &enabled, - &zero, &zero, - NULL, NULL, NULL); + NULL, NULL, NULL, NULL, NULL); } } @@ -507,8 +503,8 @@ bool layer_created(const struct layer *layer, struct short_channel_id scid) return local_channel_hash_get(layer->local_channels, scid); } -bool layer_disables(const struct layer *layer, - const struct short_channel_id_dir *scidd) +bool layer_disables_chan(const struct layer *layer, + const struct short_channel_id_dir *scidd) { const struct local_update *lu; @@ -517,6 +513,16 @@ bool layer_disables(const struct layer *layer, return (lu && lu->enabled && *lu->enabled == false); } +bool layer_disables_node(const struct layer *layer, + const struct node_id *node) +{ + for (size_t i = 0; i < tal_count(layer->disabled_nodes); i++) { + if (node_id_eq(&layer->disabled_nodes[i], node)) + return true; + } + return false; +} + void layer_memleak_mark(struct askrene *askrene, struct htable *memtable) { struct layer *l; diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index b8e700fdc46e..0bac5744a62b 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -106,7 +106,10 @@ void json_add_constraint(struct json_stream *js, bool layer_created(const struct layer *layer, struct short_channel_id scid); /* For explain_failure: did this layer disable this channel? */ -bool layer_disables(const struct layer *layer, const struct short_channel_id_dir *scidd); +bool layer_disables_chan(const struct layer *layer, const struct short_channel_id_dir *scidd); + +/* For explain_failure: did this layer disable this node? */ +bool layer_disables_node(const struct layer *layer, const struct node_id *node); /* Scan for memleaks */ void layer_memleak_mark(struct askrene *askrene, struct htable *memtable); diff --git a/tests/test_askrene.py b/tests/test_askrene.py index fe065c1e0e5b..5a7e7abe7c2a 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -845,8 +845,7 @@ def test_max_htlc(node_factory, bitcoind): amount_msat=1, inform='constrained') - # FIXME: Better diag! - with pytest.raises(RpcError, match="Could not find route"): + with pytest.raises(RpcError, match="We could not find a usable set of paths. The shortest path is 0x1x0, but 0x1x0/1 exceeds htlc_maximum_msat ~1000448msat"): l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], amount_msat=20_000_000,