Skip to content

Commit 7104ed2

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 29dd036 commit 7104ed2

File tree

3 files changed

+146
-4
lines changed

3 files changed

+146
-4
lines changed

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: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
106106

107107
pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance};
108108
use bitcoin::secp256k1::PublicKey;
109+
use bitcoin::Amount;
109110
#[cfg(feature = "uniffi")]
110111
pub use builder::ArcedNodeBuilder as Builder;
111112
pub use builder::BuildError;
@@ -128,10 +129,11 @@ use graph::NetworkGraph;
128129
pub use io::utils::generate_entropy_mnemonic;
129130
use io::utils::write_node_metrics;
130131
use lightning::chain::BestBlock;
131-
use lightning::events::bump_transaction::Wallet as LdkWallet;
132+
use lightning::events::bump_transaction::{Input, Wallet as LdkWallet};
132133
use lightning::impl_writeable_tlv_based;
133134
use lightning::ln::channel_state::ChannelShutdownState;
134135
use lightning::ln::channelmanager::PaymentId;
136+
use lightning::ln::funding::SpliceContribution;
135137
use lightning::ln::msgs::SocketAddress;
136138
use lightning::routing::gossip::NodeAlias;
137139
use lightning::util::persist::KVStoreSync;
@@ -1200,6 +1202,96 @@ impl Node {
12001202
)
12011203
}
12021204

1205+
/// Add funds from the on-chain wallet into an existing channel.
1206+
///
1207+
/// This provides for increasing a channel's outbound liquidity without re-balancing or closing
1208+
/// it. Once negotiation with the counterparty is complete, the channel remains operational
1209+
/// while waiting for a new funding transaction to confirm.
1210+
pub fn splice_in(
1211+
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
1212+
splice_amount_sats: u64,
1213+
) -> Result<(), Error> {
1214+
let open_channels =
1215+
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
1216+
if let Some(channel_details) =
1217+
open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0)
1218+
{
1219+
self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?;
1220+
1221+
const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
1222+
1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
1223+
let funding_txo = channel_details.funding_txo.ok_or_else(|| {
1224+
log_error!(self.logger, "Failed to splice channel: channel not yet ready",);
1225+
Error::ChannelSplicingFailed
1226+
})?;
1227+
let shared_input = Input {
1228+
outpoint: funding_txo.into_bitcoin_outpoint(),
1229+
previous_utxo: bitcoin::TxOut {
1230+
value: Amount::from_sat(channel_details.channel_value_satoshis),
1231+
script_pubkey: lightning::ln::chan_utils::make_funding_redeemscript(
1232+
&PublicKey::from_slice(&[2; 33]).unwrap(),
1233+
&PublicKey::from_slice(&[2; 33]).unwrap(),
1234+
)
1235+
.to_p2wsh(),
1236+
},
1237+
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT
1238+
+ lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT,
1239+
};
1240+
1241+
let shared_output = bitcoin::TxOut {
1242+
value: shared_input.previous_utxo.value + Amount::from_sat(splice_amount_sats),
1243+
script_pubkey: lightning::ln::chan_utils::make_funding_redeemscript(
1244+
&PublicKey::from_slice(&[2; 33]).unwrap(),
1245+
&PublicKey::from_slice(&[2; 33]).unwrap(),
1246+
)
1247+
.to_p2wsh(),
1248+
};
1249+
1250+
let fee_rate = self.wallet.estimate_channel_funding_fee_rate();
1251+
1252+
let inputs = self
1253+
.wallet
1254+
.select_confirmed_utxos(vec![shared_input], &[shared_output], fee_rate)
1255+
.map_err(|()| {
1256+
log_error!(
1257+
self.logger,
1258+
"Failed to splice channel: insufficient confirmed UTXOs",
1259+
);
1260+
Error::ChannelSplicingFailed
1261+
})?;
1262+
1263+
let contribution = SpliceContribution::SpliceIn {
1264+
value: Amount::from_sat(splice_amount_sats),
1265+
inputs,
1266+
change_script: None,
1267+
};
1268+
1269+
let funding_feerate_per_kw = fee_rate.to_sat_per_kwu().try_into().unwrap_or(u32::MAX);
1270+
1271+
self.channel_manager
1272+
.splice_channel(
1273+
&channel_details.channel_id,
1274+
&counterparty_node_id,
1275+
contribution,
1276+
funding_feerate_per_kw,
1277+
None,
1278+
)
1279+
.map_err(|e| {
1280+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1281+
Error::ChannelSplicingFailed
1282+
})
1283+
} else {
1284+
log_error!(
1285+
self.logger,
1286+
"Channel not found for user_channel_id: {:?} and counterparty: {}",
1287+
user_channel_id,
1288+
counterparty_node_id
1289+
);
1290+
1291+
Err(Error::ChannelSplicingFailed)
1292+
}
1293+
}
1294+
12031295
/// Manually sync the LDK and BDK wallets with the current chain state and update the fee rate
12041296
/// cache.
12051297
///

