Skip to main content

lightning_block_sync/
poll.rs

1//! Adapters that make one or more [`BlockSource`]s simpler to poll for new chain tip transitions.
2
3use crate::{BlockData, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceResult};
4
5use bitcoin::hash_types::BlockHash;
6use bitcoin::network::Network;
7use lightning::chain::BlockLocator;
8
9use std::future::Future;
10use std::ops::Deref;
11
12/// The `Poll` trait defines behavior for polling block sources for a chain tip and retrieving
13/// related chain data. It serves as an adapter for `BlockSource`.
14///
15/// [`ChainPoller`] adapts a single `BlockSource`, while any other implementations of `Poll` are
16/// required to be built in terms of it to ensure chain data validity.
17///
18/// [`ChainPoller`]: ../struct.ChainPoller.html
19pub trait Poll {
20	/// Returns a chain tip in terms of its relationship to the provided chain tip.
21	fn poll_chain_tip<'a>(
22		&'a self, best_known_chain_tip: ValidatedBlockHeader,
23	) -> impl Future<Output = BlockSourceResult<ChainTip>> + Send + 'a;
24
25	/// Returns the header that preceded the given header in the chain.
26	fn look_up_previous_header<'a>(
27		&'a self, header: &'a ValidatedBlockHeader,
28	) -> impl Future<Output = BlockSourceResult<ValidatedBlockHeader>> + Send + 'a;
29
30	/// Returns the block associated with the given header.
31	fn fetch_block<'a>(
32		&'a self, header: &'a ValidatedBlockHeader,
33	) -> impl Future<Output = BlockSourceResult<ValidatedBlock>> + Send + 'a;
34
35	/// Returns the header for a given hash and optional height hint.
36	fn get_header<'a>(
37		&'a self, block_hash: &'a BlockHash, height_hint: Option<u32>,
38	) -> impl Future<Output = BlockSourceResult<ValidatedBlockHeader>> + Send + 'a;
39}
40
41/// A chain tip relative to another chain tip in terms of block hash and chainwork.
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub enum ChainTip {
44	/// A chain tip with the same hash as another chain's tip.
45	Common,
46
47	/// A chain tip with more chainwork than another chain's tip.
48	Better(ValidatedBlockHeader),
49
50	/// A chain tip with less or equal chainwork than another chain's tip. In either case, the
51	/// hashes of each tip will be different.
52	Worse(ValidatedBlockHeader),
53}
54
55/// The `Validate` trait defines behavior for validating chain data.
56///
57/// This trait is sealed and not meant to be implemented outside of this crate.
58pub trait Validate: sealed::Validate {
59	/// The validated data wrapper which can be dereferenced to obtain the validated data.
60	type T: std::ops::Deref<Target = Self>;
61
62	/// Validates the chain data against the given block hash and any criteria needed to ensure that
63	/// it is internally consistent.
64	fn validate(self, block_hash: BlockHash) -> BlockSourceResult<Self::T>;
65}
66
67impl Validate for BlockHeaderData {
68	type T = ValidatedBlockHeader;
69
70	fn validate(self, block_hash: BlockHash) -> BlockSourceResult<Self::T> {
71		let pow_valid_block_hash =
72			self.header.validate_pow(self.header.target()).map_err(BlockSourceError::persistent)?;
73
74		if pow_valid_block_hash != block_hash {
75			return Err(BlockSourceError::persistent("invalid block hash"));
76		}
77
78		Ok(ValidatedBlockHeader { block_hash, inner: self })
79	}
80}
81
82impl Validate for BlockData {
83	type T = ValidatedBlock;
84
85	fn validate(self, block_hash: BlockHash) -> BlockSourceResult<Self::T> {
86		let header = match &self {
87			BlockData::FullBlock(block) => &block.header,
88			BlockData::HeaderOnly(header) => header,
89		};
90
91		let pow_valid_block_hash =
92			header.validate_pow(header.target()).map_err(BlockSourceError::persistent)?;
93
94		if pow_valid_block_hash != block_hash {
95			return Err(BlockSourceError::persistent("invalid block hash"));
96		}
97
98		if let BlockData::FullBlock(block) = &self {
99			if !block.check_merkle_root() {
100				return Err(BlockSourceError::persistent("invalid merkle root"));
101			}
102
103			if !block.check_witness_commitment() {
104				return Err(BlockSourceError::persistent("invalid witness commitment"));
105			}
106		}
107
108		Ok(ValidatedBlock { block_hash, inner: self })
109	}
110}
111
112/// A block header with validated proof of work and corresponding block hash.
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct ValidatedBlockHeader {
115	pub(crate) block_hash: BlockHash,
116	inner: BlockHeaderData,
117}
118
119impl std::ops::Deref for ValidatedBlockHeader {
120	type Target = BlockHeaderData;
121
122	fn deref(&self) -> &Self::Target {
123		&self.inner
124	}
125}
126
127impl ValidatedBlockHeader {
128	/// Checks that the header correctly builds on previous_header: the claimed work differential
129	/// matches the actual PoW and the difficulty transition is possible, i.e., within 4x.
130	fn check_builds_on(
131		&self, previous_header: &ValidatedBlockHeader, network: Network,
132	) -> BlockSourceResult<()> {
133		if self.header.prev_blockhash != previous_header.block_hash {
134			return Err(BlockSourceError::persistent("invalid previous block hash"));
135		}
136
137		if self.height != previous_header.height + 1 {
138			return Err(BlockSourceError::persistent("invalid block height"));
139		}
140
141		let work = self.header.work();
142		if self.chainwork != previous_header.chainwork + work {
143			return Err(BlockSourceError::persistent("invalid chainwork"));
144		}
145
146		if let Network::Bitcoin = network {
147			if self.height % 2016 == 0 {
148				let target = self.header.target();
149				let previous_target = previous_header.header.target();
150				let min_target = previous_target.min_transition_threshold();
151				let max_target = previous_target.max_transition_threshold_unchecked();
152				if target > max_target || target < min_target {
153					return Err(BlockSourceError::persistent("invalid difficulty transition"));
154				}
155			} else if self.header.bits != previous_header.header.bits {
156				return Err(BlockSourceError::persistent("invalid difficulty"));
157			}
158		}
159
160		Ok(())
161	}
162
163	/// Returns the [`BlockLocator`] corresponding to this validated block header, which can be passed
164	/// into [`ChannelManager::new`] as part of its [`ChainParameters`]. Useful for ensuring that
165	/// the [`SpvClient`] and [`ChannelManager`] are initialized to the same block during a fresh
166	/// start.
167	///
168	/// [`SpvClient`]: crate::SpvClient
169	/// [`ChainParameters`]: lightning::ln::channelmanager::ChainParameters
170	/// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager
171	/// [`ChannelManager::new`]: lightning::ln::channelmanager::ChannelManager::new
172	pub fn to_block_locator(&self) -> BlockLocator {
173		BlockLocator::new(self.block_hash, self.inner.height)
174	}
175}
176
177/// A block with validated data against its transaction list and corresponding block hash.
178pub struct ValidatedBlock {
179	pub(crate) block_hash: BlockHash,
180	inner: BlockData,
181}
182
183impl std::ops::Deref for ValidatedBlock {
184	type Target = BlockData;
185
186	fn deref(&self) -> &Self::Target {
187		&self.inner
188	}
189}
190
191mod sealed {
192	/// Used to prevent implementing [`super::Validate`] outside the crate but still allow its use.
193	pub trait Validate {}
194
195	impl Validate for crate::BlockHeaderData {}
196	impl Validate for crate::BlockData {}
197}
198
199/// The canonical `Poll` implementation used for a single `BlockSource`.
200///
201/// Other `Poll` implementations should be built using `ChainPoller` as it provides the simplest way
202/// of validating chain data and checking consistency.
203pub struct ChainPoller<B: Deref<Target = T> + Sized + Send + Sync, T: BlockSource + ?Sized> {
204	block_source: B,
205	network: Network,
206}
207
208impl<B: Deref<Target = T> + Sized + Send + Sync, T: BlockSource + ?Sized> ChainPoller<B, T> {
209	/// Creates a new poller for the given block source.
210	///
211	/// If the `network` parameter is mainnet, then the difficulty between blocks is checked for
212	/// validity.
213	pub fn new(block_source: B, network: Network) -> Self {
214		Self { block_source, network }
215	}
216}
217
218impl<B: Deref<Target = T> + Sized + Send + Sync, T: BlockSource + ?Sized> Poll
219	for ChainPoller<B, T>
220{
221	fn poll_chain_tip<'a>(
222		&'a self, best_known_chain_tip: ValidatedBlockHeader,
223	) -> impl Future<Output = BlockSourceResult<ChainTip>> + Send + 'a {
224		async move {
225			let (block_hash, height) = self.block_source.get_best_block().await?;
226			if block_hash == best_known_chain_tip.header.block_hash() {
227				return Ok(ChainTip::Common);
228			}
229
230			let chain_tip =
231				self.block_source.get_header(&block_hash, height).await?.validate(block_hash)?;
232			if chain_tip.chainwork > best_known_chain_tip.chainwork {
233				Ok(ChainTip::Better(chain_tip))
234			} else {
235				Ok(ChainTip::Worse(chain_tip))
236			}
237		}
238	}
239
240	fn look_up_previous_header<'a>(
241		&'a self, header: &'a ValidatedBlockHeader,
242	) -> impl Future<Output = BlockSourceResult<ValidatedBlockHeader>> + Send + 'a {
243		async move {
244			if header.height == 0 {
245				return Err(BlockSourceError::persistent("genesis block reached"));
246			}
247
248			let previous_hash = &header.header.prev_blockhash;
249			let height = header.height - 1;
250			let previous_header = self
251				.block_source
252				.get_header(previous_hash, Some(height))
253				.await?
254				.validate(*previous_hash)?;
255			header.check_builds_on(&previous_header, self.network)?;
256
257			Ok(previous_header)
258		}
259	}
260
261	fn fetch_block<'a>(
262		&'a self, header: &'a ValidatedBlockHeader,
263	) -> impl Future<Output = BlockSourceResult<ValidatedBlock>> + Send + 'a {
264		async move { self.block_source.get_block(&header.block_hash).await?.validate(header.block_hash) }
265	}
266
267	fn get_header<'a>(
268		&'a self, block_hash: &'a BlockHash, height_hint: Option<u32>,
269	) -> impl Future<Output = BlockSourceResult<ValidatedBlockHeader>> + Send + 'a {
270		Box::pin(async move {
271			self.block_source.get_header(block_hash, height_hint).await?.validate(*block_hash)
272		})
273	}
274}
275
276#[cfg(test)]
277mod tests {
278	use super::*;
279	use crate::test_utils::Blockchain;
280	use crate::*;
281
282	#[tokio::test]
283	async fn poll_empty_chain() {
284		let mut chain = Blockchain::default().with_height(0);
285		let best_known_chain_tip = chain.tip();
286		chain.disconnect_tip();
287
288		let poller = ChainPoller::new(&chain, Network::Bitcoin);
289		match poller.poll_chain_tip(best_known_chain_tip).await {
290			Err(e) => {
291				assert_eq!(e.kind(), BlockSourceErrorKind::Transient);
292				assert_eq!(e.into_inner().as_ref().to_string(), "empty chain");
293			},
294			Ok(_) => panic!("Expected error"),
295		}
296	}
297
298	#[tokio::test]
299	async fn poll_chain_without_headers() {
300		let chain = Blockchain::default().with_height(1).without_headers();
301		let best_known_chain_tip = chain.at_height(0);
302
303		let poller = ChainPoller::new(&chain, Network::Bitcoin);
304		match poller.poll_chain_tip(best_known_chain_tip).await {
305			Err(e) => {
306				assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
307				assert_eq!(e.into_inner().as_ref().to_string(), "header not found");
308			},
309			Ok(_) => panic!("Expected error"),
310		}
311	}
312
313	#[tokio::test]
314	async fn poll_chain_with_invalid_pow() {
315		let mut chain = Blockchain::default().with_height(1);
316		let best_known_chain_tip = chain.at_height(0);
317
318		// Invalidate the tip by changing its target.
319		chain.blocks.last_mut().unwrap().header.bits =
320			bitcoin::Target::from_be_bytes([0x01; 32]).to_compact_lossy();
321
322		let poller = ChainPoller::new(&chain, Network::Bitcoin);
323		match poller.poll_chain_tip(best_known_chain_tip).await {
324			Err(e) => {
325				assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
326				assert_eq!(
327					e.into_inner().as_ref().to_string(),
328					"block target correct but not attained"
329				);
330			},
331			Ok(_) => panic!("Expected error"),
332		}
333	}
334
335	#[tokio::test]
336	async fn poll_chain_with_malformed_headers() {
337		let chain = Blockchain::default().with_height(1).malformed_headers();
338		let best_known_chain_tip = chain.at_height(0);
339
340		let poller = ChainPoller::new(&chain, Network::Bitcoin);
341		match poller.poll_chain_tip(best_known_chain_tip).await {
342			Err(e) => {
343				assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
344				assert_eq!(e.into_inner().as_ref().to_string(), "invalid block hash");
345			},
346			Ok(_) => panic!("Expected error"),
347		}
348	}
349
350	#[tokio::test]
351	async fn poll_chain_with_common_tip() {
352		let chain = Blockchain::default().with_height(0);
353		let best_known_chain_tip = chain.tip();
354
355		let poller = ChainPoller::new(&chain, Network::Bitcoin);
356		match poller.poll_chain_tip(best_known_chain_tip).await {
357			Err(e) => panic!("Unexpected error: {:?}", e),
358			Ok(tip) => assert_eq!(tip, ChainTip::Common),
359		}
360	}
361
362	#[tokio::test]
363	async fn poll_chain_with_uncommon_tip_but_equal_chainwork() {
364		let mut chain = Blockchain::default().with_height(1);
365		let best_known_chain_tip = chain.tip();
366
367		// Change the nonce to get a different block hash with the same chainwork.
368		chain.blocks.last_mut().unwrap().header.nonce += 1;
369		let worse_chain_tip = chain.tip();
370		assert_eq!(best_known_chain_tip.chainwork, worse_chain_tip.chainwork);
371
372		let poller = ChainPoller::new(&chain, Network::Bitcoin);
373		match poller.poll_chain_tip(best_known_chain_tip).await {
374			Err(e) => panic!("Unexpected error: {:?}", e),
375			Ok(tip) => assert_eq!(tip, ChainTip::Worse(worse_chain_tip)),
376		}
377	}
378
379	#[tokio::test]
380	async fn poll_chain_with_worse_tip() {
381		let mut chain = Blockchain::default().with_height(1);
382		let best_known_chain_tip = chain.tip();
383
384		chain.disconnect_tip();
385		let worse_chain_tip = chain.tip();
386
387		let poller = ChainPoller::new(&chain, Network::Bitcoin);
388		match poller.poll_chain_tip(best_known_chain_tip).await {
389			Err(e) => panic!("Unexpected error: {:?}", e),
390			Ok(tip) => assert_eq!(tip, ChainTip::Worse(worse_chain_tip)),
391		}
392	}
393
394	#[tokio::test]
395	async fn poll_chain_with_better_tip() {
396		let chain = Blockchain::default().with_height(1);
397		let best_known_chain_tip = chain.at_height(0);
398
399		let better_chain_tip = chain.tip();
400
401		let poller = ChainPoller::new(&chain, Network::Bitcoin);
402		match poller.poll_chain_tip(best_known_chain_tip).await {
403			Err(e) => panic!("Unexpected error: {:?}", e),
404			Ok(tip) => assert_eq!(tip, ChainTip::Better(better_chain_tip)),
405		}
406	}
407}