diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index 00fc54774d3..2db32a1081c 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -68,6 +68,8 @@ if [[ $RUSTC_MINOR_VERSION -gt 67 && "$HOST_PLATFORM" != *windows* ]]; then cargo check --verbose --color always --features esplora-async cargo test --verbose --color always --features esplora-async-https cargo check --verbose --color always --features esplora-async-https + cargo test --verbose --color always --features electrum + cargo check --verbose --color always --features electrum popd fi diff --git a/lightning-transaction-sync/Cargo.toml b/lightning-transaction-sync/Cargo.toml index 782c4b7033e..20e03ce6c27 100644 --- a/lightning-transaction-sync/Cargo.toml +++ b/lightning-transaction-sync/Cargo.toml @@ -16,8 +16,9 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] esplora-async = ["async-interface", "esplora-client/async", "futures"] -esplora-async-https = ["esplora-async", "reqwest/rustls-tls"] +esplora-async-https = ["esplora-async", "esplora-client/async-https-rustls"] esplora-blocking = ["esplora-client/blocking"] +electrum = ["electrum-client"] async-interface = [] [dependencies] @@ -26,10 +27,9 @@ bitcoin = { version = "0.30.2", default-features = false } bdk-macros = "0.6" futures = { version = "0.3", optional = true } esplora-client = { version = "0.6", default-features = false, optional = true } -reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] } +electrum-client = { version = "0.18.0", optional = true } [dev-dependencies] -lightning = { version = "0.0.118", path = "../lightning", features = ["std"] } +lightning = { version = "0.0.118", path = "../lightning", features = ["std", "_test_utils"] } electrsd = { version = "0.26.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] } -electrum-client = "0.18.0" tokio = { version = "1.14.0", features = ["full"] } diff --git a/lightning-transaction-sync/src/common.rs b/lightning-transaction-sync/src/common.rs index 45f18afb99b..be49fbe96ff 100644 --- a/lightning-transaction-sync/src/common.rs +++ b/lightning-transaction-sync/src/common.rs @@ -1,6 +1,6 @@ -use lightning::chain::WatchedOutput; +use lightning::chain::{Confirm, WatchedOutput}; use bitcoin::{Txid, BlockHash, Transaction, OutPoint}; -use bitcoin::blockdata::block::Header; +use bitcoin::block::Header; use std::collections::{HashSet, HashMap}; @@ -28,6 +28,39 @@ impl SyncState { pending_sync: false, } } + pub fn sync_unconfirmed_transactions( + &mut self, confirmables: &Vec<&(dyn Confirm + Sync + Send)>, + unconfirmed_txs: Vec, + ) { + for txid in unconfirmed_txs { + for c in confirmables { + c.transaction_unconfirmed(&txid); + } + + self.watched_transactions.insert(txid); + } + } + + pub fn sync_confirmed_transactions( + &mut self, confirmables: &Vec<&(dyn Confirm + Sync + Send)>, + confirmed_txs: Vec + ) { + for ctx in confirmed_txs { + for c in confirmables { + c.transactions_confirmed( + &ctx.block_header, + &[(ctx.pos, &ctx.tx)], + ctx.block_height, + ); + } + + self.watched_transactions.remove(&ctx.tx.txid()); + + for input in &ctx.tx.input { + self.watched_outputs.remove(&input.previous_output); + } + } + } } @@ -68,6 +101,7 @@ impl FilterQueue { } } +#[derive(Debug)] pub(crate) struct ConfirmedTx { pub tx: Transaction, pub block_header: Header, diff --git a/lightning-transaction-sync/src/electrum.rs b/lightning-transaction-sync/src/electrum.rs new file mode 100644 index 00000000000..07e11338905 --- /dev/null +++ b/lightning-transaction-sync/src/electrum.rs @@ -0,0 +1,477 @@ +use crate::common::{ConfirmedTx, SyncState, FilterQueue}; +use crate::error::{TxSyncError, InternalError}; + +use electrum_client::Client as ElectrumClient; +use electrum_client::ElectrumApi; +use electrum_client::GetMerkleRes; + +use lightning::util::logger::Logger; +use lightning::{log_error, log_debug, log_trace}; +use lightning::chain::WatchedOutput; +use lightning::chain::{Confirm, Filter}; + +use bitcoin::{BlockHash, Script, Transaction, Txid}; +use bitcoin::block::Header; +use bitcoin::hash_types::TxMerkleNode; +use bitcoin::hashes::Hash; +use bitcoin::hashes::sha256d::Hash as Sha256d; + +use std::ops::Deref; +use std::sync::Mutex; +use std::collections::HashSet; +use std::time::Instant; + +/// Synchronizes LDK with a given Electrum server. +/// +/// Needs to be registered with a [`ChainMonitor`] via the [`Filter`] interface to be informed of +/// transactions and outputs to monitor for on-chain confirmation, unconfirmation, and +/// reconfirmation. +/// +/// Note that registration via [`Filter`] needs to happen before any calls to +/// [`Watch::watch_channel`] to ensure we get notified of the items to monitor. +/// +/// [`ChainMonitor`]: lightning::chain::chainmonitor::ChainMonitor +/// [`Watch::watch_channel`]: lightning::chain::Watch::watch_channel +/// [`Filter`]: lightning::chain::Filter +pub struct ElectrumSyncClient +where + L::Target: Logger, +{ + sync_state: Mutex, + queue: Mutex, + client: ElectrumClient, + logger: L, +} + +impl ElectrumSyncClient +where + L::Target: Logger, +{ + /// Returns a new [`ElectrumSyncClient`] object. + pub fn new(server_url: String, logger: L) -> Result { + let client = ElectrumClient::new(&server_url).map_err(|e| { + log_error!(logger, "Failed to connect to electrum server '{}': {}", server_url, e); + e + })?; + + Self::from_client(client, logger) + } + + /// Returns a new [`ElectrumSyncClient`] object using the given Electrum client. + pub fn from_client(client: ElectrumClient, logger: L) -> Result { + let sync_state = Mutex::new(SyncState::new()); + let queue = Mutex::new(FilterQueue::new()); + + Ok(Self { + sync_state, + queue, + client, + logger, + }) + } + + /// Synchronizes the given `confirmables` via their [`Confirm`] interface implementations. This + /// method should be called regularly to keep LDK up-to-date with current chain data. + /// + /// For example, instances of [`ChannelManager`] and [`ChainMonitor`] can be informed about the + /// newest on-chain activity related to the items previously registered via the [`Filter`] + /// interface. + /// + /// [`Confirm`]: lightning::chain::Confirm + /// [`ChainMonitor`]: lightning::chain::chainmonitor::ChainMonitor + /// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager + /// [`Filter`]: lightning::chain::Filter + pub fn sync(&self, confirmables: Vec<&(dyn Confirm + Sync + Send)>) -> Result<(), TxSyncError> { + // This lock makes sure we're syncing once at a time. + let mut sync_state = self.sync_state.lock().unwrap(); + + log_trace!(self.logger, "Starting transaction sync."); + let start_time = Instant::now(); + let mut num_confirmed = 0; + let mut num_unconfirmed = 0; + + // Clear any header notifications we might have gotten to keep the queue count low. + while let Some(_) = self.client.block_headers_pop()? {} + + let tip_notification = self.client.block_headers_subscribe()?; + let mut tip_header = tip_notification.header; + let mut tip_height = tip_notification.height as u32; + + loop { + let pending_registrations = self.queue.lock().unwrap().process_queues(&mut sync_state); + let tip_is_new = Some(tip_header.block_hash()) != sync_state.last_sync_hash; + + // We loop until any registered transactions have been processed at least once, or the + // tip hasn't been updated during the last iteration. + if !sync_state.pending_sync && !pending_registrations && !tip_is_new { + // Nothing to do. + break; + } else { + // Update the known tip to the newest one. + if tip_is_new { + // First check for any unconfirmed transactions and act on it immediately. + match self.get_unconfirmed_transactions(&confirmables) { + Ok(unconfirmed_txs) => { + // Double-check the tip hash. If it changed, a reorg happened since + // we started syncing and we need to restart last-minute. + match self.check_update_tip(&mut tip_header, &mut tip_height) { + Ok(false) => { + num_unconfirmed += unconfirmed_txs.len(); + sync_state.sync_unconfirmed_transactions( + &confirmables, + unconfirmed_txs + ); + } + Ok(true) => { + log_debug!(self.logger, + "Encountered inconsistency during transaction sync, restarting."); + sync_state.pending_sync = true; + continue; + } + Err(err) => { + // (Semi-)permanent failure, retry later. + log_error!(self.logger, + "Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.", + num_confirmed, + num_unconfirmed + ); + sync_state.pending_sync = true; + return Err(TxSyncError::from(err)); + } + } + }, + Err(err) => { + // (Semi-)permanent failure, retry later. + log_error!(self.logger, + "Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.", + num_confirmed, + num_unconfirmed + ); + sync_state.pending_sync = true; + return Err(TxSyncError::from(err)); + } + } + + // Update the best block. + for c in &confirmables { + c.best_block_updated(&tip_header, tip_height); + } + } + + match self.get_confirmed_transactions(&sync_state) { + Ok(confirmed_txs) => { + // Double-check the tip hash. If it changed, a reorg happened since + // we started syncing and we need to restart last-minute. + match self.check_update_tip(&mut tip_header, &mut tip_height) { + Ok(false) => { + num_confirmed += confirmed_txs.len(); + sync_state.sync_confirmed_transactions( + &confirmables, + confirmed_txs + ); + } + Ok(true) => { + log_debug!(self.logger, + "Encountered inconsistency during transaction sync, restarting."); + sync_state.pending_sync = true; + continue; + } + Err(err) => { + // (Semi-)permanent failure, retry later. + log_error!(self.logger, + "Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.", + num_confirmed, + num_unconfirmed + ); + sync_state.pending_sync = true; + return Err(TxSyncError::from(err)); + } + } + } + Err(InternalError::Inconsistency) => { + // Immediately restart syncing when we encounter any inconsistencies. + log_debug!(self.logger, + "Encountered inconsistency during transaction sync, restarting."); + sync_state.pending_sync = true; + continue; + } + Err(err) => { + // (Semi-)permanent failure, retry later. + log_error!(self.logger, + "Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.", + num_confirmed, + num_unconfirmed + ); + sync_state.pending_sync = true; + return Err(TxSyncError::from(err)); + } + } + sync_state.last_sync_hash = Some(tip_header.block_hash()); + sync_state.pending_sync = false; + } + } + log_debug!(self.logger, + "Finished transaction sync at tip {} in {}ms: {} confirmed, {} unconfirmed.", + tip_header.block_hash(), start_time.elapsed().as_millis(), num_confirmed, + num_unconfirmed); + Ok(()) + } + + fn check_update_tip(&self, cur_tip_header: &mut Header, cur_tip_height: &mut u32) + -> Result + { + let check_notification = self.client.block_headers_subscribe()?; + let check_tip_hash = check_notification.header.block_hash(); + + // Restart if either the tip changed or we got some divergent tip + // change notification since we started. In the latter case we + // make sure we clear the queue before continuing. + let mut restart_sync = check_tip_hash != cur_tip_header.block_hash(); + while let Some(queued_notif) = self.client.block_headers_pop()? { + if queued_notif.header.block_hash() != check_tip_hash { + restart_sync = true + } + } + + if restart_sync { + *cur_tip_header = check_notification.header; + *cur_tip_height = check_notification.height as u32; + Ok(true) + } else { + Ok(false) + } + } + + fn get_confirmed_transactions( + &self, sync_state: &SyncState, + ) -> Result, InternalError> { + + // First, check the confirmation status of registered transactions as well as the + // status of dependent transactions of registered outputs. + let mut confirmed_txs = Vec::new(); + let mut watched_script_pubkeys = Vec::with_capacity( + sync_state.watched_transactions.len() + sync_state.watched_outputs.len()); + let mut watched_txs = Vec::with_capacity(sync_state.watched_transactions.len()); + + for txid in &sync_state.watched_transactions { + match self.client.transaction_get(&txid) { + Ok(tx) => { + watched_txs.push((txid, tx.clone())); + if let Some(tx_out) = tx.output.first() { + // We watch an arbitrary output of the transaction of interest in order to + // retrieve the associated script history, before narrowing down our search + // through `filter`ing by `txid` below. + watched_script_pubkeys.push(tx_out.script_pubkey.clone()); + } else { + debug_assert!(false, "Failed due to retrieving invalid tx data."); + log_error!(self.logger, "Failed due to retrieving invalid tx data."); + return Err(InternalError::Failed); + } + } + Err(electrum_client::Error::Protocol(_)) => { + // We couldn't find the tx, do nothing. + } + Err(e) => { + log_error!(self.logger, "Failed to look up transaction {}: {}.", txid, e); + return Err(InternalError::Failed); + } + } + } + + let num_tx_lookups = watched_script_pubkeys.len(); + debug_assert_eq!(num_tx_lookups, watched_txs.len()); + + for output in sync_state.watched_outputs.values() { + watched_script_pubkeys.push(output.script_pubkey.clone()); + } + + let num_output_spend_lookups = watched_script_pubkeys.len() - num_tx_lookups; + debug_assert_eq!(num_output_spend_lookups, sync_state.watched_outputs.len()); + + match self.client.batch_script_get_history(watched_script_pubkeys.iter().map(|s| s.deref())) + { + Ok(results) => { + let (tx_results, output_results) = results.split_at(num_tx_lookups); + debug_assert_eq!(num_output_spend_lookups, output_results.len()); + + for (i, script_history) in tx_results.iter().enumerate() { + let (txid, tx) = &watched_txs[i]; + let mut filtered_history = script_history.iter().filter(|h| h.tx_hash == **txid); + if let Some(history) = filtered_history.next() + { + let prob_conf_height = history.height as u32; + let confirmed_tx = self.get_confirmed_tx(tx, prob_conf_height)?; + confirmed_txs.push(confirmed_tx); + } + debug_assert!(filtered_history.next().is_none()); + } + + for (watched_output, script_history) in sync_state.watched_outputs.values() + .zip(output_results) + { + for possible_output_spend in script_history { + if possible_output_spend.height <= 0 { + continue; + } + + let txid = possible_output_spend.tx_hash; + match self.client.transaction_get(&txid) { + Ok(tx) => { + let mut is_spend = false; + for txin in &tx.input { + let watched_outpoint = watched_output.outpoint + .into_bitcoin_outpoint(); + if txin.previous_output == watched_outpoint { + is_spend = true; + break; + } + } + + if !is_spend { + continue; + } + + let prob_conf_height = possible_output_spend.height as u32; + let confirmed_tx = self.get_confirmed_tx(&tx, prob_conf_height)?; + confirmed_txs.push(confirmed_tx); + } + Err(e) => { + log_trace!(self.logger, + "Inconsistency: Tx {} was unconfirmed during syncing: {}", + txid, e); + return Err(InternalError::Inconsistency); + } + } + } + } + } + Err(e) => { + log_error!(self.logger, "Failed to look up script histories: {}.", e); + return Err(InternalError::Failed); + } + } + + // Sort all confirmed transactions first by block height, then by in-block + // position, and finally feed them to the interface in order. + confirmed_txs.sort_unstable_by(|tx1, tx2| { + tx1.block_height.cmp(&tx2.block_height).then_with(|| tx1.pos.cmp(&tx2.pos)) + }); + + Ok(confirmed_txs) + } + + fn get_unconfirmed_transactions( + &self, confirmables: &Vec<&(dyn Confirm + Sync + Send)>, + ) -> Result, InternalError> { + // Query the interface for relevant txids and check whether the relevant blocks are still + // in the best chain, mark them unconfirmed otherwise + let relevant_txids = confirmables + .iter() + .flat_map(|c| c.get_relevant_txids()) + .collect::)>>(); + + let mut unconfirmed_txs = Vec::new(); + + for (txid, conf_height, block_hash_opt) in relevant_txids { + if let Some(block_hash) = block_hash_opt { + let block_header = self.client.block_header(conf_height as usize)?; + if block_header.block_hash() == block_hash { + // Skip if the tx is still confirmed in the block in question. + continue; + } + + unconfirmed_txs.push(txid); + } else { + log_error!(self.logger, + "Untracked confirmation of funding transaction. Please ensure none of your channels had been created with LDK prior to version 0.0.113!"); + panic!("Untracked confirmation of funding transaction. Please ensure none of your channels had been created with LDK prior to version 0.0.113!"); + } + } + Ok(unconfirmed_txs) + } + + fn get_confirmed_tx(&self, tx: &Transaction, prob_conf_height: u32) + -> Result + { + let txid = tx.txid(); + match self.client.transaction_get_merkle(&txid, prob_conf_height as usize) { + Ok(merkle_res) => { + debug_assert_eq!(prob_conf_height, merkle_res.block_height as u32); + match self.client.block_header(prob_conf_height as usize) { + Ok(block_header) => { + let pos = merkle_res.pos; + if !self.validate_merkle_proof(&txid, + &block_header.merkle_root, merkle_res)? + { + log_trace!(self.logger, + "Inconsistency: Block {} was unconfirmed during syncing.", + block_header.block_hash()); + return Err(InternalError::Inconsistency); + } + let confirmed_tx = ConfirmedTx { + tx: tx.clone(), + block_header, block_height: prob_conf_height, + pos, + }; + Ok(confirmed_tx) + } + Err(e) => { + log_error!(self.logger, + "Failed to retrieve block header for height {}: {}.", + prob_conf_height, e); + Err(InternalError::Failed) + } + } + } + Err(e) => { + log_trace!(self.logger, + "Inconsistency: Tx {} was unconfirmed during syncing: {}", + txid, e); + Err(InternalError::Inconsistency) + } + } + } + + /// Returns a reference to the underlying Electrum client. + pub fn client(&self) -> &ElectrumClient { + &self.client + } + + fn validate_merkle_proof(&self, txid: &Txid, merkle_root: &TxMerkleNode, + merkle_res: GetMerkleRes) -> Result + { + let mut index = merkle_res.pos; + let mut cur = txid.to_raw_hash(); + for mut bytes in merkle_res.merkle { + bytes.reverse(); + // unwrap() safety: `bytes` has len 32 so `from_slice` can never fail. + let next_hash = Sha256d::from_slice(&bytes).unwrap(); + let (left, right) = if index % 2 == 0 { + (cur, next_hash) + } else { + (next_hash, cur) + }; + + let data = [&left[..], &right[..]].concat(); + cur = Sha256d::hash(&data); + index /= 2; + } + + Ok(cur == merkle_root.to_raw_hash()) + } +} + +impl Filter for ElectrumSyncClient +where + L::Target: Logger, +{ + fn register_tx(&self, txid: &Txid, _script_pubkey: &Script) { + let mut locked_queue = self.queue.lock().unwrap(); + locked_queue.transactions.insert(*txid); + } + + fn register_output(&self, output: WatchedOutput) { + let mut locked_queue = self.queue.lock().unwrap(); + locked_queue.outputs.insert(output.outpoint.into_bitcoin_outpoint(), output); + } +} diff --git a/lightning-transaction-sync/src/error.rs b/lightning-transaction-sync/src/error.rs index 73d9de70169..d1f4a319e86 100644 --- a/lightning-transaction-sync/src/error.rs +++ b/lightning-transaction-sync/src/error.rs @@ -18,7 +18,6 @@ impl fmt::Display for TxSyncError { } #[derive(Debug)] -#[cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] pub(crate) enum InternalError { /// A transaction sync failed and needs to be retried eventually. Failed, @@ -26,7 +25,6 @@ pub(crate) enum InternalError { Inconsistency, } -#[cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] impl fmt::Display for InternalError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -38,9 +36,14 @@ impl fmt::Display for InternalError { } } -#[cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] impl std::error::Error for InternalError {} +impl From for TxSyncError { + fn from(_e: InternalError) -> Self { + Self::Failed + } +} + #[cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] impl From for TxSyncError { fn from(_e: esplora_client::Error) -> Self { @@ -55,9 +58,16 @@ impl From for InternalError { } } -#[cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] -impl From for TxSyncError { - fn from(_e: InternalError) -> Self { +#[cfg(feature = "electrum")] +impl From for InternalError { + fn from(_e: electrum_client::Error) -> Self { + Self::Failed + } +} + +#[cfg(feature = "electrum")] +impl From for TxSyncError { + fn from(_e: electrum_client::Error) -> Self { Self::Failed } } diff --git a/lightning-transaction-sync/src/esplora.rs b/lightning-transaction-sync/src/esplora.rs index f68be08a5a6..953f8b0718c 100644 --- a/lightning-transaction-sync/src/esplora.rs +++ b/lightning-transaction-sync/src/esplora.rs @@ -2,7 +2,7 @@ use crate::error::{TxSyncError, InternalError}; use crate::common::{SyncState, FilterQueue, ConfirmedTx}; use lightning::util::logger::Logger; -use lightning::{log_error, log_info, log_debug, log_trace}; +use lightning::{log_error, log_debug, log_trace}; use lightning::chain::WatchedOutput; use lightning::chain::{Confirm, Filter}; @@ -14,6 +14,7 @@ use esplora_client::r#async::AsyncClient; #[cfg(not(feature = "async-interface"))] use esplora_client::blocking::BlockingClient; +use std::time::Instant; use std::collections::HashSet; use core::ops::Deref; @@ -89,7 +90,10 @@ where #[cfg(feature = "async-interface")] let mut sync_state = self.sync_state.lock().await; - log_info!(self.logger, "Starting transaction sync."); + log_trace!(self.logger, "Starting transaction sync."); + let start_time = Instant::now(); + let mut num_confirmed = 0; + let mut num_unconfirmed = 0; let mut tip_hash = maybe_await!(self.client.get_tip_hash())?; @@ -110,17 +114,40 @@ where Ok(unconfirmed_txs) => { // Double-check the tip hash. If it changed, a reorg happened since // we started syncing and we need to restart last-minute. - let check_tip_hash = maybe_await!(self.client.get_tip_hash())?; - if check_tip_hash != tip_hash { - tip_hash = check_tip_hash; - continue; + match maybe_await!(self.client.get_tip_hash()) { + Ok(check_tip_hash) => { + if check_tip_hash != tip_hash { + tip_hash = check_tip_hash; + + log_debug!(self.logger, "Encountered inconsistency during transaction sync, restarting."); + sync_state.pending_sync = true; + continue; + } + num_unconfirmed += unconfirmed_txs.len(); + sync_state.sync_unconfirmed_transactions( + &confirmables, + unconfirmed_txs + ); + } + Err(err) => { + // (Semi-)permanent failure, retry later. + log_error!(self.logger, + "Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.", + num_confirmed, + num_unconfirmed + ); + sync_state.pending_sync = true; + return Err(TxSyncError::from(err)); + } } - - self.sync_unconfirmed_transactions(&mut sync_state, &confirmables, unconfirmed_txs); }, Err(err) => { // (Semi-)permanent failure, retry later. - log_error!(self.logger, "Failed during transaction sync, aborting."); + log_error!(self.logger, + "Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.", + num_confirmed, + num_unconfirmed + ); sync_state.pending_sync = true; return Err(TxSyncError::from(err)); } @@ -136,6 +163,11 @@ where } Err(err) => { // (Semi-)permanent failure, retry later. + log_error!(self.logger, + "Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.", + num_confirmed, + num_unconfirmed + ); sync_state.pending_sync = true; return Err(TxSyncError::from(err)); } @@ -146,17 +178,33 @@ where Ok(confirmed_txs) => { // Double-check the tip hash. If it changed, a reorg happened since // we started syncing and we need to restart last-minute. - let check_tip_hash = maybe_await!(self.client.get_tip_hash())?; - if check_tip_hash != tip_hash { - tip_hash = check_tip_hash; - continue; + match maybe_await!(self.client.get_tip_hash()) { + Ok(check_tip_hash) => { + if check_tip_hash != tip_hash { + tip_hash = check_tip_hash; + + log_debug!(self.logger, + "Encountered inconsistency during transaction sync, restarting."); + sync_state.pending_sync = true; + continue; + } + num_confirmed += confirmed_txs.len(); + sync_state.sync_confirmed_transactions( + &confirmables, + confirmed_txs + ); + } + Err(err) => { + // (Semi-)permanent failure, retry later. + log_error!(self.logger, + "Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.", + num_confirmed, + num_unconfirmed + ); + sync_state.pending_sync = true; + return Err(TxSyncError::from(err)); + } } - - self.sync_confirmed_transactions( - &mut sync_state, - &confirmables, - confirmed_txs, - ); } Err(InternalError::Inconsistency) => { // Immediately restart syncing when we encounter any inconsistencies. @@ -166,7 +214,11 @@ where } Err(err) => { // (Semi-)permanent failure, retry later. - log_error!(self.logger, "Failed during transaction sync, aborting."); + log_error!(self.logger, + "Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.", + num_confirmed, + num_unconfirmed + ); sync_state.pending_sync = true; return Err(TxSyncError::from(err)); } @@ -175,7 +227,8 @@ where sync_state.pending_sync = false; } } - log_info!(self.logger, "Finished transaction sync."); + log_debug!(self.logger, "Finished transaction sync at tip {} in {}ms: {} confirmed, {} unconfirmed.", + tip_hash, start_time.elapsed().as_millis(), num_confirmed, num_unconfirmed); Ok(()) } @@ -199,26 +252,6 @@ where Ok(()) } - fn sync_confirmed_transactions( - &self, sync_state: &mut SyncState, confirmables: &Vec<&(dyn Confirm + Sync + Send)>, confirmed_txs: Vec, - ) { - for ctx in confirmed_txs { - for c in confirmables { - c.transactions_confirmed( - &ctx.block_header, - &[(ctx.pos, &ctx.tx)], - ctx.block_height, - ); - } - - sync_state.watched_transactions.remove(&ctx.tx.txid()); - - for input in &ctx.tx.input { - sync_state.watched_outputs.remove(&input.previous_output); - } - } - } - #[maybe_async] fn get_confirmed_transactions( &self, sync_state: &SyncState, @@ -286,7 +319,8 @@ where return Err(InternalError::Failed); } - let pos = *indexes.get(0).ok_or(InternalError::Failed)? as usize; + // unwrap() safety: len() > 0 is checked above + let pos = *indexes.first().unwrap() as usize; if let Some(tx) = maybe_await!(self.client.get_tx(&txid))? { if let Some(block_height) = known_block_height { // We can take a shortcut here if a previous call already gave us the height. @@ -316,11 +350,11 @@ where let relevant_txids = confirmables .iter() .flat_map(|c| c.get_relevant_txids()) - .collect::)>>(); + .collect::)>>(); let mut unconfirmed_txs = Vec::new(); - for (txid, block_hash_opt) in relevant_txids { + for (txid, _conf_height, block_hash_opt) in relevant_txids { if let Some(block_hash) = block_hash_opt { let block_status = maybe_await!(self.client.get_block_status(&block_hash))?; if block_status.in_best_chain { @@ -337,18 +371,6 @@ where Ok(unconfirmed_txs) } - fn sync_unconfirmed_transactions( - &self, sync_state: &mut SyncState, confirmables: &Vec<&(dyn Confirm + Sync + Send)>, unconfirmed_txs: Vec, - ) { - for txid in unconfirmed_txs { - for c in confirmables { - c.transaction_unconfirmed(&txid); - } - - sync_state.watched_transactions.insert(txid); - } - } - /// Returns a reference to the underlying esplora client. pub fn client(&self) -> &EsploraClientType { &self.client diff --git a/lightning-transaction-sync/src/lib.rs b/lightning-transaction-sync/src/lib.rs index ca3ce3f8ad6..21b6a4e97c1 100644 --- a/lightning-transaction-sync/src/lib.rs +++ b/lightning-transaction-sync/src/lib.rs @@ -74,11 +74,17 @@ extern crate bdk_macros; #[cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] mod esplora; -#[cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] -mod common; +#[cfg(any(feature = "electrum"))] +mod electrum; +#[cfg(any(feature = "esplora-blocking", feature = "esplora-async", feature = "electrum"))] +mod common; +#[cfg(any(feature = "esplora-blocking", feature = "esplora-async", feature = "electrum"))] mod error; +#[cfg(any(feature = "esplora-blocking", feature = "esplora-async", feature = "electrum"))] pub use error::TxSyncError; #[cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] pub use esplora::EsploraSyncClient; +#[cfg(feature = "electrum")] +pub use electrum::ElectrumSyncClient; diff --git a/lightning-transaction-sync/tests/integration_tests.rs b/lightning-transaction-sync/tests/integration_tests.rs index 8a69dce3f3d..8aadf9a2ed1 100644 --- a/lightning-transaction-sync/tests/integration_tests.rs +++ b/lightning-transaction-sync/tests/integration_tests.rs @@ -1,8 +1,12 @@ -#![cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] +#![cfg(any(feature = "esplora-blocking", feature = "esplora-async", feature = "electrum"))] + +#[cfg(any(feature = "esplora-blocking", feature = "esplora-async"))] use lightning_transaction_sync::EsploraSyncClient; -use lightning::chain::{Confirm, Filter}; -use lightning::chain::transaction::TransactionData; -use lightning::util::logger::{Logger, Record}; +#[cfg(feature = "electrum")] +use lightning_transaction_sync::ElectrumSyncClient; +use lightning::chain::{Confirm, Filter, WatchedOutput}; +use lightning::chain::transaction::{OutPoint, TransactionData}; +use lightning::util::test_utils::TestLogger; use electrsd::{bitcoind, bitcoind::BitcoinD, ElectrsD}; use bitcoin::{Amount, Txid, BlockHash}; @@ -11,7 +15,7 @@ use bitcoin::blockdata::constants::genesis_block; use bitcoin::network::constants::Network; use electrsd::bitcoind::bitcoincore_rpc::bitcoincore_rpc_json::AddressType; use bitcoind::bitcoincore_rpc::RpcApi; -use electrsd::electrum_client::ElectrumApi; +use bdk_macros::maybe_await; use std::env; use std::sync::Mutex; @@ -51,6 +55,7 @@ pub fn generate_blocks_and_wait(bitcoind: &BitcoinD, electrsd: &ElectrsD, num: u } pub fn wait_for_block(electrsd: &ElectrsD, min_height: usize) { + use electrsd::electrum_client::ElectrumApi; let mut header = match electrsd.client.block_headers_subscribe_raw() { Ok(header) => header, Err(_) => { @@ -143,19 +148,134 @@ impl Confirm for TestConfirmable { self.events.lock().unwrap().push(TestConfirmableEvent::BestBlockUpdated(block_hash, height)); } - fn get_relevant_txids(&self) -> Vec<(Txid, Option)> { - self.confirmed_txs.lock().unwrap().iter().map(|(&txid, (hash, _))| (txid, Some(*hash))).collect::>() + fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { + self.confirmed_txs.lock().unwrap().iter().map(|(&txid, (hash, height))| (txid, *height, Some(*hash))).collect::>() } } -pub struct TestLogger {} +macro_rules! test_syncing { + ($tx_sync: expr, $confirmable: expr, $bitcoind: expr, $electrsd: expr) => {{ + // Check we pick up on new best blocks + assert_eq!($confirmable.best_block.lock().unwrap().1, 0); + + maybe_await!($tx_sync.sync(vec![&$confirmable])).unwrap(); + assert_eq!($confirmable.best_block.lock().unwrap().1, 102); + + let events = std::mem::take(&mut *$confirmable.events.lock().unwrap()); + assert_eq!(events.len(), 1); + + // Check registered confirmed transactions are marked confirmed + let new_address = $bitcoind.client.get_new_address(Some("test"), + Some(AddressType::Legacy)).unwrap().assume_checked(); + let txid = $bitcoind.client.send_to_address(&new_address, Amount::from_sat(5000), None, None, + None, None, None, None).unwrap(); + let second_txid = $bitcoind.client.send_to_address(&new_address, Amount::from_sat(5000), None, + None, None, None, None, None).unwrap(); + $tx_sync.register_tx(&txid, &new_address.payload.script_pubkey()); + + maybe_await!($tx_sync.sync(vec![&$confirmable])).unwrap(); + + let events = std::mem::take(&mut *$confirmable.events.lock().unwrap()); + assert_eq!(events.len(), 0); + assert!($confirmable.confirmed_txs.lock().unwrap().is_empty()); + assert!($confirmable.unconfirmed_txs.lock().unwrap().is_empty()); + + generate_blocks_and_wait(&$bitcoind, &$electrsd, 1); + maybe_await!($tx_sync.sync(vec![&$confirmable])).unwrap(); + + let events = std::mem::take(&mut *$confirmable.events.lock().unwrap()); + assert_eq!(events.len(), 2); + assert!($confirmable.confirmed_txs.lock().unwrap().contains_key(&txid)); + assert!($confirmable.unconfirmed_txs.lock().unwrap().is_empty()); + + // Now take an arbitrary output of the second transaction and check we'll confirm its spend. + let tx_res = $bitcoind.client.get_transaction(&second_txid, None).unwrap(); + let block_hash = tx_res.info.blockhash.unwrap(); + let tx = tx_res.transaction().unwrap(); + let prev_outpoint = tx.input.first().unwrap().previous_output; + let prev_tx = $bitcoind.client.get_transaction(&prev_outpoint.txid, None).unwrap().transaction() + .unwrap(); + let prev_script_pubkey = prev_tx.output[prev_outpoint.vout as usize].script_pubkey.clone(); + let output = WatchedOutput { + block_hash: Some(block_hash), + outpoint: OutPoint { txid: prev_outpoint.txid, index: prev_outpoint.vout as u16 }, + script_pubkey: prev_script_pubkey + }; + + $tx_sync.register_output(output); + maybe_await!($tx_sync.sync(vec![&$confirmable])).unwrap(); + + let events = std::mem::take(&mut *$confirmable.events.lock().unwrap()); + assert_eq!(events.len(), 1); + assert!($confirmable.confirmed_txs.lock().unwrap().contains_key(&second_txid)); + assert_eq!($confirmable.confirmed_txs.lock().unwrap().len(), 2); + assert!($confirmable.unconfirmed_txs.lock().unwrap().is_empty()); + + // Check previously confirmed transactions are marked unconfirmed when they are reorged. + let best_block_hash = $bitcoind.client.get_best_block_hash().unwrap(); + $bitcoind.client.invalidate_block(&best_block_hash).unwrap(); + + // We're getting back to the previous height with a new tip, but best block shouldn't change. + generate_blocks_and_wait(&$bitcoind, &$electrsd, 1); + assert_ne!($bitcoind.client.get_best_block_hash().unwrap(), best_block_hash); + maybe_await!($tx_sync.sync(vec![&$confirmable])).unwrap(); + let events = std::mem::take(&mut *$confirmable.events.lock().unwrap()); + assert_eq!(events.len(), 0); + + // Now we're surpassing previous height, getting new tip. + generate_blocks_and_wait(&$bitcoind, &$electrsd, 1); + assert_ne!($bitcoind.client.get_best_block_hash().unwrap(), best_block_hash); + maybe_await!($tx_sync.sync(vec![&$confirmable])).unwrap(); + + // Transactions still confirmed but under new tip. + assert!($confirmable.confirmed_txs.lock().unwrap().contains_key(&txid)); + assert!($confirmable.confirmed_txs.lock().unwrap().contains_key(&second_txid)); + assert!($confirmable.unconfirmed_txs.lock().unwrap().is_empty()); + + // Check we got unconfirmed, then reconfirmed in the meantime. + let mut seen_txids = HashSet::new(); + let events = std::mem::take(&mut *$confirmable.events.lock().unwrap()); + assert_eq!(events.len(), 5); + + match events[0] { + TestConfirmableEvent::Unconfirmed(t) => { + assert!(t == txid || t == second_txid); + assert!(seen_txids.insert(t)); + }, + _ => panic!("Unexpected event"), + } -impl Logger for TestLogger { - fn log(&self, record: &Record) { - println!("{} -- {}", - record.level, - record.args); - } + match events[1] { + TestConfirmableEvent::Unconfirmed(t) => { + assert!(t == txid || t == second_txid); + assert!(seen_txids.insert(t)); + }, + _ => panic!("Unexpected event"), + } + + match events[2] { + TestConfirmableEvent::BestBlockUpdated(..) => {}, + _ => panic!("Unexpected event"), + } + + match events[3] { + TestConfirmableEvent::Confirmed(t, _, _) => { + assert!(t == txid || t == second_txid); + assert!(seen_txids.remove(&t)); + }, + _ => panic!("Unexpected event"), + } + + match events[4] { + TestConfirmableEvent::Confirmed(t, _, _) => { + assert!(t == txid || t == second_txid); + assert!(seen_txids.remove(&t)); + }, + _ => panic!("Unexpected event"), + } + + assert_eq!(seen_txids.len(), 0); + }}; } #[test] @@ -163,82 +283,12 @@ impl Logger for TestLogger { fn test_esplora_syncs() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); generate_blocks_and_wait(&bitcoind, &electrsd, 101); - let mut logger = TestLogger {}; + let mut logger = TestLogger::new(); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); let tx_sync = EsploraSyncClient::new(esplora_url, &mut logger); let confirmable = TestConfirmable::new(); - // Check we pick up on new best blocks - assert_eq!(confirmable.best_block.lock().unwrap().1, 0); - - tx_sync.sync(vec![&confirmable]).unwrap(); - assert_eq!(confirmable.best_block.lock().unwrap().1, 102); - - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 1); - - // Check registered confirmed transactions are marked confirmed - let new_address = bitcoind.client.get_new_address(Some("test"), Some(AddressType::Legacy)).unwrap().assume_checked(); - let txid = bitcoind.client.send_to_address(&new_address, Amount::from_sat(5000), None, None, None, None, None, None).unwrap(); - tx_sync.register_tx(&txid, &new_address.payload.script_pubkey()); - - tx_sync.sync(vec![&confirmable]).unwrap(); - - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 0); - assert!(confirmable.confirmed_txs.lock().unwrap().is_empty()); - assert!(confirmable.unconfirmed_txs.lock().unwrap().is_empty()); - - generate_blocks_and_wait(&bitcoind, &electrsd, 1); - tx_sync.sync(vec![&confirmable]).unwrap(); - - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 2); - assert!(confirmable.confirmed_txs.lock().unwrap().contains_key(&txid)); - assert!(confirmable.unconfirmed_txs.lock().unwrap().is_empty()); - - // Check previously confirmed transactions are marked unconfirmed when they are reorged. - let best_block_hash = bitcoind.client.get_best_block_hash().unwrap(); - bitcoind.client.invalidate_block(&best_block_hash).unwrap(); - - // We're getting back to the previous height with a new tip, but best block shouldn't change. - generate_blocks_and_wait(&bitcoind, &electrsd, 1); - assert_ne!(bitcoind.client.get_best_block_hash().unwrap(), best_block_hash); - tx_sync.sync(vec![&confirmable]).unwrap(); - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 0); - - // Now we're surpassing previous height, getting new tip. - generate_blocks_and_wait(&bitcoind, &electrsd, 1); - assert_ne!(bitcoind.client.get_best_block_hash().unwrap(), best_block_hash); - tx_sync.sync(vec![&confirmable]).unwrap(); - - // Transaction still confirmed but under new tip. - assert!(confirmable.confirmed_txs.lock().unwrap().contains_key(&txid)); - assert!(confirmable.unconfirmed_txs.lock().unwrap().is_empty()); - - // Check we got unconfirmed, then reconfirmed in the meantime. - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 3); - - match events[0] { - TestConfirmableEvent::Unconfirmed(t) => { - assert_eq!(t, txid); - }, - _ => panic!("Unexpected event"), - } - - match events[1] { - TestConfirmableEvent::BestBlockUpdated(..) => {}, - _ => panic!("Unexpected event"), - } - - match events[2] { - TestConfirmableEvent::Confirmed(t, _, _) => { - assert_eq!(t, txid); - }, - _ => panic!("Unexpected event"), - } + test_syncing!(tx_sync, confirmable, bitcoind, electrsd); } #[tokio::test] @@ -246,80 +296,22 @@ fn test_esplora_syncs() { async fn test_esplora_syncs() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); generate_blocks_and_wait(&bitcoind, &electrsd, 101); - let mut logger = TestLogger {}; + let mut logger = TestLogger::new(); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); let tx_sync = EsploraSyncClient::new(esplora_url, &mut logger); let confirmable = TestConfirmable::new(); - // Check we pick up on new best blocks - assert_eq!(confirmable.best_block.lock().unwrap().1, 0); - - tx_sync.sync(vec![&confirmable]).await.unwrap(); - assert_eq!(confirmable.best_block.lock().unwrap().1, 102); - - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 1); - - // Check registered confirmed transactions are marked confirmed - let new_address = bitcoind.client.get_new_address(Some("test"), Some(AddressType::Legacy)).unwrap().assume_checked(); - let txid = bitcoind.client.send_to_address(&new_address, Amount::from_sat(5000), None, None, None, None, None, None).unwrap(); - tx_sync.register_tx(&txid, &new_address.payload.script_pubkey()); - - tx_sync.sync(vec![&confirmable]).await.unwrap(); - - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 0); - assert!(confirmable.confirmed_txs.lock().unwrap().is_empty()); - assert!(confirmable.unconfirmed_txs.lock().unwrap().is_empty()); - - generate_blocks_and_wait(&bitcoind, &electrsd, 1); - tx_sync.sync(vec![&confirmable]).await.unwrap(); - - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 2); - assert!(confirmable.confirmed_txs.lock().unwrap().contains_key(&txid)); - assert!(confirmable.unconfirmed_txs.lock().unwrap().is_empty()); - - // Check previously confirmed transactions are marked unconfirmed when they are reorged. - let best_block_hash = bitcoind.client.get_best_block_hash().unwrap(); - bitcoind.client.invalidate_block(&best_block_hash).unwrap(); - - // We're getting back to the previous height with a new tip, but best block shouldn't change. - generate_blocks_and_wait(&bitcoind, &electrsd, 1); - assert_ne!(bitcoind.client.get_best_block_hash().unwrap(), best_block_hash); - tx_sync.sync(vec![&confirmable]).await.unwrap(); - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 0); - - // Now we're surpassing previous height, getting new tip. - generate_blocks_and_wait(&bitcoind, &electrsd, 1); - assert_ne!(bitcoind.client.get_best_block_hash().unwrap(), best_block_hash); - tx_sync.sync(vec![&confirmable]).await.unwrap(); - - // Transaction still confirmed but under new tip. - assert!(confirmable.confirmed_txs.lock().unwrap().contains_key(&txid)); - assert!(confirmable.unconfirmed_txs.lock().unwrap().is_empty()); - - // Check we got unconfirmed, then reconfirmed in the meantime. - let events = std::mem::take(&mut *confirmable.events.lock().unwrap()); - assert_eq!(events.len(), 3); - - match events[0] { - TestConfirmableEvent::Unconfirmed(t) => { - assert_eq!(t, txid); - }, - _ => panic!("Unexpected event"), - } - - match events[1] { - TestConfirmableEvent::BestBlockUpdated(..) => {}, - _ => panic!("Unexpected event"), - } + test_syncing!(tx_sync, confirmable, bitcoind, electrsd); +} - match events[2] { - TestConfirmableEvent::Confirmed(t, _, _) => { - assert_eq!(t, txid); - }, - _ => panic!("Unexpected event"), - } +#[test] +#[cfg(feature = "electrum")] +fn test_electrum_syncs() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + generate_blocks_and_wait(&bitcoind, &electrsd, 101); + let mut logger = TestLogger::new(); + let electrum_url = format!("tcp://{}", electrsd.electrum_url); + let tx_sync = ElectrumSyncClient::new(electrum_url, &mut logger).unwrap(); + let confirmable = TestConfirmable::new(); + test_syncing!(tx_sync, confirmable, bitcoind, electrsd); } diff --git a/lightning/src/chain/chainmonitor.rs b/lightning/src/chain/chainmonitor.rs index 0186ac37488..800bee8e3b3 100644 --- a/lightning/src/chain/chainmonitor.rs +++ b/lightning/src/chain/chainmonitor.rs @@ -689,15 +689,15 @@ where }); } - fn get_relevant_txids(&self) -> Vec<(Txid, Option)> { + fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { let mut txids = Vec::new(); let monitor_states = self.monitors.read().unwrap(); for monitor_state in monitor_states.values() { txids.append(&mut monitor_state.monitor.get_relevant_txids()); } - txids.sort_unstable(); - txids.dedup(); + txids.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1))); + txids.dedup_by_key(|(txid, _, _)| *txid); txids } } diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index d78f40f3fac..65b58722762 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -1634,15 +1634,15 @@ impl ChannelMonitor { } /// Returns the set of txids that should be monitored for re-organization out of the chain. - pub fn get_relevant_txids(&self) -> Vec<(Txid, Option)> { + pub fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { let inner = self.inner.lock().unwrap(); - let mut txids: Vec<(Txid, Option)> = inner.onchain_events_awaiting_threshold_conf + let mut txids: Vec<(Txid, u32, Option)> = inner.onchain_events_awaiting_threshold_conf .iter() - .map(|entry| (entry.txid, entry.block_hash)) + .map(|entry| (entry.txid, entry.height, entry.block_hash)) .chain(inner.onchain_tx_handler.get_relevant_txids().into_iter()) .collect(); - txids.sort_unstable(); - txids.dedup(); + txids.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1))); + txids.dedup_by_key(|(txid, _, _)| *txid); txids } @@ -4171,7 +4171,7 @@ where self.0.best_block_updated(header, height, &*self.1, &*self.2, &*self.3); } - fn get_relevant_txids(&self) -> Vec<(Txid, Option)> { + fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { self.0.get_relevant_txids() } } diff --git a/lightning/src/chain/mod.rs b/lightning/src/chain/mod.rs index 239f7c91dbc..a7c3a6d88ba 100644 --- a/lightning/src/chain/mod.rs +++ b/lightning/src/chain/mod.rs @@ -152,7 +152,7 @@ pub trait Confirm { /// blocks. fn best_block_updated(&self, header: &Header, height: u32); /// Returns transactions that must be monitored for reorganization out of the chain along - /// with the hash of the block as part of which it had been previously confirmed. + /// with the height and the hash of the block as part of which it had been previously confirmed. /// /// Note that the returned `Option` might be `None` for channels created with LDK /// 0.0.112 and prior, in which case you need to manually track previous confirmations. @@ -167,12 +167,12 @@ pub trait Confirm { /// given to [`transaction_unconfirmed`]. /// /// If any of the returned transactions are confirmed in a block other than the one with the - /// given hash, they need to be unconfirmed and reconfirmed via [`transaction_unconfirmed`] and - /// [`transactions_confirmed`], respectively. + /// given hash at the given height, they need to be unconfirmed and reconfirmed via + /// [`transaction_unconfirmed`] and [`transactions_confirmed`], respectively. /// /// [`transactions_confirmed`]: Self::transactions_confirmed /// [`transaction_unconfirmed`]: Self::transaction_unconfirmed - fn get_relevant_txids(&self) -> Vec<(Txid, Option)>; + fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)>; } /// An enum representing the status of a channel monitor update persistence. diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index 2573854898c..c28c572e6b5 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -1076,13 +1076,13 @@ impl OnchainTxHandler self.claimable_outpoints.get(outpoint).is_some() } - pub(crate) fn get_relevant_txids(&self) -> Vec<(Txid, Option)> { - let mut txids: Vec<(Txid, Option)> = self.onchain_events_awaiting_threshold_conf + pub(crate) fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { + let mut txids: Vec<(Txid, u32, Option)> = self.onchain_events_awaiting_threshold_conf .iter() - .map(|entry| (entry.txid, entry.block_hash)) + .map(|entry| (entry.txid, entry.height, entry.block_hash)) .collect(); - txids.sort_unstable_by_key(|(txid, _)| *txid); - txids.dedup(); + txids.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1))); + txids.dedup_by_key(|(txid, _, _)| *txid); txids } diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 6803b11284b..740d2644807 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1111,6 +1111,16 @@ impl ChannelContext where SP::Target: SignerProvider { self.channel_transaction_parameters.funding_outpoint } + /// Returns the height in which our funding transaction was confirmed. + pub fn get_funding_tx_confirmation_height(&self) -> Option { + let conf_height = self.funding_tx_confirmation_height; + if conf_height > 0 { + Some(conf_height) + } else { + None + } + } + /// Returns the block hash in which our funding transaction was confirmed. pub fn get_funding_tx_confirmed_in(&self) -> Option { self.funding_tx_confirmed_in diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c40ab4858b1..3f6277b44bb 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8336,14 +8336,17 @@ where }); } - fn get_relevant_txids(&self) -> Vec<(Txid, Option)> { + fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { let mut res = Vec::with_capacity(self.short_to_chan_info.read().unwrap().len()); for (_cp_id, peer_state_mutex) in self.per_peer_state.read().unwrap().iter() { let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; for chan in peer_state.channel_by_id.values().filter_map(|phase| if let ChannelPhase::Funded(chan) = phase { Some(chan) } else { None }) { - if let (Some(funding_txo), Some(block_hash)) = (chan.context.get_funding_txo(), chan.context.get_funding_tx_confirmed_in()) { - res.push((funding_txo.txid, Some(block_hash))); + let txid_opt = chan.context.get_funding_txo(); + let height_opt = chan.context.get_funding_tx_confirmation_height(); + let hash_opt = chan.context.get_funding_tx_confirmed_in(); + if let (Some(funding_txo), Some(conf_height), Some(block_hash)) = (txid_opt, height_opt, hash_opt) { + res.push((funding_txo.txid, conf_height, Some(block_hash))); } } } diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs index b745453a3c6..badb78f245a 100644 --- a/lightning/src/ln/reorg_tests.rs +++ b/lightning/src/ln/reorg_tests.rs @@ -270,8 +270,9 @@ fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_ if use_funding_unconfirmed { let relevant_txids = nodes[0].node.get_relevant_txids(); assert_eq!(relevant_txids.len(), 1); - let block_hash_opt = relevant_txids[0].1; + let block_hash_opt = relevant_txids[0].2; let expected_hash = nodes[0].get_block_header(chan_conf_height).block_hash(); + assert_eq!(relevant_txids[0].1, chan_conf_height); assert_eq!(block_hash_opt, Some(expected_hash)); let txid = relevant_txids[0].0; assert_eq!(txid, chan.3.txid()); @@ -315,8 +316,9 @@ fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_ if use_funding_unconfirmed { let relevant_txids = nodes[0].node.get_relevant_txids(); assert_eq!(relevant_txids.len(), 1); - let block_hash_opt = relevant_txids[0].1; + let block_hash_opt = relevant_txids[0].2; let expected_hash = nodes[0].get_block_header(chan_conf_height).block_hash(); + assert_eq!(chan_conf_height, relevant_txids[0].1); assert_eq!(block_hash_opt, Some(expected_hash)); let txid = relevant_txids[0].0; assert_eq!(txid, chan.3.txid()); diff --git a/pending_changelog/electrum.txt b/pending_changelog/electrum.txt new file mode 100644 index 00000000000..5171f5ea082 --- /dev/null +++ b/pending_changelog/electrum.txt @@ -0,0 +1,3 @@ +## API Updates + +- The `Confirm::get_relevant_txids()` call now also returns the height under which LDK expects the respective transaction to be confirmed.