Skip to content

Commit d9336f2

Browse files
jkczyzclaude
andcommitted
Bump LDK dependency for major splicing API changes
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 7997ef9 commit d9336f2

File tree

4 files changed

+128
-102
lines changed

4 files changed

+128
-102
lines changed

Cargo.toml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,17 @@ default = []
3939
#lightning-liquidity = { version = "0.2.0", features = ["std"] }
4040
#lightning-macros = { version = "0.2.0" }
4141

42-
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] }
43-
lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
44-
lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] }
45-
lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
46-
lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["tokio"] }
47-
lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
48-
lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
49-
lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["rest-client", "rpc-client", "tokio"] }
50-
lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] }
51-
lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] }
52-
lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
42+
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["std"] }
43+
lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }
44+
lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["std"] }
45+
lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }
46+
lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["tokio"] }
47+
lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }
48+
lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }
49+
lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["rest-client", "rpc-client", "tokio"] }
50+
lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["esplora-async-https", "time", "electrum-rustls-ring"] }
51+
lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["std"] }
52+
lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }
5353

5454
bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] }
5555
bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]}
@@ -78,13 +78,13 @@ log = { version = "0.4.22", default-features = false, features = ["std"]}
7878
vss-client = { package = "vss-client-ng", version = "0.5" }
7979
prost = { version = "0.11.6", default-features = false}
8080
#bitcoin-payment-instructions = { version = "0.6" }
81-
bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", rev = "ea50a9d2a8da524b69a2af43233706666cf2ffa5" }
81+
bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "c8c470b0a4241a875bf1a1038482825e41c28bd2" }
8282

8383
[target.'cfg(windows)'.dependencies]
8484
winapi = { version = "0.3", features = ["winbase"] }
8585

8686
[dev-dependencies]
87-
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std", "_test_utils"] }
87+
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["std", "_test_utils"] }
8888
rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] }
8989
proptest = "1.0.0"
9090
regex = "1.5.6"

src/lib.rs

