Skip to content

Commit 4bd360c

Browse files
committed
Add Node::splice_in method
Instead of closing and re-opening a channel when outbound liquidity is exhausted, splicing allows to adding more funds (splice-in) while keeping the channel operational. This commit implements splice-in using funds from the BDK on-chain wallet.
1 parent 1aaf8db commit 4bd360c

File tree

5 files changed

+170
-5
lines changed

5 files changed

+170
-5
lines changed

bindings/ldk_node.udl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ interface Node {
141141
[Throws=NodeError]
142142
UserChannelId open_announced_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
143143
[Throws=NodeError]
144+
void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats);
145+
[Throws=NodeError]
144146
void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
145147
[Throws=NodeError]
146148
void force_close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, string? reason);
@@ -281,6 +283,7 @@ enum NodeError {
281283
"ProbeSendingFailed",
282284
"ChannelCreationFailed",
283285
"ChannelClosingFailed",
286+
"ChannelSplicingFailed",
284287
"ChannelConfigUpdateFailed",
285288
"PersistenceFailed",
286289
"FeerateEstimationUpdateFailed",

src/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,6 +1775,7 @@ fn build_with_store_internal(
17751775
wallet,
17761776
chain_source,
17771777
tx_broadcaster,
1778+
fee_estimator,
17781779
event_queue,
17791780
channel_manager,
17801781
chain_monitor,

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/lib.rs

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
109109

110110
pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance};
111111
use bitcoin::secp256k1::PublicKey;
112+
use bitcoin::Amount;
112113
#[cfg(feature = "uniffi")]
113114
pub use builder::ArcedNodeBuilder as Builder;
114115
pub use builder::BuildError;
@@ -124,17 +125,20 @@ pub use error::Error as NodeError;
124125
use error::Error;
125126
pub use event::Event;
126127
use event::{EventHandler, EventQueue};
128+
use fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator};
127129
#[cfg(feature = "uniffi")]
128130
use ffi::*;
129131
use gossip::GossipSource;
130132
use graph::NetworkGraph;
131133
pub use io::utils::generate_entropy_mnemonic;
132134
use io::utils::write_node_metrics;
133135
use lightning::chain::BestBlock;
134-
use lightning::events::bump_transaction::Wallet as LdkWallet;
136+
use lightning::events::bump_transaction::{Input, Wallet as LdkWallet};
135137
use lightning::impl_writeable_tlv_based;
138+
use lightning::ln::chan_utils::{make_funding_redeemscript, FUNDING_TRANSACTION_WITNESS_WEIGHT};
136139
use lightning::ln::channel_state::ChannelShutdownState;
137140
use lightning::ln::channelmanager::PaymentId;
141+
use lightning::ln::funding::SpliceContribution;
138142
use lightning::ln::msgs::SocketAddress;
139143
use lightning::routing::gossip::NodeAlias;
140144
use lightning::util::persist::KVStoreSync;
@@ -178,6 +182,7 @@ pub struct Node {
178182
wallet: Arc<Wallet>,
179183
chain_source: Arc<ChainSource>,
180184
tx_broadcaster: Arc<Broadcaster>,
185+
fee_estimator: Arc<OnchainFeeEstimator>,
181186
event_queue: Arc<EventQueue<Arc<Logger>>>,
182187
channel_manager: Arc<ChannelManager>,
183188
chain_monitor: Arc<ChainMonitor>,
@@ -1223,6 +1228,106 @@ impl Node {
12231228
)
12241229
}
12251230

