Skip to content

Commit 7ccaa2a

Browse files
committed
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> f - use select_confirmed_utxos
1 parent b55de44 commit 7ccaa2a

File tree

5 files changed

+131
-102
lines changed

5 files changed

+131
-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.4" }
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 = "c1a4b49d0406ff5cff0267dc9b5e64a2bae95929" }
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
proptest = "1.0.0"
8989
regex = "1.5.6"
9090
criterion = { version = "0.7.0", features = ["async_tokio"] }

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::util::persist::KVStoreSync;
@@ -1290,84 +1288,37 @@ 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+
let funding_template = self
1294+
.channel_manager
1295+
.splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate)
1296+
.map_err(|e| {
1297+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
13291298
Error::ChannelSplicingFailed
13301299
})?;
13311300

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-
);
1339-
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-
};
1301+
let contribution = self
1302+
.runtime
1303+
.block_on(
1304+
funding_template
1305+
.splice_in(Amount::from_sat(splice_amount_sats), Arc::clone(&self.wallet)),
1306+
)
1307+
.map_err(|()| {
1308+
log_error!(self.logger, "Failed to splice channel: coin selection failed");
1309+
Error::ChannelSplicingFailed
1310+
})?;
13471311

13481312
self.channel_manager
1349-
.splice_channel(
1313+
.funding_contributed(
13501314
&channel_details.channel_id,
13511315
&counterparty_node_id,
13521316
contribution,
1353-
funding_feerate_per_kw,
13541317
None,
13551318
)
13561319
.map_err(|e| {
13571320
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-
}
1321+
Error::ChannelSplicingFailed
13711322
})
13721323
} else {
13731324
log_error!(
@@ -1376,7 +1327,6 @@ impl Node {
13761327
user_channel_id,
13771328
counterparty_node_id
13781329
);
1379-
13801330
Err(Error::ChannelSplicingFailed)
13811331
}
13821332
}
@@ -1407,27 +1357,33 @@ impl Node {
14071357

14081358
self.wallet.parse_and_validate_address(address)?;
14091359

1410-
let contribution = SpliceContribution::splice_out(vec![bitcoin::TxOut {
1360+
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1361+
1362+
let funding_template = self
1363+
.channel_manager
1364+
.splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate)
1365+
.map_err(|e| {
1366+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1367+
Error::ChannelSplicingFailed
1368+
})?;
1369+
1370+
let outputs = vec![bitcoin::TxOut {
14111371
value: Amount::from_sat(splice_amount_sats),
14121372
script_pubkey: address.script_pubkey(),
1413-
}]);
1414-
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-
};
1373+
}];
1374+
let contribution = self
1375+
.runtime
1376+
.block_on(funding_template.splice_out(outputs, Arc::clone(&self.wallet)))
1377+
.map_err(|()| {
1378+
log_error!(self.logger, "Failed to splice channel: coin selection failed");
1379+
Error::ChannelSplicingFailed
1380+
})?;
14241381