src/wallet/mod.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
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};
@@ -19,19 +20,20 @@ use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
1920
use bitcoin::blockdata::locktime::absolute::LockTime;
2021
use bitcoin::hashes::Hash;
2122
use bitcoin::key::XOnlyPublicKey;
22-
use bitcoin::psbt::Psbt;
23+
use bitcoin::psbt::{self, Psbt};
2324
use bitcoin::secp256k1::ecdh::SharedSecret;
2425
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
2526
use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
2627
use bitcoin::{
27-
Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash,
28+
Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
2829
WitnessProgram, WitnessVersion,
2930
};
3031
use lightning::chain::chaininterface::BroadcasterInterface;
3132
use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
3233
use lightning::chain::{BestBlock, Listen};
33-
use lightning::events::bump_transaction::{Utxo, WalletSource};
34+
use lightning::events::bump_transaction::{Input, Utxo, WalletSource};
3435
use lightning::ln::channelmanager::PaymentId;
36+
use lightning::ln::funding::FundingTxInput;
3537
use lightning::ln::inbound_payment::ExpandedKey;
3638
use lightning::ln::msgs::UnsignedGossipMessage;
3739
use lightning::ln::script::ShutdownScript;
@@ -273,6 +275,10 @@ impl Wallet {
273275
Ok(tx)
274276
}
275277

278+
pub(crate) fn estimate_channel_funding_fee_rate(&self) -> FeeRate {
279+
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding)
280+
}
281+
276282
pub(crate) fn get_new_address(&self) -> Result<bitcoin::Address, Error> {
277283
let mut locked_wallet = self.inner.lock().unwrap();
278284
let mut locked_persister = self.persister.lock().unwrap();
@@ -559,6 +565,47 @@ impl Wallet {
559565
Ok(txid)
560566
}
561567

568+
pub(crate) fn select_confirmed_utxos(
569+
&self, must_spend: Vec<Input>, must_pay_to: &[TxOut], fee_rate: FeeRate,
570+
) -> Result<Vec<FundingTxInput>, ()> {
571+
let mut locked_wallet = self.inner.lock().unwrap();
572+
let mut tx_builder = locked_wallet.build_tx();
573+
tx_builder.only_witness_utxo();
574+
575+
for input in &must_spend {
576+
let psbt_input = psbt::Input {
577+
witness_utxo: Some(input.previous_utxo.clone()),
578+
..Default::default()
579+
};
580+
let weight = Weight::from_wu(input.satisfaction_weight);
581+
tx_builder.add_foreign_utxo(input.outpoint, psbt_input, weight).map_err(|_| ())?;
582+
}
583+
584+
for output in must_pay_to {
585+
tx_builder.add_recipient(output.script_pubkey.clone(), output.value);
586+
}
587+
588+
tx_builder.fee_rate(fee_rate);
589+
tx_builder.exclude_unconfirmed();
590+
591+
tx_builder
592+
.finish()
593+
.map_err(|e| {
594+
log_error!(self.logger, "Failed to select confirmed UTXOs: {}", e);
595+
})?
596+
.unsigned_tx
597+
.input
598+
.iter()
599+
.filter(|txin| must_spend.iter().all(|input| input.outpoint != txin.previous_output))
600+
.filter_map(|txin| {
601+
locked_wallet
602+
.tx_details(txin.previous_output.txid)
603+
.map(|tx_details| tx_details.tx.deref().clone())
604+
.map(|prevtx| FundingTxInput::new_p2wpkh(prevtx, txin.previous_output.vout))
605+
})
606+
.collect::<Result<Vec<_>, ()>>()
607+
}
608+
562609
fn list_confirmed_utxos_inner(&self) -> Result<Vec<Utxo>, ()> {
563610
let locked_wallet = self.inner.lock().unwrap();
564611
let mut utxos = Vec::new();

0 commit comments

Comments
 (0)