diff --git a/doc/Makefile b/doc/Makefile index fd59c4182646..f122f2ec043c 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -6,7 +6,7 @@ doc-wrongdir: GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-addpsbtoutput.7 \ - doc/lightning-askrene-create-channel.7 \ + doc/lightning-askrene-create-channels.7 \ doc/lightning-askrene-disable-node.7 \ doc/lightning-askrene-inform-channel.7 \ doc/lightning-askrene-listlayers.7 \ diff --git a/doc/schemas/lightning-askrene-create-channel.json b/doc/schemas/lightning-askrene-create-channel.json deleted file mode 100644 index ff43fdf7eb99..000000000000 --- a/doc/schemas/lightning-askrene-create-channel.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "$schema": "../rpc-schema-draft.json", - "type": "object", - "additionalProperties": false, - "rpc": "askrene-create-channel", - "title": "Command to add a channel to layer (EXPERIMENTAL)", - "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." - ], - "request": { - "required": [ - "layer", - "source", - "destination", - "short_channel_id", - "capacity_msat", - "htlc_min", - "htlc_max", - "base_fee", - "proportional_fee", - "delay" - ], - "properties": { - "layer": { - "type": "string", - "description": [ - "The name of the layer to apply this change to." - ] - }, - "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. If a channel with this short channel id already exists in *layer*, the *source*, *destination* and *capacity_msat* must be the same." - ] - }, - "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." - ] - } - } - }, - "response": { - "required": [], - "properties": {} - }, - "see_also": [ - "lightning-getroutes(7)", - "lightning-askrene-disable-node(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-create-channels.json b/doc/schemas/lightning-askrene-create-channels.json new file mode 100644 index 000000000000..56d0a3c43ebb --- /dev/null +++ b/doc/schemas/lightning-askrene-create-channels.json @@ -0,0 +1,116 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-create-channels", + "title": "Command to add channels to layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-create-channels** RPC command tells askrene to populate one direction of a list of channels in the given layer. If the channels already exist, they will be overridden. If the layer does not exist, it will be created." + ], + "request": { + "required": [ + "layer", + "channels" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "destination", + "short_channel_id", + "capacity_msat", + "htlc_mininum_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. If a channel with this short channel id already exists in *layer*, the *source*, *destination* and *capacity_msat* must be the same." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + }, + "htlc_mininum_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." + ] + } + } + } + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-disable-node(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-disable-node.json b/doc/schemas/lightning-askrene-disable-node.json index 389dacaed91e..4b690685ffb5 100644 --- a/doc/schemas/lightning-askrene-disable-node.json +++ b/doc/schemas/lightning-askrene-disable-node.json @@ -7,7 +7,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-disable-node** RPC command tells askrene to disable all channels connected to a node whenever the given layer is used. This is mainly useful to force the use of alternate paths: while individual channels can be disabled using askrene-create-channel or askrene-inform-channel, that would be racy if new channels appeared." + "The **askrene-disable-node** RPC command tells askrene to disable all channels connected to a node whenever the given layer is used. This is mainly useful to force the use of alternate paths: while individual channels can be disabled using askrene-create-channels or askrene-inform-channel, that would be racy if new channels appeared." ], "request": { "required": [ @@ -35,7 +35,7 @@ }, "see_also": [ "lightning-getroutes(7)", - "lightning-askrene-create-channel(7)", + "lightning-askrene-create-channels(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" diff --git a/doc/schemas/lightning-askrene-inform-channel.json b/doc/schemas/lightning-askrene-inform-channel.json index 8ba0f90f62e3..26013e69fd90 100644 --- a/doc/schemas/lightning-askrene-inform-channel.json +++ b/doc/schemas/lightning-askrene-inform-channel.json @@ -98,7 +98,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", + "lightning-askrene-create-channels(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" ], diff --git a/doc/schemas/lightning-askrene-listlayers.json b/doc/schemas/lightning-askrene-listlayers.json index a0a5cba70511..773a7bd3e745 100644 --- a/doc/schemas/lightning-askrene-listlayers.json +++ b/doc/schemas/lightning-askrene-listlayers.json @@ -169,7 +169,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", + "lightning-askrene-create-channels(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-age(7)" ], diff --git a/doc/schemas/lightning-askrene-reserve.json b/doc/schemas/lightning-askrene-reserve.json index 7b91e3800998..7f6744e04419 100644 --- a/doc/schemas/lightning-askrene-reserve.json +++ b/doc/schemas/lightning-askrene-reserve.json @@ -58,7 +58,7 @@ "lightning-getroutes(7)", "lightning-askrene-unreserve(7)", "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", + "lightning-askrene-create-channels(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" diff --git a/doc/schemas/lightning-askrene-unreserve.json b/doc/schemas/lightning-askrene-unreserve.json index 377595a5caa5..fbde9ad64d78 100644 --- a/doc/schemas/lightning-askrene-unreserve.json +++ b/doc/schemas/lightning-askrene-unreserve.json @@ -58,7 +58,7 @@ "lightning-getroutes(7)", "lightning-askrene-reserve(7)", "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", + "lightning-askrene-create-channels(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" diff --git a/doc/schemas/lightning-getroutes.json b/doc/schemas/lightning-getroutes.json index 5b09bda28c57..223bd6d1404a 100644 --- a/doc/schemas/lightning-getroutes.json +++ b/doc/schemas/lightning-getroutes.json @@ -173,7 +173,7 @@ "lightning-askrene-reserve(7)", "lightning-askrene-unreserve(7)", "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", + "lightning-askrene-create-channels(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-report(7)", "lightning-askrene-age(7)" diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 0952680f60c8..8613c0e1fab0 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -58,6 +58,49 @@ static struct command_result *param_string_array(struct command *cmd, return NULL; } +static struct command_result * +param_localchannel_array(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + struct local_channel **channels) +{ + size_t i; + const jsmntok_t *t; + + if (tok->type != JSMN_ARRAY) + return command_fail_badparam(cmd, name, buffer, tok, + "should be an array"); + + *channels = tal_arr(cmd, struct local_channel, tok->size); + json_for_each_arr(i, t, tok) + { + struct local_channel *c = &(*channels)[i]; + + const char *err = json_scan( + cmd, buffer, t, + "{source:%" + ",destination:%" + ",short_channel_id:%" + ",capacity_msat:%" + ",htlc_minimum_msat:%" + ",htlc_maximum_msat:%" + ",fee_base_msat:%" + ",fee_proportional_millionths:%" + ",delay:%}", + JSON_SCAN(json_to_node_id, &c->n1), + JSON_SCAN(json_to_node_id, &c->n2), + JSON_SCAN(json_to_short_channel_id, &c->scid), + JSON_SCAN(json_to_msat, &c->capacity), + JSON_SCAN(json_to_msat, &c->half[0].htlc_min), + JSON_SCAN(json_to_msat, &c->half[0].htlc_max), + JSON_SCAN(json_to_msat, &c->half[0].base_fee), + JSON_SCAN(json_to_u32, &c->half[0].proportional_fee), + JSON_SCAN(json_to_u16, &c->half[0].delay)); + if (err) + return command_fail_badparam(cmd, name, buffer, t, err); + } + return NULL; +} + static struct command_result *param_known_layer(struct command *cmd, const char *name, const char *buffer, @@ -673,56 +716,46 @@ static struct command_result *param_layername(struct command *cmd, return NULL; } -static struct command_result *json_askrene_create_channel(struct command *cmd, - const char *buffer, - const jsmntok_t *params) +static struct command_result * +json_askrene_create_channels(struct command *cmd, const char *buffer, + const jsmntok_t *params) { const char *layername; + struct local_channel *channels; 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; struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, p_req("layer", param_layername, &layername), - p_req("source", param_node_id, &src), - 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), + p_req("channels", param_localchannel_array, &channels), NULL)) 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; - if (command_check_only(cmd)) return command_check_done(cmd); + layer = find_layer(askrene, layername); 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); + for (size_t i = 0; i < tal_count(channels); i++) { + struct local_channel *c = &channels[i]; + + /* If it exists, it must match */ + lc = layer_find_local_channel(layer, c->scid); + if (lc && + !layer_check_local_channel(lc, &c->n1, &c->n2, c->capacity)) + return command_fail( + cmd, JSONRPC2_INVALID_PARAMS, + "channel already exists with different values!"); + + layer_update_local_channel( + layer, &c->n1, &c->n2, c->scid, c->capacity, + c->half[0].base_fee, c->half[0].proportional_fee, + c->half[0].delay, c->half[0].htlc_min, c->half[0].htlc_max); + } response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); @@ -866,8 +899,8 @@ static const struct plugin_command commands[] = { json_askrene_disable_node, }, { - "askrene-create-channel", - json_askrene_create_channel, + "askrene-create-channels", + json_askrene_create_channels, }, { "askrene-inform-channel", diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 3991716dc988..49ce07becdc9 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -8,23 +8,6 @@ #include #include -/* A channels which doesn't (necessarily) exist in the gossmap. */ -struct local_channel { - /* Canonical order, n1 < n2 */ - struct node_id n1, n2; - struct short_channel_id scid; - struct amount_msat capacity; - - 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]; -}; - static const struct constraint_key * constraint_key(const struct constraint *c) { diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 7acea220c072..64b53ad598ec 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -17,6 +17,23 @@ struct askrene; struct layer; struct json_stream; +/* A channels which doesn't (necessarily) exist in the gossmap. */ +struct local_channel { + /* Canonical order, n1 < n2 */ + struct node_id n1, n2; + struct short_channel_id scid; + struct amount_msat capacity; + + 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]; +}; + enum constraint_type { CONSTRAINT_MIN, CONSTRAINT_MAX, diff --git a/tests/test_askrene.py b/tests/test_askrene.py index e68fe1831339..31446deb5cf5 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -28,13 +28,22 @@ def test_layers(node_factory): assert l2.rpc.askrene_listlayers('test_layers2') == {'layers': []} # Tell it l3 connects to l1! - l2.rpc.askrene_create_channel('test_layers', - l3.info['id'], - l1.info['id'], - '0x0x1', - '1000000sat', - 100, '900000sat', - 1, 2, 18) + l2.rpc.askrene_create_channels( + "test_layers", + [ + { + "source": l3.info["id"], + "destination": l1.info["id"], + "short_channel_id": "0x0x1", + "capacity_msat": "1000000sat", + "htlc_minimum_msat": 100, + "htlc_maximum_msat": "900000sat", + "fee_base_msat": 1, + "fee_proportional_millionths": 2, + "delay": 18, + } + ], + ) expect['created_channels'].append({'source': l3.info['id'], 'destination': l1.info['id'], 'short_channel_id': '0x0x1',