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-sendpay.7 \
doc/lightning-setchannel.7 \
doc/lightning-setconfig.7 \
doc/lightning-setpsbtversion.7 \
doc/lightning-sendcustommsg.7 \
doc/lightning-signinvoice.7 \

View file

@ -122,6 +122,7 @@ Core Lightning Documentation
lightning-sendpay <lightning-sendpay.7.md>
lightning-sendpsbt <lightning-sendpsbt.7.md>
lightning-setchannel <lightning-setchannel.7.md>
lightning-setconfig <lightning-setconfig.7.md>
lightning-setpsbtversion <lightning-setpsbtversion.7.md>
lightning-signinvoice <lightning-signinvoice.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
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
ones, then command line options: later options override earlier ones
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
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
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/opt/opt.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 <common/configdir.h>
#include <common/configvar.h>
#include <common/json_command.h>
#include <common/json_param.h>
#include <common/version.h>
#include <errno.h>
#include <fcntl.h>
#include <lightningd/jsonrpc.h>
#include <lightningd/options.h>
#include <lightningd/plugin.h>
#include <unistd.h>
static void json_add_source(struct json_stream *result,
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
* 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,
struct json_stream *response,
bool always_include,
bool wrap_object,
const struct opt_table *ot,
const char **names)
{
@ -139,19 +148,22 @@ static void json_add_config(struct lightningd *ld,
return;
if (ot->type & OPT_NOARG) {
json_object_start(response, names[0]);
if (wrap_object)
json_object_start(response, names[0]);
json_add_bool(response, "set", cv != NULL);
json_add_source(response, "source", cv);
json_add_config_plugin(response, ld->plugins, "plugin", ot);
if (ot->type & OPT_DYNAMIC)
json_add_bool(response, "dynamic", true);
json_object_end(response);
if (wrap_object)
json_object_end(response);
return;
}
assert(ot->type & OPT_HASARG);
if (ot->type & OPT_MULTI) {
json_object_start(response, names[0]);
if (wrap_object)
json_object_start(response, names[0]);
json_array_start(response, configval_fieldname(ot));
while (cv) {
val = get_opt_val(ot, buf, cv);
@ -171,7 +183,8 @@ static void json_add_config(struct lightningd *ld,
json_add_config_plugin(response, ld->plugins, "plugin", ot);
if (ot->type & OPT_DYNAMIC)
json_add_bool(response, "dynamic", true);
json_object_end(response);
if (wrap_object)
json_object_end(response);
return;
}
@ -180,13 +193,15 @@ static void json_add_config(struct lightningd *ld,
if (!val)
return;
json_object_start(response, names[0]);
if (wrap_object)
json_object_start(response, names[0]);
json_add_configval(response, configval_fieldname(ot), ot, val);
json_add_source(response, "source", cv);
json_add_config_plugin(response, ld->plugins, "plugin", ot);
if (ot->type & OPT_DYNAMIC)
json_add_bool(response, "dynamic", true);
json_object_end(response);
if (wrap_object)
json_object_end(response);
}
static struct command_result *param_opt_config(struct command *cmd,
@ -292,7 +307,7 @@ modern:
}
/* We don't usually print dev or deprecated options, unless
* 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);
}
json_object_end(response);
@ -311,3 +326,172 @@ static const struct json_command listconfigs_command = {
"With [config], object only has that field"
};
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:
continue
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)