Skip to content

Commit 83c0a81

Browse files
jkczyzclaude
andcommitted
Update splice_in/splice_out to use new LDK two-phase funding API
The LDK dependency bump introduced a new splicing API that separates negotiation from coin selection, letting LDK handle transaction construction internally rather than requiring manual UTXO selection and change address management. Generated with the assistance of AI (Claude Code). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f2ed029 commit 83c0a81

File tree

3 files changed

+83
-136
lines changed

3 files changed

+83
-136
lines changed

src/lib.rs

Lines changed: 49 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,10 @@ use gossip::GossipSource;
138138
use graph::NetworkGraph;
139139
use io::utils::write_node_metrics;
140140
use lightning::chain::BestBlock;
141-
use lightning::events::bump_transaction::{Input, Wallet as LdkWallet};
141+
use lightning::events::bump_transaction::Wallet as LdkWallet;
142142
use lightning::impl_writeable_tlv_based;
143-
use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT;
144143
use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState};
145144
use lightning::ln::channelmanager::PaymentId;
146-
use lightning::ln::funding::SpliceContribution;
147145
use lightning::ln::msgs::SocketAddress;
148146
use lightning::routing::gossip::NodeAlias;
149147
use lightning::util::persist::KVStoreSync;
@@ -1290,84 +1288,43 @@ impl Node {
12901288
{
12911289
self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?;
12921290

1293-
const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
1294-
1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
1295-
1296-
let funding_txo = channel_details.funding_txo.ok_or_else(|| {
1297-
log_error!(self.logger, "Failed to splice channel: channel not yet ready",);
1298-
Error::ChannelSplicingFailed
1299-
})?;
1300-
1301-
let funding_output = channel_details.get_funding_output().ok_or_else(|| {
1302-
log_error!(self.logger, "Failed to splice channel: channel not yet ready");
1303-
Error::ChannelSplicingFailed
1304-
})?;
1305-
1306-
let shared_input = Input {
1307-
outpoint: funding_txo.into_bitcoin_outpoint(),
1308-
previous_utxo: funding_output.clone(),
1309-
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
1310-
};
1311-
1312-
let shared_output = bitcoin::TxOut {
1313-
value: shared_input.previous_utxo.value + Amount::from_sat(splice_amount_sats),
1314-
// will not actually be the exact same script pubkey after splice
1315-
// but it is the same size and good enough for coin selection purposes
1316-
script_pubkey: funding_output.script_pubkey.clone(),
1317-
};
1318-
13191291
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
13201292

1321-
let inputs = self
1322-
.wallet
1323-
.select_confirmed_utxos(vec![shared_input], &[shared_output], fee_rate)
1324-
.map_err(|()| {
1325-
log_error!(
1326-
self.logger,
1327-
"Failed to splice channel: insufficient confirmed UTXOs",
1328-
);
1293+
// Phase 1: Initiate splice negotiation
1294+
let funding_template = self
1295+
.channel_manager
1296+
.splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate)
1297+
.map_err(|e| {
1298+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
13291299
Error::ChannelSplicingFailed
13301300
})?;
13311301

1332-
let change_address = self.wallet.get_new_internal_address()?;
1333-
1334-
let contribution = SpliceContribution::splice_in(
1335-
Amount::from_sat(splice_amount_sats),
1336-
inputs,
1337-
Some(change_address.script_pubkey()),
1338-
);
1302+
// Phase 2: Coin selection via LdkWallet (wraps our WalletSource)
1303+
let ldk_wallet = LdkWallet::new(Arc::clone(&self.wallet), Arc::clone(&self.logger));
1304+
let contribution = self
1305+
.runtime
1306+
.block_on(
1307+
funding_template.splice_in(Amount::from_sat(splice_amount_sats), &ldk_wallet),
1308+
)
1309+
.map_err(|()| {
1310+
log_error!(self.logger, "Failed to splice channel: coin selection failed");
1311+
Error::ChannelSplicingFailed
1312+
})?;
13391313

1340-
let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() {
1341-
Ok(fee_rate) => fee_rate,
1342-
Err(_) => {
1343-
debug_assert!(false);
1344-
fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding)
1345-
},
1346-
};
1314+
// Persist wallet state after coin selection
1315+
self.wallet.persist()?;
13471316

1317+
// Phase 3: Submit contribution
13481318
self.channel_manager
1349-
.splice_channel(
1319+
.funding_contributed(
13501320
&channel_details.channel_id,
13511321
&counterparty_node_id,
13521322
contribution,
1353-
funding_feerate_per_kw,
13541323
None,
13551324
)
13561325
.map_err(|e| {
13571326
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1358-
let tx = bitcoin::Transaction {
1359-
version: bitcoin::transaction::Version::TWO,
1360-
lock_time: bitcoin::absolute::LockTime::ZERO,
1361-
input: vec![],
1362-
output: vec![bitcoin::TxOut {
1363-
value: Amount::ZERO,
1364-
script_pubkey: change_address.script_pubkey(),
1365-
}],
1366-
};
1367-
match self.wallet.cancel_tx(&tx) {
1368-
Ok(()) => Error::ChannelSplicingFailed,
1369-
Err(e) => e,
1370-
}
1327+
Error::ChannelSplicingFailed
13711328
})
13721329
} else {
13731330
log_error!(
@@ -1376,7 +1333,6 @@ impl Node {
13761333
user_channel_id,
13771334
counterparty_node_id
13781335
);
1379-
13801336
Err(Error::ChannelSplicingFailed)
13811337
}
13821338
}
@@ -1407,27 +1363,40 @@ impl Node {
14071363

14081364
self.wallet.parse_and_validate_address(address)?;
14091365

1410-
let contribution = SpliceContribution::splice_out(vec![bitcoin::TxOut {
1366+
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1367+
1368+
// Phase 1: Initiate splice negotiation
1369+
let funding_template = self
1370+
.channel_manager
1371+
.splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate)
1372+
.map_err(|e| {
1373+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1374+
Error::ChannelSplicingFailed
1375+
})?;
1376+
1377+
// Phase 2: Build contribution with splice-out outputs
1378+
let outputs = vec![bitcoin::TxOut {
14111379
value: Amount::from_sat(splice_amount_sats),
14121380
script_pubkey: address.script_pubkey(),
1413-
}]);
1381+
}];
1382+
let ldk_wallet = LdkWallet::new(Arc::clone(&self.wallet), Arc::clone(&self.logger));
1383+
let contribution = self
1384+
.runtime
1385+
.block_on(funding_template.splice_out(outputs, &ldk_wallet))
1386+
.map_err(|()| {
1387+
log_error!(self.logger, "Failed to splice channel: coin selection failed");
1388+
Error::ChannelSplicingFailed
1389+
})?;
14141390

1415-
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1416-
let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() {
1417-
Ok(fee_rate) => fee_rate,
1418-
Err(_) => {
1419-
debug_assert!(false, "FeeRate should always fit within u32");
1420-
log_error!(self.logger, "FeeRate should always fit within u32");
1421-
fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding)
1422-
},
1423-
};
1391+
// Persist wallet state after coin selection
1392+
self.wallet.persist()?;
14241393

1394+
// Phase 3: Submit contribution
14251395
self.channel_manager
1426-
.splice_channel(
1396+
.funding_contributed(
14271397
&channel_details.channel_id,
14281398
&counterparty_node_id,
14291399
contribution,
1430-
funding_feerate_per_kw,
14311400
None,
14321401
)
14331402
.map_err(|e| {

src/wallet/mod.rs

Lines changed: 33 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use std::str::FromStr;
1111
use std::sync::{Arc, Mutex};
1212

1313
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
14-
use bdk_wallet::descriptor::ExtendedDescriptor;
1514
use bdk_wallet::event::WalletEvent;
1615
#[allow(deprecated)]
1716
use bdk_wallet::SignOptions;
@@ -21,20 +20,20 @@ use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
2120
use bitcoin::blockdata::locktime::absolute::LockTime;
2221
use bitcoin::hashes::Hash;
2322
use bitcoin::key::XOnlyPublicKey;
24-
use bitcoin::psbt::{self, Psbt};
23+
use bitcoin::psbt::Psbt;
2524
use bitcoin::secp256k1::ecdh::SharedSecret;
2625
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
2726
use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
27+
use bitcoin::transaction::Sequence;
2828
use bitcoin::{
29-
Address, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
29+
Address, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash,
3030
WitnessProgram, WitnessVersion,
3131
};
3232
use lightning::chain::chaininterface::BroadcasterInterface;
3333
use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
3434
use lightning::chain::{BestBlock, Listen};
35-
use lightning::events::bump_transaction::{Input, Utxo, WalletSource};
35+
use lightning::events::bump_transaction::{Utxo, WalletSource};
3636
use lightning::ln::channelmanager::PaymentId;
37-
use lightning::ln::funding::FundingTxInput;
3837
use lightning::ln::inbound_payment::ExpandedKey;
3938
use lightning::ln::msgs::UnsignedGossipMessage;
4039
use lightning::ln::script::ShutdownScript;
@@ -445,6 +444,16 @@ impl Wallet {
445444
Ok(())
446445
}
447446

447+
pub(crate) fn persist(&self) -> Result<(), Error> {
448+
let mut locked_wallet = self.inner.lock().unwrap();
449+
let mut locked_persister = self.persister.lock().unwrap();
450+
locked_wallet.persist(&mut locked_persister).map_err(|e| {
451+
log_error!(self.logger, "Failed to persist wallet: {}", e);
452+
Error::PersistenceFailed
453+
})?;
454+
Ok(())
455+
}
456+
448457
pub(crate) fn get_balances(
449458
&self, total_anchor_channels_reserve_sats: u64,
450459
) -> Result<(u64, u64), Error> {
@@ -708,56 +717,6 @@ impl Wallet {
708717
Ok(txid)
709718
}
710719

711-
pub(crate) fn select_confirmed_utxos(
712-
&self, must_spend: Vec<Input>, must_pay_to: &[TxOut], fee_rate: FeeRate,
713-
) -> Result<Vec<FundingTxInput>, ()> {
714-
let mut locked_wallet = self.inner.lock().unwrap();
715-
debug_assert!(matches!(
716-
locked_wallet.public_descriptor(KeychainKind::External),
717-
ExtendedDescriptor::Wpkh(_)
718-
));
719-
debug_assert!(matches!(
720-
locked_wallet.public_descriptor(KeychainKind::Internal),
721-
ExtendedDescriptor::Wpkh(_)
722-
));
723-
724-
let mut tx_builder = locked_wallet.build_tx();
725-
tx_builder.only_witness_utxo();
726-
727-
for input in &must_spend {
728-
let psbt_input = psbt::Input {
729-
witness_utxo: Some(input.previous_utxo.clone()),
730-
..Default::default()
731-
};
732-
let weight = Weight::from_wu(input.satisfaction_weight);
733-
tx_builder.add_foreign_utxo(input.outpoint, psbt_input, weight).map_err(|_| ())?;
734-
}
735-
736-
for output in must_pay_to {
737-
tx_builder.add_recipient(output.script_pubkey.clone(), output.value);
738-
}
739-
740-
tx_builder.fee_rate(fee_rate);
741-
tx_builder.exclude_unconfirmed();
742-
743-
tx_builder
744-
.finish()
745-
.map_err(|e| {
746-
log_error!(self.logger, "Failed to select confirmed UTXOs: {}", e);
747-
})?
748-
.unsigned_tx
749-
.input
750-
.iter()
751-
.filter(|txin| must_spend.iter().all(|input| input.outpoint != txin.previous_output))
752-
.filter_map(|txin| {
753-
locked_wallet
754-
.tx_details(txin.previous_output.txid)
755-
.map(|tx_details| tx_details.tx.deref().clone())
756-
.map(|prevtx| FundingTxInput::new_p2wpkh(prevtx, txin.previous_output.vout))
757-
})
758-
.collect::<Result<Vec<_>, ()>>()
759-
}
760-
761720
fn list_confirmed_utxos_inner(&self) -> Result<Vec<Utxo>, ()> {
762721
let locked_wallet = self.inner.lock().unwrap();
763722
let mut utxos = Vec::new();
@@ -831,6 +790,7 @@ impl Wallet {
831790
},
832791
satisfaction_weight: 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64 +
833792
1 /* witness items */ + 1 /* schnorr sig len */ + 64, // schnorr sig
793+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
834794
};
835795
utxos.push(utxo);
836796
},
@@ -1094,6 +1054,24 @@ impl WalletSource for Wallet {
10941054
async move { self.get_change_script_inner() }
10951055
}
10961056

1057+
fn get_prevtx<'a>(
1058+
&'a self, outpoint: OutPoint,
1059+
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
1060+
async move {
1061+
let locked_wallet = self.inner.lock().unwrap();
1062+
locked_wallet
1063+
.tx_details(outpoint.txid)
1064+
.map(|tx_details| tx_details.tx.deref().clone())
1065+
.ok_or_else(|| {
1066+
log_error!(
1067+
self.logger,
1068+
"Failed to get previous transaction for {}",
1069+
outpoint.txid
1070+
);
1071+
})
1072+
}
1073+
}
1074+
10971075
fn sign_psbt<'a>(
10981076
&'a self, psbt: Psbt,
10991077
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {

tests/integration_tests_rust.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,7 @@ async fn splice_channel() {
983983
expect_channel_ready_event!(node_a, node_b.node_id());
984984
expect_channel_ready_event!(node_b, node_a.node_id());
985985

986-
let expected_splice_in_fee_sat = 252;
986+
let expected_splice_in_fee_sat = 253;
987987

988988
let payments = node_b.list_payments();
989989
let payment =

0 commit comments

Comments
 (0)