Skip to content

Commit 77aa6be

Browse files
committed
Allow configuring Human-Readable Name resolution
Exposes LDK Node's new `HumanReadableNamesConfig` via a new `[hrn]` section in `config.toml`. Users can pick between resolving BIP 353 names locally over DNS (`mode = "dns"`, the default) or via bLIP-32 (`mode = "blip32"`), and, when using DNS, optionally run as an HRN resolution service for the rest of the network. Defaults match LDK Node's: resolve via DNS against `8.8.8.8:53` with the resolution service disabled (enabling it requires the node to be announceable so others can route resolution requests to us). Co-Authored-By: HAL 9000
1 parent 2869459 commit 77aa6be

4 files changed

Lines changed: 244 additions & 0 deletions

File tree

contrib/ldk-server-config.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,16 @@ poll_metrics_interval = 60 # The polling interval for metrics in seconds.
103103
[tor]
104104
# Only connections to OnionV3 peers will be made via this proxy; other connections (IPv4 peers, Electrum server) will not be routed over Tor.
105105
#proxy_address = "127.0.0.1:9050" # Tor daemon SOCKS proxy address.
106+
107+
# Human-Readable Names (BIP 353) resolution
108+
[hrn]
109+
# Resolution method: "dns" (resolve locally via a DNS server) or "blip32" (ask other
110+
# nodes to resolve for us via bLIP-32). Defaults to "dns".
111+
#mode = "dns"
112+
# DNS server used when `mode = "dns"`. Defaults to 8.8.8.8:53 (Google Public DNS). The
113+
# port defaults to 53 if omitted (e.g., "1.1.1.1" is treated as "1.1.1.1:53").
114+
#dns_server_address = "8.8.8.8:53"
115+
# When set to true (and `mode = "dns"`), also offer HRN resolution to the rest of the
116+
# network over Onion Messages. Requires the node to be announceable so resolution
117+
# requests can be routed to us. Defaults to false.
118+
#enable_resolution_service = false

docs/configuration.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,23 @@ are not proxied. This does not set up inbound connections, to make your node rea
123123
hidden service, you need to configure Tor separately. See the [Tor guide](tor.md) for the
124124
full setup.
125125

