What changed, and why it matters
This commit changes the default setting so that new Lightning channels will use 'anchor outputs' whenever possible. Anchor outputs are a safer, more modern channel type, but they require the user to keep some on-chain bitcoin available to pay fees during an emergency force-close. To make sure users notice this new responsibility, the library now also forces every inbound channel request to be manually accepted. The change is a deliberate policy update, not a hidden bug, but it could surprise users who are not prepared to maintain the extra on-chain reserve.
Treat this as a behavior-changing release note rather than a vulnerability. Users upgrading should review their wallet's ability to provide on-chain UTXOs for anchor fee bumping and update their event handling to process `Event::OpenChannelRequest` for every inbound channel. Downstream tests that assume legacy channels should explicitly set `negotiate_anchors_zero_fee_htlc_tx = false`.
Security signals we found
Default channel type changed to anchor outputs, which alter fee-bumping assumptions
Inbound channels now require manual acceptance to ensure user awareness of reserve requirements
Documentation explicitly warns that users must maintain an on-chain reserve for force-close fee bumping
No cryptographic, memory-safety, or parsing vulnerability is present in the diff
Evidence from the diff
The patch flips the default of ChannelHandshakeConfig::negotiate_anchors_zero_fee_htlc_tx from false to true in lightning/src/util/config.rs. As a result, all new channels will negotiate anchor-zero-fee-HTLC-tx channels if the peer supports them, falling back to static_remote_key otherwise. The changelog notes that this is paired with the removal of UserConfig::manually_accept_inbound_channels and the unconditional generation of Event::OpenChannelRequest, requiring explicit acceptance so users can verify they hold enough UTXOs for fee bumping. Test code is updated to opt out of anchors where the existing tests assume non-anchor behavior.
Changed components
lightning/src/util/config.rslightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/channel_open_tests.rslightning/src/ln/channel_type_tests.rslightning/src/ln/functional_test_utils.rslightning-background-processor/src/lib.rsfuzz/src/chanmon_consistency.rsInspect captured patch +43 / −18
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 30b95c2..530f90e 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -710,8 +710,8 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
config.channel_config.forwarding_fee_proportional_millionths = 0;
config.channel_handshake_config.announce_for_forwarding = true;
config.reject_inbound_splices = false;
- if anchors {
- config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ if !anchors {
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
}
let network = Network::Bitcoin;
let best_block_timestamp = genesis_block(network).header.time;
@@ -760,8 +760,8 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
config.channel_config.forwarding_fee_proportional_millionths = 0;
config.channel_handshake_config.announce_for_forwarding = true;
config.reject_inbound_splices = false;
- if anchors {
- config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ if !anchors {
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
}
let mut monitors = new_hash_map();
diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs
index 659e281..94be014 100644
--- a/lightning-background-processor/src/lib.rs
+++ b/lightning-background-processor/src/lib.rs
@@ -2447,6 +2447,8 @@ mod tests {
));
let best_block = BestBlock::from_network(network);
let params = ChainParameters { network, best_block };
+ let mut config = UserConfig::default();
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
let manager = Arc::new(ChannelManager::new(
Arc::clone(&fee_estimator),
Arc::clone(&chain_monitor),
@@ -2457,7 +2459,7 @@ mod tests {
Arc::clone(&keys_manager),
Arc::clone(&keys_manager),
Arc::clone(&keys_manager),
- UserConfig::default(),
+ config,
params,
genesis_block.header.time,
));
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index be86d32..bcfd6fd 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -16026,7 +16026,8 @@ mod tests {
// Create Node A's channel pointing to Node B's pubkey
let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
- let config = UserConfig::default();
+ let mut config = UserConfig::default();
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap();
// Create Node B's channel by receiving Node A's open_channel message
@@ -16116,7 +16117,8 @@ mod tests {
let logger = TestLogger::new();
let node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
- let config = UserConfig::default();
+ let mut config = UserConfig::default();
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&fee_est, &&keys_provider, &&keys_provider, node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap();
let commitment_tx_fee_0_htlcs = commit_tx_fee_sat(chan.context.feerate_per_kw, 0, chan.funding.get_channel_type()) * 1000;
diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs
index 7c2a51e..f1336a0 100644
--- a/lightning/src/ln/channel_open_tests.rs
+++ b/lightning/src/ln/channel_open_tests.rs
@@ -172,8 +172,7 @@ fn test_0conf_limiting() {
#[test]
fn test_inbound_anchors_manual_acceptance() {
- let mut anchors_cfg = test_default_channel_config();
- anchors_cfg.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ let anchors_cfg = test_default_anchors_channel_config();
do_test_manual_inbound_accept_with_override(anchors_cfg, None);
}
@@ -191,9 +190,7 @@ fn test_inbound_anchors_config_overridden() {
update_overrides: None,
};
- let mut anchors_cfg = test_default_channel_config();
- anchors_cfg.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
-
+ let mut anchors_cfg = test_default_anchors_channel_config();
let accept_message = do_test_manual_inbound_accept_with_override(anchors_cfg, Some(overrides));
assert_eq!(accept_message.common_fields.max_htlc_value_in_flight_msat, 5_000_000);
assert_eq!(accept_message.common_fields.htlc_minimum_msat, 1_000);
@@ -1066,6 +1063,7 @@ pub fn test_user_configurable_csv_delay() {
pub fn test_accept_inbound_channel_config_override() {
let mut conf = UserConfig::default();
conf.channel_handshake_config.minimum_depth = 1;
+ conf.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
diff --git a/lightning/src/ln/channel_type_tests.rs b/lightning/src/ln/channel_type_tests.rs
index 13470d5..2b069a6 100644
--- a/lightning/src/ln/channel_type_tests.rs
+++ b/lightning/src/ln/channel_type_tests.rs
@@ -34,8 +34,10 @@ fn test_option_anchors_zero_fee_initial() {
let mut expected_type = ChannelTypeFeatures::only_static_remote_key();
expected_type.set_anchors_zero_fee_htlc_tx_required();
+ let mut start_cfg = UserConfig::default();
+ start_cfg.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
do_test_get_initial_channel_type(
- UserConfig::default(),
+ start_cfg,
InitFeatures::empty(),
ChannelTypeFeatures::only_static_remote_key(),
|cfg: &mut UserConfig| {
@@ -225,13 +227,15 @@ fn do_test_supports_channel_type(config: UserConfig, expected_channel_type: Chan
let node_id_b =
PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[2; 32]).unwrap());
+ let mut non_anchors_config = UserConfig::default();
+ non_anchors_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
// Assert that we get `static_remotekey` when no custom config is negotiated.
let channel_a = OutboundV1Channel::<&TestKeysInterface>::new(
&fee_estimator,
&&keys_provider,
&&keys_provider,
node_id_b,
- &channelmanager::provided_init_features(&UserConfig::default()),
+ &channelmanager::provided_init_features(&non_anchors_config),
10000000,
100000,
42,
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index f38245b..a160907 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -2028,7 +2028,7 @@ impl<
///
/// ## Opening Channels
///
-/// To an open a channel with a peer, call [`create_channel`]. This will initiate the process of
+/// To open a channel with a peer, call [`create_channel`]. This will initiate the process of
/// opening an outbound channel, which requires self-funding when handling
/// [`Event::FundingGenerationReady`].
///
@@ -5344,7 +5344,7 @@ impl<
/// using [`ChannelMonitorUpdateStatus::InProgress`]), the payment may be lost on restart. See
/// [`ChannelManager::list_recent_payments`] for more information.
///
- /// Routes are automatically found using the [`Router] provided on startup. To fix a route for a
+ /// Routes are automatically found using the [`Router`] provided on startup. To fix a route for a
/// particular payment, use [`Self::send_payment_with_route`] or match the [`PaymentId`] passed to
/// [`Router::find_route_with_id`].
///
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 165c08f..6ed6f5e 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -4534,6 +4534,7 @@ pub fn create_node_cfgs_with_node_id_message_router<'a>(
pub fn test_default_channel_config() -> UserConfig {
let mut default_config = UserConfig::default();
+ default_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
// Set cltv_expiry_delta slightly lower to keep the final CLTV values inside one byte in our
// tests so that our script-length checks don't fail (see ACCEPTED_HTLC_SCRIPT_WEIGHT).
default_config.channel_config.cltv_expiry_delta = MIN_CLTV_EXPIRY_DELTA;
diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs
index 1dec7bd..420fad6 100644
--- a/lightning/src/util/config.rs
+++ b/lightning/src/util/config.rs
@@ -178,7 +178,7 @@ pub struct ChannelHandshakeConfig {
/// counterparties that do not support the `anchors_zero_fee_htlc_tx` option; we will simply
/// fall back to a `static_remote_key` channel.
///
- /// Default value: `false` (This value is likely to change to `true` in the future.)
+ /// Default value: `true`
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::accept_inbound_channel`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel
@@ -252,7 +252,7 @@ impl Default for ChannelHandshakeConfig {
announce_for_forwarding: false,
commit_upfront_shutdown_pubkey: true,
their_channel_reserve_proportional_millionths: 10_000,
- negotiate_anchors_zero_fee_htlc_tx: false,
+ negotiate_anchors_zero_fee_htlc_tx: true,
negotiate_anchor_zero_fee_commitments: false,
our_max_accepted_htlcs: 50,
}
diff --git a/pending_changelog/4337-manual-channel-accept-default-anchors.txt b/pending_changelog/4337-manual-channel-accept-default-anchors.txt
new file mode 100644
index 0000000..999b249
--- /dev/null
+++ b/pending_changelog/4337-manual-channel-accept-default-anchors.txt
@@ -0,0 +1,18 @@
+# API Updates
+
+ * `ChannelHandshakeConfig::negotiate_anchors_zero_fee_htlc_tx`
+ now defaults to `true` (previously `false`). This means anchor output channels
+ will be negotiated by default for all new channels if the counterparty supports
+ it, requiring users to maintain an on-chain reserve for fee bumping in the
+ event of force-closes.
+
+ * All inbound channels now require manual acceptance.
+ `UserConfig::manually_accept_inbound_channels` has been removed, and
+ `Event::OpenChannelRequest` will now always be generated for inbound channel
+ requests. Users must handle this event and call either
+ `ChannelManager::accept_inbound_channel` (or
+ `accept_inbound_channel_from_trusted_peer_0conf` for zero-conf channels) to
+ accept the channel, or `ChannelManager::force_close_broadcasting_latest_txn`
+ to reject it. This ensures users can verify they have sufficient on-chain
+ funds before accepting channels with anchor outputs.
+
Why this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.