From f975bb37d475de3a72fd8348a2065ec760572c58 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 13 Jan 2026 13:19:15 +1030 Subject: [PATCH] lightning-hsmtool: support extracting the mnemonic from hsm_secret. We cannot use the codex32 or raw hex for recovery of 25.12 nodes, since they will then use the incorrect derivation for all paths, and be unable to spend (or even find!) their funds. So implement `getsecret` to replace `getcodexsecret`. Signed-off-by: Rusty Russell Changelog-Changed: `lightning-hsmtool`: `getsecret` replaces `getcodexsecret` for modern nodes (gives mnemonic). Changelog-Deprecated: `lightning-hsmtool`: `getcodexsecret`. Use `getsecret`. --- doc/beginners-guide/backup.md | 7 ++-- doc/developers-guide/deprecated-features.md | 2 +- doc/lightning-hsmtool.8.md | 5 ++- tests/test_wallet.py | 2 +- tools/lightning-hsmtool.c | 42 ++++++++++++++++----- 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/doc/beginners-guide/backup.md b/doc/beginners-guide/backup.md index 9811b9d26..09a24c5a8 100644 --- a/doc/beginners-guide/backup.md +++ b/doc/beginners-guide/backup.md @@ -63,11 +63,12 @@ chmod 0400 hsm_secret ``` -#### Codex32 Format +#### Readable Format -Run `tools/lightning-hsmtool getcodexsecret ` to get the `hsm_secret` in codex32 format. +Run `tools/lightning-hsmtool getsecret ` to get the `hsm_secret` mnemonic (12 words). For older +nodes, you will get a codex32 string instead, and must supply a four-letter id to attach to it, like so: -Example `tools/lightning-hsmtool getcodexsecret ~/.lightning/bitcoin/hsm_secret adt0`. +Example `tools/lightning-hsmtool getsecret ~/.lightning/bitcoin/hsm_secret adt0`. `hsm/secret/path` in the above command is `$LIGHTNINGDIR/hsm_secret`, and `id` is any 4 character string used to identify this secret. It **cannot** contain `i`, `o`, or `b`, but **can** contain all digits except `1`. diff --git a/doc/developers-guide/deprecated-features.md b/doc/developers-guide/deprecated-features.md index 5da91a552..f8cd848a6 100644 --- a/doc/developers-guide/deprecated-features.md +++ b/doc/developers-guide/deprecated-features.md @@ -26,7 +26,7 @@ privacy: | encrypted_hsm | Config | v25.12 | v26.12 | `hsm-passphrase` is a name which also makes sense for modern hsm_secrets which use BIP 39 | | newaddr.addresstype.defaultbech32 | Parameter | v25.12 | v26.12 | Use `p2tr` in the response (present since v23.08 if `addresstype` is `p2tr`, and always present since v24.12). | | channel_state_changed.null_message | Notification Field | v25.12 | v26.12 | In channel_state_changed notification, `message` will be missing instead of `null` | - +| hsmtool.getcodexsecret | Command | v25.12.1 | v26.12 | Doesn't work on nodes using mnemonic secrets (v25.12 or later). Use `getsecret` instead. | Inevitably there are features which need to change: either to be generalized, or removed when they can no longer be supported. Types of deprecation: diff --git a/doc/lightning-hsmtool.8.md b/doc/lightning-hsmtool.8.md index e198d6cfb..a9f6610ff 100644 --- a/doc/lightning-hsmtool.8.md +++ b/doc/lightning-hsmtool.8.md @@ -88,8 +88,11 @@ the last parameter. By default, mainnet-encoded keys are generated. This produces the same results as lightning-commando-rune(7) on a fresh node. You will still need to create a rune once the node starts, if you want commando to work (as it is only activated once it has generated one). +**getsecret** *hsm\_secret\_path* [*id*] + Extract the secret from the `hsm_secret` file and print it out, for use with `--recover`. This is usually a 12-word mnemonic, but for pre-v25.12 nodes, a 4-character `id` is needed to generate a BIP-93 formatted HSM secret (e.g. `ad00`): it cannot contain `i`, `o`, or `b`, but can contain digits except `1`. + **getcodexsecret** *hsm\_secret\_path* *id* - Print out the BIP-93 formatted HSM secret, for use with `--recover`. The `id` is any 4 character string you can use to identify this secret (e.g. `ad00`): it cannot contain `i`, `o`, or `b`, but can contain digits except `1`. + Deprecated alias for getsecret: only works on older (pre-v25.12) nodes. **getemergencyrecover** *emergency.recover\_path* Print out the bech32 encoded emergency.recover file. diff --git a/tests/test_wallet.py b/tests/test_wallet.py index a9a86f0d0..40282125c 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1649,7 +1649,7 @@ def test_hsmtool_all_commands_work_with_mnemonic_formats(node_factory): # Test various commands work with mnemonic format test_commands = [ (["getnodeid", hsm_path], "03653e90c1ce4660fd8505dd6d643356e93cfe202af109d382787639dd5890e87d"), - (["getcodexsecret", hsm_path, "test"], "cl10testst6cqh0wu7p5ssjyf4z4ez42ks9jlt3zneju9uuypr2hddak6tlqsghuxusm6m6azq"), + (["getsecret", hsm_path], "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"), (["makerune", hsm_path], "6VkrWMI2hm2a2UTkg-EyUrrBJN0RcuPB80I1pCVkTD89MA=="), (["dumponchaindescriptors", hsm_path], "wpkh(xpub661MyMwAqRbcG9kjo3mdWQuSDbtdJzsd3K2mvifyeUMF3GhLcBAfELqjuxCvxUkYqQVe6rJ9SzmpipoUedb5MD79MJaLL8RME2A3J3Fw6Zd/0/0/*)#2jtshmk0\nsh(wpkh(xpub661MyMwAqRbcG9kjo3mdWQuSDbtdJzsd3K2mvifyeUMF3GhLcBAfELqjuxCvxUkYqQVe6rJ9SzmpipoUedb5MD79MJaLL8RME2A3J3Fw6Zd/0/0/*))#u6am4was\ntr(xpub661MyMwAqRbcG9kjo3mdWQuSDbtdJzsd3K2mvifyeUMF3GhLcBAfELqjuxCvxUkYqQVe6rJ9SzmpipoUedb5MD79MJaLL8RME2A3J3Fw6Zd/0/0/*)#v9hf4756"), ] diff --git a/tools/lightning-hsmtool.c b/tools/lightning-hsmtool.c index a3f487a10..ca5bf542c 100644 --- a/tools/lightning-hsmtool.c +++ b/tools/lightning-hsmtool.c @@ -56,7 +56,7 @@ static void show_usage(const char *progname) printf(" - checkhsm \n"); printf(" - dumponchaindescriptors [--show-secrets] [network]\n"); printf(" - makerune \n"); - printf(" - getcodexsecret \n"); + printf(" - getsecret []\n"); printf(" - getemergencyrecover \n"); printf(" - getnodeid \n"); exit(0); @@ -269,20 +269,40 @@ static void get_channel_seed(struct secret *channel_seed, const struct node_id * info, strlen(info)); } -static void print_codexsecret(const char *hsm_secret_path, const char *id) +static void print_secret(const char *hsm_secret_path, const char *id, bool must_be_oldstyle) { struct secret hsm_secret; char *bip93; const char *err; struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); - /* Extract first 32 bytes for legacy compatibility */ - memcpy(hsm_secret.data, hsms->secret_data, 32); - err = codex32_secret_encode(tmpctx, "cl", id, 0, hsm_secret.data, 32, &bip93); - if (err) - errx(ERROR_USAGE, "%s", err); + switch (hsms->type) { + case HSM_SECRET_ENCRYPTED: + errx(ERROR_USAGE, "Encrypted hsm_secret"); + case HSM_SECRET_MNEMONIC_NO_PASS: + if (must_be_oldstyle) + errx(ERROR_USAGE, "Cannot use getcodexsecret with modern nodes: use getsecret"); + printf("%s\n", hsms->mnemonic); + return; + case HSM_SECRET_MNEMONIC_WITH_PASS: + errx(ERROR_USAGE, "hsm_secret with passphrase"); + case HSM_SECRET_PLAIN: + if (id == NULL) + errx(ERROR_USAGE, "Must set 'id' for a codex32 secret"); + /* Extract first 32 bytes for legacy compatibility */ + memcpy(hsm_secret.data, hsms->secret_data, 32); - printf("%s\n", bip93); + err = codex32_secret_encode(tmpctx, "cl", id, 0, hsm_secret.data, 32, &bip93); + if (err) + errx(ERROR_USAGE, "%s", err); + + printf("%s\n", bip93); + return; + case HSM_SECRET_INVALID: + break; + } + /* Never happens. */ + abort(); } static void print_emergencyrecover(const char *emer_rec_path) @@ -745,10 +765,14 @@ int main(int argc, char *argv[]) if (argc < 3) show_usage(argv[0]); make_rune(argv[2]); + } else if(streq(method, "getsecret")) { + if (argc < 3) + show_usage(argv[0]); + print_secret(argv[2], argv[3], false); } else if(streq(method, "getcodexsecret")) { if (argc < 4) show_usage(argv[0]); - print_codexsecret(argv[2], argv[3]); + print_secret(argv[2], argv[3], true); } else if(streq(method, "getemergencyrecover")) { if (argc < 3) show_usage(argv[0]);