Lines changed: 38 additions & 82 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::sign::EntropySource;
@@ -1295,84 +1293,37 @@ impl Node {
12951293
{
12961294
self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?;
12971295

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

1326-
let inputs = self
1327-
.wallet
1328-
.select_confirmed_utxos(vec![shared_input], &[shared_output], fee_rate)
1329-
.map_err(|()| {
1330-
log_error!(
1331-
self.logger,
1332-
"Failed to splice channel: insufficient confirmed UTXOs",
1333-
);
1298+
let funding_template = self
1299+
.channel_manager
1300+
.splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate)
1301+
.map_err(|e| {
1302+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
13341303
Error::ChannelSplicingFailed
13351304
})?;
13361305

1337-
let change_address = self.wallet.get_new_internal_address()?;
1338-
1339-
let contribution = SpliceContribution::splice_in(
1340-
Amount::from_sat(splice_amount_sats),
1341-
inputs,
1342-
Some(change_address.script_pubkey()),
1343-
);
1344-
1345-
let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() {
1346-
Ok(fee_rate) => fee_rate,
1347-
Err(_) => {
1348-
debug_assert!(false);
1349-
fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding)
1350-
},
1351-
};
1306+
let contribution = self
1307+
.runtime
1308+
.block_on(
1309+
funding_template
1310+
.splice_in(Amount::from_sat(splice_amount_sats), Arc::clone(&self.wallet)),
1311+
)
1312+
.map_err(|()| {
1313+
log_error!(self.logger, "Failed to splice channel: coin selection failed");
1314+
Error::ChannelSplicingFailed
1315+
})?;
13521316

13531317
self.channel_manager
1354-
.splice_channel(
1318+
.funding_contributed(
13551319
&channel_details.channel_id,
13561320
&counterparty_node_id,
13571321
contribution,
1358-
funding_feerate_per_kw,
13591322
None,
13601323
)
13611324
.map_err(|e| {
13621325
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1363-
let tx = bitcoin::Transaction {
1364-
version: bitcoin::transaction::Version::TWO,
1365-
lock_time: bitcoin::absolute::LockTime::ZERO,
1366-
input: vec![],
1367-
output: vec![bitcoin::TxOut {
1368-
value: Amount::ZERO,
1369-
script_pubkey: change_address.script_pubkey(),
1370-
}],
1371-
};
1372-
match self.wallet.cancel_tx(&tx) {
1373-
Ok(()) => Error::ChannelSplicingFailed,
1374-
Err(e) => e,
1375-
}
1326+
Error::ChannelSplicingFailed
13761327
})
13771328
} else {
13781329
log_error!(
@@ -1381,7 +1332,6 @@ impl Node {
13811332
user_channel_id,
13821333
counterparty_node_id
13831334
);
1384-
13851335
Err(Error::ChannelSplicingFailed)
13861336
}
13871337
}
@@ -1412,27 +1362,33 @@ impl Node {
14121362

14131363
self.wallet.parse_and_validate_address(address)?;
14141364

1415-
let contribution = SpliceContribution::splice_out(vec![bitcoin::TxOut {
1365+
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1366+
1367+
let funding_template = self
1368+
.channel_manager
1369+
.splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate)
1370+
.map_err(|e| {
1371+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1372+
Error::ChannelSplicingFailed
1373+
})?;
1374+
1375+
let outputs = vec![bitcoin::TxOut {
14161376
value: Amount::from_sat(splice_amount_sats),
14171377
script_pubkey: address.script_pubkey(),
1418-
}]);
1419-
1420-
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1421-
let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() {
1422-
Ok(fee_rate) => fee_rate,
1423-
Err(_) => {
1424-
debug_assert!(false, "FeeRate should always fit within u32");
1425-
log_error!(self.logger, "FeeRate should always fit within u32");
1426-
fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding)
1427-
},
1428-
};
1378+
}];
1379+
let contribution = self
1380+
.runtime
1381+
.block_on(funding_template.splice_out(outputs, Arc::clone(&self.wallet)))
1382+
.map_err(|()| {
1383+
log_error!(self.logger, "Failed to splice channel: coin selection failed");
1384+
Error::ChannelSplicingFailed
1385+
})?;
14291386

14301387
self.channel_manager
1431-
.splice_channel(
1388+
.funding_contributed(
14321389
&channel_details.channel_id,
14331390
&counterparty_node_id,
14341391
contribution,
1435-
funding_feerate_per_kw,
14361392
None,
14371393
)
14381394
.map_err(|e| {

src/wallet/mod.rs

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use bitcoin::psbt::{self, Psbt};
2626
use bitcoin::secp256k1::ecdh::SharedSecret;
2727
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
2828
use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
29+
use bitcoin::transaction::Sequence;
2930
use bitcoin::{
3031
Address, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
3132
WitnessProgram, WitnessVersion,
@@ -34,8 +35,10 @@ use lightning::chain::chaininterface::{
3435
BroadcasterInterface, INCREMENTAL_RELAY_FEE_SAT_PER_1000_WEIGHT,
3536
};
3637
use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
37-
use lightning::chain::{BestBlock, Listen};
38-
use lightning::events::bump_transaction::{Input, Utxo, WalletSource};
38+
use lightning::chain::{BestBlock, ClaimId, Listen};
39+
use lightning::events::bump_transaction::{
40+
CoinSelection, CoinSelectionSource, Input, Utxo, WalletSource,
41+
};
3942
use lightning::ln::channelmanager::PaymentId;
4043
use lightning::ln::funding::FundingTxInput;
4144
use lightning::ln::inbound_payment::ExpandedKey;
@@ -760,8 +763,10 @@ impl Wallet {
760763

761764
pub(crate) fn select_confirmed_utxos(
762765
&self, must_spend: Vec<Input>, must_pay_to: &[TxOut], fee_rate: FeeRate,
763-
) -> Result<Vec<FundingTxInput>, ()> {
766+
) -> Result<CoinSelection, ()> {
764767
let mut locked_wallet = self.inner.lock().unwrap();
768+
let mut locked_persister = self.persister.lock().unwrap();
769+
765770
debug_assert!(matches!(
766771
locked_wallet.public_descriptor(KeychainKind::External),
767772
ExtendedDescriptor::Wpkh(_)
@@ -790,12 +795,14 @@ impl Wallet {
790795
tx_builder.fee_rate(fee_rate);
791796
tx_builder.exclude_unconfirmed();
792797

793-
tx_builder
798+
let unsigned_tx = tx_builder
794799
.finish()
795800
.map_err(|e| {
796801
log_error!(self.logger, "Failed to select confirmed UTXOs: {}", e);
797802
})?
798-
.unsigned_tx
803+
.unsigned_tx;
804+
805+
let confirmed_utxos = unsigned_tx
799806
.input
800807
.iter()
801808
.filter(|txin| must_spend.iter().all(|input| input.outpoint != txin.previous_output))
@@ -805,7 +812,31 @@ impl Wallet {
805812
.map(|tx_details| tx_details.tx.deref().clone())
806813
.map(|prevtx| FundingTxInput::new_p2wpkh(prevtx, txin.previous_output.vout))
807814
})
808-
.collect::<Result<Vec<_>, ()>>()
815+
.collect::<Result<Vec<_>, ()>>()?;
816+
817+
if unsigned_tx.output.len() > must_pay_to.len() + 1 {
818+
log_error!(
819+
self.logger,
820+
"Unexpected number of change outputs during coin selection: {}",
821+
unsigned_tx.output.len() - must_pay_to.len(),
822+
);
823+
return Err(());
824+
}
825+
826+
let change_output = unsigned_tx
827+
.output
828+
.into_iter()
829+
.filter(|txout| must_pay_to.iter().all(|output| output != txout))
830+
.next();
831+
832+
if change_output.is_some() {
833+
locked_wallet.persist(&mut locked_persister).map_err(|e| {
834+
log_error!(self.logger, "Failed to persist wallet: {}", e);
835+
()
836+
})?;
837+
}
838+
839+
Ok(CoinSelection { confirmed_utxos, change_output })
809840
}
810841

811842
fn list_confirmed_utxos_inner(&self) -> Result<Vec<Utxo>, ()> {
@@ -881,6 +912,7 @@ impl Wallet {
881912
},
882913
satisfaction_weight: 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64 +
883914
1 /* witness items */ + 1 /* schnorr sig len */ + 64, // schnorr sig
915+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
884916
};
885917
utxos.push(utxo);
886918
},
@@ -1339,9 +1371,47 @@ impl WalletSource for Wallet {
13391371
async move { self.get_change_script_inner() }
13401372
}
13411373

1374+
fn get_prevtx<'a>(
1375+
&'a self, outpoint: OutPoint,
1376+
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
1377+
async move {
1378+
let locked_wallet = self.inner.lock().unwrap();
1379+
locked_wallet
1380+
.tx_details(outpoint.txid)
1381+
.map(|tx_details| tx_details.tx.deref().clone())
1382+
.ok_or_else(|| {
1383+
log_error!(
1384+
self.logger,
1385+
"Failed to get previous transaction for {}",
1386+
outpoint.txid
1387+
);
1388+
})
1389+
}
1390+
}
1391+
1392+
fn sign_psbt<'a>(
1393+
&'a self, psbt: Psbt,
1394+
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
1395+
async move { self.sign_psbt_inner(psbt) }
1396+
}
1397+
}
1398+
1399+
// Anchor bumping uses LdkWallet for coin selection, which wraps a WalletSource to implement
1400+
// CoinSelectionSource. Splicing uses this implementation of coin selection instead.
1401+
impl CoinSelectionSource for Wallet {
1402+
fn select_confirmed_utxos<'a>(
1403+
&'a self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &'a [TxOut],
1404+
target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64,
1405+
) -> impl Future<Output = Result<CoinSelection, ()>> + Send + 'a {
1406+
debug_assert!(claim_id.is_none());
1407+
let fee_rate = FeeRate::from_sat_per_kwu(target_feerate_sat_per_1000_weight as u64);
1408+
async move { self.select_confirmed_utxos(must_spend, must_pay_to, fee_rate) }
1409+
}
1410+
13421411
fn sign_psbt<'a>(
13431412
&'a self, psbt: Psbt,
13441413
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
1414+
debug_assert!(false);
13451415
async move { self.sign_psbt_inner(psbt) }
13461416
}
13471417
}

tests/integration_tests_rust.rs

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

987-
let expected_splice_in_fee_sat = 252;
987+
let expected_splice_in_fee_sat = 255;
988988

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

0 commit comments

Comments
 (0)