lightningd: setconfig command.

Currently only implemented for min-capacity-sat.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-Added: JSON-RPC: new command `setconfig` allows a limited number of configuration settings to be changed without restart.
This commit is contained in:
Rusty Russell 2023-06-06 10:08:52 +09:30
parent ecc030f12d
commit 6546be9757
8 changed files with 387 additions and 9 deletions

View file

@ -89,6 +89,7 @@ MANPAGES := doc/lightning-cli.1 \
doc/lightning-sendonionmessage.7 \ doc/lightning-sendonionmessage.7 \
doc/lightning-sendpay.7 \ doc/lightning-sendpay.7 \
doc/lightning-setchannel.7 \ doc/lightning-setchannel.7 \
doc/lightning-setconfig.7 \
doc/lightning-setpsbtversion.7 \ doc/lightning-setpsbtversion.7 \
doc/lightning-sendcustommsg.7 \ doc/lightning-sendcustommsg.7 \
doc/lightning-signinvoice.7 \ doc/lightning-signinvoice.7 \

View file

@ -122,6 +122,7 @@ Core Lightning Documentation
lightning-sendpay <lightning-sendpay.7.md> lightning-sendpay <lightning-sendpay.7.md>
lightning-sendpsbt <lightning-sendpsbt.7.md> lightning-sendpsbt <lightning-sendpsbt.7.md>
lightning-setchannel <lightning-setchannel.7.md> lightning-setchannel <lightning-setchannel.7.md>
lightning-setconfig <lightning-setconfig.7.md>
lightning-setpsbtversion <lightning-setpsbtversion.7.md> lightning-setpsbtversion <lightning-setpsbtversion.7.md>
lightning-signinvoice <lightning-signinvoice.7.md> lightning-signinvoice <lightning-signinvoice.7.md>
lightning-signmessage <lightning-signmessage.7.md> lightning-signmessage <lightning-signmessage.7.md>

View file

@ -0,0 +1,59 @@
lightning-setconfig -- Dynamically change some config options
=============================================================
SYNOPSIS
--------
**setconfig** *config* [*val*]
DESCRIPTION
-----------
The **setconfig** RPC command allows you set the (dynamic) configuration option named by `config`: options which take a value (as separate from simple flag options) also need a `val` parameter.
This new value will *also* be written at the end of the config file, for persistence across restarts.
You can see what options are dynamically adjustable using lightning-listconfigs(7). Note that you can also adjust existing options for stopped plugins; they will have an effect when the plugin is restarted.
RETURN VALUE
------------
[comment]: # (GENERATE-FROM-SCHEMA-START)
On success, an object containing **config** is returned. It is an object containing:
- **config** (string): name of the config variable which was set
- **source** (string): source of configuration setting (`file`:`linenum`)
- **dynamic** (boolean): whether this option is settable via setconfig (always *true*)
- **plugin** (string, optional): the plugin this configuration setting is for
- **set** (boolean, optional): for simple flag options
- **value\_str** (string, optional): for string options
- **value\_msat** (msat, optional): for msat options
- **value\_int** (integer, optional): for integer options
- **value\_bool** (boolean, optional): for boolean options
[comment]: # (GENERATE-FROM-SCHEMA-END)
ERRORS
------
The following error codes may occur:
- -32602: JSONRPC2\_INVALID\_PARAMS, i.e. the parameter is not dynamic, or the val was invalid.
AUTHOR
------
Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible for this
feature.
SEE ALSO
--------
lightningd-config(5), lightning-listconfigs(7)
RESOURCES
---------
Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:d61e4e6eea7b8c214644334ee194b273aef2a8a26465adfcd685be0d70653966)

View file

@ -14,6 +14,8 @@ file (default: **$HOME/.lightning/config**) then a network-specific
configuration file (default: **$HOME/.lightning/testnet/config**). This can configuration file (default: **$HOME/.lightning/testnet/config**). This can
be changed: see *--conf* and *--lightning-dir*. be changed: see *--conf* and *--lightning-dir*.
Note that some configuration options, marked *dynamic*m can be changed at runtime: see lightning-setconfig(7).
General configuration files are processed first, then network-specific General configuration files are processed first, then network-specific
ones, then command line options: later options override earlier ones ones, then command line options: later options override earlier ones
except *addr* options and *log-level* with subsystems, which except *addr* options and *log-level* with subsystems, which
@ -316,7 +318,7 @@ millionths, so 10000 is 1%, 1000 is 0.1%. Changing this value will only
affect new channels and not existing ones. If you want to change fees affect new channels and not existing ones. If you want to change fees
for existing channels, use the RPC call lightning-setchannel(7). for existing channels, use the RPC call lightning-setchannel(7).
* **min-capacity-sat**=*SATOSHI* * **min-capacity-sat**=*SATOSHI* [*dynamic*]
Default: 10000. This value defines the minimal effective channel Default: 10000. This value defines the minimal effective channel
capacity in satoshi to accept for channel opening requests. This will capacity in satoshi to accept for channel opening requests. This will

