Skip to content

Commit d964be9

Browse files
jkczyzclaude
andcommitted
Contribute to splice as acceptor
When both nodes want to splice simultaneously, the quiescence tie-breaker designates one as the initiator. Previously, the losing node responded with zero contribution, requiring a second full splice session after the first splice locked. This is wasteful, especially for often-offline nodes that may connect and immediately want to splice. Instead, the losing node contributes to the winner's splice as the acceptor, merging both contributions into a single splice transaction. Since the FundingContribution was originally built with initiator fees (which include common fields and shared input/output weight), the fee is adjusted to the acceptor rate before contributing, with the surplus returned to the change output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7942c74 commit d964be9

3 files changed

Lines changed: 491 additions & 94 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11851,6 +11851,26 @@ where
1185111851
self.propose_quiescence(logger, QuiescentAction::Splice { contribution, locktime })
1185211852
}
1185311853

11854+
/// Returns a reference to the funding contribution queued by a pending [`QuiescentAction`],
11855+
/// if any.
11856+
fn queued_funding_contribution(&self) -> Option<&FundingContribution> {
11857+
match &self.quiescent_action {
11858+
Some(QuiescentAction::Splice { contribution, .. }) => Some(contribution),
11859+
_ => None,
11860+
}
11861+
}
11862+
11863+
/// Consumes and returns the funding contribution from the pending [`QuiescentAction`], if any.
11864+
fn take_queued_funding_contribution(&mut self) -> Option<FundingContribution> {
11865+
match &self.quiescent_action {
11866+
Some(QuiescentAction::Splice { .. }) => match self.quiescent_action.take() {
11867+
Some(QuiescentAction::Splice { contribution, .. }) => Some(contribution),
11868+
_ => unreachable!(),
11869+
},
11870+
_ => None,
11871+
}
11872+
}
11873+
1185411874
fn send_splice_init(&mut self, context: FundingNegotiationContext) -> msgs::SpliceInit {
1185511875
debug_assert!(self.pending_splice.is_none());
1185611876
// Rotate the funding pubkey using the prev_funding_txid as a tweak
@@ -11948,10 +11968,6 @@ where
1194811968
));
1194911969
}
1195011970

