diff --git a/Cargo.toml b/Cargo.toml index 5c82d7d65..7e60a9746 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,17 +39,17 @@ default = [] #lightning-liquidity = { version = "0.2.0", features = ["std"] } #lightning-macros = { version = "0.2.0" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c", features = ["std"] } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } 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"]} vss-client = { package = "vss-client-ng", version = "0.4" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } -bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", rev = "ea50a9d2a8da524b69a2af43233706666cf2ffa5" } +bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "48f409bc1c2c140dfdb5514125ef7d714110b7f0" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "8bbc5ec0e4fd27071576374981d5d2b2ac18376c", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" criterion = { version = "0.7.0", features = ["async_tokio"] } diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 8a7167022..9deaeb7d0 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -18,11 +18,11 @@ use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarge use lightning::chain::{BestBlock, Listen}; use lightning::util::ser::Writeable; use lightning_block_sync::gossip::UtxoSource; -use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; +use lightning_block_sync::http::{HttpClientError, JsonResponse}; use lightning_block_sync::init::{synchronize_listeners, validate_best_block_header}; use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; use lightning_block_sync::rest::RestClient; -use lightning_block_sync::rpc::{RpcClient, RpcError}; +use lightning_block_sync::rpc::{RpcClient, RpcClientError}; use lightning_block_sync::{ BlockData, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceErrorKind, Cache, SpvClient, @@ -705,10 +705,10 @@ pub enum BitcoindClient { impl BitcoindClient { /// Creates a new RPC API client for the chain interactions with Bitcoin Core. pub(crate) fn new_rpc(host: String, port: u16, rpc_user: String, rpc_password: String) -> Self { - let http_endpoint = endpoint(host, port); + let url = base_url(host, port); let rpc_credentials = rpc_credentials(rpc_user, rpc_password); - let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, http_endpoint)); + let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, url)); let latest_mempool_timestamp = AtomicU64::new(0); @@ -726,12 +726,12 @@ impl BitcoindClient { rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, ) -> Self { - let rest_endpoint = endpoint(rest_host, rest_port).with_path("/rest".to_string()); - let rest_client = Arc::new(RestClient::new(rest_endpoint)); + let rest_url = format!("{}/rest", base_url(rest_host, rest_port)); + let rest_client = Arc::new(RestClient::new(rest_url)); - let rpc_endpoint = endpoint(rpc_host, rpc_port); + let rpc_url = base_url(rpc_host, rpc_port); let rpc_credentials = rpc_credentials(rpc_user, rpc_password); - let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, rpc_endpoint)); + let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, rpc_url)); let latest_mempool_timestamp = AtomicU64::new(0); @@ -773,7 +773,10 @@ impl BitcoindClient { ) -> std::io::Result { let tx_serialized = bitcoin::consensus::encode::serialize_hex(tx); let tx_json = serde_json::json!(tx_serialized); - rpc_client.call_method::("sendrawtransaction", &[tx_json]).await + rpc_client + .call_method::("sendrawtransaction", &[tx_json]) + .await + .map_err(rpc_client_error_to_io) } /// Retrieve the fee estimate needed for a transaction to begin @@ -815,6 +818,7 @@ impl BitcoindClient { &[num_blocks_json, estimation_mode_json], ) .await + .map_err(rpc_client_error_to_io) .map(|resp| resp.0) } @@ -837,6 +841,7 @@ impl BitcoindClient { rpc_client .call_method::("getmempoolinfo", &[]) .await + .map_err(rpc_client_error_to_io) .map(|resp| resp.0) } @@ -847,6 +852,7 @@ impl BitcoindClient { rest_client .request_resource::("mempool/info.json") .await + .map_err(http_client_error_to_io) .map(|resp| resp.0) } @@ -875,30 +881,15 @@ impl BitcoindClient { .await { Ok(resp) => Ok(Some(resp.0)), - Err(e) => match e.into_inner() { - Some(inner) => { - let rpc_error_res: Result, _> = inner.downcast(); - - match rpc_error_res { - Ok(rpc_error) => { - // Check if it's the 'not found' error code. - if rpc_error.code == -5 { - Ok(None) - } else { - Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error)) - } - }, - Err(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to process getrawtransaction response", - )), - } - }, - None => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to process getrawtransaction response", - )), + Err(RpcClientError::Rpc(rpc_error)) => { + // Check if it's the 'not found' error code. + if rpc_error.code == -5 { + Ok(None) + } else { + Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error)) + } }, + Err(e) => Err(rpc_client_error_to_io(e)), } } @@ -913,44 +904,15 @@ impl BitcoindClient { .await { Ok(resp) => Ok(Some(resp.0)), - Err(e) => match e.kind() { - std::io::ErrorKind::Other => { - match e.into_inner() { - Some(inner) => { - let http_error_res: Result, _> = inner.downcast(); - match http_error_res { - Ok(http_error) => { - // Check if it's the HTTP NOT_FOUND error code. - if &http_error.status_code == "404" { - Ok(None) - } else { - Err(std::io::Error::new( - std::io::ErrorKind::Other, - http_error, - )) - } - }, - Err(_) => { - let error_msg = - format!("Failed to process {} response.", tx_path); - Err(std::io::Error::new( - std::io::ErrorKind::Other, - error_msg.as_str(), - )) - }, - } - }, - None => { - let error_msg = format!("Failed to process {} response.", tx_path); - Err(std::io::Error::new(std::io::ErrorKind::Other, error_msg.as_str())) - }, - } - }, - _ => { - let error_msg = format!("Failed to process {} response.", tx_path); - Err(std::io::Error::new(std::io::ErrorKind::Other, error_msg.as_str())) - }, + Err(HttpClientError::Http(http_error)) => { + // Check if it's the HTTP NOT_FOUND error code. + if http_error.status_code == 404 { + Ok(None) + } else { + Err(std::io::Error::new(std::io::ErrorKind::Other, http_error)) + } }, + Err(e) => Err(http_client_error_to_io(e)), } } @@ -972,6 +934,7 @@ impl BitcoindClient { rpc_client .call_method::("getrawmempool", &[verbose_flag_json]) .await + .map_err(rpc_client_error_to_io) .map(|resp| resp.0) } @@ -982,6 +945,7 @@ impl BitcoindClient { "mempool/contents.json?verbose=false", ) .await + .map_err(http_client_error_to_io) .map(|resp| resp.0) } @@ -1008,30 +972,15 @@ impl BitcoindClient { match client.call_method::("getmempoolentry", &[txid_json]).await { Ok(resp) => Ok(Some(MempoolEntry { txid, time: resp.time, height: resp.height })), - Err(e) => match e.into_inner() { - Some(inner) => { - let rpc_error_res: Result, _> = inner.downcast(); - - match rpc_error_res { - Ok(rpc_error) => { - // Check if it's the 'not found' error code. - if rpc_error.code == -5 { - Ok(None) - } else { - Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error)) - } - }, - Err(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to process getmempoolentry response", - )), - } - }, - None => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to process getmempoolentry response", - )), + Err(RpcClientError::Rpc(rpc_error)) => { + // Check if it's the 'not found' error code. + if rpc_error.code == -5 { + Ok(None) + } else { + Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error)) + } }, + Err(e) => Err(rpc_client_error_to_io(e)), } } @@ -1275,17 +1224,13 @@ impl BlockSource for BitcoindClient { pub(crate) struct FeeResponse(pub FeeRate); impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { + type Error = String; + fn try_into(self) -> Result { if !self.0["errors"].is_null() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - self.0["errors"].to_string(), - )); + return Err(self.0["errors"].to_string()); } - let fee_rate_btc_per_kvbyte = self.0["feerate"] - .as_f64() - .ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse fee rate"))?; + let fee_rate_btc_per_kvbyte = + self.0["feerate"].as_f64().ok_or("Failed to parse fee rate".to_string())?; // Bitcoin Core gives us a feerate in BTC/KvB. // Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu. let fee_rate = { @@ -1299,11 +1244,10 @@ impl TryInto for JsonResponse { pub(crate) struct MempoolMinFeeResponse(pub FeeRate); impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { - let fee_rate_btc_per_kvbyte = self.0["mempoolminfee"] - .as_f64() - .ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse fee rate"))?; + type Error = String; + fn try_into(self) -> Result { + let fee_rate_btc_per_kvbyte = + self.0["mempoolminfee"].as_f64().ok_or("Failed to parse fee rate".to_string())?; // Bitcoin Core gives us a feerate in BTC/KvB. // Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu. let fee_rate = { @@ -1317,22 +1261,15 @@ impl TryInto for JsonResponse { pub(crate) struct GetRawTransactionResponse(pub Transaction); impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { + type Error = String; + fn try_into(self) -> Result { let tx = self .0 .as_str() - .ok_or(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawtransaction response", - )) + .ok_or("Failed to parse getrawtransaction response".to_string()) .and_then(|s| { - bitcoin::consensus::encode::deserialize_hex(s).map_err(|_| { - std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawtransaction response", - ) - }) + bitcoin::consensus::encode::deserialize_hex(s) + .map_err(|_| "Failed to parse getrawtransaction response".to_string()) })?; Ok(GetRawTransactionResponse(tx)) @@ -1342,12 +1279,9 @@ impl TryInto for JsonResponse { pub struct GetRawMempoolResponse(Vec); impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { - let res = self.0.as_array().ok_or(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawmempool response", - ))?; + type Error = String; + fn try_into(self) -> Result { + let res = self.0.as_array().ok_or("Failed to parse getrawmempool response".to_string())?; let mut mempool_transactions = Vec::with_capacity(res.len()); @@ -1356,17 +1290,11 @@ impl TryInto for JsonResponse { match hex_str.parse::() { Ok(txid) => txid, Err(_) => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawmempool response", - )); + return Err("Failed to parse getrawmempool response".to_string()); }, } } else { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawmempool response", - )); + return Err("Failed to parse getrawmempool response".to_string()); }; mempool_transactions.push(txid); @@ -1382,30 +1310,22 @@ pub struct GetMempoolEntryResponse { } impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { - let res = self.0.as_object().ok_or(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getmempoolentry response", - ))?; + type Error = String; + fn try_into(self) -> Result { + let res = + self.0.as_object().ok_or("Failed to parse getmempoolentry response".to_string())?; let time = match res["time"].as_u64() { Some(time) => time, None => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getmempoolentry response", - )); + return Err("Failed to parse getmempoolentry response".to_string()); }, }; let height = match res["height"].as_u64().and_then(|h| h.try_into().ok()) { Some(height) => height, None => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getmempoolentry response", - )); + return Err("Failed to parse getmempoolentry response".to_string()); }, }; @@ -1506,22 +1426,27 @@ pub(crate) fn rpc_credentials(rpc_user: String, rpc_password: String) -> String BASE64_STANDARD.encode(format!("{}:{}", rpc_user, rpc_password)) } -pub(crate) fn endpoint(host: String, port: u16) -> HttpEndpoint { - HttpEndpoint::for_host(host).with_port(port) +pub(crate) fn base_url(host: String, port: u16) -> String { + format!("http://{}:{}", host, port) } -#[derive(Debug)] -pub struct HttpError { - pub(crate) status_code: String, - pub(crate) contents: Vec, +fn rpc_client_error_to_io(e: RpcClientError) -> std::io::Error { + match e { + RpcClientError::Rpc(rpc_error) => std::io::Error::new(std::io::ErrorKind::Other, rpc_error), + RpcClientError::Http(http_err) => http_client_error_to_io(http_err), + RpcClientError::InvalidData(msg) => { + std::io::Error::new(std::io::ErrorKind::InvalidData, msg) + }, + } } -impl std::error::Error for HttpError {} - -impl std::fmt::Display for HttpError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let contents = String::from_utf8_lossy(&self.contents); - write!(f, "status_code: {}, contents: {}", self.status_code, contents) +fn http_client_error_to_io(e: HttpClientError) -> std::io::Error { + match e { + HttpClientError::Transport(err) => { + std::io::Error::new(std::io::ErrorKind::ConnectionRefused, err.to_string()) + }, + HttpClientError::Http(http_err) => std::io::Error::new(std::io::ErrorKind::Other, http_err), + HttpClientError::Parse(msg) => std::io::Error::new(std::io::ErrorKind::InvalidData, msg), } } diff --git a/src/io/test_utils.rs b/src/io/test_utils.rs index 9add2d6c1..88078b316 100644 --- a/src/io/test_utils.rs +++ b/src/io/test_utils.rs @@ -12,16 +12,17 @@ use std::path::PathBuf; use std::sync::Mutex; use lightning::events::ClosureReason; +use lightning::io; use lightning::ln::functional_test_utils::{ - check_added_monitors, check_closed_event, connect_block, create_announced_chan_between_nodes, - create_chanmon_cfgs, create_dummy_block, create_network, create_node_cfgs, - create_node_chanmgrs, send_payment, test_legacy_channel_config, TestChanMonCfg, + check_added_monitors, check_closed_broadcast, check_closed_event, connect_block, + create_announced_chan_between_nodes, create_chanmon_cfgs, create_dummy_block, create_network, + create_node_cfgs, create_node_chanmgrs, send_payment, test_legacy_channel_config, + TestChanMonCfg, }; use lightning::util::persist::{ KVStore, KVStoreSync, MonitorUpdatingPersister, KVSTORE_NAMESPACE_KEY_MAX_LEN, }; use lightning::util::test_utils; -use lightning::{check_closed_broadcast, io}; use rand::distr::Alphanumeric; use rand::{rng, Rng}; @@ -334,7 +335,7 @@ pub(crate) fn do_test_store(store_0: &K, store_1: &K) { &[nodes[1].node.get_our_node_id()], 100000, ); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let node_txn = nodes[0].tx_broadcaster.txn_broadcast(); @@ -343,7 +344,7 @@ pub(crate) fn do_test_store(store_0: &K, store_1: &K) { let dummy_block = create_dummy_block(nodes[0].best_block_hash(), 42, txn); connect_block(&nodes[1], &dummy_block); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); let reason = ClosureReason::CommitmentTxConfirmed; let node_id_0 = nodes[0].node.get_our_node_id(); check_closed_event(&nodes[1], 1, reason, &[node_id_0], 100000); diff --git a/src/lib.rs b/src/lib.rs index 2b60307b0..dd8a6ba4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,15 +138,13 @@ use gossip::GossipSource; use graph::NetworkGraph; 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::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState}; use lightning::ln::channelmanager::PaymentId; -use lightning::ln::funding::SpliceContribution; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; use lightning::util::persist::KVStoreSync; +use lightning::util::wallet_utils::Wallet as LdkWallet; use lightning_background_processor::process_events_async; use liquidity::{LSPS1Liquidity, LiquiditySource}; use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; @@ -1290,84 +1288,37 @@ impl Node { { self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?; - const EMPTY_SCRIPT_SIG_WEIGHT: u64 = - 1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64; - - 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: 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), - // 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); - let inputs = self - .wallet - .select_confirmed_utxos(vec![shared_input], &[shared_output], fee_rate) - .map_err(|()| { - log_error!( - self.logger, - "Failed to splice channel: insufficient confirmed UTXOs", - ); + let funding_template = self + .channel_manager + .splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate) + .map_err(|e| { + log_error!(self.logger, "Failed to splice channel: {:?}", e); Error::ChannelSplicingFailed })?; - let change_address = self.wallet.get_new_internal_address()?; - - let contribution = SpliceContribution::splice_in( - Amount::from_sat(splice_amount_sats), - inputs, - Some(change_address.script_pubkey()), - ); - - let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() { - Ok(fee_rate) => fee_rate, - Err(_) => { - debug_assert!(false); - fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding) - }, - }; + let contribution = self + .runtime + .block_on( + funding_template + .splice_in(Amount::from_sat(splice_amount_sats), Arc::clone(&self.wallet)), + ) + .map_err(|()| { + log_error!(self.logger, "Failed to splice channel: coin selection failed"); + Error::ChannelSplicingFailed + })?; self.channel_manager - .splice_channel( + .funding_contributed( &channel_details.channel_id, &counterparty_node_id, contribution, - funding_feerate_per_kw, None, ) .map_err(|e| { log_error!(self.logger, "Failed to splice channel: {:?}", e); - let tx = bitcoin::Transaction { - version: bitcoin::transaction::Version::TWO, - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![], - output: vec![bitcoin::TxOut { - value: Amount::ZERO, - script_pubkey: change_address.script_pubkey(), - }], - }; - match self.wallet.cancel_tx(&tx) { - Ok(()) => Error::ChannelSplicingFailed, - Err(e) => e, - } + Error::ChannelSplicingFailed }) } else { log_error!( @@ -1376,7 +1327,6 @@ impl Node { user_channel_id, counterparty_node_id ); - Err(Error::ChannelSplicingFailed) } } @@ -1407,27 +1357,33 @@ impl Node { self.wallet.parse_and_validate_address(address)?; - let contribution = SpliceContribution::splice_out(vec![bitcoin::TxOut { + let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding); + + let funding_template = self + .channel_manager + .splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate) + .map_err(|e| { + log_error!(self.logger, "Failed to splice channel: {:?}", e); + Error::ChannelSplicingFailed + })?; + + let outputs = vec![bitcoin::TxOut { value: Amount::from_sat(splice_amount_sats), script_pubkey: address.script_pubkey(), - }]); - - let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding); - let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() { - Ok(fee_rate) => fee_rate, - Err(_) => { - debug_assert!(false, "FeeRate should always fit within u32"); - log_error!(self.logger, "FeeRate should always fit within u32"); - fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding) - }, - }; + }]; + let contribution = self + .runtime + .block_on(funding_template.splice_out(outputs, Arc::clone(&self.wallet))) + .map_err(|()| { + log_error!(self.logger, "Failed to splice channel: coin selection failed"); + Error::ChannelSplicingFailed + })?; self.channel_manager - .splice_channel( + .funding_contributed( &channel_details.channel_id, &counterparty_node_id, contribution, - funding_feerate_per_kw, None, ) .map_err(|e| { diff --git a/src/types.rs b/src/types.rs index b5b1ffed7..c5ff07756 100644 --- a/src/types.rs +++ b/src/types.rs @@ -314,7 +314,7 @@ pub(crate) type Sweeper = OutputSweeper< pub(crate) type BumpTransactionEventHandler = lightning::events::bump_transaction::BumpTransactionEventHandler< Arc, - Arc, Arc>>, + Arc, Arc>>, Arc, Arc, >; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 87b544566..426eb7b11 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -25,14 +25,14 @@ use bitcoin::psbt::{self, Psbt}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; +use bitcoin::transaction::Sequence; use bitcoin::{ Address, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight, WitnessProgram, WitnessVersion, }; use lightning::chain::chaininterface::BroadcasterInterface; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; -use lightning::chain::{BestBlock, Listen}; -use lightning::events::bump_transaction::{Input, Utxo, WalletSource}; +use lightning::chain::{BestBlock, ClaimId, Listen}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::funding::FundingTxInput; use lightning::ln::inbound_payment::ExpandedKey; @@ -43,6 +43,9 @@ use lightning::sign::{ PeerStorageKey, Recipient, SignerProvider, SpendableOutputDescriptor, }; use lightning::util::message_signing; +use lightning::util::wallet_utils::{ + CoinSelection, CoinSelectionSource, Input, Utxo, WalletSource, +}; use lightning_invoice::RawBolt11Invoice; use persist::KVStoreWalletPersister; @@ -710,8 +713,10 @@ impl Wallet { pub(crate) fn select_confirmed_utxos( &self, must_spend: Vec, must_pay_to: &[TxOut], fee_rate: FeeRate, - ) -> Result, ()> { + ) -> Result { let mut locked_wallet = self.inner.lock().unwrap(); + let mut locked_persister = self.persister.lock().unwrap(); + debug_assert!(matches!( locked_wallet.public_descriptor(KeychainKind::External), ExtendedDescriptor::Wpkh(_) @@ -740,12 +745,14 @@ impl Wallet { tx_builder.fee_rate(fee_rate); tx_builder.exclude_unconfirmed(); - tx_builder + let unsigned_tx = tx_builder .finish() .map_err(|e| { log_error!(self.logger, "Failed to select confirmed UTXOs: {}", e); })? - .unsigned_tx + .unsigned_tx; + + let confirmed_utxos = unsigned_tx .input .iter() .filter(|txin| must_spend.iter().all(|input| input.outpoint != txin.previous_output)) @@ -755,7 +762,29 @@ impl Wallet { .map(|tx_details| tx_details.tx.deref().clone()) .map(|prevtx| FundingTxInput::new_p2wpkh(prevtx, txin.previous_output.vout)) }) - .collect::, ()>>() + .collect::, ()>>()?; + + if unsigned_tx.output.len() > must_pay_to.len() + 1 { + log_error!( + self.logger, + "Unexpected number of change outputs during coin selection: {}", + unsigned_tx.output.len() - must_pay_to.len(), + ); + return Err(()); + } + + let change_output = unsigned_tx + .output + .into_iter() + .filter(|txout| must_pay_to.iter().all(|output| output != txout)) + .next(); + + locked_wallet.persist(&mut locked_persister).map_err(|e| { + log_error!(self.logger, "Failed to persist wallet: {}", e); + () + })?; + + Ok(CoinSelection { confirmed_utxos, change_output }) } fn list_confirmed_utxos_inner(&self) -> Result, ()> { @@ -831,6 +860,7 @@ impl Wallet { }, satisfaction_weight: 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64 + 1 /* witness items */ + 1 /* schnorr sig len */ + 64, // schnorr sig + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, }; utxos.push(utxo); }, @@ -1094,9 +1124,47 @@ impl WalletSource for Wallet { async move { self.get_change_script_inner() } } + fn get_prevtx<'a>( + &'a self, outpoint: OutPoint, + ) -> impl Future> + Send + 'a { + async move { + let locked_wallet = self.inner.lock().unwrap(); + locked_wallet + .tx_details(outpoint.txid) + .map(|tx_details| tx_details.tx.deref().clone()) + .ok_or_else(|| { + log_error!( + self.logger, + "Failed to get previous transaction for {}", + outpoint.txid + ); + }) + } + } + + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future> + Send + 'a { + async move { self.sign_psbt_inner(psbt) } + } +} + +// Anchor bumping uses LdkWallet for coin selection, which wraps a WalletSource to implement +// CoinSelectionSource. Splicing uses this implementation of coin selection instead. +impl CoinSelectionSource for Wallet { + fn select_confirmed_utxos<'a>( + &'a self, claim_id: Option, must_spend: Vec, must_pay_to: &'a [TxOut], + target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64, + ) -> impl Future> + Send + 'a { + debug_assert!(claim_id.is_none()); + let fee_rate = FeeRate::from_sat_per_kwu(target_feerate_sat_per_1000_weight as u64); + async move { self.select_confirmed_utxos(must_spend, must_pay_to, fee_rate) } + } + fn sign_psbt<'a>( &'a self, psbt: Psbt, ) -> impl Future> + Send + 'a { + debug_assert!(false); async move { self.sign_psbt_inner(psbt) } } } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 9ea05aa1e..59cd05390 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -983,7 +983,7 @@ 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 expected_splice_in_fee_sat = 252; + let expected_splice_in_fee_sat = 255; let payments = node_b.list_payments(); let payment =