From 184c188e8863156b3a513466f9b499f4b433887a Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 8 Apr 2026 08:48:07 +0100 Subject: [PATCH 1/2] libplugin: add helpers for multi-string options Changelog-None Signed-off-by: Lagrang3 --- plugins/libplugin.c | 18 ++++++++++++++++++ plugins/libplugin.h | 4 ++++ tests/plugins/test_libplugin.c | 23 ++--------------------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/plugins/libplugin.c b/plugins/libplugin.c index b39058d46559..db2e53fb730a 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -1688,6 +1688,14 @@ char *charp_option(struct command *cmd, const char *arg, bool check_only, char * return NULL; } +char *multi_string_option(struct command *cmd, const char *arg, bool check_only, + const char ***arr) +{ + if (!check_only) + tal_arr_expand(arr, tal_strdup(*arr, arg)); + return NULL; +} + bool u64_jsonfmt(struct command *cmd, struct json_stream *js, const char *fieldname, u64 *i) { json_add_u64(js, fieldname, *i); @@ -1720,6 +1728,16 @@ bool charp_jsonfmt(struct command *cmd, struct json_stream *js, const char *fiel return true; } +bool string_array_jsonfmt(struct command *cmd, struct json_stream *js, + const char *fieldname, const char ***arr) +{ + json_array_start(js, fieldname); + for (size_t i = 0; i < tal_count(*arr); i++) + json_add_string(js, NULL, (*arr)[i]); + json_array_end(js); + return true; +} + bool flag_jsonfmt(struct command *cmd, struct json_stream *js, const char *fieldname, bool *i) { /* Don't print if the default (false) */ diff --git a/plugins/libplugin.h b/plugins/libplugin.h index bddba28f7735..3e435f65e4cb 100644 --- a/plugins/libplugin.h +++ b/plugins/libplugin.h @@ -638,6 +638,8 @@ char *u32_option(struct command *cmd, const char *arg, bool check_only, u32 *i); char *u16_option(struct command *cmd, const char *arg, bool check_only, u16 *i); char *bool_option(struct command *cmd, const char *arg, bool check_only, bool *i); char *charp_option(struct command *cmd, const char *arg, bool check_only, char **p); +char *multi_string_option(struct command *cmd, const char *arg, bool check_only, + const char ***arr); char *flag_option(struct command *cmd, const char *arg, bool check_only, bool *i); bool u64_jsonfmt(struct command *cmd, struct json_stream *js, const char *fieldname, @@ -650,6 +652,8 @@ bool bool_jsonfmt(struct command *cmd, struct json_stream *js, const char *field bool *i); bool charp_jsonfmt(struct command *cmd, struct json_stream *js, const char *fieldname, char **p); +bool string_array_jsonfmt(struct command *cmd, struct json_stream *js, + const char *fieldname, const char ***arr); /* Usually equivalent to NULL, since flag must default to false be useful! */ bool flag_jsonfmt(struct command *cmd, struct json_stream *js, const char *fieldname, diff --git a/tests/plugins/test_libplugin.c b/tests/plugins/test_libplugin.c index a89b6164946d..907d5c4b44bf 100644 --- a/tests/plugins/test_libplugin.c +++ b/tests/plugins/test_libplugin.c @@ -372,25 +372,6 @@ static const struct plugin_notification notifs[] = { { } }; -static char *set_multi_string_option(struct command *cmd, - const char *arg, - bool check_only, - const char ***arr) -{ - if (!check_only) - tal_arr_expand(arr, tal_strdup(*arr, arg)); - return NULL; -} - -static bool multi_string_jsonfmt(struct command *cmd, struct json_stream *js, const char *fieldname, const char ***arr) -{ - json_array_start(js, fieldname); - for (size_t i = 0; i < tal_count(*arr); i++) - json_add_string(js, NULL, (*arr)[i]); - json_array_end(js); - return true; -} - int main(int argc, char *argv[]) { setup_locale(); @@ -436,8 +417,8 @@ int main(int argc, char *argv[]) plugin_option_multi("multiopt", "string", "Set me multiple times!", - set_multi_string_option, - multi_string_jsonfmt, + multi_string_option, + string_array_jsonfmt, &tlp->strarr), NULL); } From 549a525d902ccc8d72ee36cdf12147fcbcdff147 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 8 Apr 2026 11:10:58 +0100 Subject: [PATCH 2/2] xpay: config option xpay-user-layer This option allow user to configure xpay to always use the layers specified. Changelog-Added: xpay: config option xpay-user-layer Signed-off-by: Lagrang3 --- doc/lightningd-config.5.md | 4 +++ plugins/libplugin.c | 2 ++ plugins/xpay/xpay.c | 7 +++++ tests/test_xpay.py | 56 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index 25721771dfa1..628ec51f3365 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -563,6 +563,10 @@ command, so they invoices can also be paid onchain. Setting this makes `xpay` wait until all parts have failed/succeeded before returning. Usually this is unnecessary, as xpay will return on the first success (we have the preimage, if they don't take all the parts that's their problem) or failure (the destination could succeed another part, but it would mean it was only partially paid). The default is `false`. +* **xpay-user-layer**=*name* [plugin `xpay`] + + Specify the name of a layer `xpay` shall use always for every payment. This is specially useful when combined with `xpay-handle-pay` since the `layers` parameter is not available in the `pay` interface. This can be specified multiple times to add more layers. + * **askrene-timeout**=*SECONDS* [plugin `askrene`, *dynamic*] This option makes the `getroutes` call fail if it takes more than this many seconds. Setting it to zero is a fun way to ensure your node never makes payments. diff --git a/plugins/libplugin.c b/plugins/libplugin.c index db2e53fb730a..861e7b4aa22c 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -1731,6 +1731,8 @@ bool charp_jsonfmt(struct command *cmd, struct json_stream *js, const char *fiel bool string_array_jsonfmt(struct command *cmd, struct json_stream *js, const char *fieldname, const char ***arr) { + if (tal_count(*arr) == 0) + return false; json_array_start(js, fieldname); for (size_t i = 0; i < tal_count(*arr); i++) json_add_string(js, NULL, (*arr)[i]); diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c index e05e2df3f85e..2f59229323c3 100644 --- a/plugins/xpay/xpay.c +++ b/plugins/xpay/xpay.c @@ -43,6 +43,7 @@ struct xpay { bool slow_mode; /* Suppress calls to askrene-age */ bool dev_no_age; + const char **user_layers; }; static struct xpay *xpay_of(struct plugin *plugin) @@ -1467,6 +1468,8 @@ static struct command_result *getroutes_for(struct command *aux_cmd, /* Add user-specified layers */ for (size_t i = 0; i < tal_count(payment->layers); i++) json_add_string(req->js, NULL, payment->layers[i]); + for (size_t i = 0; i < tal_count(xpay->user_layers); i++) + json_add_string(req->js, NULL, xpay->user_layers[i]); if (payment->disable_mpp) json_add_string(req->js, NULL, "auto.no_mpp_support"); json_array_end(req->js); @@ -2534,6 +2537,7 @@ int main(int argc, char *argv[]) xpay->take_over_pay = false; xpay->slow_mode = false; xpay->dev_no_age = false; + xpay->user_layers = tal_arr(xpay, const char *, 0); plugin_main(argv, init, take(xpay), PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), @@ -2546,6 +2550,9 @@ int main(int argc, char *argv[]) plugin_option_dynamic("xpay-slow-mode", "bool", "Wait until all parts have completed before returning success or failure", bool_option, bool_jsonfmt, &xpay->slow_mode), + plugin_option_multi("xpay-user-layer", "string", + "Add a layer that will be used for every payment", + multi_string_option, string_array_jsonfmt, &xpay->user_layers), plugin_option_dev("dev-xpay-no-age", "flag", "Don't call askrene-age", flag_option, flag_jsonfmt, &xpay->dev_no_age), diff --git a/tests/test_xpay.py b/tests/test_xpay.py index a8979acb071d..7dcd502694e8 100644 --- a/tests/test_xpay.py +++ b/tests/test_xpay.py @@ -1081,3 +1081,59 @@ def mock_getblockhash(req): # Now let it catch up, and it will retry, and succeed. l1.daemon.rpcproxy.mock_rpc('getblockhash') fut.result(TIMEOUT) + + +def test_xpay_user_layers(node_factory): + l1, l2, l3, l4 = node_factory.get_nodes( + 4, opts={"may_reconnect": True, "xpay-handle-pay": True} + ) + node_factory.join_nodes([l1, l2, l3], wait_for_announce=True) + node_factory.join_nodes([l2, l4], wait_for_announce=True) + + layer = "disable-chan23" + l1.rpc.askrene_create_layer(layer=layer, persistent=True) + scid = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]["short_channel_id"] + direction = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]["direction"] + l1.rpc.askrene_update_channel( + layer=layer, enabled=False, short_channel_id_dir=f"{scid}/{direction}" + ) + + layer = "disable-chan24" + l1.rpc.askrene_create_layer(layer=layer, persistent=True) + scid = l2.rpc.listpeerchannels(l4.info["id"])["channels"][0]["short_channel_id"] + direction = l2.rpc.listpeerchannels(l4.info["id"])["channels"][0]["direction"] + l1.rpc.askrene_update_channel( + layer=layer, enabled=False, short_channel_id_dir=f"{scid}/{direction}" + ) + + # Let us load these layers as user layers in xpay. Both payments should fail + l1.stop() + l1.daemon.opts["xpay-user-layer"] = ["disable-chan23", "disable-chan24"] + l1.start() + l1.rpc.connect(l2.info["id"], "localhost", l2.port) + l1.daemon.wait_for_log(f"channeld.*: billboard: Channel ready for use") + inv3 = l3.rpc.invoice(1000, "test-xpay-user-layer", "test-xpay-user-layer")[ + "bolt11" + ] + with pytest.raises( + RpcError, + match="We could not find a usable set of paths. The destination has disabled 1 of 1 channels", + ): + l1.rpc.pay(inv3) + inv4 = l4.rpc.invoice(1000, "test-xpay-user-layer", "test-xpay-user-layer")[ + "bolt11" + ] + with pytest.raises( + RpcError, + match="We could not find a usable set of paths. The destination has disabled 1 of 1 channels", + ): + l1.rpc.pay(inv4) + + # Without those layers, the same payments should go through + l1.stop() + del l1.daemon.opts["xpay-user-layer"] + l1.start() + l1.rpc.connect(l2.info["id"], "localhost", l2.port) + l1.daemon.wait_for_log(f"channeld.*: billboard: Channel ready for use") + l1.rpc.pay(inv3) + l1.rpc.pay(inv4)