Skip to content

Commit 37d8f85

Browse files
jkczyzbenthecarman
authored andcommitted
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 57822a6 commit 37d8f85

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
@@ -110,6 +110,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
110110
use crate::scoring::setup_background_pathfinding_scores_sync;
111111
pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance};
112112
use bitcoin::secp256k1::PublicKey;
113+
use bitcoin::Amount;
113114
#[cfg(feature = "uniffi")]
114115
pub use builder::ArcedNodeBuilder as Builder;
115116
pub use builder::BuildError;
@@ -132,10 +133,11 @@ use graph::NetworkGraph;
132133
pub use io::utils::generate_entropy_mnemonic;
133134
use io::utils::write_node_metrics;
134135
use lightning::chain::BestBlock;
135-
use lightning::events::bump_transaction::Wallet as LdkWallet;
136+
use lightning::events::bump_transaction::{Input, Wallet as LdkWallet};
136137
use lightning::impl_writeable_tlv_based;
137138
use lightning::ln::channel_state::ChannelShutdownState;
138139
use lightning::ln::channelmanager::PaymentId;
140+
use lightning::ln::funding::SpliceContribution;
139141
use lightning::ln::msgs::SocketAddress;
140142
use lightning::routing::gossip::NodeAlias;
141143
use lightning::util::persist::KVStoreSync;
@@ -1223,6 +1225,96 @@ impl Node {
12231225
)
12241226
}
12251227

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

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)