Skip to main content

lightning_transaction_sync/
electrum.rs

1// This file is Copyright its original authors, visible in version control history.
2//
3// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6// accordance with one or both of these licenses.
7
8use crate::common::{is_potentially_unsafe_merkle_leaf, ConfirmedTx, FilterQueue, SyncState};
9use crate::error::{InternalError, TxSyncError};
10
11use electrum_client::utils::validate_merkle_proof;
12use electrum_client::Client as ElectrumClient;
13use electrum_client::ElectrumApi;
14
15use lightning::chain::WatchedOutput;
16use lightning::chain::{Confirm, Filter};
17use lightning::util::logger::Logger;
18use lightning::{log_debug, log_error, log_trace};
19
20use bitcoin::block::Header;
21use bitcoin::{BlockHash, Script, Transaction, Txid};
22
23use std::collections::HashSet;
24use std::ops::Deref;
25use std::sync::{Arc, Mutex};
26use std::time::Instant;
27
28/// Synchronizes LDK with a given Electrum server.
29///
30/// Needs to be registered with a [`ChainMonitor`] via the [`Filter`] interface to be informed of
31/// transactions and outputs to monitor for on-chain confirmation, unconfirmation, and
32/// reconfirmation.
33///
34/// Note that registration via [`Filter`] needs to happen before any calls to
35/// [`Watch::watch_channel`] to ensure we get notified of the items to monitor.
36///
37/// [`ChainMonitor`]: lightning::chain::chainmonitor::ChainMonitor
38/// [`Watch::watch_channel`]: lightning::chain::Watch::watch_channel
39/// [`Filter`]: lightning::chain::Filter
40pub struct ElectrumSyncClient<L: Logger> {
41	sync_state: Mutex<SyncState>,
42	queue: Mutex<FilterQueue>,
43	client: Arc<ElectrumClient>,
44	logger: L,
45}
46
47impl<L: Logger> ElectrumSyncClient<L> {
48	/// Returns a new [`ElectrumSyncClient`] object.
49	pub fn new(server_url: String, logger: L) -> Result<Self, TxSyncError> {
50		let client = Arc::new(ElectrumClient::new(&server_url).map_err(|e| {
51			log_error!(logger, "Failed to connect to electrum server '{}': {}", server_url, e);
52			e
53		})?);
54
55		Self::from_client(client, logger)
56	}
57
58	/// Returns a new [`ElectrumSyncClient`] object using the given Electrum client.
59	///
60	/// This is not exported to bindings users as the underlying client from BDK is not exported.
61	pub fn from_client(client: Arc<ElectrumClient>, logger: L) -> Result<Self, TxSyncError> {
62		let sync_state = Mutex::new(SyncState::new());
63		let queue = Mutex::new(FilterQueue::new());
64
65		Ok(Self { sync_state, queue, client, logger })
66	}
67
68	/// Synchronizes the given `confirmables` via their [`Confirm`] interface implementations. This
69	/// method should be called regularly to keep LDK up-to-date with current chain data.
70	///
71	/// For example, instances of [`ChannelManager`] and [`ChainMonitor`] can be informed about the
72	/// newest on-chain activity related to the items previously registered via the [`Filter`]
73	/// interface.
74	///
75	/// [`Confirm`]: lightning::chain::Confirm
76	/// [`ChainMonitor`]: lightning::chain::chainmonitor::ChainMonitor
77	/// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager
78	/// [`Filter`]: lightning::chain::Filter
79	pub fn sync<C: Deref>(&self, confirmables: Vec<C>) -> Result<(), TxSyncError>
80	where
81		C::Target: Confirm,
82	{
83		// This lock makes sure we're syncing once at a time.
84		let mut sync_state = self.sync_state.lock().unwrap();
85
86		log_trace!(self.logger, "Starting transaction sync.");
87		#[cfg(feature = "time")]
88		let start_time = Instant::now();
89		let mut num_confirmed = 0;
90		let mut num_unconfirmed = 0;
91
92		// Clear any header notifications we might have gotten to keep the queue count low.
93		while let Some(_) = self.client.block_headers_pop()? {}
94
95		let tip_notification = self.client.block_headers_subscribe()?;
96		let mut tip_header = tip_notification.header;
97		let mut tip_height = tip_notification.height as u32;
98
99		for i in 0..100 {
100			if i >= 10 {
101				log_debug!(self.logger, "Giving up trying to sync transactions after 10 attempts.");
102				sync_state.pending_sync = true;
103				return Err(TxSyncError::Failed);
104			}
105
106			let pending_registrations = self.queue.lock().unwrap().process_queues(&mut sync_state);
107			let tip_is_new = Some(tip_header.block_hash()) != sync_state.last_sync_hash;
108
109			// We loop until any registered transactions have been processed at least once, or the
110			// tip hasn't been updated during the last iteration.
111			if !sync_state.pending_sync && !pending_registrations && !tip_is_new {
112				// Nothing to do.
113				break;
114			} else {
115				// Update the known tip to the newest one.
116				if tip_is_new {
117					// First check for any unconfirmed transactions and act on it immediately.
118					match self.get_unconfirmed_transactions(&confirmables) {
119						Ok(unconfirmed_txs) => {
120							// Double-check the tip hash. If it changed, a reorg happened since
121							// we started syncing and we need to restart last-minute.
122							match self.check_update_tip(&mut tip_header, &mut tip_height) {
123								Ok(false) => {
124									num_unconfirmed += unconfirmed_txs.len();
125									sync_state.sync_unconfirmed_transactions(
126										&confirmables,
127										unconfirmed_txs,
128									);
129								},
130								Ok(true) => {
131									log_debug!(self.logger,
132										"Encountered inconsistency during transaction sync, restarting.");
133									sync_state.pending_sync = true;
134									continue;
135								},
136								Err(err) => {
137									// (Semi-)permanent failure, retry later.
138									log_error!(self.logger,
139										"Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.",
140										num_confirmed,
141										num_unconfirmed
142									);
143									sync_state.pending_sync = true;
144									return Err(TxSyncError::from(err));
145								},
146							}
147						},
148						Err(err) => {
149							// (Semi-)permanent failure, retry later.
150							log_error!(self.logger,
151								"Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.",
152								num_confirmed,
153								num_unconfirmed
154							);
155							sync_state.pending_sync = true;
156							return Err(TxSyncError::from(err));
157						},
158					}
159
160					// Update the best block.
161					for c in &confirmables {
162						c.best_block_updated(&tip_header, tip_height);
163					}
164
165					// Prune any sufficiently confirmed output spends
166					sync_state.prune_output_spends(tip_height);
167				}
168
169				match self.get_confirmed_transactions(&sync_state) {
170					Ok(confirmed_txs) => {
171						// Double-check the tip hash. If it changed, a reorg happened since
172						// we started syncing and we need to restart last-minute.
173						match self.check_update_tip(&mut tip_header, &mut tip_height) {
174							Ok(false) => {
175								num_confirmed += confirmed_txs.len();
176								sync_state
177									.sync_confirmed_transactions(&confirmables, confirmed_txs);
178							},
179							Ok(true) => {
180								log_debug!(self.logger,
181									"Encountered inconsistency during transaction sync, restarting.");
182								sync_state.pending_sync = true;
183								continue;
184							},
185							Err(err) => {
186								// (Semi-)permanent failure, retry later.
187								log_error!(self.logger,
188									"Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.",
189									num_confirmed,
190									num_unconfirmed
191								);
192								sync_state.pending_sync = true;
193								return Err(TxSyncError::from(err));
194							},
195						}
196					},
197					Err(InternalError::Inconsistency) => {
198						// Immediately restart syncing when we encounter any inconsistencies.
199						log_debug!(
200							self.logger,
201							"Encountered inconsistency during transaction sync, restarting."
202						);
203						sync_state.pending_sync = true;
204						continue;
205					},
206					Err(err) => {
207						// (Semi-)permanent failure, retry later.
208						log_error!(self.logger,
209							"Failed during transaction sync, aborting. Synced so far: {} confirmed, {} unconfirmed.",
210							num_confirmed,
211							num_unconfirmed
212						);
213						sync_state.pending_sync = true;
214						return Err(TxSyncError::from(err));
215					},
216				}
217				sync_state.last_sync_hash = Some(tip_header.block_hash());
218				sync_state.pending_sync = false;
219			}
220		}
221		#[cfg(feature = "time")]
222		log_debug!(
223			self.logger,
224			"Finished transaction sync at tip {} in {}ms: {} confirmed, {} unconfirmed.",
225			tip_header.block_hash(),
226			start_time.elapsed().as_millis(),
227			num_confirmed,
228			num_unconfirmed
229		);
230		#[cfg(not(feature = "time"))]
231		log_debug!(
232			self.logger,
233			"Finished transaction sync at tip {}: {} confirmed, {} unconfirmed.",
234			tip_header.block_hash(),
235			num_confirmed,
236			num_unconfirmed
237		);
238		Ok(())
239	}
240
241	fn check_update_tip(
242		&self, cur_tip_header: &mut Header, cur_tip_height: &mut u32,
243	) -> Result<bool, InternalError> {
244		let check_notification = self.client.block_headers_subscribe()?;
245		let check_tip_hash = check_notification.header.block_hash();
246
247		// Restart if either the tip changed or we got some divergent tip
248		// change notification since we started. In the latter case we
249		// make sure we clear the queue before continuing.
250		let mut restart_sync = check_tip_hash != cur_tip_header.block_hash();
251		while let Some(queued_notif) = self.client.block_headers_pop()? {
252			if queued_notif.header.block_hash() != check_tip_hash {
253				restart_sync = true
254			}
255		}
256
257		if restart_sync {
258			*cur_tip_header = check_notification.header;
259			*cur_tip_height = check_notification.height as u32;
260			Ok(true)
261		} else {
262			Ok(false)
263		}
264	}
265
266	fn get_confirmed_transactions(
267		&self, sync_state: &SyncState,
268	) -> Result<Vec<ConfirmedTx>, InternalError> {
269		// First, check the confirmation status of registered transactions as well as the
270		// status of dependent transactions of registered outputs.
271		let mut confirmed_txs: Vec<ConfirmedTx> = Vec::new();
272		let mut watched_script_pubkeys = Vec::with_capacity(
273			sync_state.watched_transactions.len() + sync_state.watched_outputs.len(),
274		);
275		let mut watched_txs = Vec::with_capacity(sync_state.watched_transactions.len());
276
277		for txid in &sync_state.watched_transactions {
278			match self.client.transaction_get(&txid) {
279				Ok(tx) => {
280					if tx.compute_txid() != *txid {
281						log_error!(self.logger, "Retrieved transaction for txid {} doesn't match expectations. This should not happen. Please verify server integrity.", txid);
282						return Err(InternalError::Failed);
283					}
284
285					// Skip before using an arbitrary returned output to look up the
286					// transaction's script history.
287					if is_potentially_unsafe_merkle_leaf(&tx) {
288						log_error!(self.logger, "Skipping transaction {} due to retrieving potentially invalid tx data.", txid);
289						continue;
290					}
291
292					watched_txs.push((txid, tx.clone()));
293					if let Some(tx_out) = tx.output.first() {
294						// We watch an arbitrary output of the transaction of interest in order to
295						// retrieve the associated script history, before narrowing down our search
296						// through `filter`ing by `txid` below.
297						watched_script_pubkeys.push(tx_out.script_pubkey.clone());
298					} else {
299						debug_assert!(false, "Failed due to retrieving invalid tx data.");
300						log_error!(self.logger, "Failed due to retrieving invalid tx data.");
301						return Err(InternalError::Failed);
302					}
303				},
304				Err(electrum_client::Error::Protocol(_)) => {
305					// We couldn't find the tx, do nothing.
306				},
307				Err(e) => {
308					log_error!(self.logger, "Failed to look up transaction {}: {}.", txid, e);
309					return Err(InternalError::Failed);
310				},
311			}
312		}
313
314		let num_tx_lookups = watched_script_pubkeys.len();
315		debug_assert_eq!(num_tx_lookups, watched_txs.len());
316
317		for output in sync_state.watched_outputs.values() {
318			watched_script_pubkeys.push(output.script_pubkey.clone());
319		}
320
321		let num_output_spend_lookups = watched_script_pubkeys.len() - num_tx_lookups;
322		debug_assert_eq!(num_output_spend_lookups, sync_state.watched_outputs.len());
323
324		match self.client.batch_script_get_history(watched_script_pubkeys.iter().map(|s| s.deref()))
325		{
326			Ok(results) => {
327				let (tx_results, output_results) = results.split_at(num_tx_lookups);
328				debug_assert_eq!(num_output_spend_lookups, output_results.len());
329
330				for (i, script_history) in tx_results.iter().enumerate() {
331					let (txid, tx) = &watched_txs[i];
332					if confirmed_txs.iter().any(|ctx| ctx.txid == **txid) {
333						continue;
334					}
335					let mut filtered_history =
336						script_history.iter().filter(|h| h.tx_hash == **txid);
337					if let Some(history) = filtered_history.next() {
338						if history.height <= 0 {
339							// Skip if it's a an unconfirmed entry.
340							continue;
341						}
342						let prob_conf_height = history.height as u32;
343						if let Some(confirmed_tx) = self.get_confirmed_tx(tx, prob_conf_height)? {
344							confirmed_txs.push(confirmed_tx);
345						}
346					}
347					if filtered_history.next().is_some() {
348						log_error!(
349							self.logger,
350							"Failed due to server returning multiple history entries for Tx {}.",
351							txid
352						);
353						return Err(InternalError::Failed);
354					}
355				}
356
357				for (watched_output, script_history) in
358					sync_state.watched_outputs.values().zip(output_results)
359				{
360					for possible_output_spend in script_history {
361						if possible_output_spend.height <= 0 {
362							// Skip if it's a an unconfirmed entry.
363							continue;
364						}
365
366						let txid = possible_output_spend.tx_hash;
367						if confirmed_txs.iter().any(|ctx| ctx.txid == txid) {
368							continue;
369						}
370
371						match self.client.transaction_get(&txid) {
372							Ok(tx) => {
373								if tx.compute_txid() != txid {
374									log_error!(self.logger, "Retrieved transaction for txid {} doesn't match expectations. This should not happen. Please verify server integrity.", txid);
375									return Err(InternalError::Failed);
376								}
377
378								let mut is_spend = false;
379								for txin in &tx.input {
380									let watched_outpoint =
381										watched_output.outpoint.into_bitcoin_outpoint();
382									if txin.previous_output == watched_outpoint {
383										is_spend = true;
384										break;
385									}
386								}
387
388								if !is_spend {
389									continue;
390								}
391
392								let prob_conf_height = possible_output_spend.height as u32;
393								if let Some(confirmed_tx) =
394									self.get_confirmed_tx(&tx, prob_conf_height)?
395								{
396									confirmed_txs.push(confirmed_tx);
397								}
398							},
399							Err(e) => {
400								log_trace!(
401									self.logger,
402									"Inconsistency: Tx {} was unconfirmed during syncing: {}",
403									txid,
404									e
405								);
406								return Err(InternalError::Inconsistency);
407							},
408						}
409					}
410				}
411			},
412			Err(e) => {
413				log_error!(self.logger, "Failed to look up script histories: {}.", e);
414				return Err(InternalError::Failed);
415			},
416		}
417
418		// Sort all confirmed transactions first by block height, then by in-block
419		// position, and finally feed them to the interface in order.
420		confirmed_txs.sort_unstable_by(|tx1, tx2| {
421			tx1.block_height.cmp(&tx2.block_height).then_with(|| tx1.pos.cmp(&tx2.pos))
422		});
423
424		Ok(confirmed_txs)
425	}
426
427	fn get_unconfirmed_transactions<C: Deref>(
428		&self, confirmables: &Vec<C>,
429	) -> Result<Vec<Txid>, InternalError>
430	where
431		C::Target: Confirm,
432	{
433		// Query the interface for relevant txids and check whether the relevant blocks are still
434		// in the best chain, mark them unconfirmed otherwise
435		let relevant_txids = confirmables
436			.iter()
437			.flat_map(|c| c.get_relevant_txids())
438			.collect::<HashSet<(Txid, u32, Option<BlockHash>)>>();
439
440		let mut unconfirmed_txs = Vec::new();
441
442		for (txid, conf_height, block_hash_opt) in relevant_txids {
443			if let Some(block_hash) = block_hash_opt {
444				let block_header = self.client.block_header(conf_height as usize)?;
445				if block_header.block_hash() == block_hash {
446					// Skip if the tx is still confirmed in the block in question.
447					continue;
448				}
449
450				unconfirmed_txs.push(txid);
451			} else {
452				log_error!(self.logger,
453					"Untracked confirmation of funding transaction. Please ensure none of your channels had been created with LDK prior to version 0.0.113!");
454				panic!("Untracked confirmation of funding transaction. Please ensure none of your channels had been created with LDK prior to version 0.0.113!");
455			}
456		}
457		Ok(unconfirmed_txs)
458	}
459
460	fn get_confirmed_tx(
461		&self, tx: &Transaction, prob_conf_height: u32,
462	) -> Result<Option<ConfirmedTx>, InternalError> {
463		let txid = tx.compute_txid();
464		// Bitcoin Core's Merkle tree implementation has no way to discern between internal and
465		// leaf node entries. As a consequence it is susceptible to an attacker injecting
466		// additional transactions by crafting 64-byte transactions matching an inner Merkle
467		// node's hash (see https://web.archive.org/web/20240329003521/https://bitslog.com/2018/06/09/leaf-node-weakness-in-bitcoin-merkle-tree-design/).
468		if is_potentially_unsafe_merkle_leaf(tx) {
469			log_error!(
470				self.logger,
471				"Skipping transaction {} due to retrieving potentially invalid tx data.",
472				txid
473			);
474			return Ok(None);
475		}
476
477		match self.client.transaction_get_merkle(&txid, prob_conf_height as usize) {
478			Ok(merkle_res) => {
479				debug_assert_eq!(prob_conf_height, merkle_res.block_height as u32);
480				match self.client.block_header(prob_conf_height as usize) {
481					Ok(block_header) => {
482						let pos = merkle_res.pos;
483						if !validate_merkle_proof(&txid, &block_header.merkle_root, &merkle_res) {
484							log_trace!(
485								self.logger,
486								"Inconsistency: Block {} was unconfirmed during syncing.",
487								block_header.block_hash()
488							);
489							return Err(InternalError::Inconsistency);
490						}
491						let confirmed_tx = ConfirmedTx {
492							tx: tx.clone(),
493							txid,
494							block_header,
495							block_height: prob_conf_height,
496							pos,
497						};
498						Ok(Some(confirmed_tx))
499					},
500					Err(e) => {
501						log_error!(
502							self.logger,
503							"Failed to retrieve block header for height {}: {}.",
504							prob_conf_height,
505							e
506						);
507						Err(InternalError::Failed)
508					},
509				}
510			},
511			Err(e) => {
512				log_trace!(
513					self.logger,
514					"Inconsistency: Tx {} was unconfirmed during syncing: {}",
515					txid,
516					e
517				);
518				Err(InternalError::Inconsistency)
519			},
520		}
521	}
522
523	/// Returns a reference to the underlying Electrum client.
524	///
525	/// This is not exported to bindings users as the underlying client from BDK is not exported.
526	pub fn client(&self) -> Arc<ElectrumClient> {
527		Arc::clone(&self.client)
528	}
529}
530
531impl<L: Logger> Filter for ElectrumSyncClient<L> {
532	fn register_tx(&self, txid: &Txid, _script_pubkey: &Script) {
533		let mut locked_queue = self.queue.lock().unwrap();
534		locked_queue.transactions.insert(*txid);
535	}
536
537	fn register_output(&self, output: WatchedOutput) {
538		let mut locked_queue = self.queue.lock().unwrap();
539		locked_queue.outputs.insert(output.outpoint.into_bitcoin_outpoint(), output);
540	}
541}
542
543#[cfg(test)]
544mod tests {
545	#[test]
546	fn transaction_get_responses_are_verified_at_call_sites() {
547		let src = include_str!("electrum.rs");
548		let watched_transaction_check = concat!("if tx.compute_", "txid() != *txid");
549		let watched_output_spend_check = concat!("if tx.compute_", "txid() != txid");
550
551		assert!(
552			src.contains(watched_transaction_check),
553			"watched transaction_get responses must be verified against the requested txid"
554		);
555		assert!(
556			src.contains(watched_output_spend_check),
557			"watched-output spend transaction_get responses must be verified against the \
558			 requested txid"
559		);
560	}
561}