libplugin: support multi options.
What changed, and why it matters
This commit adds a new feature to Core Lightning's plugin helper library: plugin options can now be declared as 'multi', meaning they can be specified multiple times and the values accumulate into a list. It is a straightforward feature addition with no security relevance visible in the code or commit message.
No security action required; review as normal feature code.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch extends libplugin’s option registration macros and manifest generation to support a new boolean ‘multi’ flag. When an option is marked multi, the init handler iterates over the JSON array of supplied values and calls the option’s setter for each element. Existing non-multi behavior is unchanged. The change includes test coverage in tests/test_plugin.py and tests/plugins/test_libplugin.c, plus a Makefile dependency fix for plugins/xpay.
Changed components
plugins/libplugin.cplugins/libplugin.hplugins/xpay/Makefiletests/plugins/test_libplugin.ctests/test_plugin.pyInspect captured patch +67 / −11
diff --git a/plugins/libplugin.c b/plugins/libplugin.c
index ed888bdb..c2ad2a2a 100644
--- a/plugins/libplugin.c
+++ b/plugins/libplugin.c
@@ -56,6 +56,8 @@ struct plugin_option {
const char *depr_start, *depr_end;
/* If true, allow setting after plugin has initialized */
bool dynamic;
+ /* If true, allow multiple settings. */
+ bool multi;
};
struct plugin {
@@ -1272,6 +1274,7 @@ handle_getmanifest(struct command *getmanifest_cmd,
json_add_string(params, "description", p->opts[i].description);
json_add_deprecated(params, "deprecated", p->opts[i].depr_start, p->opts[i].depr_end);
json_add_bool(params, "dynamic", p->opts[i].dynamic);
+ json_add_bool(params, "multi", p->opts[i].multi);
if (p->opts[i].jsonfmt)
p->opts[i].jsonfmt(p, params, "default", p->opts[i].arg);
json_object_end(params);
@@ -1567,9 +1570,19 @@ static struct command_result *handle_init(struct command *cmd,
if (!popt)
plugin_err(p, "lightningd specified unknown option '%s'?", name);
- problem = popt->handle(p, json_strdup(tmpctx, buf, t+1), false, popt->arg);
- if (problem)
- plugin_err(p, "option '%s': %s", popt->name, problem);
+ if (popt->multi) {
+ size_t j;
+ const jsmntok_t *opt;
+ json_for_each_arr(j, opt, t+1) {
+ problem = popt->handle(p, json_strdup(tmpctx, buf, opt), false, popt->arg);
+ if (problem)
+ plugin_err(p, "option '%s': %s", popt->name, problem);
+ }
+ } else {
+ problem = popt->handle(p, json_strdup(tmpctx, buf, t+1), false, popt->arg);
+ if (problem)
+ plugin_err(p, "option '%s': %s", popt->name, problem);
+ }
}
if (p->init) {
@@ -2391,6 +2404,7 @@ static struct plugin *new_plugin(const tal_t *ctx,
o.depr_start = va_arg(ap, const char *);
o.depr_end = va_arg(ap, const char *);
o.dynamic = va_arg(ap, int); /* bool gets promoted! */
+ o.multi = va_arg(ap, int); /* bool gets promoted! */
tal_arr_expand(&p->opts, o);
}
diff --git a/plugins/libplugin.h b/plugins/libplugin.h
index 28772184..5145041f 100644
--- a/plugins/libplugin.h
+++ b/plugins/libplugin.h
@@ -593,7 +593,7 @@ void *plugin_get_data_(struct plugin *plugin);
#define plugin_get_data(plugin, type) ((type *)(plugin_get_data_(plugin)))
/* Macro to define arguments */
-#define plugin_option_(name, type, description, set, jsonfmt, arg, dev_only, depr_start, depr_end, dynamic) \
+#define plugin_option_(name, type, description, set, jsonfmt, arg, dev_only, depr_start, depr_end, dynamic, multi) \
(name), \
(type), \
(description), \
@@ -610,23 +610,27 @@ void *plugin_get_data_(struct plugin *plugin);
(dev_only), \
(depr_start), \
(depr_end), \
- (dynamic)
+ (dynamic), \
+ (multi)
/* jsonfmt can be NULL, but then default won't be printed */
#define plugin_option(name, type, description, set, jsonfmt, arg) \
- plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, false)
+ plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, false, false)
#define plugin_option_dev(name, type, description, set, jsonfmt, arg) \
- plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), true, NULL, NULL, false)
+ plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), true, NULL, NULL, false, false)
#define plugin_option_dev_dynamic(name, type, description, set, jsonfmt, arg) \
- plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), true, NULL, NULL, true)
+ plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), true, NULL, NULL, true, false)
#define plugin_option_dynamic(name, type, description, set, jsonfmt, arg) \
- plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, true)
+ plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, true, false)
#define plugin_option_deprecated(name, type, description, depr_start, depr_end, set, jsonfmt, arg) \
- plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, (depr_start), (depr_end), false)
+ plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, (depr_start), (depr_end), false, false)
+
+#define plugin_option_multi(name, type, description, set, jsonfmt, arg) \
+ plugin_option_((name), (type), (description), (set), (jsonfmt), (arg), false, NULL, NULL, false, true)
/* Standard helpers */
char *u64_option(struct plugin *plugin, const char *arg, bool check_only, u64 *i);
diff --git a/plugins/xpay/Makefile b/plugins/xpay/Makefile
index 99441f18..922d3a17 100644
--- a/plugins/xpay/Makefile
+++ b/plugins/xpay/Makefile
@@ -10,6 +10,6 @@ ALL_C_SOURCES += $(PLUGIN_XPAY_SRC)
ALL_C_HEADERS += $(PLUGIN_XPAY_HDRS)
# Make all plugins depend on all plugin headers, for simplicity (and this file)
-$(PLUGIN_XPAY_OBJS): $(PLUGIN_XPAY_HDRS) plugins/xpay/Makefile
+$(PLUGIN_XPAY_OBJS): $(PLUGIN_XPAY_HDRS) $(PLUGIN_LIB_HEADER) plugins/xpay/Makefile
plugins/cln-xpay: $(PLUGIN_XPAY_OBJS) $(PLUGIN_LIB_OBJS) libcommon.a
diff --git a/tests/plugins/test_libplugin.c b/tests/plugins/test_libplugin.c
index c51ab55b..c4f9a459 100644
--- a/tests/plugins/test_libplugin.c
+++ b/tests/plugins/test_libplugin.c
@@ -12,6 +12,7 @@ struct test_libplugin {
bool self_disable;
bool dont_shutdown;
u32 dynamic_opt;
+ const char **strarr;
};
static struct test_libplugin *get_test_libplugin(struct plugin *plugin)
@@ -287,6 +288,13 @@ static const char *init(struct command *init_cmd,
plugin_log(p, LOG_DBG, "somearg = %s", tlp->somearg);
tlp->somearg = tal_free(tlp->somearg);
+ for (size_t i = 0; i < tal_count(tlp->strarr); i++) {
+ plugin_log(p, LOG_DBG, "multiopt#%zu = %s",
+ i, tlp->strarr[i]);
+ }
+ if (tlp->somearg)
+ plugin_log(p, LOG_DBG, "somearg = %s", tlp->somearg);
+
if (tlp->self_disable)
return "Disabled via selfdisable option";
@@ -361,6 +369,25 @@ static const struct plugin_notification notifs[] = { {
}
};
+static char *set_multi_string_option(struct plugin *plugin,
+ 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 plugin *plugin, 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();
@@ -372,6 +399,7 @@ int main(int argc, char *argv[])
tlp->self_disable = false;
tlp->dont_shutdown = false;
tlp->dynamic_opt = 7;
+ tlp->strarr = tal_arr(tlp, const char *, 0);
plugin_main(argv, init, take(tlp), PLUGIN_RESTARTABLE, true, NULL,
commands, ARRAY_SIZE(commands),
@@ -402,5 +430,11 @@ int main(int argc, char *argv[])
"Set me!",
set_dynamic, u32_jsonfmt,
&tlp->dynamic_opt),
+ plugin_option_multi("multiopt",
+ "string",
+ "Set me multiple times!",
+ set_multi_string_option,
+ multi_string_jsonfmt,
+ &tlp->strarr),
NULL);
}
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index e1949529..9644d416 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -1742,6 +1742,7 @@ def test_libplugin(node_factory):
assert l1.daemon.is_in_stderr(r"somearg-deprecated=test_opt: deprecated option")
del l1.daemon.opts["somearg-deprecated"]
+ l1.daemon.opts["multiopt"] = ['hello', 'world']
l1.start()
# Test that check works as expected.
@@ -1755,6 +1756,9 @@ def test_libplugin(node_factory):
# This works
assert l1.rpc.check('checkthis', key=["test_libplugin", "name"]) == {'command_to_check': 'checkthis'}
+ assert l1.daemon.is_in_log('plugin-test_libplugin: multiopt#0 = hello')
+ assert l1.daemon.is_in_log('plugin-test_libplugin: multiopt#1 = world')
+
def test_libplugin_deprecated(node_factory):
"""Sanity checks for plugins made with libplugin using deprecated args"""
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.