Skip to content

Commit e9200ab

Browse files
authored
Merge pull request #677 from jkczyz/2025-10-splicing
Channel splicing support
2 parents 3034614 + 204e04d commit e9200ab

File tree

9 files changed

+740
-78
lines changed

9 files changed

+740
-78
lines changed

bindings/ldk_node.udl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ interface Node {
150150
[Throws=NodeError]
151151
UserChannelId open_announced_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
152152
[Throws=NodeError]
153+
void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats);
154+
[Throws=NodeError]
155+
void splice_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats);
156+
[Throws=NodeError]
153157
void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
154158
[Throws=NodeError]
155159
void force_close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, string? reason);
@@ -290,6 +294,7 @@ enum NodeError {
290294
"ProbeSendingFailed",
291295
"ChannelCreationFailed",
292296
"ChannelClosingFailed",
297+
"ChannelSplicingFailed",
293298
"ChannelConfigUpdateFailed",
294299
"PersistenceFailed",
295300
"FeerateEstimationUpdateFailed",
@@ -393,8 +398,10 @@ interface Event {
393398
PaymentForwarded(ChannelId prev_channel_id, ChannelId next_channel_id, UserChannelId?
394399
prev_user_channel_id, UserChannelId? next_user_channel_id, PublicKey? prev_node_id, PublicKey? next_node_id, u64? total_fee_earned_msat, u64? skimmed_fee_msat, boolean claim_from_onchain_tx, u64? outbound_amount_forwarded_msat);
395400
ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
396-
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
401+
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, OutPoint? funding_txo);
397402
ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
403+
SplicePending(ChannelId channel_id, UserChannelId user_channel_id, PublicKey counterparty_node_id, OutPoint new_funding_txo);
404+
SpliceFailed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey counterparty_node_id, OutPoint? abandoned_funding_txo);
398405
};
399406

