Skip to content

Commit a0a8abe

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 14ae753 commit a0a8abe

File tree

6 files changed

+206
-5
lines changed

6 files changed

+206
-5
lines changed

bindings/ldk_node.udl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ 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]
153155
void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
154156
[Throws=NodeError]
155157
void force_close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, string? reason);
@@ -290,6 +292,7 @@ enum NodeError {
290292
"ProbeSendingFailed",
291293
"ChannelCreationFailed",
292294
"ChannelClosingFailed",
295+
"ChannelSplicingFailed",
293296
"ChannelConfigUpdateFailed",
294297
"PersistenceFailed",
295298
"FeerateEstimationUpdateFailed",

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/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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,6 +1741,7 @@ where
17411741
user_channel_id,
17421742
counterparty_node_id,
17431743
abandoned_funding_txo,
1744+
contributed_outputs,
17441745
..
17451746
} => {
17461747
if let Some(funding_txo) = abandoned_funding_txo {
@@ -1760,6 +1761,17 @@ where
17601761
);
17611762
}
17621763

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+
17631775
let event = Event::SpliceFailed {
17641776
channel_id,
17651777
user_channel_id: UserChannelId(user_channel_id),

src/lib.rs

Lines changed: 117 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::{ChannelDetails as LdkChannelDetails, 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;
@@ -179,6 +183,7 @@ pub struct Node {
179183
wallet: Arc<Wallet>,
180184
chain_source: Arc<ChainSource>,
181185
tx_broadcaster: Arc<Broadcaster>,
186+
fee_estimator: Arc<OnchainFeeEstimator>,
182187
event_queue: Arc<EventQueue<Arc<Logger>>>,
183188
channel_manager: Arc<ChannelManager>,
184189
chain_monitor: Arc<ChainMonitor>,
@@ -1236,6 +1241,117 @@ impl Node {
12361241
)
12371242
}
12381243

1244+
/// Add funds from the on-chain wallet into an existing channel.
1245+
///
1246+
/// This provides for increasing a channel's outbound liquidity without re-balancing or closing
1247+
/// it. Once negotiation with the counterparty is complete, the channel remains operational
1248+
/// while waiting for a new funding transaction to confirm.
1249+
///
1250+
/// # Experimental API
1251+
///
1252+
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
1253+
/// this classification may change in the future.
1254+
pub fn splice_in(
1255+
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
1256+
splice_amount_sats: u64,
1257+
) -> Result<(), Error> {
1258+
let open_channels =
1259+
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
1260+
if let Some(channel_details) =
1261+
open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0)
1262+
{
1263+
self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?;
1264+
1265+
const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
1266+
1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
1267+
1268+
// Used for creating a redeem script for the previous funding txo and the new funding
1269+
// txo. Only needed when selecting which UTXOs to include in the funding tx that would
1270+
// be sufficient to pay for fees. Hence, the value does not matter.
1271+
let dummy_pubkey = PublicKey::from_slice(&[2; 33]).unwrap();
1272+
1273+
let funding_txo = channel_details.funding_txo.ok_or_else(|| {
1274+
log_error!(self.logger, "Failed to splice channel: channel not yet ready",);
1275+
Error::ChannelSplicingFailed
1276+
})?;
1277+
1278+
let shared_input = Input {
1279+
outpoint: funding_txo.into_bitcoin_outpoint(),
1280+
previous_utxo: bitcoin::TxOut {
1281+
value: Amount::from_sat(channel_details.channel_value_satoshis),
1282+
script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey)
1283+
.to_p2wsh(),
1284+
},
1285+
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
1286+
};
1287+
1288+
let shared_output = bitcoin::TxOut {
1289+
value: shared_input.previous_utxo.value + Amount::from_sat(splice_amount_sats),
1290+
script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey).to_p2wsh(),
1291+
};
1292+
1293+
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1294+
1295+
let inputs = self
1296+
.wallet
1297+
.select_confirmed_utxos(vec![shared_input], &[shared_output], fee_rate)
1298+
.map_err(|()| {
1299+
log_error!(
1300+
self.logger,
1301+
"Failed to splice channel: insufficient confirmed UTXOs",
1302+
);
1303+
Error::ChannelSplicingFailed
1304+
})?;
1305+
1306+
let change_address = self.wallet.get_new_internal_address()?;
1307+
1308+
let contribution = SpliceContribution::SpliceIn {
1309+
value: Amount::from_sat(splice_amount_sats),
1310+
inputs,
1311+
change_script: Some(change_address.script_pubkey()),
1312+
};
1313+
1314+
let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() {
1315+
Ok(fee_rate) => fee_rate,
1316+
Err(_) => {
1317+
debug_assert!(false);
1318+
fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding)
1319+
},
1320+
};
1321+
1322+
self.channel_manager
1323+
.splice_channel(
1324+
&channel_details.channel_id,
1325+
&counterparty_node_id,
1326+
contribution,
1327+
funding_feerate_per_kw,
1328+
None,
1329+
)
1330+
.map_err(|e| {
1331+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1332+
let tx = bitcoin::Transaction {
1333+
version: bitcoin::transaction::Version::TWO,
1334+
lock_time: bitcoin::absolute::LockTime::ZERO,
1335+
input: vec![],
1336+
output: vec![bitcoin::TxOut { value: Amount::ZERO, script_pubkey: change_address.script_pubkey() }],
1337+
};
1338+
match self.wallet.cancel_tx(&tx) {
1339+
Ok(()) => Error::ChannelSplicingFailed,
1340+
Err(e) => e,
1341+
}
1342+
})
1343+
} else {
1344+
log_error!(
1345+
self.logger,
1346+
"Channel not found for user_channel_id {} and counterparty {}",
1347+
user_channel_id,
1348+
counterparty_node_id
1349+
);
1350+
1351+
Err(Error::ChannelSplicingFailed)
1352+
}
1353+
}
1354+
12391355
/// Manually sync the LDK and BDK wallets with the current chain state and update the fee rate
12401356
/// cache.
12411357
///