View file

@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [
"config"
],
"added": "v23.08",
"properties": {
"config": {
"type": "string"
},
"val": {}
}
}

View file

@ -0,0 +1,62 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"added": "v23.08",
"required": [
"config"
],
"properties": {
"config": {
"type": "object",
"description": "config settings after completion",
"additionalProperties": false,
"required": [
"config",
"source",
"dynamic"
],
"properties": {
"config": {
"type": "string",
"description": "name of the config variable which was set"
},
"source": {
"type": "string",
"description": "source of configuration setting (`file`:`linenum`)"
},
"plugin": {
"type": "string",
"description": "the plugin this configuration setting is for"
},
"dynamic": {
"type": "boolean",
"enum": [
true
],
"description": "whether this option is settable via setconfig"
},
"set": {
"type": "boolean",
"description": "for simple flag options"
},
"value_str": {
"type": "string",
"description": "for string options"
},
"value_msat": {
"type": "msat",
"description": "for msat options"
},
"value_int": {
"type": "integer",
"description": "for integer options"
},
"value_bool": {
"type": "boolean",
"description": "for boolean options"
}
}
}
}
}

View file

@ -2,15 +2,20 @@
#include <ccan/err/err.h> #include <ccan/err/err.h>
#include <ccan/opt/opt.h> #include <ccan/opt/opt.h>
#include <ccan/opt/private.h> #include <ccan/opt/private.h>
#include <ccan/tal/grab_file/grab_file.h>
#include <ccan/tal/path/path.h>
#include <ccan/tal/str/str.h> #include <ccan/tal/str/str.h>
#include <common/configdir.h> #include <common/configdir.h>
#include <common/configvar.h> #include <common/configvar.h>
#include <common/json_command.h> #include <common/json_command.h>
#include <common/json_param.h> #include <common/json_param.h>
#include <common/version.h> #include <common/version.h>
#include <errno.h>
#include <fcntl.h>
#include <lightningd/jsonrpc.h> #include <lightningd/jsonrpc.h>
#include <lightningd/options.h> #include <lightningd/options.h>
#include <lightningd/plugin.h> #include <lightningd/plugin.h>
#include <unistd.h>
static void json_add_source(struct json_stream *result, static void json_add_source(struct json_stream *result,
const char *fieldname, const char *fieldname,
@ -113,10 +118,14 @@ static void json_add_configval(struct json_stream *result,
} }
/* Config vars can have multiple names ("--large-channels|--wumbo"), but first /* Config vars can have multiple names ("--large-channels|--wumbo"), but first
* is preferred */ * is preferred.
* wrap_object means we wrap json in an object of that name, otherwise outputs
* raw fields.
*/
static void json_add_config(struct lightningd *ld, static void json_add_config(struct lightningd *ld,
struct json_stream *response, struct json_stream *response,
bool always_include, bool always_include,
bool wrap_object,
const struct opt_table *ot, const struct opt_table *ot,
const char **names) const char **names)
{ {
@ -139,18 +148,21 @@ static void json_add_config(struct lightningd *ld,
return; return;
if (ot->type & OPT_NOARG) { if (ot->type & OPT_NOARG) {
if (wrap_object)
json_object_start(response, names[0]); json_object_start(response, names[0]);
json_add_bool(response, "set", cv != NULL); json_add_bool(response, "set", cv != NULL);
json_add_source(response, "source", cv); json_add_source(response, "source", cv);
json_add_config_plugin(response, ld->plugins, "plugin", ot); json_add_config_plugin(response, ld->plugins, "plugin", ot);
if (ot->type & OPT_DYNAMIC) if (ot->type & OPT_DYNAMIC)
json_add_bool(response, "dynamic", true); json_add_bool(response, "dynamic", true);
if (wrap_object)
json_object_end(response); json_object_end(response);
return; return;
} }
assert(ot->type & OPT_HASARG); assert(ot->type & OPT_HASARG);
if (ot->type & OPT_MULTI) { if (ot->type & OPT_MULTI) {
if (wrap_object)
json_object_start(response, names[0]); json_object_start(response, names[0]);
json_array_start(response, configval_fieldname(ot)); json_array_start(response, configval_fieldname(ot));
while (cv) { while (cv) {
@ -171,6 +183,7 @@ static void json_add_config(struct lightningd *ld,
json_add_config_plugin(response, ld->plugins, "plugin", ot); json_add_config_plugin(response, ld->plugins, "plugin", ot);
if (ot->type & OPT_DYNAMIC) if (ot->type & OPT_DYNAMIC)
json_add_bool(response, "dynamic", true); json_add_bool(response, "dynamic", true);
if (wrap_object)
json_object_end(response); json_object_end(response);
return; return;
} }
@ -180,12 +193,14 @@ static void json_add_config(struct lightningd *ld,
if (!val) if (!val)
return; return;
if (wrap_object)
json_object_start(response, names[0]); json_object_start(response, names[0]);
json_add_configval(response, configval_fieldname(ot), ot, val); json_add_configval(response, configval_fieldname(ot), ot, val);
json_add_source(response, "source", cv); json_add_source(response, "source", cv);
json_add_config_plugin(response, ld->plugins, "plugin", ot); json_add_config_plugin(response, ld->plugins, "plugin", ot);
if (ot->type & OPT_DYNAMIC) if (ot->type & OPT_DYNAMIC)
json_add_bool(response, "dynamic", true); json_add_bool(response, "dynamic", true);
if (wrap_object)
json_object_end(response); json_object_end(response);
} }
@ -292,7 +307,7 @@ modern:
} }
/* We don't usually print dev or deprecated options, unless /* We don't usually print dev or deprecated options, unless
* they explicitly ask, or they're set. */ * they explicitly ask, or they're set. */
json_add_config(cmd->ld, response, config != NULL, json_add_config(cmd->ld, response, config != NULL, true,
&opt_table[i], names); &opt_table[i], names);
} }
json_object_end(response); json_object_end(response);
@ -311,3 +326,172 @@ static const struct json_command listconfigs_command = {
"With [config], object only has that field" "With [config], object only has that field"
}; };
AUTODATA(json_command, &listconfigs_command); AUTODATA(json_command, &listconfigs_command);
static struct command_result *param_opt_dynamic_config(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
const struct opt_table **config)
{
struct command_result *ret;
ret = param_opt_config(cmd, name, buffer, tok, config);
if (ret)
return ret;
if (!((*config)->type & OPT_DYNAMIC))
return command_fail_badparam(cmd, name, buffer, tok,
"Not a dynamic config option");
return NULL;
}
/* FIXME: put in ccan/mem! */
static size_t memcount(const void *mem, size_t len, char c)
{
size_t count = 0;
for (size_t i = 0; i < len; i++) {
if (((char *)mem)[i] == c)
count++;
}
return count;
}
static void configvar_append_file(struct lightningd *ld,
const char *fname,
enum configvar_src src,
const char *confline,
bool must_exist)
{
int fd;
size_t num_lines;
const char *buffer, *insert;
bool needs_term;
struct configvar *cv;
time_t now = time(NULL);
fd = open(fname, O_RDWR|O_APPEND);
if (fd < 0) {
if (errno != ENOENT || must_exist)
fatal("Could not write to config %s: %s",
fname, strerror(errno));
fd = open(fname, O_RDWR|O_APPEND|O_CREAT, 0644);
if (fd < 0)
fatal("Could not create config file %s: %s",
fname, strerror(errno));
}
/* Note: always nul terminates */
buffer = grab_fd(tmpctx, fd);
if (!buffer)
fatal("Error reading %s: %s", fname, strerror(errno));
num_lines = memcount(buffer, tal_bytelen(buffer)-1, '\n');
/* If there's a last character and it's not \n, add one */
if (tal_bytelen(buffer) == 1)
needs_term = false;
else
needs_term = (buffer[tal_bytelen(buffer)-2] != '\n');
/* Note: ctime() contains a \n! */
insert = tal_fmt(tmpctx, "%s# Inserted by setconfig %s%s\n",
needs_term ? "\n": "",
ctime(&now), confline);
if (write(fd, insert, strlen(insert)) != strlen(insert))
fatal("Could not write to config file %s: %s",
fname, strerror(errno));
cv = configvar_new(ld->configvars, src, fname, num_lines+2, confline);
configvar_unparsed(cv);
log_info(ld->log, "setconfig: %s %s (updated %s:%u)",
cv->optvar, cv->optarg ? cv->optarg : "SET",
cv->file, cv->linenum);
tal_arr_expand(&ld->configvars, cv);
configvar_finalize_overrides(ld->configvars);
}
static void configvar_save(struct lightningd *ld, const char *confline)
{
/* If they used --conf then append to that */
if (ld->config_filename)
configvar_append_file(ld,
ld->config_filename,
CONFIGVAR_EXPLICIT_CONF,
confline, true);
else {
const char *fname;
fname = path_join(tmpctx, ld->config_netdir, "config");
configvar_append_file(ld,
fname,
CONFIGVAR_NETWORK_CONF,
confline,
false);
}
}
static struct command_result *json_setconfig(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
const jsmntok_t *params)
{
struct json_stream *response;
const struct opt_table *ot;
const char *val, **names, *confline;
unsigned int len;
char *err;
if (!param(cmd, buffer, params,
p_req("config", param_opt_dynamic_config, &ot),
p_opt("val", param_string, &val),
NULL))
return command_param_failed();
/* We don't handle DYNAMIC MULTI, at least yet! */
assert(!(ot->type & OPT_MULTI));
names = tal_arr(tmpctx, const char *, 1);
/* This includes leading -! */
names[0] = first_name(ot->names, &len) + 1;
if (ot->type & OPT_NOARG) {
if (val)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"%s does not take a value",
ot->names + 2);
confline = tal_strdup(tmpctx, names[0]);
err = ot->cb(ot->u.arg);
} else {
assert(ot->type & OPT_HASARG);
if (!val)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"%s requires a value",
ot->names + 2);
confline = tal_fmt(tmpctx, "%s=%s", names[0], val);
err = ot->cb_arg(val, ot->u.arg);
}
if (err) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Error setting %s: %s", ot->names + 2, err);
}
configvar_save(cmd->ld, confline);
response = json_stream_success(cmd);
json_object_start(response, "config");
json_add_string(response, "config", names[0]);
json_add_config(cmd->ld, response, true, false, ot, names);
json_object_end(response);
return command_success(cmd, response);
}
static const struct json_command setconfig_command = {
"setconfig",
"utility",
json_setconfig,
"Set a dynamically-adjustable config."
};
AUTODATA(json_command, &setconfig_command);

