From a3dded178bbd00b58322153d3067dc98fbe9fe8d Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Mon, 23 Mar 2026 03:38:05 +0000 Subject: [PATCH 01/11] Set the correct floor for the reserves in inbound V2 channels The floor for *our* selected reserve is *their* dust limit. --- lightning/src/ln/channel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 8b05d984e30..c2b7e0662c4 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -14623,9 +14623,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); - let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); + let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( + channel_value_satoshis, msg.common_fields.dust_limit_satoshis); let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; From 98b71c8804b6a627b3b7e5836a7100103505be26 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Mon, 23 Mar 2026 03:39:31 +0000 Subject: [PATCH 02/11] 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 | 93 ++++++---- 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 | 5 +- lightning/src/sign/tx_builder.rs | 184 ++++++++++++++++++-- 7 files changed, 247 insertions(+), 53 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index c2b7e0662c4..c8c93eece74 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2781,11 +2781,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 { @@ -5155,27 +5162,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(()) @@ -5269,6 +5276,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 {}", @@ -6395,7 +6408,11 @@ fn get_holder_max_htlc_value_in_flight_msat( /// the counterparty. pub(crate) fn get_holder_selected_channel_reserve_satoshis( channel_value_satoshis: u64, their_dust_limit_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 = @@ -6423,7 +6440,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)) @@ -12363,12 +12385,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 @@ -13846,7 +13875,8 @@ impl OutboundV1Channel { let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis( channel_value_satoshis, their_dust_limit_satoshis, - config + 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 @@ -14231,7 +14261,8 @@ impl InboundV1Channel { let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis( msg.common_fields.funding_satoshis, msg.common_fields.dust_limit_satoshis, - config + config, + false, ); let counterparty_pubkeys = ChannelPublicKeys { funding_pubkey: msg.common_fields.funding_pubkey, @@ -14484,7 +14515,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) @@ -14623,9 +14654,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, MIN_CHAN_DUST_LIMIT_SATOSHIS); + channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, false); let holder_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 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 e13343ade76..1de51bff5f7 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, 0, &legacy_cfg); + get_holder_selected_channel_reserve_satoshis(channel_value_sat, 0, &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 a3252475965..12b6aab14f0 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -414,7 +414,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, 0, &default_config); + let reserve = get_holder_selected_channel_reserve_satoshis(100_000, 0, &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 3069783dffa..862d94740e1 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -51,7 +51,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, 0, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false) * 1000; let push = if send_from_initiator { 0 } else { push_amt }; let temp_channel_id = @@ -997,7 +998,8 @@ pub fn test_chan_reserve_violation_outbound_htlc_inbound_chan() { &channel_type_features, ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false) * 1000; let _ = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt); @@ -1041,7 +1043,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, 0, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, 0, &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 @@ -1119,7 +1122,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, 0, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, 0, &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 7d198d2d70d..be52459a872 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, 0, &config) * 1000; + get_holder_selected_channel_reserve_satoshis(CHAN_AMT, 0, &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 9c309b59519..fc80059bbd3 100644 --- a/lightning/src/ln/update_fee_tests.rs +++ b/lightning/src/ln/update_fee_tests.rs @@ -410,7 +410,7 @@ 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, 0, &cfg); + get_holder_selected_channel_reserve_satoshis(channel_value, 0, &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) @@ -886,7 +886,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, 0, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, 0, &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..ca61b27b78d 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,75 @@ 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); + // In case we are the funder, 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 = if is_outbound_from_holder { + cmp::max(dust_limit_satoshis + current_spiked_tx_fee_sat, spike_buffer_tx_fee_sat) + * 1000 + // In case we are the fundee, we can send dust HTLCs as long as our own balance output + // remains above the dust limit. + } else { + dust_limit_satoshis * 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 03d81f7e53c5b05af72c41cc140914b7ee64d74e Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 26 Feb 2026 03:10:47 +0000 Subject: [PATCH 03/11] 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 | 44 +++++----- 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, 134 insertions(+), 73 deletions(-) diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index b8a4a5adebb..fbff2eae4cd 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; @@ -1503,10 +1505,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 011b7f595bc..73c4a39c76f 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1657,7 +1657,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 8d47b6f8dc1..f238c1db060 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 0d8a4a020f0..9c81b903fed 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, Confirm, 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, @@ -3241,7 +3241,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(); @@ -3350,7 +3356,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 c8c93eece74..bd25de7c098 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::{ @@ -3693,7 +3693,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, @@ -3780,7 +3780,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))); @@ -3792,7 +3792,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))); } @@ -3841,7 +3841,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)) @@ -14250,7 +14250,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); @@ -14262,7 +14263,7 @@ impl InboundV1Channel { msg.common_fields.funding_satoshis, msg.common_fields.dust_limit_satoshis, config, - false, + trusted_channel_features.is_some_and(|f| f.is_0reserve()), ); let counterparty_pubkeys = ChannelPublicKeys { funding_pubkey: msg.common_fields.funding_pubkey, @@ -14282,7 +14283,7 @@ impl InboundV1Channel { config, current_chain_height, &&logger, - is_0conf, + trusted_channel_features, 0, counterparty_pubkeys, @@ -14678,7 +14679,7 @@ impl PendingV2Channel { config, current_chain_height, logger, - false, + None, our_funding_contribution_sats, counterparty_pubkeys, channel_type, @@ -16319,7 +16320,7 @@ mod tests { 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}; @@ -16523,7 +16524,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(); @@ -16668,7 +16669,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(); @@ -16743,12 +16744,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); @@ -16767,14 +16768,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); } @@ -16827,7 +16828,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); @@ -16835,7 +16836,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()); } } @@ -16862,7 +16863,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(); @@ -16965,7 +16966,7 @@ mod tests { &config, 0, &&logger, - false, + None, ) .unwrap(); outbound_chan @@ -18620,7 +18621,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 1de51bff5f7..9645d3c23b5 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(); @@ -968,7 +970,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, _)) => { @@ -1028,7 +1030,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 30eb7f85d71..d8302eed76a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3536,6 +3536,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 the 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, @@ -11057,10 +11099,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 @@ -11076,38 +11118,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, ) @@ -11116,7 +11152,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(); @@ -11165,7 +11201,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) @@ -11242,7 +11278,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() { @@ -11257,7 +11293,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 80274d180b4..7b7408121ce 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}; @@ -1646,10 +1646,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 1e50f2044c42d1ed971fc732ba9d54cc3a4e853a Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 19 Feb 2026 07:32:12 +0000 Subject: [PATCH 04/11] Add `ChannelManager::create_channel_to_trusted_peer_0reserve` This new method sets 0-reserve for the channel accepter. --- lightning/src/ln/channel.rs | 38 +++++++++++--------- 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, 77 insertions(+), 19 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index bd25de7c098..7f0e9e510f5 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -4510,7 +4510,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 { @@ -13866,23 +13866,24 @@ 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> { // At this point, we do not know what `dust_limit_satoshis` the counterparty will want for themselves, // so we set the channel reserve with no regard for their dust limit, and fail the channel if they want // a dust limit higher than our selected reserve. let their_dust_limit_satoshis = 0; + 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, their_dust_limit_satoshis, config, - false, + is_0reserve, ); - if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { + 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); @@ -16462,6 +16463,7 @@ mod tests { 42, None, &logger, + None, ); match res { Err(APIError::IncompatibleShutdownScript { script }) => { @@ -16488,7 +16490,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. @@ -16518,7 +16520,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. @@ -16609,7 +16611,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; @@ -16664,7 +16666,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(); @@ -16730,12 +16732,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); @@ -16755,14 +16757,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); @@ -16818,7 +16820,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); @@ -16857,7 +16859,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. @@ -16949,6 +16951,7 @@ mod tests { 42, None, &logger, + None, ) .unwrap(); let open_channel_msg = &outbound_chan @@ -17305,6 +17308,7 @@ mod tests { 42, None, &*logger, + None, ) .unwrap(); // Nothing uses their network key in this test chan.context.holder_dust_limit_satoshis = 546; @@ -18029,6 +18033,7 @@ mod tests { 0, None, &*logger, + None, ) .unwrap(); @@ -18604,7 +18609,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 9645d3c23b5..d28d157488d 100644 --- a/lightning/src/ln/channel_open_tests.rs +++ b/lightning/src/ln/channel_open_tests.rs @@ -939,6 +939,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 d8302eed76a..d896fbe947b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3789,8 +3789,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) }); } @@ -3826,7 +3870,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 7968512c3e13f25d1917364c382cb71fe60ffe9d Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 8 Feb 2026 01:24:17 +0000 Subject: [PATCH 05/11] Shakedown zero reserve channels --- lightning/src/ln/htlc_reserve_unit_tests.rs | 1066 ++++++++++++++++++- 1 file changed, 1060 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index 862d94740e1..3c91808fa07 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -2,30 +2,34 @@ 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::sign::ChannelSigner; 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, @@ -2423,3 +2427,1053 @@ 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 that the fundee can send back the full amount they just received, since they have 0-reserve. + 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; + let channel_value_sat = 1000; + + 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.get_enforcement_state().last_holder_commitment -= 1; + + ( + chan_signer.release_commitment_secret(INITIAL_COMMITMENT_NUMBER).unwrap(), + chan_signer.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.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 + .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, + }; + + // 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, + 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; + let channel_value_sat = 1000; + + 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; + } + if high_feerate { + 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 ac75e02ca9d1418aa8862b3045451df370549f7f Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 5 Mar 2026 09:00:32 +0000 Subject: [PATCH 06/11] Update `chanmon_consistency` to include 0FC and 0-reserve channels Co-Authored-By: HAL 9000 --- fuzz/src/chanmon_consistency.rs | 135 +++++++++++++++++++++++--------- 1 file changed, 99 insertions(+), 36 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index e4fd3475024..9e88083e31a 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}; @@ -862,30 +863,41 @@ fn assert_action_timeout_awaiting_response(action: &msgs::ErrorAction) { )); } +enum ChanType { + Legacy, + KeyedAnchors, + ZeroFeeCommitments, +} + #[inline] -pub fn do_test( - data: &[u8], underlying_out: Out, anchors: bool, -) { +pub fn do_test(data: &[u8], underlying_out: Out) { let out = SearchingOutput::new(underlying_out); let broadcast_a = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) }); let broadcast_b = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) }); let broadcast_c = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) }); let router = FuzzRouter {}; - // Read initial monitor styles from fuzz input (1 byte: 2 bits per node) - let initial_mon_styles = if !data.is_empty() { data[0] } else { 0 }; + // Read initial monitor styles and channel type from fuzz input byte 0: + // bits 0-2: monitor styles (1 bit per node) + // bits 3-4: channel type (0=Legacy, 1=KeyedAnchors, 2=ZeroFeeCommitments) + let config_byte = if !data.is_empty() { data[0] } else { 0 }; + let chan_type = match (config_byte >> 3) & 0b11 { + 0 => ChanType::Legacy, + 1 => ChanType::KeyedAnchors, + _ => ChanType::ZeroFeeCommitments, + }; let mon_style = [ - RefCell::new(if initial_mon_styles & 0b01 != 0 { + RefCell::new(if config_byte & 0b01 != 0 { ChannelMonitorUpdateStatus::InProgress } else { ChannelMonitorUpdateStatus::Completed }), - RefCell::new(if initial_mon_styles & 0b10 != 0 { + RefCell::new(if config_byte & 0b10 != 0 { ChannelMonitorUpdateStatus::InProgress } else { ChannelMonitorUpdateStatus::Completed }), - RefCell::new(if initial_mon_styles & 0b100 != 0 { + RefCell::new(if config_byte & 0b100 != 0 { ChannelMonitorUpdateStatus::InProgress } else { ChannelMonitorUpdateStatus::Completed @@ -925,8 +937,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; @@ -977,8 +1000,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(); @@ -1077,8 +1111,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); @@ -1103,14 +1152,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"); } @@ -1286,12 +1347,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`. @@ -1375,7 +1440,7 @@ pub fn do_test( }}; } - let mut read_pos = 1; // First byte was consumed for initial mon_style + let mut read_pos = 1; // First byte was consumed for initial config (mon_style + chan_type) macro_rules! get_slice { ($len: expr) => {{ let slice_len = $len as usize; @@ -2332,7 +2397,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 { @@ -2347,7 +2412,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 { @@ -2362,7 +2427,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 { @@ -2832,12 +2897,10 @@ 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); } #[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 {}); } From b61e100a94a5d96dfdbef86f1f3e3c34b659fb4f Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Tue, 24 Mar 2026 19:19:59 +0000 Subject: [PATCH 07/11] Don't fail channel if inbound UA breaches counterparty-selected reserve We do not care if our balance drops below the counterparty-selected reserve upon an inbound `update_add_htlc`. This is the counterparty's problem. Hence, we drop the assumption that once our balance rises above the counterparty-selected reserve, it will always remain above this reserve for the lifetime of a funding scope. In the following commit, we make the assumption that the counterparty does not complain if we push them below our selected reserve when adding a HTLC, so we accommodate this assumption here. --- lightning/src/ln/channel.rs | 20 +++--- lightning/src/ln/htlc_reserve_unit_tests.rs | 78 --------------------- 2 files changed, 9 insertions(+), 89 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 7f0e9e510f5..ce6be6ee3d9 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5162,7 +5162,10 @@ impl ChannelContext { )); } - let (local_stats, _local_htlcs) = self + // Here we check two things 1) that our local commitment still has at least 1 output + // (particularly relevant in 0-reserve channels), and 2) that the counterparty can + // still afford the fee on our commitment if they are the funder. + let (_local_stats, _local_htlcs) = self .get_next_local_commitment_stats( funding, Some(HTLCAmountDirection { outbound: false, amount_msat: msg.amount_msat }), @@ -5175,16 +5178,6 @@ impl ChannelContext { 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() - )); - } - Ok(()) } @@ -5717,6 +5710,11 @@ impl ChannelContext { funding.counterparty_prev_commitment_tx_balance.lock().unwrap() }; + // This assumes that once our balance rises above the counterparty selected + // reserve, it never drops below again. But we allow our counterparty to + // push us under our reserve when we are the funder and they add a HTLC, as + // this is really their problem. Hence, we only run this assert in tests. + #[cfg(test)] if _stats.local_balance_before_fee_msat / 1000 < funding.counterparty_selected_channel_reserve_satoshis.unwrap() { // If the local balance is below the reserve on this new commitment, it MUST be // greater than or equal to the one on the previous commitment. diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index 3c91808fa07..608ac143c8d 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -1023,84 +1023,6 @@ pub fn test_chan_reserve_violation_outbound_htlc_inbound_chan() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); } -#[xtest(feature = "_externalize_tests")] -pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { - let mut chanmon_cfgs = create_chanmon_cfgs(2); - let feerate_per_kw = *chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap(); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let legacy_cfg = test_legacy_channel_config(); - let node_chanmgrs = - create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg.clone()), Some(legacy_cfg)]); - let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); - - let node_b_id = nodes[1].node.get_our_node_id(); - - let default_config = UserConfig::default(); - let channel_type_features = ChannelTypeFeatures::only_static_remote_key(); - - // Set nodes[0]'s balance such that they will consider any above-dust received HTLC to be a - // channel reserve violation (so their balance is channel reserve (1000 sats) + commitment - // transaction fee with 0 HTLCs (183 sats)). - let mut push_amt = 100_000_000; - push_amt -= commit_tx_fee_msat( - feerate_per_kw, - MIN_AFFORDABLE_HTLC_COUNT as u64, - &channel_type_features, - ); - push_amt -= - get_holder_selected_channel_reserve_satoshis(100_000, 0, &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 - for _ in 0..MIN_AFFORDABLE_HTLC_COUNT { - route_payment(&nodes[1], &[&nodes[0]], 1_000_000); - } - - let (mut route, payment_hash, _, payment_secret) = - get_route_and_payment_hash!(nodes[1], nodes[0], 1000); - route.paths[0].hops[0].fee_msat = 700_000; - // Need to manually create the update_add_htlc message to go around the channel reserve check in send_htlc() - let secp_ctx = Secp256k1::new(); - let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); - let cur_height = nodes[1].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, 700_000); - 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(); - let onion_packet = - onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash) - .unwrap(); - let msg = msgs::UpdateAddHTLC { - channel_id: chan.2, - htlc_id: MIN_AFFORDABLE_HTLC_COUNT as u64, - amount_msat: htlc_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[0].node.handle_update_add_htlc(node_b_id, &msg); - // Check that the payment failed and the channel is closed in response to the malicious UpdateAdd. - nodes[0].logger.assert_log_contains("lightning::ln::channelmanager", "Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value", 3); - assert_eq!(nodes[0].node.list_channels().len(), 0); - let err_msg = check_closed_broadcast(&nodes[0], 1, true).pop().unwrap(); - assert_eq!(err_msg.data, "Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value"); - let reason = ClosureReason::ProcessingError { err: "Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value".to_string() }; - check_added_monitors(&nodes[0], 1); - check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); -} - #[xtest(feature = "_externalize_tests")] pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() { // Test that if we receive many dust HTLCs over an outbound channel, they don't count when From 1cc5df5cba27f5525254c4d7972075b065514661 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 18 Mar 2026 23:10:10 +0000 Subject: [PATCH 08/11] Add 0-reserve to the internal API of V2 channels Note that this currently does not match the spec as we use an odd TLV for the `disable_channel_reserve` field in `open_channel2` and `accept_channel2` msgs. If the counterparty does not understand this field, that's ok as it just means that the counterparty will not send some HTLCs we would have accepted. We make the assumption that the counterparty will not complain if we send a HTLC that pushes their balance below our selected reserve; this could happen if the counterparty is the funder of the channel. They should not complain because if we push them below our selected reserve, this is our problem. --- 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 ce6be6ee3d9..a2a18dc6c9f 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -14505,7 +14505,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); @@ -14515,7 +14515,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) @@ -14633,6 +14633,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(()), } } @@ -14645,7 +14646,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); @@ -14654,9 +14655,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, MIN_CHAN_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, msg.common_fields.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)?; @@ -14678,7 +14679,7 @@ impl PendingV2Channel { config, current_chain_height, logger, - None, + trusted_channel_features, our_funding_contribution_sats, counterparty_pubkeys, channel_type, @@ -14797,6 +14798,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 d896fbe947b..1b3206a9242 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11274,6 +11274,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 29089032843..e2a8d247fd0 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -303,6 +303,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. @@ -379,6 +381,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. @@ -2960,6 +2964,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), + (3, self.disable_channel_reserve, option), }); Ok(()) } @@ -2986,10 +2991,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), + (3, disable_channel_reserve, option), }); Ok(AcceptChannelV2 { @@ -3013,6 +3020,7 @@ impl LengthReadable for AcceptChannelV2 { funding_satoshis, second_per_commitment_point, require_confirmed_inputs, + disable_channel_reserve, }) } } @@ -3390,6 +3398,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), + (3, self.disable_channel_reserve, option), }); Ok(()) } @@ -3420,10 +3429,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), + (3, disable_channel_reserve, option), }); Ok(OpenChannelV2 { common_fields: CommonOpenChannelFields { @@ -3450,6 +3461,7 @@ impl LengthReadable for OpenChannelV2 { locktime, second_per_commitment_point, require_confirmed_inputs, + disable_channel_reserve, }) } } @@ -5187,6 +5199,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!( @@ -5255,7 +5268,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(); @@ -5340,27 +5354,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("0300").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) { @@ -5436,7 +5469,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", @@ -5492,11 +5528,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 = @@ -5557,13 +5598,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("0300").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 b19a2457f4efcfd4a662e2accb4c528ab5e88567 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 26 Feb 2026 03:01:46 +0000 Subject: [PATCH 09/11] 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 | 737 ++++++++++++++++++++--------- lightning/src/ln/channelmanager.rs | 61 ++- 2 files changed, 559 insertions(+), 239 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index a2a18dc6c9f..1b9f0a0ab29 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -3682,160 +3682,266 @@ 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 + ))); } - let max_chan_dust_limit_satoshis = if channel_type.supports_anchors_zero_fee_htlc_tx() || channel_type.supports_anchor_zero_fee_commitments() { + let max_chan_dust_limit_satoshis = if channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_anchor_zero_fee_commitments() + { MAX_CHAN_DUST_LIMIT_SATOSHIS } else { MAX_LEGACY_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))); + 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(); @@ -3857,9 +3963,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()), @@ -3891,7 +4003,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, @@ -3901,7 +4015,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, @@ -3953,19 +4067,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, @@ -4002,100 +4132,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 { @@ -4106,9 +4275,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()), @@ -4138,7 +4313,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, @@ -4201,11 +4378,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, @@ -4248,15 +4436,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)) } @@ -4482,109 +4678,189 @@ 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 + ))); } - let max_chan_dust_limit_satoshis = if channel_type.supports_anchors_zero_fee_htlc_tx() || channel_type.supports_anchor_zero_fee_commitments() { + let max_chan_dust_limit_satoshis = if channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_anchor_zero_fee_commitments() + { MAX_CHAN_DUST_LIMIT_SATOSHIS } else { MAX_LEGACY_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; @@ -4599,20 +4875,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. @@ -13860,11 +14139,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> { // At this point, we do not know what `dust_limit_satoshis` the counterparty will want for themselves, // so we set the channel reserve with no regard for their dust limit, and fail the channel if they want @@ -13880,16 +14161,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, @@ -13911,7 +14195,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 @@ -14244,7 +14531,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, @@ -14252,11 +14538,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, @@ -14268,8 +14560,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( @@ -14284,7 +14578,6 @@ impl InboundV1Channel { &&logger, trusted_channel_features, 0, - counterparty_pubkeys, channel_type, holder_selected_channel_reserve_satoshis, @@ -14294,9 +14587,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 1b3206a9242..a4225c51951 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3836,7 +3836,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); @@ -3845,17 +3850,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 + ), + }); } } @@ -3863,15 +3877,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); @@ -3891,14 +3913,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) } From 778f668fe5d2584734339939a1b771c98e71087e Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Mon, 23 Mar 2026 23:12:04 +0000 Subject: [PATCH 10/11] Create better helper methods in `tx_builder` Reduce line count and indentation --- lightning/src/sign/tx_builder.rs | 118 ++++++++++++------------------- 1 file changed, 44 insertions(+), 74 deletions(-) diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index ca61b27b78d..527b00578d3 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -95,49 +95,33 @@ fn commit_plus_htlc_tx_fees_msat( (total_fees_msat, extra_accepted_htlc_total_fees_msat) } -fn checked_sub_anchor_outputs( - is_outbound_from_holder: bool, value_to_self_after_htlcs_msat: u64, - value_to_remote_after_htlcs_msat: u64, channel_type: &ChannelTypeFeatures, -) -> Result<(u64, u64), ()> { - let total_anchors_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() { +fn total_anchors_sat(channel_type: &ChannelTypeFeatures) -> u64 { + if channel_type.supports_anchors_zero_fee_htlc_tx() { ANCHOR_OUTPUT_VALUE_SATOSHI * 2 } else { 0 - }; + } +} +fn checked_sub_from_funder( + is_outbound_from_holder: bool, value_to_holder: u64, value_to_counterparty: u64, + subtrahend: u64, +) -> Result<(u64, u64), ()> { if is_outbound_from_holder { - Ok(( - value_to_self_after_htlcs_msat.checked_sub(total_anchors_sat * 1000).ok_or(())?, - value_to_remote_after_htlcs_msat, - )) + Ok((value_to_holder.checked_sub(subtrahend).ok_or(())?, value_to_counterparty)) } else { - Ok(( - value_to_self_after_htlcs_msat, - value_to_remote_after_htlcs_msat.checked_sub(total_anchors_sat * 1000).ok_or(())?, - )) + Ok((value_to_holder, value_to_counterparty.checked_sub(subtrahend).ok_or(())?)) } } -fn saturating_sub_anchor_outputs( - is_outbound_from_holder: bool, value_to_self_after_htlcs: u64, - value_to_remote_after_htlcs: u64, channel_type: &ChannelTypeFeatures, +fn saturating_sub_from_funder( + is_outbound_from_holder: bool, value_to_holder: u64, value_to_counterparty: u64, + subtrahend: u64, ) -> (u64, u64) { - let total_anchors_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() { - ANCHOR_OUTPUT_VALUE_SATOSHI * 2 - } else { - 0 - }; - if is_outbound_from_holder { - ( - value_to_self_after_htlcs.saturating_sub(total_anchors_sat * 1000), - value_to_remote_after_htlcs, - ) + (value_to_holder.saturating_sub(subtrahend), value_to_counterparty) } else { - ( - value_to_self_after_htlcs, - value_to_remote_after_htlcs.saturating_sub(total_anchors_sat * 1000), - ) + (value_to_holder, value_to_counterparty.saturating_sub(subtrahend)) } } @@ -212,23 +196,17 @@ fn has_output( 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), - ) - }; + let (holder_balance_msat, counterparty_balance_msat) = saturating_sub_from_funder( + is_outbound_from_holder, + holder_balance_before_fee_msat, + counterparty_balance_before_fee_msat, + commit_tx_fee_sat.saturating_mul(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 + let has_no_output = holder_balance_msat < dust_limit_msat + && 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(); @@ -271,12 +249,13 @@ fn get_next_commitment_stats( // commitment transaction *before* checking whether the remote party's balance is enough to // cover the total anchor sum. + let total_anchors_sat = total_anchors_sat(channel_type); let (holder_balance_before_fee_msat, counterparty_balance_before_fee_msat) = - checked_sub_anchor_outputs( + checked_sub_from_funder( is_outbound_from_holder, value_to_holder_after_htlcs_msat, value_to_counterparty_after_htlcs_msat, - channel_type, + total_anchors_sat.saturating_mul(1000), )?; let (dust_exposure_msat, _extra_accepted_htlc_dust_exposure_msat) = get_dust_exposure_stats( @@ -318,18 +297,12 @@ fn get_next_commitment_stats( nondust_htlc_count + addl_nondust_htlc_count, channel_type, ); - - let (holder_balance_msat, counterparty_balance_msat) = if is_outbound_from_holder { - ( - holder_balance_before_fee_msat.checked_sub(commit_tx_fee_sat * 1000).ok_or(())?, - counterparty_balance_before_fee_msat, - ) - } else { - ( - holder_balance_before_fee_msat, - counterparty_balance_before_fee_msat.checked_sub(commit_tx_fee_sat * 1000).ok_or(())?, - ) - }; + let (holder_balance_msat, counterparty_balance_msat) = checked_sub_from_funder( + is_outbound_from_holder, + holder_balance_before_fee_msat, + counterparty_balance_before_fee_msat, + commit_tx_fee_sat.saturating_mul(1000), + )?; Ok(NextCommitmentStats { holder_balance_msat, @@ -425,15 +398,16 @@ fn get_available_balances( pending_htlcs.iter().filter_map(|htlc| htlc.outbound.then_some(htlc.amount_msat)).sum(); let inbound_htlcs_value_msat: u64 = pending_htlcs.iter().filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat)).sum(); + let total_anchors_sat = total_anchors_sat(channel_type); let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = - saturating_sub_anchor_outputs( + saturating_sub_from_funder( is_outbound_from_holder, value_to_holder_msat.saturating_sub(outbound_htlcs_value_msat), (channel_value_satoshis * 1000) .checked_sub(value_to_holder_msat) .unwrap() .saturating_sub(inbound_htlcs_value_msat), - &channel_type, + total_anchors_sat.saturating_mul(1000), ); let outbound_capacity_msat = local_balance_before_fee_msat @@ -821,12 +795,13 @@ impl TxBuilder for SpecTxBuilder { // commitment transaction *before* checking whether the remote party's balance is enough to // cover the total anchor sum. + let total_anchors_sat = total_anchors_sat(&channel_parameters.channel_type_features); let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = - saturating_sub_anchor_outputs( + saturating_sub_from_funder( channel_parameters.is_outbound_from_holder, value_to_self_after_htlcs_msat, value_to_remote_after_htlcs_msat, - &channel_parameters.channel_type_features, + total_anchors_sat.saturating_mul(1000), ); // We MUST use saturating subs here, as the funder's balance is not guaranteed to be greater @@ -836,17 +811,12 @@ impl TxBuilder for SpecTxBuilder { // commitment transaction *before* checking whether the remote party's balance is enough to // cover the total fee. - let (value_to_self, value_to_remote) = if channel_parameters.is_outbound_from_holder { - ( - (local_balance_before_fee_msat / 1000).saturating_sub(commit_tx_fee_sat), - remote_balance_before_fee_msat / 1000, - ) - } else { - ( - local_balance_before_fee_msat / 1000, - (remote_balance_before_fee_msat / 1000).saturating_sub(commit_tx_fee_sat), - ) - }; + let (value_to_self, value_to_remote) = saturating_sub_from_funder( + channel_parameters.is_outbound_from_holder, + local_balance_before_fee_msat / 1000, + remote_balance_before_fee_msat / 1000, + commit_tx_fee_sat, + ); let mut to_broadcaster_value_sat = if local { value_to_self } else { value_to_remote }; let mut to_countersignatory_value_sat = if local { value_to_remote } else { value_to_self }; From 91565f1cdd550f5ea92eb96352d3f2ecae382bab Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Mon, 23 Mar 2026 23:33:33 +0000 Subject: [PATCH 11/11] Use inline format variables in channel/channelmanager format strings Convert format string arguments to inline `{var}` captures where the argument is a simple identifier (variable or constant). Field accesses, method calls, and expressions remain as positional args. Co-Authored-By: Claude Opus 4.6 (1M context) --- lightning/src/ln/channel.rs | 106 ++++++++++++----------------- lightning/src/ln/channelmanager.rs | 15 ++-- 2 files changed, 51 insertions(+), 70 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 1b9f0a0ab29..8f8ab4a9b2e 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -3708,41 +3708,38 @@ impl ChannelContext { 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 + "Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks. It must be greater than {BREAKDOWN_TIMEOUT}", + config.channel_handshake_config.our_to_self_delay ))); } 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 + "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 + "Bogus channel_reserve_satoshis ({msg_channel_reserve_satoshis}). Must be no greater than channel_value_satoshis: {channel_value_satoshis}" ))); } 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 + "push_msat {msg_push_msat} was larger than channel amount minus reserve ({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 + "dust_limit_satoshis {} was larger than channel_value_satoshis {channel_value_satoshis}. Peer never wants payout outputs?", + open_channel_fields.dust_limit_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 + "Minimum htlc value ({}) was larger than full channel value ({full_channel_value_msat})", + open_channel_fields.htlc_minimum_msat ))); } FundedChannel::::check_remote_fee( @@ -3759,8 +3756,8 @@ impl ChannelContext { ); 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 + "They wanted our payments to be delayed by a needlessly long period. Upper limit: {max_counterparty_selected_contest_delay}. Actual: {}", + open_channel_fields.to_self_delay ))); } if open_channel_fields.max_accepted_htlcs < 1 { @@ -3779,8 +3776,8 @@ impl ChannelContext { // 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 + "Funding satoshis ({channel_value_satoshis}) is less than the user specified limit ({})", + config.channel_handshake_limits.min_funding_satoshis ))); } if open_channel_fields.htlc_minimum_msat @@ -3805,8 +3802,7 @@ impl ChannelContext { > 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, + "channel_reserve_satoshis ({msg_channel_reserve_satoshis}) is higher than the user specified limit ({})", config.channel_handshake_limits.max_channel_reserve_satoshis ))); } @@ -3821,8 +3817,8 @@ impl ChannelContext { } 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 + "dust_limit_satoshis ({}) is less than the implementation limit ({MIN_CHAN_DUST_LIMIT_SATOSHIS})", + open_channel_fields.dust_limit_satoshis ))); } @@ -3835,8 +3831,8 @@ impl ChannelContext { }; 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 + "dust_limit_satoshis ({}) is greater than the implementation limit ({max_chan_dust_limit_satoshis})", + open_channel_fields.dust_limit_satoshis ))); } @@ -3856,29 +3852,27 @@ impl ChannelContext { // 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 + "Suitable channel reserve not found. remote_channel_reserve was ({holder_selected_channel_reserve_satoshis}). dust_limit_satoshis is ({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 + "Suitable channel reserve not found. remote_channel_reserve was ({})msats. Channel value is ({full_channel_value_msat} - {msg_push_msat})msats.", + holder_selected_channel_reserve_satoshis * 1000 ))); } 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.", - msg_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); + "channel_reserve_satoshis ({msg_channel_reserve_satoshis}) is smaller than our dust limit ({MIN_CHAN_DUST_LIMIT_SATOSHIS}). We can broadcast \ + stale states without any risk, implying this channel is very insecure for our counterparty."); } 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 + "Dust limit ({}) too high for the channel reserve we require the remote to keep ({holder_selected_channel_reserve_satoshis})", + open_channel_fields.dust_limit_satoshis ))); } @@ -3896,8 +3890,7 @@ impl ChannelContext { } 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 + "Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {script}" ))); } Some(script.clone()) @@ -3931,8 +3924,7 @@ impl ChannelContext { 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 + "Provided a scriptpubkey format not accepted by peer: {shutdown_scriptpubkey}" ))); } } @@ -4187,16 +4179,14 @@ impl ChannelContext { { return Err(APIError::APIMisuseError { err: format!( - "funding_value must not exceed {}, it was {}", - MAX_FUNDING_SATOSHIS_NO_WUMBO, channel_value_satoshis + "funding_value must not exceed {MAX_FUNDING_SATOSHIS_NO_WUMBO}, it was {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 + "funding_value must be smaller than the total bitcoin supply, it was {channel_value_satoshis}" ), }); } @@ -4204,16 +4194,14 @@ impl ChannelContext { if push_msat > channel_value_msat { return Err(APIError::APIMisuseError { err: format!( - "Push value ({}) was larger than channel_value ({})", - push_msat, channel_value_msat + "Push value ({push_msat}) was larger than channel_value ({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 + "Configured with an unreasonable our_to_self_delay ({holder_selected_contest_delay}) putting user funds at risks" ), }); } @@ -4720,8 +4708,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, + "Bogus channel_reserve_satoshis ({channel_reserve_satoshis}). Must not be greater than ({})", funding.get_value_satoshis() ))); } @@ -4737,8 +4724,7 @@ impl ChannelContext { > 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, + "Bogus channel_reserve_satoshis ({channel_reserve_satoshis}). Must not be greater than channel value minus our reserve ({})", funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis ))); } @@ -4746,16 +4732,16 @@ impl ChannelContext { (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 + "Minimum htlc value ({}) is full channel value ({full_channel_value_msat})", + common_fields.htlc_minimum_msat ))); } 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 + "They wanted our payments to be delayed by a needlessly long period. Upper limit: {max_delay_acceptable}. Actual: {}", + common_fields.to_self_delay ))); } if common_fields.max_accepted_htlcs < 1 { @@ -4791,8 +4777,8 @@ impl ChannelContext { } 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 + "channel_reserve_satoshis ({channel_reserve_satoshis}) is higher than the user specified limit ({})", + peer_limits.max_channel_reserve_satoshis ))); } if common_fields.max_accepted_htlcs < peer_limits.min_max_accepted_htlcs { @@ -4803,8 +4789,8 @@ impl ChannelContext { } 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 + "dust_limit_satoshis ({}) is less than the implementation limit ({MIN_CHAN_DUST_LIMIT_SATOSHIS})", + common_fields.dust_limit_satoshis ))); } @@ -4817,8 +4803,8 @@ impl ChannelContext { }; 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 + "dust_limit_satoshis ({}) is greater than the implementation limit ({max_chan_dust_limit_satoshis})", + common_fields.dust_limit_satoshis ))); } if common_fields.minimum_depth > peer_limits.max_minimum_depth { @@ -4838,8 +4824,7 @@ impl ChannelContext { } 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 + "Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {script}" ))); } Some(script.clone()) @@ -14163,8 +14148,7 @@ impl OutboundV1Channel { // 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, + "Holder selected channel reserve below implementation limit dust_limit_satoshis {holder_selected_channel_reserve_satoshis}" ), }); } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a4225c51951..184bc405200 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3838,8 +3838,7 @@ impl< if channel_value_satoshis < 1000 { return Err(APIError::APIMisuseError { err: format!( - "Channel value must be at least 1000 satoshis. It was {}", - channel_value_satoshis + "Channel value must be at least 1000 satoshis. It was {channel_value_satoshis}" ), }); } @@ -3850,15 +3849,14 @@ 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), + err: format!("Not connected to node: {their_network_key}"), }); } @@ -3866,8 +3864,7 @@ impl< 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 + "Channel with temporary channel ID {temporary_channel_id} already exists!" ), }); }