Skip to content

Commit 00146df

Browse files
authored
Merge pull request #4613 from tankyleo/2026-05-ban-0reserve-legacy-chans
Disallow holders from selecting 0-reserve in legacy channels
2 parents e47a231 + 257033f commit 00146df

7 files changed

Lines changed: 291 additions & 347 deletions

File tree

fuzz/src/chanmon_consistency.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ fn assert_action_timeout_awaiting_response(action: &msgs::ErrorAction) {
628628
);
629629
}
630630

631-
#[derive(Copy, Clone)]
631+
#[derive(Clone, Copy, PartialEq)]
632632
enum ChanType {
633633
Legacy,
634634
KeyedAnchors,
@@ -2082,19 +2082,20 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
20822082
connect_peers(&nodes[0], &nodes[1]);
20832083
connect_peers(&nodes[1], &nodes[2]);
20842084

2085+
let set_0reserve = chan_type != ChanType::Legacy;
20852086
// Create 3 channels between A-B and 3 channels between B-C (6 total).
20862087
//
20872088
// Use distinct version numbers for each funding transaction so each test
20882089
// channel gets its own txid and funding outpoint.
20892090
// A-B: channel 2 A and B have 0-reserve (trusted open + trusted accept),
2090-
// channel 3 A has 0-reserve (trusted accept).
2091+
// channel 3 A has 0-reserve (trusted accept), if channels are non-legacy.
20912092
make_channel(&nodes[0], &nodes[1], 1, false, false, &mut chain_state);
2092-
make_channel(&nodes[0], &nodes[1], 2, true, true, &mut chain_state);
2093-
make_channel(&nodes[0], &nodes[1], 3, false, true, &mut chain_state);
2093+
make_channel(&nodes[0], &nodes[1], 2, set_0reserve, set_0reserve, &mut chain_state);
2094+
make_channel(&nodes[0], &nodes[1], 3, false, set_0reserve, &mut chain_state);
20942095
// B-C: channel 4 B has 0-reserve (via trusted accept),
2095-
// channel 5 C has 0-reserve (via trusted open).
2096-
make_channel(&nodes[1], &nodes[2], 4, false, true, &mut chain_state);
2097-
make_channel(&nodes[1], &nodes[2], 5, true, false, &mut chain_state);
2096+
// channel 5 C has 0-reserve (via trusted open), if channels are non-legacy.
2097+
make_channel(&nodes[1], &nodes[2], 4, false, set_0reserve, &mut chain_state);
2098+
make_channel(&nodes[1], &nodes[2], 5, set_0reserve, false, &mut chain_state);
20982099
make_channel(&nodes[1], &nodes[2], 6, false, false, &mut chain_state);
20992100

21002101
// Wipe the transactions-broadcasted set to make sure we don't broadcast

lightning/src/ln/channel.rs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3835,6 +3835,14 @@ impl<SP: SignerProvider> ChannelContext<SP> {
38353835
"Funding must be smaller than the total bitcoin supply. It was {channel_value_satoshis}"
38363836
)));
38373837
}
3838+
if !channel_type.supports_anchors_zero_fee_htlc_tx()
3839+
&& !channel_type.supports_anchor_zero_fee_commitments()
3840+
&& holder_selected_channel_reserve_satoshis == 0
3841+
{
3842+
return Err(ChannelError::close(
3843+
"0-reserve is not allowed on legacy channels".to_owned(),
3844+
));
3845+
}
38383846
if msg_channel_reserve_satoshis > channel_value_satoshis {
38393847
return Err(ChannelError::close(format!(
38403848
"Bogus channel_reserve_satoshis ({msg_channel_reserve_satoshis}). Must be no greater than channel_value_satoshis: {channel_value_satoshis}"
@@ -4326,6 +4334,14 @@ impl<SP: SignerProvider> ChannelContext<SP> {
43264334
}
43274335

43284336
let channel_type = get_initial_channel_type(&config, their_features);
4337+
if !channel_type.supports_anchors_zero_fee_htlc_tx()
4338+
&& !channel_type.supports_anchor_zero_fee_commitments()
4339+
&& holder_selected_channel_reserve_satoshis == 0
4340+
{
4341+
return Err(APIError::APIMisuseError {
4342+
err: "0-reserve is not allowed on legacy channels".to_owned(),
4343+
});
4344+
}
43294345
debug_assert!(!channel_type.supports_any_optional_bits());
43304346
debug_assert!(!channel_type
43314347
.requires_unknown_bits_from(&channelmanager::provided_channel_type_features(&config)));
@@ -4872,6 +4888,14 @@ impl<SP: SignerProvider> ChannelContext<SP> {
48724888
}
48734889

48744890
let channel_type = funding.get_channel_type();
4891+
if !channel_type.supports_anchors_zero_fee_htlc_tx()
4892+
&& !channel_type.supports_anchor_zero_fee_commitments()
4893+
&& funding.holder_selected_channel_reserve_satoshis == 0
4894+
{
4895+
return Err(ChannelError::close(
4896+
"0-reserve is not allowed on legacy channels".to_owned(),
4897+
));
4898+
}
48754899
if common_fields.max_accepted_htlcs > max_htlcs(channel_type) {
48764900
return Err(ChannelError::close(format!(
48774901
"max_accepted_htlcs was {}. It must not be larger than {}",
@@ -6540,17 +6564,15 @@ impl<SP: SignerProvider> ChannelContext<SP> {
65406564
/// If we receive an error message when attempting to open a channel, it may only be a rejection
65416565
/// of the channel type we tried, not of our ability to open any channel at all. We can see if a
65426566
/// downgrade of channel features would be possible so that we can still open the channel.
6543-
#[rustfmt::skip]
65446567
pub(crate) fn maybe_downgrade_channel_features<F: FeeEstimator>(
65456568
&mut self, funding: &mut FundingScope, fee_estimator: &LowerBoundedFeeEstimator<F>,
65466569
user_config: &UserConfig, their_features: &InitFeatures,
65476570
) -> Result<(), ()> {
6548-
if !funding.is_outbound() ||
6549-
!matches!(
6571+
if !funding.is_outbound()
6572+
|| !matches!(
65506573
self.channel_state, ChannelState::NegotiatingFunding(flags)
65516574
if flags == NegotiatingFundingFlags::OUR_INIT_SENT
6552-
)
6553-
{
6575+
) {
65546576
return Err(());
65556577
}
65566578
if funding.get_channel_type() == &ChannelTypeFeatures::only_static_remote_key() {
@@ -6581,11 +6603,17 @@ impl<SP: SignerProvider> ChannelContext<SP> {
65816603
}
65826604

65836605
let next_channel_type = get_initial_channel_type(user_config, &eligible_features);
6606+
if !next_channel_type.supports_anchors_zero_fee_htlc_tx()
6607+
&& !next_channel_type.supports_anchor_zero_fee_commitments()
6608+
&& funding.holder_selected_channel_reserve_satoshis == 0
6609+
{
6610+
// 0-reserve is not allowed on legacy channels
6611+
return Err(());
6612+
}
65846613

6585-
self.feerate_per_kw = selected_commitment_sat_per_1000_weight(
6586-
&fee_estimator, &next_channel_type,
6587-
);
6588-
funding.channel_transaction_parameters.channel_type_features = next_channel_type;
6614+
self.feerate_per_kw =
6615+
selected_commitment_sat_per_1000_weight(&fee_estimator, &next_channel_type);
6616+
funding.channel_transaction_parameters.channel_type_features = next_channel_type;
65896617

65906618
Ok(())
65916619
}

lightning/src/ln/channel_open_tests.rs

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ use crate::ln::channelmanager::{
2424
MAX_UNFUNDED_CHANS_PER_PEER,
2525
};
2626
use crate::ln::msgs::{
27-
AcceptChannel, BaseMessageHandler, ChannelMessageHandler, ErrorAction, MessageSendEvent,
27+
AcceptChannel, BaseMessageHandler, ChannelMessageHandler, ErrorAction, ErrorMessage,
28+
MessageSendEvent,
2829
};
2930
use crate::ln::types::ChannelId;
3031
use crate::ln::{functional_test_utils::*, msgs};
@@ -48,6 +49,7 @@ use bitcoin::{Amount, Sequence, Transaction, TxIn, TxOut, Witness};
4849
use lightning_macros::xtest;
4950

5051
use lightning_types::features::ChannelTypeFeatures;
52+
use types::string::UntrustedString;
5153

5254
#[test]
5355
fn test_outbound_chans_unlimited() {
@@ -2496,3 +2498,189 @@ fn test_fund_pending_channel() {
24962498
};
24972499
check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100_000);
24982500
}
2501+
2502+
#[xtest(feature = "_externalize_tests")]
2503+
fn test_holder_selected_0reserve_on_legacy_channel_is_not_allowed() {
2504+
{
2505+
let chanmon_cfgs = create_chanmon_cfgs(2);
2506+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2507+
let mut channel_config = test_default_channel_config();
2508+
assert!(channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx);
2509+
let node_chanmgrs = create_node_chanmgrs(
2510+
2,
2511+
&node_cfgs,
2512+
&[Some(channel_config.clone()), Some(channel_config.clone())],
2513+
);
2514+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2515+
2516+
let _node_a_id = nodes[0].node.get_our_node_id();
2517+
let node_b_id = nodes[1].node.get_our_node_id();
2518+
2519+
let mut legacy_channel_config = test_default_channel_config();
2520+
legacy_channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
2521+
legacy_channel_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments =
2522+
false;
2523+
2524+
// User tries to open a legacy 0-reserve channel with a config override, we fail
2525+
assert_eq!(
2526+
nodes[0]
2527+
.node
2528+
.create_channel_to_trusted_peer_0reserve(
2529+
node_b_id,
2530+
100_000,
2531+
0,
2532+
42,
2533+
None,
2534+
Some(legacy_channel_config)
2535+
)
2536+
.unwrap_err(),
2537+
APIError::APIMisuseError {
2538+
err: "0-reserve is not allowed on legacy channels".to_owned()
2539+
}
2540+
);
2541+
}
2542+
{
2543+
let chanmon_cfgs = create_chanmon_cfgs(2);
2544+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2545+
let mut channel_config = test_default_channel_config();
2546+
channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
2547+
channel_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
2548+
let node_chanmgrs = create_node_chanmgrs(
2549+
2,
2550+
&node_cfgs,
2551+
&[Some(channel_config.clone()), Some(channel_config.clone())],
2552+
);
2553+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2554+
2555+
let _node_a_id = nodes[0].node.get_our_node_id();
2556+
let node_b_id = nodes[1].node.get_our_node_id();
2557+
2558+
// User tries to open a legacy 0-reserve channel from the default config, we fail
2559+
assert_eq!(
2560+
nodes[0]
2561+
.node
2562+
.create_channel_to_trusted_peer_0reserve(node_b_id, 100_000, 0, 42, None, None)
2563+
.unwrap_err(),
2564+
APIError::APIMisuseError {
2565+
err: "0-reserve is not allowed on legacy channels".to_owned()
2566+
}
2567+
);
2568+
}
2569+
2570+
let chanmon_cfgs = create_chanmon_cfgs(2);
2571+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2572+
let mut channel_config = test_default_channel_config();
2573+
channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
2574+
channel_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
2575+
let node_chanmgrs = create_node_chanmgrs(
2576+
2,
2577+
&node_cfgs,
2578+
&[Some(channel_config.clone()), Some(channel_config.clone())],
2579+
);
2580+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2581+
2582+
let node_a_id = nodes[0].node.get_our_node_id();
2583+
let node_b_id = nodes[1].node.get_our_node_id();
2584+
2585+
nodes[0].node.create_channel(node_b_id, 100_000, 0, 42, None, None).unwrap();
2586+
let mut open_channel_msg_0reserve =
2587+
get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
2588+
open_channel_msg_0reserve.channel_reserve_satoshis = 0;
2589+
assert_eq!(
2590+
open_channel_msg_0reserve.common_fields.channel_type,
2591+
Some(ChannelTypeFeatures::only_static_remote_key())
2592+
);
2593+
2594+
// User accepts a legacy channel, and sets 0-reserve for the counterparty, we fail
2595+
nodes[1].node.handle_open_channel(node_a_id, &open_channel_msg_0reserve);
2596+
let events = nodes[1].node.get_and_clear_pending_events();
2597+
match events[0] {
2598+
Event::OpenChannelRequest { temporary_channel_id, .. } => {
2599+
let error = nodes[1]
2600+
.node
2601+
.accept_inbound_channel_from_trusted_peer(
2602+
&temporary_channel_id,
2603+
&node_a_id,
2604+
42,
2605+
TrustedChannelFeatures::ZeroReserve,
2606+
None,
2607+
)
2608+
.unwrap_err();
2609+
assert_eq!(
2610+
error,
2611+
APIError::ChannelUnavailable {
2612+
err: "0-reserve is not allowed on legacy channels".to_owned()
2613+
}
2614+
);
2615+
},
2616+
_ => panic!("Unexpected event"),
2617+
}
2618+
let err_msg = get_err_msg(&nodes[1], &node_a_id);
2619+
assert_eq!(
2620+
err_msg,
2621+
ErrorMessage {
2622+
channel_id: open_channel_msg_0reserve.common_fields.temporary_channel_id,
2623+
data: "0-reserve is not allowed on legacy channels".to_string()
2624+
}
2625+
);
2626+
2627+
// But legacy channels where only the counterparty sets 0-reserve are ok!
2628+
// Here node 1 accepts 0-reserve from node 0, and node 1 sets some non-zero reserve...
2629+
handle_and_accept_open_channel(&nodes[1], node_a_id, &open_channel_msg_0reserve);
2630+
let mut accept_channel_msg =
2631+
get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id);
2632+
// Override the reserve selected by node 1, make sure node 0 accepts too
2633+
accept_channel_msg.channel_reserve_satoshis = 0;
2634+
2635+
nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_msg);
2636+
2637+
let events = nodes[0].node.get_and_clear_pending_events();
2638+
assert_eq!(events.len(), 1);
2639+
assert!(
2640+
matches!(events[0], Event::FundingGenerationReady { channel_value_satoshis: 100_000, user_channel_id: 42, counterparty_node_id, .. } if counterparty_node_id == node_b_id)
2641+
);
2642+
}
2643+
2644+
#[xtest(feature = "_externalize_tests")]
2645+
fn test_error_if_0reserve_negotiates_down_to_legacy() {
2646+
let chanmon_cfgs = create_chanmon_cfgs(2);
2647+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2648+
let channel_config = test_default_channel_config();
2649+
assert!(channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx);
2650+
let node_chanmgrs = create_node_chanmgrs(
2651+
2,
2652+
&node_cfgs,
2653+
&[Some(channel_config.clone()), Some(channel_config.clone())],
2654+
);
2655+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2656+
2657+
let node_b_id = nodes[1].node.get_our_node_id();
2658+
2659+
nodes[0]
2660+
.node
2661+
.create_channel_to_trusted_peer_0reserve(node_b_id, 100_000, 0, 42, None, None)
2662+
.unwrap();
2663+
let open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
2664+
assert_eq!(
2665+
open_channel_msg.common_fields.channel_type,
2666+
Some(ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies())
2667+
);
2668+
assert_eq!(open_channel_msg.channel_reserve_satoshis, 0);
2669+
2670+
let reason = "Don't like your channel".to_owned();
2671+
nodes[0].node.handle_error(
2672+
node_b_id,
2673+
&ErrorMessage {
2674+
channel_id: open_channel_msg.common_fields.temporary_channel_id,
2675+
data: reason.clone(),
2676+
},
2677+
);
2678+
2679+
let reason = ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(reason) };
2680+
let expected_closing = ExpectedCloseEvent::from_id_reason(
2681+
open_channel_msg.common_fields.temporary_channel_id,
2682+
false,
2683+
reason,
2684+
);
2685+
check_closed_events(&nodes[0], &[expected_closing]);
2686+
}

lightning/src/ln/channelmanager.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3617,6 +3617,8 @@ pub enum TrustedChannelFeatures {
36173617
/// with a revoked commitment transaction *for free*.
36183618
///
36193619
/// Note that there is no guarantee that the counterparty accepts such a channel themselves.
3620+
///
3621+
/// The zero-reserve feature is not allowed on legacy / anchorless channels.
36203622
ZeroReserve,
36213623
/// Sets the combination of [`TrustedChannelFeatures::ZeroConf`] and [`TrustedChannelFeatures::ZeroReserve`]
36223624
ZeroConfZeroReserve,
@@ -3873,6 +3875,8 @@ impl<
38733875
/// transaction *for free*.
38743876
///
38753877
/// Note that there is no guarantee that the counterparty accepts such a channel.
3878+
///
3879+
/// The zero-reserve feature is not allowed on legacy / anchorless channels.
38763880
pub fn create_channel_to_trusted_peer_0reserve(
38773881
&self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64,
38783882
user_channel_id: u128, temporary_channel_id: Option<ChannelId>,

0 commit comments

Comments
 (0)