Skip to content

Commit 27a8dbb

Browse files
authored
Merge pull request #4494 from jkczyz/2026-03-rbf-feerate
Ensure minimum RBF feerate satisfies BIP125
2 parents 67bf852 + 1ff1bb4 commit 27a8dbb

File tree

3 files changed

+143
-58
lines changed

3 files changed

+143
-58
lines changed

lightning/src/ln/channel.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6750,6 +6750,19 @@ fn get_v2_channel_reserve_satoshis(
67506750
cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis))
67516751
}
67526752

6753+
/// Returns the minimum feerate for our own RBF attempts given a previous feerate.
6754+
///
6755+
/// The spec (tx_init_rbf) requires the new feerate to be >= 25/24 of the previous feerate.
6756+
/// However, at low feerates that multiplier doesn't always satisfy BIP125's relay requirement of
6757+
/// an absolute fee increase, so we take the max of a flat +25 sat/kwu (0.1 sat/vB) increment
6758+
/// and the spec's multiplicative rule. We still accept the bare 25/24 rule from counterparties
6759+
/// in [`FundedChannel::validate_tx_init_rbf`].
6760+
fn min_rbf_feerate(prev_feerate: u32) -> FeeRate {
6761+
let flat_increment = (prev_feerate as u64).saturating_add(25);
6762+
let spec_increment = ((prev_feerate as u64) * 25).div_ceil(24);
6763+
FeeRate::from_sat_per_kwu(cmp::max(flat_increment, spec_increment))
6764+
}
6765+
67536766
/// Context for negotiating channels (dual-funded V2 open, splicing)
67546767
#[derive(Debug)]
67556768
pub(super) struct FundingNegotiationContext {
@@ -12290,10 +12303,7 @@ where
1229012303
prev_feerate.is_some(),
1229112304
"pending_splice should have last_funding_feerate or funding_negotiation",
1229212305
);
12293-
let min_rbf_feerate = prev_feerate.map(|f| {
12294-
let min_feerate_kwu = ((f as u64) * 25).div_ceil(24);
12295-
FeeRate::from_sat_per_kwu(min_feerate_kwu)
12296-
});
12306+
let min_rbf_feerate = prev_feerate.map(min_rbf_feerate);
1229712307
let prior = if pending_splice.last_funding_feerate_sat_per_1000_weight.is_some() {
1229812308
self.build_prior_contribution()
1229912309
} else {
@@ -12385,10 +12395,7 @@ where
1238512395
}
1238612396