1231+
/// Add funds from the on-chain wallet into an existing channel.
1232+
///
1233+
/// This provides for increasing a channel's outbound liquidity without re-balancing or closing
1234+
/// it. Once negotiation with the counterparty is complete, the channel remains operational
1235+
/// while waiting for a new funding transaction to confirm.
1236+
///
1237+
/// # Experimental API
1238+
///
1239+
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
1240+
/// this classification may change in the future.
1241+
pub fn splice_in(
1242+
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
1243+
splice_amount_sats: u64,
1244+
) -> Result<(), Error> {
1245+
let open_channels =
1246+
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
1247+
if let Some(channel_details) =
1248+
open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0)
1249+
{
1250+
self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?;
1251+
1252+
const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
1253+
1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
1254+
let funding_txo = channel_details.funding_txo.ok_or_else(|| {
1255+
log_error!(self.logger, "Failed to splice channel: channel not yet ready",);
1256+
Error::ChannelSplicingFailed
1257+
})?;
1258+
let shared_input = Input {
1259+
outpoint: funding_txo.into_bitcoin_outpoint(),
1260+
previous_utxo: bitcoin::TxOut {
1261+
value: Amount::from_sat(channel_details.channel_value_satoshis),
1262+
script_pubkey: make_funding_redeemscript(
1263+
&PublicKey::from_slice(&[2; 33]).unwrap(),
1264+
&PublicKey::from_slice(&[2; 33]).unwrap(),
1265+
)
1266+
.to_p2wsh(),
1267+
},
1268+
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
1269+
};
1270+
1271+
let shared_output = bitcoin::TxOut {
1272+
value: shared_input.previous_utxo.value + Amount::from_sat(splice_amount_sats),
1273+
script_pubkey: make_funding_redeemscript(
1274+
&PublicKey::from_slice(&[2; 33]).unwrap(),
1275+
&PublicKey::from_slice(&[2; 33]).unwrap(),
1276+
)
1277+
.to_p2wsh(),
1278+
};
1279+
1280+
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1281+
1282+
let inputs = self
1283+
.wallet
1284+
.select_confirmed_utxos(vec![shared_input], &[shared_output], fee_rate)
1285+
.map_err(|()| {
1286+
log_error!(
1287+
self.logger,
1288+
"Failed to splice channel: insufficient confirmed UTXOs",
1289+
);
1290+
Error::ChannelSplicingFailed
1291+
})?;
1292+
1293+
let contribution = SpliceContribution::SpliceIn {
1294+
value: Amount::from_sat(splice_amount_sats),
1295+
inputs,
1296+
change_script: None,
1297+
};
1298+
1299+
let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() {
1300+
Ok(fee_rate) => fee_rate,
1301+
Err(_) => {
1302+
debug_assert!(false);
1303+
fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding)
1304+
},
1305+
};
1306+
1307+
self.channel_manager
1308+
.splice_channel(
1309+
&channel_details.channel_id,
1310+
&counterparty_node_id,
1311+
contribution,
1312+
funding_feerate_per_kw,
1313+
None,
1314+
)
1315+
.map_err(|e| {
1316+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1317+
Error::ChannelSplicingFailed
1318+
})
1319+
} else {
1320+
log_error!(
1321+
self.logger,
1322+
"Channel not found for user_channel_id: {:?} and counterparty: {}",
1323+
user_channel_id,
1324+
counterparty_node_id
1325+
);
1326+
1327+
Err(Error::ChannelSplicingFailed)
1328+
}
1329+
}
1330+
12261331
/// Manually sync the LDK and BDK wallets with the current chain state and update the fee rate
12271332
/// cache.
12281333
///

src/wallet/mod.rs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
// accordance with one or both of these licenses.
77

88
use std::future::Future;
9+
use std::ops::Deref;
910
use std::pin::Pin;
1011
use std::str::FromStr;
1112
use std::sync::{Arc, Mutex};
1213

