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
9 changes: 9 additions & 0 deletions contrib/ldk-server-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ alias = "ldk_server" # Lightning node alias
#pathfinding_scores_source_url = "" # External Pathfinding Scores Source
#rgs_server_url = "https://rapidsync.lightningdevkit.org/snapshot/v2/" # Optional: RGS URL for rapid gossip sync

# Node entropy settings
[node.entropy]
# Path to a BIP39 mnemonic file. If unset and no legacy `keys_seed` file exists, a fresh
# 24-word mnemonic is generated on first start. Defaults to "<storage_dir>/keys_mnemonic".
#mnemonic_file = "/tmp/ldk-server/keys_mnemonic"
# Legacy: path to a raw 64-byte seed file used by ldk-server installs initialized before
# BIP39 mnemonic support. Mutually exclusive with `mnemonic_file`.
#seed_file = "/tmp/ldk-server/keys_seed"

# Storage settings
[storage.disk]
dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persistence, optional, defaults to ~/Library/Application Support/ldk-server/ on macOS, ~/.ldk-server/ on Linux
Expand Down
23 changes: 19 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ full setup.

```
<storage_dir>/
keys_seed # Node entropy/seed
keys_mnemonic # BIP39 mnemonic (default for new installs)
keys_seed # Legacy raw seed (only present on installs initialized before mnemonic support)
tls.crt # TLS certificate (PEM)
tls.key # TLS private key (PEM)
<network>/ # e.g., bitcoin/, regtest/, signet/
Expand All @@ -137,6 +138,20 @@ full setup.
ldk_server_data.sqlite # Payment and forwarding history
```

The `keys_seed` file is the node's master secret, required to recover on-chain funds.
`ldk_node_data.sqlite` holds channel state, both are required to recover channel funds. See
[Operations - Backups](operations.md#backups) for backup guidance.
The mnemonic (or, for legacy installs, the raw seed) is the node's master secret, required to
recover on-chain funds. `ldk_node_data.sqlite` holds channel state, both are required to recover
channel funds. See [Operations - Backups](operations.md#backups) for backup guidance.

### Node entropy (`[node.entropy]`)

By default, ldk-server reads or generates a 24-word BIP39 mnemonic at `<storage_dir>/keys_mnemonic`,
which can be imported into any standard BIP39-compatible wallet to recover on-chain funds. The
defaults can be overridden under `[node.entropy]`:

- `mnemonic_file`: path to the BIP39 mnemonic file. Defaults to `<storage_dir>/keys_mnemonic`. If
the file does not exist on first start, a fresh 24-word mnemonic is generated and written.
- `seed_file`: path to a raw 64-byte seed file. Provided for backwards compatibility with installs
initialized before BIP39 mnemonic support. Mutually exclusive with `mnemonic_file`.

For backwards compatibility, if neither field is configured and a `keys_seed` file exists at the
storage root, ldk-server will continue to use it.
9 changes: 5 additions & 4 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ setup):

| File | Priority | Description |
|----------------------------------------|--------------|----------------------------------------------------------------------------|
| `<storage_dir>/keys_seed` | **Critical** | Node identity and master secret. Required to recover on-chain funds. |
| `<storage_dir>/keys_mnemonic` | **Critical** | BIP39 mnemonic. Required to recover on-chain funds. Default for new installs. |
| `<storage_dir>/keys_seed` | **Critical** | Legacy raw seed file. Only present on installs initialized before mnemonic support. |
| `<network_dir>/ldk_node_data.sqlite` | **Critical** | Channel state and on-chain wallet data. Required to recover channel funds. |
| `<network_dir>/ldk_server_data.sqlite` | Nice-to-have | Payment and forwarding history |

Expand Down Expand Up @@ -162,6 +163,6 @@ Data is stored in per-network subdirectories (`bitcoin/`, `testnet/`, `signet/`,
etc.) under the storage root. This means you can run multiple networks from one storage
directory without conflicts.

The `keys_seed` file is shared across networks (stored at the storage root, not per-network).
Keys are split by network at the derivation path level, so the same seed will produce
different keys.
The `keys_mnemonic` file (or, on legacy installs, `keys_seed`) is shared across networks
(stored at the storage root, not per-network). Keys are split by network at the derivation
path level, so the same mnemonic/seed will produce different keys.
9 changes: 5 additions & 4 deletions ldk-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ use hyper::server::conn::http2;
use hyper_util::rt::{TokioExecutor, TokioIo};
use ldk_node::bitcoin::Network;
use ldk_node::config::Config;
use ldk_node::entropy::NodeEntropy;
use ldk_node::lightning::ln::channelmanager::PaymentId;
use ldk_node::{Builder, Event, Node};
use ldk_server_grpc::events;
Expand Down Expand Up @@ -204,11 +203,13 @@ fn main() {

builder.set_runtime(runtime.handle().clone());

let seed_path = storage_dir.join("keys_seed").to_str().unwrap().to_string();
let node_entropy = match NodeEntropy::from_seed_path(seed_path) {
let node_entropy = match crate::util::entropy::load_or_generate_node_entropy(
&storage_dir,
&config_file.entropy,
) {
Ok(entropy) => entropy,
Err(e) => {
error!("Failed to load or generate seed: {e}");
error!("Failed to load or generate node entropy: {e}");
std::process::exit(-1);
},
};
Expand Down
95 changes: 95 additions & 0 deletions ldk-server/src/util/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ pub struct Config {
pub metrics_username: Option<String>,
pub metrics_password: Option<String>,
pub tor_config: Option<TorConfig>,
pub entropy: EntropyConfig,
}

/// Configuration for the node's entropy source.
///
/// When both `mnemonic_file` and `seed_file` are unset, the node defaults to loading or
/// generating a BIP39 mnemonic at `<storage_dir>/keys_mnemonic`. If a legacy raw-seed file
/// exists at `<storage_dir>/keys_seed`, it is used for backwards compatibility.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct EntropyConfig {
pub mnemonic_file: Option<String>,
pub seed_file: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -114,6 +126,7 @@ struct ConfigBuilder {
metrics_username: Option<String>,
metrics_password: Option<String>,
tor_proxy_address: Option<String>,
entropy: EntropyConfig,
}

impl ConfigBuilder {
Expand All @@ -130,6 +143,11 @@ impl ConfigBuilder {
self.pathfinding_scores_source_url =
node.pathfinding_scores_source_url.or(self.pathfinding_scores_source_url.clone());
self.rgs_server_url = node.rgs_server_url.or(self.rgs_server_url.clone());
if let Some(entropy) = node.entropy {
self.entropy.mnemonic_file =
entropy.mnemonic_file.or(self.entropy.mnemonic_file.take());
self.entropy.seed_file = entropy.seed_file.or(self.entropy.seed_file.take());
}
}

if let Some(storage) = toml.storage {
Expand Down Expand Up @@ -402,6 +420,13 @@ impl ConfigBuilder {
})
.transpose()?;

if self.entropy.mnemonic_file.is_some() && self.entropy.seed_file.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Only one of `node.entropy.mnemonic_file` and `node.entropy.seed_file` may be configured.".to_string(),
));
}

Ok(Config {
network,
listening_addrs,
Expand All @@ -422,6 +447,7 @@ impl ConfigBuilder {
metrics_username,
metrics_password,
tor_config: tor_proxy_address.map(|proxy_address| TorConfig { proxy_address }),
entropy: self.entropy,
})
}
}
Expand Down Expand Up @@ -450,6 +476,13 @@ struct NodeConfig {
alias: Option<String>,
pathfinding_scores_source_url: Option<String>,
rgs_server_url: Option<String>,
entropy: Option<NodeEntropyTomlConfig>,
}

