diff --git a/contrib/ldk-server-config.toml b/contrib/ldk-server-config.toml index 82edb062..280b2953 100644 --- a/contrib/ldk-server-config.toml +++ b/contrib/ldk-server-config.toml @@ -9,6 +9,18 @@ alias = "ldk_server" # Lightning node alias #rgs_server_url = "https://rapidsync.lightningdevkit.org/snapshot/v2/" # Optional: RGS URL for rapid gossip sync #async_payments_role = "client" # Optional async payments role: "client" or "server" +# Node entropy settings +[node.entropy] +# Path to a BIP39 mnemonic file. If unset, a fresh 24-word mnemonic is generated on +# first start. Defaults to "/keys_mnemonic". +# CLI/env: --node-entropy-mnemonic-file / LDK_SERVER_NODE_ENTROPY_MNEMONIC_FILE +#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. Configure this explicitly to keep using an existing keys_seed file. +# CLI/env: --node-entropy-seed-file / LDK_SERVER_NODE_ENTROPY_SEED_FILE +# 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 diff --git a/docs/configuration.md b/docs/configuration.md index 5a0efe8a..670d4739 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -151,7 +151,8 @@ Two resolution methods are supported via the `mode` field: ``` / - keys_seed # Node entropy/seed + keys_mnemonic # BIP39 mnemonic (default for new installs) + keys_seed # Legacy raw seed (only used when explicitly configured) tls.crt # TLS certificate (PEM) tls.key # TLS private key (PEM) / # e.g., bitcoin/, regtest/, signet/ @@ -161,6 +162,23 @@ Two resolution methods are supported via the `mode` field: 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 `/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 `/keys_mnemonic`. If + the file does not exist on first start, a fresh 24-word mnemonic is generated and written. + CLI/env: `--node-entropy-mnemonic-file` / `LDK_SERVER_NODE_ENTROPY_MNEMONIC_FILE`. +- `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`. + CLI/env: `--node-entropy-seed-file` / `LDK_SERVER_NODE_ENTROPY_SEED_FILE`. + +Legacy raw-seed installs are not auto-detected. To keep using an existing +`/keys_seed`, set `seed_file` explicitly or use the corresponding CLI argument or +environment variable. diff --git a/docs/operations.md b/docs/operations.md index 28e7f4c6..fd3e923a 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -48,7 +48,8 @@ setup): | File | Priority | Description | | -------------------------------------- | ------------ | -------------------------------------------------------------------------- | -| `/keys_seed` | **Critical** | Node identity and master secret. Required to recover on-chain funds. | +| `/keys_mnemonic` | **Critical** | BIP39 mnemonic. Required to recover on-chain funds. Default for new installs. | +| `/keys_seed` | **Critical** | Legacy raw seed file. Only used when explicitly configured. | | `/ldk_node_data.sqlite` | **Critical** | Channel state and on-chain wallet data. Required to recover channel funds. | | `/ldk_server_data.sqlite` | Nice-to-have | Payment and forwarding history | @@ -195,6 +196,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 an explicitly configured legacy `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. diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index ba64e7ce..1ecd0998 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -27,7 +27,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::events::ClosureReason; use ldk_node::lightning::ln::channelmanager::PaymentId; use ldk_node::lightning::ln::types::ChannelId; @@ -213,11 +212,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); }, }; diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index 61af98f5..53740d52 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -64,6 +64,19 @@ pub struct Config { pub metrics_password: Option, pub tor_config: Option, pub hrn_config: HumanReadableNamesConfig, + pub entropy: EntropyConfig, +} + +/// Configuration for the node's entropy source. +/// +/// When unset, the node defaults to loading or generating a BIP39 mnemonic at +/// `/keys_mnemonic`. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub enum EntropyConfig { + #[default] + Default, + MnemonicFile(String), + SeedFile(String), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -119,10 +132,11 @@ struct ConfigBuilder { metrics_password: Option, tor_proxy_address: Option, hrn: Option, + entropy: EntropyConfig, } impl ConfigBuilder { - fn merge_toml(&mut self, toml: TomlConfig) { + fn merge_toml(&mut self, toml: TomlConfig) -> io::Result<()> { if let Some(node) = toml.node { self.network = node.network.or(self.network); self.listening_addresses = @@ -137,6 +151,9 @@ impl ConfigBuilder { self.async_payments_role = node.async_payments_role.or(self.async_payments_role.clone()); self.rgs_server_url = node.rgs_server_url.or(self.rgs_server_url.clone()); + if let Some(entropy) = node.entropy { + self.entropy = EntropyConfig::from_paths(entropy.mnemonic_file, entropy.seed_file)?; + } } if let Some(storage) = toml.storage { @@ -191,9 +208,11 @@ impl ConfigBuilder { if let Some(hrn) = toml.hrn { self.hrn = Some(hrn); } + + Ok(()) } - fn merge_args(&mut self, args: &ArgsConfig) { + fn merge_args(&mut self, args: &ArgsConfig) -> io::Result<()> { if let Some(network) = args.node_network { self.network = Some(network); } @@ -257,6 +276,15 @@ impl ConfigBuilder { if let Some(tor_proxy_address) = &args.tor_proxy_address { self.tor_proxy_address = Some(tor_proxy_address.clone()); } + + if args.node_entropy_mnemonic_file.is_some() || args.node_entropy_seed_file.is_some() { + self.entropy = EntropyConfig::from_paths( + args.node_entropy_mnemonic_file.clone(), + args.node_entropy_seed_file.clone(), + )?; + } + + Ok(()) } fn build(self) -> io::Result { @@ -454,10 +482,25 @@ impl ConfigBuilder { metrics_password, tor_config: tor_proxy_address.map(|proxy_address| TorConfig { proxy_address }), hrn_config, + entropy: self.entropy, }) } } +impl EntropyConfig { + fn from_paths(mnemonic_file: Option, seed_file: Option) -> io::Result { + match (mnemonic_file, seed_file) { + (Some(_), Some(_)) => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Only one of `node.entropy.mnemonic_file`/`--node-entropy-mnemonic-file` and `node.entropy.seed_file`/`--node-entropy-seed-file` may be configured.".to_string(), + )), + (Some(mnemonic_file), None) => Ok(Self::MnemonicFile(mnemonic_file)), + (None, Some(seed_file)) => Ok(Self::SeedFile(seed_file)), + (None, None) => Ok(Self::Default), + } + } +} + /// Configuration loaded from a TOML file. #[derive(Deserialize, Serialize)] pub struct TomlConfig { @@ -484,6 +527,13 @@ struct NodeConfig { pathfinding_scores_source_url: Option, async_payments_role: Option, rgs_server_url: Option, + entropy: Option, +} + +#[derive(Deserialize, Serialize)] +struct NodeEntropyTomlConfig { + mnemonic_file: Option, + seed_file: Option, } #[derive(Deserialize, Serialize)] @@ -813,6 +863,20 @@ pub struct ArgsConfig { )] node_async_payments_role: Option, + #[arg( + long, + env = "LDK_SERVER_NODE_ENTROPY_MNEMONIC_FILE", + help = "Path to the BIP39 mnemonic file used for node entropy." + )] + node_entropy_mnemonic_file: Option, + + #[arg( + long, + env = "LDK_SERVER_NODE_ENTROPY_SEED_FILE", + help = "Legacy path to a raw 64-byte seed file used for node entropy." + )] + node_entropy_seed_file: Option, + #[arg( long, env = "LDK_SERVER_METRICS_ENABLED", @@ -870,10 +934,10 @@ pub fn load_config(args: &ArgsConfig) -> io::Result { ) })?; - builder.merge_toml(toml_config); + builder.merge_toml(toml_config)?; } - builder.merge_args(args); + builder.merge_args(args)?; builder.build() } @@ -913,8 +977,10 @@ fn parse_host_port(addr: &str) -> io::Result<(String, u16)> { #[cfg(test)] mod tests { + use std::ffi::OsString; use std::str::FromStr; + use clap::Parser; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::Network; use ldk_node::lightning::ln::msgs::SocketAddress; @@ -988,6 +1054,8 @@ mod tests { metrics_username: None, metrics_password: None, tor_proxy_address: None, + node_entropy_mnemonic_file: None, + node_entropy_seed_file: None, } } @@ -1010,6 +1078,8 @@ mod tests { metrics_username: None, metrics_password: None, tor_proxy_address: None, + node_entropy_mnemonic_file: None, + node_entropy_seed_file: None, } } @@ -1020,6 +1090,13 @@ mod tests { ) } + fn restore_env_var(key: &str, old_value: Option) { + match old_value { + Some(value) => std::env::set_var(key, value), + None => std::env::remove_var(key), + } + } + #[test] fn test_config_from_file() { let storage_path = std::env::temp_dir(); @@ -1087,6 +1164,7 @@ mod tests { proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(), }), hrn_config: HumanReadableNamesConfig::default(), + entropy: EntropyConfig::default(), }; assert_eq!(config.listening_addrs, expected.listening_addrs); @@ -1395,6 +1473,7 @@ mod tests { metrics_password: None, tor_config: None, hrn_config: HumanReadableNamesConfig::default(), + entropy: EntropyConfig::default(), }; assert_eq!(config.listening_addrs, expected.listening_addrs); @@ -1507,6 +1586,7 @@ mod tests { proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(), }), hrn_config: HumanReadableNamesConfig::default(), + entropy: EntropyConfig::default(), }; assert_eq!(config.listening_addrs, expected.listening_addrs); @@ -1789,4 +1869,123 @@ mod tests { ); assert!(parse_dns_server_address("invalid@address").is_err()); } + + #[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, + EntropyConfig::MnemonicFile("/some/path/keys_mnemonic".to_string()) + ); + } + + #[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")); + } + + #[test] + fn test_node_entropy_args_override_file() { + let storage_path = std::env::temp_dir(); + let config_file_name = "test_node_entropy_args_override_file.toml"; + + let toml_config = r#" + [node] + network = "regtest" + grpc_service_address = "127.0.0.1:3002" + + [node.entropy] + mnemonic_file = "/config/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()); + args_config.node_entropy_seed_file = Some("/legacy/keys_seed".to_string()); + + let config = load_config(&args_config).unwrap(); + assert_eq!(config.entropy, EntropyConfig::SeedFile("/legacy/keys_seed".to_string())); + } + + #[test] + fn test_rejects_both_entropy_args() { + let mut args_config = default_args_config(); + args_config.node_entropy_mnemonic_file = Some("/some/path/keys_mnemonic".to_string()); + args_config.node_entropy_seed_file = Some("/some/path/keys_seed".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")); + } + + #[test] + fn test_node_entropy_mnemonic_file_env() { + let mnemonic_key = "LDK_SERVER_NODE_ENTROPY_MNEMONIC_FILE"; + let seed_key = "LDK_SERVER_NODE_ENTROPY_SEED_FILE"; + let old_mnemonic = std::env::var_os(mnemonic_key); + let old_seed = std::env::var_os(seed_key); + + std::env::set_var(mnemonic_key, "/env/keys_mnemonic"); + std::env::remove_var(seed_key); + let parse_result = ArgsConfig::try_parse_from(["ldk-server"]); + restore_env_var(mnemonic_key, old_mnemonic); + restore_env_var(seed_key, old_seed); + + let args_config = parse_result.unwrap(); + assert_eq!(args_config.node_entropy_mnemonic_file, Some("/env/keys_mnemonic".to_string())); + assert_eq!(args_config.node_entropy_seed_file, None); + } } diff --git a/ldk-server/src/util/entropy.rs b/ldk-server/src/util/entropy.rs new file mode 100644 index 00000000..8c3aa6c2 --- /dev/null +++ b/ldk-server/src/util/entropy.rs @@ -0,0 +1,185 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use std::fs; +use std::io::{self, Write}; +use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use ldk_node::bip39::Mnemonic; +use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; +use log::info; + +use crate::util::config::EntropyConfig; + +const DEFAULT_MNEMONIC_FILE: &str = "keys_mnemonic"; +#[cfg(test)] +const LEGACY_SEED_FILE: &str = "keys_seed"; + +pub(crate) fn load_or_generate_node_entropy( + storage_dir: &Path, entropy_config: &EntropyConfig, +) -> io::Result { + let mnemonic_path = match entropy_config { + EntropyConfig::SeedFile(seed_file) => { + info!("Loading node entropy from raw seed file at {}", seed_file); + return NodeEntropy::from_seed_path(seed_file.clone()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())); + }, + EntropyConfig::MnemonicFile(path) => PathBuf::from(path), + EntropyConfig::Default => storage_dir.join(DEFAULT_MNEMONIC_FILE), + }; + + let mnemonic = if mnemonic_path.exists() { + let raw = fs::read_to_string(&mnemonic_path)?; + Mnemonic::from_str(raw.trim()).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid BIP39 mnemonic in {}: {}", mnemonic_path.display(), e), + ) + })? + } else { + if let Some(parent) = mnemonic_path.parent() { + fs::create_dir_all(parent)?; + } + let mnemonic = generate_entropy_mnemonic(None); + write_mnemonic_file(&mnemonic_path, &mnemonic)?; + info!( + "Generated new BIP39 mnemonic at {}. Back up this file securely — it is required to recover on-chain funds.", + mnemonic_path.display() + ); + mnemonic + }; + + Ok(NodeEntropy::from_bip39_mnemonic(mnemonic, None)) +} + +fn write_mnemonic_file(path: &Path, mnemonic: &Mnemonic) -> io::Result<()> { + let mut f = fs::OpenOptions::new().create_new(true).write(true).mode(0o600).open(path)?; + f.set_permissions(fs::Permissions::from_mode(0o600))?; + writeln!(f, "{}", mnemonic)?; + f.sync_all()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::os::unix::fs::MetadataExt; + + const KNOWN_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; + + fn tempdir(tag: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!( + "ldk-server-entropy-test-{}-{}", + tag, + std::process::id() + )); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn generates_mnemonic_on_fresh_start() { + let dir = tempdir("fresh"); + let cfg = EntropyConfig::default(); + + load_or_generate_node_entropy(&dir, &cfg).unwrap(); + + let mnemonic_path = dir.join(DEFAULT_MNEMONIC_FILE); + assert!(mnemonic_path.exists(), "keys_mnemonic was not created"); + + let perms = fs::metadata(&mnemonic_path).unwrap().permissions(); + assert_eq!(perms.mode() & 0o777, 0o600, "expected 0600 permissions"); + + let content = fs::read_to_string(&mnemonic_path).unwrap(); + let word_count = content.trim().split_whitespace().count(); + assert_eq!(word_count, 24, "expected 24-word mnemonic, got {}", word_count); + + let mtime_before = fs::metadata(&mnemonic_path).unwrap().mtime(); + load_or_generate_node_entropy(&dir, &cfg).unwrap(); + let mtime_after = fs::metadata(&mnemonic_path).unwrap().mtime(); + assert_eq!(mtime_before, mtime_after, "mnemonic file was rewritten on second call"); + } + + #[test] + fn rereads_existing_mnemonic_without_mutation() { + let dir = tempdir("reread"); + let mnemonic_path = dir.join(DEFAULT_MNEMONIC_FILE); + fs::write(&mnemonic_path, format!("{}\n", KNOWN_MNEMONIC)).unwrap(); + let bytes_before = fs::read(&mnemonic_path).unwrap(); + + load_or_generate_node_entropy(&dir, &EntropyConfig::default()).unwrap(); + + let bytes_after = fs::read(&mnemonic_path).unwrap(); + assert_eq!(bytes_before, bytes_after, "mnemonic file content changed"); + } + + #[test] + fn default_entropy_ignores_legacy_keys_seed() { + let dir = tempdir("legacy"); + let legacy_path = dir.join(LEGACY_SEED_FILE); + fs::write(&legacy_path, vec![0x42u8; 64]).unwrap(); + + load_or_generate_node_entropy(&dir, &EntropyConfig::default()).unwrap(); + + assert!(dir.join(DEFAULT_MNEMONIC_FILE).exists(), "keys_mnemonic was not created"); + assert!(legacy_path.exists(), "legacy keys_seed was removed"); + } + + #[test] + fn explicit_seed_file_used_directly() { + let dir = tempdir("explicit-seed"); + let custom_seed = dir.join("custom-seed.bin"); + fs::write(&custom_seed, vec![0x17u8; 64]).unwrap(); + + let cfg = EntropyConfig::SeedFile(custom_seed.to_string_lossy().into_owned()); + + load_or_generate_node_entropy(&dir, &cfg).unwrap(); + + assert!( + !dir.join(DEFAULT_MNEMONIC_FILE).exists(), + "keys_mnemonic was created despite seed_file being set" + ); + } + + #[test] + fn rejects_invalid_mnemonic_file() { + let dir = tempdir("invalid"); + fs::write( + dir.join(DEFAULT_MNEMONIC_FILE), + "these words are definitely not a valid bip39 phrase at all nope", + ) + .unwrap(); + + let err = load_or_generate_node_entropy(&dir, &EntropyConfig::default()).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn custom_mnemonic_path_respected() { + let dir = tempdir("custom-mnemonic"); + let custom_path = dir.join("elsewhere").join("my_mnemonic"); + let cfg = EntropyConfig::MnemonicFile(custom_path.to_string_lossy().into_owned()); + + load_or_generate_node_entropy(&dir, &cfg).unwrap(); + + assert!(custom_path.exists(), "custom mnemonic file was not created"); + assert!( + !dir.join(DEFAULT_MNEMONIC_FILE).exists(), + "default keys_mnemonic was unexpectedly created" + ); + + let content_before = fs::read(&custom_path).unwrap(); + load_or_generate_node_entropy(&dir, &cfg).unwrap(); + let content_after = fs::read(&custom_path).unwrap(); + assert_eq!(content_before, content_after); + } +} diff --git a/ldk-server/src/util/mod.rs b/ldk-server/src/util/mod.rs index a57dbd00..20a34679 100644 --- a/ldk-server/src/util/mod.rs +++ b/ldk-server/src/util/mod.rs @@ -8,6 +8,7 @@ // licenses. pub(crate) mod config; +pub(crate) mod entropy; pub(crate) mod logger; pub(crate) mod metrics; pub(crate) mod proto_adapter;