diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 3dfed10d5c8..7f1def5a61f 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -716,14 +716,28 @@ impl_writeable_tlv_based_enum_upgradable!(PaymentFailureReason, /// Used to indicate the kind of funding for this channel by the channel acceptor (us). /// /// Allows the differentiation between a request for a dual-funded and non-dual-funded channel. +/// For V1 channels, this also carries the `channel_reserve_satoshis` value set by the channel +/// initiator, since it is only available in V1 `open_channel` messages. #[derive(Clone, Debug, PartialEq, Eq)] pub enum InboundChannelFunds { - /// For a non-dual-funded channel, the `push_msat` value from the channel initiator to us. - PushMsat(u64), + /// For a non-dual-funded (V1) channel, the `push_msat` value and `channel_reserve_satoshis` + /// from the channel initiator. + PushMsat { + /// The amount, in millisatoshis, that the channel initiator is pushing to us. + push_msat: u64, + /// The minimum value unencumbered by HTLCs for the non-channel-initiator to keep in the + /// channel, as set by the channel initiator in the `open_channel` message. + channel_reserve_satoshis: u64, + }, /// Indicates the open request is for a dual funded channel. /// /// Note that these channels do not support starting with initial funds pushed from the counterparty, /// who is the channel opener in this case. + /// + /// For V2 channels, the channel reserve is calculated as + /// `max(1% of total_channel_value, dust_limit_satoshis)` per the spec, and is not known at + /// the time of [`Event::OpenChannelRequest`] because the acceptor's funding contribution has + /// not yet been determined. DualFunded, } @@ -1622,8 +1636,9 @@ pub enum Event { /// The channel value of the requested channel. funding_satoshis: u64, /// If `channel_negotiation_type` is `InboundChannelFunds::DualFunded`, this indicates that the peer wishes to - /// open a dual-funded channel. Otherwise, this field will be `InboundChannelFunds::PushMsats`, - /// indicating the `push_msats` value our peer is pushing to us for a non-dual-funded channel. + /// open a dual-funded channel. Otherwise, this field will be `InboundChannelFunds::PushMsat`, + /// indicating the `push_msat` value and `channel_reserve_satoshis` our peer set for a + /// non-dual-funded (V1) channel. channel_negotiation_type: InboundChannelFunds, /// The features that this channel will operate with. If you reject the channel, a /// well-behaved counterparty may automatically re-attempt the channel with a new set of diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs index 059639330f8..c425d4178be 100644 --- a/lightning/src/ln/channel_open_tests.rs +++ b/lightning/src/ln/channel_open_tests.rs @@ -13,7 +13,7 @@ use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::chain::channelmonitor::{self, ChannelMonitorUpdateStep}; use crate::chain::transaction::OutPoint; use crate::chain::{self, ChannelMonitorUpdateStatus}; -use crate::events::{ClosureReason, Event, FundingInfo}; +use crate::events::{ClosureReason, Event, FundingInfo, InboundChannelFunds}; use crate::ln::channel::{ get_holder_selected_channel_reserve_satoshis, ChannelError, InboundV1Channel, OutboundV1Channel, COINBASE_MATURITY, UNFUNDED_CHANNEL_AGE_LIMIT_TICKS, @@ -1706,6 +1706,164 @@ pub fn test_invalid_funding_tx() { mine_transaction(&nodes[1], &spend_tx); } +#[xtest(feature = "_externalize_tests")] +pub fn test_open_channel_request_channel_reserve_satoshis() { + // Test that the `channel_reserve_satoshis` field is correctly populated in the + // `InboundChannelFunds::PushMsat` variant for V1 channels. + 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, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + // Create channel with 100,000 sats + nodes[0].node.create_channel(node_b_id, 100_000, 10_001, 42, None, None).unwrap(); + let open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + + // The channel_reserve_satoshis in the open_channel message is set by the opener + let expected_reserve = open_channel_msg.channel_reserve_satoshis; + + nodes[1].node.handle_open_channel(node_a_id, &open_channel_msg); + + // Verify the OpenChannelRequest event contains the correct channel_reserve_satoshis + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::OpenChannelRequest { + temporary_channel_id, + channel_negotiation_type, + params, + .. + } => { + // For V1 channels, channel_reserve_satoshis should be in the PushMsat variant + match channel_negotiation_type { + InboundChannelFunds::PushMsat { channel_reserve_satoshis, .. } => { + assert_eq!( + *channel_reserve_satoshis, expected_reserve, + "channel_reserve_satoshis in InboundChannelFunds::PushMsat should match the open_channel message" + ); + }, + _ => panic!("Expected InboundChannelFunds::PushMsat for V1 channel"), + } + + // Verify params fields are correctly populated + assert_eq!( + params.dust_limit_satoshis, + open_channel_msg.common_fields.dust_limit_satoshis + ); + assert_eq!( + params.max_htlc_value_in_flight_msat, + open_channel_msg.common_fields.max_htlc_value_in_flight_msat + ); + assert_eq!(params.htlc_minimum_msat, open_channel_msg.common_fields.htlc_minimum_msat); + assert_eq!(params.to_self_delay, open_channel_msg.common_fields.to_self_delay); + assert_eq!( + params.max_accepted_htlcs, + open_channel_msg.common_fields.max_accepted_htlcs + ); + + // Accept the channel to clean up + nodes[1] + .node + .accept_inbound_channel(temporary_channel_id, &node_a_id, 0, None) + .unwrap(); + }, + _ => panic!("Expected OpenChannelRequest event"), + } + + // Clear the SendAcceptChannel message event generated by accepting the channel + nodes[1].node.get_and_clear_pending_msg_events(); +} + +#[xtest(feature = "_externalize_tests")] +pub fn test_open_channel_request_channel_reserve_satoshis_v2() { + // Test that the `channel_negotiation_type` is `InboundChannelFunds::DualFunded` + // for V2 (dual-funded) channels, which does not carry a channel reserve field. + let mut dual_funded_conf = UserConfig::default(); + dual_funded_conf.enable_dual_funded_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(dual_funded_conf.clone()), Some(dual_funded_conf.clone())], + ); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + // Get the open_channel message from node 0 to use as a template for the common fields + nodes[0] + .node + .create_channel(node_b_id, 100_000, 10_001, 42, None, Some(dual_funded_conf.clone())) + .unwrap(); + let open_channel_v1_msg = + get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + + // Create an OpenChannelV2 message using the common fields from V1 + let open_channel_v2_msg = msgs::OpenChannelV2 { + common_fields: open_channel_v1_msg.common_fields.clone(), + funding_feerate_sat_per_1000_weight: 1000, + locktime: 0, + second_per_commitment_point: open_channel_v1_msg.common_fields.first_per_commitment_point, + require_confirmed_inputs: None, + }; + + nodes[1].node.handle_open_channel_v2(node_a_id, &open_channel_v2_msg); + + // Verify the OpenChannelRequest event contains channel_reserve_satoshis = None for V2 channels + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::OpenChannelRequest { + temporary_channel_id, + channel_negotiation_type, + params, + .. + } => { + // For V2 channels, channel_negotiation_type should be DualFunded (no reserve field) + assert_eq!( + *channel_negotiation_type, + InboundChannelFunds::DualFunded, + "channel_negotiation_type should be DualFunded for V2 channels" + ); + + // Verify params fields are correctly populated + assert_eq!( + params.dust_limit_satoshis, + open_channel_v2_msg.common_fields.dust_limit_satoshis + ); + assert_eq!( + params.max_htlc_value_in_flight_msat, + open_channel_v2_msg.common_fields.max_htlc_value_in_flight_msat + ); + assert_eq!( + params.htlc_minimum_msat, + open_channel_v2_msg.common_fields.htlc_minimum_msat + ); + assert_eq!(params.to_self_delay, open_channel_v2_msg.common_fields.to_self_delay); + assert_eq!( + params.max_accepted_htlcs, + open_channel_v2_msg.common_fields.max_accepted_htlcs + ); + + // Accept the channel to clean up + nodes[1] + .node + .accept_inbound_channel(temporary_channel_id, &node_a_id, 0, None) + .unwrap(); + }, + _ => panic!("Expected OpenChannelRequest event"), + } + + // Clear the SendAcceptChannelV2 message event generated by accepting the channel + nodes[1].node.get_and_clear_pending_msg_events(); +} + #[xtest(feature = "_externalize_tests")] pub fn test_coinbase_funding_tx() { // Miners are able to fund channels directly from coinbase transactions, however diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e840d705b8e..cfa1a170e94 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9050,7 +9050,8 @@ impl< ComplFunc: FnOnce( Option, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: HTLCPreviousHopData, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, @@ -9088,7 +9089,8 @@ impl< ComplFunc: FnOnce( Option, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: HTLCClaimSource, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, @@ -10852,7 +10854,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ counterparty_node_id: *counterparty_node_id, funding_satoshis: common_fields.funding_satoshis, channel_negotiation_type: match msg { - OpenChannelMessageRef::V1(msg) => InboundChannelFunds::PushMsat(msg.push_msat), + OpenChannelMessageRef::V1(msg) => InboundChannelFunds::PushMsat { + push_msat: msg.push_msat, + channel_reserve_satoshis: msg.channel_reserve_satoshis, + }, OpenChannelMessageRef::V2(_) => InboundChannelFunds::DualFunded, }, channel_type, diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 67f7807a487..7e4500aff46 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -3939,7 +3939,8 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPaylo used_aad, } => { if amt.is_some() - || cltv_value.is_some() || total_msat.is_some() + || cltv_value.is_some() + || total_msat.is_some() || keysend_preimage.is_some() || invoice_request.is_some() || used_aad @@ -3961,7 +3962,8 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPaylo used_aad, } => { if amt.is_some() - || cltv_value.is_some() || total_msat.is_some() + || cltv_value.is_some() + || total_msat.is_some() || keysend_preimage.is_some() || invoice_request.is_some() || !used_aad @@ -4107,7 +4109,8 @@ impl ReadableArgs<(Option, NS)> for InboundTrampoline used_aad, } => { if amt.is_some() - || cltv_value.is_some() || total_msat.is_some() + || cltv_value.is_some() + || total_msat.is_some() || keysend_preimage.is_some() || invoice_request.is_some() || used_aad diff --git a/pending_changelog/3137-accept-dual-funding-without-contributing.txt b/pending_changelog/3137-accept-dual-funding-without-contributing.txt index 5e1d0de2d86..312c7b8a478 100644 --- a/pending_changelog/3137-accept-dual-funding-without-contributing.txt +++ b/pending_changelog/3137-accept-dual-funding-without-contributing.txt @@ -6,7 +6,7 @@ * `Event::OpenChannelRequest::push_msat` has been replaced by the field `channel_negotiation_type` to differentiate between an inbound request for a dual-funded (V2) or non-dual-funded (V1) channel to be opened, with value being either of the enum variants `InboundChannelFunds::DualFunded` and - `InboundChannelFunds::PushMsat(u64)` corresponding to V2 and V1 channel open requests respectively. + `InboundChannelFunds::PushMsat { push_msat, channel_reserve_satoshis }` corresponding to V2 and V1 channel open requests respectively. * Similar to V1 channels, `ChannelManager::accept_inbound_channel()` can also be used to accept an inbound V2 channel. * 0conf dual-funded channels are not supported.