Skip to content
Open
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
20 changes: 16 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions contrib/ldk-server-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,15 @@ poll_metrics_interval = 60 # The polling interval for metrics in seconds.
[tor]
# Only connections to OnionV3 peers will be made via this proxy; other connections (IPv4 peers, Electrum server) will not be routed over Tor.
#proxy_address = "127.0.0.1:9050" # Tor daemon SOCKS proxy address.

# Human-Readable Names (BIP 353) resolution
[hrn]
# Resolution method: "dns" (resolve locally via a DNS server) or "blip32" (ask other
# nodes to resolve for us via bLIP-32). Defaults to "dns".
#mode = "dns"
# DNS server used when `mode = "dns"`. Defaults to 8.8.8.8:53 (Google Public DNS).
#dns_server_address = "8.8.8.8:53"
# When set to true (and `mode = "dns"`), also offer HRN resolution to the rest of the
# network over Onion Messages. Requires the node to be announceable so resolution
# requests can be routed to us. Defaults to false.
#enable_resolution_service = false
15 changes: 15 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,21 @@ are not proxied. This does not set up inbound connections, to make your node rea
hidden service, you need to configure Tor separately. See the [Tor guide](tor.md) for the
full setup.

### `[hrn]`

Configures how the node resolves [BIP 353](https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki)
Human-Readable Names (e.g., `₿alice@example.com`) to Lightning payment destinations.

Two resolution methods are supported via the `mode` field:

