Skip to content

Commit 7f2ec42

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 2bf6e69 commit 7f2ec42

File tree

3 files changed

+336
-2
lines changed

3 files changed

+336
-2
lines changed

lightning-block-sync/src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#[cfg(any(feature = "rest-client", feature = "rpc-client"))]
1515
pub mod http;
1616

17+
pub mod poll;
18+
1719
#[cfg(feature = "rest-client")]
1820
pub mod rest;
1921

@@ -23,6 +25,9 @@ pub mod rpc;
2325
#[cfg(any(feature = "rest-client", feature = "rpc-client"))]
2426
mod convert;
2527

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

@@ -65,13 +70,14 @@ type AsyncBlockSourceResult<'a, T> = Pin<Box<dyn Future<Output = BlockSourceResu
6570
///
6671
/// Transient errors may be resolved when re-polling, but no attempt will be made to re-poll on
6772
/// persistent errors.
73+
#[derive(Debug)]
6874
pub struct BlockSourceError {
6975
kind: BlockSourceErrorKind,
7076
error: Box<dyn std::error::Error + Send + Sync>,
7177
}
7278

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

lightning-block-sync/src/poll.rs

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
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`.
911
pub trait Poll {
@@ -140,3 +142,203 @@ impl std::ops::Deref for ValidatedBlock {
140142
&self.inner
141143
}
142144
}
145+
146+
pub struct ChainPoller<'b, B: DerefMut<Target=dyn BlockSource + 'b> + Sized + Sync + Send> {
147+
block_source: B,
148+
network: Network,
149+
}
150+
151+
impl<'b, B: DerefMut<Target=dyn BlockSource + 'b> + Sized + Sync + Send> ChainPoller<'b, B> {
152+
pub fn new(block_source: B, network: Network) -> Self {
153+
Self { block_source, network }
154+
}
155+
}
156+
157+
impl<'b, B: DerefMut<Target=dyn BlockSource + 'b> + Sized + Sync + Send> Poll for ChainPoller<'b, B> {
158+
fn poll_chain_tip<'a>(&'a mut self, best_known_chain_tip: ValidatedBlockHeader) ->
159+
AsyncBlockSourceResult<'a, ChainTip>
160+
{
161+
Box::pin(async move {
162+
let (block_hash, height) = self.block_source.get_best_block().await?;
163+
if block_hash == best_known_chain_tip.header.block_hash() {
164+
return Ok(ChainTip::Common);
165+
}
166+
167+
let chain_tip = self.block_source
168+
.get_header(&block_hash, height).await?
169+
.validate(block_hash)?;
170+
if chain_tip.chainwork > best_known_chain_tip.chainwork {
171+
Ok(ChainTip::Better(chain_tip))
172+
} else {
173+
Ok(ChainTip::Worse(chain_tip))
174+
}
175+
})
176+
}
177+
178+
fn look_up_previous_header<'a>(&'a mut self, header: &'a ValidatedBlockHeader) ->
179+
AsyncBlockSourceResult<'a, ValidatedBlockHeader>
180+
{
181+
Box::pin(async move {
182+
if header.height == 0 {
183+
return Err(BlockSourceError::persistent("genesis block reached"));
184+
}
185+
186+
let previous_hash = &header.header.prev_blockhash;
187+
let height = header.height - 1;
188+
let previous_header = self.block_source
189+
.get_header(previous_hash, Some(height)).await?
190+
.validate(*previous_hash)?;
191+
header.check_builds_on(&previous_header, self.network)?;
192+
193+
Ok(previous_header)
194+
})
195+
}
196+
197+
fn fetch_block<'a>(&'a mut self, header: &'a ValidatedBlockHeader) ->
198+
AsyncBlockSourceResult<'a, ValidatedBlock>
199+
{
200+
Box::pin(async move {
201+
self.block_source
202+
.get_block(&header.block_hash).await?
203+
.validate(header.block_hash)
204+
})
205+
}
206+
}
207+
208+
#[cfg(test)]
209+
mod tests {
210+
use crate::*;
211+
use crate::test_utils::Blockchain;
212+
use super::*;
213+
use bitcoin::util::uint::Uint256;
214+
215+
#[tokio::test]
216+
async fn poll_empty_chain() {
217+
let mut chain = Blockchain::default().with_height(0);
218+
let best_known_chain_tip = chain.tip();
219+
chain.disconnect_tip();
220+
221+
let mut poller = ChainPoller::new(&mut chain as &mut dyn BlockSource, Network::Bitcoin);
222+
match poller.poll_chain_tip(best_known_chain_tip).await {
223+
Err(e) => {
224+
assert_eq!(e.kind(), BlockSourceErrorKind::Transient);
225+
assert_eq!(e.into_inner().as_ref().to_string(), "empty chain");
226+
},
227+
Ok(_) => panic!("Expected error"),
228+
}
229+
}
230+
231+
#[tokio::test]
232+
async fn poll_chain_without_headers() {
233+
let mut chain = Blockchain::default().with_height(1).without_headers();
234+
let best_known_chain_tip = chain.at_height(0);
235+
236+
let mut poller = ChainPoller::new(&mut chain as &mut dyn BlockSource, Network::Bitcoin);
237+
match poller.poll_chain_tip(best_known_chain_tip).await {
238+
Err(e) => {
239+
assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
240+
assert_eq!(e.into_inner().as_ref().to_string(), "header not found");
241+
},
242+
Ok(_) => panic!("Expected error"),
243+
}
244+
}
245+
246+
#[tokio::test]
247+
async fn poll_chain_with_invalid_pow() {
248+
let mut chain = Blockchain::default().with_height(1);
249+
let best_known_chain_tip = chain.at_height(0);
250+
251+
// Invalidate the tip by changing its target.
252+
chain.blocks.last_mut().unwrap().header.bits =
253+
BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0; 32]));
254+
255+
let mut poller = ChainPoller::new(&mut chain as &mut dyn BlockSource, 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(), "block target correct but not attained");
260+
},
261+
Ok(_) => panic!("Expected error"),
262+
}
263+
}
264+
265+
#[tokio::test]
266+
async fn poll_chain_with_malformed_headers() {
267+
let mut chain = Blockchain::default().with_height(1).malformed_headers();
268+
let best_known_chain_tip = chain.at_height(0);
269+
270+
let mut poller = ChainPoller::new(&mut chain as &mut dyn BlockSource, Network::Bitcoin);
271+
match poller.poll_chain_tip(best_known_chain_tip).await {
272+
Err(e) => {
273+
assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
274+
assert_eq!(e.into_inner().as_ref().to_string(), "invalid block hash");
275+
},
276+
Ok(_) => panic!("Expected error"),
277+
}
278+
}
279+
280+
#[tokio::test]
281+
async fn poll_chain_with_common_tip() {
282+
let mut chain = Blockchain::default().with_height(0);
283+
let best_known_chain_tip = chain.tip();
284+
285+
let mut poller = ChainPoller::new(&mut chain as &mut dyn BlockSource, Network::Bitcoin);
286+
match poller.poll_chain_tip(best_known_chain_tip).await {
287+
Err(e) => panic!("Unexpected error: {:?}", e),
288+
Ok(tip) => assert_eq!(tip, ChainTip::Common),
289+
}
290+
}
291+
292+
#[tokio::test]
293+
async fn poll_chain_with_uncommon_tip_but_equal_chainwork() {
294+
let mut chain = Blockchain::default().with_height(1);
295+
let best_known_chain_tip = chain.tip();
296+
297+
// Change the nonce to get a different block hash with the same chainwork.
298+
chain.blocks.last_mut().unwrap().header.nonce += 1;
299+
300+
let worse_chain_tip = chain.tip();
301+
let worse_chain_tip_hash = worse_chain_tip.header.block_hash();
302+
let worse_chain_tip = worse_chain_tip.validate(worse_chain_tip_hash).unwrap();
303+
assert_eq!(best_known_chain_tip.chainwork, worse_chain_tip.chainwork);
304+
305+
let mut poller = ChainPoller::new(&mut chain as &mut dyn BlockSource, Network::Bitcoin);
306+
match poller.poll_chain_tip(best_known_chain_tip).await {
307+
Err(e) => panic!("Unexpected error: {:?}", e),
308+
Ok(tip) => assert_eq!(tip, ChainTip::Worse(worse_chain_tip)),
309+
}
310+
}
311+
312+
#[tokio::test]
313+
async fn poll_chain_with_worse_tip() {
314+
let mut chain = Blockchain::default().with_height(1);
315+
let best_known_chain_tip = chain.tip();
316+
chain.disconnect_tip();
317+
318+
let worse_chain_tip = chain.tip();
319+
let worse_chain_tip_hash = worse_chain_tip.header.block_hash();
320+
let worse_chain_tip = worse_chain_tip.validate(worse_chain_tip_hash).unwrap();
321+
322+
let mut poller = ChainPoller::new(&mut chain as &mut dyn BlockSource, Network::Bitcoin);
323+
match poller.poll_chain_tip(best_known_chain_tip).await {
324+
Err(e) => panic!("Unexpected error: {:?}", e),
325+
Ok(tip) => assert_eq!(tip, ChainTip::Worse(worse_chain_tip)),
326+
}
327+
}
328+
329+
#[tokio::test]
330+
async fn poll_chain_with_better_tip() {
331+
let mut chain = Blockchain::default().with_height(1);
332+
let best_known_chain_tip = chain.at_height(0);
333+
334+
let better_chain_tip = chain.tip();
335+
let better_chain_tip_hash = better_chain_tip.header.block_hash();
336+
let better_chain_tip = better_chain_tip.validate(better_chain_tip_hash).unwrap();
337+
338+
let mut poller = ChainPoller::new(&mut chain as &mut dyn BlockSource, Network::Bitcoin);
339+
match poller.poll_chain_tip(best_known_chain_tip).await {
340+
Err(e) => panic!("Unexpected error: {:?}", e),
341+
Ok(tip) => assert_eq!(tip, ChainTip::Better(better_chain_tip)),
342+
}
343+
}
344+
}
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)