Skip to content

Commit 4546132

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 ee4302a commit 4546132

File tree

3 files changed

+327
-2
lines changed

3 files changed

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