400407
enum PaymentFailureReason {

src/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,6 +1795,7 @@ fn build_with_store_internal(
17951795
wallet,
17961796
chain_source,
17971797
tx_broadcaster,
1798+
fee_estimator,
17981799
event_queue,
17991800
channel_manager,
18001801
chain_monitor,

src/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig {
325325
user_config.manually_accept_inbound_channels = true;
326326
user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx =
327327
config.anchor_channels_config.is_some();
328+
user_config.reject_inbound_splices = false;
328329

329330
if may_announce_channel(config).is_err() {
330331
user_config.accept_forwards_to_priv_channels = false;

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ pub enum Error {
4343
ChannelCreationFailed,
4444
/// A channel could not be closed.
4545
ChannelClosingFailed,
46+
/// A channel could not be spliced.
47+
ChannelSplicingFailed,
4648
/// A channel configuration could not be updated.
4749
ChannelConfigUpdateFailed,
4850
/// Persistence failed.
@@ -145,6 +147,7 @@ impl fmt::Display for Error {
145147
Self::ProbeSendingFailed => write!(f, "Failed to send the given payment probe."),
146148
Self::ChannelCreationFailed => write!(f, "Failed to create channel."),
147149
Self::ChannelClosingFailed => write!(f, "Failed to close channel."),
150+
Self::ChannelSplicingFailed => write!(f, "Failed to splice channel."),
148151
Self::ChannelConfigUpdateFailed => write!(f, "Failed to update channel config."),
149152
Self::PersistenceFailed => write!(f, "Failed to persist data."),
150153
Self::FeerateEstimationUpdateFailed => {

src/event.rs

Lines changed: 179 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ pub enum Event {
199199
funding_txo: OutPoint,
200200
},
201201
/// A channel is ready to be used.
202+
///
203+
/// This event is emitted when:
204+
/// - A new channel has been established and is ready for use
205+
/// - An existing channel has been spliced and is ready with the new funding output
202206
ChannelReady {
203207
/// The `channel_id` of the channel.
204208
channel_id: ChannelId,
@@ -208,6 +212,14 @@ pub enum Event {
208212
///
209213
/// This will be `None` for events serialized by LDK Node v0.1.0 and prior.
210214
counterparty_node_id: Option<PublicKey>,
215+
/// The outpoint of the channel's funding transaction.
216+
///
217+
/// This represents the channel's current funding output, which may change when the
218+
/// channel is spliced. For spliced channels, this will contain the new funding output
219+
/// from the confirmed splice transaction.
220+
///
221+
/// This will be `None` for events serialized by LDK Node v0.6.0 and prior.
222+
funding_txo: Option<OutPoint>,
211223
},
212224
/// A channel has been closed.
213225
ChannelClosed {
@@ -222,6 +234,28 @@ pub enum Event {
222234
/// This will be `None` for events serialized by LDK Node v0.2.1 and prior.
223235
reason: Option<ClosureReason>,
224236
},
237+
/// A channel splice is pending confirmation on-chain.
238+
SplicePending {
239+
/// The `channel_id` of the channel.
240+
channel_id: ChannelId,
241+
/// The `user_channel_id` of the channel.
242+
user_channel_id: UserChannelId,
243+
/// The `node_id` of the channel counterparty.
244+
counterparty_node_id: PublicKey,
245+
/// The outpoint of the channel's splice funding transaction.
246+
new_funding_txo: OutPoint,
247+
},
248+
/// A channel splice has failed.
249+
SpliceFailed {
250+
/// The `channel_id` of the channel.
251+
channel_id: ChannelId,
252+
/// The `user_channel_id` of the channel.
253+
user_channel_id: UserChannelId,
254+
/// The `node_id` of the channel counterparty.
255+
counterparty_node_id: PublicKey,
256+
/// The outpoint of the channel's splice funding transaction, if one was created.
257+
abandoned_funding_txo: Option<OutPoint>,
258+
},
225259
}
226260

227261
impl_writeable_tlv_based_enum!(Event,
@@ -246,6 +280,7 @@ impl_writeable_tlv_based_enum!(Event,
246280
(0, channel_id, required),
247281
(1, counterparty_node_id, option),
248282
(2, user_channel_id, required),
283+
(3, funding_txo, option),
249284
},
250285
(4, ChannelPending) => {
251286
(0, channel_id, required),
@@ -278,7 +313,19 @@ impl_writeable_tlv_based_enum!(Event,
278313
(10, skimmed_fee_msat, option),
279314
(12, claim_from_onchain_tx, required),
280315
(14, outbound_amount_forwarded_msat, option),
281-
}
316+
},
317+
(8, SplicePending) => {
318+
(1, channel_id, required),
319+
(3, counterparty_node_id, required),
320+
(5, user_channel_id, required),
321+
(7, new_funding_txo, required),
322+
},
323+
(9, SpliceFailed) => {
324+
(1, channel_id, required),
325+
(3, counterparty_node_id, required),
326+
(5, user_channel_id, required),
327+
(7, abandoned_funding_txo, option),
328+
},
282329
);
283330

284331
pub struct EventQueue<L: Deref>
@@ -1397,14 +1444,28 @@ where
13971444
}
13981445
},
13991446
LdkEvent::ChannelReady {
1400-
channel_id, user_channel_id, counterparty_node_id, ..
1447+
channel_id,
1448+
user_channel_id,
1449+
counterparty_node_id,
1450+
funding_txo,
1451+
..
14011452
} => {
1402-
log_info!(
1403-
self.logger,
1404-
"Channel {} with counterparty {} ready to be used.",
1405-
channel_id,
1406-
counterparty_node_id,
1407-
);
1453+
if let Some(funding_txo) = funding_txo {
1454+
log_info!(
1455+
self.logger,
1456+
"Channel {} with counterparty {} ready to be used with funding_txo {}",
1457+
channel_id,
1458+
counterparty_node_id,
1459+
funding_txo,
1460+
);
1461+
} else {
1462+
log_info!(
1463+
self.logger,
1464+
"Channel {} with counterparty {} ready to be used",
1465+
channel_id,
1466+
counterparty_node_id,
1467+
);
1468+
}
14081469

14091470
if let Some(liquidity_source) = self.liquidity_source.as_ref() {
14101471
liquidity_source
@@ -1416,6 +1477,7 @@ where
14161477
channel_id,
14171478
user_channel_id: UserChannelId(user_channel_id),
14181479
counterparty_node_id: Some(counterparty_node_id),
1480+
funding_txo,
14191481
};
14201482
match self.event_queue.add_event(event).await {
14211483
Ok(_) => {},
@@ -1614,20 +1676,116 @@ where
16141676
}
16151677
}
16161678
},
1617-
LdkEvent::FundingTransactionReadyForSigning { .. } => {
1618-
debug_assert!(false, "We currently don't support interactive-tx, so this event should never be emitted.");
1679+
// TODO(splicing): Revisit error handling once splicing API is settled in LDK 0.3
1680+
LdkEvent::FundingTransactionReadyForSigning {
1681+
channel_id,
1682+
counterparty_node_id,
1683+
unsigned_transaction,
1684+
..
1685+
} => match self.wallet.sign_owned_inputs(unsigned_transaction) {
1686+
Ok(partially_signed_tx) => {
1687+
match self.channel_manager.funding_transaction_signed(
1688+
&channel_id,
1689+
&counterparty_node_id,
1690+
partially_signed_tx,
1691+
) {
1692+
Ok(()) => {
1693+
log_info!(
1694+
self.logger,
1695+
"Signed funding transaction for channel {} with counterparty {}",
1696+
channel_id,
1697+
counterparty_node_id
1698+
);
1699+
},
1700+
Err(e) => {
1701+
// TODO(splicing): Abort splice once supported in LDK 0.3
1702+
debug_assert!(false, "Failed signing funding transaction: {:?}", e);
1703+
log_error!(self.logger, "Failed signing funding transaction: {:?}", e);
1704+
},
1705+
}
1706+
},
1707+
Err(()) => log_error!(self.logger, "Failed signing funding transaction"),
16191708
},
1620-
LdkEvent::SplicePending { .. } => {
1621-
debug_assert!(
1622-
false,
1623-
"We currently don't support splicing, so this event should never be emitted."
1709+
LdkEvent::SplicePending {
1710+
channel_id,
1711+
user_channel_id,
1712+
counterparty_node_id,
1713+
new_funding_txo,
1714+
..
1715+
} => {
1716+
log_info!(
1717+
self.logger,
1718+
"Channel {} with counterparty {} pending splice with funding_txo {}",
1719+
channel_id,
1720+
counterparty_node_id,
1721+
new_funding_txo,
16241722
);
1723+
1724+
let event = Event::SplicePending {
1725+
channel_id,
1726+
user_channel_id: UserChannelId(user_channel_id),
1727+
counterparty_node_id,
1728+
new_funding_txo,
1729+
};
1730+
1731+
match self.event_queue.add_event(event).await {
1732+
Ok(_) => {},
1733+
Err(e) => {
1734+
log_error!(self.logger, "Failed to push to event queue: {}", e);
1735+
return Err(ReplayEvent());
1736+
},
1737+
};
16251738
},
1626-
LdkEvent::SpliceFailed { .. } => {
1627-
debug_assert!(
1628-
false,
1629-
"We currently don't support splicing, so this event should never be emitted."
1630-
);
1739+
LdkEvent::SpliceFailed {
1740+
channel_id,
1741+
user_channel_id,
1742+
counterparty_node_id,
1743+
abandoned_funding_txo,
1744+
contributed_outputs,
1745+
..
1746+
} => {
1747+
if let Some(funding_txo) = abandoned_funding_txo {
1748+
log_info!(
1749+
self.logger,
1750+
"Channel {} with counterparty {} failed splice with funding_txo {}",
1751+
channel_id,
1752+
counterparty_node_id,
1753+
funding_txo,
1754+
);
1755+
} else {
1756+
log_info!(
1757+
self.logger,
1758+
"Channel {} with counterparty {} failed splice",
1759+
channel_id,
1760+
counterparty_node_id,
1761+
);
1762+
}
1763+
1764+
let tx = bitcoin::Transaction {
1765+
version: bitcoin::transaction::Version::TWO,
1766+
lock_time: bitcoin::absolute::LockTime::ZERO,
1767+
input: vec![],
1768+
output: contributed_outputs,
1769+
};
1770+
if let Err(e) = self.wallet.cancel_tx(&tx) {
1771+
log_error!(self.logger, "Failed reclaiming unused addresses: {}", e);
1772+
return Err(ReplayEvent());
1773+
}
1774+
1775+
let event = Event::SpliceFailed {
1776+
channel_id,
1777+
user_channel_id: UserChannelId(user_channel_id),
1778+
counterparty_node_id,
1779+
abandoned_funding_txo,
1780+
};
1781+
1782+
match self.event_queue.add_event(event).await {
1783+
Ok(_) => {},
1784+
Err(e) => {
1785+
log_error!(self.logger, "Failed to push to event queue: {}", e);
1786+
return Err(ReplayEvent());
1787+
},
1788+
};
16311789
},
16321790
}
16331791
Ok(())
@@ -1655,6 +1813,7 @@ mod tests {
16551813
channel_id: ChannelId([23u8; 32]),
16561814
user_channel_id: UserChannelId(2323),
16571815
counterparty_node_id: None,
1816+
funding_txo: None,
16581817
};
16591818
event_queue.add_event(expected_event.clone()).await.unwrap();
16601819

@@ -1692,6 +1851,7 @@ mod tests {
16921851
channel_id: ChannelId([23u8; 32]),
16931852
user_channel_id: UserChannelId(2323),
16941853
counterparty_node_id: None,
1854+
funding_txo: None,
16951855
};
16961856

16971857
// Check `next_event_async` won't return if the queue is empty and always rather timeout.

0 commit comments

Comments
 (0)