Skip to content
Merged
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
7 changes: 7 additions & 0 deletions contrib/ldk-server-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ 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"
#enable_zero_fee_commitments = false # Enable zero-fee commitment channels (default: false)
#anchor_per_channel_reserve_sats = 25000 # On-chain sats reserved per anchor channel for fee-bumping (default: 25000)
# WARNING: setting `anchor_per_channel_reserve_sats = 0` allows inbound anchor channels
# (e.g. LSP-funded LSPS1/LSPS2 channels) with no on-chain balance. Anchor-channel HTLC
# claim transactions are signed with zero fee and need wallet inputs to confirm. With
# no on-chain funds, in-flight HTLC value can be lost if the counterparty claims via
# the other path first. If enable_zero_fee_commitments uses a zero-fee commitment,
# a zero reserve on an empty wallet means a force-close cannot confirm until funds arrive.

# Forwarding history: "detailed" retains individual records for the current and previous hour,
# then aggregates them into hourly channel-pair statistics and removes the individual records.
Expand Down
27 changes: 26 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ on macOS).

Core node settings: which Bitcoin network to use, Lightning peer listening and announcement
addresses, the gRPC bind address, node alias, optional Rapid Gossip Sync / pathfinding
scores URLs, the async payments role, and zero-fee commitment channels.
scores URLs, the async payments role, zero-fee commitment channels, and the per-channel
anchor reserve.

Set `async_payments_role = "client"` to ask peers to hold HTLCs where possible, allowing
this node to go offline. Set `async_payments_role = "server"` to hold async payment HTLCs
Expand All @@ -64,6 +65,30 @@ falling back to other channel types supported by the peer. Enabling this setting
source that supports Bitcoin Core's `submitpackage` RPC, and relays TRUC, P2A, and ephemeral
dust. The default is `false`.

Set `anchor_per_channel_reserve_sats` to the amount of on-chain funds, in satoshis, that LDK
Comment thread
frnandu marked this conversation as resolved.
keeps available per anchor channel to fee-bump the commitment and closing transactions. The
default is `25000`, matching LDK Node. The reserve scales with the number of anchor channels,
reduces the spendable on-chain balance, is kept back from send-all transactions, and also applies
to outbound channel opens. Lowering it on a node with existing anchor channels makes some of
their previously reserved funds withdrawable.

Inbound anchor channels are rejected while the node's spendable on-chain balance is below the
required reserve, which blocks LSP-funded channels (such as LSPS1 purchases or LSPS2 JIT
channels) on a wallet that has not been funded yet. Set the value to `0` to accept inbound
anchor channels with zero on-chain balance while keeping LDK's fee-bump handling enabled for
funds that arrive later. This differs from LDK Node's
`trusted_peers_no_reserve` (not currently configurable in LDK Server), which additionally
makes LDK rely on the peer to confirm channel closing transactions.

> **Warning:** Anchor-channel HTLC claim transactions are signed with zero fee and need wallet
> inputs to confirm. With no on-chain funds, in-flight HTLC value can be lost if the counterparty
> claims via the other path first. A zero (or very low) reserve can also delay an urgent channel
> close. If `enable_zero_fee_commitments = true` results in a zero-fee commitment, a zero reserve
> on an empty wallet means a force-close cannot confirm at all until funds arrive.

The same setting is available as `--node-anchor-per-channel-reserve-sats` or
`LDK_SERVER_NODE_ANCHOR_PER_CHANNEL_RESERVE_SATS`.

