Skip to content

Channel Establishment for V3 Channels #3792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 6, 2025
64 changes: 43 additions & 21 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,7 @@ impl<SP: Deref> Channel<SP> where

pub fn maybe_handle_error_without_close<F: Deref, L: Deref>(
&mut self, chain_hash: ChainHash, fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L,
user_config: &UserConfig, their_features: &InitFeatures,
) -> Result<Option<OpenChannelMessage>, ()>
where
F::Target: FeeEstimator,
Expand All @@ -1567,13 +1568,17 @@ impl<SP: Deref> Channel<SP> where
ChannelPhase::Funded(_) => Ok(None),
ChannelPhase::UnfundedOutboundV1(chan) => {
let logger = WithChannelContext::from(logger, &chan.context, None);
chan.maybe_handle_error_without_close(chain_hash, fee_estimator, &&logger)
chan.maybe_handle_error_without_close(
chain_hash, fee_estimator, &&logger, user_config, their_features,
)
.map(|msg| Some(OpenChannelMessage::V1(msg)))
},
ChannelPhase::UnfundedInboundV1(_) => Ok(None),
ChannelPhase::UnfundedV2(chan) => {
if chan.funding.is_outbound() {
chan.maybe_handle_error_without_close(chain_hash, fee_estimator)
chan.maybe_handle_error_without_close(
chain_hash, fee_estimator, user_config, their_features,
)
.map(|msg| Some(OpenChannelMessage::V2(msg)))
} else {
Ok(None)
Expand Down Expand Up @@ -4868,7 +4873,8 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
/// of the channel type we tried, not of our ability to open any channel at all. We can see if a
/// downgrade of channel features would be possible so that we can still open the channel.
pub(crate) fn maybe_downgrade_channel_features<F: Deref>(
&mut self, funding: &mut FundingScope, fee_estimator: &LowerBoundedFeeEstimator<F>
&mut self, funding: &mut FundingScope, fee_estimator: &LowerBoundedFeeEstimator<F>,
user_config: &UserConfig, their_features: &InitFeatures,
) -> Result<(), ()>
where
F::Target: FeeEstimator
Expand All @@ -4885,25 +4891,35 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
// We've exhausted our options
return Err(());
}

// We should never have negotiated `anchors_nonzero_fee_htlc_tx` because it can result in a
// loss of funds.
let channel_type = &funding.channel_transaction_parameters.channel_type_features;
assert!(!channel_type.supports_anchors_nonzero_fee_htlc_tx());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved this assert up because we should never negotiate a non-zero-htlc-anchor channel, and it's a risk of lost funds if we do.

Couldn't see a historical reason why we only checked it if the channel type was supports_anchors_zero_fee_htlc_tx - went back a few commits and couldn't find reasoning. cc @TheBlueMatt


// We support opening a few different types of channels. Try removing our additional
// features one by one until we've either arrived at our default or the counterparty has
// accepted one.
//
// Due to the order below, we may not negotiate `option_anchors_zero_fee_htlc_tx` if the
// counterparty doesn't support `option_scid_privacy`. Since `get_initial_channel_type`
// checks whether the counterparty supports every feature, this would only happen if the
// counterparty is advertising the feature, but rejecting channels proposing the feature for
// whatever reason.
let channel_type = &mut funding.channel_transaction_parameters.channel_type_features;
// accepted one. Features are un-set for the current channel type or any that come before
// it in our order of preference, allowing us to negotiate the "next best" based on the
// counterparty's remaining features per our ranking in `get_initial_channel_type`.
let mut eligible_features = their_features.clone();
if channel_type.supports_anchors_zero_fee_htlc_tx() {
channel_type.clear_anchors_zero_fee_htlc_tx();
self.feerate_per_kw = fee_estimator.bounded_sat_per_1000_weight(ConfirmationTarget::NonAnchorChannelFee);
assert!(!channel_type.supports_anchors_nonzero_fee_htlc_tx());
eligible_features.clear_anchors_zero_fee_htlc_tx();
} else if channel_type.supports_scid_privacy() {
channel_type.clear_scid_privacy();
} else {
*channel_type = ChannelTypeFeatures::only_static_remote_key();
eligible_features.clear_scid_privacy();
eligible_features.clear_anchors_zero_fee_htlc_tx();
}

let next_channel_type = get_initial_channel_type(user_config, &eligible_features);

let conf_target = if next_channel_type.supports_anchors_zero_fee_htlc_tx() {
ConfirmationTarget::AnchorChannelFee
} else {
ConfirmationTarget::NonAnchorChannelFee
};
self.feerate_per_kw = fee_estimator.bounded_sat_per_1000_weight(conf_target);
funding.channel_transaction_parameters.channel_type_features = next_channel_type;

Ok(())
}