14251382
self.channel_manager
1426-
.splice_channel(
1383+
.funding_contributed(
14271384
&channel_details.channel_id,
14281385
&counterparty_node_id,
14291386
contribution,
1430-
funding_feerate_per_kw,
14311387
None,
14321388
)
14331389
.map_err(|e| {

src/payment/unified.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ impl UnifiedPayment {
225225
PaymentMethod::LightningBolt12(_) => 0,
226226
PaymentMethod::LightningBolt11(_) => 1,
227227
PaymentMethod::OnChain(_) => 2,
228+
PaymentMethod::Cashu(_) => 3,
228229
});
229230

230231
for method in sorted_payment_methods {
@@ -278,6 +279,10 @@ impl UnifiedPayment {
278279
let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?;
279280
return Ok(UnifiedPaymentResult::Onchain { txid });
280281
},
282+
PaymentMethod::Cashu(_) => {
283+
log_error!(self.logger, "Cashu payments not supported. Aborting the payment.");
284+
return Err(Error::PaymentSendingFailed);
285+
},
281286
}
282287
}
283288

src/wallet/mod.rs

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ use bitcoin::psbt::{self, Psbt};
2525
use bitcoin::secp256k1::ecdh::SharedSecret;
2626
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
2727
use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
28+
use bitcoin::transaction::Sequence;
2829
use bitcoin::{
2930
Address, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
3031
WitnessProgram, WitnessVersion,
3132
};
3233
use lightning::chain::chaininterface::BroadcasterInterface;
3334
use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
34-
use lightning::chain::{BestBlock, Listen};
35-
use lightning::events::bump_transaction::{Input, Utxo, WalletSource};
35+
use lightning::chain::{BestBlock, ClaimId, Listen};
36+
use lightning::events::bump_transaction::{
37+
CoinSelection, CoinSelectionSource, Input, Utxo, WalletSource,
38+
};
3639
use lightning::ln::channelmanager::PaymentId;
3740
use lightning::ln::funding::FundingTxInput;
3841
use lightning::ln::inbound_payment::ExpandedKey;
@@ -710,8 +713,10 @@ impl Wallet {
710713

711714
pub(crate) fn select_confirmed_utxos(
712715
&self, must_spend: Vec<Input>, must_pay_to: &[TxOut], fee_rate: FeeRate,
713-
) -> Result<Vec<FundingTxInput>, ()> {
716+
) -> Result<CoinSelection, ()> {
714717
let mut locked_wallet = self.inner.lock().unwrap();
718+
let mut locked_persister = self.persister.lock().unwrap();
719+
715720
debug_assert!(matches!(
716721
locked_wallet.public_descriptor(KeychainKind::External),
717722
ExtendedDescriptor::Wpkh(_)
@@ -740,12 +745,14 @@ impl Wallet {
740745
tx_builder.fee_rate(fee_rate);
741746
tx_builder.exclude_unconfirmed();
742747

743-
tx_builder
748+
let unsigned_tx = tx_builder
744749
.finish()
745750
.map_err(|e| {
746751
log_error!(self.logger, "Failed to select confirmed UTXOs: {}", e);
747752
})?
748-
.unsigned_tx
753+
.unsigned_tx;
754+
755+
let confirmed_utxos = unsigned_tx
749756
.input
750757
.iter()
751758
.filter(|txin| must_spend.iter().all(|input| input.outpoint != txin.previous_output))
@@ -755,7 +762,29 @@ impl Wallet {
755762
.map(|tx_details| tx_details.tx.deref().clone())
756763
.map(|prevtx| FundingTxInput::new_p2wpkh(prevtx, txin.previous_output.vout))
757764
})
758-
.collect::<Result<Vec<_>, ()>>()
765+
.collect::<Result<Vec<_>, ()>>()?;
766+
767+
if unsigned_tx.output.len() > must_pay_to.len() + 1 {
768+
log_error!(
769+
self.logger,
770+
"Unexpected number of change outputs during coin selection: {}",
771+
unsigned_tx.output.len() - must_pay_to.len(),
772+
);
773+
return Err(());
774+
}
775+
776+
let change_output = unsigned_tx
777+
.output
778+
.into_iter()
779+
.filter(|txout| must_pay_to.iter().all(|output| output != txout))
780+
.next();
781+
782+
locked_wallet.persist(&mut locked_persister).map_err(|e| {
783+
log_error!(self.logger, "Failed to persist wallet: {}", e);
784+
()
785+
})?;
786+
787+
Ok(CoinSelection { confirmed_utxos, change_output })
759788
}
760789

761790
fn list_confirmed_utxos_inner(&self) -> Result<Vec<Utxo>, ()> {
@@ -831,6 +860,7 @@ impl Wallet {
831860
},
832861
satisfaction_weight: 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64 +
833862
1 /* witness items */ + 1 /* schnorr sig len */ + 64, // schnorr sig
863+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
834864
};
835865
utxos.push(utxo);
836866
},
@@ -1094,9 +1124,47 @@ impl WalletSource for Wallet {
10941124
async move { self.get_change_script_inner() }
10951125
}
10961126

1127+
fn get_prevtx<'a>(
1128+
&'a self, outpoint: OutPoint,
1129+
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
1130+
async move {
1131+
let locked_wallet = self.inner.lock().unwrap();
1132+
locked_wallet
1133+
.tx_details(outpoint.txid)
1134+
.map(|tx_details| tx_details.tx.deref().clone())
1135+
.ok_or_else(|| {
1136+
log_error!(
1137+
self.logger,
1138+
"Failed to get previous transaction for {}",
1139+
outpoint.txid
1140+
);
1141+
})
1142+
}
1143+
}
1144+
1145+
fn sign_psbt<'a>(
1146+
&'a self, psbt: Psbt,
1147+
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
1148+
async move { self.sign_psbt_inner(psbt) }
1149+
}
1150+
}
1151+
1152+
// Anchor bumping uses LdkWallet for coin selection, which wraps a WalletSource to implement
1153+
// CoinSelectionSource. Splicing uses this implementation of coin selection instead.
1154+
impl CoinSelectionSource for Wallet {
1155+
fn select_confirmed_utxos<'a>(
1156+
&'a self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &'a [TxOut],
1157+
target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64,
1158+
) -> impl Future<Output = Result<CoinSelection, ()>> + Send + 'a {
1159+
debug_assert!(claim_id.is_none());
1160+
let fee_rate = FeeRate::from_sat_per_kwu(target_feerate_sat_per_1000_weight as u64);
1161+
async move { self.select_confirmed_utxos(must_spend, must_pay_to, fee_rate) }
1162+
}
1163+
10971164
fn sign_psbt<'a>(
10981165
&'a self, psbt: Psbt,
10991166
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
1167+
debug_assert!(false);
11001168
async move { self.sign_psbt_inner(psbt) }
11011169
}
11021170
}

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 = 255;
987987

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

0 commit comments

Comments
 (0)