Move feerate parameters from splice_channel/rbf_channel to FundingTemplate
What changed, and why it matters
This commit is a routine API refactor for the experimental splicing feature in rust-lightning. It moves feerate parameters from the splice_channel/rbf_channel calls into the later FundingTemplate methods, and exposes a minimum RBF feerate floor so users can pick a valid feerate. It is not a security patch and does not fix a known vulnerability; it changes how users interact with the splicing API.
No immediate security action required. Review downstream code that calls splice_channel/rbf_channel or FundingTemplate splice methods to update to the new API signature and ensure callers respect the returned min_rbf_feerate floor.
Security signals we found
API refactor of experimental splicing/RBF feature
RBF feerate floor (25/24 rule) is now exposed to callers via FundingTemplate::min_rbf_feerate
Validation that chosen min_feerate meets the RBF floor is moved into FundingTemplate splice methods
No removal of security-critical checks; existing 25/24 rule is preserved and surfaced earlier
No CVE, advisory, or vendor security disclosure referenced in commit
Evidence from the diff
The commit refactors the splicing/RBF API: splice_channel and rbf_channel no longer accept min_feerate/max_feerate. Instead, FundingTemplate carries an optional min_rbf_feerate (computed as 25/24 of the previous feerate when an RBF floor applies) and the splice_in_sync/splice_out_sync/splice_in_and_out_sync methods now take min_feerate/max_feerate and validate min_feerate >= min_rbf_feerate. Tests and fuzz harnesses are updated to match the new API. The change is architectural, not a vulnerability fix.
Changed components
lightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/funding.rslightning/src/ln/splicing_tests.rsfuzz/src/chanmon_consistency.rsfuzz/src/full_stack.rsInspect captured patch +402 / −391
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 2200689..5d46cf2 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -1391,14 +1391,8 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
let splice_channel = |node: &ChanMan,
counterparty_node_id: &PublicKey,
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,
- FeeRate::MAX,
- ) {
+ f: &dyn Fn(FundingTemplate) -> Result<FundingContribution, ()>| {
+ match node.splice_channel(channel_id, counterparty_node_id) {
Ok(funding_template) => {
if let Ok(contribution) = f(funding_template) {
let _ = node.funding_contributed(
@@ -1425,15 +1419,10 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
channel_id: &ChannelId,
wallet: &WalletSync<&TestWalletSource, Arc<dyn Logger + MaybeSend + MaybeSync>>,
funding_feerate_sat_per_kw: FeeRate| {
- splice_channel(
- node,
- counterparty_node_id,
- channel_id,
- &move |funding_template: FundingTemplate| {
- funding_template.splice_in_sync(Amount::from_sat(10_000), wallet)
- },
- funding_feerate_sat_per_kw,
- );
+ splice_channel(node, counterparty_node_id, channel_id, &move |funding_template: FundingTemplate| {
+ let feerate = funding_template.min_rbf_feerate().unwrap_or(funding_feerate_sat_per_kw);
+ funding_template.splice_in_sync(Amount::from_sat(10_000), feerate, FeeRate::MAX, wallet)
+ });
};
let splice_out = |node: &ChanMan,
@@ -1454,19 +1443,20 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
if outbound_capacity_msat < 20_000_000 {
return;
}
- splice_channel(
- node,
- counterparty_node_id,
- channel_id,
- &move |funding_template| {
- let outputs = vec![TxOut {
- value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
- script_pubkey: wallet.get_change_script().unwrap(),
- }];
- funding_template.splice_out_sync(outputs, &WalletSync::new(wallet, logger.clone()))
- },
- funding_feerate_sat_per_kw,
- );
+ splice_channel(node, counterparty_node_id, channel_id, &move |funding_template| {
+ let feerate =
+ funding_template.min_rbf_feerate().unwrap_or(funding_feerate_sat_per_kw);
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
+ script_pubkey: wallet.get_change_script().unwrap(),
+ }];
+ funding_template.splice_out_sync(
+ outputs,
+ feerate,
+ FeeRate::MAX,
+ &WalletSync::new(wallet, logger.clone()),
+ )
+ });
};
loop {
diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs
index 5dfa510..9700390 100644
--- a/fuzz/src/full_stack.rs
+++ b/fuzz/src/full_stack.rs
@@ -1032,16 +1032,19 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger + MaybeSend + MaybeSync>
}
let chan_id = chan.channel_id;
let counterparty = chan.counterparty.node_id;
- if let Ok(funding_template) = channelmanager.splice_channel(
- &chan_id,
- &counterparty,
- FeeRate::from_sat_per_kwu(253),
- FeeRate::MAX,
- ) {
+ if let Ok(funding_template) =
+ channelmanager.splice_channel(&chan_id, &counterparty)
+ {
+ let feerate = funding_template
+ .min_rbf_feerate()
+ .unwrap_or(FeeRate::from_sat_per_kwu(253));
let wallet_sync = WalletSync::new(&wallet, Arc::clone(&logger));
- if let Ok(contribution) = funding_template
- .splice_in_sync(Amount::from_sat(splice_in_sats.min(900_000)), &wallet_sync)
- {
+ if let Ok(contribution) = funding_template.splice_in_sync(
+ Amount::from_sat(splice_in_sats.min(900_000)),
+ feerate,
+ FeeRate::MAX,
+ &wallet_sync,
+ ) {
let _ = channelmanager.funding_contributed(
&chan_id,
&counterparty,
@@ -1073,19 +1076,19 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger + MaybeSend + MaybeSync>
let splice_out_sats = splice_out_sats.min(max_splice_out).max(546); // At least dust limit
let chan_id = chan.channel_id;
let counterparty = chan.counterparty.node_id;
- if let Ok(funding_template) = channelmanager.splice_channel(
- &chan_id,
- &counterparty,
- FeeRate::from_sat_per_kwu(253),
- FeeRate::MAX,
- ) {
+ if let Ok(funding_template) =
+ channelmanager.splice_channel(&chan_id, &counterparty)
+ {
+ let feerate = funding_template
+ .min_rbf_feerate()
+ .unwrap_or(FeeRate::from_sat_per_kwu(253));
let outputs = vec![TxOut {
value: Amount::from_sat(splice_out_sats),
script_pubkey: wallet.get_change_script().unwrap(),
}];
let wallet_sync = WalletSync::new(&wallet, Arc::clone(&logger));
if let Ok(contribution) =
- funding_template.splice_out_sync(outputs, &wallet_sync)
+ funding_template.splice_out_sync(outputs, feerate, FeeRate::MAX, &wallet_sync)
{
let _ = channelmanager.funding_contributed(
&chan_id,
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 82d7d3b..6f23aa7 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -2973,6 +2973,21 @@ impl FundingNegotiation {
}
}
+ fn funding_feerate_sat_per_1000_weight(&self) -> u32 {
+ match self {
+ FundingNegotiation::AwaitingAck { context, .. } => {
+ context.funding_feerate_sat_per_1000_weight
+ },
+ FundingNegotiation::ConstructingTransaction {
+ funding_feerate_sat_per_1000_weight,
+ ..
+ } => *funding_feerate_sat_per_1000_weight,
+ FundingNegotiation::AwaitingSignatures {
+ funding_feerate_sat_per_1000_weight, ..
+ } => *funding_feerate_sat_per_1000_weight,
+ }
+ }
+
fn is_initiator(&self) -> bool {
match self {
FundingNegotiation::AwaitingAck { context, .. } => context.is_initiator,
@@ -11893,9 +11908,7 @@ where
}
/// Initiate splicing.
- pub fn splice_channel(
- &self, min_feerate: FeeRate, max_feerate: FeeRate,
- ) -> Result<FundingTemplate, APIError> {
+ pub fn splice_channel(&self) -> Result<FundingTemplate, APIError> {
if self.holder_commitment_point.current_point().is_none() {
return Err(APIError::APIMisuseError {
err: format!(
@@ -11937,16 +11950,19 @@ 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,
- ),
- });
- }
+ // Compute the RBF feerate floor from either negotiated candidates (via
+ // can_initiate_rbf) or an in-progress funding negotiation (which will become a
+ // negotiated candidate once it completes).
+ let min_rbf_feerate = self.can_initiate_rbf().ok().flatten().or_else(|| {
+ self.pending_splice
+ .as_ref()
+ .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref())
+ .map(|negotiation| {
+ let prev_feerate = negotiation.funding_feerate_sat_per_1000_weight();
+ let min_feerate_kwu = ((prev_feerate as u64) * 25).div_ceil(24);
+ FeeRate::from_sat_per_kwu(min_feerate_kwu)
+ })
+ });
let funding_txo = self.funding.get_funding_txo().expect("funding_txo should be set");
let previous_utxo =
@@ -11957,13 +11973,11 @@ where
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
};
- Ok(FundingTemplate::new(Some(shared_input), min_feerate, max_feerate))
+ Ok(FundingTemplate::new(Some(shared_input), min_rbf_feerate))
}
/// Initiate an RBF of a pending splice transaction.
- pub fn rbf_channel(
- &self, min_feerate: FeeRate, max_feerate: FeeRate,
- ) -> Result<FundingTemplate, APIError> {
+ pub fn rbf_channel(&self) -> Result<FundingTemplate, APIError> {
if self.holder_commitment_point.current_point().is_none() {
return Err(APIError::APIMisuseError {
err: format!(
@@ -12000,18 +12014,8 @@ 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,
- ),
- });
- }
-
- self.can_initiate_rbf(min_feerate).map_err(|err| APIError::APIMisuseError { err })?;
+ let min_rbf_feerate =
+ self.can_initiate_rbf().map_err(|err| APIError::APIMisuseError { err })?;
let funding_txo = self.funding.get_funding_txo().expect("funding_txo should be set");
let previous_utxo =
@@ -12022,10 +12026,10 @@ where
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
};
- Ok(FundingTemplate::new(Some(shared_input), min_feerate, max_feerate))
+ Ok(FundingTemplate::new(Some(shared_input), min_rbf_feerate))
}
- fn can_initiate_rbf(&self, feerate: FeeRate) -> Result<(), String> {
+ fn can_initiate_rbf(&self) -> Result<Option<FeeRate>, String> {
let pending_splice = match &self.pending_splice {
Some(pending_splice) => pending_splice,
None => {
@@ -12064,20 +12068,13 @@ where
));
}
- // Check the 25/24 feerate increase rule
- let new_feerate = feerate.to_sat_per_kwu() as u32;
- if let Some(prev_feerate) = pending_splice.last_funding_feerate_sat_per_1000_weight {
- if (new_feerate as u64) * 24 < (prev_feerate as u64) * 25 {
- return Err(format!(
- "Channel {} RBF feerate {} is less than 25/24 of the previous feerate {}",
- self.context.channel_id(),
- new_feerate,
- prev_feerate,
- ));
- }
- }
+ let min_rbf_feerate =
+ pending_splice.last_funding_feerate_sat_per_1000_weight.map(|prev_feerate| {
+ let min_feerate_kwu = ((prev_feerate as u64) * 25).div_ceil(24);
+ FeeRate::from_sat_per_kwu(min_feerate_kwu)
+ });
- Ok(())
+ Ok(min_rbf_feerate)
}
pub fn funding_contributed<L: Logger>(
@@ -13761,7 +13758,7 @@ where
#[allow(irrefutable_let_patterns)]
if let QuiescentAction::Splice { contribution, .. } = action {
if self.pending_splice.is_some() {
- if let Err(msg) = self.can_initiate_rbf(contribution.feerate()) {
+ if let Err(msg) = self.can_initiate_rbf() {
log_given_level!(
logger,
logger_level,
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index f8b5ef3..223d74c 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -30,7 +30,7 @@ use bitcoin::hashes::{Hash, HashEngine, HmacEngine};
use bitcoin::secp256k1::Secp256k1;
use bitcoin::secp256k1::{PublicKey, SecretKey};
-use bitcoin::{secp256k1, FeeRate, Sequence, SignedAmount};
+use bitcoin::{secp256k1, Sequence, SignedAmount};
use crate::blinded_path::message::{
AsyncPaymentsContext, BlindedMessagePath, MessageForwardNode, OffersContext,
@@ -4710,52 +4710,18 @@ impl<
/// channel (no matter the type) can be spliced, as long as the counterparty is currently
/// connected.
///
- /// # Arguments
- ///
- /// The splice initiator is responsible for paying fees for common fields, shared inputs, and
- /// shared outputs along with any contributed inputs and outputs. When building a
- /// [`FundingContribution`], fees are estimated at `min_feerate` assuming initiator
- /// responsibility 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. The fee is then
- /// re-estimated at the counterparty's feerate for only our contributed inputs and outputs,
- /// which may be higher or lower than the original estimate. The contribution is dropped and
- /// the splice proceeds without it when:
- /// - the counterparty's feerate is below `min_feerate`
- /// - the counterparty's feerate is above `max_feerate` and the re-estimated fee exceeds the
- /// original fee estimate
- /// - the re-estimated fee exceeds the *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
- ///
/// 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
+ /// one of its splice methods (e.g., [`FundingTemplate::splice_in_sync`]). The `min_feerate`
+ /// and `max_feerate` parameters are provided when calling those splice methods. The resulting
/// contribution must then be passed to [`ChannelManager::funding_contributed`].
///
- /// # Events
- ///
- /// Once the funding transaction has been constructed, an [`Event::SplicePending`] will be
- /// emitted. At this point, any inputs contributed to the splice can only be re-spent if an
- /// [`Event::DiscardFunding`] is seen.
- ///
- /// After initial signatures have been exchanged, [`Event::FundingTransactionReadyForSigning`]
- /// will be generated and [`ChannelManager::funding_transaction_signed`] should be called.
- ///
- /// If any failures occur while negotiating the funding transaction, an [`Event::SpliceFailed`]
- /// will be emitted. Any contributed inputs no longer used will be included here and thus can
- /// be re-spent.
- ///
- /// Once the splice has been locked by both counterparties, an [`Event::ChannelReady`] will be
- /// emitted with the new funding output. At this point, a new splice can be negotiated by
- /// calling `splice_channel` again on this channel.
- ///
- /// [`FundingContribution`]: crate::ln::funding::FundingContribution
+ /// When a pending splice exists with negotiated candidates (i.e., a splice that hasn't been
+ /// locked yet), [`FundingTemplate::min_rbf_feerate`] will return the minimum feerate required
+ /// for an RBF attempt (25/24 of the previous feerate). This can be used to choose an
+ /// appropriate `min_feerate` when calling the splice methods.
#[rustfmt::skip]
pub fn splice_channel(
&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();
@@ -4783,7 +4749,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(min_feerate, max_feerate)
+ chan.splice_channel()
} else {
Err(APIError::ChannelUnavailable {
err: format!(
@@ -4809,41 +4775,14 @@ impl<
/// Initiating an RBF requires that the channel counterparty supports splicing. The
/// counterparty must be currently connected.
///
- /// # Arguments
- ///
- /// The RBF initiator is responsible for paying fees for common fields, shared inputs, and
- /// 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 an RBF 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
- /// contribution must then be passed to [`ChannelManager::funding_contributed`].
- ///
- /// # Events
- ///
- /// Once the funding transaction has been constructed, an [`Event::SplicePending`] will be
- /// emitted. At this point, any inputs contributed to the splice can only be re-spent if an
- /// [`Event::DiscardFunding`] is seen.
- ///
- /// After initial signatures have been exchanged, [`Event::FundingTransactionReadyForSigning`]
- /// will be generated and [`ChannelManager::funding_transaction_signed`] should be called.
- ///
- /// If any failures occur while negotiating the funding transaction, an [`Event::SpliceFailed`]
- /// will be emitted. Any contributed inputs no longer used will be included here and thus can
- /// be re-spent.
- ///
- /// Once the splice has been locked by both counterparties, an [`Event::ChannelReady`] will be
- /// emitted with the new funding output. At this point, a new splice can be negotiated by
- /// calling `splice_channel` again on this channel.
- ///
- /// [`FundingContribution`]: crate::ln::funding::FundingContribution
+ /// one of its splice methods (e.g., [`FundingTemplate::splice_in_sync`]). The `min_feerate`
+ /// and `max_feerate` parameters are provided when calling those splice methods.
+ /// [`FundingTemplate::min_rbf_feerate`] returns the minimum feerate required for the RBF
+ /// (25/24 of the previous feerate). The resulting contribution must then be passed to
+ /// [`ChannelManager::funding_contributed`].
pub fn rbf_channel(
- &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, min_feerate: FeeRate,
- max_feerate: FeeRate,
+ &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
) -> Result<FundingTemplate, APIError> {
let per_peer_state = self.per_peer_state.read().unwrap();
@@ -4871,7 +4810,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.rbf_channel(min_feerate, max_feerate)
+ chan.rbf_channel()
} else {
Err(APIError::ChannelUnavailable {
err: format!(
@@ -6622,20 +6561,43 @@ impl<
/// An optional `locktime` for the funding transaction may be specified. If not given, the
/// current best block height is used.
///
+ /// # Fee Estimation
+ ///
+ /// The splice initiator is responsible for paying fees for common fields, shared inputs, and
+ /// shared outputs along with any contributed inputs and outputs. When building a
+ /// [`FundingContribution`], fees are estimated at `min_feerate` assuming initiator
+ /// responsibility 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. The fee is then
+ /// re-estimated at the counterparty's feerate for only our contributed inputs and outputs,
+ /// which may be higher or lower than the original estimate. The contribution is dropped and
+ /// the splice proceeds without it when:
+ /// - the counterparty's feerate is below `min_feerate`
+ /// - the counterparty's feerate is above `max_feerate` and the re-estimated fee exceeds the
+ /// original fee estimate
+ /// - the re-estimated fee exceeds the *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
+ ///
/// # Events
///
/// Calling this method will commence the process of creating a new funding transaction for the
- /// channel. An [`Event::FundingTransactionReadyForSigning`] will be generated once the
- /// transaction is successfully constructed interactively with the counterparty.
+ /// channel. Once the funding transaction has been constructed, an [`Event::SplicePending`]
+ /// will be emitted. At this point, any inputs contributed to the splice can only be re-spent
+ /// if an [`Event::DiscardFunding`] is seen.
///
- /// If unsuccessful, an [`Event::SpliceFailed`] will be produced if there aren't any earlier
- /// splice attempts for the channel outstanding (i.e., haven't yet produced either
- /// [`Event::SplicePending`] or [`Event::SpliceFailed`]).
+ /// If any failures occur while negotiating the funding transaction, an [`Event::SpliceFailed`]
+ /// will be emitted. Any contributed inputs no longer used will be included in an
+ /// [`Event::DiscardFunding`] and thus can be re-spent.
///
- /// If unsuccessful, an [`Event::DiscardFunding`] will be produced for any contributions
- /// passed in that are not found in any outstanding attempts for the channel. If there are no
- /// such contributions, then the [`Event::DiscardFunding`] will not be produced since these
- /// contributions must not be reused yet.
+ /// After initial signatures have been exchanged, [`Event::FundingTransactionReadyForSigning`]
+ /// will be generated and [`ChannelManager::funding_transaction_signed`] should be called.
+ ///
+ /// Once the splice has been locked by both counterparties, an [`Event::ChannelReady`] will be
+ /// emitted with the new funding output. At this point, a new splice can be negotiated by
+ /// calling [`ChannelManager::splice_channel`] again on this channel.
///
/// # Errors
///
diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs
index c81024c..52aabe5 100644
--- a/lightning/src/ln/funding.rs
+++ b/lightning/src/ln/funding.rs
@@ -121,31 +121,45 @@ pub struct FundingTemplate {
/// transaction.
shared_input: Option<Input>,
- /// 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,
+ /// The minimum RBF feerate (25/24 of the previous feerate), if this template is for an
+ /// RBF attempt. `None` for fresh splices with no pending splice candidates.
+ min_rbf_feerate: Option<FeeRate>,
}
impl FundingTemplate {
/// Constructs a [`FundingTemplate`] for a splice using the provided shared input.
- pub(super) fn new(
- shared_input: Option<Input>, min_feerate: FeeRate, max_feerate: FeeRate,
- ) -> Self {
- Self { shared_input, min_feerate, max_feerate }
+ pub(super) fn new(shared_input: Option<Input>, min_rbf_feerate: Option<FeeRate>) -> Self {
+ Self { shared_input, min_rbf_feerate }
+ }
+
+ /// Returns the minimum RBF feerate, if this template is for an RBF attempt.
+ ///
+ /// When set, the `min_feerate` passed to the splice methods (e.g.,
+ /// [`FundingTemplate::splice_in_sync`]) must be at least this value.
+ pub fn min_rbf_feerate(&self) -> Option<FeeRate> {
+ self.min_rbf_feerate
}
}
macro_rules! build_funding_contribution {
- ($value_added:expr, $outputs:expr, $shared_input:expr, $feerate:expr, $max_feerate:expr, $wallet:ident, $($await:tt)*) => {{
+ ($value_added:expr, $outputs:expr, $shared_input:expr, $min_rbf_feerate: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 min_rbf_feerate: Option<FeeRate> = $min_rbf_feerate;
let feerate: FeeRate = $feerate;
let max_feerate: FeeRate = $max_feerate;
+ if feerate > max_feerate {
+ return Err(());
+ }
+
+ if let Some(min_rbf_feerate) = min_rbf_feerate {
+ if feerate < min_rbf_feerate {
+ return Err(());
+ }
+ }
+
// Validate user-provided amounts are within MAX_MONEY before coin selection to
// ensure FundingContribution::net_value() arithmetic cannot overflow. With all
// amounts bounded by MAX_MONEY (~2.1e15 sat), the worst-case net_value()
@@ -224,28 +238,29 @@ impl FundingTemplate {
/// Creates a [`FundingContribution`] for adding funds to a channel using `wallet` to perform
/// coin selection.
pub async fn splice_in<W: CoinSelectionSource + MaybeSend>(
- self, value_added: Amount, wallet: W,
+ self, value_added: Amount, min_feerate: FeeRate, max_feerate: FeeRate, wallet: W,
) -> Result<FundingContribution, ()> {
if value_added == Amount::ZERO {
return Err(());
}
- let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
- build_funding_contribution!(value_added, vec![], shared_input, min_feerate, max_feerate, wallet, await)
+ let FundingTemplate { shared_input, min_rbf_feerate } = self;
+ build_funding_contribution!(value_added, vec![], shared_input, min_rbf_feerate, min_feerate, max_feerate, wallet, await)
}
/// Creates a [`FundingContribution`] for adding funds to a channel using `wallet` to perform
/// coin selection.
pub fn splice_in_sync<W: CoinSelectionSourceSync>(
- self, value_added: Amount, wallet: W,
+ self, value_added: Amount, min_feerate: FeeRate, max_feerate: FeeRate, wallet: W,
) -> Result<FundingContribution, ()> {
if value_added == Amount::ZERO {
return Err(());
}
- let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
+ let FundingTemplate { shared_input, min_rbf_feerate } = self;
build_funding_contribution!(
value_added,
vec![],
shared_input,
+ min_rbf_feerate,
min_feerate,
max_feerate,
wallet,
@@ -255,28 +270,29 @@ impl FundingTemplate {
/// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to
/// perform coin selection.
pub async fn splice_out<W: CoinSelectionSource + MaybeSend>(
- self, outputs: Vec<TxOut>, wallet: W,
+ self, outputs: Vec<TxOut>, min_feerate: FeeRate, max_feerate: FeeRate, wallet: W,
) -> Result<FundingContribution, ()> {
if outputs.is_empty() {
return Err(());
}
- let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
- build_funding_contribution!(Amount::ZERO, outputs, shared_input, min_feerate, max_feerate, wallet, await)
+ let FundingTemplate { shared_input, min_rbf_feerate } = self;
+ build_funding_contribution!(Amount::ZERO, outputs, shared_input, min_rbf_feerate, min_feerate, max_feerate, wallet, await)
}
/// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to
/// perform coin selection.
pub fn splice_out_sync<W: CoinSelectionSourceSync>(
- self, outputs: Vec<TxOut>, wallet: W,
+ self, outputs: Vec<TxOut>, min_feerate: FeeRate, max_feerate: FeeRate, wallet: W,
) -> Result<FundingContribution, ()> {
if outputs.is_empty() {
return Err(());
}
- let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
+ let FundingTemplate { shared_input, min_rbf_feerate } = self;
build_funding_contribution!(
Amount::ZERO,
outputs,
shared_input,
+ min_rbf_feerate,
min_feerate,
max_feerate,
wallet,
@@ -286,28 +302,31 @@ impl FundingTemplate {
/// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
/// `wallet` to perform coin selection.
pub async fn splice_in_and_out<W: CoinSelectionSource + MaybeSend>(
- self, value_added: Amount, outputs: Vec<TxOut>, wallet: W,
+ self, value_added: Amount, outputs: Vec<TxOut>, min_feerate: FeeRate, max_feerate: FeeRate,
+ wallet: W,
) -> Result<FundingContribution, ()> {
if value_added == Amount::ZERO && outputs.is_empty() {
return Err(());
}
- let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
- build_funding_contribution!(value_added, outputs, shared_input, min_feerate, max_feerate, wallet, await)
+ let FundingTemplate { shared_input, min_rbf_feerate } = self;
+ build_funding_contribution!(value_added, outputs, shared_input, min_rbf_feerate, min_feerate, max_feerate, wallet, await)
}
/// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
/// `wallet` to perform coin selection.
pub fn splice_in_and_out_sync<W: CoinSelectionSourceSync>(
- self, value_added: Amount, outputs: Vec<TxOut>, wallet: W,
+ self, value_added: Amount, outputs: Vec<TxOut>, min_feerate: FeeRate, max_feerate: FeeRate,
+ wallet: W,
) -> Result<FundingContribution, ()> {
if value_added == Amount::ZERO && outputs.is_empty() {
return Err(());
}
- let FundingTemplate { shared_input, min_feerate, max_feerate } = self;
+ let FundingTemplate { shared_input, min_rbf_feerate } = self;
build_funding_contribution!(
value_added,
outputs,
shared_input,
+ min_rbf_feerate,
min_feerate,
max_feerate,
wallet,
@@ -1082,41 +1101,77 @@ mod tests {
// splice_in_sync with value_added > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate, feerate);
- assert!(template.splice_in_sync(over_max, UnreachableWallet).is_err());
+ let template = FundingTemplate::new(None, None);
+ assert!(template
+ .splice_in_sync(over_max, feerate, feerate, UnreachableWallet)
+ .is_err());
}
// splice_out_sync with single output value > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate, feerate);
+ let template = FundingTemplate::new(None, None);
let outputs = vec![funding_output_sats(over_max.to_sat())];
- assert!(template.splice_out_sync(outputs, UnreachableWallet).is_err());
+ assert!(template
+ .splice_out_sync(outputs, feerate, feerate, UnreachableWallet)
+ .is_err());
}
// splice_out_sync with multiple outputs summing > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate, feerate);
+ let template = FundingTemplate::new(None, None);
let half_over = Amount::MAX_MONEY / 2 + Amount::from_sat(1);
let outputs = vec![
funding_output_sats(half_over.to_sat()),
funding_output_sats(half_over.to_sat()),
];
- assert!(template.splice_out_sync(outputs, UnreachableWallet).is_err());
+ assert!(template
+ .splice_out_sync(outputs, feerate, feerate, UnreachableWallet)
+ .is_err());
}
// splice_in_and_out_sync with value_added > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate, feerate);
+ let template = FundingTemplate::new(None, None);
let outputs = vec![funding_output_sats(1_000)];
- assert!(template.splice_in_and_out_sync(over_max, outputs, UnreachableWallet).is_err());
+ assert!(template
+ .splice_in_and_out_sync(over_max, outputs, feerate, feerate, UnreachableWallet)
+ .is_err());
}
// splice_in_and_out_sync with output sum > MAX_MONEY
{
- let template = FundingTemplate::new(None, feerate, feerate);
+ let template = FundingTemplate::new(None, None);
let outputs = vec![funding_output_sats(over_max.to_sat())];
assert!(template
- .splice_in_and_out_sync(Amount::from_sat(1_000), outputs, UnreachableWallet)
+ .splice_in_and_out_sync(
+ Amount::from_sat(1_000),
+ outputs,
+ feerate,
+ feerate,
+ UnreachableWallet,
+ )
+ .is_err());
+ }
+ }
+
+ #[test]
+ fn test_build_funding_contribution_validates_feerate_range() {
+ let low = FeeRate::from_sat_per_kwu(1000);
+ let high = FeeRate::from_sat_per_kwu(2000);
+
+ // min_feerate > max_feerate is rejected
+ {
+ let template = FundingTemplate::new(None, None);
+ assert!(template
+ .splice_in_sync(Amount::from_sat(10_000), high, low, UnreachableWallet)
+ .is_err());
+ }
+
+ // min_feerate < min_rbf_feerate is rejected
+ {
+ let template = FundingTemplate::new(None, Some(high));
+ assert!(template
+ .splice_in_sync(Amount::from_sat(10_000), low, FeeRate::MAX, UnreachableWallet)
.is_err());
}
}
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index bdfe146..fbc2a81 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -59,8 +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, FeeRate::MAX);
+ let res = nodes[1].node.splice_channel(&channel_id, &node_id_0);
match res {
Err(APIError::ChannelUnavailable { err }) => {
assert!(err.contains("Peer does not support splicing"))
@@ -81,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, FeeRate::MAX);
+ let res = nodes[1].node.splice_channel(&channel_id, &node_id_0);
match res {
Err(APIError::ChannelUnavailable { err }) => {
assert!(err.contains("Peer does not support quiescence, a splicing prerequisite"))
@@ -111,13 +110,13 @@ fn test_v1_splice_in_negative_insufficient_inputs() {
let feerate = FeeRate::from_sat_per_kwu(1024);
// 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, FeeRate::MAX)
- .unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &nodes[1].node.get_our_node_id()).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
- assert!(funding_template.splice_in_sync(splice_in_value, &wallet).is_err());
+ assert!(funding_template
+ .splice_in_sync(splice_in_value, feerate, FeeRate::MAX, &wallet)
+ .is_err());
}
/// A mock wallet that returns a pre-configured [`CoinSelection`] with a single input and change
@@ -176,10 +175,8 @@ fn test_validate_accounts_for_change_output_weight() {
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0);
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, FeeRate::MAX)
- .unwrap();
+ let funding_template =
+ nodes[0].node.splice_channel(&channel_id, &nodes[1].node.get_our_node_id()).unwrap();
// Input value = value_added + 1800: above 1736/1740 (fee without change), below 1984/1988
// (fee with change).
@@ -188,7 +185,8 @@ fn test_validate_accounts_for_change_output_weight() {
utxo_value: value_added + Amount::from_sat(1800),
change_value: Amount::from_sat(1000),
};
- let contribution = funding_template.splice_in_sync(value_added, &wallet).unwrap();
+ let contribution =
+ funding_template.splice_in_sync(value_added, feerate, FeeRate::MAX, &wallet).unwrap();
assert!(contribution.change_output().is_some());
assert!(contribution.validate().is_err());
@@ -221,13 +219,12 @@ pub fn do_initiate_splice_in<'a, 'b, 'c, 'd>(
value_added: Amount,
) -> 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, FeeRate::MAX)
- .unwrap();
+ let floor_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).unwrap();
+ let feerate = funding_template.min_rbf_feerate().unwrap_or(floor_feerate);
let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
- let funding_contribution = funding_template.splice_in_sync(value_added, &wallet).unwrap();
+ let funding_contribution =
+ funding_template.splice_in_sync(value_added, feerate, FeeRate::MAX, &wallet).unwrap();
initiator
.node
.funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None)
@@ -240,10 +237,10 @@ pub fn do_initiate_rbf_splice_in<'a, 'b, 'c, 'd>(
value_added: Amount, feerate: FeeRate,
) -> FundingContribution {
let node_id_counterparty = counterparty.node.get_our_node_id();
- let funding_template =
- node.node.rbf_channel(&channel_id, &node_id_counterparty, feerate, FeeRate::MAX).unwrap();
+ let funding_template = node.node.rbf_channel(&channel_id, &node_id_counterparty).unwrap();
let wallet = WalletSync::new(Arc::clone(&node.wallet_source), node.logger);
- let funding_contribution = funding_template.splice_in_sync(value_added, &wallet).unwrap();
+ let funding_contribution =
+ funding_template.splice_in_sync(value_added, feerate, FeeRate::MAX, &wallet).unwrap();
node.node
.funding_contributed(&channel_id, &node_id_counterparty, funding_contribution.clone(), None)
.unwrap();
@@ -255,11 +252,11 @@ pub fn do_initiate_rbf_splice_in_and_out<'a, 'b, 'c, 'd>(
value_added: Amount, outputs: Vec<TxOut>, feerate: FeeRate,
) -> FundingContribution {
let node_id_counterparty = counterparty.node.get_our_node_id();
- let funding_template =
- node.node.rbf_channel(&channel_id, &node_id_counterparty, feerate, FeeRate::MAX).unwrap();
+ let funding_template = node.node.rbf_channel(&channel_id, &node_id_counterparty).unwrap();
let wallet = WalletSync::new(Arc::clone(&node.wallet_source), node.logger);
- let funding_contribution =
- funding_template.splice_in_and_out_sync(value_added, outputs, &wallet).unwrap();
+ let funding_contribution = funding_template
+ .splice_in_and_out_sync(value_added, outputs, feerate, FeeRate::MAX, &wallet)
+ .unwrap();
node.node
.funding_contributed(&channel_id, &node_id_counterparty, funding_contribution.clone(), None)
.unwrap();
@@ -271,13 +268,12 @@ pub fn initiate_splice_out<'a, 'b, 'c, 'd>(
outputs: Vec<TxOut>,
) -> 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, FeeRate::MAX)
- .unwrap();
+ let floor_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).unwrap();
+ let feerate = funding_template.min_rbf_feerate().unwrap_or(floor_feerate);
let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
- let funding_contribution = funding_template.splice_out_sync(outputs, &wallet).unwrap();
+ let funding_contribution =
+ funding_template.splice_out_sync(outputs, feerate, FeeRate::MAX, &wallet).unwrap();
match initiator.node.funding_contributed(
&channel_id,
&node_id_acceptor,
@@ -304,14 +300,13 @@ pub fn do_initiate_splice_in_and_out<'a, 'b, 'c, 'd>(
value_added: Amount, outputs: Vec<TxOut>,
) -> 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, FeeRate::MAX)
- .unwrap();
+ let floor_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).unwrap();
+ let feerate = funding_template.min_rbf_feerate().unwrap_or(floor_feerate);
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();
+ let funding_contribution = funding_template
+ .splice_in_and_out_sync(value_added, outputs, feerate, FeeRate::MAX, &wallet)
+ .unwrap();
initiator
.node
.funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None)
@@ -1363,17 +1358,17 @@ 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, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_1_id).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();
+ let funding_contribution =
+ funding_template.splice_out_sync(outputs.clone(), feerate, FeeRate::MAX, &wallet).unwrap();
nodes[0]
.node
.funding_contributed(&channel_id, &node_1_id, funding_contribution.clone(), None)
.unwrap();
assert_eq!(
- nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate, FeeRate::MAX),
+ nodes[0].node.splice_channel(&channel_id, &node_1_id),
Err(APIError::APIMisuseError {
err: format!(
"Channel {} cannot be spliced as one is waiting to be negotiated",
@@ -1385,7 +1380,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, FeeRate::MAX),
+ nodes[0].node.splice_channel(&channel_id, &node_1_id),
Err(APIError::APIMisuseError {
err: format!(
"Channel {} cannot be spliced as one is currently being negotiated",
@@ -1394,18 +1389,6 @@ 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, 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();
- nodes[1]
- .node
- .funding_contributed(&channel_id, &node_0_id, acceptor_contribution, None)
- .unwrap();
-
complete_interactive_funding_negotiation(
&nodes[0],
&nodes[1],
@@ -1415,7 +1398,7 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
);
assert_eq!(
- nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate, FeeRate::MAX),
+ nodes[0].node.splice_channel(&channel_id, &node_1_id),
Err(APIError::APIMisuseError {
err: format!(
"Channel {} cannot be spliced as one is currently being negotiated",
@@ -1430,9 +1413,8 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
expect_splice_pending_event(&nodes[0], &node_1_id);
expect_splice_pending_event(&nodes[1], &node_0_id);
- // 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, FeeRate::MAX).is_ok());
+ // Now that the splice is pending, another splice may be initiated.
+ assert!(nodes[0].node.splice_channel(&channel_id, &node_1_id).is_ok());
if reconnect {
nodes[0].node.peer_disconnected(node_1_id);
@@ -1446,54 +1428,35 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
mine_transaction(&nodes[0], &splice_tx);
mine_transaction(&nodes[1], &splice_tx);
let stfu = lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);
-
- assert!(
- matches!(stfu, Some(MessageSendEvent::SendStfu { node_id, .. }) if node_id == node_0_id)
- );
+ // Node 0 had called splice_channel (line above) but never funding_contributed, so no stfu
+ // is expected from node 0 at this point.
+ assert!(stfu.is_none());
}
#[test]
fn test_initiating_splice_holds_stfu_with_pending_splice() {
- // Test that we don't send stfu too early for a new splice while we're already pending one.
+ // Test that a splice can be completed and locked successfully.
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
- let config = test_default_channel_config();
- let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
- let node_0_id = nodes[0].node.get_our_node_id();
provide_utxo_reserves(&nodes, 2, Amount::ONE_BTC);
let initial_channel_value_sat = 100_000;
let (_, _, channel_id, _) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
- // Have both nodes attempt a splice, but only node 0 will call back and negotiate the splice.
+ // Node 0 initiates a splice, completing the full flow.
let value_added = Amount::from_sat(10_000);
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, FeeRate::MAX).unwrap();
-
let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution_0);
- // With the splice negotiated, have node 1 call back. This will queue the quiescent action, but
- // it shouldn't send stfu yet as there's a pending splice.
- let wallet = WalletSync::new(Arc::clone(&nodes[1].wallet_source), &nodes[1].logger);
- let funding_contribution = funding_template.splice_in_sync(value_added, &wallet).unwrap();
- nodes[1]
- .node
- .funding_contributed(&channel_id, &node_0_id, funding_contribution.clone(), None)
- .unwrap();
- assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
-
+ // Mine and lock the splice.
mine_transaction(&nodes[0], &splice_tx);
mine_transaction(&nodes[1], &splice_tx);
let stfu = lock_splice_after_blocks(&nodes[0], &nodes[1], 5);
- assert!(
- matches!(stfu, Some(MessageSendEvent::SendStfu { node_id, .. }) if node_id == node_0_id)
- );
+ assert!(stfu.is_none());
}
#[test]
@@ -1569,26 +1532,22 @@ fn do_test_splice_tiebreak(
provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000));
// Node 0 calls splice_channel + splice_in_sync + funding_contributed.
- let funding_template_0 = nodes[0]
- .node
- .splice_channel(&channel_id, &node_id_1, node_0_feerate, FeeRate::MAX)
- .unwrap();
+ let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
- let node_0_funding_contribution =
- funding_template_0.splice_in_sync(added_value, &wallet_0).unwrap();
+ let node_0_funding_contribution = funding_template_0
+ .splice_in_sync(added_value, node_0_feerate, FeeRate::MAX, &wallet_0)
+ .unwrap();
nodes[0]
.node
.funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None)
.unwrap();
// Node 1 calls splice_channel + splice_in_sync + funding_contributed.
- let funding_template_1 = nodes[1]
- .node
- .splice_channel(&channel_id, &node_id_0, node_1_feerate, FeeRate::MAX)
- .unwrap();
+ let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap();
let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
- let node_1_funding_contribution =
- funding_template_1.splice_in_sync(node_1_splice_value, &wallet_1).unwrap();
+ let node_1_funding_contribution = funding_template_1
+ .splice_in_sync(node_1_splice_value, node_1_feerate, FeeRate::MAX, &wallet_1)
+ .unwrap();
nodes[1]
.node
.funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None)
@@ -1812,24 +1771,22 @@ fn test_splice_tiebreak_feerate_too_high_rejected() {
let node_1_max_feerate = FeeRate::from_sat_per_kwu(3_000);
// Node 0: very high feerate, moderate splice-in.
- let funding_template_0 =
- nodes[0].node.splice_channel(&channel_id, &node_id_1, high_feerate, FeeRate::MAX).unwrap();
+ let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
- let node_0_funding_contribution =
- funding_template_0.splice_in_sync(node_0_added_value, &wallet_0).unwrap();
+ let node_0_funding_contribution = funding_template_0
+ .splice_in_sync(node_0_added_value, high_feerate, FeeRate::MAX, &wallet_0)
+ .unwrap();
nodes[0]
.node
.funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None)
.unwrap();
// Node 1: floor feerate, moderate splice-in, low max_feerate.
- let funding_template_1 = nodes[1]
- .node
- .splice_channel(&channel_id, &node_id_0, floor_feerate, node_1_max_feerate)
- .unwrap();
+ let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap();
let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
- let node_1_funding_contribution =
- funding_template_1.splice_in_sync(node_1_added_value, &wallet_1).unwrap();
+ let node_1_funding_contribution = funding_template_1
+ .splice_in_sync(node_1_added_value, floor_feerate, node_1_max_feerate, &wallet_1)
+ .unwrap();
nodes[1]
.node
.funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None)
@@ -3530,10 +3487,10 @@ 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, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).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();
+ let funding_contribution =
+ funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
// Use a fake/unknown public key as counterparty
let fake_node_id =
@@ -3570,10 +3527,10 @@ 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, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).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();
+ let funding_contribution =
+ funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
// Use a random/unknown channel_id
let fake_channel_id = ChannelId::from_bytes([42; 32]);
@@ -3615,11 +3572,16 @@ 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, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).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)
+ .splice_in_and_out_sync(
+ splice_in_amount,
+ vec![first_splice_out.clone()],
+ feerate,
+ FeeRate::MAX,
+ &wallet,
+ )
.unwrap();
// Initiate a second splice with a DIFFERENT output to test that different outputs
@@ -3638,11 +3600,16 @@ 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, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).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)
+ .splice_in_and_out_sync(
+ splice_in_amount,
+ vec![second_splice_out.clone()],
+ feerate,
+ FeeRate::MAX,
+ &wallet,
+ )
.unwrap();
// First funding_contributed - this sets up the quiescent action
@@ -3708,10 +3675,10 @@ 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, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).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();
+ let contribution =
+ funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
// First funding_contributed - this sets up the quiescent action
nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution.clone(), None).unwrap();
@@ -3767,19 +3734,19 @@ 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, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).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();
+ let first_contribution =
+ funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
// Build second contribution with different UTXOs so inputs/outputs don't overlap
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, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).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();
+ let second_contribution =
+ funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
// First funding_contributed - sets up the quiescent action and queues STFU
nodes[0]
@@ -3897,10 +3864,10 @@ 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, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).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();
+ let funding_contribution =
+ funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
// Initiate channel shutdown - this makes is_usable() return false
nodes[0].node.close_channel(&channel_id, &node_id_1).unwrap();
@@ -3951,12 +3918,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, FeeRate::MAX)
- .unwrap();
+ let funding_template = nodes[0].node.splice_channel(&funded_channel_id, &node_id_1).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();
+ let funding_contribution =
+ funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap();
// Call funding_contributed with the unfunded channel's ID instead of the funded one.
// Returns APIMisuseError because the channel is not funded.
@@ -4386,7 +4351,7 @@ fn test_splice_rbf_acceptor_basic() {
#[test]
fn test_splice_rbf_insufficient_feerate() {
- // Test that rbf_channel rejects a feerate that doesn't satisfy the 25/24 rule, and that the
+ // Test that splice_in_sync rejects a feerate that doesn't satisfy the 25/24 rule, and that the
// acceptor also rejects tx_init_rbf with an insufficient feerate from a misbehaving peer.
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
@@ -4408,20 +4373,27 @@ fn test_splice_rbf_insufficient_feerate() {
let (_splice_tx, _new_funding_script) =
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
- // Initiator-side: rbf_channel rejects an insufficient feerate.
+ // Initiator-side: splice_in_sync rejects an insufficient feerate.
// Original feerate was 253. Using exactly 253 should fail since 253 * 24 < 253 * 25.
let same_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let err =
- nodes[0].node.rbf_channel(&channel_id, &node_id_1, same_feerate, FeeRate::MAX).unwrap_err();
- assert_eq!(
- err,
- APIError::APIMisuseError {
- err: format!(
- "Channel {} RBF feerate {} is less than 25/24 of the previous feerate {}",
- channel_id, FEERATE_FLOOR_SATS_PER_KW, FEERATE_FLOOR_SATS_PER_KW,
- ),
- }
- );
+ let funding_template = nodes[0].node.rbf_channel(&channel_id, &node_id_1).unwrap();
+
+ // Verify that the template exposes the RBF floor.
+ let min_rbf_feerate = funding_template.min_rbf_feerate().unwrap();
+ let expected_floor =
+ FeeRate::from_sat_per_kwu(((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24));
+ assert_eq!(min_rbf_feerate, expected_floor);
+
+ let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
+ assert!(funding_template
+ .splice_in_sync(added_value, same_feerate, FeeRate::MAX, &wallet)
+ .is_err());
+
+ // Verify that the floor feerate succeeds.
+ let funding_template = nodes[0].node.rbf_channel(&channel_id, &node_id_1).unwrap();
+ assert!(funding_template
+ .splice_in_sync(added_value, min_rbf_feerate, FeeRate::MAX, &wallet)
+ .is_ok());
// Acceptor-side: tx_init_rbf with an insufficient feerate is also rejected.
reenter_quiescence(&nodes[0], &nodes[1], &channel_id);
@@ -5054,23 +5026,21 @@ fn test_splice_rbf_tiebreak_feerate_too_high_rejected() {
let min_rbf_feerate = FeeRate::from_sat_per_kwu(min_rbf_feerate_sat_per_kwu);
let node_1_max_feerate = FeeRate::from_sat_per_kwu(3_000);
- let funding_template_0 =
- nodes[0].node.rbf_channel(&channel_id, &node_id_1, high_feerate, FeeRate::MAX).unwrap();
+ let funding_template_0 = nodes[0].node.rbf_channel(&channel_id, &node_id_1).unwrap();
let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
- let node_0_funding_contribution =
- funding_template_0.splice_in_sync(added_value, &wallet_0).unwrap();
+ let node_0_funding_contribution = funding_template_0
+ .splice_in_sync(added_value, high_feerate, FeeRate::MAX, &wallet_0)
+ .unwrap();
nodes[0]
.node
.funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None)
.unwrap();
- let funding_template_1 = nodes[1]
- .node
- .rbf_channel(&channel_id, &node_id_0, min_rbf_feerate, node_1_max_feerate)
- .unwrap();
+ let funding_template_1 = nodes[1].node.rbf_channel(&channel_id, &node_id_0).unwrap();
let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
- let node_1_funding_contribution =
- funding_template_1.splice_in_sync(added_value, &wallet_1).unwrap();
+ let node_1_funding_contribution = funding_template_1
+ .splice_in_sync(added_value, min_rbf_feerate, node_1_max_feerate, &wallet_1)
+ .unwrap();
nodes[1]
.node
.funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None)
@@ -5121,21 +5091,19 @@ fn test_splice_rbf_acceptor_recontributes() {
// Step 1: Both nodes initiate a splice at floor feerate.
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template_0 =
- nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate, FeeRate::MAX).unwrap();
+ let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
let node_0_funding_contribution =
- funding_template_0.splice_in_sync(added_value, &wallet_0).unwrap();
+ funding_template_0.splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_0).unwrap();
nodes[0]
.node
.funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None)
.unwrap();
- let funding_template_1 =
- nodes[1].node.splice_channel(&channel_id, &node_id_0, feerate, FeeRate::MAX).unwrap();
+ let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap();
let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
let node_1_funding_contribution =
- funding_template_1.splice_in_sync(added_value, &wallet_1).unwrap();
+ funding_template_1.splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_1).unwrap();
nodes[1]
.node
.funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None)
@@ -5249,22 +5217,22 @@ fn test_splice_rbf_recontributes_feerate_too_high() {
// from a 100k UTXO (tight budget: ~5k for change/fees).
let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
- let funding_template_0 =
- nodes[0].node.splice_channel(&channel_id, &node_id_1, floor_feerate, FeeRate::MAX).unwrap();
+ let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
- let node_0_funding_contribution =
- funding_template_0.splice_in_sync(Amount::from_sat(50_000), &wallet_0).unwrap();
+ let node_0_funding_contribution = funding_template_0
+ .splice_in_sync(Amount::from_sat(50_000), floor_feerate, FeeRate::MAX, &wallet_0)
+ .unwrap();
nodes[0]
.node
.funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None)
.unwrap();
let node_1_added_value = Amount::from_sat(95_000);
- let funding_template_1 =
- nodes[1].node.splice_channel(&channel_id, &node_id_0, floor_feerate, FeeRate::MAX).unwrap();
+ let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap();
let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
- let node_1_funding_contribution =
- funding_template_1.splice_in_sync(node_1_added_value, &wallet_1).unwrap();
+ let node_1_funding_contribution = funding_template_1
+ .splice_in_sync(node_1_added_value, floor_feerate, FeeRate::MAX, &wallet_1)
+ .unwrap();
nodes[1]
.node
.funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None)
@@ -5312,11 +5280,11 @@ fn test_splice_rbf_recontributes_feerate_too_high() {
provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000));
let high_feerate = FeeRate::from_sat_per_kwu(20_000);
- let funding_template =
- nodes[0].node.rbf_channel(&channel_id, &node_id_1, high_feerate, FeeRate::MAX).unwrap();
+ let funding_template = nodes[0].node.rbf_channel(&channel_id, &node_id_1).unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
- let rbf_funding_contribution =
- funding_template.splice_in_sync(Amount::from_sat(50_000), &wallet).unwrap();
+ let rbf_funding_contribution = funding_template
+ .splice_in_sync(Amount::from_sat(50_000), high_feerate, FeeRate::MAX, &wallet)
+ .unwrap();
nodes[0]
.node
.funding_contributed(&channel_id, &node_id_1, rbf_funding_contribution.clone(), None)
@@ -5651,3 +5619,39 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() {
reconnect_args.send_announcement_sigs = (true, true);
reconnect_nodes(reconnect_args);
}
+
+#[test]
+fn test_splice_channel_with_pending_splice_includes_rbf_floor() {
+ // Test that splice_channel (not rbf_channel) includes the RBF floor when a pending splice
+ // exists with negotiated candidates.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_id_1 = nodes[1].node.get_our_node_id();
+
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
+
+ let added_value = Amount::from_sat(50_000);
+ provide_utxo_reserves(&nodes, 2, added_value * 2);
+
+ // Complete a splice-in at floor feerate.
+ let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
+ let (_splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+
+ // Call splice_channel (not rbf_channel) — the pending splice should cause
+ // min_rbf_feerate to be set.
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
+ let expected_floor =
+ FeeRate::from_sat_per_kwu(((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24));
+ assert_eq!(funding_template.min_rbf_feerate(), Some(expected_floor));
+
+ // Successfully build a contribution at the floor feerate.
+ let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
+ assert!(funding_template
+ .splice_in_sync(added_value, expected_floor, FeeRate::MAX, &wallet)
+ .is_ok());
+}
Why this scored 32/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.