View file

@ -3396,3 +3396,57 @@ def test_fast_shutdown(node_factory):
except ConnectionRefusedError: except ConnectionRefusedError:
continue continue
break break
def test_setconfig(node_factory):
l1, l2 = node_factory.line_graph(2, fundchannel=False)
configfile = os.path.join(l2.daemon.opts.get("lightning-dir"), TEST_NETWORK, 'config')
assert (l2.rpc.listconfigs('min-capacity-sat')['configs']
== {'min-capacity-sat':
{'source': 'default',
'value_int': 10000,
'dynamic': True}})
with pytest.raises(RpcError, match='requires a value'):
l2.rpc.setconfig('min-capacity-sat')
with pytest.raises(RpcError, match='requires a value'):
l2.rpc.setconfig(config='min-capacity-sat')
with pytest.raises(RpcError, match='is not a number'):
l2.rpc.setconfig(config='min-capacity-sat', val="abcd")
ret = l2.rpc.setconfig(config='min-capacity-sat', val=500000)
assert ret == {'config':
{'config': 'min-capacity-sat',
'source': '{}:2'.format(configfile),
'value_int': 500000,
'dynamic': True}}
with open(configfile, 'r') as f:
lines = f.read().splitlines()
assert lines[0].startswith('# Inserted by setconfig ')
assert lines[1] == 'min-capacity-sat=500000'
assert len(lines) == 2
# Now we need to meet minumum
with pytest.raises(RpcError, match='which is below 500000sat'):
l1.fundchannel(l2, 400000)
l1.fundchannel(l2, 10**6)
l1.rpc.close(l2.info['id'])
# It's persistent!
l2.restart()
assert (l2.rpc.listconfigs('min-capacity-sat')['configs']
== {'min-capacity-sat':
{'source': '{}:2'.format(configfile),
'value_int': 500000,
'dynamic': True}})
# Still need to meet minumum
l1.connect(l2)
with pytest.raises(RpcError, match='which is below 500000sat'):
l1.fundchannel(l2, 400000)