1314
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
15+
use bdk_wallet::descriptor::ExtendedDescriptor;
1416
#[allow(deprecated)]
1517
use bdk_wallet::SignOptions;
1618
use bdk_wallet::{Balance, KeychainKind, PersistedWallet, Update};
@@ -19,19 +21,20 @@ use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
1921
use bitcoin::blockdata::locktime::absolute::LockTime;
2022
use bitcoin::hashes::Hash;
2123
use bitcoin::key::XOnlyPublicKey;
22-
use bitcoin::psbt::Psbt;
24+
use bitcoin::psbt::{self, Psbt};
2325
use bitcoin::secp256k1::ecdh::SharedSecret;
2426
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
2527
use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
2628
use bitcoin::{
27-
Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash,
29+
Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
2830
WitnessProgram, WitnessVersion,
2931
};
3032
use lightning::chain::chaininterface::BroadcasterInterface;
3133
use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
3234
use lightning::chain::{BestBlock, Listen};
33-
use lightning::events::bump_transaction::{Utxo, WalletSource};
35+
use lightning::events::bump_transaction::{Input, Utxo, WalletSource};
3436
use lightning::ln::channelmanager::PaymentId;
37+
use lightning::ln::funding::FundingTxInput;
3538
use lightning::ln::inbound_payment::ExpandedKey;
3639
use lightning::ln::msgs::UnsignedGossipMessage;
3740
use lightning::ln::script::ShutdownScript;
@@ -559,6 +562,56 @@ impl Wallet {
559562
Ok(txid)
560563
}
561564

565+
pub(crate) fn select_confirmed_utxos(
566+
&self, must_spend: Vec<Input>, must_pay_to: &[TxOut], fee_rate: FeeRate,
567+
) -> Result<Vec<FundingTxInput>, ()> {
568+
let mut locked_wallet = self.inner.lock().unwrap();
569+
debug_assert!(matches!(
570+
locked_wallet.public_descriptor(KeychainKind::External),
571+
ExtendedDescriptor::Wpkh(_)
572+
));
573+
debug_assert!(matches!(
574+
locked_wallet.public_descriptor(KeychainKind::Internal),
575+
ExtendedDescriptor::Wpkh(_)
576+
));
577+
578+
let mut tx_builder = locked_wallet.build_tx();
579+
tx_builder.only_witness_utxo();
580+
581+
for input in &must_spend {
582+
let psbt_input = psbt::Input {
583+
witness_utxo: Some(input.previous_utxo.clone()),
584+
..Default::default()
585+
};
586+
let weight = Weight::from_wu(input.satisfaction_weight);
587+
tx_builder.add_foreign_utxo(input.outpoint, psbt_input, weight).map_err(|_| ())?;
588+
}
589+
590+
for output in must_pay_to {
591+
tx_builder.add_recipient(output.script_pubkey.clone(), output.value);
592+
}
593+
594+
tx_builder.fee_rate(fee_rate);
595+
tx_builder.exclude_unconfirmed();
596+
597+
tx_builder
598+
.finish()
599+
.map_err(|e| {
600+
log_error!(self.logger, "Failed to select confirmed UTXOs: {}", e);
601+
})?
602+
.unsigned_tx
603+
.input
604+
.iter()
605+
.filter(|txin| must_spend.iter().all(|input| input.outpoint != txin.previous_output))
606+
.filter_map(|txin| {
607+
locked_wallet
608+
.tx_details(txin.previous_output.txid)
609+
.map(|tx_details| tx_details.tx.deref().clone())
610+
.map(|prevtx| FundingTxInput::new_p2wpkh(prevtx, txin.previous_output.vout))
611+
})
612+
.collect::<Result<Vec<_>, ()>>()
613+
}
614+
562615
fn list_confirmed_utxos_inner(&self) -> Result<Vec<Utxo>, ()> {
563616
let locked_wallet = self.inner.lock().unwrap();
564617
let mut utxos = Vec::new();
@@ -696,7 +749,7 @@ impl Wallet {
696749
Err(e) => {
697750
log_error!(self.logger, "Failed to extract transaction: {}", e);
698751
Err(())
699-
}
752+
},
700753
}
701754
}
702755

0 commit comments

Comments
 (0)