Set `forwarded_payment_tracking_mode = "detailed"` to store individual
forwards. LDK Node retains these records for the current and previous one-hour buckets, then
aggregates them into hourly channel-pair statistics and removes the individual records.
Expand Down
2 changes: 2 additions & 0 deletions ldk-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ fn main() {
ldk_node_config.hrn_config = config_file.hrn_config;
ldk_node_config.anchor_channels_config.enable_zero_fee_commitments =
config_file.enable_zero_fee_commitments;
ldk_node_config.anchor_channels_config.per_channel_reserve_sats =
config_file.anchor_per_channel_reserve_sats;
// The server exposes receive-for-hash APIs, so unknown inbound BOLT11 HTLCs
// must emit PaymentClaimable instead of being failed back.
ldk_node_config.manually_handle_unknown_bolt11_payments = true;
Expand Down
86 changes: 85 additions & 1 deletion ldk-server/src/util/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ use clap::Parser;
use ldk_node::bitcoin::secp256k1::PublicKey;
use ldk_node::bitcoin::Network;
use ldk_node::config::{
AsyncPaymentsRole, ForwardedPaymentTrackingMode, HRNResolverConfig, HumanReadableNamesConfig,
AnchorChannelsConfig, AsyncPaymentsRole, ForwardedPaymentTrackingMode, HRNResolverConfig,
HumanReadableNamesConfig,
};
use ldk_node::lightning::ln::msgs::SocketAddress;
use ldk_node::lightning::routing::gossip::NodeAlias;
Expand Down Expand Up @@ -78,6 +79,7 @@ pub struct Config {
pub probing_config: Option<ProbingConfig>,
pub async_payments_role: Option<AsyncPaymentsRole>,
pub enable_zero_fee_commitments: bool,
pub anchor_per_channel_reserve_sats: u64,
pub forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode,
pub metrics_enabled: bool,
pub poll_metrics_interval: Option<u64>,
Expand Down Expand Up @@ -173,6 +175,7 @@ struct ConfigBuilder {
probing: Option<ProbingTomlConfig>,
async_payments_role: Option<String>,
enable_zero_fee_commitments: Option<bool>,
anchor_per_channel_reserve_sats: Option<u64>,
forwarded_payment_tracking_mode: Option<String>,
metrics_enabled: Option<bool>,
poll_metrics_interval: Option<u64>,
Expand All @@ -199,6 +202,8 @@ impl ConfigBuilder {
node.async_payments_role.or(self.async_payments_role.clone());
self.enable_zero_fee_commitments =
node.enable_zero_fee_commitments.or(self.enable_zero_fee_commitments);
self.anchor_per_channel_reserve_sats =
node.anchor_per_channel_reserve_sats.or(self.anchor_per_channel_reserve_sats);
self.forwarded_payment_tracking_mode = node
.forwarded_payment_tracking_mode
.or(self.forwarded_payment_tracking_mode.clone());
Expand Down Expand Up @@ -355,6 +360,10 @@ impl ConfigBuilder {
self.enable_zero_fee_commitments = Some(enable_zero_fee_commitments);
}

if let Some(anchor_per_channel_reserve_sats) = args.node_anchor_per_channel_reserve_sats {
self.anchor_per_channel_reserve_sats = Some(anchor_per_channel_reserve_sats);
}

if let Some(mode) = &args.node_forwarded_payment_tracking_mode {
self.forwarded_payment_tracking_mode = Some(mode.clone());
}
Expand Down Expand Up @@ -738,6 +747,9 @@ impl ConfigBuilder {
probing_config,
async_payments_role,
enable_zero_fee_commitments: self.enable_zero_fee_commitments.unwrap_or(false),
anchor_per_channel_reserve_sats: self
.anchor_per_channel_reserve_sats
.unwrap_or(AnchorChannelsConfig::default().per_channel_reserve_sats),
forwarded_payment_tracking_mode,
metrics_enabled,
poll_metrics_interval,
Expand Down Expand Up @@ -778,6 +790,7 @@ struct NodeConfig {
pathfinding_scores_source_url: Option<String>,
async_payments_role: Option<String>,
enable_zero_fee_commitments: Option<bool>,
anchor_per_channel_reserve_sats: Option<u64>,
forwarded_payment_tracking_mode: Option<String>,
rgs_server_url: Option<String>,
}
Expand Down Expand Up @@ -1318,6 +1331,13 @@ pub struct ArgsConfig {
)]
node_enable_zero_fee_commitments: Option<bool>,

#[arg(
long,
env = "LDK_SERVER_NODE_ANCHOR_PER_CHANNEL_RESERVE_SATS",
help = "On-chain reserve in satoshis that LDK keeps available to fee-bump anchor channels. Defaults to 25000. Set to 0 to accept inbound anchor channels without any on-chain balance, at the risk of being unable to urgently close a channel while the wallet is unfunded."
)]
node_anchor_per_channel_reserve_sats: Option<u64>,

#[arg(
long,
env = "LDK_SERVER_NODE_FORWARDED_PAYMENT_TRACKING_MODE",
Expand Down Expand Up @@ -1508,6 +1528,7 @@ mod tests {
rgs_server_url = "https://rapidsync.lightningdevkit.org/snapshot/v2/"
async_payments_role = "client"
enable_zero_fee_commitments = true
anchor_per_channel_reserve_sats = 50000

[tls]
cert_path = "/path/to/tls.crt"
Expand Down Expand Up @@ -1574,6 +1595,7 @@ mod tests {
pathfinding_scores_source_url: Some(String::from("https://example.com/")),
node_async_payments_role: Some(String::from("server")),
node_enable_zero_fee_commitments: Some(false),
node_anchor_per_channel_reserve_sats: Some(10000),
node_forwarded_payment_tracking_mode: None,
probing_strategy: None,
probing_top_node_count: None,
Expand Down Expand Up @@ -1616,6 +1638,7 @@ mod tests {
pathfinding_scores_source_url: None,
node_async_payments_role: None,
node_enable_zero_fee_commitments: None,
node_anchor_per_channel_reserve_sats: None,
node_forwarded_payment_tracking_mode: None,
probing_strategy: None,
probing_top_node_count: None,
Expand Down Expand Up @@ -1736,6 +1759,7 @@ mod tests {
probing_config: None,
async_payments_role: Some(AsyncPaymentsRole::Client),
enable_zero_fee_commitments: true,
anchor_per_channel_reserve_sats: 50000,
forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode::default(),
metrics_enabled: false,
poll_metrics_interval: None,
Expand Down Expand Up @@ -1764,6 +1788,10 @@ mod tests {
assert_eq!(config.pathfinding_scores_source_url, expected.pathfinding_scores_source_url);
assert!(matches!(config.async_payments_role, Some(AsyncPaymentsRole::Client)));
assert_eq!(config.enable_zero_fee_commitments, expected.enable_zero_fee_commitments);
assert_eq!(
config.anchor_per_channel_reserve_sats,
expected.anchor_per_channel_reserve_sats
);
assert_eq!(
config.forwarded_payment_tracking_mode,
expected.forwarded_payment_tracking_mode
Expand Down Expand Up @@ -2713,6 +2741,7 @@ mod tests {
probing_config: None,
async_payments_role: Some(AsyncPaymentsRole::Server),
enable_zero_fee_commitments: false,
anchor_per_channel_reserve_sats: 10000,
forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode::default(),
metrics_enabled: false,
poll_metrics_interval: None,
Expand All @@ -2738,6 +2767,10 @@ mod tests {
assert_eq!(config.pathfinding_scores_source_url, expected.pathfinding_scores_source_url);
assert!(matches!(config.async_payments_role, Some(AsyncPaymentsRole::Server)));
assert_eq!(config.enable_zero_fee_commitments, expected.enable_zero_fee_commitments);
assert_eq!(
config.anchor_per_channel_reserve_sats,
expected.anchor_per_channel_reserve_sats
);
assert_eq!(
config.forwarded_payment_tracking_mode,
expected.forwarded_payment_tracking_mode
Expand Down Expand Up @@ -2845,6 +2878,7 @@ mod tests {
probing_config: None,
async_payments_role: Some(AsyncPaymentsRole::Server),
enable_zero_fee_commitments: false,
anchor_per_channel_reserve_sats: 10000,
forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode::default(),
metrics_enabled: false,
poll_metrics_interval: None,
Expand Down Expand Up @@ -2874,6 +2908,10 @@ mod tests {
assert_eq!(config.pathfinding_scores_source_url, expected.pathfinding_scores_source_url);
assert!(matches!(config.async_payments_role, Some(AsyncPaymentsRole::Server)));
assert_eq!(config.enable_zero_fee_commitments, expected.enable_zero_fee_commitments);
assert_eq!(
config.anchor_per_channel_reserve_sats,
expected.anchor_per_channel_reserve_sats
);
assert_eq!(
config.forwarded_payment_tracking_mode,
expected.forwarded_payment_tracking_mode
Expand Down Expand Up @@ -3367,6 +3405,52 @@ mod tests {
}
}

#[test]
fn test_accepts_anchor_per_channel_reserve_arg() {
for value in ["0", "25000"] {
let args_config = ArgsConfig::try_parse_from([
"ldk-server",
"--node-anchor-per-channel-reserve-sats",
value,
])
.unwrap();

assert_eq!(
args_config.node_anchor_per_channel_reserve_sats,
Some(value.parse::<u64>().unwrap())
);
}
}

#[test]
Comment thread
frnandu marked this conversation as resolved.
fn test_anchor_per_channel_reserve_defaults_to_25k_sats() {
let storage_path = std::env::temp_dir();
let config_file_name = "test_anchor_per_channel_reserve_defaults_to_25k_sats.toml";

let toml_config = format!(
r#"
[node]
network = "regtest"

[bitcoind]
rpc_address = "127.0.0.1:8332"
rpc_user = "bitcoind-testuser"
rpc_password = "bitcoind-testpassword"
{}
"#,
lsps2_service_config_for_feature()
);

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.anchor_per_channel_reserve_sats, 25_000);
}

#[test]
fn test_probing_args_override_strategy_specific_file_options() {
let mut builder = ConfigBuilder {
Expand Down
Loading