Adjust FundingContribution for acceptor
What changed, and why it matters
This commit fixes a fee-calculation bug in Lightning splicing. Previously, when both sides tried to splice at the same time, the loser of a tie-break (the 'acceptor') built their contribution as if they were going to pay for the whole shared transaction. That made them overpay fees, and a high fee rate chosen by the other side could drain more from their wallet than intended or even make the transaction invalid. The patch adds logic to re-estimate fees from the acceptor's perspective and lets users set a maximum fee rate they are willing to accept.
Review the new fee-adjustment logic for off-by-one and dust-limit edge cases; ensure the max_feerate default in existing callers (FeeRate::MAX) is intentional and documented; run the new unit tests and any splicing integration/fuzz tests.
Security signals we found
Fee overpayment / economic griefing: acceptor previously paid fees estimated for the initiator role
Insufficient-fee risk: acceptor's selected UTXOs might not cover a higher counterparty feerate
New defensive validation: min_feerate > max_feerate is rejected at API entry
New fee-buffer checks with checked arithmetic to prevent overflow and underpayment
Splice-out balance check prevents spending more than the channel balance
Evidence from the diff
The change introduces a FeeRateAdjustmentError enum and two new methods on FundingContribution: for_acceptor_at_feerate and net_value_for_acceptor_at_feerate. These recompute the acceptor’s share of the fee at the initiator’s chosen feerate, accounting for the fact that the acceptor does not pay for common transaction fields or the shared input/output. It also adds a max_feerate parameter to splice_channel/FundingTemplate so an acceptor can reject (or conditionally accept) a counterparty-proposed feerate that is too high. The patch updates callers and adds extensive unit tests covering change adjustment, change removal, max-feerate rejection, splice-out balance checks, and arithmetic overflow.
Changed components
lightning/src/ln/funding.rslightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/splicing_tests.rsfuzz/src/chanmon_consistency.rsfuzz/src/full_stack.rsInspect captured patch +1120 / −60
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 4763623..2200689 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -1393,7 +1393,12 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
channel_id: &ChannelId,
f: &dyn Fn(FundingTemplate) -> Result<FundingContribution, ()>,
funding_feerate_sat_per_kw: FeeRate| {
- match node.splice_channel(channel_id, counterparty_node_id, funding_feerate_sat_per_kw) {
+ match node.splice_channel(
+ channel_id,
+ counterparty_node_id,
+ funding_feerate_sat_per_kw,
+ FeeRate::MAX,
+ ) {
Ok(funding_template) => {
if let Ok(contribution) = f(funding_template) {
let _ = node.funding_contributed(
diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs
index 03d5e48..5dfa510 100644
--- a/fuzz/src/full_stack.rs
+++ b/fuzz/src/full_stack.rs
@@ -1036,6 +1036,7 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger + MaybeSend + MaybeSync>
&chan_id,
&counterparty,
FeeRate::from_sat_per_kwu(253),
+ FeeRate::MAX,
) {
let wallet_sync = WalletSync::new(&wallet, Arc::clone(&logger));
if let Ok(contribution) = funding_template
@@ -1076,6 +1077,7 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger + MaybeSend + MaybeSync>
&chan_id,
&counterparty,
FeeRate::from_sat_per_kwu(253),
+ FeeRate::MAX,
) {
let outputs = vec![TxOut {
value: Amount::from_sat(splice_out_sats),
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index a87fff2..2c1117a 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -11708,7 +11708,9 @@ where
}
/// Initiate splicing.
- pub fn splice_channel(&self, feerate: FeeRate) -> Result<FundingTemplate, APIError> {
+ pub fn splice_channel(
+ &self, min_feerate: FeeRate, max_feerate: FeeRate,
+ ) -> Result<FundingTemplate, APIError> {
if self.holder_commitment_point.current_point().is_none() {
return Err(APIError::APIMisuseError {
err: format!(
@@ -11750,6 +11752,17 @@ where
});
}
+ if min_feerate > max_feerate {
+ return Err(APIError::APIMisuseError {
+ err: format!(
+ "Channel {} min_feerate {} exceeds max_feerate {}",
+ self.context.channel_id(),
+ min_feerate,
+ max_feerate,
+ ),
+ });
+ }
+
let funding_txo = self.funding.get_funding_txo().expect("funding_txo should be set");
let previous_utxo =
self.funding.get_funding_output().expect("funding_output should be set");
@@ -11759,7 +11772,7 @@ where
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
};
- Ok(FundingTemplate::new(Some(shared_input), feerate))
+ Ok(FundingTemplate::new(Some(shared_input), min_feerate, max_feerate))
}
pub fn funding_contributed<L: Logger>(
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 8e06129..19767de 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -4642,9 +4642,12 @@ impl<
/// # Arguments
///
/// The splice initiator is responsible for paying fees for common fields, shared inputs, and
- /// shared outputs along with any contributed inputs and outputs. Fees are determined using
- /// `feerate` and must be covered by the supplied inputs for splice-in or the channel balance
- /// for splice-out.
+ /// shared outputs along with any contributed inputs and outputs. When building a
+ /// [`FundingContribution`], fees are estimated using `min_feerate` and must be covered by the
+ /// supplied inputs for splice-in or the channel balance for splice-out. If the counterparty
+ /// also initiates a splice and wins the tie-break, they become the initiator and choose the
+ /// feerate. In that case, `max_feerate` is used to reject a feerate that is too high for our
+ /// contribution.
///
/// Returns a [`FundingTemplate`] which should be used to build a [`FundingContribution`] via
/// one of its splice methods (e.g., [`FundingTemplate::splice_in_sync`]). The resulting
@@ -4670,7 +4673,8 @@ impl<
/// [`FundingContribution`]: crate::ln::funding::FundingContribution
#[rustfmt::skip]
pub fn splice_channel(
- &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, feerate: FeeRate,
+ &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
+ min_feerate: FeeRate, max_feerate: FeeRate,
) -> Result<FundingTemplate, APIError> {
let per_peer_state = self.per_peer_state.read().unwrap();
@@ -4698,7 +4702,7 @@ impl<
match peer_state.channel_by_id.entry(*channel_id) {
hash_map::Entry::Occupied(chan_phase_entry) => {
if let Some(chan) = chan_phase_entry.get().as_funded() {
- chan.splice_channel(feerate)
+ chan.splice_channel(min_feerate, max_feerate)
} else {
Err(APIError::ChannelUnavailable {
err: format!(
diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs
index 18c05a5..84c9d4d 100644
--- a/lightning/src/ln/funding.rs
+++ b/lightning/src/ln/funding.rs
@@ -27,6 +27,75 @@ use crate::util::wallet_utils::{
CoinSelection, CoinSelectionSource, CoinSelectionSourceSync, Input,
};
+/// Error returned when the acceptor's contribution cannot accommodate the initiator's proposed
+/// feerate.
+///
+/// When building a [`FundingContribution`], fees are estimated at `min_feerate` assuming initiator
+/// responsibility. If the counterparty also initiates a splice and wins the tie-break, they become
+/// the initiator and choose the feerate. The fee is then re-estimated at the counterparty's
+/// feerate for only our contributed inputs and outputs. When this re-estimation fails, the
+/// contribution is dropped and the counterparty's splice proceeds without it.
+///
+/// See [`ChannelManager::splice_channel`] for further details.
+///
+/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
+#[derive(Debug)]
+pub(super) enum FeeRateAdjustmentError {
+ /// The counterparty's proposed feerate is below `min_feerate`, which was used as the feerate
+ /// during coin selection.
+ FeeRateTooLow { target_feerate: FeeRate, min_feerate: FeeRate },
+ /// The counterparty's proposed feerate is above `max_feerate` and the re-estimated fee for
+ /// our contributed inputs and outputs exceeds the original fee estimate (computed at
+ /// `min_feerate` assuming initiator responsibility). If the re-estimated fee were within the
+ /// original estimate, a feerate above `max_feerate` would be tolerable since the acceptor
+ /// doesn't pay for common fields or the shared input/output.
+ FeeRateTooHigh {
+ target_feerate: FeeRate,
+ max_feerate: FeeRate,
+ target_fee: Amount,
+ original_fee: Amount,
+ },
+ /// Arithmetic overflow when computing the fee buffer.
+ FeeBufferOverflow,
+ /// The re-estimated fee exceeds the available fee buffer regardless of `max_feerate`. The fee
+ /// buffer is the maximum fee that can be accommodated:
+ /// - **splice-in**: the selected inputs' value minus the contributed amount
+ /// - **splice-out**: the channel balance minus the withdrawal outputs
+ FeeBufferInsufficient { source: &'static str, available: Amount, required: Amount },
+}
+
+impl core::fmt::Display for FeeRateAdjustmentError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ FeeRateAdjustmentError::FeeRateTooLow { target_feerate, min_feerate } => {
+ write!(f, "Target feerate {} is below our minimum {}", target_feerate, min_feerate)
+ },
+ FeeRateAdjustmentError::FeeRateTooHigh {
+ target_feerate,
+ max_feerate,
+ target_fee,
+ original_fee,
+ } => {
+ write!(
+ f,
+ "Target feerate {} exceeds our maximum {} and target fee {} exceeds original fee estimate {}",
+ target_feerate, max_feerate, target_fee, original_fee,
+ )
+ },
+ FeeRateAdjustmentError::FeeBufferOverflow => {
+ write!(f, "Arithmetic overflow when computing available fee buffer")
+ },
+ FeeRateAdjustmentError::FeeBufferInsufficient { source, available, required } => {
+ write!(
+ f,
+ "Fee buffer {} ({}) is insufficient for required fee {}",
+ available, source, required,
+ )
+ },
+ }
+ }
+}
+
/// A template for contributing to a channel's splice funding transaction.
///
/// This is returned from [`ChannelManager::splice_channel`] when a channel is ready to be
@@ -42,23 +111,30 @@ pub struct FundingTemplate {
/// transaction.
shared_input: Option<Input>,
- /// The fee rate to use for coin selection.
- feerate: FeeRate,
+ /// The minimum fee rate for the splice transaction, used to propose as initiator.
+ min_feerate: FeeRate,
+
+ /// The maximum fee rate to accept as acceptor before declining to add our contribution to the
+ /// splice.
+ max_feerate: FeeRate,
}
impl FundingTemplate {
/// Constructs a [`FundingTemplate`] for a splice using the provided shared input.
- pub(super) fn new(shared_input: Option<Input>, feerate: FeeRate) -> Self {
- Self { shared_input, feerate }
+ pub(super) fn new(
+ shared_input: Option<Input>, min_feerate: FeeRate, max_feerate: FeeRate,
+ ) -> Self {
+ Self { shared_input, min_feerate, max_feerate }
}
}
macro_rules! build_funding_contribution {
- ($value_added:expr, $outputs:expr, $shared_input:expr, $feerate:expr, $wallet:ident, $($await:tt)*) => {{
+ ($value_added:expr, $outputs:expr, $shared_input:expr, $feerate:expr, $max_feerate:expr, $wallet:ident, $($await:tt)*) => {{
let value_added: Amount = $value_added;
let outputs: Vec<TxOut> = $outputs;
let shared_input: Option<Input> = $shared_input;
let feerate: FeeRate = $feerate;
+ let max_feerate: FeeRate = $max_feerate;
// Validate user-provided amounts are within MAX_MONEY before coin selection to
// ensure FundingContribution::net_value() arithmetic cannot overflow. With all
@@ -126,6 +202,7 @@ macro_rules! build_funding_contribution {
outputs,
change_output,
feerate,
+ max_feerate,
is_splice,
};
@@ -142,8 +219,8 @@ impl FundingTemplate {
if value_added == Amount::ZERO {
return Err(());
}
- let FundingTemplate { shared_input, feerate } = self;
- build_funding_contribution!(value_added, vec![], shared_input, feerate, wallet, await)
+ let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
+ build_funding_contribution!(value_added, vec![], shared_input, min_feerate, max_feerate, wallet, await)
}
/// Creates a [`FundingContribution`] for adding funds to a channel using `wallet` to perform
@@ -154,8 +231,15 @@ impl FundingTemplate {
if value_added == Amount::ZERO {
return Err(());
}
- let FundingTemplate { shared_input, feerate } = self;
- build_funding_contribution!(value_added, vec![], shared_input, feerate, wallet,)
+ let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
+ build_funding_contribution!(
+ value_added,
+ vec![],
+ shared_input,
+ min_feerate,
+ max_feerate,
+ wallet,
+ )
}
/// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to
@@ -166,8 +250,8 @@ impl FundingTemplate {
if outputs.is_empty() {
return Err(());
}
- let FundingTemplate { shared_input, feerate } = self;
- build_funding_contribution!(Amount::ZERO, outputs, shared_input, feerate, wallet, await)
+ let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
+ build_funding_contribution!(Amount::ZERO, outputs, shared_input, min_feerate, max_feerate, wallet, await)
}
/// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to
@@ -178,8 +262,15 @@ impl FundingTemplate {
if outputs.is_empty() {
return Err(());
}
- let FundingTemplate { shared_input, feerate } = self;
- build_funding_contribution!(Amount::ZERO, outputs, shared_input, feerate, wallet,)
+ let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
+ build_funding_contribution!(
+ Amount::ZERO,
+ outputs,
+ shared_input,
+ min_feerate,
+ max_feerate,
+ wallet,
+ )
}
/// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
@@ -190,8 +281,8 @@ impl FundingTemplate {
if value_added == Amount::ZERO && outputs.is_empty() {
return Err(());
}
- let FundingTemplate { shared_input, feerate } = self;
- build_funding_contribution!(value_added, outputs, shared_input, feerate, wallet, await)
+ let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
+ build_funding_contribution!(value_added, outputs, shared_input, min_feerate, max_feerate, wallet, await)
}
/// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
@@ -202,8 +293,15 @@ impl FundingTemplate {
if value_added == Amount::ZERO && outputs.is_empty() {
return Err(());
}
- let FundingTemplate { shared_input, feerate } = self;
- build_funding_contribution!(value_added, outputs, shared_input, feerate, wallet,)
+ let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
+ build_funding_contribution!(
+ value_added,
+ outputs,
+ shared_input,
+ min_feerate,
+ max_feerate,
+ wallet,
+ )
}
}
@@ -280,9 +378,12 @@ pub struct FundingContribution {
/// The output where any change will be sent.
change_output: Option<TxOut>,
- /// The fee rate used to select `inputs`.
+ /// The fee rate used to select `inputs` (the minimum feerate).
feerate: FeeRate,
+ /// The maximum fee rate to accept as acceptor before rejecting the splice.
+ max_feerate: FeeRate,
+
/// Whether the contribution is for funding a splice.
is_splice: bool,
}
@@ -404,11 +505,224 @@ impl FundingContribution {
Ok(())
}
+ /// Computes the adjusted fee and change output value for the acceptor at the initiator's
+ /// proposed feerate, which may differ from the feerate used during coin selection.
+ ///
+ /// On success, returns the new estimated fee and, if applicable, the new change output value:
+ /// - `Some(change)` — the adjusted change output value
+ /// - `None` — no change output (no inputs or change fell below dust)
+ ///
+ /// Returns `Err` if the contribution cannot accommodate the target feerate.
+ fn compute_feerate_adjustment(
+ &self, target_feerate: FeeRate, holder_balance: Amount,
+ ) -> Result<(Amount, Option<Amount>), FeeRateAdjustmentError> {
+ if target_feerate < self.feerate {
+ return Err(FeeRateAdjustmentError::FeeRateTooLow {
+ target_feerate,
+ min_feerate: self.feerate,
+ });
+ }
+
+ // If the target fee rate exceeds our max fee rate, we may still add our contribution
+ // if we pay less in fees. This may happen because the acceptor doesn't pay for common
+ // fields and the shared input / output.
+ if target_feerate > self.max_feerate {
+ let target_fee = estimate_transaction_fee(
+ &self.inputs,
+ &self.outputs,
+ self.change_output.as_ref(),
+ false,
+ self.is_splice,
+ target_feerate,
+ );
+ if target_fee > self.estimated_fee {
+ return Err(FeeRateAdjustmentError::FeeRateTooHigh {
+ target_feerate,
+ max_feerate: self.max_feerate,
+ target_fee,
+ original_fee: self.estimated_fee,
+ });
+ }
+ }
+
+ if !self.inputs.is_empty() {
+ if let Some(ref change_output) = self.change_output {
+ let old_change_value = change_output.value;
+ let dust_limit = change_output.script_pubkey.minimal_non_dust();
+
+ // Target fee including the change output's weight.
+ let target_fee = estimate_transaction_fee(
+ &self.inputs,
+ &self.outputs,
+ self.change_output.as_ref(),
+ false,
+ self.is_splice,
+ target_feerate,
+ );
+
+ let fee_buffer = self
+ .estimated_fee
+ .checked_add(old_change_value)
+ .ok_or(FeeRateAdjustmentError::FeeBufferOverflow)?;
+
+ match fee_buffer.checked_sub(target_fee) {
+ Some(new_change_value) if new_change_value >= dust_limit => {
+ Ok((target_fee, Some(new_change_value)))
+ },
+ _ => {
+ // Change would be below dust or negative. Try without change.
+ let target_fee_no_change = estimate_transaction_fee(
+ &self.inputs,
+ &self.outputs,
+ None,
+ false,
+ self.is_splice,
+ target_feerate,
+ );
+ if target_fee_no_change > fee_buffer {
+ Err(FeeRateAdjustmentError::FeeBufferInsufficient {
+ source: "estimated fee + change value",
+ available: fee_buffer,
+ required: target_fee_no_change,
+ })
+ } else {
+ Ok((target_fee_no_change, None))
+ }
+ },
+ }
+ } else {
+ // No change output.
+ let target_fee = estimate_transaction_fee(
+ &self.inputs,
+ &self.outputs,
+ None,
+ false,
+ self.is_splice,
+ target_feerate,
+ );
+ // The fee buffer is total input value minus value_added and output values.
+ // This is estimated_fee plus the coin selection surplus (dust burned to
+ // fees), ensuring we never silently reduce value_added beyond the small
+ // surplus from coin selection.
+ let total_input_value: Amount =
+ self.inputs.iter().map(|i| i.utxo.output.value).sum();
+ let output_values: Amount = self.outputs.iter().map(|o| o.value).sum();
+ let fee_buffer = total_input_value
+ .checked_sub(self.value_added)
+ .and_then(|v| v.checked_sub(output_values))
+ .ok_or(FeeRateAdjustmentError::FeeBufferOverflow)?;
+ if target_fee > fee_buffer {
+ return Err(FeeRateAdjustmentError::FeeBufferInsufficient {
+ source: "estimated fee + coin selection surplus",
+ available: fee_buffer,
+ required: target_fee,
+ });
+ }
+ Ok((target_fee, None))
+ }
+ } else {
+ // No inputs (splice-out): fees paid from channel balance.
+ let target_fee = estimate_transaction_fee(
+ &[],
+ &self.outputs,
+ None,
+ false,
+ self.is_splice,
+ target_feerate,
+ );
+
+ // Check that the channel balance can cover the withdrawal outputs plus fees.
+ let value_removed: Amount = self.outputs.iter().map(|o| o.value).sum();
+ let total_cost = target_fee
+ .checked_add(value_removed)
+ .ok_or(FeeRateAdjustmentError::FeeBufferOverflow)?;
+ if total_cost > holder_balance {
+ return Err(FeeRateAdjustmentError::FeeBufferInsufficient {
+ source: "channel balance - withdrawal outputs",
+ available: holder_balance.checked_sub(value_removed).unwrap_or(Amount::ZERO),
+ required: target_fee,
+ });
+ }
+ // Surplus goes back to the channel balance.
+ Ok((target_fee, None))
+ }
+ }
+
+ /// Adjusts the contribution's change output for the initiator's feerate.
+ ///
+ /// When the acceptor has a pending contribution (from the quiescence tie-breaker scenario),
+ /// the initiator's proposed feerate may differ from the feerate used during coin selection.
+ /// This adjusts the change output so the acceptor pays their target fee at the target
+ /// feerate.
+ pub(super) fn for_acceptor_at_feerate(
+ mut self, feerate: FeeRate, holder_balance: Amount,
+ ) -> Result<Self, FeeRateAdjustmentError> {
+ let (new_estimated_fee, new_change) =
+ self.compute_feerate_adjustment(feerate, holder_balance)?;
+ let surplus = self.fee_buffer_surplus(new_estimated_fee, &new_change);
+ match new_change {
+ Some(value) => self.change_output.as_mut().unwrap().value = value,
+ None => self.change_output = None,
+ }
+ self.value_added += surplus;
+ self.estimated_fee = new_estimated_fee;
+ self.feerate = feerate;
+ Ok(self)
+ }
+
+ /// Returns the net value at the given target feerate without mutating `self`.
+ ///
+ /// This serves double duty: it checks feerate compatibility (returning `Err` if the feerate
+ /// can't be accommodated) and computes the adjusted net value (returning `Ok` with the value
+ /// accounting for the target feerate).
+ pub(super) fn net_value_for_acceptor_at_feerate(
+ &self, target_feerate: FeeRate, holder_balance: Amount,
+ ) -> Result<SignedAmount, FeeRateAdjustmentError> {
+ let (new_estimated_fee, new_change) =
+ self.compute_feerate_adjustment(target_feerate, holder_balance)?;
+ let surplus = self
+ .fee_buffer_surplus(new_estimated_fee, &new_change)
+ .to_signed()
+ .expect("surplus does not exceed Amount::MAX_MONEY");
+ let net_value = self
+ .net_value_with_fee(new_estimated_fee)
+ .checked_add(surplus)
+ .expect("net_value + surplus does not overflow");
+ Ok(net_value)
+ }
+
+ /// Returns the fee buffer surplus when a change output is removed.
+ ///
+ /// The fee buffer is the actual amount available for fees from inputs: total input value
+ /// minus value_added and output values. This includes both the weight-based estimated_fee
+ /// and any coin selection surplus (dust burned to fees). When the change output is removed,
+ /// the fee buffer may exceed the new fee; the surplus is returned so it can be redirected
+ /// to value_added rather than being burned as excess fees.
+ ///
+ /// Returns [`Amount::ZERO`] when there are no inputs or the change output is kept.
+ fn fee_buffer_surplus(&self, new_estimated_fee: Amount, new_change: &Option<Amount>) -> Amount {
+ if !self.inputs.is_empty() && new_change.is_none() {
+ let total_input_value: Amount = self.inputs.iter().map(|i| i.utxo.output.value).sum();
+ let output_values: Amount = self.outputs.iter().map(|o| o.value).sum();
+ let fee_buffer = total_input_value - self.value_added - output_values;
+ debug_assert!(fee_buffer >= new_estimated_fee);
+ fee_buffer - new_estimated_fee
+ } else {
+ Amount::ZERO
+ }
+ }
+
/// The net value contributed to a channel by the splice. If negative, more value will be
/// spliced out than spliced in. Fees will be deducted from the expected splice-out amount
/// if no inputs were included.
pub fn net_value(&self) -> SignedAmount {
- let unpaid_fees = if self.inputs.is_empty() { self.estimated_fee } else { Amount::ZERO }
+ self.net_value_with_fee(self.estimated_fee)
+ }
+
+ /// Computes the net value using the given `estimated_fee` for the splice-out (no inputs)
+ /// case. For splice-in, fees are paid by inputs so `estimated_fee` is not deducted.
+ fn net_value_with_fee(&self, estimated_fee: Amount) -> SignedAmount {
+ let unpaid_fees = if self.inputs.is_empty() { estimated_fee } else { Amount::ZERO }
.to_signed()
.expect("estimated_fee is validated to not exceed Amount::MAX_MONEY");
let value_added = self
@@ -436,7 +750,10 @@ pub type FundingTxInput = crate::util::wallet_utils::ConfirmedUtxo;
#[cfg(test)]
mod tests {
- use super::{estimate_transaction_fee, FundingContribution, FundingTemplate, FundingTxInput};
+ use super::{
+ estimate_transaction_fee, FeeRateAdjustmentError, FundingContribution, FundingTemplate,
+ FundingTxInput,
+ };
use crate::chain::ClaimId;
use crate::util::wallet_utils::{CoinSelection, CoinSelectionSourceSync, Input};
use bitcoin::hashes::Hash;
@@ -556,6 +873,7 @@ mod tests {
change_output: None,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
+ max_feerate: FeeRate::MAX,
};
assert!(contribution.validate().is_ok());
assert_eq!(contribution.net_value(), contribution.value_added.to_signed().unwrap());
@@ -577,6 +895,7 @@ mod tests {
change_output: None,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
+ max_feerate: FeeRate::MAX,
};
assert!(contribution.validate().is_ok());
assert_eq!(contribution.net_value(), SignedAmount::from_sat(220_000 - 200_000));
@@ -598,6 +917,7 @@ mod tests {
change_output: None,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
+ max_feerate: FeeRate::MAX,
};
assert!(contribution.validate().is_ok());
assert_eq!(contribution.net_value(), SignedAmount::from_sat(220_000 - 400_000));
@@ -619,6 +939,7 @@ mod tests {
change_output: None,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(90000),
+ max_feerate: FeeRate::MAX,
};
assert_eq!(
contribution.validate(),
@@ -642,6 +963,7 @@ mod tests {
change_output: None,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
+ max_feerate: FeeRate::MAX,
};
assert_eq!(
contribution.validate(),
@@ -666,6 +988,7 @@ mod tests {
change_output: None,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
+ max_feerate: FeeRate::MAX,
};
assert!(contribution.validate().is_ok());
assert_eq!(contribution.net_value(), contribution.value_added.to_signed().unwrap());
@@ -685,6 +1008,7 @@ mod tests {
change_output: None,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2200),
+ max_feerate: FeeRate::MAX,
};
assert_eq!(
contribution.validate(),
@@ -709,6 +1033,7 @@ mod tests {
change_output: None,
is_splice: false,
feerate: FeeRate::from_sat_per_kwu(2000),
+ max_feerate: FeeRate::MAX,
};
assert!(contribution.validate().is_ok());
assert_eq!(contribution.net_value(), contribution.value_added.to_signed().unwrap());
@@ -736,20 +1061,20 @@ mod tests {
// splice_in_sync with value_added > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate);
+ let template = FundingTemplate::new(None, feerate, feerate);
assert!(template.splice_in_sync(over_max, UnreachableWallet).is_err());
}
// splice_out_sync with single output value > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate);
+ let template = FundingTemplate::new(None, feerate, feerate);
let outputs = vec![funding_output_sats(over_max.to_sat())];
assert!(template.splice_out_sync(outputs, UnreachableWallet).is_err());
}
// splice_out_sync with multiple outputs summing > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate);
+ let template = FundingTemplate::new(None, feerate, feerate);
let half_over = Amount::MAX_MONEY / 2 + Amount::from_sat(1);
let outputs = vec![
funding_output_sats(half_over.to_sat()),
@@ -760,18 +1085,710 @@ mod tests {
// splice_in_and_out_sync with value_added > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate);
+ let template = FundingTemplate::new(None, feerate, feerate);
let outputs = vec![funding_output_sats(1_000)];
assert!(template.splice_in_and_out_sync(over_max, outputs, UnreachableWallet).is_err());
}
// splice_in_and_out_sync with output sum > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate);
+ let template = FundingTemplate::new(None, feerate, feerate);
let outputs = vec![funding_output_sats(over_max.to_sat())];
assert!(template
.splice_in_and_out_sync(Amount::from_sat(1_000), outputs, UnreachableWallet)
.is_err());
}
}
+
+ #[test]
+ fn test_for_acceptor_at_feerate_higher_change_adjusted() {
+ // Splice-in: higher target feerate reduces the change output.
+ // The fee overestimates (with is_initiator=true) by including common TX fields, shared
+ // output, and shared input weight. So we need a sufficiently high target feerate for the
+ // acceptor's target fee to exceed the original fee estimate, causing the change to decrease.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(6000);
+ let inputs = vec![funding_input_sats(100_000)];
+ let change = funding_output_sats(10_000);
+
+ // Fee estimate computed as initiator (overestimate), including change output weight.
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs: inputs.clone(),
+ outputs: vec![],
+ change_output: Some(change.clone()),
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let net_value_before = contribution.net_value();
+ let contribution =
+ contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX).unwrap();
+
+ // Target fee at target feerate for acceptor (is_initiator=false), including change weight.
+ let expected_target_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), false, true, target_feerate);
+ let expected_change = estimated_fee + Amount::from_sat(10_000) - expected_target_fee;
+
+ assert_eq!(contribution.estimated_fee, expected_target_fee);
+ assert!(contribution.change_output.is_some());
+ assert_eq!(contribution.change_output.as_ref().unwrap().value, expected_change);
+ assert!(expected_change < Amount::from_sat(10_000)); // Change reduced
+ assert_eq!(contribution.net_value(), net_value_before);
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_lower_rejected_too_low() {
+ // Splice-in: target feerate below our minimum is rejected as FeeRateTooLow.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(1000);
+ let inputs = vec![funding_input_sats(100_000)];
+ let change = funding_output_sats(10_000);
+
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: Some(change),
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeRateTooLow { .. })));
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_change_removed() {
+ // Splice-in: feerate high enough that change drops below dust and is removed,
+ // but the fee buffer (estimated_fee + change) still covers the fee without the change output.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(7000);
+ let value_added = Amount::from_sat(50_000);
+ let change_value = Amount::from_sat(500);
+
+ // Compute estimated_fee first (weight-based, independent of input value).
+ let dummy_inputs = vec![funding_input_sats(1)];
+ let change = funding_output_sats(change_value.to_sat());
+ let estimated_fee = estimate_transaction_fee(
+ &dummy_inputs,
+ &[],
+ Some(&change),
+ true,
+ true,
+ original_feerate,
+ );
+
+ // Realistic input: value_added + estimated_fee + change (what coin selection produces).
+ let input_value = value_added + estimated_fee + change_value;
+ let inputs = vec![funding_input_sats(input_value.to_sat())];
+ let change = funding_output_sats(change_value.to_sat());
+
+ let contribution = FundingContribution {
+ value_added,
+ estimated_fee,
+ inputs: inputs.clone(),
+ outputs: vec![],
+ change_output: Some(change),
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let net_value_before = contribution.net_value();
+ let contribution =
+ contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX).unwrap();
+
+ // Change should be removed; estimated_fee updated to no-change target fee.
+ assert!(contribution.change_output.is_none());
+ let expected_fee_no_change =
+ estimate_transaction_fee(&inputs, &[], None, false, true, target_feerate);
+ assert_eq!(contribution.estimated_fee, expected_fee_no_change);
+ // The surplus (old fee buffer - new fee) goes to value_added, increasing net_value.
+ let surplus = estimated_fee + change_value - expected_fee_no_change;
+ assert_eq!(contribution.net_value(), net_value_before + surplus.to_signed().unwrap());
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_too_high_rejected() {
+ // Splice-in: feerate so high that even without change, the fee can't be covered.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(100_000);
+ let inputs = vec![funding_input_sats(100_000)];
+ let change = funding_output_sats(500);
+
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: Some(change),
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. })));
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_splice_out_sufficient() {
+ // Splice-out (no inputs): the fee estimate from the is_initiator=true overestimate covers
+ // the acceptor's target fee at a moderately higher target feerate.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(3000);
+ let outputs = vec![funding_output_sats(50_000)];
+
+ let estimated_fee =
+ estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::ZERO,
+ estimated_fee,
+ inputs: vec![],
+ outputs: outputs.clone(),
+ change_output: None,
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let contribution =
+ contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX).unwrap();
+ // estimated_fee is updated to the target fee; surplus goes back to channel balance.
+ let expected_target_fee =
+ estimate_transaction_fee(&[], &outputs, None, false, true, target_feerate);
+ assert_eq!(contribution.estimated_fee, expected_target_fee);
+ assert!(expected_target_fee <= estimated_fee);
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_splice_out_insufficient() {
+ // Splice-out: channel balance too small for outputs + target fee at high target feerate.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(50_000);
+ let outputs = vec![funding_output_sats(50_000)];
+
+ let estimated_fee =
+ estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::ZERO,
+ estimated_fee,
+ inputs: vec![],
+ outputs,
+ change_output: None,
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ // Balance of 55,000 sats can't cover outputs (50,000) + target_fee at 50k sat/kwu.
+ let holder_balance = Amount::from_sat(55_000);
+ let result = contribution.for_acceptor_at_feerate(target_feerate, holder_balance);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. })));
+ }
+
+ #[test]
+ fn test_net_value_for_acceptor_at_feerate_splice_in() {
+ // Splice-in: net_value_for_acceptor_at_feerate returns the same value as net_value() since
+ // splice-in fees are paid by inputs, not from channel balance.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(3000);
+ let inputs = vec![funding_input_sats(100_000)];
+ let change = funding_output_sats(10_000);
+
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: Some(change),
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ // For splice-in with change that stays above dust, the surplus is absorbed by the change
+ // output so net_value_for_acceptor_at_feerate equals net_value.
+ let net_at_feerate =
+ contribution.net_value_for_acceptor_at_feerate(target_feerate, Amount::MAX).unwrap();
+ assert_eq!(net_at_feerate, contribution.net_value());
+ assert_eq!(net_at_feerate, Amount::from_sat(50_000).to_signed().unwrap());
+ }
+
+ #[test]
+ fn test_net_value_for_acceptor_at_feerate_splice_out() {
+ // Splice-out: net_value_for_acceptor_at_feerate returns the adjusted value using the target fee
+ // at the target feerate.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(3000);
+ let outputs = vec![funding_output_sats(50_000)];
+
+ let estimated_fee =
+ estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::ZERO,
+ estimated_fee,
+ inputs: vec![],
+ outputs: outputs.clone(),
+ change_output: None,
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let net_at_feerate =
+ contribution.net_value_for_acceptor_at_feerate(target_feerate, Amount::MAX).unwrap();
+
+ // The target fee at target feerate should be less than the initiator's fee estimate.
+ let target_fee = estimate_transaction_fee(&[], &outputs, None, false, true, target_feerate);
+ let expected_net = SignedAmount::ZERO
+ - Amount::from_sat(50_000).to_signed().unwrap()
+ - target_fee.to_signed().unwrap();
+ assert_eq!(net_at_feerate, expected_net);
+
+ // Should be less negative than net_value() which uses the higher fee estimate.
+ assert!(net_at_feerate > contribution.net_value());
+ }
+
+ #[test]
+ fn test_net_value_for_acceptor_at_feerate_does_not_mutate() {
+ // Verify net_value_for_acceptor_at_feerate does not modify the contribution.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(5000);
+ let inputs = vec![funding_input_sats(100_000)];
+ let change = funding_output_sats(10_000);
+
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: Some(change),
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let net_before = contribution.net_value();
+ let fee_before = contribution.estimated_fee;
+ let change_before = contribution.change_output.as_ref().unwrap().value;
+
+ let _ = contribution.net_value_for_acceptor_at_feerate(target_feerate, Amount::MAX);
+
+ // Nothing should have changed.
+ assert_eq!(contribution.net_value(), net_before);
+ assert_eq!(contribution.estimated_fee, fee_before);
+ assert_eq!(contribution.change_output.as_ref().unwrap().value, change_before);
+ }
+
+ #[test]
+ fn test_net_value_for_acceptor_at_feerate_too_high() {
+ // net_value_for_acceptor_at_feerate returns Err when feerate can't be accommodated.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(100_000);
+ let inputs = vec![funding_input_sats(100_000)];
+ let change = funding_output_sats(500);
+
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: Some(change),
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let result = contribution.net_value_for_acceptor_at_feerate(target_feerate, Amount::MAX);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. })));
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_exceeds_max_rejected() {
+ // Splice-in: target feerate exceeds max_feerate and target fee exceeds the fee buffer,
+ // so the adjustment is rejected as FeeRateTooHigh.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let max_feerate = FeeRate::from_sat_per_kwu(3000);
+ let target_feerate = FeeRate::from_sat_per_kwu(100_000);
+ let inputs = vec![funding_input_sats(100_000)];
+ let change = funding_output_sats(10_000);
+
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: Some(change),
+ feerate: original_feerate,
+ max_feerate,
+ is_splice: true,
+ };
+
+ let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeRateTooHigh { .. })));
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_exceeds_max_allowed() {
+ // Splice-in: target feerate exceeds max_feerate but the acceptor's target fee
+ // (is_initiator=false at target) is less than the fee buffer (is_initiator=true at
+ // original feerate). This works because the initiator fee estimate includes ~598 WU of
+ // extra weight (common TX fields, funding output, shared input) that the acceptor
+ // doesn't pay for, so the fee buffer is ~2.5x larger than the acceptor's target fee at
+ // the same feerate.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let max_feerate = FeeRate::from_sat_per_kwu(3000);
+ let target_feerate = FeeRate::from_sat_per_kwu(4000);
+ let inputs = vec![funding_input_sats(100_000)];
+ let change = funding_output_sats(10_000);
+
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: Some(change.clone()),
+ feerate: original_feerate,
+ max_feerate,
+ is_splice: true,
+ };
+
+ let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX);
+ assert!(result.is_ok());
+ let adjusted = result.unwrap();
+
+ // The acceptor's target fee at target (4000, is_initiator=false) is less than the
+ // fee estimate at original (2000, is_initiator=true) due to the ~2.5x weight ratio,
+ // so change increases despite the higher feerate.
+ assert!(adjusted.change_output.is_some());
+ assert!(adjusted.change_output.as_ref().unwrap().value > Amount::from_sat(10_000));
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_within_range() {
+ // Splice-in: target feerate is between min and max, so the min/max checks
+ // don't interfere and the normal adjustment logic applies.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let max_feerate = FeeRate::from_sat_per_kwu(5000);
+ let target_feerate = FeeRate::from_sat_per_kwu(3000);
+ let inputs = vec![funding_input_sats(100_000)];
+ let change = funding_output_sats(10_000);
+
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: Some(change),
+ feerate: original_feerate,
+ max_feerate,
+ is_splice: true,
+ };
+
+ let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX);
+ assert!(result.is_ok());
+ let adjusted = result.unwrap();
+
+ // At a higher target feerate, the target fee increases so change should decrease
+ // (or stay the same if the fee estimate absorbs the difference).
+ // The key assertion is that the adjustment succeeds with a valid change output.
+ assert!(adjusted.change_output.is_some());
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_no_change_shortfall_from_value_added() {
+ // Inputs present, no change output. Higher target feerate makes target_fee > estimated_fee.
+ // With realistic inputs (no coin selection surplus), the fee buffer is just estimated_fee,
+ // so the shortfall cannot be absorbed and the contribution is dropped.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(20_000);
+ let value_added = Amount::from_sat(50_000);
+
+ // Compute estimated_fee first (weight-based, independent of input value).
+ let dummy_inputs = vec![funding_input_sats(1)];
+ let estimated_fee =
+ estimate_transaction_fee(&dummy_inputs, &[], None, true, true, original_feerate);
+
+ // Realistic input: value_added + estimated_fee (what coin selection produces, no surplus).
+ let inputs = vec![funding_input_sats((value_added + estimated_fee).to_sat())];
+ let target_fee = estimate_transaction_fee(&inputs, &[], None, false, true, target_feerate);
+
+ // Verify our setup: target_fee > estimated_fee (shortfall exists) and the fee buffer
+ // (estimated_fee, with no coin selection surplus) cannot cover it.
+ assert!(target_fee > estimated_fee);
+
+ let contribution = FundingContribution {
+ value_added,
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: None,
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. })));
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_no_change_insufficient() {
+ // Inputs present, no change output. The target feerate is so high that the fee buffer
+ // (total input value minus value_added) cannot cover the target fee.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(20_000);
+ let value_added = Amount::from_sat(1);
+
+ // Compute estimated_fee first (weight-based, independent of input value).
+ let dummy_inputs = vec![funding_input_sats(1)];
+ let estimated_fee =
+ estimate_transaction_fee(&dummy_inputs, &[], None, true, true, original_feerate);
+
+ // Realistic input: value_added + estimated_fee (no surplus).
+ let inputs = vec![funding_input_sats((value_added + estimated_fee).to_sat())];
+ let target_fee = estimate_transaction_fee(&inputs, &[], None, false, true, target_feerate);
+ assert!(target_fee > estimated_fee);
+
+ let contribution = FundingContribution {
+ value_added,
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: None,
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. })));
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_no_change_surplus_below_dust() {
+ // Inputs present, no change output. The acceptor built their contribution at a low
+ // feerate as if they were the initiator (including common TX fields in estimated_fee).
+ // The initiator proposes a ~3x higher feerate. At that rate, the acceptor's target fee
+ // (only their personal input weight) nearly matches the original fee estimate, leaving a
+ // small surplus below the dust limit.
+ let original_feerate = FeeRate::from_sat_per_kwu(1000);
+ let target_feerate = FeeRate::from_sat_per_kwu(3000);
+ let inputs = vec![funding_input_sats(100_000)];
+
+ // estimated_fee includes common TX fields (is_initiator=true) at the original feerate.
+ let estimated_fee =
+ estimate_transaction_fee(&inputs, &[], None, true, true, original_feerate);
+
+ // target_fee only includes the acceptor's contributed weight (is_initiator=false) at the
+ // higher target feerate.
+ let target_fee = estimate_transaction_fee(&inputs, &[], None, false, true, target_feerate);
+
+ // Verify our setup: surplus is positive and below the P2WPKH dust limit (294 sats).
+ assert!(estimated_fee > target_fee);
+ let dust_limit = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()).minimal_non_dust();
+ assert!(estimated_fee - target_fee < dust_limit);
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: None,
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX);
+ assert!(result.is_ok());
+ let adjusted = result.unwrap();
+ assert!(adjusted.change_output.is_none());
+ assert_eq!(adjusted.estimated_fee, target_fee);
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_no_change_surplus_absorbed() {
+ // Inputs, no change. The estimated_fee (is_initiator=true) far exceeds the acceptor's
+ // target fee (is_initiator=false). The surplus stays in the channel balance rather than
+ // being burned as excess fees.
+ let feerate = FeeRate::from_sat_per_kwu(2000);
+ let value_added = Amount::from_sat(50_000);
+
+ // Compute estimated_fee first (weight-based, independent of input value).
+ let dummy_inputs = vec![funding_input_sats(1)];
+ let estimated_fee = estimate_transaction_fee(&dummy_inputs, &[], None, true, true, feerate);
+
+ // Realistic input: value_added + estimated_fee (no surplus).
+ let inputs = vec![funding_input_sats((value_added + estimated_fee).to_sat())];
+
+ // Initiator fee estimate includes common TX fields + shared output + shared input weight,
+ // making it ~3x the acceptor's target fee at the same feerate.
+ let target_fee = estimate_transaction_fee(&inputs, &[], None, false, true, feerate);
+
+ let contribution = FundingContribution {
+ value_added,
+ estimated_fee,
+ inputs,
+ outputs: vec![],
+ change_output: None,
+ feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ // target == min feerate, so FeeRateTooLow check passes.
+ // The surplus (estimated_fee - target_fee) goes to value_added (shared output).
+ let net_value_before = contribution.net_value();
+ let result = contribution.for_acceptor_at_feerate(feerate, Amount::MAX);
+ assert!(result.is_ok());
+ let adjusted = result.unwrap();
+ assert!(adjusted.change_output.is_none());
+ assert_eq!(adjusted.estimated_fee, target_fee);
+ let surplus = estimated_fee - target_fee;
+ assert_eq!(adjusted.value_added, value_added + surplus);
+ assert_eq!(adjusted.net_value(), net_value_before + surplus.to_signed().unwrap());
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_fee_buffer_overflow() {
+ // Construct a contribution with estimated_fee and change values that overflow Amount.
+ let feerate = FeeRate::from_sat_per_kwu(2000);
+ let inputs = vec![funding_input_sats(100_000)];
+
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(50_000),
+ estimated_fee: Amount::MAX,
+ inputs,
+ outputs: vec![],
+ change_output: Some(funding_output_sats(1)),
+ feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ let result = contribution.for_acceptor_at_feerate(feerate, Amount::MAX);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferOverflow)));
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_splice_out_balance_insufficient() {
+ // Splice-out: channel balance too small to cover outputs + target fee.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(3000);
+ let outputs = vec![funding_output_sats(50_000)];
+
+ let estimated_fee =
+ estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::ZERO,
+ estimated_fee,
+ inputs: vec![],
+ outputs: outputs.clone(),
+ change_output: None,
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ // Balance of 40,000 sats is less than outputs (50,000) + target_fee.
+ let holder_balance = Amount::from_sat(40_000);
+ let result = contribution.for_acceptor_at_feerate(target_feerate, holder_balance);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. })));
+ }
+
+ #[test]
+ fn test_for_acceptor_at_feerate_splice_out_balance_sufficient() {
+ // Splice-out: channel balance large enough to cover outputs + target fee.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(3000);
+ let outputs = vec![funding_output_sats(50_000)];
+
+ let estimated_fee =
+ estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::ZERO,
+ estimated_fee,
+ inputs: vec![],
+ outputs: outputs.clone(),
+ change_output: None,
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ // Balance of 100,000 sats is more than outputs (50,000) + target_fee.
+ let holder_balance = Amount::from_sat(100_000);
+ let contribution =
+ contribution.for_acceptor_at_feerate(target_feerate, holder_balance).unwrap();
+ let expected_target_fee =
+ estimate_transaction_fee(&[], &outputs, None, false, true, target_feerate);
+ assert_eq!(contribution.estimated_fee, expected_target_fee);
+ }
+
+ #[test]
+ fn test_net_value_for_acceptor_at_feerate_splice_out_balance_insufficient() {
+ // Splice-out: net_value_for_acceptor_at_feerate returns Err when channel balance
+ // is too small to cover outputs + target fee.
+ let original_feerate = FeeRate::from_sat_per_kwu(2000);
+ let target_feerate = FeeRate::from_sat_per_kwu(3000);
+ let outputs = vec![funding_output_sats(50_000)];
+
+ let estimated_fee =
+ estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate);
+
+ let contribution = FundingContribution {
+ value_added: Amount::ZERO,
+ estimated_fee,
+ inputs: vec![],
+ outputs,
+ change_output: None,
+ feerate: original_feerate,
+ max_feerate: FeeRate::MAX,
+ is_splice: true,
+ };
+
+ // Balance of 40,000 sats is less than outputs (50,000) + target_fee.
+ let holder_balance = Amount::from_sat(40_000);
+ let result = contribution.net_value_for_acceptor_at_feerate(target_feerate, holder_balance);
+ assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. })));
+ }
}
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 9bcc473..70b347e 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -59,7 +59,7 @@ fn test_splicing_not_supported_api_error() {
let (_, _, channel_id, _) = create_announced_chan_between_nodes(&nodes, 0, 1);
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let res = nodes[1].node.splice_channel(&channel_id, &node_id_0, feerate);
+ let res = nodes[1].node.splice_channel(&channel_id, &node_id_0, feerate, FeeRate::MAX);
match res {
Err(APIError::ChannelUnavailable { err }) => {
assert!(err.contains("Peer does not support splicing"))
@@ -80,7 +80,7 @@ fn test_splicing_not_supported_api_error() {
reconnect_args.send_announcement_sigs = (true, true);
reconnect_nodes(reconnect_args);
- let res = nodes[1].node.splice_channel(&channel_id, &node_id_0, feerate);
+ let res = nodes[1].node.splice_channel(&channel_id, &node_id_0, feerate, FeeRate::MAX);
match res {
Err(APIError::ChannelUnavailable { err }) => {
assert!(err.contains("Peer does not support quiescence, a splicing prerequisite"))
@@ -112,7 +112,7 @@ fn test_v1_splice_in_negative_insufficient_inputs() {
// Initiate splice-in, with insufficient input contribution
let funding_template = nodes[0]
.node
- .splice_channel(&channel_id, &nodes[1].node.get_our_node_id(), feerate)
+ .splice_channel(&channel_id, &nodes[1].node.get_our_node_id(), feerate, FeeRate::MAX)
.unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
@@ -177,7 +177,7 @@ fn test_validate_accounts_for_change_output_weight() {
let feerate = FeeRate::from_sat_per_kwu(2000);
let funding_template = nodes[0]
.node
- .splice_channel(&channel_id, &nodes[1].node.get_our_node_id(), feerate)
+ .splice_channel(&channel_id, &nodes[1].node.get_our_node_id(), feerate, FeeRate::MAX)
.unwrap();
// Input value = value_added + 1800: above 1736/1740 (fee without change), below 1984/1988
@@ -221,8 +221,10 @@ pub fn do_initiate_splice_in<'a, 'b, 'c, 'd>(
) -> FundingContribution {
let node_id_acceptor = acceptor.node.get_our_node_id();
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template =
- initiator.node.splice_channel(&channel_id, &node_id_acceptor, feerate).unwrap();
+ let funding_template = initiator
+ .node
+ .splice_channel(&channel_id, &node_id_acceptor, feerate, FeeRate::MAX)
+ .unwrap();
let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
let funding_contribution = funding_template.splice_in_sync(value_added, &wallet).unwrap();
initiator
@@ -238,8 +240,10 @@ pub fn initiate_splice_out<'a, 'b, 'c, 'd>(
) -> Result<FundingContribution, APIError> {
let node_id_acceptor = acceptor.node.get_our_node_id();
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template =
- initiator.node.splice_channel(&channel_id, &node_id_acceptor, feerate).unwrap();
+ let funding_template = initiator
+ .node
+ .splice_channel(&channel_id, &node_id_acceptor, feerate, FeeRate::MAX)
+ .unwrap();
let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
let funding_contribution = funding_template.splice_out_sync(outputs, &wallet).unwrap();
match initiator.node.funding_contributed(
@@ -269,8 +273,10 @@ pub fn do_initiate_splice_in_and_out<'a, 'b, 'c, 'd>(
) -> FundingContribution {
let node_id_acceptor = acceptor.node.get_our_node_id();
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template =
- initiator.node.splice_channel(&channel_id, &node_id_acceptor, feerate).unwrap();
+ let funding_template = initiator
+ .node
+ .splice_channel(&channel_id, &node_id_acceptor, feerate, FeeRate::MAX)
+ .unwrap();
let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
let funding_contribution =
funding_template.splice_in_and_out_sync(value_added, outputs, &wallet).unwrap();
@@ -1162,7 +1168,8 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
}];
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template = nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate).unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate, FeeRate::MAX).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let funding_contribution = funding_template.splice_out_sync(outputs.clone(), &wallet).unwrap();
nodes[0]
@@ -1171,7 +1178,7 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
.unwrap();
assert_eq!(
- nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate),
+ nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate, FeeRate::MAX),
Err(APIError::APIMisuseError {
err: format!(
"Channel {} cannot be spliced as one is waiting to be negotiated",
@@ -1183,7 +1190,7 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
let new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]);
assert_eq!(
- nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate),
+ nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate, FeeRate::MAX),
Err(APIError::APIMisuseError {
err: format!(
"Channel {} cannot be spliced as one is currently being negotiated",
@@ -1194,7 +1201,8 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
// The acceptor can enqueue a quiescent action while the current splice is pending.
let added_value = Amount::from_sat(initial_channel_value_sat);
- let acceptor_template = nodes[1].node.splice_channel(&channel_id, &node_0_id, feerate).unwrap();
+ let acceptor_template =
+ nodes[1].node.splice_channel(&channel_id, &node_0_id, feerate, FeeRate::MAX).unwrap();
let acceptor_wallet = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
let acceptor_contribution =
acceptor_template.splice_in_sync(added_value, &acceptor_wallet).unwrap();
@@ -1212,7 +1220,7 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
);
assert_eq!(
- nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate),
+ nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate, FeeRate::MAX),
Err(APIError::APIMisuseError {
err: format!(
"Channel {} cannot be spliced as one is currently being negotiated",
@@ -1229,7 +1237,7 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
// Now that the splice is pending, another splice may be initiated, but we must wait until
// the `splice_locked` exchange to send the initiator `stfu`.
- assert!(nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate).is_ok());
+ assert!(nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate, FeeRate::MAX).is_ok());
if reconnect {
nodes[0].node.peer_disconnected(node_1_id);
@@ -1270,7 +1278,8 @@ fn test_initiating_splice_holds_stfu_with_pending_splice() {
let funding_contribution_0 = initiate_splice_in(&nodes[0], &nodes[1], channel_id, value_added);
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template = nodes[1].node.splice_channel(&channel_id, &node_0_id, feerate).unwrap();
+ let funding_template =
+ nodes[1].node.splice_channel(&channel_id, &node_0_id, feerate, FeeRate::MAX).unwrap();
let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution_0);
@@ -2956,7 +2965,8 @@ fn test_funding_contributed_counterparty_not_found() {
provide_utxo_reserves(&nodes, 1, splice_in_amount * 2);
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate, FeeRate::MAX).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let funding_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap();
@@ -2995,7 +3005,8 @@ fn test_funding_contributed_channel_not_found() {
provide_utxo_reserves(&nodes, 1, splice_in_amount * 2);
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate, FeeRate::MAX).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let funding_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap();
@@ -3039,7 +3050,8 @@ fn test_funding_contributed_splice_already_pending() {
script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_raw_hash(Hash::all_zeros())),
};
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate, FeeRate::MAX).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let first_contribution = funding_template
.splice_in_and_out_sync(splice_in_amount, vec![first_splice_out.clone()], &wallet)
@@ -3061,7 +3073,8 @@ fn test_funding_contributed_splice_already_pending() {
nodes[0].wallet_source.clear_utxos();
provide_utxo_reserves(&nodes, 1, splice_in_amount * 3);
- let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate, FeeRate::MAX).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let second_contribution = funding_template
.splice_in_and_out_sync(splice_in_amount, vec![second_splice_out.clone()], &wallet)
@@ -3130,7 +3143,8 @@ fn test_funding_contributed_duplicate_contribution_no_event() {
provide_utxo_reserves(&nodes, 1, splice_in_amount * 2);
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate, FeeRate::MAX).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap();
@@ -3188,7 +3202,8 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) {
// Build first contribution
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate, FeeRate::MAX).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let first_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap();
@@ -3196,7 +3211,8 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) {
nodes[0].wallet_source.clear_utxos();
provide_utxo_reserves(&nodes, 1, splice_in_amount * 3);
- let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate, FeeRate::MAX).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let second_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap();
@@ -3316,7 +3332,8 @@ fn test_funding_contributed_channel_shutdown() {
provide_utxo_reserves(&nodes, 1, splice_in_amount * 2);
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate, FeeRate::MAX).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let funding_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap();
@@ -3369,8 +3386,10 @@ fn test_funding_contributed_unfunded_channel() {
provide_utxo_reserves(&nodes, 1, splice_in_amount * 2);
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template =
- nodes[0].node.splice_channel(&funded_channel_id, &node_id_1, feerate).unwrap();
+ let funding_template = nodes[0]
+ .node
+ .splice_channel(&funded_channel_id, &node_id_1, feerate, FeeRate::MAX)
+ .unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let funding_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap();
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.