Add 0-reserve to `accept_inbound_channel_from_trusted_peer`
What changed, and why it matters
This commit renames and expands a special Lightning channel-acceptance API. Previously, users could manually accept an inbound channel from a trusted peer and treat it as confirmed immediately (zero-conf). Now the same API also allows setting the counterparty's required channel reserve to zero. A zero reserve means the peer can spend their entire balance and force-close the channel at no cost to themselves, which removes a key financial deterrent against cheating. The change is explicitly documented as dangerous and only appropriate for trusted peers, but it introduces a new risky option that did not exist before.
Treat this as a feature addition with significant security caveats rather than a vulnerability fix. Review downstream callers of `accept_inbound_channel_from_trusted_peer` to ensure they do not select `ZeroReserve` or `ZeroConfZeroReserve` for peers that are not fully trusted, and verify that user-facing documentation and API naming make the risks clear. No urgent patch is required, but consider whether the enum should be marked `non_exhaustive` or gated further to prevent accidental misuse.
Security signals we found
New API option explicitly removes counterparty channel reserve (zero-reserve), eliminating the economic penalty for revoked commitment broadcasts
Documentation warns that zero-reserve lets the counterparty force-close with a revoked commitment 'for free'
Safety checks in channel reserve validation are relaxed only for the exact zero value, not for arbitrary low reserves
Existing zero-conf behavior is preserved and combined with the new zero-reserve option
No CVE, advisory, or vendor security disclosure is present in the supplied materials
Evidence from the diff
The patch replaces the boolean is_0conf parameter and the accept_inbound_channel_from_trusted_peer_0conf method with a new TrustedChannelFeatures enum and accept_inbound_channel_from_trusted_peer. The enum offers ZeroConf, ZeroReserve, and ZeroConfZeroReserve. When ZeroReserve is selected, InboundV1Channel::new passes true for the zero-reserve flag into channel reserve selection logic, and the protocol-level safety checks that normally reject a reserve below the dust limit are bypassed only when the reserve is exactly zero. The commit updates call sites and documentation throughout the codebase. It does not fix a vulnerability; it adds a new, opt-in, high-trust feature.
Changed components
lightning/src/ln/channelmanager.rslightning/src/ln/channel.rslightning/src/util/config.rslightning/src/events/mod.rslightning/src/ln/async_signer_tests.rslightning/src/ln/chanmon_update_fail_tests.rslightning/src/ln/channel_open_tests.rslightning/src/ln/channel_type_tests.rslightning/src/ln/functional_test_utils.rslightning/src/ln/priv_short_conf_tests.rslightning-liquidity/tests/lsps2_integration_tests.rsInspect captured patch +134 / −73
diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs
index b8a4a5a..fbff2ea 100644
--- a/lightning-liquidity/tests/lsps2_integration_tests.rs
+++ b/lightning-liquidity/tests/lsps2_integration_tests.rs
@@ -9,7 +9,9 @@ use common::{
use lightning::events::{ClosureReason, Event};
use lightning::get_event_msg;
-use lightning::ln::channelmanager::{OptionalBolt11PaymentParams, PaymentId};
+use lightning::ln::channelmanager::{
+ OptionalBolt11PaymentParams, PaymentId, TrustedChannelFeatures,
+};
use lightning::ln::functional_test_utils::*;
use lightning::ln::msgs::BaseMessageHandler;
use lightning::ln::msgs::ChannelMessageHandler;
@@ -1503,10 +1505,11 @@ fn create_channel_with_manual_broadcast(
Event::OpenChannelRequest { temporary_channel_id, .. } => {
client_node
.node
- .accept_inbound_channel_from_trusted_peer_0conf(
+ .accept_inbound_channel_from_trusted_peer(
&temporary_channel_id,
&service_node_id,
user_channel_id,
+ TrustedChannelFeatures::ZeroConf,
None,
)
.unwrap();
diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs
index 011b7f5..73c4a39 100644
--- a/lightning/src/events/mod.rs
+++ b/lightning/src/events/mod.rs
@@ -1657,7 +1657,7 @@ pub enum Event {
/// Furthermore, note that if [`ChannelTypeFeatures::supports_zero_conf`] returns true on this type,
/// the resulting [`ChannelManager`] will not be readable by versions of LDK prior to
/// 0.0.107. Channels setting this type also need to get manually accepted via
- /// [`crate::ln::channelmanager::ChannelManager::accept_inbound_channel_from_trusted_peer_0conf`],
+ /// [`crate::ln::channelmanager::ChannelManager::accept_inbound_channel_from_trusted_peer`],
/// or will be rejected otherwise.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs
index 8d47b6f..f238c1d 100644
--- a/lightning/src/ln/async_signer_tests.rs
+++ b/lightning/src/ln/async_signer_tests.rs
@@ -22,7 +22,7 @@ use crate::events::{ClosureReason, Event};
use crate::ln::chan_utils::ClosingTransaction;
use crate::ln::channel::DISCONNECT_PEER_AWAITING_RESPONSE_TICKS;
use crate::ln::channel_state::{ChannelDetails, ChannelShutdownState};
-use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder};
+use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder, TrustedChannelFeatures};
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, ErrorAction, MessageSendEvent};
use crate::ln::outbound_payment::RecipientOnionFields;
use crate::ln::{functional_test_utils::*, msgs};
@@ -78,10 +78,11 @@ fn do_test_open_channel(zero_conf: bool) {
Event::OpenChannelRequest { temporary_channel_id, .. } => {
nodes[1]
.node
- .accept_inbound_channel_from_trusted_peer_0conf(
+ .accept_inbound_channel_from_trusted_peer(
temporary_channel_id,
&node_a_id,
0,
+ TrustedChannelFeatures::ZeroConf,
None,
)
.expect("Unable to accept inbound zero-conf channel");
@@ -383,10 +384,11 @@ fn do_test_funding_signed_0conf(signer_ops: Vec<SignerOp>) {
Event::OpenChannelRequest { temporary_channel_id, .. } => {
nodes[1]
.node
- .accept_inbound_channel_from_trusted_peer_0conf(
+ .accept_inbound_channel_from_trusted_peer(
temporary_channel_id,
&node_a_id,
0,
+ TrustedChannelFeatures::ZeroConf,
None,
)
.expect("Unable to accept inbound zero-conf channel");
diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs
index 0d8a4a0..9c81b90 100644
--- a/lightning/src/ln/chanmon_update_fail_tests.rs
+++ b/lightning/src/ln/chanmon_update_fail_tests.rs
@@ -19,7 +19,7 @@ use crate::chain::transaction::OutPoint;
use crate::chain::{ChannelMonitorUpdateStatus, Confirm, Listen, Watch};
use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaymentPurpose};
use crate::ln::channel::AnnouncementSigsState;
-use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder};
+use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder, TrustedChannelFeatures};
use crate::ln::msgs;
use crate::ln::msgs::{
BaseMessageHandler, ChannelMessageHandler, MessageSendEvent, RoutingMessageHandler,
@@ -3241,7 +3241,13 @@ fn do_test_outbound_reload_without_init_mon(use_0conf: bool) {
if use_0conf {
nodes[1]
.node
- .accept_inbound_channel_from_trusted_peer_0conf(&chan_id, &node_a_id, 0, None)
+ .accept_inbound_channel_from_trusted_peer(
+ &chan_id,
+ &node_a_id,
+ 0,
+ TrustedChannelFeatures::ZeroConf,
+ None,
+ )
.unwrap();
} else {
nodes[1].node.accept_inbound_channel(&chan_id, &node_a_id, 0, None).unwrap();
@@ -3350,7 +3356,13 @@ fn do_test_inbound_reload_without_init_mon(use_0conf: bool, lock_commitment: boo
if use_0conf {
nodes[1]
.node
- .accept_inbound_channel_from_trusted_peer_0conf(&chan_id, &node_a_id, 0, None)
+ .accept_inbound_channel_from_trusted_peer(
+ &chan_id,
+ &node_a_id,
+ 0,
+ TrustedChannelFeatures::ZeroConf,
+ None,
+ )
.unwrap();
} else {
nodes[1].node.accept_inbound_channel(&chan_id, &node_a_id, 0, None).unwrap();
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 5370920..0b6d173 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -52,7 +52,7 @@ use crate::ln::channel_state::{
use crate::ln::channelmanager::{
self, BlindedFailure, ChannelReadyOrder, FundingConfirmedMessage, HTLCFailureMsg,
HTLCPreviousHopData, HTLCSource, OpenChannelMessage, PaymentClaimDetails, PendingHTLCInfo,
- PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT,
+ PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, TrustedChannelFeatures, BREAKDOWN_TIMEOUT,
MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA,
};
use crate::ln::funding::{
@@ -3693,7 +3693,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
config: &'a UserConfig,
current_chain_height: u32,
logger: &'a L,
- is_0conf: bool,
+ trusted_channel_features: Option<TrustedChannelFeatures>,
our_funding_satoshis: u64,
counterparty_pubkeys: ChannelPublicKeys,
channel_type: ChannelTypeFeatures,
@@ -3780,7 +3780,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
}
- if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS {
+ if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS && holder_selected_channel_reserve_satoshis != 0 {
// Protocol level safety check in place, although it should never happen because
// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`
return Err(ChannelError::close(format!("Suitable channel reserve not found. remote_channel_reserve was ({}). dust_limit_satoshis is ({}).", holder_selected_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS)));
@@ -3792,7 +3792,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
log_debug!(logger, "channel_reserve_satoshis ({}) is smaller than our dust limit ({}). We can broadcast stale states without any risk, implying this channel is very insecure for our counterparty.",
msg_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS);
}
- if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis {
+ if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis && holder_selected_channel_reserve_satoshis != 0 {
return Err(ChannelError::close(format!("Dust limit ({}) too high for the channel reserve we require the remote to keep ({})", open_channel_fields.dust_limit_satoshis, holder_selected_channel_reserve_satoshis)));
}
@@ -3841,7 +3841,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
let mut secp_ctx = Secp256k1::new();
secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes());
- let minimum_depth = if is_0conf {
+ let minimum_depth = if trusted_channel_features.is_some_and(|f| f.is_0conf()) {
Some(0)
} else {
Some(cmp::max(config.channel_handshake_config.minimum_depth, 1))
@@ -14250,7 +14250,8 @@ impl<SP: SignerProvider> InboundV1Channel<SP> {
fee_estimator: &LowerBoundedFeeEstimator<F>, entropy_source: &ES, signer_provider: &SP,
counterparty_node_id: PublicKey, our_supported_features: &ChannelTypeFeatures,
their_features: &InitFeatures, msg: &msgs::OpenChannel, user_id: u128, config: &UserConfig,
- current_chain_height: u32, logger: &L, is_0conf: bool,
+ current_chain_height: u32, logger: &L,
+ trusted_channel_features: Option<TrustedChannelFeatures>,
) -> Result<InboundV1Channel<SP>, ChannelError> {
let logger = WithContext::from(logger, Some(counterparty_node_id), Some(msg.common_fields.temporary_channel_id), None);
@@ -14262,7 +14263,7 @@ impl<SP: SignerProvider> InboundV1Channel<SP> {
msg.common_fields.funding_satoshis,
msg.common_fields.dust_limit_satoshis,
config,
- false,
+ trusted_channel_features.is_some_and(|f| f.is_0reserve()),
);
let counterparty_pubkeys = ChannelPublicKeys {
funding_pubkey: msg.common_fields.funding_pubkey,
@@ -14282,7 +14283,7 @@ impl<SP: SignerProvider> InboundV1Channel<SP> {
config,
current_chain_height,
&&logger,
- is_0conf,
+ trusted_channel_features,
0,
counterparty_pubkeys,
@@ -14678,7 +14679,7 @@ impl<SP: SignerProvider> PendingV2Channel<SP> {
config,
current_chain_height,
logger,
- false,
+ None,
our_funding_contribution_sats,
counterparty_pubkeys,
channel_type,
@@ -16327,7 +16328,7 @@ mod tests {
MIN_THEIR_CHAN_RESERVE_SATOSHIS,
};
use crate::ln::channel_keys::{RevocationBasepoint, RevocationKey};
- use crate::ln::channelmanager::{self, HTLCSource, PaymentId};
+ use crate::ln::channelmanager::{self, HTLCSource, PaymentId, TrustedChannelFeatures};
use crate::ln::msgs;
use crate::ln::msgs::{ChannelUpdate, UnsignedChannelUpdate, MAX_VALUE_MSAT};
use crate::ln::onion_utils::{AttributionData, LocalHTLCFailureReason};
@@ -16531,7 +16532,7 @@ mod tests {
// Make sure A's dust limit is as we expect.
let open_channel_msg = node_a_chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap();
let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[7; 32]).unwrap());
- let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, /*is_0conf=*/false).unwrap();
+ let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, None).unwrap();
// Node B --> Node A: accept channel, explicitly setting B's dust limit.
let mut accept_channel_msg = node_b_chan.accept_inbound_channel(&&logger).unwrap();
@@ -16676,7 +16677,7 @@ mod tests {
// Create Node B's channel by receiving Node A's open_channel message
let open_channel_msg = node_a_chan.get_open_channel(chain_hash, &&logger).unwrap();
let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[7; 32]).unwrap());
- let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, /*is_0conf=*/false).unwrap();
+ let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, None).unwrap();
// Node B --> Node A: accept channel
let accept_channel_msg = node_b_chan.accept_inbound_channel(&&logger).unwrap();
@@ -16751,12 +16752,12 @@ mod tests {
// Test that `InboundV1Channel::new` creates a channel with the correct value for
// `holder_max_htlc_value_in_flight_msat`, when configured with a valid percentage value,
// which is set to the lower bound - 1 (2%) of the `channel_value`.
- let chan_3 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_2_percent), &channelmanager::provided_init_features(&config_2_percent), &chan_1_open_channel_msg, 7, &config_2_percent, 0, &&logger, /*is_0conf=*/false).unwrap();
+ let chan_3 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_2_percent), &channelmanager::provided_init_features(&config_2_percent), &chan_1_open_channel_msg, 7, &config_2_percent, 0, &&logger, None).unwrap();
let chan_3_value_msat = chan_3.funding.get_value_satoshis() * 1000;
assert_eq!(chan_3.context.holder_max_htlc_value_in_flight_msat, (chan_3_value_msat as f64 * 0.02) as u64);
// Test with the upper bound - 1 of valid values (99%).
- let chan_4 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_99_percent), &channelmanager::provided_init_features(&config_99_percent), &chan_1_open_channel_msg, 7, &config_99_percent, 0, &&logger, /*is_0conf=*/false).unwrap();
+ let chan_4 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_99_percent), &channelmanager::provided_init_features(&config_99_percent), &chan_1_open_channel_msg, 7, &config_99_percent, 0, &&logger, None).unwrap();
let chan_4_value_msat = chan_4.funding.get_value_satoshis() * 1000;
assert_eq!(chan_4.context.holder_max_htlc_value_in_flight_msat, (chan_4_value_msat as f64 * 0.99) as u64);
@@ -16775,14 +16776,14 @@ mod tests {
// Test that `InboundV1Channel::new` uses the lower bound of the configurable percentage values (1%)
// if `max_inbound_htlc_value_in_flight_percent_of_channel` is set to a value less than 1.
- let chan_7 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_0_percent), &channelmanager::provided_init_features(&config_0_percent), &chan_1_open_channel_msg, 7, &config_0_percent, 0, &&logger, /*is_0conf=*/false).unwrap();
+ let chan_7 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_0_percent), &channelmanager::provided_init_features(&config_0_percent), &chan_1_open_channel_msg, 7, &config_0_percent, 0, &&logger, None).unwrap();
let chan_7_value_msat = chan_7.funding.get_value_satoshis() * 1000;
assert_eq!(chan_7.context.holder_max_htlc_value_in_flight_msat, (chan_7_value_msat as f64 * 0.01) as u64);
// Test that `InboundV1Channel::new` uses the upper bound of the configurable percentage values
// (100%) if `max_inbound_htlc_value_in_flight_percent_of_channel` is set to a larger value
// than 100.
- let chan_8 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_101_percent), &channelmanager::provided_init_features(&config_101_percent), &chan_1_open_channel_msg, 7, &config_101_percent, 0, &&logger, /*is_0conf=*/false).unwrap();
+ let chan_8 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_101_percent), &channelmanager::provided_init_features(&config_101_percent), &chan_1_open_channel_msg, 7, &config_101_percent, 0, &&logger, None).unwrap();
let chan_8_value_msat = chan_8.funding.get_value_satoshis() * 1000;
assert_eq!(chan_8.context.holder_max_htlc_value_in_flight_msat, chan_8_value_msat);
}
@@ -16835,7 +16836,7 @@ mod tests {
inbound_node_config.channel_handshake_config.their_channel_reserve_proportional_millionths = (inbound_selected_channel_reserve_perc * 1_000_000.0) as u32;
if outbound_selected_channel_reserve_perc + inbound_selected_channel_reserve_perc < 1.0 {
- let chan_inbound_node = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, /*is_0conf=*/false).unwrap();
+ let chan_inbound_node = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, None).unwrap();
let expected_inbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * inbound_selected_channel_reserve_perc) as u64);
@@ -16843,7 +16844,7 @@ mod tests {
assert_eq!(chan_inbound_node.funding.counterparty_selected_channel_reserve_satoshis.unwrap(), expected_outbound_selected_chan_reserve);
} else {
// Channel Negotiations failed
- let result = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, /*is_0conf=*/false);
+ let result = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, None);
assert!(result.is_err());
}
}
@@ -16870,7 +16871,7 @@ mod tests {
// Make sure A's dust limit is as we expect.
let open_channel_msg = node_a_chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap();
let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[7; 32]).unwrap());
- let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, /*is_0conf=*/false).unwrap();
+ let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, None).unwrap();
// Node B --> Node A: accept channel, explicitly setting B's dust limit.
let mut accept_channel_msg = node_b_chan.accept_inbound_channel(&&logger).unwrap();
@@ -16973,7 +16974,7 @@ mod tests {
&config,
0,
&&logger,
- false,
+ None,
)
.unwrap();
outbound_chan
@@ -18628,7 +18629,8 @@ mod tests {
&config,
0,
&&logger,
- true, // Allow node b to send a 0conf channel_ready.
+ // Allow node b to send a 0conf channel_ready.
+ Some(TrustedChannelFeatures::ZeroConf),
).unwrap();
let accept_channel_msg = node_b_chan.accept_inbound_channel(&&logger).unwrap();
diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs
index 1de51bf..9645d3c 100644
--- a/lightning/src/ln/channel_open_tests.rs
+++ b/lightning/src/ln/channel_open_tests.rs
@@ -19,7 +19,8 @@ use crate::ln::channel::{
OutboundV1Channel, COINBASE_MATURITY, UNFUNDED_CHANNEL_AGE_LIMIT_TICKS,
};
use crate::ln::channelmanager::{
- self, BREAKDOWN_TIMEOUT, MAX_UNFUNDED_CHANNEL_PEERS, MAX_UNFUNDED_CHANS_PER_PEER,
+ self, TrustedChannelFeatures, BREAKDOWN_TIMEOUT, MAX_UNFUNDED_CHANNEL_PEERS,
+ MAX_UNFUNDED_CHANS_PER_PEER,
};
use crate::ln::msgs::{
AcceptChannel, BaseMessageHandler, ChannelMessageHandler, ErrorAction, MessageSendEvent,
@@ -157,10 +158,11 @@ fn test_0conf_limiting() {
Event::OpenChannelRequest { temporary_channel_id, .. } => {
nodes[1]
.node
- .accept_inbound_channel_from_trusted_peer_0conf(
+ .accept_inbound_channel_from_trusted_peer(
&temporary_channel_id,
&last_random_pk,
23,
+ TrustedChannelFeatures::ZeroConf,
None,
)
.unwrap();
@@ -968,7 +970,7 @@ pub fn test_user_configurable_csv_delay() {
&low_our_to_self_config,
0,
&nodes[0].logger,
- /*is_0conf=*/ false,
+ None,
) {
match error {
ChannelError::Close((err, _)) => {
@@ -1028,7 +1030,7 @@ pub fn test_user_configurable_csv_delay() {
&high_their_to_self_config,
0,
&nodes[0].logger,
- /*is_0conf=*/ false,
+ None,
) {
match error {
ChannelError::Close((err, _)) => {
diff --git a/lightning/src/ln/channel_type_tests.rs b/lightning/src/ln/channel_type_tests.rs
index 2b069a6..dc58655 100644
--- a/lightning/src/ln/channel_type_tests.rs
+++ b/lightning/src/ln/channel_type_tests.rs
@@ -167,7 +167,7 @@ fn test_zero_conf_channel_type_support() {
&config,
0,
&&logger,
- /*is_0conf=*/ false,
+ None,
);
assert!(res.is_ok());
}
@@ -282,7 +282,7 @@ fn do_test_supports_channel_type(config: UserConfig, expected_channel_type: Chan
&config,
0,
&&logger,
- /*is_0conf=*/ false,
+ None,
)
.unwrap();
@@ -350,7 +350,7 @@ fn test_rejects_if_channel_type_not_set() {
&config,
0,
&&logger,
- /*is_0conf=*/ false,
+ None,
);
assert!(channel_b.is_err());
@@ -368,7 +368,7 @@ fn test_rejects_if_channel_type_not_set() {
&config,
0,
&&logger,
- /*is_0conf=*/ false,
+ None,
)
.unwrap();
@@ -434,7 +434,7 @@ fn test_rejects_if_channel_type_differ() {
&config,
0,
&&logger,
- /*is_0conf=*/ false,
+ None,
)
.unwrap();
@@ -518,7 +518,7 @@ fn test_rejects_simple_anchors_channel_type() {
&config,
0,
&&logger,
- /*is_0conf=*/ false,
+ None,
);
assert!(res.is_err());
@@ -558,7 +558,7 @@ fn test_rejects_simple_anchors_channel_type() {
&config,
0,
&&logger,
- /*is_0conf=*/ false,
+ None,
)
.unwrap();
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 30eb7f8..d8302ee 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -3536,6 +3536,48 @@ fn create_htlc_intercepted_event(
})
}
+/// Sets the features of the accepted channel in [`ChannelManager::accept_inbound_channel_from_trusted_peer`]
+#[derive(Clone, Copy)]
+pub enum TrustedChannelFeatures {
+ /// Accepts the incoming channel and (if the counterparty agrees), enables forwarding of payments immediately.
+ ///
+ /// This fully trusts that the counterparty has honestly and correctly constructed the funding transaction and
+ /// blindly assumes that it will eventually confirm.
+ ///
+ /// If it does not confirm before we decide to close the channel, or if the funding transaction
+ /// does not pay to the correct script the correct amount, *you will lose funds*.
+ ZeroConf,
+ /// Accepts the incoming channel and sets the reserve the counterparty must keep at all times in the channel to
+ /// zero.
+ ///
+ /// This allows the counterparty to spend their entire channel balance, and attempt to force-close the channel
+ /// with a revoked commitment transaction *for free*.
+ ///
+ /// Note that there is no guarantee that the counterparty accepts such a channel themselves.
+ ZeroReserve,
+ /// Sets the combination of [`TrustedChannelFeatures::ZeroConf`] and [`TrustedChannelFeatures::ZeroReserve`]
+ ZeroConfZeroReserve,
+}
+
+impl TrustedChannelFeatures {
+ /// True if and only if `ZeroConf` is set
+ pub fn is_0conf(&self) -> bool {
+ match self {
+ TrustedChannelFeatures::ZeroConf | TrustedChannelFeatures::ZeroConfZeroReserve => true,
+ TrustedChannelFeatures::ZeroReserve => false,
+ }
+ }
+ /// True if and only if `ZeroReserve` is set
+ pub fn is_0reserve(&self) -> bool {
+ match self {
+ TrustedChannelFeatures::ZeroReserve | TrustedChannelFeatures::ZeroConfZeroReserve => {
+ true
+ },
+ TrustedChannelFeatures::ZeroConf => false,
+ }
+ }
+}
+
impl<
M: chain::Watch<SP::EcdsaSigner>,
T: BroadcasterInterface,
@@ -11057,10 +11099,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
///
/// The `user_channel_id` parameter will be provided back in
/// [`Event::ChannelClosed::user_channel_id`] to allow tracking of which events correspond
- /// with which `accept_inbound_channel`/`accept_inbound_channel_from_trusted_peer_0conf` call.
+ /// with which `accept_inbound_channel`/`accept_inbound_channel_from_trusted_peer` call.
///
/// Note that this method will return an error and reject the channel, if it requires support
- /// for zero confirmations. Instead, `accept_inbound_channel_from_trusted_peer_0conf` must be
+ /// for zero confirmations. Instead, `accept_inbound_channel_from_trusted_peer` must be
/// used to accept such channels.
///
/// NOTE: LDK makes no attempt to prevent the counterparty from using non-standard inputs which
@@ -11076,38 +11118,32 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
self.do_accept_inbound_channel(
temporary_channel_id,
counterparty_node_id,
- false,
+ None,
user_channel_id,
config_overrides,
)
}
- /// Accepts a request to open a channel after a [`Event::OpenChannelRequest`], treating
- /// it as confirmed immediately.
+ /// Accepts a request to open a channel after a [`Event::OpenChannelRequest`]. Unlike
+ /// [`ChannelManager::accept_inbound_channel`], this method allows some combination of the
+ /// zero-conf and zero-reserve features to be set for the channel, see a description of these
+ /// features in [`TrustedChannelFeatures`].
///
/// The `user_channel_id` parameter will be provided back in
/// [`Event::ChannelClosed::user_channel_id`] to allow tracking of which events correspond
- /// with which `accept_inbound_channel`/`accept_inbound_channel_from_trusted_peer_0conf` call.
- ///
- /// Unlike [`ChannelManager::accept_inbound_channel`], this method accepts the incoming channel
- /// and (if the counterparty agrees), enables forwarding of payments immediately.
- ///
- /// This fully trusts that the counterparty has honestly and correctly constructed the funding
- /// transaction and blindly assumes that it will eventually confirm.
- ///
- /// If it does not confirm before we decide to close the channel, or if the funding transaction
- /// does not pay to the correct script the correct amount, *you will lose funds*.
+ /// with which `accept_inbound_channel`/`accept_inbound_channel_from_trusted_peer` call.
///
/// [`Event::OpenChannelRequest`]: events::Event::OpenChannelRequest
/// [`Event::ChannelClosed::user_channel_id`]: events::Event::ChannelClosed::user_channel_id
- pub fn accept_inbound_channel_from_trusted_peer_0conf(
+ pub fn accept_inbound_channel_from_trusted_peer(
&self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey,
- user_channel_id: u128, config_overrides: Option<ChannelConfigOverrides>,
+ user_channel_id: u128, trusted_channel_features: TrustedChannelFeatures,
+ config_overrides: Option<ChannelConfigOverrides>,
) -> Result<(), APIError> {
self.do_accept_inbound_channel(
temporary_channel_id,
counterparty_node_id,
- true,
+ Some(trusted_channel_features),
user_channel_id,
config_overrides,
)
@@ -11116,7 +11152,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
/// TODO(dual_funding): Allow contributions, pass intended amount and inputs
fn do_accept_inbound_channel(
&self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey,
- accept_0conf: bool, user_channel_id: u128,
+ trusted_channel_features: Option<TrustedChannelFeatures>, user_channel_id: u128,
config_overrides: Option<ChannelConfigOverrides>,
) -> Result<(), APIError> {
let mut config = self.config.read().unwrap().clone();
@@ -11165,7 +11201,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
&config,
best_block_height,
&self.logger,
- accept_0conf,
+ trusted_channel_features,
)
.map_err(|err| {
MsgHandleErrInternal::from_chan_no_close(err, *temporary_channel_id)
@@ -11242,7 +11278,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
},
};
- if accept_0conf {
+ if trusted_channel_features.is_some_and(|f| f.is_0conf()) {
// This should have been correctly configured by the call to Inbound(V1/V2)Channel::new.
debug_assert!(channel.minimum_depth().unwrap() == 0);
} else if channel.funding().get_channel_type().requires_zero_conf() {
@@ -11257,7 +11293,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
};
debug_assert!(peer_state.is_connected);
peer_state.pending_msg_events.push(send_msg_err_event);
- let err_str = "Please use accept_inbound_channel_from_trusted_peer_0conf to accept channels with zero confirmations.".to_owned();
+ let err_str = "Please use accept_inbound_channel_from_trusted_peer to accept channels with zero confirmations.".to_owned();
log_error!(logger, "{}", err_str);
return Err(APIError::APIMisuseError { err: err_str });
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 80274d1..7b74081 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -25,7 +25,7 @@ use crate::ln::chan_utils::{
};
use crate::ln::channelmanager::{
AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId,
- RAACommitmentOrder, MIN_CLTV_EXPIRY_DELTA,
+ RAACommitmentOrder, TrustedChannelFeatures, MIN_CLTV_EXPIRY_DELTA,
};
use crate::ln::funding::{FundingContribution, FundingTxInput};
use crate::ln::msgs::{self, OpenChannel};
@@ -1646,10 +1646,11 @@ pub fn exchange_open_accept_zero_conf_chan<'a, 'b, 'c, 'd>(
Event::OpenChannelRequest { temporary_channel_id, .. } => {
receiver
.node
- .accept_inbound_channel_from_trusted_peer_0conf(
+ .accept_inbound_channel_from_trusted_peer(
&temporary_channel_id,
&initiator_node_id,
0,
+ TrustedChannelFeatures::ZeroConf,
None,
)
.unwrap();
diff --git a/lightning/src/ln/priv_short_conf_tests.rs b/lightning/src/ln/priv_short_conf_tests.rs
index ffe5ea6..6ea67f2 100644
--- a/lightning/src/ln/priv_short_conf_tests.rs
+++ b/lightning/src/ln/priv_short_conf_tests.rs
@@ -14,7 +14,7 @@
use crate::chain::ChannelMonitorUpdateStatus;
use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaymentFailureReason};
use crate::ln::channel::CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY;
-use crate::ln::channelmanager::{PaymentId, MIN_CLTV_EXPIRY_DELTA};
+use crate::ln::channelmanager::{PaymentId, TrustedChannelFeatures, MIN_CLTV_EXPIRY_DELTA};
use crate::ln::msgs;
use crate::ln::msgs::{
BaseMessageHandler, ChannelMessageHandler, ErrorAction, MessageSendEvent, RoutingMessageHandler,
@@ -774,7 +774,7 @@ fn test_simple_0conf_channel() {
// If our peer tells us they will accept our channel with 0 confs, and we funded the channel,
// we should trust the funding won't be double-spent (assuming `trust_own_funding_0conf` is
// set)!
- // Further, if we `accept_inbound_channel_from_trusted_peer_0conf`, `channel_ready` messages
+ // Further, if we `accept_inbound_channel_from_trusted_peer`, `channel_ready` messages
// should fly immediately and the channel should be available for use as soon as they are
// received.
@@ -818,10 +818,11 @@ fn test_0conf_channel_with_async_monitor() {
Event::OpenChannelRequest { temporary_channel_id, .. } => {
nodes[1]
.node
- .accept_inbound_channel_from_trusted_peer_0conf(
+ .accept_inbound_channel_from_trusted_peer(
&temporary_channel_id,
&node_a_id,
0,
+ TrustedChannelFeatures::ZeroConf,
None,
)
.unwrap();
@@ -1369,11 +1370,12 @@ fn test_zero_conf_accept_reject() {
// Assert we can accept via the 0conf method
assert!(nodes[1]
.node
- .accept_inbound_channel_from_trusted_peer_0conf(
+ .accept_inbound_channel_from_trusted_peer(
&temporary_channel_id,
&node_a_id,
0,
- None
+ TrustedChannelFeatures::ZeroConf,
+ None,
)
.is_ok());
},
@@ -1411,10 +1413,11 @@ fn test_connect_before_funding() {
Event::OpenChannelRequest { temporary_channel_id, .. } => {
nodes[1]
.node
- .accept_inbound_channel_from_trusted_peer_0conf(
+ .accept_inbound_channel_from_trusted_peer(
&temporary_channel_id,
&node_a_id,
0,
+ TrustedChannelFeatures::ZeroConf,
None,
)
.unwrap();
diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs
index e415891..14c5071 100644
--- a/lightning/src/util/config.rs
+++ b/lightning/src/util/config.rs
@@ -31,11 +31,11 @@ pub struct ChannelHandshakeConfig {
/// A lower-bound of `1` is applied, requiring all channels to have a confirmed commitment
/// transaction before operation. If you wish to accept channels with zero confirmations,
/// manually accept them via [`Event::OpenChannelRequest`] using
- /// [`ChannelManager::accept_inbound_channel_from_trusted_peer_0conf`].
+ /// [`ChannelManager::accept_inbound_channel_from_trusted_peer`].
///
/// Default value: `6`
///
- /// [`ChannelManager::accept_inbound_channel_from_trusted_peer_0conf`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel_from_trusted_peer_0conf
+ /// [`ChannelManager::accept_inbound_channel_from_trusted_peer`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel_from_trusted_peer
/// [`Event::OpenChannelRequest`]: crate::events::Event::OpenChannelRequest
pub minimum_depth: u32,
/// Set to the number of blocks we require our counterparty to wait to claim their money (ie
Why this scored 34/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.