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:
Rusty Russell
2026-01-13 13:19:19 +10:30
parent 4f5e5aad18
commit 779a478437
9 changed files with 126 additions and 66 deletions

View File

@@ -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();

View File

@@ -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

View File

@@ -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,

View File

@@ -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,