Skip to content

Commit 31ebde8

Browse files
committed
Expose channel_reserve_satoshis via ChannelParameters
Add `channel_reserve_satoshis` field to `ChannelParameters` struct to expose the channel reserve value in `OpenChannelRequest` events. This allows users handling inbound channel requests to see the reserve requirement. For V1 channels, this is the explicit value from the `open_channel` message. For V2 (dual-funded) channels, this is `None` since the reserve is calculated based on total channel value which isn't known until both parties contribute. Also implements `channel_parameters()` on `OpenChannelMessageRef` to DRY up the code, as suggested in review.
1 parent 9e91b2e commit 31ebde8

File tree

3 files changed

+178
-15
lines changed

3 files changed

+178
-15
lines changed

lightning/src/ln/channel_open_tests.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,151 @@ pub fn test_invalid_funding_tx() {
17501750
mine_transaction(&nodes[1], &spend_tx);
17511751
}
17521752

1753+
#[xtest(feature = "_externalize_tests")]
1754+
pub fn test_open_channel_request_channel_reserve_satoshis() {
1755+
// Test that the `channel_reserve_satoshis` field is correctly populated in the
1756+
// `OpenChannelRequest` event's `params` field for V1 channels.
1757+
let mut manually_accept_conf = UserConfig::default();
1758+
manually_accept_conf.manually_accept_inbound_channels = true;
1759+
1760+
let chanmon_cfgs = create_chanmon_cfgs(2);
1761+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1762+
let node_chanmgrs =
1763+
create_node_chanmgrs(2, &node_cfgs, &[None, Some(manually_accept_conf.clone())]);
1764+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1765+
1766+
let node_a_id = nodes[0].node.get_our_node_id();
1767+
let node_b_id = nodes[1].node.get_our_node_id();
1768+
1769+
// Create channel with 100,000 sats
1770+
nodes[0]
1771+
.node
1772+
.create_channel(node_b_id, 100_000, 10_001, 42, None, Some(manually_accept_conf))
1773+
.unwrap();
1774+
let open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
1775+
1776+
// The channel_reserve_satoshis in the open_channel message is set by the opener
1777+
let expected_reserve = open_channel_msg.channel_reserve_satoshis;
1778+
1779+
nodes[1].node.handle_open_channel(node_a_id, &open_channel_msg);
1780+
1781+
// Verify the OpenChannelRequest event contains the correct channel_reserve_satoshis
1782+
let events = nodes[1].node.get_and_clear_pending_events();
1783+
assert_eq!(events.len(), 1);
1784+
match &events[0] {
1785+
Event::OpenChannelRequest { temporary_channel_id, params, .. } => {
1786+
// For V1 channels, channel_reserve_satoshis should be Some with the value from the message
1787+
assert_eq!(
1788+
params.channel_reserve_satoshis,
1789+
Some(expected_reserve),
1790+
"channel_reserve_satoshis in OpenChannelRequest params should match the open_channel message"
1791+
);
1792+
1793+
// Verify other params fields are also correctly populated
1794+
assert_eq!(
1795+
params.dust_limit_satoshis,
1796+
open_channel_msg.common_fields.dust_limit_satoshis
1797+
);
1798+
assert_eq!(
1799+
params.max_htlc_value_in_flight_msat,
1800+
open_channel_msg.common_fields.max_htlc_value_in_flight_msat
1801+
);
1802+
assert_eq!(params.htlc_minimum_msat, open_channel_msg.common_fields.htlc_minimum_msat);
1803+
assert_eq!(params.to_self_delay, open_channel_msg.common_fields.to_self_delay);
1804+
assert_eq!(
1805+
params.max_accepted_htlcs,
1806+
open_channel_msg.common_fields.max_accepted_htlcs
1807+
);
1808+
1809+
// Accept the channel to clean up
1810+
nodes[1]
1811+
.node
1812+
.accept_inbound_channel(temporary_channel_id, &node_a_id, 0, None)
1813+
.unwrap();
1814+
},
1815+
_ => panic!("Expected OpenChannelRequest event"),
1816+
}
1817+
1818+
// Clear the SendAcceptChannel message event generated by accepting the channel
1819+
nodes[1].node.get_and_clear_pending_msg_events();
1820+
}
1821+
1822+
#[xtest(feature = "_externalize_tests")]
1823+
pub fn test_open_channel_request_channel_reserve_satoshis_v2() {
1824+
// Test that the `channel_reserve_satoshis` field is `None` in the
1825+
// `OpenChannelRequest` event's `params` field for V2 (dual-funded) channels.
1826+
let mut manually_accept_conf = UserConfig::default();
1827+
manually_accept_conf.manually_accept_inbound_channels = true;
1828+
manually_accept_conf.enable_dual_funded_channels = true;
1829+
1830+
let chanmon_cfgs = create_chanmon_cfgs(2);
1831+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1832+
let node_chanmgrs =
1833+
create_node_chanmgrs(2, &node_cfgs, &[Some(manually_accept_conf.clone()), Some(manually_accept_conf.clone())]);
1834+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1835+
1836+
let node_a_id = nodes[0].node.get_our_node_id();
1837+
let node_b_id = nodes[1].node.get_our_node_id();
1838+
1839+
// Get the open_channel message from node 0 to use as a template for the common fields
1840+
nodes[0]
1841+
.node
1842+
.create_channel(node_b_id, 100_000, 10_001, 42, None, Some(manually_accept_conf.clone()))
1843+
.unwrap();
1844+
let open_channel_v1_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
1845+
1846+
// Create an OpenChannelV2 message using the common fields from V1
1847+
let open_channel_v2_msg = msgs::OpenChannelV2 {
1848+
common_fields: open_channel_v1_msg.common_fields.clone(),
1849+
funding_feerate_sat_per_1000_weight: 1000,
1850+
locktime: 0,
1851+
second_per_commitment_point: open_channel_v1_msg.common_fields.first_per_commitment_point,
1852+
require_confirmed_inputs: None,
1853+
};
1854+
1855+
nodes[1].node.handle_open_channel_v2(node_a_id, &open_channel_v2_msg);
1856+
1857+
// Verify the OpenChannelRequest event contains channel_reserve_satoshis = None for V2 channels
1858+
let events = nodes[1].node.get_and_clear_pending_events();
1859+
assert_eq!(events.len(), 1);
1860+
match &events[0] {
1861+
Event::OpenChannelRequest { temporary_channel_id, params, .. } => {
1862+
// For V2 channels, channel_reserve_satoshis should be None
1863+
assert_eq!(
1864+
params.channel_reserve_satoshis,
1865+
None,
1866+
"channel_reserve_satoshis in OpenChannelRequest params should be None for V2 channels"
1867+
);
1868+
1869+
// Verify other params fields are correctly populated
1870+
assert_eq!(
1871+
params.dust_limit_satoshis,
1872+
open_channel_v2_msg.common_fields.dust_limit_satoshis
1873+
);
1874+
assert_eq!(
1875+
params.max_htlc_value_in_flight_msat,
1876+
open_channel_v2_msg.common_fields.max_htlc_value_in_flight_msat
1877+
);
1878+
assert_eq!(params.htlc_minimum_msat, open_channel_v2_msg.common_fields.htlc_minimum_msat);
1879+
assert_eq!(params.to_self_delay, open_channel_v2_msg.common_fields.to_self_delay);
1880+
assert_eq!(
1881+
params.max_accepted_htlcs,
1882+
open_channel_v2_msg.common_fields.max_accepted_htlcs
1883+
);
1884+
1885+
// Accept the channel to clean up
1886+
nodes[1]
1887+
.node
1888+
.accept_inbound_channel(temporary_channel_id, &node_a_id, 0, None)
1889+
.unwrap();
1890+
},
1891+
_ => panic!("Expected OpenChannelRequest event"),
1892+
}
1893+
1894+
// Clear the SendAcceptChannelV2 message event generated by accepting the channel
1895+
nodes[1].node.get_and_clear_pending_msg_events();
1896+
}
1897+
17531898
#[xtest(feature = "_externalize_tests")]
17541899
pub fn test_coinbase_funding_tx() {
17551900
// Miners are able to fund channels directly from coinbase transactions, however

lightning/src/ln/channelmanager.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1682,6 +1682,25 @@ pub(super) enum OpenChannelMessageRef<'a> {
16821682
V2(&'a msgs::OpenChannelV2),
16831683
}
16841684

1685+
impl<'a> OpenChannelMessageRef<'a> {
1686+
pub(super) fn channel_parameters(&self) -> msgs::ChannelParameters {
1687+
let (common_fields, channel_reserve_satoshis) = match self {
1688+
Self::V1(msg) => (&msg.common_fields, Some(msg.channel_reserve_satoshis)),
1689+
Self::V2(msg) => (&msg.common_fields, None),
1690+
};
1691+
msgs::ChannelParameters {
1692+
dust_limit_satoshis: common_fields.dust_limit_satoshis,
1693+
max_htlc_value_in_flight_msat: common_fields.max_htlc_value_in_flight_msat,
1694+
htlc_minimum_msat: common_fields.htlc_minimum_msat,
1695+
commitment_feerate_sat_per_1000_weight: common_fields
1696+
.commitment_feerate_sat_per_1000_weight,
1697+
to_self_delay: common_fields.to_self_delay,
1698+
max_accepted_htlcs: common_fields.max_accepted_htlcs,
1699+
channel_reserve_satoshis,
1700+
}
1701+
}
1702+
}
1703+
16851704
/// A not-yet-accepted inbound (from counterparty) channel. Once
16861705
/// accepted, the parameters will be used to construct a channel.
16871706
pub(super) struct InboundChannelRequest {
@@ -10716,7 +10735,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1071610735
},
1071710736
channel_type,
1071810737
is_announced,
10719-
params: common_fields.channel_parameters(),
10738+
params: msg.channel_parameters(),
1072010739
}, None));
1072110740
peer_state.inbound_channel_request_by_id.insert(channel_id, InboundChannelRequest {
1072210741
open_channel_msg: match msg {

lightning/src/ln/msgs.rs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -244,20 +244,6 @@ pub struct CommonOpenChannelFields {
244244
pub channel_type: Option<ChannelTypeFeatures>,
245245
}
246246

247-
impl CommonOpenChannelFields {
248-
/// The [`ChannelParameters`] for this channel.
249-
pub fn channel_parameters(&self) -> ChannelParameters {
250-
ChannelParameters {
251-
dust_limit_satoshis: self.dust_limit_satoshis,
252-
max_htlc_value_in_flight_msat: self.max_htlc_value_in_flight_msat,
253-
htlc_minimum_msat: self.htlc_minimum_msat,
254-
commitment_feerate_sat_per_1000_weight: self.commitment_feerate_sat_per_1000_weight,
255-
to_self_delay: self.to_self_delay,
256-
max_accepted_htlcs: self.max_accepted_htlcs,
257-
}
258-
}
259-
}
260-
261247
/// A subset of [`CommonOpenChannelFields`], containing various parameters which are set by the
262248
/// channel initiator and which are not part of the channel funding transaction.
263249
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -277,6 +263,19 @@ pub struct ChannelParameters {
277263
pub to_self_delay: u16,
278264
/// The maximum number of pending HTLCs towards the channel initiator.
279265
pub max_accepted_htlcs: u16,
266+
/// The minimum value unencumbered by HTLCs for the non-channel-initiator to keep in the channel.
267+
///
268+
/// For V1 channels (`open_channel`), this is the explicit `channel_reserve_satoshis` value
269+
/// from the channel initiator.
270+
///
271+
/// For V2 channels (`open_channel2`), this is `None` at the time of [`Event::OpenChannelRequest`]
272+
/// because the channel reserve is calculated as `max(1% of total_channel_value, dust_limit_satoshis)`
273+
/// per the spec, where `total_channel_value` includes both the initiator's and acceptor's funding
274+
/// contributions. Since the acceptor's contribution is not yet known when the event is generated,
275+
/// the final reserve value cannot be determined at that point.
276+
///
277+
/// [`Event::OpenChannelRequest`]: crate::events::Event::OpenChannelRequest
278+
pub channel_reserve_satoshis: Option<u64>,
280279
}
281280

282281
/// An [`open_channel`] message to be sent to or received from a peer.

0 commit comments

Comments
 (0)