1238712397
match pending_splice.last_funding_feerate_sat_per_1000_weight {
12388-
Some(prev_feerate) => {
12389-
let min_feerate_kwu = ((prev_feerate as u64) * 25).div_ceil(24);
12390-
Ok(FeeRate::from_sat_per_kwu(min_feerate_kwu))
12391-
},
12398+
Some(prev_feerate) => Ok(min_rbf_feerate(prev_feerate)),
1239212399
None => Err(format!(
1239312400
"Channel {} has no prior feerate to compute RBF minimum",
1239412401
self.context.channel_id(),

lightning/src/ln/funding.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,9 @@ impl PriorContribution {
218218
/// prior contribution logic internally — reusing an adjusted prior when possible, re-running
219219
/// coin selection when needed, or creating a fee-bump-only contribution.
220220
///
221-
/// Check [`FundingTemplate::min_rbf_feerate`] for the minimum feerate required (25/24 of
222-
/// the previous feerate). Use [`FundingTemplate::prior_contribution`] to inspect the prior
221+
/// Check [`FundingTemplate::min_rbf_feerate`] for the minimum feerate required (the greater of
222+
/// the previous feerate + 25 sat/kwu and the spec's 25/24 rule). Use
223+
/// [`FundingTemplate::prior_contribution`] to inspect the prior
223224
/// contribution's parameters (e.g., [`FundingContribution::value_added`],
224225
/// [`FundingContribution::outputs`]) before deciding whether to reuse it via the RBF methods
225226
/// or build a fresh contribution with different parameters using the splice methods above.
@@ -232,8 +233,9 @@ pub struct FundingTemplate {
232233
/// transaction.
233234
shared_input: Option<Input>,
234235

235-
/// The minimum RBF feerate (25/24 of the previous feerate), if this template is for an
236-
/// RBF attempt. `None` for fresh splices with no pending splice candidates.
236+
/// The minimum RBF feerate (the greater of previous feerate + 25 sat/kwu and the spec's
237+
/// 25/24 rule), if this template is for an RBF attempt. `None` for fresh splices with no
238+
/// pending splice candidates.
237239
min_rbf_feerate: Option<FeeRate>,
238240

239241
/// The user's prior contribution from a previous splice negotiation, if available.
@@ -2262,8 +2264,8 @@ mod tests {
22622264
// When the caller's max_feerate is below the minimum RBF feerate, rbf_sync should
22632265
// return Err(()).
22642266
let prior_feerate = FeeRate::from_sat_per_kwu(2000);
2265-
let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000);
2266-
let max_feerate = FeeRate::from_sat_per_kwu(3000);
2267+
let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025);
2268+
let max_feerate = FeeRate::from_sat_per_kwu(2020);
22672269

22682270
let prior = FundingContribution {
22692271
value_added: Amount::from_sat(50_000),
@@ -2276,7 +2278,7 @@ mod tests {
22762278
is_splice: true,
22772279
};
22782280

2279-
// max_feerate (3000) < min_rbf_feerate (5000).
2281+
// max_feerate (2020) < min_rbf_feerate (2025).
22802282
let template = FundingTemplate::new(
22812283
None,
22822284
Some(min_rbf_feerate),
@@ -2359,8 +2361,8 @@ mod tests {
23592361
// When the prior contribution's feerate is below the minimum RBF feerate and no
23602362
// holder balance is available, rbf_sync should run coin selection to add inputs that
23612363
// cover the higher RBF fee.
2362-
let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000);
23632364
let prior_feerate = FeeRate::from_sat_per_kwu(2000);
2365+
let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025);
23642366
let withdrawal = funding_output_sats(20_000);
23652367

23662368
let prior = FundingContribution {
@@ -2397,7 +2399,7 @@ mod tests {
23972399
fn test_rbf_sync_no_prior_fee_bump_only_runs_coin_selection() {
23982400
// When there is no prior contribution (e.g., acceptor), rbf_sync should run coin
23992401
// selection to add inputs for a fee-bump-only contribution.
2400-
let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000);
2402+
let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025);
24012403

24022404
let template =
24032405
FundingTemplate::new(Some(shared_input(100_000)), Some(min_rbf_feerate), None);
@@ -2419,7 +2421,7 @@ mod tests {
24192421
// When the prior contribution's feerate is below the minimum RBF feerate and no
24202422
// holder balance is available, rbf_sync should use the caller's max_feerate (not the
24212423
// prior's) for the resulting contribution.
2422-
let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000);
2424+
let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025);
24232425
let prior_max_feerate = FeeRate::from_sat_per_kwu(50_000);
24242426
let callers_max_feerate = FeeRate::from_sat_per_kwu(10_000);
24252427
let withdrawal = funding_output_sats(20_000);
@@ -2458,8 +2460,8 @@ mod tests {
24582460
// When splice_out_sync is called on a template with min_rbf_feerate set (user
24592461
// choosing a fresh splice-out instead of rbf_sync), coin selection should NOT run.
24602462
// Fees come from the channel balance.
2461-
let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000);
2462-
let feerate = FeeRate::from_sat_per_kwu(5000);
2463+
let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025);
2464+
let feerate = FeeRate::from_sat_per_kwu(2025);
24632465
let withdrawal = funding_output_sats(20_000);
24642466

24652467
let template =

0 commit comments

Comments
 (0)