11951-
// TODO(splicing): Once splice acceptor can contribute, check that inputs are sufficient,
11952-
// similarly to the check in `funding_contributed`.
11953-
debug_assert_eq!(our_funding_contribution, SignedAmount::ZERO);
11954-
1195511971
let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis);
1195611972
if their_funding_contribution == SignedAmount::ZERO {
1195711973
return Err(ChannelError::WarnAndDisconnect(format!(
@@ -12075,11 +12091,52 @@ where
1207512091
}
1207612092

1207712093
pub(crate) fn splice_init<ES: EntropySource, L: Logger>(
12078-
&mut self, msg: &msgs::SpliceInit, our_funding_contribution_satoshis: i64,
12079-
entropy_source: &ES, holder_node_id: &PublicKey, logger: &L,
12094+
&mut self, msg: &msgs::SpliceInit, entropy_source: &ES, holder_node_id: &PublicKey,
12095+
logger: &L,
1208012096
) -> Result<msgs::SpliceAck, ChannelError> {
12081-
let our_funding_contribution = SignedAmount::from_sat(our_funding_contribution_satoshis);
12082-
let splice_funding = self.validate_splice_init(msg, our_funding_contribution)?;
12097+
let feerate = FeeRate::from_sat_per_kwu(msg.funding_feerate_per_kw as u64);
12098+
let holder_balance = self
12099+
.get_holder_counterparty_balances_floor_incl_fee(&self.funding)
12100+
.map(|(holder, _)| holder)
12101+
.map_err(|e| {
12102+
log_info!(
12103+
logger,
12104+
"Cannot compute holder balance for channel {}: {}; \
12105+
proceeding without contribution",
12106+
self.context.channel_id(),
12107+
e,
12108+
);
12109+
})
12110+
.ok();
12111+
let our_funding_contribution =
12112+
holder_balance.and_then(|_| self.queued_funding_contribution()).and_then(|c| {
12113+
c.net_value_for_acceptor_at_feerate(feerate, holder_balance.unwrap())
12114+
.map_err(|e| {
12115+
log_info!(
12116+
logger,
12117+
"Cannot accommodate initiator's feerate ({}) for channel {}: {}; \
12118+
proceeding without contribution",
12119+
feerate,
12120+
self.context.channel_id(),
12121+
e,
12122+
);
12123+
})
12124+
.ok()
12125+
});
12126+
12127+
let splice_funding =
12128+
self.validate_splice_init(msg, our_funding_contribution.unwrap_or(SignedAmount::ZERO))?;
12129+
12130+
let (our_funding_inputs, our_funding_outputs) = if our_funding_contribution.is_some() {
12131+
self.take_queued_funding_contribution()
12132+
.expect("queued_funding_contribution was Some")
12133+
.for_acceptor_at_feerate(feerate, holder_balance.unwrap())
12134+
.expect("feerate compatibility already checked")
12135+
.into_tx_parts()
12136+
} else {
12137+
Default::default()
12138+
};
12139+
let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
1208312140

1208412141
log_info!(
1208512142
logger,
@@ -12096,8 +12153,8 @@ where
1209612153
funding_tx_locktime: LockTime::from_consensus(msg.locktime),
1209712154
funding_feerate_sat_per_1000_weight: msg.funding_feerate_per_kw,
1209812155
shared_funding_input: Some(prev_funding_input),
12099-
our_funding_inputs: Vec::new(),
12100-
our_funding_outputs: Vec::new(),
12156+
our_funding_inputs,
12157+
our_funding_outputs,
1210112158
};
1210212159

1210312160
let (interactive_tx_constructor, first_message) = funding_negotiation_context
@@ -12109,11 +12166,6 @@ where
1210912166
);
1211012167
debug_assert!(first_message.is_none());
1211112168

12112-
// TODO(splicing): if quiescent_action is set, integrate what the user wants to do into the
12113-
// counterparty-initiated splice. For always-on nodes this probably isn't a useful
12114-
// optimization, but for often-offline nodes it may be, as we may connect and immediately
12115-
// go into splicing from both sides.
12116-
1211712169
let new_funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey;
1211812170
self.pending_splice = Some(PendingFunding {
1211912171
funding_negotiation: Some(FundingNegotiation::ConstructingTransaction {

lightning/src/ln/channelmanager.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4643,11 +4643,21 @@ impl<
46434643
///
46444644
/// The splice initiator is responsible for paying fees for common fields, shared inputs, and
46454645
/// shared outputs along with any contributed inputs and outputs. When building a
4646-
/// [`FundingContribution`], fees are estimated using `min_feerate` and must be covered by the
4647-
/// supplied inputs for splice-in or the channel balance for splice-out. If the counterparty
4648-
/// also initiates a splice and wins the tie-break, they become the initiator and choose the
4649-
/// feerate. In that case, `max_feerate` is used to reject a feerate that is too high for our
4650-
/// contribution.
4646+
/// [`FundingContribution`], fees are estimated at `min_feerate` assuming initiator
4647+
/// responsibility and must be covered by the supplied inputs for splice-in or the channel
4648+
/// balance for splice-out. If the counterparty also initiates a splice and wins the
4649+
/// tie-break, they become the initiator and choose the feerate. The fee is then
4650+
/// re-estimated at the counterparty's feerate for only our contributed inputs and outputs,
4651+
/// which may be higher or lower than the original estimate. The contribution is dropped and
4652+
/// the splice proceeds without it when:
4653+
/// - the counterparty's feerate is below `min_feerate`
4654+
/// - the counterparty's feerate is above `max_feerate` and the re-estimated fee exceeds the
4655+
/// original fee estimate
4656+
/// - the re-estimated fee exceeds the *fee buffer* regardless of `max_feerate`
4657+
///
4658+
/// The fee buffer is the maximum fee that can be accommodated:
4659+
/// - **splice-in**: the selected inputs' value minus the contributed amount
4660+
/// - **splice-out**: the channel balance minus the withdrawal outputs
46514661
///
46524662
/// Returns a [`FundingTemplate`] which should be used to build a [`FundingContribution`] via
46534663
/// one of its splice methods (e.g., [`FundingTemplate::splice_in_sync`]). The resulting
@@ -12826,9 +12836,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1282612836
let mut peer_state_lock = peer_state_mutex.lock().unwrap();
1282712837
let peer_state = &mut *peer_state_lock;
1282812838

12829-
// TODO(splicing): Currently not possible to contribute on the splicing-acceptor side
12830-
let our_funding_contribution = 0i64;
12831-
1283212839
// Look for the channel
1283312840
match peer_state.channel_by_id.entry(msg.channel_id) {
1283412841
hash_map::Entry::Vacant(_) => {
@@ -12848,7 +12855,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1284812855
if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() {
1284912856
let init_res = funded_channel.splice_init(
1285012857
msg,
12851-
our_funding_contribution,
1285212858
&self.entropy_source,
1285312859
&self.get_our_node_id(),
1285412860
&self.logger,

0 commit comments

Comments
 (0)