src/wallet/mod.rs

Lines changed: 70 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;
@@ -285,7 +288,7 @@ impl Wallet {
285288
Ok(address_info.address)
286289
}
287290

288-
fn get_new_internal_address(&self) -> Result<bitcoin::Address, Error> {
291+
pub(crate) fn get_new_internal_address(&self) -> Result<bitcoin::Address, Error> {
289292
let mut locked_wallet = self.inner.lock().unwrap();
290293
let mut locked_persister = self.persister.lock().unwrap();
291294

@@ -297,6 +300,19 @@ impl Wallet {
297300
Ok(address_info.address)
298301
}
299302

303+
pub(crate) fn cancel_tx(&self, tx: &Transaction) -> Result<(), Error> {
304+
let mut locked_wallet = self.inner.lock().unwrap();
305+
let mut locked_persister = self.persister.lock().unwrap();
306+
307+
locked_wallet.cancel_tx(tx);
308+
locked_wallet.persist(&mut locked_persister).map_err(|e| {
309+
log_error!(self.logger, "Failed to persist wallet: {}", e);
310+
Error::PersistenceFailed
311+
})?;
312+
313+
Ok(())
314+
}
315+
300316
pub(crate) fn get_balances(
301317
&self, total_anchor_channels_reserve_sats: u64,
302318
) -> Result<(u64, u64), Error> {
@@ -559,6 +575,56 @@ impl Wallet {
559575
Ok(txid)
560576
}
561577

578+
pub(crate) fn select_confirmed_utxos(
579+
&self, must_spend: Vec<Input>, must_pay_to: &[TxOut], fee_rate: FeeRate,
580+
) -> Result<Vec<FundingTxInput>, ()> {
581+
let mut locked_wallet = self.inner.lock().unwrap();
582+
debug_assert!(matches!(
583+
locked_wallet.public_descriptor(KeychainKind::External),
584+
ExtendedDescriptor::Wpkh(_)
585+
));
586+
debug_assert!(matches!(
587+
locked_wallet.public_descriptor(KeychainKind::Internal),
588+
ExtendedDescriptor::Wpkh(_)
589+
));
590+
591+
let mut tx_builder = locked_wallet.build_tx();
592+
tx_builder.only_witness_utxo();
593+
594+
for input in &must_spend {
595+
let psbt_input = psbt::Input {
596+
witness_utxo: Some(input.previous_utxo.clone()),
597+
..Default::default()
598+
};
599+
let weight = Weight::from_wu(input.satisfaction_weight);
600+
tx_builder.add_foreign_utxo(input.outpoint, psbt_input, weight).map_err(|_| ())?;
601+
}
602+
603+
for output in must_pay_to {
604+
tx_builder.add_recipient(output.script_pubkey.clone(), output.value);
605+
}
606+
607+
tx_builder.fee_rate(fee_rate);
608+
tx_builder.exclude_unconfirmed();
609+
610+
tx_builder
611+
.finish()
612+
.map_err(|e| {
613+
log_error!(self.logger, "Failed to select confirmed UTXOs: {}", e);
614+
})?
615+
.unsigned_tx
616+
.input
617+
.iter()
618+
.filter(|txin| must_spend.iter().all(|input| input.outpoint != txin.previous_output))
619+
.filter_map(|txin| {
620+
locked_wallet
621+
.tx_details(txin.previous_output.txid)
622+
.map(|tx_details| tx_details.tx.deref().clone())
623+
.map(|prevtx| FundingTxInput::new_p2wpkh(prevtx, txin.previous_output.vout))
624+
})
625+
.collect::<Result<Vec<_>, ()>>()
626+
}
627+
562628
fn list_confirmed_utxos_inner(&self) -> Result<Vec<Utxo>, ()> {
563629
let locked_wallet = self.inner.lock().unwrap();
564630
let mut utxos = Vec::new();

0 commit comments

Comments
 (0)