From e467197a12baea6367fba0df960a86f98be2dd27 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 19 Feb 2026 04:45:50 +0000 Subject: [PATCH 1/7] Add inbound and outbound checks for zero reserve channels The goal is to prevent any commitments with no outputs, since these are not broadcastable. --- lightning/src/ln/channel.rs | 92 ++++++---- lightning/src/ln/channel_open_tests.rs | 2 +- lightning/src/ln/functional_tests.rs | 2 +- lightning/src/ln/htlc_reserve_unit_tests.rs | 12 +- lightning/src/ln/payment_tests.rs | 2 +- lightning/src/ln/update_fee_tests.rs | 6 +- lightning/src/sign/tx_builder.rs | 179 ++++++++++++++++++-- 7 files changed, 241 insertions(+), 54 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 9361cd3c749..7c48835af1c 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2786,11 +2786,18 @@ impl FundingScope { .funding_pubkey = counterparty_funding_pubkey; // New reserve values are based on the new channel value and are v2-specific - let counterparty_selected_channel_reserve_satoshis = - get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS); + let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( + post_channel_value, + MIN_CHAN_DUST_LIMIT_SATOSHIS, + prev_funding + .counterparty_selected_channel_reserve_satoshis + .expect("counterparty reserve is set") + == 0, + ); let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( post_channel_value, context.counterparty_dust_limit_satoshis, + prev_funding.holder_selected_channel_reserve_satoshis == 0, ); Self { @@ -5032,27 +5039,27 @@ impl ChannelContext { )); } - if funding.is_outbound() { - let (local_stats, _local_htlcs) = self - .get_next_local_commitment_stats( - funding, - Some(HTLCAmountDirection { outbound: false, amount_msat: msg.amount_msat }), - include_counterparty_unknown_htlcs, - fee_spike_buffer_htlc, - self.feerate_per_kw, - dust_exposure_limiting_feerate, - ) - .map_err(|()| { - ChannelError::close(String::from("Balance exhausted on local commitment")) - })?; - // Check that they won't violate our local required channel reserve by adding this HTLC. - if local_stats.commitment_stats.holder_balance_msat + let (local_stats, _local_htlcs) = self + .get_next_local_commitment_stats( + funding, + Some(HTLCAmountDirection { outbound: false, amount_msat: msg.amount_msat }), + include_counterparty_unknown_htlcs, + fee_spike_buffer_htlc, + self.feerate_per_kw, + dust_exposure_limiting_feerate, + ) + .map_err(|()| { + ChannelError::close(String::from("Balance exhausted on local commitment")) + })?; + + // Check that they won't violate our local required channel reserve by adding this HTLC. + if funding.is_outbound() + && local_stats.commitment_stats.holder_balance_msat < funding.counterparty_selected_channel_reserve_satoshis.unwrap() * 1000 - { - return Err(ChannelError::close( - "Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value".to_owned() - )); - } + { + return Err(ChannelError::close( + "Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value".to_owned() + )); } Ok(()) @@ -5146,6 +5153,12 @@ impl ChannelContext { let commitment_txid = { let trusted_tx = commitment_data.tx.trust(); let bitcoin_tx = trusted_tx.built_transaction(); + if bitcoin_tx.transaction.output.is_empty() { + return Err(ChannelError::close( + "Commitment tx from peer has 0 outputs".to_owned(), + )); + } + let sighash = bitcoin_tx.get_sighash_all(&funding_script, funding.get_value_satoshis()); log_trace!(logger, "Checking commitment tx signature {} by key {} against tx {} (sighash {}) with redeemscript {} in channel {}", @@ -6282,8 +6295,11 @@ fn get_holder_max_htlc_value_in_flight_msat( /// This is used both for outbound and inbound channels and has lower bound /// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`. pub(crate) fn get_holder_selected_channel_reserve_satoshis( - channel_value_satoshis: u64, config: &UserConfig, + channel_value_satoshis: u64, config: &UserConfig, is_0reserve: bool, ) -> u64 { + if is_0reserve { + return 0; + } let counterparty_chan_reserve_prop_mil = config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64; let calculated_reserve = @@ -6309,7 +6325,12 @@ pub(crate) fn get_legacy_default_holder_selected_channel_reserve_satoshis( /// /// This is used both for outbound and inbound channels and has lower bound /// of `dust_limit_satoshis`. -fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satoshis: u64) -> u64 { +fn get_v2_channel_reserve_satoshis( + channel_value_satoshis: u64, dust_limit_satoshis: u64, is_0reserve: bool, +) -> u64 { + if is_0reserve { + return 0; + } // Fixed at 1% of channel value by spec. let (q, _) = channel_value_satoshis.overflowing_div(100); cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) @@ -12033,12 +12054,19 @@ where our_funding_contribution.to_sat(), their_funding_contribution.to_sat(), ); - let counterparty_selected_channel_reserve = Amount::from_sat( - get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS), - ); + let counterparty_selected_channel_reserve = + Amount::from_sat(get_v2_channel_reserve_satoshis( + post_channel_value, + MIN_CHAN_DUST_LIMIT_SATOSHIS, + self.funding + .counterparty_selected_channel_reserve_satoshis + .expect("counterparty reserve is set") + == 0, + )); let holder_selected_channel_reserve = Amount::from_sat(get_v2_channel_reserve_satoshis( post_channel_value, self.context.counterparty_dust_limit_satoshis, + self.funding.holder_selected_channel_reserve_satoshis == 0, )); // We allow parties to draw from their previous reserve, as long as they satisfy their v2 reserve @@ -13267,7 +13295,7 @@ impl OutboundV1Channel { channel_value_satoshis: u64, push_msat: u64, user_id: u128, config: &UserConfig, current_chain_height: u32, outbound_scid_alias: u64, temporary_channel_id: Option, logger: L ) -> Result, APIError> { - let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config); + let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config, false); if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { // Protocol level safety check in place, although it should never happen because // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` @@ -13649,7 +13677,7 @@ impl InboundV1Channel { // support this channel type. let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; - let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(msg.common_fields.funding_satoshis, config); + let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(msg.common_fields.funding_satoshis, config, false); let counterparty_pubkeys = ChannelPublicKeys { funding_pubkey: msg.common_fields.funding_pubkey, revocation_basepoint: RevocationBasepoint::from(msg.common_fields.revocation_basepoint), @@ -13903,7 +13931,7 @@ impl PendingV2Channel { }); let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); + funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, false); let funding_feerate_sat_per_1000_weight = fee_estimator.bounded_sat_per_1000_weight(funding_confirmation_target); let funding_tx_locktime = LockTime::from_height(current_chain_height) @@ -14042,9 +14070,9 @@ impl PendingV2Channel { let channel_value_satoshis = our_funding_contribution_sats.saturating_add(msg.common_fields.funding_satoshis); let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - channel_value_satoshis, msg.common_fields.dust_limit_satoshis); + channel_value_satoshis, msg.common_fields.dust_limit_satoshis, false); let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); + channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, false); let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs index 08cabc053c5..16375a91774 100644 --- a/lightning/src/ln/channel_open_tests.rs +++ b/lightning/src/ln/channel_open_tests.rs @@ -470,7 +470,7 @@ pub fn test_insane_channel_opens() { // funding satoshis let channel_value_sat = 31337; // same as funding satoshis let channel_reserve_satoshis = - get_holder_selected_channel_reserve_satoshis(channel_value_sat, &legacy_cfg); + get_holder_selected_channel_reserve_satoshis(channel_value_sat, &legacy_cfg, false); let push_msat = (channel_value_sat - channel_reserve_satoshis) * 1000; // Have node0 initiate a channel to node1 with aforementioned parameters diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 09a87d93156..a27c7d07560 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -413,7 +413,7 @@ pub fn test_inbound_outbound_capacity_is_not_zero() { assert_eq!(channels0.len(), 1); assert_eq!(channels1.len(), 1); - let reserve = get_holder_selected_channel_reserve_satoshis(100_000, &default_config); + let reserve = get_holder_selected_channel_reserve_satoshis(100_000, &default_config, false); assert_eq!(channels0[0].inbound_capacity_msat, 95000000 - reserve * 1000); assert_eq!(channels1[0].outbound_capacity_msat, 95000000 - reserve * 1000); diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index d88b9a2dc3f..0fce34b2b9b 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -50,7 +50,8 @@ fn do_test_counterparty_no_reserve(send_from_initiator: bool) { push_amt -= feerate_per_kw as u64 * (commitment_tx_base_weight(&channel_type_features) + 4 * COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000 * 1000; - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config, false) * 1000; let push = if send_from_initiator { 0 } else { push_amt }; let temp_channel_id = @@ -1008,7 +1009,8 @@ pub fn test_chan_reserve_violation_outbound_htlc_inbound_chan() { &channel_type_features, ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config, false) * 1000; let _ = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt); @@ -1052,7 +1054,8 @@ pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { MIN_AFFORDABLE_HTLC_COUNT as u64, &channel_type_features, ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config, false) * 1000; let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt); // Send four HTLCs to cover the initial push_msat buffer we're required to include @@ -1130,7 +1133,8 @@ pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() { MIN_AFFORDABLE_HTLC_COUNT as u64, &channel_type_features, ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config, false) * 1000; create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, push_amt); let (htlc_success_tx_fee_sat, _) = diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index b5cbe0fee98..e830d2b2389 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -4985,7 +4985,7 @@ fn test_htlc_forward_considers_anchor_outputs_value() { create_announced_chan_between_nodes_with_value(&nodes, 1, 2, CHAN_AMT, PUSH_MSAT); let channel_reserve_msat = - get_holder_selected_channel_reserve_satoshis(CHAN_AMT, &config) * 1000; + get_holder_selected_channel_reserve_satoshis(CHAN_AMT, &config, false) * 1000; let commitment_fee_msat = chan_utils::commit_tx_fee_sat( *nodes[1].fee_estimator.sat_per_kw.lock().unwrap(), 2, diff --git a/lightning/src/ln/update_fee_tests.rs b/lightning/src/ln/update_fee_tests.rs index ac566393bdb..a478d78b546 100644 --- a/lightning/src/ln/update_fee_tests.rs +++ b/lightning/src/ln/update_fee_tests.rs @@ -408,7 +408,8 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann ); let channel_id = chan.2; let secp_ctx = Secp256k1::new(); - let bs_channel_reserve_sats = get_holder_selected_channel_reserve_satoshis(channel_value, &cfg); + let bs_channel_reserve_sats = + get_holder_selected_channel_reserve_satoshis(channel_value, &cfg, false); let (anchor_outputs_value_sats, outputs_num_no_htlcs) = if channel_type_features.supports_anchors_zero_fee_htlc_tx() { (ANCHOR_OUTPUT_VALUE_SATOSHI * 2, 4) @@ -892,7 +893,8 @@ pub fn test_chan_init_feerate_unaffordability() { // During open, we don't have a "counterparty channel reserve" to check against, so that // requirement only comes into play on the open_channel handling side. - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config, false) * 1000; nodes[0].node.create_channel(node_b_id, 100_000, push_amt, 42, None, None).unwrap(); let mut open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index 4273b62c7b7..6362647f26a 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -206,6 +206,35 @@ fn get_dust_exposure_stats( } } +fn has_output( + is_outbound_from_holder: bool, holder_balance_before_fee_msat: u64, + counterparty_balance_before_fee_msat: u64, feerate_per_kw: u32, nondust_htlc_count: usize, + broadcaster_dust_limit_satoshis: u64, channel_type: &ChannelTypeFeatures, +) -> bool { + let commit_tx_fee_sat = commit_tx_fee_sat(feerate_per_kw, nondust_htlc_count, channel_type); + + let (real_holder_balance_msat, real_counterparty_balance_msat) = if is_outbound_from_holder { + ( + holder_balance_before_fee_msat.saturating_sub(commit_tx_fee_sat * 1000), + counterparty_balance_before_fee_msat, + ) + } else { + ( + holder_balance_before_fee_msat, + counterparty_balance_before_fee_msat.saturating_sub(commit_tx_fee_sat * 1000), + ) + }; + + // Make sure the commitment transaction has at least one output + let dust_limit_msat = broadcaster_dust_limit_satoshis * 1000; + let has_no_output = real_holder_balance_msat < dust_limit_msat + && real_counterparty_balance_msat < dust_limit_msat + && nondust_htlc_count == 0 + // 0FC channels always have a P2A output on the commitment transaction + && !channel_type.supports_anchor_zero_fee_commitments(); + !has_no_output +} + fn get_next_commitment_stats( local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection], @@ -250,6 +279,15 @@ fn get_next_commitment_stats( channel_type, )?; + let (dust_exposure_msat, _extra_accepted_htlc_dust_exposure_msat) = get_dust_exposure_stats( + local, + next_commitment_htlcs, + feerate_per_kw, + dust_exposure_limiting_feerate, + broadcaster_dust_limit_satoshis, + channel_type, + ); + // Calculate fees on commitment transaction let nondust_htlc_count = next_commitment_htlcs .iter() @@ -257,18 +295,27 @@ fn get_next_commitment_stats( !htlc.is_dust(local, feerate_per_kw, broadcaster_dust_limit_satoshis, channel_type) }) .count(); - let commit_tx_fee_sat = commit_tx_fee_sat( + + // For zero-reserve channels, we check two things independently: + // 1) Given the current set of HTLCs and feerate, does the commitment have at least one output ? + if !has_output( + is_outbound_from_holder, + holder_balance_before_fee_msat, + counterparty_balance_before_fee_msat, feerate_per_kw, - nondust_htlc_count + addl_nondust_htlc_count, + nondust_htlc_count, + broadcaster_dust_limit_satoshis, channel_type, - ); + ) { + return Err(()); + } - let (dust_exposure_msat, _extra_accepted_htlc_dust_exposure_msat) = get_dust_exposure_stats( - local, - next_commitment_htlcs, + // 2) Now including any additional non-dust HTLCs (usually the fee spike buffer HTLC), does the funder cover + // this bigger transaction fee ? The funder can dip below their dust limit to cover this case, as the + // commitment will have at least one output: the non-dust fee spike buffer HTLC offered by the counterparty. + let commit_tx_fee_sat = commit_tx_fee_sat( feerate_per_kw, - dust_exposure_limiting_feerate, - broadcaster_dust_limit_satoshis, + nondust_htlc_count + addl_nondust_htlc_count, channel_type, ); @@ -316,7 +363,7 @@ fn get_available_balances( if channel_type.supports_anchor_zero_fee_commitments() { 0 } else { 1 }; // Note that the feerate is 0 in zero-fee commitment channels, so this statement is a noop - let local_feerate = feerate_per_kw + let spiked_feerate = feerate_per_kw * if is_outbound_from_holder && !channel_type.supports_anchors_zero_fee_htlc_tx() { crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32 } else { @@ -328,19 +375,19 @@ fn get_available_balances( .filter(|htlc| { !htlc.is_dust( true, - local_feerate, + spiked_feerate, channel_constraints.holder_dust_limit_satoshis, channel_type, ) }) .count(); let local_max_commit_tx_fee_sat = commit_tx_fee_sat( - local_feerate, + spiked_feerate, local_nondust_htlc_count + fee_spike_buffer_htlc + 1, channel_type, ); let local_min_commit_tx_fee_sat = commit_tx_fee_sat( - local_feerate, + spiked_feerate, local_nondust_htlc_count + fee_spike_buffer_htlc, channel_type, ); @@ -512,7 +559,49 @@ fn get_available_balances( available_capacity_msat = 0; } - #[allow(deprecated)] // TODO: Remove once balance_msat is removed + // Now adjust our min and max size HTLC to make sure both the local and the remote commitments still have + // at least one output at the spiked feerate. + + let remote_nondust_htlc_count = pending_htlcs + .iter() + .filter(|htlc| { + !htlc.is_dust( + false, + spiked_feerate, + channel_constraints.counterparty_dust_limit_satoshis, + channel_type, + ) + }) + .count(); + + let (next_outbound_htlc_minimum_msat, available_capacity_msat) = + adjust_boundaries_if_max_dust_htlc_produces_no_output( + true, + is_outbound_from_holder, + local_balance_before_fee_msat, + remote_balance_before_fee_msat, + local_nondust_htlc_count, + spiked_feerate, + channel_constraints.holder_dust_limit_satoshis, + channel_type, + next_outbound_htlc_minimum_msat, + available_capacity_msat, + ); + + let (next_outbound_htlc_minimum_msat, available_capacity_msat) = + adjust_boundaries_if_max_dust_htlc_produces_no_output( + false, + is_outbound_from_holder, + local_balance_before_fee_msat, + remote_balance_before_fee_msat, + remote_nondust_htlc_count, + spiked_feerate, + channel_constraints.counterparty_dust_limit_satoshis, + channel_type, + next_outbound_htlc_minimum_msat, + available_capacity_msat, + ); + crate::ln::channel::AvailableBalances { inbound_capacity_msat: remote_balance_before_fee_msat .saturating_sub(channel_constraints.holder_selected_channel_reserve_satoshis * 1000), @@ -522,6 +611,70 @@ fn get_available_balances( } } +fn adjust_boundaries_if_max_dust_htlc_produces_no_output( + local: bool, is_outbound_from_holder: bool, holder_balance_before_fee_msat: u64, + counterparty_balance_before_fee_msat: u64, nondust_htlc_count: usize, spiked_feerate: u32, + dust_limit_satoshis: u64, channel_type: &ChannelTypeFeatures, + next_outbound_htlc_minimum_msat: u64, available_capacity_msat: u64, +) -> (u64, u64) { + // First, determine the biggest dust HTLC we could send + let (htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = + second_stage_tx_fees_sat(channel_type, spiked_feerate); + let min_nondust_htlc_sat = + dust_limit_satoshis + if local { htlc_timeout_tx_fee_sat } else { htlc_success_tx_fee_sat }; + let max_dust_htlc_msat = (min_nondust_htlc_sat.saturating_mul(1000)).saturating_sub(1); + + // If this dust HTLC produces no outputs, then we have to say something! It is now possible to produce a + // commitment with no outputs. + if !has_output( + is_outbound_from_holder, + holder_balance_before_fee_msat.saturating_sub(max_dust_htlc_msat), + counterparty_balance_before_fee_msat, + spiked_feerate, + nondust_htlc_count, + dust_limit_satoshis, + channel_type, + ) { + // If we are allowed to send non-dust HTLCs, set the min HTLC to the smallest non-dust HTLC... + if available_capacity_msat >= min_nondust_htlc_sat.saturating_mul(1000) { + ( + cmp::max( + min_nondust_htlc_sat.saturating_mul(1000), + next_outbound_htlc_minimum_msat, + ), + available_capacity_msat, + ) + // Otherwise, set the max HTLC to the biggest that still leaves our main balance output untrimmed. + // Note that this will be a dust HTLC. + } else { + // Remember we've got no non-dust HTLCs on the commitment here + let current_spiked_tx_fee_sat = commit_tx_fee_sat(spiked_feerate, 0, channel_type); + let spike_buffer_tx_fee_sat = commit_tx_fee_sat(spiked_feerate, 1, channel_type); + // We must cover the greater of + // 1) The dust_limit_satoshis plus the fee of the existing commitment at the spiked feerate. + // 2) The fee of the commitment with an additional non-dust HTLC, aka the fee spike buffer HTLC. + // In this case we don't mind the holder balance output dropping below the dust limit, as + // this additional non-dust HTLC will create the single remaining output on the commitment. + let min_balance_msat = + cmp::max(dust_limit_satoshis + current_spiked_tx_fee_sat, spike_buffer_tx_fee_sat) + * 1000; + ( + next_outbound_htlc_minimum_msat, + // We make no assumptions about the size of `available_capacity_msat` passed to this + // function, we only care that the new `available_capacity_msat` is under + // `holder_balance_before_fee_msat - min_balance_msat` + cmp::min( + holder_balance_before_fee_msat.saturating_sub(min_balance_msat), + available_capacity_msat, + ), + ) + } + // Otherwise, it is impossible to produce no outputs with this upcoming HTLC add, so we stay quiet + } else { + (next_outbound_htlc_minimum_msat, available_capacity_msat) + } +} + pub(crate) trait TxBuilder { fn get_channel_stats( &self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, From 7625f14dbef54c47aea50dba5f41453fecf9d241 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 26 Feb 2026 03:10:47 +0000 Subject: [PATCH 2/7] Add 0-reserve to `accept_inbound_channel_from_trusted_peer` This new flag sets 0-reserve for the channel opener. --- .../tests/lsps2_integration_tests.rs | 7 +- lightning/src/events/mod.rs | 2 +- lightning/src/ln/async_signer_tests.rs | 8 +- lightning/src/ln/chanmon_update_fail_tests.rs | 18 ++++- lightning/src/ln/channel.rs | 48 ++++++----- lightning/src/ln/channel_open_tests.rs | 10 ++- lightning/src/ln/channel_type_tests.rs | 14 ++-- lightning/src/ln/channelmanager.rs | 80 ++++++++++++++----- lightning/src/ln/functional_test_utils.rs | 5 +- lightning/src/ln/priv_short_conf_tests.rs | 15 ++-- lightning/src/util/config.rs | 4 +- 11 files changed, 138 insertions(+), 73 deletions(-) diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 33a6dd697cf..e34fdc5e613 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; @@ -1513,10 +1515,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 3f6bb0efb01..1f11d3c0ee2 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1643,7 +1643,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 451af3918bf..c3ac81623c6 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) { 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 cd32d219b93..c70c99e5274 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, 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, @@ -3235,7 +3235,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(); @@ -3344,7 +3350,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 7c48835af1c..94b127cca38 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::{FundingContribution, FundingTemplate, FundingTxInput}; @@ -3582,7 +3582,7 @@ impl ChannelContext { config: &'a UserConfig, current_chain_height: u32, logger: &'a L, - is_0conf: bool, + trusted_channel_features: Option, our_funding_satoshis: u64, counterparty_pubkeys: ChannelPublicKeys, channel_type: ChannelTypeFeatures, @@ -3663,7 +3663,7 @@ impl ChannelContext { } } - 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))); @@ -3675,7 +3675,7 @@ impl ChannelContext { 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))); } @@ -3724,7 +3724,7 @@ impl ChannelContext { 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)) @@ -13669,7 +13669,8 @@ impl InboundV1Channel { fee_estimator: &LowerBoundedFeeEstimator, 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, ) -> Result, ChannelError> { let logger = WithContext::from(logger, Some(counterparty_node_id), Some(msg.common_fields.temporary_channel_id), None); @@ -13677,7 +13678,11 @@ impl InboundV1Channel { // support this channel type. let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; - let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(msg.common_fields.funding_satoshis, config, false); + let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis( + msg.common_fields.funding_satoshis, + config, + trusted_channel_features.is_some_and(|f| f.is_0reserve()), + ); let counterparty_pubkeys = ChannelPublicKeys { funding_pubkey: msg.common_fields.funding_pubkey, revocation_basepoint: RevocationBasepoint::from(msg.common_fields.revocation_basepoint), @@ -13696,7 +13701,7 @@ impl InboundV1Channel { config, current_chain_height, &&logger, - is_0conf, + trusted_channel_features, 0, counterparty_pubkeys, @@ -14094,7 +14099,7 @@ impl PendingV2Channel { config, current_chain_height, logger, - false, + None, our_funding_contribution_sats, counterparty_pubkeys, channel_type, @@ -15734,7 +15739,7 @@ mod tests { OutboundHTLCOutput, OutboundHTLCState, OutboundV1Channel, 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}; @@ -15940,7 +15945,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(); @@ -16085,7 +16090,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(); @@ -16160,12 +16165,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); @@ -16184,14 +16189,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); } @@ -16244,7 +16249,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); @@ -16252,7 +16257,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()); } } @@ -16279,7 +16284,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(); @@ -16382,7 +16387,7 @@ mod tests { &config, 0, &&logger, - false, + None, ) .unwrap(); outbound_chan @@ -18037,7 +18042,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 16375a91774..1adaf8abe6e 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(); @@ -969,7 +971,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, _)) => { @@ -1029,7 +1031,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 2b069a6d314..dc586555f39 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 ada27af749f..91805989fc0 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3459,6 +3459,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 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, T: BroadcasterInterface, @@ -10710,10 +10752,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 @@ -10729,38 +10771,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, + user_channel_id: u128, trusted_channel_features: TrustedChannelFeatures, + config_overrides: Option, ) -> Result<(), APIError> { self.do_accept_inbound_channel( temporary_channel_id, counterparty_node_id, - true, + Some(trusted_channel_features), user_channel_id, config_overrides, ) @@ -10769,7 +10805,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, user_channel_id: u128, config_overrides: Option, ) -> Result<(), APIError> { let mut config = self.config.read().unwrap().clone(); @@ -10818,7 +10854,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) @@ -10895,7 +10931,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() { @@ -10910,7 +10946,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 2d971c3a100..4720bef685e 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}; @@ -1638,10 +1638,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 ffe5ea6cbb1..6ea67f235e7 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 e4158910b9a..14c507184ac 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 From 3a371729751bda5028eebb6995085a7c0daf3720 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 19 Feb 2026 07:32:12 +0000 Subject: [PATCH 3/7] Add `ChannelManager::create_channel_to_trusted_peer_0reserve` This new method sets 0-reserve for the channel accepter. --- lightning/src/ln/channel.rs | 42 +++++++++++++--------- lightning/src/ln/channel_open_tests.rs | 1 + lightning/src/ln/channel_type_tests.rs | 7 ++++ lightning/src/ln/channelmanager.rs | 50 ++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 94b127cca38..23ea68d8d1a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -4393,7 +4393,7 @@ impl ChannelContext { if channel_reserve_satoshis > funding.get_value_satoshis() { return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than ({})", channel_reserve_satoshis, funding.get_value_satoshis()))); } - if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis { + if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis && funding.holder_selected_channel_reserve_satoshis != 0 { return Err(ChannelError::close(format!("Dust limit ({}) is bigger than our channel reserve ({})", common_fields.dust_limit_satoshis, funding.holder_selected_channel_reserve_satoshis))); } if channel_reserve_satoshis > funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis { @@ -13293,14 +13293,19 @@ impl OutboundV1Channel { pub fn new( fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, their_features: &InitFeatures, channel_value_satoshis: u64, push_msat: u64, user_id: u128, config: &UserConfig, current_chain_height: u32, - outbound_scid_alias: u64, temporary_channel_id: Option, logger: L + outbound_scid_alias: u64, temporary_channel_id: Option, logger: L, trusted_channel_features: Option, ) -> Result, APIError> { - let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config, false); - if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { + let is_0reserve = trusted_channel_features.is_some_and(|f| f.is_0reserve()); + let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis( + channel_value_satoshis, + config, + is_0reserve, + ); + if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS && !is_0reserve { // Protocol level safety check in place, although it should never happen because // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` return Err(APIError::APIMisuseError { err: format!("Holder selected channel reserve below \ - implemention limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) }); + implementation limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) }); } let channel_keys_id = signer_provider.generate_channel_keys_id(false, user_id); @@ -15883,6 +15888,7 @@ mod tests { 42, None, &logger, + None, ); match res { Err(APIError::IncompatibleShutdownScript { script }) => { @@ -15909,7 +15915,7 @@ mod tests { let node_a_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let config = UserConfig::default(); - let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&bounded_fee_estimator, &&keys_provider, &&keys_provider, node_a_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&bounded_fee_estimator, &&keys_provider, &&keys_provider, node_a_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger, None).unwrap(); // Now change the fee so we can check that the fee in the open_channel message is the // same as the old fee. @@ -15939,7 +15945,7 @@ mod tests { let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let mut config = UserConfig::default(); config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; - let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10_000_000, 100_000_000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10_000_000, 100_000_000, 42, &config, 0, 42, None, &logger, None).unwrap(); // Create Node B's channel by receiving Node A's open_channel message // Make sure A's dust limit is as we expect. @@ -16030,7 +16036,7 @@ mod tests { let node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let mut config = UserConfig::default(); config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; - let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&fee_est, &&keys_provider, &&keys_provider, node_id, &channelmanager::provided_init_features(&config), 10_000_000, 100_000_000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&fee_est, &&keys_provider, &&keys_provider, node_id, &channelmanager::provided_init_features(&config), 10_000_000, 100_000_000, 42, &config, 0, 42, None, &logger, None).unwrap(); chan.context.counterparty_max_htlc_value_in_flight_msat = 1_000_000_000; let commitment_tx_fee_0_htlcs = commit_tx_fee_sat(chan.context.feerate_per_kw, 0, chan.funding.get_channel_type()) * 1000; @@ -16085,7 +16091,7 @@ mod tests { // Create Node A's channel pointing to Node B's pubkey let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let config = UserConfig::default(); - let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger, None).unwrap(); // 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(); @@ -16151,12 +16157,12 @@ mod tests { // Test that `OutboundV1Channel::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 mut chan_1 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_2_percent), 10000000, 100000, 42, &config_2_percent, 0, 42, None, &logger).unwrap(); + let mut chan_1 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_2_percent), 10000000, 100000, 42, &config_2_percent, 0, 42, None, &logger, None).unwrap(); let chan_1_value_msat = chan_1.funding.get_value_satoshis() * 1000; assert_eq!(chan_1.context.holder_max_htlc_value_in_flight_msat, (chan_1_value_msat as f64 * 0.02) as u64); // Test with the upper bound - 1 of valid values (99%). - let chan_2 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_99_percent), 10000000, 100000, 42, &config_99_percent, 0, 42, None, &logger).unwrap(); + let chan_2 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_99_percent), 10000000, 100000, 42, &config_99_percent, 0, 42, None, &logger, None).unwrap(); let chan_2_value_msat = chan_2.funding.get_value_satoshis() * 1000; assert_eq!(chan_2.context.holder_max_htlc_value_in_flight_msat, (chan_2_value_msat as f64 * 0.99) as u64); @@ -16176,14 +16182,14 @@ mod tests { // Test that `OutboundV1Channel::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_5 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_0_percent), 10000000, 100000, 42, &config_0_percent, 0, 42, None, &logger).unwrap(); + let chan_5 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_0_percent), 10000000, 100000, 42, &config_0_percent, 0, 42, None, &logger, None).unwrap(); let chan_5_value_msat = chan_5.funding.get_value_satoshis() * 1000; assert_eq!(chan_5.context.holder_max_htlc_value_in_flight_msat, (chan_5_value_msat as f64 * 0.01) as u64); // Test that `OutboundV1Channel::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_6 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_101_percent), 10000000, 100000, 42, &config_101_percent, 0, 42, None, &logger).unwrap(); + let chan_6 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_101_percent), 10000000, 100000, 42, &config_101_percent, 0, 42, None, &logger, None).unwrap(); let chan_6_value_msat = chan_6.funding.get_value_satoshis() * 1000; assert_eq!(chan_6.context.holder_max_htlc_value_in_flight_msat, chan_6_value_msat); @@ -16239,7 +16245,7 @@ mod tests { let mut outbound_node_config = UserConfig::default(); outbound_node_config.channel_handshake_config.their_channel_reserve_proportional_millionths = (outbound_selected_channel_reserve_perc * 1_000_000.0) as u32; - let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&outbound_node_config), channel_value_satoshis, 100_000, 42, &outbound_node_config, 0, 42, None, &logger).unwrap(); + let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&outbound_node_config), channel_value_satoshis, 100_000, 42, &outbound_node_config, 0, 42, None, &logger, None).unwrap(); let expected_outbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * outbound_selected_channel_reserve_perc) as u64); assert_eq!(chan.funding.holder_selected_channel_reserve_satoshis, expected_outbound_selected_chan_reserve); @@ -16278,7 +16284,7 @@ mod tests { // Create Node A's channel pointing to Node B's pubkey let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let config = UserConfig::default(); - let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger, None).unwrap(); // Create Node B's channel by receiving Node A's open_channel message // Make sure A's dust limit is as we expect. @@ -16370,6 +16376,7 @@ mod tests { 42, None, &logger, + None, ) .unwrap(); let open_channel_msg = &outbound_chan @@ -16726,6 +16733,7 @@ mod tests { 42, None, &*logger, + None, ) .unwrap(); // Nothing uses their network key in this test chan.context.holder_dust_limit_satoshis = 546; @@ -17450,6 +17458,7 @@ mod tests { 0, None, &*logger, + None, ) .unwrap(); @@ -18025,7 +18034,8 @@ mod tests { 0, 42, None, - &logger + &logger, + None, ).unwrap(); let open_channel_msg = node_a_chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap(); diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs index 1adaf8abe6e..c8e016cc796 100644 --- a/lightning/src/ln/channel_open_tests.rs +++ b/lightning/src/ln/channel_open_tests.rs @@ -940,6 +940,7 @@ pub fn test_user_configurable_csv_delay() { 42, None, &logger, + None, ) { match error { APIError::APIMisuseError { err } => { diff --git a/lightning/src/ln/channel_type_tests.rs b/lightning/src/ln/channel_type_tests.rs index dc586555f39..77caa8a2bc4 100644 --- a/lightning/src/ln/channel_type_tests.rs +++ b/lightning/src/ln/channel_type_tests.rs @@ -144,6 +144,7 @@ fn test_zero_conf_channel_type_support() { 42, None, &logger, + None, ) .unwrap(); @@ -244,6 +245,7 @@ fn do_test_supports_channel_type(config: UserConfig, expected_channel_type: Chan 42, None, &logger, + None, ) .unwrap(); assert_eq!( @@ -265,6 +267,7 @@ fn do_test_supports_channel_type(config: UserConfig, expected_channel_type: Chan 42, None, &logger, + None, ) .unwrap(); @@ -330,6 +333,7 @@ fn test_rejects_if_channel_type_not_set() { 42, None, &logger, + None, ) .unwrap(); @@ -416,6 +420,7 @@ fn test_rejects_if_channel_type_differ() { 42, None, &logger, + None, ) .unwrap(); @@ -499,6 +504,7 @@ fn test_rejects_simple_anchors_channel_type() { 42, None, &logger, + None, ) .unwrap(); @@ -540,6 +546,7 @@ fn test_rejects_simple_anchors_channel_type() { 42, None, &logger, + None, ) .unwrap(); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 91805989fc0..4d813c4d4cb 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3712,8 +3712,52 @@ impl< /// [`Event::FundingGenerationReady::user_channel_id`]: events::Event::FundingGenerationReady::user_channel_id /// [`Event::FundingGenerationReady::temporary_channel_id`]: events::Event::FundingGenerationReady::temporary_channel_id /// [`Event::ChannelClosed::channel_id`]: events::Event::ChannelClosed::channel_id - #[rustfmt::skip] - pub fn create_channel(&self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, user_channel_id: u128, temporary_channel_id: Option, override_config: Option) -> Result { + pub fn create_channel( + &self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, + user_channel_id: u128, temporary_channel_id: Option, + override_config: Option, + ) -> Result { + self.create_channel_internal( + their_network_key, + channel_value_satoshis, + push_msat, + user_channel_id, + temporary_channel_id, + override_config, + None, + ) + } + + /// Creates a new outbound channel to the given remote node and with the given value. + /// + /// The only difference between this method and [`ChannelManager::create_channel`] is that this method 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. + pub fn create_channel_to_trusted_peer_0reserve( + &self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, + user_channel_id: u128, temporary_channel_id: Option, + override_config: Option, + ) -> Result { + self.create_channel_internal( + their_network_key, + channel_value_satoshis, + push_msat, + user_channel_id, + temporary_channel_id, + override_config, + Some(TrustedChannelFeatures::ZeroReserve), + ) + } + + fn create_channel_internal( + &self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, + user_channel_id: u128, temporary_channel_id: Option, + override_config: Option, + trusted_channel_features: Option, + ) -> Result { if channel_value_satoshis < 1000 { return Err(APIError::APIMisuseError { err: format!("Channel value must be at least 1000 satoshis. It was {}", channel_value_satoshis) }); } @@ -3749,7 +3793,7 @@ impl< }; match OutboundV1Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, their_network_key, their_features, channel_value_satoshis, push_msat, user_channel_id, config, - self.best_block.read().unwrap().height, outbound_scid_alias, temporary_channel_id, &self.logger) + self.best_block.read().unwrap().height, outbound_scid_alias, temporary_channel_id, &self.logger, trusted_channel_features) { Ok(res) => res, Err(e) => { From 46ba1b3d249d293b8a84989249ec10123fbc1f1b Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 8 Feb 2026 01:24:17 +0000 Subject: [PATCH 4/7] Shakedown zero reserve channels --- lightning/src/ln/htlc_reserve_unit_tests.rs | 1077 ++++++++++++++++++- 1 file changed, 1071 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index 0fce34b2b9b..89edcd60359 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -2,29 +2,33 @@ use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaymentPurpose}; use crate::ln::chan_utils::{ - self, commitment_tx_base_weight, second_stage_tx_fees_sat, CommitmentTransaction, - COMMITMENT_TX_WEIGHT_PER_HTLC, + self, commit_tx_fee_sat, commitment_tx_base_weight, second_stage_tx_fees_sat, + shared_anchor_script_pubkey, CommitmentTransaction, COMMITMENT_TX_WEIGHT_PER_HTLC, + TRUC_CHILD_MAX_WEIGHT, }; use crate::ln::channel::{ - get_holder_selected_channel_reserve_satoshis, Channel, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, - MIN_AFFORDABLE_HTLC_COUNT, MIN_CHAN_DUST_LIMIT_SATOSHIS, + get_holder_selected_channel_reserve_satoshis, Channel, ANCHOR_OUTPUT_VALUE_SATOSHI, + FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MIN_AFFORDABLE_HTLC_COUNT, + MIN_CHAN_DUST_LIMIT_SATOSHIS, }; -use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder}; +use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder, TrustedChannelFeatures}; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; use crate::ln::onion_utils::{self, AttributionData}; use crate::ln::outbound_payment::RecipientOnionFields; +use crate::ln::types::ChannelId; use crate::routing::router::PaymentParameters; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::tx_builder::{SpecTxBuilder, TxBuilder}; use crate::types::features::ChannelTypeFeatures; -use crate::types::payment::PaymentPreimage; +use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::config::UserConfig; use crate::util::errors::APIError; use lightning_macros::xtest; use bitcoin::secp256k1::{Secp256k1, SecretKey}; +use bitcoin::{Amount, Transaction}; fn do_test_counterparty_no_reserve(send_from_initiator: bool) { // A peer providing a channel_reserve_satoshis of 0 (or less than our dust limit) is insecure, @@ -2443,3 +2447,1064 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { check_added_monitors(&nodes[1], 3); } } + +#[xtest(feature = "_externalize_tests")] +fn test_create_channel_to_trusted_peer_0reserve() { + let mut config = test_default_channel_config(); + + // Legacy channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let channel_type = do_test_create_channel_to_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::only_static_remote_key()); + + // Anchor channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let channel_type = do_test_create_channel_to_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + // 0FC channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + let channel_type = do_test_create_channel_to_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_fee_commitments()); +} + +fn do_test_create_channel_to_trusted_peer_0reserve(mut config: UserConfig) -> ChannelTypeFeatures { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + 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(); + + let channel_value_sat = 100_000; + + let temp_channel_id = nodes[0] + .node + .create_channel_to_trusted_peer_0reserve(node_b_id, channel_value_sat, 0, 42, None, None) + .unwrap(); + let mut open_channel_message = + get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + handle_and_accept_open_channel(&nodes[1], node_a_id, &open_channel_message); + let mut accept_channel_message = + get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id); + nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_message); + let funding_tx = sign_funding_transaction(&nodes[0], &nodes[1], 100_000, temp_channel_id); + let funding_msgs = + create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &funding_tx); + create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &funding_msgs.0); + + let details = &nodes[0].node.list_channels()[0]; + let reserve_sat = details.unspendable_punishment_reserve.unwrap(); + assert_ne!(reserve_sat, 0); + let channel_type = details.channel_type.clone().unwrap(); + let feerate_per_kw = details.feerate_sat_per_1000_weight.unwrap(); + let anchors_sat = + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + 2 * 330 + } else { + 0 + }; + let spike_multiple = if channel_type == ChannelTypeFeatures::only_static_remote_key() { + FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32 + } else { + 1 + }; + let spiked_feerate = spike_multiple * feerate_per_kw; + let reserved_commit_tx_fee_sat = chan_utils::commit_tx_fee_sat( + spiked_feerate, + 2, // We reserve space for two HTLCs, the next outbound non-dust HTLC, and the fee spike buffer HTLC + &channel_type, + ); + + let max_outbound_htlc_sat = + channel_value_sat - anchors_sat - reserved_commit_tx_fee_sat - reserve_sat; + assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000); + send_payment(&nodes[0], &[&nodes[1]], max_outbound_htlc_sat * 1000); + + let details = &nodes[1].node.list_channels()[0]; + assert_eq!(details.unspendable_punishment_reserve.unwrap(), 0); + assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000); + send_payment(&nodes[1], &[&nodes[0]], max_outbound_htlc_sat * 1000); + + channel_type +} + +#[xtest(feature = "_externalize_tests")] +fn test_accept_inbound_channel_from_trusted_peer_0reserve() { + let mut config = test_default_channel_config(); + + // Legacy channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let channel_type = do_test_accept_inbound_channel_from_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::only_static_remote_key()); + + // Anchor channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let channel_type = do_test_accept_inbound_channel_from_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + // 0FC channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + let channel_type = do_test_accept_inbound_channel_from_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_fee_commitments()); +} + +fn do_test_accept_inbound_channel_from_trusted_peer_0reserve( + mut config: UserConfig, +) -> ChannelTypeFeatures { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + 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(); + + let channel_value_sat = 100_000; + + nodes[0].node.create_channel(node_b_id, channel_value_sat, 0, 42, None, None).unwrap(); + + let mut open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + nodes[1].node.handle_open_channel(node_a_id, &open_channel); + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::OpenChannelRequest { temporary_channel_id: chan_id, .. } => { + nodes[1] + .node + .accept_inbound_channel_from_trusted_peer( + &chan_id, + &node_a_id, + 0, + TrustedChannelFeatures::ZeroReserve, + None, + ) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + + let mut accept_channel_msg = + get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id); + nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_msg); + + let (chan_id, tx, _) = create_funding_transaction(&nodes[0], &node_b_id, channel_value_sat, 42); + + nodes[0].node.funding_transaction_generated(chan_id, node_b_id, tx.clone()).unwrap(); + nodes[1].node.handle_funding_created( + node_a_id, + &get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id), + ); + check_added_monitors(&nodes[1], 1); + expect_channel_pending_event(&nodes[1], &node_a_id); + + nodes[0].node.handle_funding_signed( + node_b_id, + &get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_a_id), + ); + check_added_monitors(&nodes[0], 1); + expect_channel_pending_event(&nodes[0], &node_b_id); + + let (channel_ready, _channel_id) = + create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &tx); + let (announcement, as_update, bs_update) = + create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &channel_ready); + update_nodes_with_chan_announce(&nodes, 0, 1, &announcement, &as_update, &bs_update); + + let details = &nodes[0].node.list_channels()[0]; + assert_eq!(details.unspendable_punishment_reserve.unwrap(), 0); + let channel_type = details.channel_type.clone().unwrap(); + let feerate_per_kw = details.feerate_sat_per_1000_weight.unwrap(); + let anchors_sat = + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + 2 * 330 + } else { + 0 + }; + let spike_multiple = if channel_type == ChannelTypeFeatures::only_static_remote_key() { + FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32 + } else { + 1 + }; + let spiked_feerate = spike_multiple * feerate_per_kw; + let reserved_commit_tx_fee_sat = chan_utils::commit_tx_fee_sat( + spiked_feerate, + 2, // We reserve space for two HTLCs, the next outbound non-dust HTLC, and the fee spike buffer HTLC + &channel_type, + ); + + let max_outbound_htlc_sat = channel_value_sat - reserved_commit_tx_fee_sat - anchors_sat; + assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000); + send_payment(&nodes[0], &[&nodes[1]], max_outbound_htlc_sat * 1000); + + let details = &nodes[1].node.list_channels()[0]; + let reserve_sat = details.unspendable_punishment_reserve.unwrap(); + assert_ne!(reserve_sat, 0); + let max_outbound_htlc_sat = max_outbound_htlc_sat - reserve_sat; + assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000); + send_payment(&nodes[1], &[&nodes[0]], max_outbound_htlc_sat * 1000); + + channel_type +} + +enum LegacyChannelsNoOutputs { + PaymentSucceeds, + FailsReceiverUpdateAddHTLC, + FailsReceiverCanAcceptHTLCA, + FailsReceiverCanAcceptHTLCB, +} + +#[xtest(feature = "_externalize_tests")] +fn test_0reserve_no_outputs() { + do_test_0reserve_no_outputs_legacy(LegacyChannelsNoOutputs::PaymentSucceeds); + do_test_0reserve_no_outputs_legacy(LegacyChannelsNoOutputs::FailsReceiverCanAcceptHTLCA); + do_test_0reserve_no_outputs_legacy(LegacyChannelsNoOutputs::FailsReceiverCanAcceptHTLCB); + do_test_0reserve_no_outputs_legacy(LegacyChannelsNoOutputs::FailsReceiverUpdateAddHTLC); + + do_test_0reserve_no_outputs_keyed_anchors(true); + do_test_0reserve_no_outputs_keyed_anchors(false); + + do_test_0reserve_no_outputs_p2a_anchor(); +} + +fn setup_0reserve_no_outputs_channels<'a, 'b, 'c, 'd>( + nodes: &'a Vec>, channel_value_sat: u64, dust_limit_satoshis: u64, +) -> (ChannelId, Transaction) { + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + // Create a channel with an identical, high dust limit and zero-reserve on both sides to make our lives easier + + nodes[0] + .node + .create_channel_to_trusted_peer_0reserve(node_b_id, channel_value_sat, 0, 42, None, None) + .unwrap(); + + let mut open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + open_channel.common_fields.dust_limit_satoshis = dust_limit_satoshis; + nodes[1].node.handle_open_channel(node_a_id, &open_channel); + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::OpenChannelRequest { temporary_channel_id: chan_id, .. } => { + nodes[1] + .node + .accept_inbound_channel_from_trusted_peer( + &chan_id, + &node_a_id, + 0, + TrustedChannelFeatures::ZeroReserve, + None, + ) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + + let mut accept_channel_msg = + get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id); + accept_channel_msg.common_fields.dust_limit_satoshis = dust_limit_satoshis; + nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_msg); + + let (chan_id, tx, _) = create_funding_transaction(&nodes[0], &node_b_id, channel_value_sat, 42); + + nodes[0].node.funding_transaction_generated(chan_id, node_b_id, tx.clone()).unwrap(); + nodes[1].node.handle_funding_created( + node_a_id, + &get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id), + ); + check_added_monitors(&nodes[1], 1); + expect_channel_pending_event(&nodes[1], &node_a_id); + + nodes[0].node.handle_funding_signed( + node_b_id, + &get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_a_id), + ); + check_added_monitors(&nodes[0], 1); + expect_channel_pending_event(&nodes[0], &node_b_id); + + assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); + assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx); + nodes[0].tx_broadcaster.clear(); + + let (channel_ready, channel_id) = + create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &tx); + let (announcement, as_update, bs_update) = + create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &channel_ready); + update_nodes_with_chan_announce(nodes, 0, 1, &announcement, &as_update, &bs_update); + + { + let mut per_peer_lock; + let mut peer_state_lock; + let channel = + get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, channel_id); + if let Some(mut chan) = channel.as_funded_mut() { + chan.context.holder_dust_limit_satoshis = dust_limit_satoshis; + } else { + panic!("Unexpected Channel phase"); + } + } + + { + let mut per_peer_lock; + let mut peer_state_lock; + let channel = + get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, channel_id); + if let Some(mut chan) = channel.as_funded_mut() { + chan.context.holder_dust_limit_satoshis = dust_limit_satoshis; + } else { + panic!("Unexpected Channel phase"); + } + } + + (channel_id, tx) +} + +fn do_test_0reserve_no_outputs_legacy(no_outputs_case: LegacyChannelsNoOutputs) { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + + let channel_type = ChannelTypeFeatures::only_static_remote_key(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + 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(); + + let feerate_per_kw = 253; + let spike_multiple = FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; + let dust_limit_satoshis: u64 = 546; + // This is the fundee 1000sat reserve + 2 min HTLCs + let channel_value_sat = 1002; + + let (channel_id, _funding_tx) = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + // Sending the biggest dust HTLC possible trims our balance output! + let (timeout_tx_fee_sat, success_tx_fee_sat) = + second_stage_tx_fees_sat(&channel_type, spike_multiple * feerate_per_kw); + let max_dust_htlc_sat = dust_limit_satoshis + success_tx_fee_sat - 1; + assert!( + channel_value_sat + .saturating_sub(commit_tx_fee_sat(feerate_per_kw, 0, &channel_type)) + .saturating_sub(max_dust_htlc_sat) + < dust_limit_satoshis + ); + + // We can't afford the fee for an additional non-dust HTLC + the fee spike HTLC, so we can only send + // dust HTLCs... + let min_local_nondust_htlc_sat = dust_limit_satoshis + timeout_tx_fee_sat; + assert!( + channel_value_sat - commit_tx_fee_sat(spike_multiple * feerate_per_kw, 2, &channel_type) + < min_local_nondust_htlc_sat + ); + + // We cannot trim our own balance output, otherwise we'd have no outputs on the commitment. We must + // also reserve enough fees to pay for an incoming non-dust HTLC, aka the fee spike buffer HTLC. + let min_value_sat = core::cmp::max( + commit_tx_fee_sat(spike_multiple * feerate_per_kw, 0, &channel_type) + dust_limit_satoshis, + commit_tx_fee_sat(spike_multiple * feerate_per_kw, 1, &channel_type), + ); + // At this point the tighter requirement is "must have an output" + assert!( + commit_tx_fee_sat(spike_multiple * feerate_per_kw, 0, &channel_type) + dust_limit_satoshis + > commit_tx_fee_sat(spike_multiple * feerate_per_kw, 1, &channel_type) + ); + // But say at 9sat/vb with default dust limit, + // the tighter requirement is actually "must have funds for an inbound HTLC" ! + assert!( + commit_tx_fee_sat(9 * 250, 0, &channel_type) + 354 + < commit_tx_fee_sat(9 * 250, 1, &channel_type) + ); + let sender_amount_msat = (channel_value_sat - min_value_sat) * 1000; + let details_0 = &nodes[0].node.list_channels()[0]; + assert_eq!(details_0.next_outbound_htlc_minimum_msat, 1000); + assert_eq!(details_0.next_outbound_htlc_limit_msat, sender_amount_msat); + assert!(details_0.next_outbound_htlc_limit_msat > details_0.next_outbound_htlc_minimum_msat); + + let (sender_amount_msat, receiver_amount_msat) = match no_outputs_case { + LegacyChannelsNoOutputs::PaymentSucceeds => (sender_amount_msat, sender_amount_msat), + LegacyChannelsNoOutputs::FailsReceiverCanAcceptHTLCA => { + // A dust HTLC with 1msat added to it will break counterparty `can_accept_incoming_htlc` + // validation, as this dust HTLC would push the holder's balance output below the + // dust limit at the spike multiple feerate. + (sender_amount_msat, sender_amount_msat + 1) + }, + LegacyChannelsNoOutputs::FailsReceiverCanAcceptHTLCB => { + // In `validate_update_add_htlc`, we check that there is still some output present on + // the commitment given the *current* set of HTLCs, and the *current* feerate. So this + // HTLC will pass at `validate_update_add_htlc`, but will fail in + // `can_accept_incoming_htlc` due to failed fee spike buffer checks. + let receiver_amount_msat = (channel_value_sat + - commit_tx_fee_sat(feerate_per_kw, 0, &channel_type) + - dust_limit_satoshis) + * 1000; + (sender_amount_msat, receiver_amount_msat) + }, + LegacyChannelsNoOutputs::FailsReceiverUpdateAddHTLC => { + // Same value as above, just add 1msat, and this fails at `validate_update_add_htlc` + let receiver_amount_msat = (channel_value_sat + - commit_tx_fee_sat(feerate_per_kw, 0, &channel_type) + - dust_limit_satoshis) + * 1000; + (sender_amount_msat, receiver_amount_msat + 1) + }, + }; + + if let LegacyChannelsNoOutputs::PaymentSucceeds = no_outputs_case { + send_payment(&nodes[0], &[&nodes[1]], sender_amount_msat); + // Node 1 the fundee has 0-reserve too, so whatever they receive, they can send right back! + // Node 0 should *always* have the funds to cover the fee of a single non-dust HTLC from node 1. + assert_eq!( + nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat, + sender_amount_msat + ); + send_payment(&nodes[1], &[&nodes[0]], sender_amount_msat); + } else { + let (route, payment_hash, _, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], sender_amount_msat); + let secp_ctx = Secp256k1::new(); + let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); + let cur_height = nodes[0].node.best_block.read().unwrap().height + 1; + let onion_keys = + onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, sender_amount_msat); + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( + &route.paths[0], + &recipient_onion_fields, + cur_height, + &None, + None, + None, + ) + .unwrap(); + assert_eq!(htlc_msat, sender_amount_msat); + let onion_packet = + onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash) + .unwrap(); + let msg = msgs::UpdateAddHTLC { + channel_id, + htlc_id: 0, + amount_msat: receiver_amount_msat, + payment_hash, + cltv_expiry: htlc_cltv, + onion_routing_packet: onion_packet, + skimmed_fee_msat: None, + blinding_point: None, + hold_htlc: None, + accountable: None, + }; + + nodes[1].node.handle_update_add_htlc(node_a_id, &msg); + + if let LegacyChannelsNoOutputs::FailsReceiverUpdateAddHTLC = no_outputs_case { + nodes[1].logger.assert_log_contains( + "lightning::ln::channelmanager", + "Remote HTLC add would overdraw remaining funds", + 3, + ); + assert_eq!(nodes[1].node.list_channels().len(), 0); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); + assert_eq!(err_msg.data, "Remote HTLC add would overdraw remaining funds"); + let reason = ClosureReason::ProcessingError { + err: "Remote HTLC add would overdraw remaining funds".to_string(), + }; + check_added_monitors(&nodes[1], 1); + check_closed_event(&nodes[1], 1, reason, &[node_a_id], channel_value_sat); + + return; + } + + manually_trigger_update_fail_htlc( + &nodes, + channel_id, + channel_value_sat, + dust_limit_satoshis, + receiver_amount_msat, + htlc_cltv, + payment_hash, + ); + } +} + +fn manually_trigger_update_fail_htlc<'a, 'b, 'c, 'd>( + nodes: &'a Vec>, channel_id: ChannelId, channel_value_sat: u64, + dust_limit_satoshis: u64, receiver_amount_msat: u64, htlc_cltv: u32, payment_hash: PaymentHash, +) { + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + let secp_ctx = Secp256k1::new(); + + // Now manually create the commitment_signed message corresponding to the update_add + // nodes[0] just sent. In the code for construction of this message, "local" refers + // to the sender of the message, and "remote" refers to the receiver. + + let feerate_per_kw = get_feerate!(nodes[0], nodes[1], channel_id); + + const INITIAL_COMMITMENT_NUMBER: u64 = (1 << 48) - 1; + + let (local_secret, next_local_point) = { + let per_peer_state = nodes[0].node.per_peer_state.read().unwrap(); + let chan_lock = per_peer_state.get(&node_b_id).unwrap().lock().unwrap(); + let local_chan = + chan_lock.channel_by_id.get(&channel_id).and_then(Channel::as_funded).unwrap(); + let chan_signer = local_chan.get_signer(); + // Make the signer believe we validated another commitment, so we can release the secret + chan_signer.as_ecdsa().unwrap().get_enforcement_state().last_holder_commitment -= 1; + + ( + chan_signer.as_ref().release_commitment_secret(INITIAL_COMMITMENT_NUMBER).unwrap(), + chan_signer + .as_ref() + .get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 2, &secp_ctx) + .unwrap(), + ) + }; + let remote_point = { + let per_peer_lock; + let mut peer_state_lock; + + let channel = + get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, channel_id); + let chan_signer = channel.as_funded().unwrap().get_signer(); + chan_signer + .as_ref() + .get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 1, &secp_ctx) + .unwrap() + }; + + // Build the remote commitment transaction so we can sign it, and then later use the + // signature for the commitment_signed message. + let accepted_htlc_info = chan_utils::HTLCOutputInCommitment { + offered: false, + amount_msat: receiver_amount_msat, + cltv_expiry: htlc_cltv, + payment_hash, + transaction_output_index: Some(1), + }; + + let local_chan_balance_msat = channel_value_sat * 1000; + let commitment_number = INITIAL_COMMITMENT_NUMBER - 1; + + let res = { + let per_peer_lock; + let mut peer_state_lock; + + let channel = + get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, channel_id); + let chan_signer = channel.as_funded().unwrap().get_signer(); + + let (commitment_tx, _stats) = SpecTxBuilder {}.build_commitment_transaction( + false, + commitment_number, + &remote_point, + &channel.funding().channel_transaction_parameters, + &secp_ctx, + local_chan_balance_msat, + vec![accepted_htlc_info], + feerate_per_kw, + dust_limit_satoshis, + &nodes[0].logger, + ); + let params = &channel.funding().channel_transaction_parameters; + chan_signer + .as_ecdsa() + .unwrap() + .sign_counterparty_commitment(params, &commitment_tx, Vec::new(), Vec::new(), &secp_ctx) + .unwrap() + }; + + let commit_signed_msg = msgs::CommitmentSigned { + channel_id, + signature: res.0, + htlc_signatures: res.1, + funding_txid: None, + #[cfg(taproot)] + partial_signature_with_nonce: None, + }; + + // Send the commitment_signed message to the nodes[1]. + nodes[1].node.handle_commitment_signed(node_a_id, &commit_signed_msg); + let _ = nodes[1].node.get_and_clear_pending_msg_events(); + + // Send the RAA to nodes[1]. + let raa_msg = msgs::RevokeAndACK { + channel_id, + per_commitment_secret: local_secret, + next_per_commitment_point: next_local_point, + #[cfg(taproot)] + next_local_nonce: None, + release_htlc_message_paths: Vec::new(), + }; + nodes[1].node.handle_revoke_and_ack(node_a_id, &raa_msg); + expect_and_process_pending_htlcs(&nodes[1], false); + + expect_htlc_handling_failed_destinations!( + nodes[1].node.get_and_clear_pending_events(), + &[HTLCHandlingFailureType::Receive { payment_hash }] + ); + + let events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + // Make sure the HTLC failed in the way we expect. + match events[0] { + MessageSendEvent::UpdateHTLCs { + updates: msgs::CommitmentUpdate { ref update_fail_htlcs, .. }, + .. + } => { + assert_eq!(update_fail_htlcs.len(), 1); + update_fail_htlcs[0].clone() + }, + _ => panic!("Unexpected event"), + }; + nodes[1].logger.assert_log( + "lightning::ln::channel", + "Attempting to fail HTLC due to balance exhausted on remote commitment".to_string(), + 1, + ); + + check_added_monitors(&nodes[1], 3); +} + +fn do_test_0reserve_no_outputs_keyed_anchors(payment_success: bool) { + let mut config = test_default_channel_config(); + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + + let channel_type = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + 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(); + + let feerate_per_kw = 253; + let anchors_sat = 2 * ANCHOR_OUTPUT_VALUE_SATOSHI; + let dust_limit_satoshis: u64 = 546; + let channel_value_sat = { + // min opener balance is the fee for 4 HTLCs, the anchors, and the dust limit + let min_channel_size = + commit_tx_fee_sat(feerate_per_kw, MIN_AFFORDABLE_HTLC_COUNT, &channel_type) + + anchors_sat + dust_limit_satoshis; + assert!(min_channel_size > 1002); + min_channel_size + }; + + let (channel_id, _funding_tx) = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + // Sending the biggest dust HTLC possible trims our balance output! + let max_dust_htlc_sat = dust_limit_satoshis - 1; + assert!( + channel_value_sat + .saturating_sub(anchors_sat) + .saturating_sub(commit_tx_fee_sat(feerate_per_kw, 0, &channel_type)) + .saturating_sub(max_dust_htlc_sat) + < dust_limit_satoshis + ); + + // We can afford the fee for an additional non-dust HTLC plus the fee spike HTLC, so we can send + // non-dust HTLCs + let capacity_minus_max_commitment_fee_sat = + channel_value_sat - anchors_sat - commit_tx_fee_sat(feerate_per_kw, 2, &channel_type); + assert!(capacity_minus_max_commitment_fee_sat > dust_limit_satoshis); + // And since the biggest dust HTLC results in no outputs on the commitment, + // we can *only* send non-dust HTLCs + let details_0 = &nodes[0].node.list_channels()[0]; + assert_eq!(details_0.next_outbound_htlc_minimum_msat, dust_limit_satoshis * 1000); + assert_eq!( + details_0.next_outbound_htlc_limit_msat, + capacity_minus_max_commitment_fee_sat * 1000 + ); + + // Send the smallest non-dust HTLC possible, this will pass both holder and counterparty validation + // + // One msat below the non-dust HTLC value will break counterparty validation at + // `validate_update_add_htlc`. This is why we don't bother taking a look at the range between the + // failure of `can_accept_incoming_htlc` and the failure of `validate_update_add_htlc`. + let sender_amount_msat = dust_limit_satoshis * 1000; + + let (sender_amount_msat, receiver_amount_msat) = if payment_success { + (sender_amount_msat, sender_amount_msat) + } else { + (sender_amount_msat, sender_amount_msat - 1) + }; + + if payment_success { + send_payment(&nodes[0], &[&nodes[1]], sender_amount_msat); + // Node 1 the fundee has 0-reserve too, so whatever they receive, they can send right back! + // Node 0 should *always* have the funds to cover the fee of a single non-dust HTLC from node 1. + assert_eq!( + nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat, + sender_amount_msat + ); + send_payment(&nodes[1], &[&nodes[0]], sender_amount_msat); + } else { + let (route, payment_hash, _, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], sender_amount_msat); + let secp_ctx = Secp256k1::new(); + let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); + let cur_height = nodes[0].node.best_block.read().unwrap().height + 1; + let onion_keys = + onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, sender_amount_msat); + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( + &route.paths[0], + &recipient_onion_fields, + cur_height, + &None, + None, + None, + ) + .unwrap(); + assert_eq!(htlc_msat, sender_amount_msat); + let onion_packet = + onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash) + .unwrap(); + let msg = msgs::UpdateAddHTLC { + channel_id, + htlc_id: 0, + amount_msat: receiver_amount_msat, + payment_hash, + cltv_expiry: htlc_cltv, + onion_routing_packet: onion_packet, + skimmed_fee_msat: None, + blinding_point: None, + hold_htlc: None, + accountable: None, + }; + + nodes[1].node.handle_update_add_htlc(node_a_id, &msg); + + nodes[1].logger.assert_log_contains( + "lightning::ln::channelmanager", + "Remote HTLC add would overdraw remaining funds", + 3, + ); + assert_eq!(nodes[1].node.list_channels().len(), 0); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); + assert_eq!(err_msg.data, "Remote HTLC add would overdraw remaining funds"); + let reason = ClosureReason::ProcessingError { + err: "Remote HTLC add would overdraw remaining funds".to_string(), + }; + check_added_monitors(&nodes[1], 1); + check_closed_event(&nodes[1], 1, reason, &[node_a_id], channel_value_sat); + } +} + +fn do_test_0reserve_no_outputs_p2a_anchor() { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + + let channel_type = ChannelTypeFeatures::anchors_zero_fee_commitments(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + 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(); + + let dust_limit_satoshis: u64 = 546; + // This is the fundee 1000sat reserve + 2 min HTLCs + let channel_value_sat = 1002; + + let _channel_id = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + // Sending the biggest dust HTLC possible trims our balance output! + let max_dust_htlc_sat = dust_limit_satoshis - 1; + assert!(channel_value_sat.saturating_sub(max_dust_htlc_sat) < dust_limit_satoshis); + + // We'll always have the P2A output on the commitment, so we are free to send any size HTLC, + // including those that result in only a single output on the commitment, the P2A output. + let details_0 = &nodes[0].node.list_channels()[0]; + assert_eq!(details_0.next_outbound_htlc_minimum_msat, 1000); + // 0FC + 0-reserve baby! + assert_eq!(details_0.next_outbound_htlc_limit_msat, channel_value_sat * 1000); + + // Send the max size dust HTLC; this results in a commitment with only the P2A output present + let sender_amount_msat = max_dust_htlc_sat * 1000; + + send_payment(&nodes[0], &[&nodes[1]], sender_amount_msat); + // Node 1 the fundee has 0-reserve too, so whatever they receive, they can send right back! + assert_eq!(nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat, sender_amount_msat); + send_payment(&nodes[1], &[&nodes[0]], sender_amount_msat); +} + +#[xtest(feature = "_externalize_tests")] +pub fn test_0reserve_force_close_with_single_p2a_output() { + do_test_0reserve_force_close_with_single_p2a_output(false); + do_test_0reserve_force_close_with_single_p2a_output(true); +} + +fn do_test_0reserve_force_close_with_single_p2a_output(high_feerate: bool) { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + + let chanmon_cfgs = create_chanmon_cfgs(2); + if high_feerate { + let mut feerate_lock = chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap(); + *feerate_lock = 2500; + let mut feerate_lock = chanmon_cfgs[1].fee_estimator.sat_per_kw.lock().unwrap(); + *feerate_lock = 2500; + } + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + + let channel_type = ChannelTypeFeatures::anchors_zero_fee_commitments(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let coinbase_tx = provide_anchor_reserves(&nodes); + + let _node_a_id = nodes[0].node.get_our_node_id(); + let _node_b_id = nodes[1].node.get_our_node_id(); + + let dust_limit_satoshis: u64 = 546; + // This is the fundee 1000sat reserve + 2 min HTLCs + let channel_value_sat = 1002; + + let (channel_id, funding_tx) = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + // Send the smallest HTLC possible that trims our own balance output, this will be a dust HTLC + let htlc_sat = channel_value_sat - dust_limit_satoshis + 1; + assert!(htlc_sat < dust_limit_satoshis); + route_payment(&nodes[0], &[&nodes[1]], htlc_sat * 1000); + + let commitment_tx = get_local_commitment_txn!(nodes[0], channel_id).pop().unwrap(); + let commitment_txid = commitment_tx.compute_txid(); + + let message = "Channel force-closed".to_owned(); + nodes[0] + .node + .force_close_broadcasting_latest_txn( + &channel_id, + &nodes[1].node.get_our_node_id(), + message.clone(), + ) + .unwrap(); + check_closed_broadcast(&nodes[0], 1, true); + check_added_monitors(&nodes[0], 1); + let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; + check_closed_event(&nodes[0], 1, reason, &[nodes[1].node.get_our_node_id()], channel_value_sat); + + let mut events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events.pop().unwrap() { + Event::BumpTransaction(bump_event) => { + nodes[0].bump_tx_handler.handle_event(&bump_event); + }, + _ => panic!("Unexpected event"), + } + let txns = nodes[0].tx_broadcaster.txn_broadcast(); + + if high_feerate { + assert_eq!(txns.len(), 2); + check_spends!(txns[1], txns[0], coinbase_tx); + assert!(txns[1].weight().to_wu() < TRUC_CHILD_MAX_WEIGHT); + assert_eq!(txns[1].input.len(), 2); + assert_eq!(txns[1].output.len(), 1); + + assert_eq!(txns[0].compute_txid(), commitment_txid); + assert_eq!(txns[0].input.len(), 1); + assert_eq!(txns[0].output.len(), 1); + assert_eq!(txns[0].output[0].value, Amount::from_sat(240)); + assert_eq!(txns[0].output[0].script_pubkey, shared_anchor_script_pubkey()); + check_spends!(txns[0], funding_tx); + + nodes[0].logger.assert_log( + "lightning::events::bump_transaction", + format!( + "Broadcasting anchor transaction {} to bump channel close with txid {}", + txns[1].compute_txid(), + txns[0].compute_txid() + ), + 1, + ); + } else { + assert_eq!(txns.len(), 1); + assert_eq!(txns[0].compute_txid(), commitment_txid); + assert_eq!(txns[0].input.len(), 1); + assert_eq!(txns[0].output.len(), 1); + assert_eq!(txns[0].output[0].value, Amount::from_sat(240)); + assert_eq!(txns[0].output[0].script_pubkey, shared_anchor_script_pubkey()); + check_spends!(txns[0], funding_tx); + + let weight = txns[0].weight(); + let feerate = (channel_value_sat - 240) * 1000 / weight.to_wu(); + + nodes[0].logger.assert_log( + "lightning::events::bump_transaction", + format!( + "Pre-signed commitment {} already has feerate {} sat/kW above required 253 sat/kW, broadcasting.", + txns[0].compute_txid(), + feerate, + ), + 1, + ); + } +} + +#[xtest(feature = "_externalize_tests")] +fn test_0reserve_zero_conf_combined() { + // Test that zero-reserve and zero-conf features work together: a channel that + // is immediately usable (no confirmations needed) and has zero reserve for the opener. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + 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(); + + let channel_value_sat = 100_000; + + // Node 0 creates a channel to node 1. + nodes[0].node.create_channel(node_b_id, channel_value_sat, 0, 42, None, None).unwrap(); + let open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + + // Node 1 accepts with both zero-conf AND zero-reserve. + nodes[1].node.handle_open_channel(node_a_id, &open_channel); + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::OpenChannelRequest { temporary_channel_id: chan_id, .. } => { + nodes[1] + .node + .accept_inbound_channel_from_trusted_peer( + &chan_id, + &node_a_id, + 0, + TrustedChannelFeatures::ZeroConfZeroReserve, + None, + ) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + + // Verify zero-conf: minimum_depth should be 0. + let accept_channel = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id); + assert_eq!(accept_channel.common_fields.minimum_depth, 0); + nodes[0].node.handle_accept_channel(node_b_id, &accept_channel); + + // Create the funding transaction (no block confirmations needed for zero-conf). + let (temporary_channel_id, tx, _) = + create_funding_transaction(&nodes[0], &node_b_id, channel_value_sat, 42); + nodes[0] + .node + .funding_transaction_generated(temporary_channel_id, node_b_id, tx.clone()) + .unwrap(); + let funding_created = get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id); + + // Node 1 handles funding_created and immediately sends both FundingSigned and ChannelReady. + nodes[1].node.handle_funding_created(node_a_id, &funding_created); + check_added_monitors(&nodes[1], 1); + let bs_signed_locked = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(bs_signed_locked.len(), 2); + + let as_channel_ready; + match &bs_signed_locked[0] { + MessageSendEvent::SendFundingSigned { node_id, msg } => { + assert_eq!(*node_id, node_a_id); + nodes[0].node.handle_funding_signed(node_b_id, &msg); + expect_channel_pending_event(&nodes[0], &node_b_id); + expect_channel_pending_event(&nodes[1], &node_a_id); + check_added_monitors(&nodes[0], 1); + + assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); + assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx); + nodes[0].tx_broadcaster.clear(); + + as_channel_ready = + get_event_msg!(nodes[0], MessageSendEvent::SendChannelReady, node_b_id); + }, + _ => panic!("Unexpected event"), + } + match &bs_signed_locked[1] { + MessageSendEvent::SendChannelReady { node_id, msg } => { + assert_eq!(*node_id, node_a_id); + nodes[0].node.handle_channel_ready(node_b_id, &msg); + expect_channel_ready_event(&nodes[0], &node_b_id); + }, + _ => panic!("Unexpected event"), + } + + nodes[1].node.handle_channel_ready(node_a_id, &as_channel_ready); + expect_channel_ready_event(&nodes[1], &node_a_id); + + let as_channel_update = + get_event_msg!(nodes[0], MessageSendEvent::SendChannelUpdate, node_b_id); + let bs_channel_update = + get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, node_a_id); + nodes[0].node.handle_channel_update(node_b_id, &bs_channel_update); + nodes[1].node.handle_channel_update(node_a_id, &as_channel_update); + + // Channel should be immediately usable without any block confirmations. + assert_eq!(nodes[0].node.list_usable_channels().len(), 1); + assert_eq!(nodes[1].node.list_usable_channels().len(), 1); + + // Verify zero-reserve: opener (node 0) should have 0 reserve. + let details_a = &nodes[0].node.list_channels()[0]; + let node_0_reserve = details_a.unspendable_punishment_reserve.unwrap(); + let node_0_max_htlc = details_a.next_outbound_htlc_limit_msat; + let channel_type = details_a.channel_type.clone().unwrap(); + assert_eq!(node_0_reserve, 0); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + assert!(details_a.is_usable); + assert_eq!(details_a.confirmations.unwrap(), 0); + assert_eq!( + node_0_max_htlc, + (channel_value_sat - commit_tx_fee_sat(253, 2, &channel_type) - 2 * 330) * 1000 + ); + + // Verify acceptor (node 1) has a non-zero reserve. + let details_b = &nodes[1].node.list_channels()[0]; + assert_ne!(details_b.unspendable_punishment_reserve.unwrap(), 0); + assert!(details_b.is_usable); + + // Send payments in both directions to verify the combined feature works end-to-end. + send_payment(&nodes[0], &[&nodes[1]], node_0_max_htlc); + + let details_b = &nodes[1].node.list_channels()[0]; + let node_1_reserve = details_b.unspendable_punishment_reserve.unwrap(); + let node_1_max_htlc = details_b.next_outbound_htlc_limit_msat; + assert_eq!(node_1_reserve, 1000); + assert_eq!(node_1_max_htlc, node_0_max_htlc - node_1_reserve * 1000); + send_payment(&nodes[1], &[&nodes[0]], node_1_max_htlc); +} From 5d06723eb76fa3a9321a638b730a69e6c03ef9f1 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 5 Mar 2026 09:00:32 +0000 Subject: [PATCH 5/7] Update `chanmon_consistency` to include 0FC and 0-reserve channels Co-Authored-By: HAL 9000 --- fuzz/src/chanmon_consistency.rs | 130 +++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 28 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 22006897a0f..bf4ec8722b3 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -53,6 +53,7 @@ use lightning::ln::channel::{ use lightning::ln::channel_state::ChannelDetails; use lightning::ln::channelmanager::{ ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, RecentPaymentDetails, + TrustedChannelFeatures, }; use lightning::ln::functional_test_utils::*; use lightning::ln::funding::{FundingContribution, FundingTemplate}; @@ -863,9 +864,15 @@ fn assert_action_timeout_awaiting_response(action: &msgs::ErrorAction) { )); } +pub enum ChanType { + Legacy, + KeyedAnchors, + ZeroFeeCommitments, +} + #[inline] pub fn do_test( - data: &[u8], underlying_out: Out, anchors: bool, + data: &[u8], underlying_out: Out, chan_type: ChanType, ) { let out = SearchingOutput::new(underlying_out); let broadcast_a = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) }); @@ -926,8 +933,19 @@ pub fn do_test( config.channel_config.forwarding_fee_proportional_millionths = 0; config.channel_handshake_config.announce_for_forwarding = true; config.reject_inbound_splices = false; - if !anchors { - config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + match chan_type { + ChanType::Legacy => { + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + }, + ChanType::KeyedAnchors => { + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + }, + ChanType::ZeroFeeCommitments => { + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + }, } let network = Network::Bitcoin; let best_block_timestamp = genesis_block(network).header.time; @@ -978,8 +996,19 @@ pub fn do_test( config.channel_config.forwarding_fee_proportional_millionths = 0; config.channel_handshake_config.announce_for_forwarding = true; config.reject_inbound_splices = false; - if !anchors { - config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + match chan_type { + ChanType::Legacy => { + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + }, + ChanType::KeyedAnchors => { + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + }, + ChanType::ZeroFeeCommitments => { + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + }, } let mut monitors = new_hash_map(); @@ -1078,8 +1107,23 @@ pub fn do_test( }}; } macro_rules! make_channel { - ($source: expr, $dest: expr, $source_monitor: expr, $dest_monitor: expr, $dest_keys_manager: expr, $chan_id: expr) => {{ - $source.create_channel($dest.get_our_node_id(), 100_000, 42, 0, None, None).unwrap(); + ($source: expr, $dest: expr, $source_monitor: expr, $dest_monitor: expr, $dest_keys_manager: expr, $chan_id: expr, $trusted_open: expr, $trusted_accept: expr) => {{ + if $trusted_open { + $source + .create_channel_to_trusted_peer_0reserve( + $dest.get_our_node_id(), + 100_000, + 42, + 0, + None, + None, + ) + .unwrap(); + } else { + $source + .create_channel($dest.get_our_node_id(), 100_000, 42, 0, None, None) + .unwrap(); + } let open_channel = { let events = $source.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -1104,14 +1148,26 @@ pub fn do_test( random_bytes .copy_from_slice(&$dest_keys_manager.get_secure_random_bytes()[..16]); let user_channel_id = u128::from_be_bytes(random_bytes); - $dest - .accept_inbound_channel( - temporary_channel_id, - counterparty_node_id, - user_channel_id, - None, - ) - .unwrap(); + if $trusted_accept { + $dest + .accept_inbound_channel_from_trusted_peer( + temporary_channel_id, + counterparty_node_id, + user_channel_id, + TrustedChannelFeatures::ZeroReserve, + None, + ) + .unwrap(); + } else { + $dest + .accept_inbound_channel( + temporary_channel_id, + counterparty_node_id, + user_channel_id, + None, + ) + .unwrap(); + } } else { panic!("Wrong event type"); } @@ -1287,12 +1343,16 @@ pub fn do_test( // Fuzz mode uses XOR-based hashing (all bytes XOR to one byte), and // versions 0-5 cause collisions between A-B and B-C channel pairs // (e.g., A-B with Version(1) collides with B-C with Version(3)). - make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 1); - make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 2); - make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 3); - make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 4); - make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 5); - make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 6); + // A-B: channel 2 A and B have 0-reserve (trusted open + trusted accept), + // channel 3 A has 0-reserve (trusted accept) + make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 1, false, false); + make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 2, true, true); + make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 3, false, true); + // B-C: channel 4 B has 0-reserve (via trusted accept), + // channel 5 C has 0-reserve (via trusted open) + make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 4, false, true); + make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 5, true, false); + make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 6, false, false); // Wipe the transactions-broadcasted set to make sure we don't broadcast any transactions // during normal operation in `test_return`. @@ -2301,7 +2361,7 @@ pub fn do_test( 0x80 => { let mut max_feerate = last_htlc_clear_fee_a; - if !anchors { + if matches!(chan_type, ChanType::Legacy) { max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; } if fee_est_a.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate { @@ -2316,7 +2376,7 @@ pub fn do_test( 0x84 => { let mut max_feerate = last_htlc_clear_fee_b; - if !anchors { + if matches!(chan_type, ChanType::Legacy) { max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; } if fee_est_b.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate { @@ -2331,7 +2391,7 @@ pub fn do_test( 0x88 => { let mut max_feerate = last_htlc_clear_fee_c; - if !anchors { + if matches!(chan_type, ChanType::Legacy) { max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; } if fee_est_c.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate { @@ -2783,12 +2843,26 @@ impl SearchingOutput { } pub fn chanmon_consistency_test(data: &[u8], out: Out) { - do_test(data, out.clone(), false); - do_test(data, out, true); + do_test(data, out.clone(), ChanType::Legacy); + do_test(data, out.clone(), ChanType::KeyedAnchors); + do_test(data, out, ChanType::ZeroFeeCommitments); } #[no_mangle] pub extern "C" fn chanmon_consistency_run(data: *const u8, datalen: usize) { - do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}, false); - do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}, true); + do_test( + unsafe { std::slice::from_raw_parts(data, datalen) }, + test_logger::DevNull {}, + ChanType::Legacy, + ); + do_test( + unsafe { std::slice::from_raw_parts(data, datalen) }, + test_logger::DevNull {}, + ChanType::KeyedAnchors, + ); + do_test( + unsafe { std::slice::from_raw_parts(data, datalen) }, + test_logger::DevNull {}, + ChanType::ZeroFeeCommitments, + ); } From 7f15e49655f840c2781e7869738b29646abe9327 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 18 Mar 2026 23:10:10 +0000 Subject: [PATCH 6/7] Add 0-reserve to the internal API of V2 channels --- lightning/src/ln/channel.rs | 15 ++-- lightning/src/ln/channelmanager.rs | 1 + lightning/src/ln/msgs.rs | 108 +++++++++++++++++++++++------ 3 files changed, 96 insertions(+), 28 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 23ea68d8d1a..5b064e50e0d 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -13931,7 +13931,7 @@ impl PendingV2Channel { counterparty_node_id: PublicKey, their_features: &InitFeatures, funding_satoshis: u64, funding_inputs: Vec, user_id: u128, config: &UserConfig, current_chain_height: u32, outbound_scid_alias: u64, funding_confirmation_target: ConfirmationTarget, - logger: L, + logger: L, trusted_channel_features: Option, ) -> Result { let channel_keys_id = signer_provider.generate_channel_keys_id(false, user_id); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -13941,7 +13941,7 @@ impl PendingV2Channel { }); let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, false); + funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, trusted_channel_features.is_some_and(|f| f.is_0reserve())); let funding_feerate_sat_per_1000_weight = fee_estimator.bounded_sat_per_1000_weight(funding_confirmation_target); let funding_tx_locktime = LockTime::from_height(current_chain_height) @@ -14059,6 +14059,7 @@ impl PendingV2Channel { second_per_commitment_point, locktime: self.funding_negotiation_context.funding_tx_locktime.to_consensus_u32(), require_confirmed_inputs: None, + disable_channel_reserve: (self.funding.holder_selected_channel_reserve_satoshis == 0).then_some(()), } } @@ -14071,7 +14072,7 @@ impl PendingV2Channel { fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, holder_node_id: PublicKey, counterparty_node_id: PublicKey, our_supported_features: &ChannelTypeFeatures, their_features: &InitFeatures, msg: &msgs::OpenChannelV2, - user_id: u128, config: &UserConfig, current_chain_height: u32, logger: &L, + user_id: u128, config: &UserConfig, current_chain_height: u32, logger: &L, trusted_channel_features: Option, ) -> Result { // TODO(dual_funding): Take these as input once supported let (our_funding_contribution, our_funding_contribution_sats) = (SignedAmount::ZERO, 0u64); @@ -14080,9 +14081,9 @@ impl PendingV2Channel { let channel_value_satoshis = our_funding_contribution_sats.saturating_add(msg.common_fields.funding_satoshis); let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - channel_value_satoshis, msg.common_fields.dust_limit_satoshis, false); + channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, msg.disable_channel_reserve.is_some()); let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, false); + channel_value_satoshis, msg.common_fields.dust_limit_satoshis, trusted_channel_features.is_some_and(|f| f.is_0reserve())); let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; @@ -14104,7 +14105,7 @@ impl PendingV2Channel { config, current_chain_height, logger, - None, + trusted_channel_features, our_funding_contribution_sats, counterparty_pubkeys, channel_type, @@ -14223,6 +14224,8 @@ impl PendingV2Channel { as u64, second_per_commitment_point, require_confirmed_inputs: None, + disable_channel_reserve: (self.funding.holder_selected_channel_reserve_satoshis == 0) + .then_some(()), } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 4d813c4d4cb..1bfea47b79c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10927,6 +10927,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &config, best_block_height, &self.logger, + trusted_channel_features, ) .map_err(|e| { let channel_id = open_channel_msg.common_fields.temporary_channel_id; diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index ac549ddd50c..b12e71ea58e 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -311,6 +311,8 @@ pub struct OpenChannelV2 { pub second_per_commitment_point: PublicKey, /// Optionally, a requirement that only confirmed inputs can be added pub require_confirmed_inputs: Option<()>, + /// Optionally, disables the channel reserve of the receiver + pub disable_channel_reserve: Option<()>, } /// Contains fields that are both common to [`accept_channel`] and [`accept_channel2`] messages. @@ -390,6 +392,8 @@ pub struct AcceptChannelV2 { pub second_per_commitment_point: PublicKey, /// Optionally, a requirement that only confirmed inputs can be added pub require_confirmed_inputs: Option<()>, + /// Optionally, disables the channel reserve of the receiver + pub disable_channel_reserve: Option<()>, } /// A [`funding_created`] message to be sent to or received from a peer. @@ -3004,6 +3008,7 @@ impl Writeable for AcceptChannelV2 { (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. (1, self.common_fields.channel_type, option), (2, self.require_confirmed_inputs, option), + (4, self.disable_channel_reserve, option), }); Ok(()) } @@ -3030,10 +3035,12 @@ impl LengthReadable for AcceptChannelV2 { let mut shutdown_scriptpubkey: Option = None; let mut channel_type: Option = None; let mut require_confirmed_inputs: Option<()> = None; + let mut disable_channel_reserve: Option<()> = None; decode_tlv_stream!(r, { (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), (1, channel_type, option), (2, require_confirmed_inputs, option), + (4, disable_channel_reserve, option), }); Ok(AcceptChannelV2 { @@ -3057,6 +3064,7 @@ impl LengthReadable for AcceptChannelV2 { funding_satoshis, second_per_commitment_point, require_confirmed_inputs, + disable_channel_reserve, }) } } @@ -3465,6 +3473,7 @@ impl Writeable for OpenChannelV2 { (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. (1, self.common_fields.channel_type, option), (2, self.require_confirmed_inputs, option), + (4, self.disable_channel_reserve, option), }); Ok(()) } @@ -3495,10 +3504,12 @@ impl LengthReadable for OpenChannelV2 { let mut shutdown_scriptpubkey: Option = None; let mut channel_type: Option = None; let mut require_confirmed_inputs: Option<()> = None; + let mut disable_channel_reserve: Option<()> = None; decode_tlv_stream!(r, { (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), (1, channel_type, option), (2, require_confirmed_inputs, option), + (4, disable_channel_reserve, option), }); Ok(OpenChannelV2 { common_fields: CommonOpenChannelFields { @@ -3525,6 +3536,7 @@ impl LengthReadable for OpenChannelV2 { locktime, second_per_commitment_point, require_confirmed_inputs, + disable_channel_reserve, }) } } @@ -5273,6 +5285,7 @@ mod tests { fn do_encoding_open_channelv2( random_bit: bool, shutdown: bool, incl_chan_type: bool, require_confirmed_inputs: bool, + disable_channel_reserve: bool, ) { let secp_ctx = Secp256k1::new(); let (_, pubkey_1) = get_keys_from!( @@ -5341,7 +5354,8 @@ mod tests { funding_feerate_sat_per_1000_weight: 821716, locktime: 305419896, second_per_commitment_point: pubkey_7, - require_confirmed_inputs: if require_confirmed_inputs { Some(()) } else { None }, + require_confirmed_inputs: require_confirmed_inputs.then_some(()), + disable_channel_reserve: disable_channel_reserve.then_some(()), }; let encoded_value = open_channelv2.encode(); let mut target_value = Vec::new(); @@ -5426,27 +5440,46 @@ mod tests { if require_confirmed_inputs { target_value.append(&mut >::from_hex("0200").unwrap()); } + if disable_channel_reserve { + target_value.append(&mut >::from_hex("0400").unwrap()); + } assert_eq!(encoded_value, target_value); } #[test] fn encoding_open_channelv2() { - do_encoding_open_channelv2(false, false, false, false); - do_encoding_open_channelv2(false, false, false, true); - do_encoding_open_channelv2(false, false, true, false); - do_encoding_open_channelv2(false, false, true, true); - do_encoding_open_channelv2(false, true, false, false); - do_encoding_open_channelv2(false, true, false, true); - do_encoding_open_channelv2(false, true, true, false); - do_encoding_open_channelv2(false, true, true, true); - do_encoding_open_channelv2(true, false, false, false); - do_encoding_open_channelv2(true, false, false, true); - do_encoding_open_channelv2(true, false, true, false); - do_encoding_open_channelv2(true, false, true, true); - do_encoding_open_channelv2(true, true, false, false); - do_encoding_open_channelv2(true, true, false, true); - do_encoding_open_channelv2(true, true, true, false); - do_encoding_open_channelv2(true, true, true, true); + do_encoding_open_channelv2(false, false, false, false, false); + do_encoding_open_channelv2(false, false, false, false, true); + do_encoding_open_channelv2(false, false, false, true, false); + do_encoding_open_channelv2(false, false, false, true, true); + do_encoding_open_channelv2(false, false, true, false, false); + do_encoding_open_channelv2(false, false, true, false, true); + do_encoding_open_channelv2(false, false, true, true, false); + do_encoding_open_channelv2(false, false, true, true, true); + do_encoding_open_channelv2(false, true, false, false, false); + do_encoding_open_channelv2(false, true, false, false, true); + do_encoding_open_channelv2(false, true, false, true, false); + do_encoding_open_channelv2(false, true, false, true, true); + do_encoding_open_channelv2(false, true, true, false, false); + do_encoding_open_channelv2(false, true, true, false, true); + do_encoding_open_channelv2(false, true, true, true, false); + do_encoding_open_channelv2(false, true, true, true, true); + do_encoding_open_channelv2(true, false, false, false, false); + do_encoding_open_channelv2(true, false, false, false, true); + do_encoding_open_channelv2(true, false, false, true, false); + do_encoding_open_channelv2(true, false, false, true, true); + do_encoding_open_channelv2(true, false, true, false, false); + do_encoding_open_channelv2(true, false, true, false, true); + do_encoding_open_channelv2(true, false, true, true, false); + do_encoding_open_channelv2(true, false, true, true, true); + do_encoding_open_channelv2(true, true, false, false, false); + do_encoding_open_channelv2(true, true, false, false, true); + do_encoding_open_channelv2(true, true, false, true, false); + do_encoding_open_channelv2(true, true, false, true, true); + do_encoding_open_channelv2(true, true, true, false, false); + do_encoding_open_channelv2(true, true, true, false, true); + do_encoding_open_channelv2(true, true, true, true, false); + do_encoding_open_channelv2(true, true, true, true, true); } fn do_encoding_accept_channel(shutdown: bool) { @@ -5524,7 +5557,10 @@ mod tests { do_encoding_accept_channel(true); } - fn do_encoding_accept_channelv2(shutdown: bool) { + fn do_encoding_accept_channelv2( + shutdown: bool, incl_chan_type: bool, require_confirmed_inputs: bool, + disable_channel_reserve: bool, + ) { let secp_ctx = Secp256k1::new(); let (_, pubkey_1) = get_keys_from!( "0101010101010101010101010101010101010101010101010101010101010101", @@ -5580,11 +5616,16 @@ mod tests { } else { None }, - channel_type: None, + channel_type: if incl_chan_type { + Some(ChannelTypeFeatures::empty()) + } else { + None + }, }, funding_satoshis: 1311768467284833366, second_per_commitment_point: pubkey_7, - require_confirmed_inputs: None, + require_confirmed_inputs: require_confirmed_inputs.then_some(()), + disable_channel_reserve: disable_channel_reserve.then_some(()), }; let encoded_value = accept_channelv2.encode(); let mut target_value = @@ -5645,13 +5686,36 @@ mod tests { .unwrap(), ); } + if incl_chan_type { + target_value.append(&mut >::from_hex("0100").unwrap()); + } + if require_confirmed_inputs { + target_value.append(&mut >::from_hex("0200").unwrap()); + } + if disable_channel_reserve { + target_value.append(&mut >::from_hex("0400").unwrap()); + } assert_eq!(encoded_value, target_value); } #[test] fn encoding_accept_channelv2() { - do_encoding_accept_channelv2(false); - do_encoding_accept_channelv2(true); + do_encoding_accept_channelv2(false, false, false, false); + do_encoding_accept_channelv2(false, false, false, true); + do_encoding_accept_channelv2(false, false, true, false); + do_encoding_accept_channelv2(false, false, true, true); + do_encoding_accept_channelv2(false, true, false, false); + do_encoding_accept_channelv2(false, true, false, true); + do_encoding_accept_channelv2(false, true, true, false); + do_encoding_accept_channelv2(false, true, true, true); + do_encoding_accept_channelv2(true, false, false, false); + do_encoding_accept_channelv2(true, false, false, true); + do_encoding_accept_channelv2(true, false, true, false); + do_encoding_accept_channelv2(true, false, true, true); + do_encoding_accept_channelv2(true, true, false, false); + do_encoding_accept_channelv2(true, true, false, true); + do_encoding_accept_channelv2(true, true, true, false); + do_encoding_accept_channelv2(true, true, true, true); } #[test] From 3542a15cae81916481ca54f706d1b539d9150c1f Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 26 Feb 2026 03:01:46 +0000 Subject: [PATCH 7/7] Format `ChannelManager::create_channel_internal` and... `ChannelContext::do_accept_channel_checks`, `ChannelContext::new_for_outbound_channel`, `ChannelContext::new_for_inbound_channel`, `InboundV1Channel::new`, `OutboundV1Channel::new`. --- lightning/src/ln/channel.rs | 731 ++++++++++++++++++++--------- lightning/src/ln/channelmanager.rs | 61 ++- 2 files changed, 554 insertions(+), 238 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 5b064e50e0d..1e6e23f5bad 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -3571,154 +3571,258 @@ impl InitialRemoteCommitmentReceiver for FundedChannel ChannelContext { - #[rustfmt::skip] fn new_for_inbound_channel<'a, ES: EntropySource, F: FeeEstimator, L: Logger>( - fee_estimator: &'a LowerBoundedFeeEstimator, - entropy_source: &'a ES, - signer_provider: &'a SP, - counterparty_node_id: PublicKey, - their_features: &'a InitFeatures, - user_id: u128, - config: &'a UserConfig, - current_chain_height: u32, - logger: &'a L, - trusted_channel_features: Option, - our_funding_satoshis: u64, - counterparty_pubkeys: ChannelPublicKeys, - channel_type: ChannelTypeFeatures, - holder_selected_channel_reserve_satoshis: u64, - msg_channel_reserve_satoshis: u64, - msg_push_msat: u64, - open_channel_fields: msgs::CommonOpenChannelFields, + fee_estimator: &'a LowerBoundedFeeEstimator, entropy_source: &'a ES, + signer_provider: &'a SP, counterparty_node_id: PublicKey, their_features: &'a InitFeatures, + user_id: u128, config: &'a UserConfig, current_chain_height: u32, logger: &'a L, + trusted_channel_features: Option, our_funding_satoshis: u64, + counterparty_pubkeys: ChannelPublicKeys, channel_type: ChannelTypeFeatures, + holder_selected_channel_reserve_satoshis: u64, msg_channel_reserve_satoshis: u64, + msg_push_msat: u64, open_channel_fields: msgs::CommonOpenChannelFields, ) -> Result<(FundingScope, ChannelContext), ChannelError> { - let logger = WithContext::from(logger, Some(counterparty_node_id), Some(open_channel_fields.temporary_channel_id), None); - let announce_for_forwarding = if (open_channel_fields.channel_flags & 1) == 1 { true } else { false }; + let logger = WithContext::from( + logger, + Some(counterparty_node_id), + Some(open_channel_fields.temporary_channel_id), + None, + ); + let announce_for_forwarding = + if (open_channel_fields.channel_flags & 1) == 1 { true } else { false }; - let channel_value_satoshis = our_funding_satoshis.saturating_add(open_channel_fields.funding_satoshis); + let channel_value_satoshis = + our_funding_satoshis.saturating_add(open_channel_fields.funding_satoshis); let channel_keys_id = signer_provider.generate_channel_keys_id(true, user_id); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); if config.channel_handshake_config.our_to_self_delay < BREAKDOWN_TIMEOUT { - return Err(ChannelError::close(format!("Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks. It must be greater than {}", config.channel_handshake_config.our_to_self_delay, BREAKDOWN_TIMEOUT))); + return Err(ChannelError::close(format!( + "Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks. It must be greater than {}", + config.channel_handshake_config.our_to_self_delay, BREAKDOWN_TIMEOUT + ))); } if channel_value_satoshis >= TOTAL_BITCOIN_SUPPLY_SATOSHIS { - return Err(ChannelError::close(format!("Funding must be smaller than the total bitcoin supply. It was {}", channel_value_satoshis))); + return Err(ChannelError::close(format!( + "Funding must be smaller than the total bitcoin supply. It was {}", + channel_value_satoshis + ))); } if msg_channel_reserve_satoshis > channel_value_satoshis { - return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must be no greater than channel_value_satoshis: {}", msg_channel_reserve_satoshis, channel_value_satoshis))); + return Err(ChannelError::close(format!( + "Bogus channel_reserve_satoshis ({}). Must be no greater than channel_value_satoshis: {}", + msg_channel_reserve_satoshis, channel_value_satoshis + ))); } - let full_channel_value_msat = (channel_value_satoshis - msg_channel_reserve_satoshis) * 1000; + let full_channel_value_msat = + (channel_value_satoshis - msg_channel_reserve_satoshis) * 1000; if msg_push_msat > full_channel_value_msat { - return Err(ChannelError::close(format!("push_msat {} was larger than channel amount minus reserve ({})", msg_push_msat, full_channel_value_msat))); + return Err(ChannelError::close(format!( + "push_msat {} was larger than channel amount minus reserve ({})", + msg_push_msat, full_channel_value_msat + ))); } if open_channel_fields.dust_limit_satoshis > channel_value_satoshis { - return Err(ChannelError::close(format!("dust_limit_satoshis {} was larger than channel_value_satoshis {}. Peer never wants payout outputs?", open_channel_fields.dust_limit_satoshis, channel_value_satoshis))); + return Err(ChannelError::close(format!( + "dust_limit_satoshis {} was larger than channel_value_satoshis {}. Peer never wants payout outputs?", + open_channel_fields.dust_limit_satoshis, channel_value_satoshis + ))); } if open_channel_fields.htlc_minimum_msat >= full_channel_value_msat { - return Err(ChannelError::close(format!("Minimum htlc value ({}) was larger than full channel value ({})", open_channel_fields.htlc_minimum_msat, full_channel_value_msat))); + return Err(ChannelError::close(format!( + "Minimum htlc value ({}) was larger than full channel value ({})", + open_channel_fields.htlc_minimum_msat, full_channel_value_msat + ))); } - FundedChannel::::check_remote_fee(&channel_type, fee_estimator, open_channel_fields.commitment_feerate_sat_per_1000_weight, None, &&logger)?; + FundedChannel::::check_remote_fee( + &channel_type, + fee_estimator, + open_channel_fields.commitment_feerate_sat_per_1000_weight, + None, + &&logger, + )?; - let max_counterparty_selected_contest_delay = u16::min(config.channel_handshake_limits.their_to_self_delay, MAX_LOCAL_BREAKDOWN_TIMEOUT); + let max_counterparty_selected_contest_delay = u16::min( + config.channel_handshake_limits.their_to_self_delay, + MAX_LOCAL_BREAKDOWN_TIMEOUT, + ); if open_channel_fields.to_self_delay > max_counterparty_selected_contest_delay { - return Err(ChannelError::close(format!("They wanted our payments to be delayed by a needlessly long period. Upper limit: {}. Actual: {}", max_counterparty_selected_contest_delay, open_channel_fields.to_self_delay))); + return Err(ChannelError::close(format!( + "They wanted our payments to be delayed by a needlessly long period. Upper limit: {}. Actual: {}", + max_counterparty_selected_contest_delay, open_channel_fields.to_self_delay + ))); } if open_channel_fields.max_accepted_htlcs < 1 { - return Err(ChannelError::close("0 max_accepted_htlcs makes for a useless channel".to_owned())); + return Err(ChannelError::close( + "0 max_accepted_htlcs makes for a useless channel".to_owned(), + )); } if open_channel_fields.max_accepted_htlcs > max_htlcs(&channel_type) { - return Err(ChannelError::close(format!("max_accepted_htlcs was {}. It must not be larger than {}", open_channel_fields.max_accepted_htlcs, max_htlcs(&channel_type)))); + return Err(ChannelError::close(format!( + "max_accepted_htlcs was {}. It must not be larger than {}", + open_channel_fields.max_accepted_htlcs, + max_htlcs(&channel_type) + ))); } // Now check against optional parameters as set by config... if channel_value_satoshis < config.channel_handshake_limits.min_funding_satoshis { - return Err(ChannelError::close(format!("Funding satoshis ({}) is less than the user specified limit ({})", channel_value_satoshis, config.channel_handshake_limits.min_funding_satoshis))); + return Err(ChannelError::close(format!( + "Funding satoshis ({}) is less than the user specified limit ({})", + channel_value_satoshis, config.channel_handshake_limits.min_funding_satoshis + ))); } - if open_channel_fields.htlc_minimum_msat > config.channel_handshake_limits.max_htlc_minimum_msat { - return Err(ChannelError::close(format!("htlc_minimum_msat ({}) is higher than the user specified limit ({})", open_channel_fields.htlc_minimum_msat, config.channel_handshake_limits.max_htlc_minimum_msat))); + if open_channel_fields.htlc_minimum_msat + > config.channel_handshake_limits.max_htlc_minimum_msat + { + return Err(ChannelError::close(format!( + "htlc_minimum_msat ({}) is higher than the user specified limit ({})", + open_channel_fields.htlc_minimum_msat, + config.channel_handshake_limits.max_htlc_minimum_msat + ))); } - if open_channel_fields.max_htlc_value_in_flight_msat < config.channel_handshake_limits.min_max_htlc_value_in_flight_msat { - return Err(ChannelError::close(format!("max_htlc_value_in_flight_msat ({}) is less than the user specified limit ({})", open_channel_fields.max_htlc_value_in_flight_msat, config.channel_handshake_limits.min_max_htlc_value_in_flight_msat))); + if open_channel_fields.max_htlc_value_in_flight_msat + < config.channel_handshake_limits.min_max_htlc_value_in_flight_msat + { + return Err(ChannelError::close(format!( + "max_htlc_value_in_flight_msat ({}) is less than the user specified limit ({})", + open_channel_fields.max_htlc_value_in_flight_msat, + config.channel_handshake_limits.min_max_htlc_value_in_flight_msat + ))); } - if msg_channel_reserve_satoshis > config.channel_handshake_limits.max_channel_reserve_satoshis { - return Err(ChannelError::close(format!("channel_reserve_satoshis ({}) is higher than the user specified limit ({})", msg_channel_reserve_satoshis, config.channel_handshake_limits.max_channel_reserve_satoshis))); + if msg_channel_reserve_satoshis + > config.channel_handshake_limits.max_channel_reserve_satoshis + { + return Err(ChannelError::close(format!( + "channel_reserve_satoshis ({}) is higher than the user specified limit ({})", + msg_channel_reserve_satoshis, + config.channel_handshake_limits.max_channel_reserve_satoshis + ))); } - if open_channel_fields.max_accepted_htlcs < config.channel_handshake_limits.min_max_accepted_htlcs { - return Err(ChannelError::close(format!("max_accepted_htlcs ({}) is less than the user specified limit ({})", open_channel_fields.max_accepted_htlcs, config.channel_handshake_limits.min_max_accepted_htlcs))); + if open_channel_fields.max_accepted_htlcs + < config.channel_handshake_limits.min_max_accepted_htlcs + { + return Err(ChannelError::close(format!( + "max_accepted_htlcs ({}) is less than the user specified limit ({})", + open_channel_fields.max_accepted_htlcs, + config.channel_handshake_limits.min_max_accepted_htlcs + ))); } if open_channel_fields.dust_limit_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { - return Err(ChannelError::close(format!("dust_limit_satoshis ({}) is less than the implementation limit ({})", open_channel_fields.dust_limit_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS))); + return Err(ChannelError::close(format!( + "dust_limit_satoshis ({}) is less than the implementation limit ({})", + open_channel_fields.dust_limit_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS + ))); } - if open_channel_fields.dust_limit_satoshis > MAX_CHAN_DUST_LIMIT_SATOSHIS { - return Err(ChannelError::close(format!("dust_limit_satoshis ({}) is greater than the implementation limit ({})", open_channel_fields.dust_limit_satoshis, MAX_CHAN_DUST_LIMIT_SATOSHIS))); + if open_channel_fields.dust_limit_satoshis > MAX_CHAN_DUST_LIMIT_SATOSHIS { + return Err(ChannelError::close(format!( + "dust_limit_satoshis ({}) is greater than the implementation limit ({})", + open_channel_fields.dust_limit_satoshis, MAX_CHAN_DUST_LIMIT_SATOSHIS + ))); } // Convert things into internal flags and prep our state: if config.channel_handshake_limits.force_announced_channel_preference { if config.channel_handshake_config.announce_for_forwarding != announce_for_forwarding { - return Err(ChannelError::close("Peer tried to open channel but their announcement preference is different from ours".to_owned())); + return Err(ChannelError::close(String::from( + "Peer tried to open channel but their announcement preference is different from ours" + ))); } } - if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS && holder_selected_channel_reserve_satoshis != 0 { + 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))); + 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 + ))); } if holder_selected_channel_reserve_satoshis * 1000 >= full_channel_value_msat { - return Err(ChannelError::close(format!("Suitable channel reserve not found. remote_channel_reserve was ({})msats. Channel value is ({} - {})msats.", holder_selected_channel_reserve_satoshis * 1000, full_channel_value_msat, msg_push_msat))); + return Err(ChannelError::close(format!( + "Suitable channel reserve not found. remote_channel_reserve was ({})msats. Channel value is ({} - {})msats.", + holder_selected_channel_reserve_satoshis * 1000, full_channel_value_msat, msg_push_msat + ))); } if msg_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { - 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.", + 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 && 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))); + 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 + ))); } // v1 channel opens set `our_funding_satoshis` to 0, and v2 channel opens set `msg_push_msat` to 0. debug_assert!(our_funding_satoshis == 0 || msg_push_msat == 0); let value_to_self_msat = our_funding_satoshis * 1000 + msg_push_msat; - let counterparty_shutdown_scriptpubkey = if their_features.supports_upfront_shutdown_script() { - match &open_channel_fields.shutdown_scriptpubkey { - &Some(ref script) => { - // Peer is signaling upfront_shutdown and has opt-out with a 0-length script. We don't enforce anything - if script.len() == 0 { - None - } else { - if !script::is_bolt2_compliant(&script, their_features) { - return Err(ChannelError::close(format!("Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {}", script))) + let counterparty_shutdown_scriptpubkey = + if their_features.supports_upfront_shutdown_script() { + match &open_channel_fields.shutdown_scriptpubkey { + &Some(ref script) => { + // Peer is signaling upfront_shutdown and has opt-out with a 0-length script. We don't enforce anything + if script.len() == 0 { + None + } else { + if !script::is_bolt2_compliant(&script, their_features) { + return Err(ChannelError::close(format!( + "Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {}", + script + ))); + } + Some(script.clone()) } - Some(script.clone()) - } - }, - // Peer is signaling upfront shutdown but don't opt-out with correct mechanism (a.k.a 0-length script). Peer looks buggy, we fail the channel - &None => { - return Err(ChannelError::close("Peer is signaling upfront_shutdown but we don't get any script. Use 0-length script to opt-out".to_owned())); + }, + // Peer is signaling upfront shutdown but don't opt-out with correct mechanism (a.k.a 0-length script). Peer looks buggy, we fail the channel + &None => { + return Err(ChannelError::close(String::from( + "Peer is signaling upfront_shutdown but we don't get any script. Use 0-length script to opt-out" + ))); + }, } - } - } else { None }; + } else { + None + }; - let shutdown_scriptpubkey = if config.channel_handshake_config.commit_upfront_shutdown_pubkey { - match signer_provider.get_shutdown_scriptpubkey() { - Ok(scriptpubkey) => Some(scriptpubkey), - Err(_) => return Err(ChannelError::close("Failed to get upfront shutdown scriptpubkey".to_owned())), - } - } else { None }; + let shutdown_scriptpubkey = + if config.channel_handshake_config.commit_upfront_shutdown_pubkey { + match signer_provider.get_shutdown_scriptpubkey() { + Ok(scriptpubkey) => Some(scriptpubkey), + Err(_) => { + return Err(ChannelError::close( + "Failed to get upfront shutdown scriptpubkey".to_owned(), + )) + }, + } + } else { + None + }; if let Some(shutdown_scriptpubkey) = &shutdown_scriptpubkey { if !shutdown_scriptpubkey.is_compatible(&their_features) { - return Err(ChannelError::close(format!("Provided a scriptpubkey format not accepted by peer: {}", shutdown_scriptpubkey))); + return Err(ChannelError::close(format!( + "Provided a scriptpubkey format not accepted by peer: {}", + shutdown_scriptpubkey + ))); } } let destination_script = match signer_provider.get_destination_script(channel_keys_id) { Ok(script) => script, - Err(_) => return Err(ChannelError::close("Failed to get destination script".to_owned())), + Err(_) => { + return Err(ChannelError::close("Failed to get destination script".to_owned())) + }, }; let mut secp_ctx = Secp256k1::new(); @@ -3740,9 +3844,15 @@ impl ChannelContext { holder_selected_channel_reserve_satoshis, #[cfg(debug_assertions)] - holder_prev_commitment_tx_balance: Mutex::new((value_to_self_msat, (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat))), + holder_prev_commitment_tx_balance: Mutex::new(( + value_to_self_msat, + (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat), + )), #[cfg(debug_assertions)] - counterparty_prev_commitment_tx_balance: Mutex::new((value_to_self_msat, (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat))), + counterparty_prev_commitment_tx_balance: Mutex::new(( + value_to_self_msat, + (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat), + )), #[cfg(any(test, fuzzing))] next_local_fee: Mutex::new(PredictedNextFee::default()), @@ -3774,7 +3884,9 @@ impl ChannelContext { config: LegacyChannelConfig { options: config.channel_config.clone(), announce_for_forwarding, - commit_upfront_shutdown_pubkey: config.channel_handshake_config.commit_upfront_shutdown_pubkey, + commit_upfront_shutdown_pubkey: config + .channel_handshake_config + .commit_upfront_shutdown_pubkey, }, prev_config: None, @@ -3784,7 +3896,7 @@ impl ChannelContext { temporary_channel_id: Some(open_channel_fields.temporary_channel_id), channel_id: open_channel_fields.temporary_channel_id, channel_state: ChannelState::NegotiatingFunding( - NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT + NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT, ), announcement_sigs_state: AnnouncementSigsState::NotSent, secp_ctx, @@ -3836,19 +3948,35 @@ impl ChannelContext { feerate_per_kw: open_channel_fields.commitment_feerate_sat_per_1000_weight, counterparty_dust_limit_satoshis: open_channel_fields.dust_limit_satoshis, holder_dust_limit_satoshis: MIN_CHAN_DUST_LIMIT_SATOSHIS, - counterparty_max_htlc_value_in_flight_msat: cmp::min(open_channel_fields.max_htlc_value_in_flight_msat, channel_value_satoshis * 1000), - holder_max_htlc_value_in_flight_msat: get_holder_max_htlc_value_in_flight_msat(channel_value_satoshis, &config.channel_handshake_config), + counterparty_max_htlc_value_in_flight_msat: cmp::min( + open_channel_fields.max_htlc_value_in_flight_msat, + channel_value_satoshis * 1000, + ), + holder_max_htlc_value_in_flight_msat: get_holder_max_htlc_value_in_flight_msat( + channel_value_satoshis, + &config.channel_handshake_config, + ), counterparty_htlc_minimum_msat: open_channel_fields.htlc_minimum_msat, - holder_htlc_minimum_msat: if config.channel_handshake_config.our_htlc_minimum_msat == 0 { 1 } else { config.channel_handshake_config.our_htlc_minimum_msat }, + holder_htlc_minimum_msat: if config.channel_handshake_config.our_htlc_minimum_msat == 0 + { + 1 + } else { + config.channel_handshake_config.our_htlc_minimum_msat + }, counterparty_max_accepted_htlcs: open_channel_fields.max_accepted_htlcs, - holder_max_accepted_htlcs: cmp::min(config.channel_handshake_config.our_max_accepted_htlcs, max_htlcs(&channel_type)), + holder_max_accepted_htlcs: cmp::min( + config.channel_handshake_config.our_max_accepted_htlcs, + max_htlcs(&channel_type), + ), minimum_depth, counterparty_forwarding_info: None, is_batch_funding: None, - counterparty_next_commitment_point: Some(open_channel_fields.first_per_commitment_point), + counterparty_next_commitment_point: Some( + open_channel_fields.first_per_commitment_point, + ), counterparty_current_commitment_point: None, counterparty_node_id, @@ -3885,100 +4013,139 @@ impl ChannelContext { // check if the funder's amount for the initial commitment tx is sufficient // for full fee payment plus a few HTLCs to ensure the channel will be useful. - let funders_amount_msat = funding.get_value_satoshis() * 1000 - funding.get_value_to_self_msat(); + let funders_amount_msat = + funding.get_value_satoshis() * 1000 - funding.get_value_to_self_msat(); let htlc_candidate = None; let include_counterparty_unknown_htlcs = false; let addl_nondust_htlc_count = MIN_AFFORDABLE_HTLC_COUNT; - let dust_exposure_limiting_feerate = channel_context.get_dust_exposure_limiting_feerate(&fee_estimator, funding.get_channel_type()); - let (remote_stats, _remote_htlcs) = channel_context.get_next_remote_commitment_stats( - &funding, - htlc_candidate, - include_counterparty_unknown_htlcs, - addl_nondust_htlc_count, - channel_context.feerate_per_kw, - dust_exposure_limiting_feerate - ).map_err(|()| ChannelError::close(format!("Funding amount ({} sats) can't even pay fee for initial commitment transaction.", funders_amount_msat / 1000)))?; + let dust_exposure_limiting_feerate = channel_context + .get_dust_exposure_limiting_feerate(&fee_estimator, funding.get_channel_type()); + let (remote_stats, _remote_htlcs) = channel_context + .get_next_remote_commitment_stats( + &funding, + htlc_candidate, + include_counterparty_unknown_htlcs, + addl_nondust_htlc_count, + channel_context.feerate_per_kw, + dust_exposure_limiting_feerate, + ) + .map_err(|()| { + ChannelError::close(format!( + "Funding amount ({} sats) can't even pay fee for initial commitment transaction.", + funders_amount_msat / 1000 + )) + })?; // While it's reasonable for us to not meet the channel reserve initially (if they don't // want to push much to us), our counterparty should always have more than our reserve. - if remote_stats.commitment_stats.counterparty_balance_msat / 1000 < funding.holder_selected_channel_reserve_satoshis { - return Err(ChannelError::close("Insufficient funding amount for initial reserve".to_owned())); + if remote_stats.commitment_stats.counterparty_balance_msat / 1000 + < funding.holder_selected_channel_reserve_satoshis + { + return Err(ChannelError::close( + "Insufficient funding amount for initial reserve".to_owned(), + )); } Ok((funding, channel_context)) } - #[rustfmt::skip] fn new_for_outbound_channel<'a, ES: EntropySource, F: FeeEstimator, L: Logger>( - fee_estimator: &'a LowerBoundedFeeEstimator, - entropy_source: &'a ES, - signer_provider: &'a SP, - counterparty_node_id: PublicKey, - their_features: &'a InitFeatures, - funding_satoshis: u64, - push_msat: u64, - user_id: u128, - config: &'a UserConfig, - current_chain_height: u32, - outbound_scid_alias: u64, + fee_estimator: &'a LowerBoundedFeeEstimator, entropy_source: &'a ES, + signer_provider: &'a SP, counterparty_node_id: PublicKey, their_features: &'a InitFeatures, + funding_satoshis: u64, push_msat: u64, user_id: u128, config: &'a UserConfig, + current_chain_height: u32, outbound_scid_alias: u64, temporary_channel_id_fn: Option ChannelId>, - holder_selected_channel_reserve_satoshis: u64, - channel_keys_id: [u8; 32], - holder_signer: SP::EcdsaSigner, - _logger: L, + holder_selected_channel_reserve_satoshis: u64, channel_keys_id: [u8; 32], + holder_signer: SP::EcdsaSigner, _logger: L, ) -> Result<(FundingScope, ChannelContext), APIError> { // This will be updated with the counterparty contribution if this is a dual-funded channel let channel_value_satoshis = funding_satoshis; let holder_selected_contest_delay = config.channel_handshake_config.our_to_self_delay; - if !their_features.supports_wumbo() && channel_value_satoshis > MAX_FUNDING_SATOSHIS_NO_WUMBO { - return Err(APIError::APIMisuseError{err: format!("funding_value must not exceed {}, it was {}", MAX_FUNDING_SATOSHIS_NO_WUMBO, channel_value_satoshis)}); + if !their_features.supports_wumbo() + && channel_value_satoshis > MAX_FUNDING_SATOSHIS_NO_WUMBO + { + return Err(APIError::APIMisuseError { + err: format!( + "funding_value must not exceed {}, it was {}", + MAX_FUNDING_SATOSHIS_NO_WUMBO, channel_value_satoshis + ), + }); } if channel_value_satoshis >= TOTAL_BITCOIN_SUPPLY_SATOSHIS { - return Err(APIError::APIMisuseError{err: format!("funding_value must be smaller than the total bitcoin supply, it was {}", channel_value_satoshis)}); + return Err(APIError::APIMisuseError { + err: format!( + "funding_value must be smaller than the total bitcoin supply, it was {}", + channel_value_satoshis + ), + }); } let channel_value_msat = channel_value_satoshis * 1000; if push_msat > channel_value_msat { - return Err(APIError::APIMisuseError { err: format!("Push value ({}) was larger than channel_value ({})", push_msat, channel_value_msat) }); + return Err(APIError::APIMisuseError { + err: format!( + "Push value ({}) was larger than channel_value ({})", + push_msat, channel_value_msat + ), + }); } if holder_selected_contest_delay < BREAKDOWN_TIMEOUT { - return Err(APIError::APIMisuseError {err: format!("Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks", holder_selected_contest_delay)}); + return Err(APIError::APIMisuseError { + err: format!( + "Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks", + holder_selected_contest_delay + ), + }); } let channel_type = get_initial_channel_type(&config, their_features); debug_assert!(!channel_type.supports_any_optional_bits()); - debug_assert!(!channel_type.requires_unknown_bits_from(&channelmanager::provided_channel_type_features(&config))); + debug_assert!(!channel_type + .requires_unknown_bits_from(&channelmanager::provided_channel_type_features(&config))); - let commitment_feerate = selected_commitment_sat_per_1000_weight( - &fee_estimator, &channel_type, - ); + let commitment_feerate = + selected_commitment_sat_per_1000_weight(&fee_estimator, &channel_type); let value_to_self_msat = channel_value_satoshis * 1000 - push_msat; let mut secp_ctx = Secp256k1::new(); secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); - let shutdown_scriptpubkey = if config.channel_handshake_config.commit_upfront_shutdown_pubkey { - match signer_provider.get_shutdown_scriptpubkey() { - Ok(scriptpubkey) => Some(scriptpubkey), - Err(_) => return Err(APIError::ChannelUnavailable { err: "Failed to get shutdown scriptpubkey".to_owned()}), - } - } else { None }; + let shutdown_scriptpubkey = + if config.channel_handshake_config.commit_upfront_shutdown_pubkey { + match signer_provider.get_shutdown_scriptpubkey() { + Ok(scriptpubkey) => Some(scriptpubkey), + Err(_) => { + return Err(APIError::ChannelUnavailable { + err: "Failed to get shutdown scriptpubkey".to_owned(), + }) + }, + } + } else { + None + }; if let Some(shutdown_scriptpubkey) = &shutdown_scriptpubkey { if !shutdown_scriptpubkey.is_compatible(&their_features) { - return Err(APIError::IncompatibleShutdownScript { script: shutdown_scriptpubkey.clone() }); + return Err(APIError::IncompatibleShutdownScript { + script: shutdown_scriptpubkey.clone(), + }); } } let destination_script = match signer_provider.get_destination_script(channel_keys_id) { Ok(script) => script, - Err(_) => return Err(APIError::ChannelUnavailable { err: "Failed to get destination script".to_owned()}), + Err(_) => { + return Err(APIError::ChannelUnavailable { + err: "Failed to get destination script".to_owned(), + }) + }, }; let pubkeys = holder_signer.pubkeys(&secp_ctx); - let temporary_channel_id = temporary_channel_id_fn.map(|f| f(&pubkeys)) + let temporary_channel_id = temporary_channel_id_fn + .map(|f| f(&pubkeys)) .unwrap_or_else(|| ChannelId::temporary_from_entropy_source(entropy_source)); let funding = FundingScope { @@ -3989,9 +4156,15 @@ impl ChannelContext { // We'll add our counterparty's `funding_satoshis` to these max commitment output assertions // when we receive `accept_channel2`. #[cfg(debug_assertions)] - holder_prev_commitment_tx_balance: Mutex::new((channel_value_satoshis * 1000 - push_msat, push_msat)), + holder_prev_commitment_tx_balance: Mutex::new(( + channel_value_satoshis * 1000 - push_msat, + push_msat, + )), #[cfg(debug_assertions)] - counterparty_prev_commitment_tx_balance: Mutex::new((channel_value_satoshis * 1000 - push_msat, push_msat)), + counterparty_prev_commitment_tx_balance: Mutex::new(( + channel_value_satoshis * 1000 - push_msat, + push_msat, + )), #[cfg(any(test, fuzzing))] next_local_fee: Mutex::new(PredictedNextFee::default()), @@ -4021,7 +4194,9 @@ impl ChannelContext { config: LegacyChannelConfig { options: config.channel_config.clone(), announce_for_forwarding: config.channel_handshake_config.announce_for_forwarding, - commit_upfront_shutdown_pubkey: config.channel_handshake_config.commit_upfront_shutdown_pubkey, + commit_upfront_shutdown_pubkey: config + .channel_handshake_config + .commit_upfront_shutdown_pubkey, }, prev_config: None, @@ -4084,11 +4259,22 @@ impl ChannelContext { counterparty_max_htlc_value_in_flight_msat: 0, // We'll adjust this to include our counterparty's `funding_satoshis` when we // receive `accept_channel2`. - holder_max_htlc_value_in_flight_msat: get_holder_max_htlc_value_in_flight_msat(channel_value_satoshis, &config.channel_handshake_config), + holder_max_htlc_value_in_flight_msat: get_holder_max_htlc_value_in_flight_msat( + channel_value_satoshis, + &config.channel_handshake_config, + ), counterparty_htlc_minimum_msat: 0, - holder_htlc_minimum_msat: if config.channel_handshake_config.our_htlc_minimum_msat == 0 { 1 } else { config.channel_handshake_config.our_htlc_minimum_msat }, + holder_htlc_minimum_msat: if config.channel_handshake_config.our_htlc_minimum_msat == 0 + { + 1 + } else { + config.channel_handshake_config.our_htlc_minimum_msat + }, counterparty_max_accepted_htlcs: 0, - holder_max_accepted_htlcs: cmp::min(config.channel_handshake_config.our_max_accepted_htlcs, max_htlcs(&channel_type)), + holder_max_accepted_htlcs: cmp::min( + config.channel_handshake_config.our_max_accepted_htlcs, + max_htlcs(&channel_type), + ), minimum_depth: None, // Filled in in accept_channel counterparty_forwarding_info: None, @@ -4131,15 +4317,23 @@ impl ChannelContext { let htlc_candidate = None; let include_counterparty_unknown_htlcs = false; let addl_nondust_htlc_count = MIN_AFFORDABLE_HTLC_COUNT; - let dust_exposure_limiting_feerate = channel_context.get_dust_exposure_limiting_feerate(&fee_estimator, funding.get_channel_type()); - let _local_stats = channel_context.get_next_local_commitment_stats( - &funding, - htlc_candidate, - include_counterparty_unknown_htlcs, - addl_nondust_htlc_count, - channel_context.feerate_per_kw, - dust_exposure_limiting_feerate, - ).map_err(|()| APIError::APIMisuseError { err: format!("Funding amount ({}) can't even pay fee for initial commitment transaction.", funding.get_value_to_self_msat() / 1000)})?; + let dust_exposure_limiting_feerate = channel_context + .get_dust_exposure_limiting_feerate(&fee_estimator, funding.get_channel_type()); + let _local_stats = channel_context + .get_next_local_commitment_stats( + &funding, + htlc_candidate, + include_counterparty_unknown_htlcs, + addl_nondust_htlc_count, + channel_context.feerate_per_kw, + dust_exposure_limiting_feerate, + ) + .map_err(|()| APIError::APIMisuseError { + err: format!( + "Funding amount ({}) can't even pay fee for initial commitment transaction.", + funding.get_value_to_self_msat() / 1000 + ), + })?; Ok((funding, channel_context)) } @@ -4365,103 +4559,181 @@ impl ChannelContext { /// Performs checks against necessary constraints after receiving either an `accept_channel` or /// `accept_channel2` message. - #[rustfmt::skip] pub fn do_accept_channel_checks( &mut self, funding: &mut FundingScope, default_limits: &ChannelHandshakeLimits, their_features: &InitFeatures, common_fields: &msgs::CommonAcceptChannelFields, channel_reserve_satoshis: u64, ) -> Result<(), ChannelError> { - let peer_limits = if let Some(ref limits) = self.inbound_handshake_limits_override { limits } else { default_limits }; + let peer_limits = if let Some(ref limits) = self.inbound_handshake_limits_override { + limits + } else { + default_limits + }; // Check sanity of message fields: if !funding.is_outbound() { - return Err(ChannelError::close("Got an accept_channel message from an inbound peer".to_owned())); + return Err(ChannelError::close( + "Got an accept_channel message from an inbound peer".to_owned(), + )); } - if !matches!(self.channel_state, ChannelState::NegotiatingFunding(flags) if flags == NegotiatingFundingFlags::OUR_INIT_SENT) { - return Err(ChannelError::close("Got an accept_channel message at a strange time".to_owned())); + if !matches!(self.channel_state, ChannelState::NegotiatingFunding(flags) + if flags == NegotiatingFundingFlags::OUR_INIT_SENT) + { + return Err(ChannelError::close( + "Got an accept_channel message at a strange time".to_owned(), + )); } - let channel_type = common_fields.channel_type.as_ref() - .ok_or_else(|| ChannelError::close("option_channel_type assumed to be supported".to_owned()))?; + let channel_type = common_fields.channel_type.as_ref().ok_or_else(|| { + ChannelError::close("option_channel_type assumed to be supported".to_owned()) + })?; if channel_type != funding.get_channel_type() { - return Err(ChannelError::close("Channel Type in accept_channel didn't match the one sent in open_channel.".to_owned())); + return Err(ChannelError::close(String::from( + "Channel Type in accept_channel didn't match the one sent in open_channel.", + ))); } if common_fields.dust_limit_satoshis > 21000000 * 100000000 { - return Err(ChannelError::close(format!("Peer never wants payout outputs? dust_limit_satoshis was {}", common_fields.dust_limit_satoshis))); + return Err(ChannelError::close(format!( + "Peer never wants payout outputs? dust_limit_satoshis was {}", + common_fields.dust_limit_satoshis + ))); } if channel_reserve_satoshis > funding.get_value_satoshis() { - return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than ({})", channel_reserve_satoshis, funding.get_value_satoshis()))); + return Err(ChannelError::close(format!( + "Bogus channel_reserve_satoshis ({}). Must not be greater than ({})", + channel_reserve_satoshis, + funding.get_value_satoshis() + ))); } - if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis && funding.holder_selected_channel_reserve_satoshis != 0 { - return Err(ChannelError::close(format!("Dust limit ({}) is bigger than our channel reserve ({})", common_fields.dust_limit_satoshis, funding.holder_selected_channel_reserve_satoshis))); + if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis + && funding.holder_selected_channel_reserve_satoshis != 0 + { + return Err(ChannelError::close(format!( + "Dust limit ({}) is bigger than our channel reserve ({})", + common_fields.dust_limit_satoshis, funding.holder_selected_channel_reserve_satoshis + ))); } - if channel_reserve_satoshis > funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis { - return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than channel value minus our reserve ({})", - channel_reserve_satoshis, funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis))); + if channel_reserve_satoshis + > funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis + { + return Err(ChannelError::close(format!( + "Bogus channel_reserve_satoshis ({}). Must not be greater than channel value minus our reserve ({})", + channel_reserve_satoshis, + funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis + ))); } - let full_channel_value_msat = (funding.get_value_satoshis() - channel_reserve_satoshis) * 1000; + let full_channel_value_msat = + (funding.get_value_satoshis() - channel_reserve_satoshis) * 1000; if common_fields.htlc_minimum_msat >= full_channel_value_msat { - return Err(ChannelError::close(format!("Minimum htlc value ({}) is full channel value ({})", common_fields.htlc_minimum_msat, full_channel_value_msat))); + return Err(ChannelError::close(format!( + "Minimum htlc value ({}) is full channel value ({})", + common_fields.htlc_minimum_msat, full_channel_value_msat + ))); } - let max_delay_acceptable = u16::min(peer_limits.their_to_self_delay, MAX_LOCAL_BREAKDOWN_TIMEOUT); + let max_delay_acceptable = + u16::min(peer_limits.their_to_self_delay, MAX_LOCAL_BREAKDOWN_TIMEOUT); if common_fields.to_self_delay > max_delay_acceptable { - return Err(ChannelError::close(format!("They wanted our payments to be delayed by a needlessly long period. Upper limit: {}. Actual: {}", max_delay_acceptable, common_fields.to_self_delay))); + return Err(ChannelError::close(format!( + "They wanted our payments to be delayed by a needlessly long period. Upper limit: {}. Actual: {}", + max_delay_acceptable, common_fields.to_self_delay + ))); } if common_fields.max_accepted_htlcs < 1 { - return Err(ChannelError::close("0 max_accepted_htlcs makes for a useless channel".to_owned())); + return Err(ChannelError::close( + "0 max_accepted_htlcs makes for a useless channel".to_owned(), + )); } let channel_type = funding.get_channel_type(); if common_fields.max_accepted_htlcs > max_htlcs(channel_type) { - return Err(ChannelError::close(format!("max_accepted_htlcs was {}. It must not be larger than {}", common_fields.max_accepted_htlcs, max_htlcs(channel_type)))); + return Err(ChannelError::close(format!( + "max_accepted_htlcs was {}. It must not be larger than {}", + common_fields.max_accepted_htlcs, + max_htlcs(channel_type) + ))); } // Now check against optional parameters as set by config... if common_fields.htlc_minimum_msat > peer_limits.max_htlc_minimum_msat { - return Err(ChannelError::close(format!("htlc_minimum_msat ({}) is higher than the user specified limit ({})", common_fields.htlc_minimum_msat, peer_limits.max_htlc_minimum_msat))); + return Err(ChannelError::close(format!( + "htlc_minimum_msat ({}) is higher than the user specified limit ({})", + common_fields.htlc_minimum_msat, peer_limits.max_htlc_minimum_msat + ))); } - if common_fields.max_htlc_value_in_flight_msat < peer_limits.min_max_htlc_value_in_flight_msat { - return Err(ChannelError::close(format!("max_htlc_value_in_flight_msat ({}) is less than the user specified limit ({})", common_fields.max_htlc_value_in_flight_msat, peer_limits.min_max_htlc_value_in_flight_msat))); + if common_fields.max_htlc_value_in_flight_msat + < peer_limits.min_max_htlc_value_in_flight_msat + { + return Err(ChannelError::close(format!( + "max_htlc_value_in_flight_msat ({}) is less than the user specified limit ({})", + common_fields.max_htlc_value_in_flight_msat, + peer_limits.min_max_htlc_value_in_flight_msat + ))); } if channel_reserve_satoshis > peer_limits.max_channel_reserve_satoshis { - return Err(ChannelError::close(format!("channel_reserve_satoshis ({}) is higher than the user specified limit ({})", channel_reserve_satoshis, peer_limits.max_channel_reserve_satoshis))); + return Err(ChannelError::close(format!( + "channel_reserve_satoshis ({}) is higher than the user specified limit ({})", + channel_reserve_satoshis, peer_limits.max_channel_reserve_satoshis + ))); } if common_fields.max_accepted_htlcs < peer_limits.min_max_accepted_htlcs { - return Err(ChannelError::close(format!("max_accepted_htlcs ({}) is less than the user specified limit ({})", common_fields.max_accepted_htlcs, peer_limits.min_max_accepted_htlcs))); + return Err(ChannelError::close(format!( + "max_accepted_htlcs ({}) is less than the user specified limit ({})", + common_fields.max_accepted_htlcs, peer_limits.min_max_accepted_htlcs + ))); } if common_fields.dust_limit_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { - return Err(ChannelError::close(format!("dust_limit_satoshis ({}) is less than the implementation limit ({})", common_fields.dust_limit_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS))); + return Err(ChannelError::close(format!( + "dust_limit_satoshis ({}) is less than the implementation limit ({})", + common_fields.dust_limit_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS + ))); } if common_fields.dust_limit_satoshis > MAX_CHAN_DUST_LIMIT_SATOSHIS { - return Err(ChannelError::close(format!("dust_limit_satoshis ({}) is greater than the implementation limit ({})", common_fields.dust_limit_satoshis, MAX_CHAN_DUST_LIMIT_SATOSHIS))); + return Err(ChannelError::close(format!( + "dust_limit_satoshis ({}) is greater than the implementation limit ({})", + common_fields.dust_limit_satoshis, MAX_CHAN_DUST_LIMIT_SATOSHIS + ))); } if common_fields.minimum_depth > peer_limits.max_minimum_depth { - return Err(ChannelError::close(format!("We consider the minimum depth to be unreasonably large. Expected minimum: ({}). Actual: ({})", peer_limits.max_minimum_depth, common_fields.minimum_depth))); + return Err(ChannelError::close(format!( + "We consider the minimum depth to be unreasonably large. Expected minimum: ({}). Actual: ({})", + peer_limits.max_minimum_depth, common_fields.minimum_depth + ))); } - let counterparty_shutdown_scriptpubkey = if their_features.supports_upfront_shutdown_script() { - match &common_fields.shutdown_scriptpubkey { - &Some(ref script) => { - // Peer is signaling upfront_shutdown and has opt-out with a 0-length script. We don't enforce anything - if script.len() == 0 { - None - } else { - if !script::is_bolt2_compliant(&script, their_features) { - return Err(ChannelError::close(format!("Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {}", script))); + let counterparty_shutdown_scriptpubkey = + if their_features.supports_upfront_shutdown_script() { + match &common_fields.shutdown_scriptpubkey { + &Some(ref script) => { + // Peer is signaling upfront_shutdown and has opt-out with a 0-length script. We don't enforce anything + if script.len() == 0 { + None + } else { + if !script::is_bolt2_compliant(&script, their_features) { + return Err(ChannelError::close(format!( + "Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {}", + script + ))); + } + Some(script.clone()) } - Some(script.clone()) - } - }, - // Peer is signaling upfront shutdown but don't opt-out with correct mechanism (a.k.a 0-length script). Peer looks buggy, we fail the channel - &None => { - return Err(ChannelError::close("Peer is signaling upfront_shutdown but we don't get any script. Use 0-length script to opt-out".to_owned())); + }, + // Peer is signaling upfront shutdown but don't opt-out with correct mechanism (a.k.a 0-length script). Peer looks buggy, we fail the channel + &None => { + return Err(ChannelError::close(String::from( + "Peer is signaling upfront_shutdown but we don't get any script. Use 0-length script to opt-out" + ))); + }, } - } - } else { None }; + } else { + None + }; self.counterparty_dust_limit_satoshis = common_fields.dust_limit_satoshis; - self.counterparty_max_htlc_value_in_flight_msat = cmp::min(common_fields.max_htlc_value_in_flight_msat, funding.get_value_satoshis() * 1000); + self.counterparty_max_htlc_value_in_flight_msat = cmp::min( + common_fields.max_htlc_value_in_flight_msat, + funding.get_value_satoshis() * 1000, + ); funding.counterparty_selected_channel_reserve_satoshis = Some(channel_reserve_satoshis); self.counterparty_htlc_minimum_msat = common_fields.htlc_minimum_msat; self.counterparty_max_accepted_htlcs = common_fields.max_accepted_htlcs; @@ -4476,20 +4748,23 @@ impl ChannelContext { funding_pubkey: common_fields.funding_pubkey, revocation_basepoint: RevocationBasepoint::from(common_fields.revocation_basepoint), payment_point: common_fields.payment_basepoint, - delayed_payment_basepoint: DelayedPaymentBasepoint::from(common_fields.delayed_payment_basepoint), - htlc_basepoint: HtlcBasepoint::from(common_fields.htlc_basepoint) + delayed_payment_basepoint: DelayedPaymentBasepoint::from( + common_fields.delayed_payment_basepoint, + ), + htlc_basepoint: HtlcBasepoint::from(common_fields.htlc_basepoint), }; - funding.channel_transaction_parameters.counterparty_parameters = Some(CounterpartyChannelTransactionParameters { - selected_contest_delay: common_fields.to_self_delay, - pubkeys: counterparty_pubkeys, - }); + funding.channel_transaction_parameters.counterparty_parameters = + Some(CounterpartyChannelTransactionParameters { + selected_contest_delay: common_fields.to_self_delay, + pubkeys: counterparty_pubkeys, + }); self.counterparty_next_commitment_point = Some(common_fields.first_per_commitment_point); self.counterparty_shutdown_scriptpubkey = counterparty_shutdown_scriptpubkey; self.channel_state = ChannelState::NegotiatingFunding( - NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT + NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT, ); self.inbound_handshake_limits_override = None; // We're done enforcing limits on our peer's handshake now. @@ -13289,11 +13564,13 @@ impl OutboundV1Channel { } #[allow(dead_code)] // TODO(dual_funding): Remove once opending V2 channels is enabled. - #[rustfmt::skip] pub fn new( - fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, their_features: &InitFeatures, - channel_value_satoshis: u64, push_msat: u64, user_id: u128, config: &UserConfig, current_chain_height: u32, - outbound_scid_alias: u64, temporary_channel_id: Option, logger: L, trusted_channel_features: Option, + fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, + counterparty_node_id: PublicKey, their_features: &InitFeatures, + channel_value_satoshis: u64, push_msat: u64, user_id: u128, config: &UserConfig, + current_chain_height: u32, outbound_scid_alias: u64, + temporary_channel_id: Option, logger: L, + trusted_channel_features: Option, ) -> Result, APIError> { let is_0reserve = trusted_channel_features.is_some_and(|f| f.is_0reserve()); let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis( @@ -13304,16 +13581,19 @@ impl OutboundV1Channel { if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS && !is_0reserve { // Protocol level safety check in place, although it should never happen because // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` - return Err(APIError::APIMisuseError { err: format!("Holder selected channel reserve below \ - implementation limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) }); + return Err(APIError::APIMisuseError { + err: format!( + "Holder selected channel reserve below implementation limit dust_limit_satoshis {}", + holder_selected_channel_reserve_satoshis, + ), + }); } let channel_keys_id = signer_provider.generate_channel_keys_id(false, user_id); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); - let temporary_channel_id_fn = temporary_channel_id.map(|id| { - move |_: &ChannelPublicKeys| id - }); + let temporary_channel_id_fn = + temporary_channel_id.map(|id| move |_: &ChannelPublicKeys| id); let (funding, context) = ChannelContext::new_for_outbound_channel( fee_estimator, @@ -13335,7 +13615,10 @@ impl OutboundV1Channel { )?; let unfunded_context = UnfundedChannelContext { unfunded_channel_age_ticks: 0, - holder_commitment_point: HolderCommitmentPoint::new(&context.holder_signer, &context.secp_ctx), + holder_commitment_point: HolderCommitmentPoint::new( + &context.holder_signer, + &context.secp_ctx, + ), }; // We initialize `signer_pending_open_channel` to false, and leave setting the flag @@ -13669,7 +13952,6 @@ pub(super) fn channel_type_from_open_channel( impl InboundV1Channel { /// Creates a new channel from a remote sides' request for one. /// Assumes chain_hash has already been checked and corresponds with what we expect! - #[rustfmt::skip] pub fn new( fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, our_supported_features: &ChannelTypeFeatures, @@ -13677,11 +13959,17 @@ impl InboundV1Channel { current_chain_height: u32, logger: &L, trusted_channel_features: Option, ) -> Result, ChannelError> { - let logger = WithContext::from(logger, Some(counterparty_node_id), Some(msg.common_fields.temporary_channel_id), None); + let logger = WithContext::from( + logger, + Some(counterparty_node_id), + Some(msg.common_fields.temporary_channel_id), + None, + ); // First check the channel type is known, failing before we do anything else if we don't // support this channel type. - let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; + let channel_type = + channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis( msg.common_fields.funding_satoshis, @@ -13692,8 +13980,10 @@ impl InboundV1Channel { funding_pubkey: msg.common_fields.funding_pubkey, revocation_basepoint: RevocationBasepoint::from(msg.common_fields.revocation_basepoint), payment_point: msg.common_fields.payment_basepoint, - delayed_payment_basepoint: DelayedPaymentBasepoint::from(msg.common_fields.delayed_payment_basepoint), - htlc_basepoint: HtlcBasepoint::from(msg.common_fields.htlc_basepoint) + delayed_payment_basepoint: DelayedPaymentBasepoint::from( + msg.common_fields.delayed_payment_basepoint, + ), + htlc_basepoint: HtlcBasepoint::from(msg.common_fields.htlc_basepoint), }; let (funding, context) = ChannelContext::new_for_inbound_channel( @@ -13708,7 +13998,6 @@ impl InboundV1Channel { &&logger, trusted_channel_features, 0, - counterparty_pubkeys, channel_type, holder_selected_channel_reserve_satoshis, @@ -13718,9 +14007,13 @@ impl InboundV1Channel { )?; let unfunded_context = UnfundedChannelContext { unfunded_channel_age_ticks: 0, - holder_commitment_point: HolderCommitmentPoint::new(&context.holder_signer, &context.secp_ctx), + holder_commitment_point: HolderCommitmentPoint::new( + &context.holder_signer, + &context.secp_ctx, + ), }; - let chan = Self { funding, context, unfunded_context, signer_pending_accept_channel: false }; + let chan = + Self { funding, context, unfunded_context, signer_pending_accept_channel: false }; Ok(chan) } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 1bfea47b79c..f47062155a1 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3759,7 +3759,12 @@ impl< trusted_channel_features: Option, ) -> Result { if channel_value_satoshis < 1000 { - return Err(APIError::APIMisuseError { err: format!("Channel value must be at least 1000 satoshis. It was {}", channel_value_satoshis) }); + return Err(APIError::APIMisuseError { + err: format!( + "Channel value must be at least 1000 satoshis. It was {}", + channel_value_satoshis + ), + }); } let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -3768,17 +3773,26 @@ impl< let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(&their_network_key) - .ok_or_else(|| APIError::APIMisuseError{ err: format!("Not connected to node: {}", their_network_key) })?; + let peer_state_mutex = + per_peer_state.get(&their_network_key).ok_or_else(|| APIError::APIMisuseError { + err: format!("Not connected to node: {}", their_network_key), + })?; let mut peer_state = peer_state_mutex.lock().unwrap(); if !peer_state.is_connected { - return Err(APIError::APIMisuseError{ err: format!("Not connected to node: {}", their_network_key) }); + return Err(APIError::APIMisuseError { + err: format!("Not connected to node: {}", their_network_key), + }); } if let Some(temporary_channel_id) = temporary_channel_id { if peer_state.channel_by_id.contains_key(&temporary_channel_id) { - return Err(APIError::APIMisuseError{ err: format!("Channel with temporary channel ID {} already exists!", temporary_channel_id)}); + return Err(APIError::APIMisuseError { + err: format!( + "Channel with temporary channel ID {} already exists!", + temporary_channel_id + ), + }); } } @@ -3786,15 +3800,23 @@ impl< let outbound_scid_alias = self.create_and_insert_outbound_scid_alias(); let their_features = &peer_state.latest_features; let config = self.config.read().unwrap(); - let config = if let Some(config) = &override_config { - config - } else { - &*config - }; - match OutboundV1Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, their_network_key, - their_features, channel_value_satoshis, push_msat, user_channel_id, config, - self.best_block.read().unwrap().height, outbound_scid_alias, temporary_channel_id, &self.logger, trusted_channel_features) - { + let config = if let Some(config) = &override_config { config } else { &*config }; + match OutboundV1Channel::new( + &self.fee_estimator, + &self.entropy_source, + &self.signer_provider, + their_network_key, + their_features, + channel_value_satoshis, + push_msat, + user_channel_id, + config, + self.best_block.read().unwrap().height, + outbound_scid_alias, + temporary_channel_id, + &self.logger, + trusted_channel_features, + ) { Ok(res) => res, Err(e) => { self.outbound_scid_aliases.lock().unwrap().remove(&outbound_scid_alias); @@ -3814,14 +3836,15 @@ impl< panic!("RNG is bad???"); } }, - hash_map::Entry::Vacant(entry) => { entry.insert(Channel::from(channel)); } + hash_map::Entry::Vacant(entry) => { + entry.insert(Channel::from(channel)); + }, } if let Some(msg) = res { - peer_state.pending_msg_events.push(MessageSendEvent::SendOpenChannel { - node_id: their_network_key, - msg, - }); + peer_state + .pending_msg_events + .push(MessageSendEvent::SendOpenChannel { node_id: their_network_key, msg }); } Ok(temporary_channel_id) }