Skip to content

Commit 99390f0

Browse files
jkczyzclaude
andcommitted
Preserve our funding contribution across counterparty RBF attempts
When the counterparty initiates an RBF and we have no new contribution queued via QuiescentAction, we must re-use our prior contribution so that our splice is not lost. Track contributions in a new field on PendingFunding so the last entry can be re-used in this scenario. Each entry stores the feerate-adjusted version because that reflects what was actually negotiated and allows correct feerate re-adjustment on subsequent RBFs. Only explicitly provided contributions (from a QuiescentAction) append to the vec. Re-used contributions are replaced in-place with the version adjusted for the new feerate so they remain accurate for further RBF rounds, without growing the vec. Add test_splice_rbf_acceptor_recontributes to verify that when the counterparty initiates an RBF and we have no new QuiescentAction queued, our prior contribution is automatically re-used so the splice is preserved. Add test_splice_rbf_recontributes_feerate_too_high to verify that when the counterparty RBFs at a feerate too high for our prior contribution to cover, the RBF is rejected rather than proceeding without our contribution. Add test for sequential RBF splice attempts Add test_splice_rbf_sequential that exercises three consecutive RBF rounds on the same splice (initial → RBF #1 → RBF #2) to verify: - Each round requires the 25/24 feerate increase (253 → 264 → 275) - DiscardFunding events reference the correct funding txid from each replaced candidate - The final RBF splice can be mined and splice_locked successfully Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d808155 commit 99390f0

3 files changed

Lines changed: 537 additions & 15 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2902,6 +2902,13 @@ struct PendingFunding {
29022902
/// The feerate used in the last successfully negotiated funding transaction.
29032903
/// Used for validating the 25/24 feerate increase rule on RBF attempts.
29042904
last_funding_feerate_sat_per_1000_weight: Option<u32>,
2905+
2906+
/// The funding contributions from all explicit splice/RBF attempts on this channel.
2907+
/// Each entry reflects the feerate-adjusted contribution that was actually used in that
2908+
/// negotiation. The last entry is re-used when the counterparty initiates an RBF and we
2909+
/// have no pending `QuiescentAction`. When re-used as acceptor, the last entry is replaced
2910+
/// with the version adjusted for the new feerate.
2911+
contributions: Vec<FundingContribution>,
29052912
}
29062913

29072914
impl_writeable_tlv_based!(PendingFunding, {
@@ -2910,6 +2917,7 @@ impl_writeable_tlv_based!(PendingFunding, {
29102917
(5, sent_funding_txid, option),
29112918
(7, received_funding_txid, option),
29122919
(8, last_funding_feerate_sat_per_1000_weight, option),
2920+
(10, contributions, optional_vec),
29132921
});
29142922

29152923
#[derive(Debug)]
@@ -12149,6 +12157,7 @@ where
1214912157
sent_funding_txid: None,
1215012158
received_funding_txid: None,
1215112159
last_funding_feerate_sat_per_1000_weight: None,
12160+
contributions: vec![],
1215212161
});
1215312162

1215412163
msgs::SpliceInit {
@@ -12415,15 +12424,19 @@ where
1241512424
let splice_funding =
1241612425
self.validate_splice_init(msg, our_funding_contribution.unwrap_or(SignedAmount::ZERO))?;
1241712426

12418-
let (our_funding_inputs, our_funding_outputs) = if our_funding_contribution.is_some() {
12419-
self.take_queued_funding_contribution()
12420-
.expect("queued_funding_contribution was Some")
12421-
.for_acceptor_at_feerate(feerate, holder_balance.unwrap())
12422-
.expect("feerate compatibility already checked")
12423-
.into_tx_parts()
12424-
} else {
12425-
Default::default()
12426-
};
12427+
// Adjust for the feerate and clone so we can store it for future RBF re-use.
12428+
let (adjusted_contribution, our_funding_inputs, our_funding_outputs) =
12429+
if our_funding_contribution.is_some() {
12430+
let adjusted_contribution = self
12431+
.take_queued_funding_contribution()
12432+
.expect("queued_funding_contribution was Some")
12433+
.for_acceptor_at_feerate(feerate, holder_balance.unwrap())
12434+
.expect("feerate compatibility already checked");
12435+
let (inputs, outputs) = adjusted_contribution.clone().into_tx_parts();
12436+
(Some(adjusted_contribution), inputs, outputs)
12437+
} else {
12438+
(None, Default::default(), Default::default())
12439+
};
1242712440
let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
1242812441

1242912442
log_info!(
@@ -12454,6 +12467,7 @@ where
1245412467
received_funding_txid: None,
1245512468
sent_funding_txid: None,
1245612469
last_funding_feerate_sat_per_1000_weight: None,
12470+
contributions: adjusted_contribution.into_iter().collect(),
1245712471
});
1245812472

1245912473
Ok(msgs::SpliceAck {
@@ -12563,24 +12577,71 @@ where
1256312577
fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L,
1256412578
) -> Result<msgs::TxAckRbf, ChannelError> {
1256512579
let feerate = FeeRate::from_sat_per_kwu(msg.feerate_sat_per_1000_weight as u64);
12566-
let (our_funding_contribution, holder_balance) =
12567-
self.resolve_queued_contribution(feerate, logger);
12580+
let (queued_net_value, holder_balance) = self.resolve_queued_contribution(feerate, logger);
12581+
12582+
// If no queued contribution, try prior contribution from previous negotiation.
12583+
// Failing here means the RBF would erase our splice — reject it.
12584+
let prior_net_value = if queued_net_value.is_some() {
12585+
None
12586+
} else if let Some(prior) = self
12587+
.pending_splice
12588+
.as_ref()
12589+
.and_then(|pending_splice| pending_splice.contributions.last())
12590+
{
12591+
let net_value = holder_balance
12592+
.ok_or_else(|| ChannelError::Abort(AbortReason::InsufficientRbfFeerate))
12593+
.and_then(|holder_balance| {
12594+
prior
12595+
.net_value_for_acceptor_at_feerate(feerate, holder_balance)
12596+
.map_err(|_| ChannelError::Abort(AbortReason::InsufficientRbfFeerate))
12597+
})?;
12598+
Some(net_value)
12599+
} else {
12600+
None
12601+
};
12602+
12603+
let our_funding_contribution = queued_net_value.or(prior_net_value);
1256812604

1256912605
let rbf_funding = self.validate_tx_init_rbf(
1257012606
msg,
1257112607
our_funding_contribution.unwrap_or(SignedAmount::ZERO),
1257212608
fee_estimator,
1257312609
)?;
1257412610

12575-
let (our_funding_inputs, our_funding_outputs) = if our_funding_contribution.is_some() {
12576-
self.take_queued_funding_contribution()
12611+
// Consume the appropriate contribution source.
12612+
let (our_funding_inputs, our_funding_outputs) = if queued_net_value.is_some() {
12613+
let adjusted_contribution = self
12614+
.take_queued_funding_contribution()
1257712615
.expect("queued_funding_contribution was Some")
1257812616
.for_acceptor_at_feerate(feerate, holder_balance.unwrap())
12579-
.expect("feerate compatibility already checked")
12580-
.into_tx_parts()
12617+
.expect("feerate compatibility already checked");
12618+
self.pending_splice
12619+
.as_mut()
12620+
.expect("pending_splice is Some")
12621+
.contributions
12622+
.push(adjusted_contribution.clone());
12623+
adjusted_contribution.into_tx_parts()
12624+
} else if prior_net_value.is_some() {
12625+
let prior_contribution = self
12626+
.pending_splice
12627+
.as_mut()
12628+
.expect("pending_splice is Some")
12629+
.contributions
12630+
.pop()
12631+
.expect("prior_net_value was Some");
12632+
let adjusted_contribution = prior_contribution
12633+
.for_acceptor_at_feerate(feerate, holder_balance.unwrap())
12634+
.expect("feerate compatibility already checked");
12635+
self.pending_splice
12636+
.as_mut()
12637+
.expect("pending_splice is Some")
12638+
.contributions
12639+
.push(adjusted_contribution.clone());
12640+
adjusted_contribution.into_tx_parts()
1258112641
} else {
1258212642
Default::default()
1258312643
};
12644+
1258412645
let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
1258512646

1258612647
log_info!(
@@ -13567,6 +13628,7 @@ where
1356713628
));
1356813629
},
1356913630
Some(QuiescentAction::Splice { contribution, locktime }) => {
13631+
let prior_contribution = contribution.clone();
1357013632
let prev_funding_input = self.funding.to_splice_funding_input();
1357113633
let our_funding_contribution = contribution.net_value();
1357213634
let funding_feerate_per_kw = contribution.feerate().to_sat_per_kwu() as u32;
@@ -13584,10 +13646,15 @@ where
1358413646

1358513647
if self.pending_splice.is_some() {
1358613648
let tx_init_rbf = self.send_tx_init_rbf(context);
13649+
self.pending_splice.as_mut().unwrap()
13650+
.contributions.push(prior_contribution);
1358713651
return Ok(Some(StfuResponse::TxInitRbf(tx_init_rbf)));
1358813652
}
1358913653

1359013654
let splice_init = self.send_splice_init(context);
13655+
debug_assert!(self.pending_splice.is_some());
13656+
self.pending_splice.as_mut().unwrap()
13657+
.contributions.push(prior_contribution);
1359113658
return Ok(Some(StfuResponse::SpliceInit(splice_init)));
1359213659
},
1359313660
#[cfg(any(test, fuzzing, feature = "_test_utils"))]

lightning/src/ln/funding.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,17 @@ pub struct FundingContribution {
388388
is_splice: bool,
389389
}
390390

391+
impl_writeable_tlv_based!(FundingContribution, {
392+
(1, value_added, required),
393+
(3, estimated_fee, required),
394+
(5, inputs, optional_vec),
395+
(7, outputs, optional_vec),
396+
(9, change_output, option),
397+
(11, feerate, required),
398+
(13, max_feerate, required),
399+
(15, is_splice, required),
400+
});
401+
391402
impl FundingContribution {
392403
pub(super) fn feerate(&self) -> FeeRate {
393404
self.feerate

0 commit comments

Comments
 (0)