Skip to content

Commit a2a864d

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 7126276 commit a2a864d

File tree

3 files changed

+334
-2
lines changed

3 files changed

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