lightningd: allow --recover / recover JSON RPC to take mnemonic.
In fact, you *must* use mnemonic to successfully recover a modern node! Signed-off-by: Rusty Russell <rusty@rustcorp.com.au> Changelog-Changed: JSON-RPC: `recover` takes a 12-word mnemonic for nodes created by v25.12 or later.
This commit is contained in:
@@ -30382,7 +30382,9 @@
|
||||
"description": [
|
||||
"The **recover** RPC command wipes your node and restarts it with the `--recover` option. This is only permitted if the node is unused: no channels, no bitcoin addresses issued (you can use `check` to see if recovery is possible).",
|
||||
"",
|
||||
"*hsmsecret* is either a codex32 secret starting with \"cl1\" as returned by `lightning-hsmtool getcodexsecret`, or a raw 64 character hex string.",
|
||||
"For nodes created with v25.12 or later, *hsmsecret* MUST be the 12-word mnemonic.",
|
||||
"",
|
||||
"For earlier nodes, *hsmsecret* is either a codex32 secret starting with \"cl1\" as returned by `lightning-hsmtool getcodexsecret`, or a raw 64 character hex string.",
|
||||
"",
|
||||
"NOTE: this command only currently works with the `sqlite3` database backend."
|
||||
],
|
||||
@@ -30395,7 +30397,7 @@
|
||||
"hsmsecret": {
|
||||
"type": "string",
|
||||
"description": [
|
||||
"Either a codex32 secret starting with `cl1` as returned by `lightning-hsmtool getcodexsecret`, or a raw 64 character hex string."
|
||||
"Usually a 12-word mnemonic; but for old nodes either a codex32 secret starting with `cl1` as returned by `lightning-hsmtool getcodexsecret` or a raw 64 character hex string."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,9 +336,11 @@ connections. Default is 9736.
|
||||
|
||||
### Lightning node customization options
|
||||
|
||||
* **recover**=*hsmsecret*
|
||||
* **recover**=*mnemonic*
|
||||
|
||||
Restore the node from a 32-byte secret encoded as either a codex32 secret string or a 64-character hex string: this will fail if the `hsm_secret` file exists. Your node will start the node in offline mode, for manual recovery. The secret can be extracted from the `hsm_secret` using lightning-hsmtool(8).
|
||||
Restore the node from a mnemonic. For pre-25.12 nodes (which didn't have a mnemonic), use a 32-byte secret encoded as either a codex32 secret string or a 64-character hex string.
|
||||
|
||||
This will fail if the `hsm_secret` file exists. Your node will start the node in offline mode, for manual recovery. The secret can be extracted from the `hsm_secret` using lightning-hsmtool(8)'s `getsecret`.
|
||||
|
||||
* **alias**=*NAME*
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
"description": [
|
||||
"The **recover** RPC command wipes your node and restarts it with the `--recover` option. This is only permitted if the node is unused: no channels, no bitcoin addresses issued (you can use `check` to see if recovery is possible).",
|
||||
"",
|
||||
"*hsmsecret* is either a codex32 secret starting with \"cl1\" as returned by `lightning-hsmtool getcodexsecret`, or a raw 64 character hex string.",
|
||||
"For nodes created with v25.12 or later, *hsmsecret* MUST be the 12-word mnemonic.",
|
||||
"",
|
||||
"For earlier nodes, *hsmsecret* is either a codex32 secret starting with \"cl1\" as returned by `lightning-hsmtool getcodexsecret`, or a raw 64 character hex string.",
|
||||
"",
|
||||
"NOTE: this command only currently works with the `sqlite3` database backend."
|
||||
],
|
||||
@@ -19,7 +21,7 @@
|
||||
"hsmsecret": {
|
||||
"type": "string",
|
||||
"description": [
|
||||
"Either a codex32 secret starting with `cl1` as returned by `lightning-hsmtool getcodexsecret`, or a raw 64 character hex string."
|
||||
"Usually a 12-word mnemonic; but for old nodes either a codex32 secret starting with `cl1` as returned by `lightning-hsmtool getcodexsecret` or a raw 64 character hex string."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,17 +240,18 @@ static bool have_channels(struct lightningd *ld)
|
||||
return false;
|
||||
}
|
||||
|
||||
static struct command_result *param_codex32_or_hex(struct command *cmd,
|
||||
const char *name,
|
||||
const char *buffer,
|
||||
const jsmntok_t *tok,
|
||||
const char **hsm_secret)
|
||||
static struct command_result *param_hsm_secret(struct command *cmd,
|
||||
const char *name,
|
||||
const char *buffer,
|
||||
const jsmntok_t *tok,
|
||||
const char **hsm_secret)
|
||||
{
|
||||
char *err;
|
||||
const u8 *payload;
|
||||
/* We parse here for sanity checking, but we just hand string to --recover */
|
||||
const struct hsm_secret *hsms;
|
||||
|
||||
*hsm_secret = json_strdup(cmd, buffer, tok);
|
||||
err = hsm_secret_arg(tmpctx, *hsm_secret, &payload);
|
||||
err = hsm_secret_arg(tmpctx, *hsm_secret, &hsms);
|
||||
if (err)
|
||||
return command_fail_badparam(cmd, name, buffer, tok, err);
|
||||
return NULL;
|
||||
@@ -279,10 +280,10 @@ static struct command_result *json_recover(struct command *cmd,
|
||||
const jsmntok_t *obj UNNEEDED,
|
||||
const jsmntok_t *params)
|
||||
{
|
||||
const char *hsm_secret, *dir;
|
||||
const char *dir, *hsm_secret;
|
||||
|
||||
if (!param_check(cmd, buffer, params,
|
||||
p_req("hsmsecret", param_codex32_or_hex, &hsm_secret),
|
||||
p_req("hsmsecret", param_hsm_secret, &hsm_secret),
|
||||
NULL))
|
||||
return command_param_failed();
|
||||
|
||||
|
||||
@@ -1280,41 +1280,53 @@ static char *opt_add_api_beg(const char *arg, struct lightningd *ld)
|
||||
|
||||
char *hsm_secret_arg(const tal_t *ctx,
|
||||
const char *arg,
|
||||
const u8 **hsm_secret)
|
||||
const struct hsm_secret **hsm_secret)
|
||||
{
|
||||
char *codex32_fail;
|
||||
struct codex32 *codex32;
|
||||
struct hsm_secret *hsms = tal(tmpctx, struct hsm_secret);
|
||||
|
||||
/* We accept hex, or codex32. hex is very very very unlikely to
|
||||
* give a valid codex32, so try that first */
|
||||
codex32 = codex32_decode(tmpctx, "cl", arg, &codex32_fail);
|
||||
if (codex32) {
|
||||
*hsm_secret = tal_steal(ctx, codex32->payload);
|
||||
hsms->type = HSM_SECRET_PLAIN;
|
||||
hsms->secret_data = tal_steal(hsms, codex32->payload);
|
||||
if (codex32->threshold != 0
|
||||
|| codex32->type != CODEX32_ENCODING_SECRET) {
|
||||
return "This is only one share of codex32!";
|
||||
}
|
||||
if (tal_count(hsms->secret_data) != 32)
|
||||
return "Invalid length: must be 32 bytes";
|
||||
/* Not codex32, was it hex? */
|
||||
} else if ((hsms->secret_data = tal_hexdata(hsms, arg, strlen(arg))) != NULL) {
|
||||
hsms->type = HSM_SECRET_PLAIN;
|
||||
if (tal_count(hsms->secret_data) != 32)
|
||||
return "Invalid length: must be 32 bytes";
|
||||
/* Not hex, is is a mnemonic? */
|
||||
} else {
|
||||
/* Not codex32, was it hex? */
|
||||
*hsm_secret = tal_hexdata(ctx, arg, strlen(arg));
|
||||
if (!*hsm_secret) {
|
||||
/* It's not hex! So give codex32 error */
|
||||
return codex32_fail;
|
||||
enum hsm_secret_error err = validate_mnemonic(arg);
|
||||
if (err != HSM_SECRET_OK) {
|
||||
/* If it looks kinda like a codex32, give that error. */
|
||||
if (strstarts(arg, "cl"))
|
||||
return codex32_fail;
|
||||
else
|
||||
return "Not a valid mnemonic, hex, or codex32 string";
|
||||
}
|
||||
hsms->type = HSM_SECRET_MNEMONIC_NO_PASS;
|
||||
hsms->mnemonic = tal_strdup(hsms, arg);
|
||||
}
|
||||
|
||||
if (tal_count(*hsm_secret) != 32)
|
||||
return "Invalid length: must be 32 bytes";
|
||||
|
||||
*hsm_secret = tal_steal(ctx, hsms);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *opt_set_codex32_or_hex(const char *arg, struct lightningd *ld)
|
||||
static char *opt_set_hsm_secret(const char *arg, struct lightningd *ld)
|
||||
{
|
||||
char *err;
|
||||
const u8 *payload;
|
||||
const struct hsm_secret *hsm_secret;
|
||||
|
||||
err = hsm_secret_arg(tmpctx, arg, &payload);
|
||||
err = hsm_secret_arg(tmpctx, arg, &hsm_secret);
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
@@ -1329,10 +1341,36 @@ static char *opt_set_codex32_or_hex(const char *arg, struct lightningd *ld)
|
||||
strerror(errno));
|
||||
}
|
||||
|
||||
if (!write_all(fd, payload, tal_count(payload))) {
|
||||
switch (hsm_secret->type) {
|
||||
case HSM_SECRET_PLAIN:
|
||||
/* Legacy 32-byte format */
|
||||
if (!write_all(fd, hsm_secret->secret_data, tal_count(hsm_secret->secret_data))) {
|
||||
unlink_noerr("hsm_secret");
|
||||
return tal_fmt(tmpctx, "Writing HSM: %s",
|
||||
strerror(errno));
|
||||
}
|
||||
break;
|
||||
case HSM_SECRET_ENCRYPTED:
|
||||
case HSM_SECRET_MNEMONIC_WITH_PASS:
|
||||
return tal_fmt(tmpctx, "Recovery of encrypted/passworded secrets not supported");
|
||||
case HSM_SECRET_MNEMONIC_NO_PASS: {
|
||||
struct sha256 seed_hash;
|
||||
if (!derive_seed_hash(hsm_secret->mnemonic, NULL, &seed_hash)) {
|
||||
unlink_noerr("hsm_secret");
|
||||
return tal_fmt(tmpctx, "Deriving from mnemonic failed!");
|
||||
}
|
||||
/* Write seed hash (32 bytes) + mnemonic */
|
||||
if (!write_all(fd, &seed_hash, sizeof(seed_hash))
|
||||
|| !write_all(fd, hsm_secret->mnemonic, strlen(hsm_secret->mnemonic))) {
|
||||
unlink_noerr("hsm_secret");
|
||||
return tal_fmt(tmpctx, "Error writing to hsm_secret file: %s", strerror(errno));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HSM_SECRET_INVALID:
|
||||
/* Shouldn't happen? */
|
||||
unlink_noerr("hsm_secret");
|
||||
return tal_fmt(tmpctx, "Writing HSM: %s",
|
||||
strerror(errno));
|
||||
return tal_fmt(tmpctx, "invalid hsm secret?");
|
||||
}
|
||||
|
||||
/*~ fsync (mostly!) ensures that the file has reached the disk. */
|
||||
@@ -1414,9 +1452,9 @@ static void register_opts(struct lightningd *ld)
|
||||
&ld->wallet_dsn,
|
||||
"Location of the wallet database.");
|
||||
|
||||
opt_register_early_arg("--recover", opt_set_codex32_or_hex, NULL,
|
||||
opt_register_early_arg("--recover", opt_set_hsm_secret, NULL,
|
||||
ld,
|
||||
"Populate hsm_secret with the given codex32 secret"
|
||||
"Populate hsm_secret with the given codex32/hex/mnemonic secret"
|
||||
" and starts the node in `offline` mode.");
|
||||
|
||||
/* This affects our features, so set early. */
|
||||
@@ -1856,7 +1894,7 @@ bool is_known_opt_cb_arg(char *(*cb_arg)(const char *, void *))
|
||||
|| cb_arg == (void *)opt_set_db_upgrade
|
||||
|| cb_arg == (void *)arg_log_to_file
|
||||
|| cb_arg == (void *)opt_add_accept_htlc_tlv
|
||||
|| cb_arg == (void *)opt_set_codex32_or_hex
|
||||
|| cb_arg == (void *)opt_set_hsm_secret
|
||||
|| cb_arg == (void *)opt_subd_dev_disconnect
|
||||
|| cb_arg == (void *)opt_set_crash_timeout
|
||||
|| cb_arg == (void *)opt_add_api_beg
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "config.h"
|
||||
#include <ccan/ccan/opt/opt.h>
|
||||
|
||||
struct hsm_secret;
|
||||
struct json_stream;
|
||||
struct lightningd;
|
||||
|
||||
@@ -16,7 +17,7 @@ void handle_opts(struct lightningd *ld);
|
||||
void setup_color_and_alias(struct lightningd *ld);
|
||||
|
||||
/**
|
||||
* hsm_secret_arg - parse an hsm_secret as hex or codex32
|
||||
* hsm_secret_arg - parse an hsm_secret as hex, codex32 or mnemonic.
|
||||
* @ctx: context to allocate @hsm_secret from
|
||||
* @arg: string to parse
|
||||
* @hsm_secret: set on success.
|
||||
@@ -25,7 +26,7 @@ void setup_color_and_alias(struct lightningd *ld);
|
||||
*/
|
||||
char *hsm_secret_arg(const tal_t *ctx,
|
||||
const char *arg,
|
||||
const u8 **hsm_secret);
|
||||
const struct hsm_secret **hsm_secret);
|
||||
|
||||
enum opt_autobool {
|
||||
OPT_AUTOBOOL_FALSE = 0,
|
||||
|
||||
@@ -32,7 +32,7 @@ u32 get_feerate_floor(const struct chain_topology *topo UNNEEDED)
|
||||
/* Generated stub for hsm_secret_arg */
|
||||
char *hsm_secret_arg(const tal_t *ctx UNNEEDED,
|
||||
const char *arg UNNEEDED,
|
||||
const u8 **hsm_secret UNNEEDED)
|
||||
const struct hsm_secret **hsm_secret UNNEEDED)
|
||||
{ fprintf(stderr, "hsm_secret_arg called!\n"); abort(); }
|
||||
/* Generated stub for lightningd_deprecated_in_ok */
|
||||
bool lightningd_deprecated_in_ok(struct lightningd *ld UNNEEDED,
|
||||
|
||||
@@ -1523,50 +1523,61 @@ def test_decode(node_factory, bitcoind):
|
||||
|
||||
|
||||
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "deletes database, which is assumed sqlite3")
|
||||
def test_recover(node_factory, bitcoind):
|
||||
@pytest.mark.parametrize("old_hsmsecret", [False, True])
|
||||
def test_recover(node_factory, bitcoind, old_hsmsecret):
|
||||
"""Test the recover option
|
||||
"""
|
||||
# Start the node with --recovery with valid codex32 secret
|
||||
if old_hsmsecret:
|
||||
recoverarg = "cl10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqjdsjnzedu43ns"
|
||||
hsmsecret = bytes.fromhex("ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100")
|
||||
bad_recoverarg = "CL10LEETSLLHDMN9M42VCSAMX24ZRXGS3QQAT3LTDVAKMT73"
|
||||
else:
|
||||
recoverarg = "hockey enroll sure trip track rescue original plate abandon abandon abandon account"
|
||||
hsmsecret = bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000000") + recoverarg.encode('utf-8')
|
||||
bad_recoverarg = "hockey enroll sure trip track rescue original plate abandon abandon abandon abandon"
|
||||
|
||||
# Start the node with --recovery with valid secret
|
||||
l1 = node_factory.get_node(start=False,
|
||||
options={"recover": "cl10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqjdsjnzedu43ns"})
|
||||
options={"recover": recoverarg}, old_hsmsecret=old_hsmsecret)
|
||||
|
||||
os.unlink(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret"))
|
||||
l1.daemon.start()
|
||||
|
||||
cmd_line = ["tools/lightning-hsmtool", "getcodexsecret", os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")]
|
||||
out = subprocess.check_output(cmd_line + ["leet", "0"]).decode('utf-8')
|
||||
assert out == "cl10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqjdsjnzedu43ns\n"
|
||||
cmd_line = ["tools/lightning-hsmtool", "getsecret", os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")]
|
||||
out = subprocess.check_output(cmd_line + ["leet"]).decode('utf-8')
|
||||
assert out == recoverarg + "\n"
|
||||
|
||||
# Check bad ids.
|
||||
out = subprocess.run(cmd_line + ["lee", "0"], stderr=subprocess.PIPE, timeout=TIMEOUT)
|
||||
assert 'Invalid id: must be 4 characters' in out.stderr.decode('utf-8')
|
||||
assert out.returncode == 2
|
||||
|
||||
out = subprocess.run(cmd_line + ["Leet", "0"], stderr=subprocess.PIPE, timeout=TIMEOUT)
|
||||
assert 'Invalid id: must be lower-case' in out.stderr.decode('utf-8')
|
||||
assert out.returncode == 2
|
||||
|
||||
out = subprocess.run(cmd_line + ["💔", "0"], stderr=subprocess.PIPE, timeout=TIMEOUT)
|
||||
assert 'Invalid id: must be ASCII' in out.stderr.decode('utf-8')
|
||||
assert out.returncode == 2
|
||||
|
||||
for bad_bech32 in ['b', 'o', 'i', '1']:
|
||||
out = subprocess.run(cmd_line + [bad_bech32 + "eet", "0"], stderr=subprocess.PIPE, timeout=TIMEOUT)
|
||||
assert 'Invalid id: must be valid bech32 string' in out.stderr.decode('utf-8')
|
||||
# Check bad ids (we ignore id for modern hsm_secret)
|
||||
if old_hsmsecret:
|
||||
out = subprocess.run(cmd_line + ["lee"], stderr=subprocess.PIPE, timeout=TIMEOUT)
|
||||
assert 'Invalid id: must be 4 characters' in out.stderr.decode('utf-8')
|
||||
assert out.returncode == 2
|
||||
|
||||
out = subprocess.run(cmd_line + ["Leet"], stderr=subprocess.PIPE, timeout=TIMEOUT)
|
||||
assert 'Invalid id: must be lower-case' in out.stderr.decode('utf-8')
|
||||
assert out.returncode == 2
|
||||
|
||||
out = subprocess.run(cmd_line + ["💔"], stderr=subprocess.PIPE, timeout=TIMEOUT)
|
||||
assert 'Invalid id: must be ASCII' in out.stderr.decode('utf-8')
|
||||
assert out.returncode == 2
|
||||
|
||||
for bad_bech32 in ['b', 'o', 'i', '1']:
|
||||
out = subprocess.run(cmd_line + [bad_bech32 + "eet"], stderr=subprocess.PIPE, timeout=TIMEOUT)
|
||||
assert 'Invalid id: must be valid bech32 string' in out.stderr.decode('utf-8')
|
||||
assert out.returncode == 2
|
||||
|
||||
basedir = l1.daemon.opts.get("lightning-dir")
|
||||
with open(os.path.join(basedir, TEST_NETWORK, 'hsm_secret'), 'rb') as f:
|
||||
buff = f.read()
|
||||
|
||||
# Check the node secret
|
||||
assert buff.hex() == "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100"
|
||||
assert buff == hsmsecret
|
||||
l1.stop()
|
||||
|
||||
os.unlink(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "lightningd.sqlite3"))
|
||||
|
||||
# Node should throw error to recover flag if HSM already exists.
|
||||
l1.daemon.opts['recover'] = "cl10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqjdsjnzedu43ns"
|
||||
l1.daemon.opts['recover'] = recoverarg
|
||||
l1.daemon.start(wait_for_initialized=False, stderr_redir=True)
|
||||
|
||||
# Will exit with failure code.
|
||||
@@ -1575,12 +1586,15 @@ def test_recover(node_factory, bitcoind):
|
||||
|
||||
os.unlink(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret"))
|
||||
|
||||
l1.daemon.opts.update({"recover": "CL10LEETSLLHDMN9M42VCSAMX24ZRXGS3QQAT3LTDVAKMT73"})
|
||||
l1.daemon.opts.update({"recover": bad_recoverarg})
|
||||
l1.daemon.start(wait_for_initialized=False, stderr_redir=True)
|
||||
assert l1.daemon.wait() == 1
|
||||
assert l1.daemon.is_in_stderr(r"Invalid length: must be 32 bytes")
|
||||
if old_hsmsecret:
|
||||
assert l1.daemon.is_in_stderr(r"Invalid length: must be 32 bytes")
|
||||
else:
|
||||
assert l1.daemon.is_in_stderr(r"Not a valid mnemonic, hex, or codex32 string")
|
||||
|
||||
# Can do HSM secret in hex, too!
|
||||
# Old-style can do HSM secret in hex, too!
|
||||
l1.daemon.opts["recover"] = "6c696768746e696e672d31000000000000000000000000000000000000000000"
|
||||
l1.daemon.start()
|
||||
l1.stop()
|
||||
|
||||
@@ -766,11 +766,11 @@ int main(int argc, char *argv[])
|
||||
show_usage(argv[0]);
|
||||
make_rune(argv[2]);
|
||||
} else if(streq(method, "getsecret")) {
|
||||
if (argc < 3)
|
||||
if (argc < 3 || argc > 4)
|
||||
show_usage(argv[0]);
|
||||
print_secret(argv[2], argv[3], false);
|
||||
} else if(streq(method, "getcodexsecret")) {
|
||||
if (argc < 4)
|
||||
if (argc != 4)
|
||||
show_usage(argv[0]);
|
||||
print_secret(argv[2], argv[3], true);
|
||||
} else if(streq(method, "getemergencyrecover")) {
|
||||
|
||||
Reference in New Issue
Block a user