- **`"dns"`** (default) - Resolve names locally using a DNS server. The server is set via
`dns_server_address` (default: `8.8.8.8:53`, Google Public DNS). When
`enable_resolution_service = true`, the node additionally offers HRN resolution to the
rest of the network over Onion Messages. This requires the node to be announceable so
resolution requests can be routed to it, and is therefore disabled by default.
- **`"blip32"`** - Ask other nodes to resolve names on our behalf via
[bLIP-32](https://github.com/lightning/blips/blob/master/blip-0032.md).

## Storage Layout

```
Expand Down
2 changes: 1 addition & 1 deletion ldk-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"

[dependencies]
ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "c754e2fe85c70741b5e370334cd16856c615265e" }
ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "21eea8c881790db7a90bcad4f7f45f341c72222b" }
serde = { version = "1.0.203", default-features = false, features = ["derive"] }
hyper = { version = "1", default-features = false, features = ["server", "http2"] }
http-body-util = { version = "0.1", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions ldk-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ fn main() {
ldk_node_config.listening_addresses = config_file.listening_addrs;
ldk_node_config.announcement_addresses = config_file.announcement_addrs;
ldk_node_config.network = config_file.network;
ldk_node_config.hrn_config = config_file.hrn_config;

let mut builder = Builder::from_config(ldk_node_config);
builder.set_log_facade_logger();
Expand Down
140 changes: 140 additions & 0 deletions ldk-server/src/util/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::{fs, io};
use clap::Parser;
use ldk_node::bitcoin::secp256k1::PublicKey;
use ldk_node::bitcoin::Network;
use ldk_node::config::{HRNResolverConfig, HumanReadableNamesConfig};
use ldk_node::lightning::ln::msgs::SocketAddress;
use ldk_node::lightning::routing::gossip::NodeAlias;
use ldk_node::liquidity::LSPS2ServiceConfig;
Expand Down Expand Up @@ -61,6 +62,7 @@ pub struct Config {
pub metrics_username: Option<String>,
pub metrics_password: Option<String>,
pub tor_config: Option<TorConfig>,
pub hrn_config: HumanReadableNamesConfig,
}

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

impl ConfigBuilder {
Expand Down Expand Up @@ -180,6 +183,10 @@ impl ConfigBuilder {
if let Some(tor) = toml.tor {
self.tor_proxy_address = Some(tor.proxy_address)
}

if let Some(hrn) = toml.hrn {
self.hrn = Some(hrn);
}
}

fn merge_args(&mut self, args: &ArgsConfig) {
Expand Down Expand Up @@ -402,6 +409,11 @@ impl ConfigBuilder {
})
.transpose()?;

let hrn_config = match self.hrn {
Some(hrn) => HumanReadableNamesConfig::try_from(hrn)?,
None => HumanReadableNamesConfig::default(),
};

Ok(Config {
network,
listening_addrs,
Expand All @@ -422,6 +434,7 @@ impl ConfigBuilder {
metrics_username,
metrics_password,
tor_config: tor_proxy_address.map(|proxy_address| TorConfig { proxy_address }),
hrn_config,
})
}
}
Expand All @@ -439,6 +452,7 @@ pub struct TomlConfig {
tls: Option<TomlTlsConfig>,
metrics: Option<MetricsTomlConfig>,
tor: Option<TomlTorConfig>,
hrn: Option<HrnTomlConfig>,
}

#[derive(Deserialize, Serialize)]
Expand Down Expand Up @@ -505,6 +519,52 @@ struct TomlTorConfig {
proxy_address: String,
}

#[derive(Deserialize, Serialize)]
struct HrnTomlConfig {
mode: Option<String>,
dns_server_address: Option<String>,
enable_resolution_service: Option<bool>,
}

impl TryFrom<HrnTomlConfig> for HumanReadableNamesConfig {
type Error = io::Error;

fn try_from(value: HrnTomlConfig) -> Result<Self, Self::Error> {
let mut config = HumanReadableNamesConfig::default();

match value.mode.as_deref() {
None | Some("dns") => {},
Copy link
Copy Markdown
Collaborator

@benthecarman benthecarman Apr 21, 2026

Choose a reason for hiding this comment

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

here we assume that the default is HRNResolverConfig::Dns and then modify the settings below. Would be safer to have the logic of the if let HRNResolverConfig::Dns ... in this arm so if the default ever changed (say to http to support lnurl) then we could better handle it and not silently drop the dns_server_address and enable_resolution_service settings.

Some("blip32") => {
config.resolution_config = HRNResolverConfig::Blip32;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would be good to throw an error if enable_resolution_service or dns_server_address is set. Otherwise users could be confused that they have something even though we are silently dropping the config option

},
Some(other) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid HRN mode '{}' configured; expected 'dns' or 'blip32'", other),
))
},
}

if let HRNResolverConfig::Dns { dns_server_address, enable_hrn_resolution_service } =
&mut config.resolution_config
{
if let Some(addr) = value.dns_server_address.as_deref() {
*dns_server_address = SocketAddress::from_str(addr).map_err(|e| {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we do something here to assume port 53 if its not provided?

io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid HRN DNS server address configured: {}", e),
)
})?;
}
if let Some(enable) = value.enable_resolution_service {
*enable_hrn_resolution_service = enable;
}
}

Ok(config)
}
}

#[derive(Deserialize, Serialize)]
struct LiquidityConfig {
lsps2_client: Option<LSPSClientTomlConfig>,
Expand Down Expand Up @@ -936,6 +996,7 @@ mod tests {
tor_config: Some(TorConfig {
proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(),
}),
hrn_config: HumanReadableNamesConfig::default(),
};

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

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

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

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

let base_config = r#"
[node]
network = "regtest"

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

[liquidity.lsps2_service]
advertise_service = false
channel_opening_fee_ppm = 1000
channel_over_provisioning_ppm = 500000
min_channel_opening_fee_msat = 10000000
min_channel_lifetime = 4320
max_client_to_self_delay = 1440
min_payment_size_msat = 10000000
max_payment_size_msat = 25000000000
client_trusts_lsp = true
disable_client_reserve = false
"#;

let mut args_config = empty_args_config();
args_config.config_file =
Some(storage_path.join(config_file_name).to_string_lossy().to_string());

// Default: no `[hrn]` section -> DNS against 8.8.8.8:53, resolution service disabled.
fs::write(storage_path.join(config_file_name), base_config).unwrap();
let config = load_config(&args_config).unwrap();
match config.hrn_config.resolution_config {
HRNResolverConfig::Dns { dns_server_address, enable_hrn_resolution_service } => {
assert_eq!(dns_server_address, SocketAddress::from_str("8.8.8.8:53").unwrap());
assert!(!enable_hrn_resolution_service);
},
other => panic!("unexpected default HRN resolver config: {:?}", other),
}

// Custom DNS server address with resolution service enabled.
let toml_config = format!(
"{}\n[hrn]\ndns_server_address = \"1.1.1.1:53\"\nenable_resolution_service = true\n",
base_config
);
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
let config = load_config(&args_config).unwrap();
match config.hrn_config.resolution_config {
HRNResolverConfig::Dns { dns_server_address, enable_hrn_resolution_service } => {
assert_eq!(dns_server_address, SocketAddress::from_str("1.1.1.1:53").unwrap());
assert!(enable_hrn_resolution_service);
},
other => panic!("unexpected HRN resolver config: {:?}", other),
}

// Blip32 mode.
let toml_config = format!("{}\n[hrn]\nmode = \"blip32\"\n", base_config);
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
let config = load_config(&args_config).unwrap();
assert!(matches!(config.hrn_config.resolution_config, HRNResolverConfig::Blip32));

// Invalid mode is rejected.
let toml_config = format!("{}\n[hrn]\nmode = \"bogus\"\n", base_config);
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
let err = load_config(&args_config).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);

// Invalid DNS server address is rejected.
let toml_config =
format!("{}\n[hrn]\ndns_server_address = \"not-a-socket-address\"\n", base_config);
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
let err = load_config(&args_config).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
}