126+
### `[hrn]`
127+
128+
Configures how the node resolves [BIP 353](https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki)
129+
Human-Readable Names (e.g., `₿alice@example.com`) to Lightning payment destinations.
130+
131+
Two resolution methods are supported via the `mode` field:
132+
133+
- **`"dns"`** (default) - Resolve names locally using a DNS server. The server is set via
134+
`dns_server_address` (default: `8.8.8.8:53`, Google Public DNS). The port defaults to
135+
`53` if omitted. When `enable_resolution_service = true`, the node additionally offers
136+
HRN resolution to the rest of the network over Onion Messages. This requires the node
137+
to be announceable so resolution requests can be routed to it, and is therefore
138+
disabled by default.
139+
- **`"blip32"`** - Ask other nodes to resolve names on our behalf via
140+
[bLIP-32](https://github.com/lightning/blips/blob/master/blip-0032.md). `dns_server_address`
141+
and `enable_resolution_service` only apply in `"dns"` mode and are rejected here.
142+
126143
## Storage Layout
127144

128145
```

ldk-server/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ fn main() {
141141
ldk_node_config.listening_addresses = config_file.listening_addrs;
142142
ldk_node_config.announcement_addresses = config_file.announcement_addrs;
143143
ldk_node_config.network = config_file.network;
144+
ldk_node_config.hrn_config = config_file.hrn_config;
144145

145146
let mut builder = Builder::from_config(ldk_node_config);
146147
builder.set_log_facade_logger();

ldk-server/src/util/config.rs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use std::{fs, io};
1515
use clap::Parser;
1616
use ldk_node::bitcoin::secp256k1::PublicKey;
1717
use ldk_node::bitcoin::Network;
18+
use ldk_node::config::{HRNResolverConfig, HumanReadableNamesConfig};
1819
use ldk_node::lightning::ln::msgs::SocketAddress;
1920
use ldk_node::lightning::routing::gossip::NodeAlias;
2021
use ldk_node::liquidity::LSPS2ServiceConfig;
@@ -61,6 +62,7 @@ pub struct Config {
6162
pub metrics_username: Option<String>,
6263
pub metrics_password: Option<String>,
6364
pub tor_config: Option<TorConfig>,
65+
pub hrn_config: HumanReadableNamesConfig,
6466
}
6567

6668
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -114,6 +116,7 @@ struct ConfigBuilder {
114116
metrics_username: Option<String>,
115117
metrics_password: Option<String>,
116118
tor_proxy_address: Option<String>,
119+
hrn: Option<HrnTomlConfig>,
117120
}
118121

119122
impl ConfigBuilder {
@@ -180,6 +183,10 @@ impl ConfigBuilder {
180183
if let Some(tor) = toml.tor {
181184
self.tor_proxy_address = Some(tor.proxy_address)
182185
}
186+
187+
if let Some(hrn) = toml.hrn {
188+
self.hrn = Some(hrn);
189+
}
183190
}
184191

185192
fn merge_args(&mut self, args: &ArgsConfig) {
@@ -402,6 +409,11 @@ impl ConfigBuilder {
402409
})
403410
.transpose()?;
404411

412+
let hrn_config = match self.hrn {
413+
Some(hrn) => HumanReadableNamesConfig::try_from(hrn)?,
414+
None => HumanReadableNamesConfig::default(),
415+
};
416+
405417
Ok(Config {
406418
network,
407419
listening_addrs,
@@ -422,6 +434,7 @@ impl ConfigBuilder {
422434
metrics_username,
423435
metrics_password,
424436
tor_config: tor_proxy_address.map(|proxy_address| TorConfig { proxy_address }),
437+
hrn_config,
425438
})
426439
}
427440
}
@@ -439,6 +452,7 @@ pub struct TomlConfig {
439452
tls: Option<TomlTlsConfig>,
440453
metrics: Option<MetricsTomlConfig>,
441454
tor: Option<TomlTorConfig>,
455+
hrn: Option<HrnTomlConfig>,
442456
}
443457

444458
#[derive(Deserialize, Serialize)]
@@ -505,6 +519,93 @@ struct TomlTorConfig {
505519
proxy_address: String,
506520
}
507521

522+
#[derive(Deserialize, Serialize)]
523+
struct HrnTomlConfig {
524+
mode: Option<String>,
525+
dns_server_address: Option<String>,
526+
enable_resolution_service: Option<bool>,
527+
}
528+
529+
impl TryFrom<HrnTomlConfig> for HumanReadableNamesConfig {
530+
type Error = io::Error;
531+
532+
fn try_from(value: HrnTomlConfig) -> Result<Self, Self::Error> {
533+
let HrnTomlConfig { mode, dns_server_address, enable_resolution_service } = value;
534+
535+
let resolution_config = match mode.as_deref() {
536+
None | Some("dns") => {
537+
// Start from LDK Node's DNS defaults so we don't have to hardcode them, but fall
538+
// back to explicit values if the upstream default ever stops being `Dns`.
539+
let (mut dns_server_address_val, mut enable_hrn_resolution_service) =
540+
if let HRNResolverConfig::Dns {
541+
dns_server_address,
542+
enable_hrn_resolution_service,
543+
} = HumanReadableNamesConfig::default().resolution_config
544+
{
545+
(dns_server_address, enable_hrn_resolution_service)
546+
} else {
547+
(
548+
SocketAddress::from_str("8.8.8.8:53")
549+
.expect("`8.8.8.8:53` is a valid socket address"),
550+
false,
551+
)
552+
};
553+
554+
if let Some(addr) = dns_server_address.as_deref() {
555+
dns_server_address_val = parse_dns_server_address(addr)?;
556+
}
557+
if let Some(enable) = enable_resolution_service {
558+
enable_hrn_resolution_service = enable;
559+
}
560+
561+
HRNResolverConfig::Dns {
562+
dns_server_address: dns_server_address_val,
563+
enable_hrn_resolution_service,
564+
}
565+
},
566+
Some("blip32") => {
567+
if dns_server_address.is_some() {
568+
return Err(io::Error::new(
569+
io::ErrorKind::InvalidInput,
570+
"`hrn.dns_server_address` only applies when `hrn.mode = \"dns\"`"
571+
.to_string(),
572+
));
573+
}
574+
if enable_resolution_service.is_some() {
575+
return Err(io::Error::new(
576+
io::ErrorKind::InvalidInput,
577+
"`hrn.enable_resolution_service` only applies when `hrn.mode = \"dns\"`"
578+
.to_string(),
579+
));
580+
}
581+
HRNResolverConfig::Blip32
582+
},
583+
Some(other) => {
584+
return Err(io::Error::new(
585+
io::ErrorKind::InvalidInput,
586+
format!("Invalid HRN mode '{}' configured; expected 'dns' or 'blip32'", other),
587+
))
588+
},
589+
};
590+
591+
Ok(HumanReadableNamesConfig { resolution_config })
592+
}
593+
}
594+
595+
/// Parses a DNS server address, falling back to port 53 if the user omitted the port.
596+
fn parse_dns_server_address(addr: &str) -> io::Result<SocketAddress> {
597+
if let Ok(sa) = SocketAddress::from_str(addr) {
598+
return Ok(sa);
599+
}
600+
let with_default_port = format!("{}:53", addr);
601+
SocketAddress::from_str(&with_default_port).map_err(|e| {
602+
io::Error::new(
603+
io::ErrorKind::InvalidInput,
604+
format!("Invalid HRN DNS server address configured: {}", e),
605+
)
606+
})
607+
}
608+
508609
#[derive(Deserialize, Serialize)]
509610
struct LiquidityConfig {
510611
lsps2_client: Option<LSPSClientTomlConfig>,
@@ -936,6 +1037,7 @@ mod tests {
9361037
tor_config: Some(TorConfig {
9371038
proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(),
9381039
}),
1040+
hrn_config: HumanReadableNamesConfig::default(),
9391041
};
9401042

9411043
assert_eq!(config.listening_addrs, expected.listening_addrs);
@@ -1241,6 +1343,7 @@ mod tests {
12411343
metrics_username: None,
12421344
metrics_password: None,
12431345
tor_config: None,
1346+
hrn_config: HumanReadableNamesConfig::default(),
12441347
};
12451348

12461349
assert_eq!(config.listening_addrs, expected.listening_addrs);
@@ -1350,6 +1453,7 @@ mod tests {
13501453
tor_config: Some(TorConfig {
13511454
proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(),
13521455
}),
1456+
hrn_config: HumanReadableNamesConfig::default(),
13531457
};
13541458

13551459
assert_eq!(config.listening_addrs, expected.listening_addrs);
@@ -1501,4 +1605,113 @@ mod tests {
15011605
let err = result.unwrap_err();
15021606
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
15031607
}
1608+
1609+
#[test]
1610+
fn test_hrn_config() {
1611+
let storage_path = std::env::temp_dir();
1612+
let config_file_name = "test_hrn_config.toml";
1613+
1614+
let base_config = r#"
1615+
[node]
1616+
network = "regtest"
1617+
1618+
[bitcoind]
1619+
rpc_address = "127.0.0.1:8332"
1620+
rpc_user = "bitcoind-testuser"
1621+
rpc_password = "bitcoind-testpassword"
1622+
1623+
[liquidity.lsps2_service]
1624+
advertise_service = false
1625+
channel_opening_fee_ppm = 1000
1626+
channel_over_provisioning_ppm = 500000
1627+
min_channel_opening_fee_msat = 10000000
1628+
min_channel_lifetime = 4320
1629+
max_client_to_self_delay = 1440
1630+
min_payment_size_msat = 10000000
1631+
max_payment_size_msat = 25000000000
1632+
client_trusts_lsp = true
1633+
disable_client_reserve = false
1634+
"#;
1635+
1636+
let mut args_config = empty_args_config();
1637+
args_config.config_file =
1638+
Some(storage_path.join(config_file_name).to_string_lossy().to_string());
1639+
1640+
// Default: no `[hrn]` section -> DNS against 8.8.8.8:53, resolution service disabled.
1641+
fs::write(storage_path.join(config_file_name), base_config).unwrap();
1642+
let config = load_config(&args_config).unwrap();
1643+
match config.hrn_config.resolution_config {
1644+
HRNResolverConfig::Dns { dns_server_address, enable_hrn_resolution_service } => {
1645+
assert_eq!(dns_server_address, SocketAddress::from_str("8.8.8.8:53").unwrap());
1646+
assert!(!enable_hrn_resolution_service);
1647+
},
1648+
other => panic!("unexpected default HRN resolver config: {:?}", other),
1649+
}
1650+
1651+
// Custom DNS server address with resolution service enabled.
1652+
let toml_config = format!(
1653+
"{}\n[hrn]\ndns_server_address = \"1.1.1.1:53\"\nenable_resolution_service = true\n",
1654+
base_config
1655+
);
1656+
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
1657+
let config = load_config(&args_config).unwrap();
1658+
match config.hrn_config.resolution_config {
1659+
HRNResolverConfig::Dns { dns_server_address, enable_hrn_resolution_service } => {
1660+
assert_eq!(dns_server_address, SocketAddress::from_str("1.1.1.1:53").unwrap());
1661+
assert!(enable_hrn_resolution_service);
1662+
},
1663+
other => panic!("unexpected HRN resolver config: {:?}", other),
1664+
}
1665+
1666+
// Blip32 mode.
1667+
let toml_config = format!("{}\n[hrn]\nmode = \"blip32\"\n", base_config);
1668+
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
1669+
let config = load_config(&args_config).unwrap();
1670+
assert!(matches!(config.hrn_config.resolution_config, HRNResolverConfig::Blip32));
1671+
1672+
// Invalid mode is rejected.
1673+
let toml_config = format!("{}\n[hrn]\nmode = \"bogus\"\n", base_config);
1674+
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
1675+
let err = load_config(&args_config).unwrap_err();
1676+
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1677+
1678+
// Invalid DNS server address is rejected (contains chars disallowed in hostnames, so
1679+
// neither the as-is parse nor the `:53` fallback can accept it).
1680+
let toml_config =
1681+
format!("{}\n[hrn]\ndns_server_address = \"invalid@address\"\n", base_config);
1682+
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
1683+
let err = load_config(&args_config).unwrap_err();
1684+
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1685+
1686+
// DNS server address without an explicit port defaults to port 53.
1687+
let toml_config = format!("{}\n[hrn]\ndns_server_address = \"1.1.1.1\"\n", base_config);
1688+
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
1689+
let config = load_config(&args_config).unwrap();
1690+
match config.hrn_config.resolution_config {
1691+
HRNResolverConfig::Dns { dns_server_address, .. } => {
1692+
assert_eq!(dns_server_address, SocketAddress::from_str("1.1.1.1:53").unwrap());
1693+
},
1694+
other => panic!("unexpected HRN resolver config: {:?}", other),
1695+
}
1696+
1697+
// `blip32` mode combined with DNS-only settings is rejected so users aren't confused
1698+
// by settings that would silently have no effect.
1699+
let toml_config = format!(
1700+
"{}\n[hrn]\nmode = \"blip32\"\ndns_server_address = \"1.1.1.1:53\"\n",
1701+
base_config
1702+
);
1703+
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
1704+
let err = load_config(&args_config).unwrap_err();
1705+
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1706+
assert!(err.to_string().contains("dns_server_address"));
1707+
1708+
let toml_config = format!(
1709+
"{}\n[hrn]\nmode = \"blip32\"\nenable_resolution_service = true\n",
1710+
base_config
1711+
);
1712+
fs::write(storage_path.join(config_file_name), &toml_config).unwrap();
1713+
let err = load_config(&args_config).unwrap_err();
1714+
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1715+
assert!(err.to_string().contains("enable_resolution_service"));
1716+
}
15041717
}

0 commit comments

Comments
 (0)