#[derive(Deserialize, Serialize)]
struct NodeEntropyTomlConfig {
mnemonic_file: Option<String>,
seed_file: Option<String>,
}

#[derive(Deserialize, Serialize)]
Expand Down Expand Up @@ -936,6 +969,7 @@ mod tests {
tor_config: Some(TorConfig {
proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(),
}),
entropy: EntropyConfig::default(),
};

assert_eq!(config.listening_addrs, expected.listening_addrs);
Expand Down Expand Up @@ -1241,6 +1275,7 @@ mod tests {
metrics_username: None,
metrics_password: None,
tor_config: None,
entropy: EntropyConfig::default(),
};

assert_eq!(config.listening_addrs, expected.listening_addrs);
Expand Down Expand Up @@ -1350,6 +1385,7 @@ mod tests {
tor_config: Some(TorConfig {
proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(),
}),
entropy: EntropyConfig::default(),
};

assert_eq!(config.listening_addrs, expected.listening_addrs);
Expand Down Expand Up @@ -1501,4 +1537,63 @@ mod tests {
let err = result.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}

#[test]
fn test_parses_node_entropy_section() {
let storage_path = std::env::temp_dir();
let config_file_name = "test_parses_node_entropy_section.toml";

let toml_config = r#"
[node]
network = "regtest"
grpc_service_address = "127.0.0.1:3002"

[node.entropy]
mnemonic_file = "/some/path/keys_mnemonic"

[bitcoind]
rpc_address = "127.0.0.1:8332"
rpc_user = "bitcoind-testuser"
rpc_password = "bitcoind-testpassword"
"#;

fs::write(storage_path.join(config_file_name), toml_config).unwrap();
let mut args_config = empty_args_config();
args_config.config_file =
Some(storage_path.join(config_file_name).to_string_lossy().to_string());

let config = load_config(&args_config).unwrap();
assert_eq!(config.entropy.mnemonic_file, Some("/some/path/keys_mnemonic".to_string()));
assert_eq!(config.entropy.seed_file, None);
}

#[test]
fn test_rejects_both_mnemonic_and_seed_file() {
let storage_path = std::env::temp_dir();
let config_file_name = "test_rejects_both_mnemonic_and_seed_file.toml";

let toml_config = r#"
[node]
network = "regtest"
grpc_service_address = "127.0.0.1:3002"

[node.entropy]
mnemonic_file = "/some/path/keys_mnemonic"
seed_file = "/some/path/keys_seed"

[bitcoind]
rpc_address = "127.0.0.1:8332"
rpc_user = "bitcoind-testuser"
rpc_password = "bitcoind-testpassword"
"#;

fs::write(storage_path.join(config_file_name), toml_config).unwrap();
let mut args_config = empty_args_config();
args_config.config_file =
Some(storage_path.join(config_file_name).to_string_lossy().to_string());

let err = load_config(&args_config).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert!(err.to_string().contains("Only one of"));
}
}
Loading