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/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/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/lib.rs b/src/lib.rs index bbae8ac72..88f7a83cd 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); @@ -1321,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/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, 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> { 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);