From 31c12f92d2fd5185b779051bd561a216f8e4434d Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 9 Dec 2025 01:37:16 -0600 Subject: [PATCH 1/4] Use actual funding output when constructing shared input/output in splices LDK gives us the actual funding output so we no longer need to create a dummy one with fake pubkeys --- src/lib.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bbae8ac72..853052070 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,7 +138,7 @@ use io::utils::write_node_metrics; use lightning::chain::BestBlock; use lightning::events::bump_transaction::{Input, Wallet as LdkWallet}; use lightning::impl_writeable_tlv_based; -use lightning::ln::chan_utils::{make_funding_redeemscript, FUNDING_TRANSACTION_WITNESS_WEIGHT}; +use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::funding::SpliceContribution; @@ -1267,29 +1267,27 @@ impl Node { const EMPTY_SCRIPT_SIG_WEIGHT: u64 = 1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64; - // Used for creating a redeem script for the previous funding txo and the new funding - // txo. Only needed when selecting which UTXOs to include in the funding tx that would - // be sufficient to pay for fees. Hence, the value does not matter. - let dummy_pubkey = PublicKey::from_slice(&[2; 33]).unwrap(); - let funding_txo = channel_details.funding_txo.ok_or_else(|| { log_error!(self.logger, "Failed to splice channel: channel not yet ready",); Error::ChannelSplicingFailed })?; + let funding_output = channel_details.get_funding_output().ok_or_else(|| { + log_error!(self.logger, "Failed to splice channel: channel not yet ready"); + Error::ChannelSplicingFailed + })?; + let shared_input = Input { outpoint: funding_txo.into_bitcoin_outpoint(), - previous_utxo: bitcoin::TxOut { - value: Amount::from_sat(channel_details.channel_value_satoshis), - script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey) - .to_p2wsh(), - }, + previous_utxo: funding_output.clone(), satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT, }; let shared_output = bitcoin::TxOut { value: shared_input.previous_utxo.value + Amount::from_sat(splice_amount_sats), - script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey).to_p2wsh(), + // will not actually be the exact same script pubkey after splice + // but it is the same size and good enough for coin selection purposes + script_pubkey: funding_output.script_pubkey.clone(), }; let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding); From 1eca8a894911b42339944f8f158fe1a5e69736e9 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 9 Dec 2025 01:38:54 -0600 Subject: [PATCH 2/4] Insert channel funding utxo before a splice-in We insert a channel's funding utxo into our wallet so we can later calculate the fees for the transaction, otherwise our wallet would have incomplete information. We do it before the splice-in and not just on the ChannelReady event to ensure better backwards compatibility. --- src/lib.rs | 9 +++++++++ src/wallet/mod.rs | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 853052070..88f7a83cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1319,6 +1319,15 @@ impl Node { }, }; + // insert channel's funding utxo into the wallet so we can later calculate fees + // correctly when viewing this splice-in. + self.wallet.insert_txo(funding_txo.into_bitcoin_outpoint(), funding_output).map_err( + |e| { + log_error!(self.logger, "Failed to splice channel: {:?}", e); + Error::ChannelSplicingFailed + }, + )?; + self.channel_manager .splice_channel( &channel_details.channel_id, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 2f8daa500..a8e791f34 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -26,7 +26,7 @@ use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; use bitcoin::{ - Address, Amount, FeeRate, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight, + Address, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight, WitnessProgram, WitnessVersion, }; use lightning::chain::chaininterface::BroadcasterInterface; @@ -153,6 +153,19 @@ impl Wallet { Ok(()) } + pub(crate) fn insert_txo(&self, outpoint: OutPoint, txout: TxOut) -> Result<(), Error> { + let mut locked_wallet = self.inner.lock().unwrap(); + locked_wallet.insert_txout(outpoint, txout); + + let mut locked_persister = self.persister.lock().unwrap(); + locked_wallet.persist(&mut locked_persister).map_err(|e| { + log_error!(self.logger, "Failed to persist wallet: {}", e); + Error::PersistenceFailed + })?; + + Ok(()) + } + fn update_payment_store<'a>( &self, locked_wallet: &'a mut PersistedWallet, ) -> Result<(), Error> { From 97f0ee8abf312d7ad9a9976717bbe5e26705542b Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 3 Dec 2025 13:44:32 -0600 Subject: [PATCH 3/4] Insert channel funding outputs into Wallet When doing a splice, to properly calculate fees we need the channel's funding utxo in our wallet, otherwise our wallet won't know the channel's original size. This adds the channel funding txo on ChannelReady events and modifies the splicing test to make sure we can calculate fees on splice-ins. --- src/event.rs | 27 +++++++++++++++++++++++++++ tests/integration_tests_rust.rs | 11 ++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/event.rs b/src/event.rs index 41f76f216..b760dcba8 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1458,6 +1458,33 @@ where counterparty_node_id, funding_txo, ); + + let chans = + self.channel_manager.list_channels_with_counterparty(&counterparty_node_id); + let chan_output = chans + .iter() + .find(|c| c.user_channel_id == user_channel_id) + .and_then(|c| c.get_funding_output()); + match chan_output { + None => { + log_error!( + self.logger, + "Failed to find channel info for pending channel {channel_id} with counterparty {counterparty_node_id}" + ); + debug_assert!(false, + "Failed to find channel info for pending channel {channel_id} with counterparty {counterparty_node_id}" + ); + }, + Some(output) => { + if let Err(e) = self.wallet.insert_txo(funding_txo, output) { + log_error!( + self.logger, + "Failed to insert funding TXO into wallet: {e}" + ); + return Err(ReplayEvent()); + } + }, + } } else { log_info!( self.logger, diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 7c1ed8344..87d309211 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -995,7 +995,7 @@ async fn splice_channel() { // Splice-in funds for Node B so that it has outbound liquidity to make a payment node_b.splice_in(&user_channel_id_b, node_a.node_id(), 4_000_000).unwrap(); - expect_splice_pending_event!(node_a, node_b.node_id()); + let txo = expect_splice_pending_event!(node_a, node_b.node_id()); expect_splice_pending_event!(node_b, node_a.node_id()); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; @@ -1006,11 +1006,16 @@ async fn splice_channel() { expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); - let splice_in_fee_sat = 252; + let expected_splice_in_fee_sat = 252; + + let payments = node_b.list_payments(); + let payment = + payments.into_iter().find(|p| p.id == PaymentId(txo.txid.to_byte_array())).unwrap(); + assert_eq!(payment.fee_paid_msat, Some(expected_splice_in_fee_sat * 1_000)); assert_eq!( node_b.list_balances().total_onchain_balance_sats, - premine_amount_sat - 4_000_000 - splice_in_fee_sat + premine_amount_sat - 4_000_000 - expected_splice_in_fee_sat ); assert_eq!(node_b.list_balances().total_lightning_balance_sats, 4_000_000); From 4c3450e6554297cda506c5a2029a670984e9d1a6 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 3 Dec 2025 13:50:53 -0600 Subject: [PATCH 4/4] Add funding_redeem_script to ChannelDetails Exposes the funding_redeem_script that LDK already exposes --- bindings/ldk_node.udl | 5 +++++ src/error.rs | 3 +++ src/ffi/types.rs | 18 +++++++++++++++++- src/types.rs | 10 ++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c4ebf56a6..e89158b59 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -339,6 +339,7 @@ enum NodeError { "InvalidNodeAlias", "InvalidDateTime", "InvalidFeeRate", + "InvalidScriptPubKey", "DuplicatePayment", "UnsupportedCurrency", "InsufficientFunds", @@ -575,6 +576,7 @@ dictionary ChannelDetails { ChannelId channel_id; PublicKey counterparty_node_id; OutPoint? funding_txo; + ScriptBuf? funding_redeem_script; u64? short_channel_id; u64? outbound_scid_alias; u64? inbound_scid_alias; @@ -901,3 +903,6 @@ typedef string LSPS1OrderId; [Custom] typedef string LSPSDateTime; + +[Custom] +typedef string ScriptBuf; diff --git a/src/error.rs b/src/error.rs index 20b1cceab..55e180c15 100644 --- a/src/error.rs +++ b/src/error.rs @@ -113,6 +113,8 @@ pub enum Error { InvalidDateTime, /// The given fee rate is invalid. InvalidFeeRate, + /// The given script public key is invalid. + InvalidScriptPubKey, /// A payment with the given hash has already been initiated. DuplicatePayment, /// The provided offer was denonminated in an unsupported currency. @@ -186,6 +188,7 @@ impl fmt::Display for Error { Self::InvalidNodeAlias => write!(f, "The given node alias is invalid."), Self::InvalidDateTime => write!(f, "The given date time is invalid."), Self::InvalidFeeRate => write!(f, "The given fee rate is invalid."), + Self::InvalidScriptPubKey => write!(f, "The given script pubkey is invalid."), Self::DuplicatePayment => { write!(f, "A payment with the given hash has already been initiated.") }, diff --git a/src/ffi/types.rs b/src/ffi/types.rs index c69987c96..bd3c2192d 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -20,7 +20,7 @@ pub use bip39::Mnemonic; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; +pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Txid}; pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; use lightning::ln::channelmanager::PaymentId; @@ -106,6 +106,22 @@ impl UniffiCustomTypeConverter for Address { } } +impl UniffiCustomTypeConverter for ScriptBuf { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(key) = ScriptBuf::from_hex(&val) { + return Ok(key); + } + + Err(Error::InvalidScriptPubKey.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum OfferAmount { Bitcoin { amount_msats: u64 }, diff --git a/src/types.rs b/src/types.rs index 38519eca7..1ec9a39d3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -222,6 +222,15 @@ pub struct ChannelDetails { /// state until the splice transaction reaches sufficient confirmations to be locked (and we /// exchange `splice_locked` messages with our peer). pub funding_txo: Option, + /// The witness script that is used to lock the channel's funding output to commitment transactions. + /// + /// This field will be `None` if we have not negotiated the funding transaction with our + /// counterparty already. + /// + /// When a channel is spliced, this continues to refer to the original pre-splice channel + /// state until the splice transaction reaches sufficient confirmations to be locked (and we + /// exchange `splice_locked` messages with our peer). + pub funding_redeem_script: Option, /// The position of the funding transaction in the chain. None if the funding transaction has /// not yet been confirmed and the channel fully opened. /// @@ -378,6 +387,7 @@ impl From for ChannelDetails { channel_id: value.channel_id, counterparty_node_id: value.counterparty.node_id, funding_txo: value.funding_txo.map(|o| o.into_bitcoin_outpoint()), + funding_redeem_script: value.funding_redeem_script, short_channel_id: value.short_channel_id, outbound_scid_alias: value.outbound_scid_alias, inbound_scid_alias: value.inbound_scid_alias,