Expand Down Expand Up @@ -9893,13 +9909,16 @@ impl<SP: Deref> OutboundV1Channel<SP> where SP::Target: SignerProvider {
/// not of our ability to open any channel at all. Thus, on error, we should first call this
/// and see if we get a new `OpenChannel` message, otherwise the channel is failed.
pub(crate) fn maybe_handle_error_without_close<F: Deref, L: Deref>(
&mut self, chain_hash: ChainHash, fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L
&mut self, chain_hash: ChainHash, fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L,
user_config: &UserConfig, their_features: &InitFeatures,
) -> Result<msgs::OpenChannel, ()>
where
F::Target: FeeEstimator,
L::Target: Logger,
{
self.context.maybe_downgrade_channel_features(&mut self.funding, fee_estimator)?;
self.context.maybe_downgrade_channel_features(
&mut self.funding, fee_estimator, user_config, their_features,
)?;
self.get_open_channel(chain_hash, logger).ok_or(())
}

Expand Down Expand Up @@ -10405,12 +10424,15 @@ impl<SP: Deref> PendingV2Channel<SP> where SP::Target: SignerProvider {
/// not of our ability to open any channel at all. Thus, on error, we should first call this
/// and see if we get a new `OpenChannelV2` message, otherwise the channel is failed.
pub(crate) fn maybe_handle_error_without_close<F: Deref>(
&mut self, chain_hash: ChainHash, fee_estimator: &LowerBoundedFeeEstimator<F>
&mut self, chain_hash: ChainHash, fee_estimator: &LowerBoundedFeeEstimator<F>,
user_config: &UserConfig, their_features: &InitFeatures,
) -> Result<msgs::OpenChannelV2, ()>
where
F::Target: FeeEstimator
{
self.context.maybe_downgrade_channel_features(&mut self.funding, fee_estimator)?;
self.context.maybe_downgrade_channel_features(
&mut self.funding, fee_estimator, user_config, their_features,
)?;
Ok(self.get_open_channel_v2(chain_hash))
}

Expand Down
110 changes: 96 additions & 14 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12472,6 +12472,7 @@ where
match peer_state.channel_by_id.get_mut(&msg.channel_id) {
Some(chan) => match chan.maybe_handle_error_without_close(
self.chain_hash, &self.fee_estimator, &self.logger,
&self.default_configuration, &peer_state.latest_features,
) {
Ok(Some(OpenChannelMessage::V1(msg))) => {
peer_state.pending_msg_events.push(MessageSendEvent::SendOpenChannel {
Expand Down Expand Up @@ -15210,13 +15211,16 @@ mod tests {
use crate::routing::router::{find_route, PaymentParameters, RouteParameters};
use crate::sign::EntropySource;
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
use crate::util::config::{ChannelConfig, ChannelConfigUpdate, ChannelHandshakeConfigUpdate};
use crate::util::config::{
ChannelConfig, ChannelConfigUpdate, ChannelHandshakeConfigUpdate, UserConfig,
};
use crate::util::errors::APIError;
use crate::util::ser::Writeable;
use crate::util::test_utils;
use bitcoin::secp256k1::ecdh::SharedSecret;
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use core::sync::atomic::Ordering;
use lightning_types::features::ChannelTypeFeatures;

#[test]
#[rustfmt::skip]
Expand Down Expand Up @@ -16349,23 +16353,103 @@ mod tests {
}

#[test]
#[rustfmt::skip]
fn test_anchors_zero_fee_htlc_tx_fallback() {
fn test_anchors_zero_fee_htlc_tx_downgrade() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Congrats, you touched it, now you get to move it out of channelmanager into some other test-specific file that isn't 15000 lines of code 😂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done this in a follow up in #3797 so that move + format can be reviewed separately.

// Tests that if both nodes support anchors, but the remote node does not want to accept
// anchor channels at the moment, an error it sent to the local node such that it can retry
// the channel without the anchors feature.
let mut initiator_cfg = test_default_channel_config();
initiator_cfg.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;

let mut receiver_cfg = test_default_channel_config();
receiver_cfg.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
receiver_cfg.manually_accept_inbound_channels = true;

let start_type = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies();
let end_type = ChannelTypeFeatures::only_static_remote_key();
do_test_channel_type_downgrade(initiator_cfg, receiver_cfg, start_type, vec![end_type]);
}

#[test]
fn test_scid_privacy_downgrade() {
// Tests downgrade from `anchors_zero_fee_htlc_tx` with `option_scid_alias` when the
// remote node advertises the features but does not accept the channel, asserting that
// `option_scid_alias` is the last feature to be downgraded.
let mut initiator_cfg = test_default_channel_config();
initiator_cfg.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
initiator_cfg.channel_handshake_config.negotiate_scid_privacy = true;
initiator_cfg.channel_handshake_config.announce_for_forwarding = false;

let mut receiver_cfg = test_default_channel_config();
receiver_cfg.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
receiver_cfg.channel_handshake_config.negotiate_scid_privacy = true;
receiver_cfg.manually_accept_inbound_channels = true;

let mut start_type = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies();
start_type.set_scid_privacy_required();
let mut with_scid_privacy = ChannelTypeFeatures::only_static_remote_key();
with_scid_privacy.set_scid_privacy_required();
let static_remote = ChannelTypeFeatures::only_static_remote_key();
let downgrade_types = vec![with_scid_privacy, static_remote];

do_test_channel_type_downgrade(initiator_cfg, receiver_cfg, start_type, downgrade_types);
}

#[rustfmt::skip]
fn do_test_channel_type_downgrade(initiator_cfg: UserConfig, acceptor_cfg: UserConfig,
start_type: ChannelTypeFeatures, downgrade_types: Vec<ChannelTypeFeatures>) {
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let mut anchors_config = test_default_channel_config();
anchors_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
anchors_config.manually_accept_inbound_channels = true;
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(anchors_config.clone()), Some(anchors_config.clone())]);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(initiator_cfg), Some(acceptor_cfg)]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
let error_message = "Channel force-closed";

nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100_000, 0, 0, None, None).unwrap();
let mut open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id());
assert_eq!(open_channel_msg.common_fields.channel_type.as_ref().unwrap(), &start_type);

for downgrade_type in downgrade_types {
nodes[1].node.handle_open_channel(nodes[0].node.get_our_node_id(), &open_channel_msg);
let events = nodes[1].node.get_and_clear_pending_events();
match events[0] {
Event::OpenChannelRequest { temporary_channel_id, .. } => {
nodes[1].node.force_close_broadcasting_latest_txn(&temporary_channel_id, &nodes[0].node.get_our_node_id(), error_message.to_string()).unwrap();
}
_ => panic!("Unexpected event"),
}

let error_msg = get_err_msg(&nodes[1], &nodes[0].node.get_our_node_id());
nodes[0].node.handle_error(nodes[1].node.get_our_node_id(), &error_msg);

open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id());
let channel_type = open_channel_msg.common_fields.channel_type.as_ref().unwrap();
assert_eq!(channel_type, &downgrade_type);

// Since nodes[1] should not have accepted the channel, it should
// not have generated any events.
assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
}
}

#[test]
#[rustfmt::skip]
fn test_no_channel_downgrade() {
// Tests that the local node will not retry when a `option_static_remote` channel is
// rejected by a peer that advertises support for the feature.
let initiator_cfg = test_default_channel_config();
let mut receiver_cfg = test_default_channel_config();
receiver_cfg.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
receiver_cfg.manually_accept_inbound_channels = true;

let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(initiator_cfg), Some(receiver_cfg)]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
let error_message = "Channel force-closed";

nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100_000, 0, 0, None, None).unwrap();
let open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id());
assert!(open_channel_msg.common_fields.channel_type.as_ref().unwrap().supports_anchors_zero_fee_htlc_tx());
let start_type = ChannelTypeFeatures::only_static_remote_key();
assert_eq!(open_channel_msg.common_fields.channel_type.as_ref().unwrap(), &start_type);

nodes[1].node.handle_open_channel(nodes[0].node.get_our_node_id(), &open_channel_msg);
let events = nodes[1].node.get_and_clear_pending_events();
Expand All @@ -16379,12 +16463,10 @@ mod tests {
let error_msg = get_err_msg(&nodes[1], &nodes[0].node.get_our_node_id());
nodes[0].node.handle_error(nodes[1].node.get_our_node_id(), &error_msg);

let open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id());
assert!(!open_channel_msg.common_fields.channel_type.unwrap().supports_anchors_zero_fee_htlc_tx());

// Since nodes[1] should not have accepted the channel, it should
// not have generated any events.
assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
// Since nodes[0] could not retry the channel with a different type, it should close it.
let chan_closed_events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(chan_closed_events.len(), 1);
if let Event::ChannelClosed { .. } = chan_closed_events[0] { } else { panic!(); }
}

#[test]
Expand Down