Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/src/getting-started/register.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ phrase and then convert it into a seed secret we can use:
--8<-- "getting_started.py:create_seed"
```

=== "Bash"
```bash
# For example gl-cli can be used to create an hsm_secret file
gl-cli signer generate-secret
```

!!! important
Remember to store the seed somewhere (file on disk, registry, etc)
because without it, you will not have access to the node, and any
Expand Down
1 change: 1 addition & 0 deletions libs/gl-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ test = true
doc = true

[dependencies]
bip39 = { version = "2.2", features = ["rand"] }
clap = { version = "4.5", features = ["derive"] }
dirs = "6.0"
env_logger = "0.11"
Expand Down
49 changes: 49 additions & 0 deletions libs/gl-cli/src/signer.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::error::{Error, Result};
use crate::util;
use bip39::Mnemonic;
use clap::Subcommand;
use core::fmt::Debug;
use gl_client::signer::Signer;
use lightning_signer::bitcoin::Network;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use tokio::{join, signal};
use util::{CREDENTIALS_FILE_NAME, SEED_FILE_NAME};
Expand All @@ -19,15 +22,61 @@ pub enum Command {
Run,
/// Prints the version of the signer used
Version,
/// Generate a new hsm_secret file from a 12 word seed phrase
GenerateSecret {
#[arg(long)]
mnemonic: Option<String>,
#[arg(long)]
passphrase: Option<String>,
},
}

pub async fn command_handler<P: AsRef<Path>>(cmd: Command, config: Config<P>) -> Result<()> {
match cmd {
Command::Run => run_handler(config).await,
Command::Version => version(config).await,
Command::GenerateSecret {
mnemonic,
passphrase,
} => generate_secret(config, mnemonic, passphrase).await,
}
}

async fn generate_secret<P: AsRef<Path>>(
Copy link

@sangbida sangbida Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Lagrang3! This will generate an older version of the hsm_secret and while we do support it, you won't get the benefit of recoverable addresses that you will with the modern version of the hsm secret. So for reference, we support 4 types of hsm_secrets:

  • Legacy 32 byte secrets - This is your usual 32 bytes, so in your case your mnemonic produces a seed and you store the first 32 bytes of it in the file. Your password case is also a variant of this, this is quite different to how it is in CLN, In CLN if the secret is 32 bytes it means that there is no passphrase. We rely on the 32 bytes == no passphrase in our recovery tools such as exposesecret. In your case, we're adding a passphrase option that leads to a completely different 32 byte secret.

  • Legacy 73 byte encrypted secrets - This is how passphrase support used to work, the passphrase would be used to encrypt the 32 byte secret to produce an encrypted 73 byte secret.

  • Modern mnemonics (no passphrase) - In this case we'll have 32 bytes of zero padding followed by the mnemonic itself. So it would look something like:

Full content (hex): 00000000000000000000000000000000000000000000000000000000000000006162616e646f6e206162616e646f6e206162616e646f6e206162616e646f6e206162616e646f6e206162616e646f6e206162616e646f6e206162616e646f6e206162616e646f6e206162616e646f6e206162616e646f6e2061626f7574
Full content (repr): b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
  • Modern mnemonic with passphrase: In this case, instead of the 32 byte zero padding we pad with 32 bytes of the sha256 hash of the full BIP39 seed generated from the mnemonic and passphrase. We do this to validate that the password that we have been given is correct without exposing the passphrase. So an example of this would be:
Full content (hex): 61ce9e277eb9894701400a422e43ba27c7d9d10b72bad52c16a411d2dbaf82f172697475616c2069646c65206861742073756e6e7920756e69766572736520706c75636b206b657920616c7068612077696e672063616b6520686176652077656464696e67
Full content (repr): b"a\xce\x9e'~\xb9\x89G\x01@\nB.C\xba'\xc7\xd9\xd1\x0br\xba\xd5,\x16\xa4\x11\xd2\xdb\xaf\x82\xf1ritual idle hat sunny universe pluck key alpha wing cake have wedding"

You can see how it's done in generate_hsm() and derive_seed_hash()

My recommendation would be to follow the modern standard for hsm_secrets so taproot wallet addresses are recoverable!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice explanation! Thank you.
I do prefer the modern mnemonic with passphrase.

I've implemented the legacy 32 byte secret because I had troubles with registering a node with the modern hsm_secret. It Might have been an issue with the signer not being recently enough. Let me investigate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it is

sec.copy_from_slice(&secret[0..32]);

The current signer only reads the first 32 bytes from the hsm_secret file.
Ok. It seems a bit more of work is needed to make the Signer work with the modern hsm_secret format.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aah yep, that won't work for the modern hsms unfortunately :( Let me know if you would like to pair on this 😄

config: Config<P>,
mnemonic: Option<String>,
passphrase: Option<String>,
) -> Result<()> {
// Check if we can find a seed file, refuse to generate a new one if exists
let seed_path = config.data_dir.as_ref().join(SEED_FILE_NAME);
let seed = util::read_seed(&seed_path);
if seed.is_some() {
return Err(Error::custom(format!(
"Seed already exists: {}",
seed_path.display()
)));
}
let mnemonic = match mnemonic {
Some(sentence) => {
Mnemonic::parse(sentence).map_err(|e| Error::custom(format!("Bad mnemonic: {e}")))?
}
None => Mnemonic::generate(12)
.map_err(|e| Error::custom(format!("Failed to generate mnemonic: {e}")))?,
};
let passphrase = passphrase.unwrap_or(String::new());
println!(
"Mnemonic sentence: \"{}\"",
mnemonic.words().collect::<Vec<_>>().join(" ")
);
println!("Passphrase: \"{passphrase}\"",);
let seed = &mnemonic.to_seed(passphrase)[0..32];
let mut file = File::create(seed_path)
.map_err(|e| Error::custom(format!("Failed to create seed: {e}")))?;
file.write_all(seed)
.map_err(|e| Error::custom(format!("Failed to write seed to file: {e}")))?;
Ok(())
}

async fn run_handler<P: AsRef<Path>>(config: Config<P>) -> Result<()> {
// Check if we can find a seed file, if we can not find one, we need to register first.
let seed_path = config.data_dir.as_ref().join(SEED_FILE_NAME);
Expand Down
Loading