Skip to content

Commit 05ec061

Browse files
committed
Add ChainPoller implementation of Poll trait
ChainPoller defines a strategy for polling a single BlockSource. It handles validating chain data returned from the BlockSource. Thus, other implementations of Poll must be defined in terms of ChainPoller.
1 parent 7d6fce7 commit 05ec061

File tree

3 files changed

+340
-2
lines changed

3 files changed

+340
-2
lines changed

lightning-block-sync/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ pub mod rpc;
2525
#[cfg(any(feature = "rest-client", feature = "rpc-client"))]
2626
mod convert;
2727

28+
#[cfg(test)]
29+
mod test_utils;
30+
2831
#[cfg(any(feature = "rest-client", feature = "rpc-client"))]
2932
mod utils;
3033

@@ -67,13 +70,14 @@ type AsyncBlockSourceResult<'a, T> = Pin<Box<dyn Future<Output = BlockSourceResu
6770
///
6871
/// Transient errors may be resolved when re-polling, but no attempt will be made to re-poll on
6972
/// persistent errors.
73+
#[derive(Debug)]
7074
pub struct BlockSourceError {
7175
kind: BlockSourceErrorKind,
7276
error: Box<dyn std::error::Error + Send + Sync>,
7377
}
7478

7579
/// The kind of `BlockSourceError`, either persistent or transient.
76-
#[derive(Clone, Copy)]
80+
#[derive(Clone, Copy, Debug, PartialEq)]
7781
pub enum BlockSourceErrorKind {
7882
/// Indicates an error that won't resolve when retrying a request (e.g., invalid data).
7983
Persistent,

lightning-block-sync/src/poll.rs

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
use crate::{AsyncBlockSourceResult, BlockHeaderData, BlockSourceError, BlockSourceResult};
1+
use crate::{AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceResult};
22

33
use bitcoin::blockdata::block::Block;
44
use bitcoin::hash_types::BlockHash;
55
use bitcoin::network::constants::Network;
66

7+
use std::ops::DerefMut;
8+
79
/// The `Poll` trait defines behavior for polling block sources for a chain tip and retrieving
810
/// related chain data. It serves as an adapter for `BlockSource`.
11+
///
12+
/// [`ChainPoller`] adapts a single `BlockSource`, while any other implementations of `Poll` are
13+
/// required to be built in terms of it to ensure chain data validity.
14+
///
15+
/// [`ChainPoller`]: ../struct.ChainPoller.html
916
pub trait Poll {
1017
/// Returns a chain tip in terms of its relationship to the provided chain tip.
1118
fn poll_chain_tip<'a>(&'a mut self, best_known_chain_tip: ValidatedBlockHeader) ->
@@ -146,3 +153,204 @@ impl std::ops::Deref for ValidatedBlock {
146153
&self.inner
147154
}
148155
}
156+
157+
/// The canonical `Poll` implementation used for a single `BlockSource`.
158+
///
159+
/// Other `Poll` implementations must be built using `ChainPoller` as it provides the only means of
160+
/// validating chain data.
161+
pub struct ChainPoller<B: DerefMut<Target=T> + Sized + Sync + Send, T: BlockSource> {
162+
block_source: B,
163+
network: Network,
164+
}
165+
166+
impl<B: DerefMut<Target=T> + Sized + Sync + Send, T: BlockSource> ChainPoller<B, T> {
167+
/// Creates a new poller for the given block source.
168+
///
169+
/// If the `network` parameter is mainnet, then the difficulty between blocks is checked for
170+
/// validity.
171+
pub fn new(block_source: B, network: Network) -> Self {
172+
Self { block_source, network }
173+
}
174+
}
175+
176+
impl<B: DerefMut<Target=T> + Sized + Sync + Send, T: BlockSource> Poll for ChainPoller<B, T> {
177+
fn poll_chain_tip<'a>(&'a mut self, best_known_chain_tip: ValidatedBlockHeader) ->
178+
AsyncBlockSourceResult<'a, ChainTip>
179+
{
180+
Box::pin(async move {
181+
let (block_hash, height) = self.block_source.get_best_block().await?;
182+
if block_hash == best_known_chain_tip.header.block_hash() {
183+
return Ok(ChainTip::Common);
184+
}
185+
186+
let chain_tip = self.block_source
187+
.get_header(&block_hash, height).await?
188+
.validate(block_hash)?;
189+
if chain_tip.chainwork > best_known_chain_tip.chainwork {
190+
Ok(ChainTip::Better(chain_tip))
191+
} else {
192+
Ok(ChainTip::Worse(chain_tip))
193+
}
194+
})
195+
}
196+
197+
fn look_up_previous_header<'a>(&'a mut self, header: &'a ValidatedBlockHeader) ->
198+
AsyncBlockSourceResult<'a, ValidatedBlockHeader>
199+
{
200+
Box::pin(async move {
201+
if header.height == 0 {
202+
return Err(BlockSourceError::persistent("genesis block reached"));
203+
}
204+
205+
let previous_hash = &header.header.prev_blockhash;
206+
let height = header.height - 1;
207+
let previous_header = self.block_source
208+
.get_header(previous_hash, Some(height)).await?
209+
.validate(*previous_hash)?;
210+
header.check_builds_on(&previous_header, self.network)?;
211+
212+
Ok(previous_header)
213+
})
214+
}
215+
216+
fn fetch_block<'a>(&'a mut self, header: &'a ValidatedBlockHeader) ->
217+
AsyncBlockSourceResult<'a, ValidatedBlock>
218+
{
219+
Box::pin(async move {
220+
self.block_source
221+
.get_block(&header.block_hash).await?
222+
.validate(header.block_hash)
223+
})
224+
}
225+
}
226+
227+
#[cfg(test)]
228+
mod tests {
229+
use crate::*;
230+
use crate::test_utils::Blockchain;
231+
use super::*;
232+
use bitcoin::util::uint::Uint256;
233+
234+
#[tokio::test]
235+
async fn poll_empty_chain() {
236+
let mut chain = Blockchain::default().with_height(0);
237+
let best_known_chain_tip = chain.tip();
238+
chain.disconnect_tip();
239+
240+
let mut poller = ChainPoller::new(&mut chain, Network::Bitcoin);
241+
match poller.poll_chain_tip(best_known_chain_tip).await {
242+
Err(e) => {
243+
assert_eq!(e.kind(), BlockSourceErrorKind::Transient);
244+
assert_eq!(e.into_inner().as_ref().to_string(), "empty chain");
245+
},
246+
Ok(_) => panic!("Expected error"),
247+
}
248+
}
249+
250+
#[tokio::test]
251+
async fn poll_chain_without_headers() {
252+
let mut chain = Blockchain::default().with_height(1).without_headers();
253+
let best_known_chain_tip = chain.at_height(0);
254+
255+
let mut poller = ChainPoller::new(&mut chain, Network::Bitcoin);
256+
match poller.poll_chain_tip(best_known_chain_tip).await {
257+
Err(e) => {
258+
assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
259+
assert_eq!(e.into_inner().as_ref().to_string(), "header not found");
260+
},
261+
Ok(_) => panic!("Expected error"),
262+
}
263+
}
264+
265+
#[tokio::test]
266+
async fn poll_chain_with_invalid_pow() {
267+
let mut chain = Blockchain::default().with_height(1);
268+
let best_known_chain_tip = chain.at_height(0);
269+
270+
// Invalidate the tip by changing its target.
271+
chain.blocks.last_mut().unwrap().header.bits =
272+
BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0; 32]));
273+
274+
let mut poller = ChainPoller::new(&mut chain, Network::Bitcoin);
275+
match poller.poll_chain_tip(best_known_chain_tip).await {
276+
Err(e) => {
277+
assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
278+
assert_eq!(e.into_inner().as_ref().to_string(), "block target correct but not attained");
279+
},
280+
Ok(_) => panic!("Expected error"),
281+
}
282+
}
283+
284+
#[tokio::test]
285+
async fn poll_chain_with_malformed_headers() {
286+
let mut chain = Blockchain::default().with_height(1).malformed_headers();
287+
let best_known_chain_tip = chain.at_height(0);
288+
289+
let mut poller = ChainPoller::new(&mut chain, Network::Bitcoin);
290+
match poller.poll_chain_tip(best_known_chain_tip).await {
291+
Err(e) => {
292+
assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
293+
assert_eq!(e.into_inner().as_ref().to_string(), "invalid block hash");
294+
},
295+
Ok(_) => panic!("Expected error"),
296+
}
297+
}
298+
299+
#[tokio::test]
300+
async fn poll_chain_with_common_tip() {
301+
let mut chain = Blockchain::default().with_height(0);
302+
let best_known_chain_tip = chain.tip();
303+
304+
let mut poller = ChainPoller::new(&mut chain, Network::Bitcoin);
305+
match poller.poll_chain_tip(best_known_chain_tip).await {
306+
Err(e) => panic!("Unexpected error: {:?}", e),
307+
Ok(tip) => assert_eq!(tip, ChainTip::Common),
308+
}
309+
}
310+
311+
#[tokio::test]
312+
async fn poll_chain_with_uncommon_tip_but_equal_chainwork() {
313+
let mut chain = Blockchain::default().with_height(1);
314+
let best_known_chain_tip = chain.tip();
315+
316+
// Change the nonce to get a different block hash with the same chainwork.
317+
chain.blocks.last_mut().unwrap().header.nonce += 1;
318+
let worse_chain_tip = chain.tip();
319+
assert_eq!(best_known_chain_tip.chainwork, worse_chain_tip.chainwork);
320+
321+
let mut poller = ChainPoller::new(&mut chain, Network::Bitcoin);
322+
match poller.poll_chain_tip(best_known_chain_tip).await {
323+
Err(e) => panic!("Unexpected error: {:?}", e),
324+
Ok(tip) => assert_eq!(tip, ChainTip::Worse(worse_chain_tip)),
325+
}
326+
}
327+
328+
#[tokio::test]
329+
async fn poll_chain_with_worse_tip() {
330+
let mut chain = Blockchain::default().with_height(1);
331+
let best_known_chain_tip = chain.tip();
332+
333+
chain.disconnect_tip();
334+
let worse_chain_tip = chain.tip();
335+
336+
let mut poller = ChainPoller::new(&mut chain, Network::Bitcoin);
337+
match poller.poll_chain_tip(best_known_chain_tip).await {
338+
Err(e) => panic!("Unexpected error: {:?}", e),
339+
Ok(tip) => assert_eq!(tip, ChainTip::Worse(worse_chain_tip)),
340+
}
341+
}
342+
343+
#[tokio::test]
344+
async fn poll_chain_with_better_tip() {
345+
let mut chain = Blockchain::default().with_height(1);
346+
let best_known_chain_tip = chain.at_height(0);
347+
348+
let better_chain_tip = chain.tip();
349+
350+
let mut poller = ChainPoller::new(&mut chain, Network::Bitcoin);
351+
match poller.poll_chain_tip(best_known_chain_tip).await {
352+
Err(e) => panic!("Unexpected error: {:?}", e),
353+
Ok(tip) => assert_eq!(tip, ChainTip::Better(better_chain_tip)),
354+
}
355+
}
356+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use crate::{AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError};
2+
use crate::poll::{Validate, ValidatedBlockHeader};
3+
4+
use bitcoin::blockdata::block::{Block, BlockHeader};
5+
use bitcoin::blockdata::constants::genesis_block;
6+
use bitcoin::hash_types::BlockHash;
7+
use bitcoin::network::constants::Network;
8+
use bitcoin::util::uint::Uint256;
9+
10+
#[derive(Default)]
11+
pub struct Blockchain {
12+
pub blocks: Vec<Block>,
13+
without_headers: bool,
14+
malformed_headers: bool,
15+
}
16+
17+
impl Blockchain {
18+
pub fn default() -> Self {
19+
Blockchain::with_network(Network::Bitcoin)
20+
}
21+
22+
pub fn with_network(network: Network) -> Self {
23+
let blocks = vec![genesis_block(network)];
24+
Self { blocks, ..Default::default() }
25+
}
26+
27+
pub fn with_height(mut self, height: usize) -> Self {
28+
self.blocks.reserve_exact(height);
29+
let bits = BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0xff; 32]));
30+
for i in 1..=height {
31+
let prev_block = &self.blocks[i - 1];
32+
let prev_blockhash = prev_block.block_hash();
33+
let time = prev_block.header.time + height as u32;
34+
self.blocks.push(Block {
35+
header: BlockHeader {
36+
version: 0,
37+
prev_blockhash,
38+
merkle_root: Default::default(),
39+
time,
40+
bits,
41+
nonce: 0,
42+
},
43+
txdata: vec![],
44+
});
45+
}
46+
self
47+
}
48+
49+
pub fn without_headers(self) -> Self {
50+
Self { without_headers: true, ..self }
51+
}
52+
53+
pub fn malformed_headers(self) -> Self {
54+
Self { malformed_headers: true, ..self }
55+
}
56+
57+
pub fn at_height(&self, height: usize) -> ValidatedBlockHeader {
58+
let block_header = self.at_height_unvalidated(height);
59+
let block_hash = self.blocks[height].block_hash();
60+
block_header.validate(block_hash).unwrap()
61+
}
62+
63+
fn at_height_unvalidated(&self, height: usize) -> BlockHeaderData {
64+
assert!(!self.blocks.is_empty());
65+
assert!(height < self.blocks.len());
66+
BlockHeaderData {
67+
chainwork: self.blocks[0].header.work() + Uint256::from_u64(height as u64).unwrap(),
68+
height: height as u32,
69+
header: self.blocks[height].header.clone(),
70+
}
71+
}
72+
73+
pub fn tip(&self) -> ValidatedBlockHeader {
74+
assert!(!self.blocks.is_empty());
75+
self.at_height(self.blocks.len() - 1)
76+
}
77+
78+
pub fn disconnect_tip(&mut self) -> Option<Block> {
79+
self.blocks.pop()
80+
}
81+
}
82+
83+
impl BlockSource for Blockchain {
84+
fn get_header<'a>(&'a mut self, header_hash: &'a BlockHash, _height_hint: Option<u32>) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
85+
Box::pin(async move {
86+
if self.without_headers {
87+
return Err(BlockSourceError::persistent("header not found"));
88+
}
89+
90+
for (height, block) in self.blocks.iter().enumerate() {
91+
if block.header.block_hash() == *header_hash {
92+
let mut header_data = self.at_height_unvalidated(height);
93+
if self.malformed_headers {
94+
header_data.header.time += 1;
95+
}
96+
97+
return Ok(header_data);
98+
}
99+
}
100+
Err(BlockSourceError::transient("header not found"))
101+
})
102+
}
103+
104+
fn get_block<'a>(&'a mut self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> {
105+
Box::pin(async move {
106+
for block in self.blocks.iter() {
107+
if block.header.block_hash() == *header_hash {
108+
return Ok(block.clone());
109+
}
110+
}
111+
Err(BlockSourceError::transient("block not found"))
112+
})
113+
}
114+
115+
fn get_best_block<'a>(&'a mut self) -> AsyncBlockSourceResult<'a, (BlockHash, Option<u32>)> {
116+
Box::pin(async move {
117+
match self.blocks.last() {
118+
None => Err(BlockSourceError::transient("empty chain")),
119+
Some(block) => {
120+
let height = (self.blocks.len() - 1) as u32;
121+
Ok((block.block_hash(), Some(height)))
122+
},
123+
}
124+
})
125+
}
126+
}

0 commit comments

Comments
 (0)