Skip to content

Commit a922830

Browse files
authored
Merge pull request #1706 from jkczyz/2022-09-filtered-blocks
Support filtered blocks in `lightning-block-sync`
2 parents a82fb62 + c1938e8 commit a922830

File tree

6 files changed

+119
-30
lines changed

6 files changed

+119
-30
lines changed

lightning-block-sync/src/init.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,16 @@ impl<'a, L: chain::Listen + ?Sized> chain::Listen for DynamicChainListener<'a, L
216216
struct ChainListenerSet<'a, L: chain::Listen + ?Sized>(Vec<(u32, &'a L)>);
217217

218218
impl<'a, L: chain::Listen + ?Sized> chain::Listen for ChainListenerSet<'a, L> {
219+
// Needed to differentiate test expectations.
220+
#[cfg(test)]
221+
fn block_connected(&self, block: &bitcoin::Block, height: u32) {
222+
for (starting_height, chain_listener) in self.0.iter() {
223+
if height > *starting_height {
224+
chain_listener.block_connected(block, height);
225+
}
226+
}
227+
}
228+
219229
fn filtered_block_connected(&self, header: &BlockHeader, txdata: &chain::transaction::TransactionData, height: u32) {
220230
for (starting_height, chain_listener) in self.0.iter() {
221231
if height > *starting_height {

lightning-block-sync/src/lib.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ pub trait BlockSource : Sync + Send {
6868

6969
/// Returns the block for a given hash. A headers-only block source should return a `Transient`
7070
/// error.
71-
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block>;
71+
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, BlockData>;
7272

7373
/// Returns the hash of the best block and, optionally, its height.
7474
///
@@ -152,6 +152,18 @@ pub struct BlockHeaderData {
152152
pub chainwork: Uint256,
153153
}
154154

155+
/// A block including either all its transactions or only the block header.
156+
///
157+
/// [`BlockSource`] may be implemented to either always return full blocks or, in the case of
158+
/// compact block filters (BIP 157/158), return header-only blocks when no pertinent transactions
159+
/// match. See [`chain::Filter`] for details on how to notify a source of such transactions.
160+
pub enum BlockData {
161+
/// A block containing all its transactions.
162+
FullBlock(Block),
163+
/// A block header for when the block does not contain any pertinent transactions.
164+
HeaderOnly(BlockHeader),
165+
}
166+
155167
/// A lightweight client for keeping a listener in sync with the chain, allowing for Simplified
156168
/// Payment Verification (SPV).
157169
///
@@ -396,13 +408,22 @@ impl<'a, C: Cache, L: Deref> ChainNotifier<'a, C, L> where L::Target: chain::Lis
396408
chain_poller: &mut P,
397409
) -> Result<(), (BlockSourceError, Option<ValidatedBlockHeader>)> {
398410
for header in connected_blocks.drain(..).rev() {
399-
let block = chain_poller
411+
let height = header.height;
412+
let block_data = chain_poller
400413
.fetch_block(&header).await
401414
.or_else(|e| Err((e, Some(new_tip))))?;
402-
debug_assert_eq!(block.block_hash, header.block_hash);
415+
debug_assert_eq!(block_data.block_hash, header.block_hash);
416+
417+
match block_data.deref() {
418+
BlockData::FullBlock(block) => {
419+
self.chain_listener.block_connected(&block, height);
420+
},
421+
BlockData::HeaderOnly(header) => {
422+
self.chain_listener.filtered_block_connected(&header, &[], height);
423+
},
424+
}
403425

404426
self.header_cache.block_connected(header.block_hash, header);
405-
self.chain_listener.block_connected(&block, header.height);
406427
new_tip = header;
407428
}
408429

@@ -707,4 +728,25 @@ mod chain_notifier_tests {
707728
Ok(_) => panic!("Expected error"),
708729
}
709730
}
731+
732+
#[tokio::test]
733+
async fn sync_from_chain_with_filtered_blocks() {
734+
let mut chain = Blockchain::default().with_height(3).filtered_blocks();
735+
736+
let new_tip = chain.tip();
737+
let old_tip = chain.at_height(1);
738+
let chain_listener = &MockChainListener::new()
739+
.expect_filtered_block_connected(*chain.at_height(2))
740+
.expect_filtered_block_connected(*new_tip);
741+
let mut notifier = ChainNotifier {
742+
header_cache: &mut chain.header_cache(0..=1),
743+
chain_listener,
744+
};
745+
let mut poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
746+
match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
747+
Err((e, _)) => panic!("Unexpected error: {:?}", e),
748+
Ok(_) => {},
749+
}
750+
}
751+
710752
}

lightning-block-sync/src/poll.rs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
//! Adapters that make one or more [`BlockSource`]s simpler to poll for new chain tip transitions.
22
3-
use crate::{AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceResult};
3+
use crate::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceResult};
44

5-
use bitcoin::blockdata::block::Block;
65
use bitcoin::hash_types::BlockHash;
76
use bitcoin::network::constants::Network;
87

@@ -71,24 +70,31 @@ impl Validate for BlockHeaderData {
7170
}
7271
}
7372

74-
impl Validate for Block {
73+
impl Validate for BlockData {
7574
type T = ValidatedBlock;
7675

7776
fn validate(self, block_hash: BlockHash) -> BlockSourceResult<Self::T> {
78-
let pow_valid_block_hash = self.header
79-
.validate_pow(&self.header.target())
77+
let header = match &self {
78+
BlockData::FullBlock(block) => &block.header,
79+
BlockData::HeaderOnly(header) => header,
80+
};
81+
82+
let pow_valid_block_hash = header
83+
.validate_pow(&header.target())
8084
.or_else(|e| Err(BlockSourceError::persistent(e)))?;
8185

8286
if pow_valid_block_hash != block_hash {
8387
return Err(BlockSourceError::persistent("invalid block hash"));
8488
}
8589

86-
if !self.check_merkle_root() {
87-
return Err(BlockSourceError::persistent("invalid merkle root"));
88-
}
90+
if let BlockData::FullBlock(block) = &self {
91+
if !block.check_merkle_root() {
92+
return Err(BlockSourceError::persistent("invalid merkle root"));
93+
}
8994

90-
if !self.check_witness_commitment() {
91-
return Err(BlockSourceError::persistent("invalid witness commitment"));
95+
if !block.check_witness_commitment() {
96+
return Err(BlockSourceError::persistent("invalid witness commitment"));
97+
}
9298
}
9399

94100
Ok(ValidatedBlock { block_hash, inner: self })
@@ -145,11 +151,11 @@ impl ValidatedBlockHeader {
145151
/// A block with validated data against its transaction list and corresponding block hash.
146152
pub struct ValidatedBlock {
147153
pub(crate) block_hash: BlockHash,
148-
inner: Block,
154+
inner: BlockData,
149155
}
150156

151157
impl std::ops::Deref for ValidatedBlock {
152-
type Target = Block;
158+
type Target = BlockData;
153159

154160
fn deref(&self) -> &Self::Target {
155161
&self.inner
@@ -161,7 +167,7 @@ mod sealed {
161167
pub trait Validate {}
162168

163169
impl Validate for crate::BlockHeaderData {}
164-
impl Validate for bitcoin::blockdata::block::Block {}
170+
impl Validate for crate::BlockData {}
165171
}
166172

167173
/// The canonical `Poll` implementation used for a single `BlockSource`.

lightning-block-sync/src/rest.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
//! Simple REST client implementation which implements [`BlockSource`] against a Bitcoin Core REST
22
//! endpoint.
33
4-
use crate::{BlockHeaderData, BlockSource, AsyncBlockSourceResult};
4+
use crate::{BlockData, BlockHeaderData, BlockSource, AsyncBlockSourceResult};
55
use crate::http::{BinaryResponse, HttpEndpoint, HttpClient, JsonResponse};
66

7-
use bitcoin::blockdata::block::Block;
87
use bitcoin::hash_types::BlockHash;
98
use bitcoin::hashes::hex::ToHex;
109

@@ -45,10 +44,10 @@ impl BlockSource for RestClient {
4544
})
4645
}
4746

48-
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> {
47+
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, BlockData> {
4948
Box::pin(async move {
5049
let resource_path = format!("block/{}.bin", header_hash.to_hex());
51-
Ok(self.request_resource::<BinaryResponse, _>(&resource_path).await?)
50+
Ok(BlockData::FullBlock(self.request_resource::<BinaryResponse, _>(&resource_path).await?))
5251
})
5352
}
5453

lightning-block-sync/src/rpc.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
//! Simple RPC client implementation which implements [`BlockSource`] against a Bitcoin Core RPC
22
//! endpoint.
33
4-
use crate::{BlockHeaderData, BlockSource, AsyncBlockSourceResult};
4+
use crate::{BlockData, BlockHeaderData, BlockSource, AsyncBlockSourceResult};
55
use crate::http::{HttpClient, HttpEndpoint, HttpError, JsonResponse};
66

7-
use bitcoin::blockdata::block::Block;
87
use bitcoin::hash_types::BlockHash;
98
use bitcoin::hashes::hex::ToHex;
109

@@ -91,11 +90,11 @@ impl BlockSource for RpcClient {
9190
})
9291
}
9392

94-
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> {
93+
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, BlockData> {
9594
Box::pin(async move {
9695
let header_hash = serde_json::json!(header_hash.to_hex());
9796
let verbosity = serde_json::json!(0);
98-
Ok(self.call_method("getblock", &[header_hash, verbosity]).await?)
97+
Ok(BlockData::FullBlock(self.call_method("getblock", &[header_hash, verbosity]).await?))
9998
})
10099
}
101100

lightning-block-sync/src/test_utils.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError, UnboundedCache};
1+
use crate::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, BlockSourceError, UnboundedCache};
22
use crate::poll::{Validate, ValidatedBlockHeader};
33

44
use bitcoin::blockdata::block::{Block, BlockHeader};
@@ -20,6 +20,7 @@ pub struct Blockchain {
2020
without_blocks: Option<std::ops::RangeFrom<usize>>,
2121
without_headers: bool,
2222
malformed_headers: bool,
23+
filtered_blocks: bool,
2324
}
2425

2526
impl Blockchain {
@@ -77,6 +78,10 @@ impl Blockchain {
7778
Self { malformed_headers: true, ..self }
7879
}
7980

81+
pub fn filtered_blocks(self) -> Self {
82+
Self { filtered_blocks: true, ..self }
83+
}
84+
8085
pub fn fork_at_height(&self, height: usize) -> Self {
8186
assert!(height + 1 < self.blocks.len());
8287
let mut blocks = self.blocks.clone();
@@ -146,7 +151,7 @@ impl BlockSource for Blockchain {
146151
})
147152
}
148153

149-
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> {
154+
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, BlockData> {
150155
Box::pin(async move {
151156
for (height, block) in self.blocks.iter().enumerate() {
152157
if block.header.block_hash() == *header_hash {
@@ -156,7 +161,11 @@ impl BlockSource for Blockchain {
156161
}
157162
}
158163

159-
return Ok(block.clone());
164+
if self.filtered_blocks {
165+
return Ok(BlockData::HeaderOnly(block.header.clone()));
166+
} else {
167+
return Ok(BlockData::FullBlock(block.clone()));
168+
}
160169
}
161170
}
162171
Err(BlockSourceError::transient("block not found"))
@@ -185,13 +194,15 @@ impl chain::Listen for NullChainListener {
185194

186195
pub struct MockChainListener {
187196
expected_blocks_connected: RefCell<VecDeque<BlockHeaderData>>,
197+
expected_filtered_blocks_connected: RefCell<VecDeque<BlockHeaderData>>,
188198
expected_blocks_disconnected: RefCell<VecDeque<BlockHeaderData>>,
189199
}
190200

191201
impl MockChainListener {
192202
pub fn new() -> Self {
193203
Self {
194204
expected_blocks_connected: RefCell::new(VecDeque::new()),
205+
expected_filtered_blocks_connected: RefCell::new(VecDeque::new()),
195206
expected_blocks_disconnected: RefCell::new(VecDeque::new()),
196207
}
197208
}
@@ -201,17 +212,34 @@ impl MockChainListener {
201212
self
202213
}
203214

215+
pub fn expect_filtered_block_connected(self, block: BlockHeaderData) -> Self {
216+
self.expected_filtered_blocks_connected.borrow_mut().push_back(block);
217+
self
218+
}
219+
204220
pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
205221
self.expected_blocks_disconnected.borrow_mut().push_back(block);
206222
self
207223
}
208224
}
209225

210226
impl chain::Listen for MockChainListener {
211-
fn filtered_block_connected(&self, header: &BlockHeader, _txdata: &chain::transaction::TransactionData, height: u32) {
227+
fn block_connected(&self, block: &Block, height: u32) {
212228
match self.expected_blocks_connected.borrow_mut().pop_front() {
213229
None => {
214-
panic!("Unexpected block connected: {:?}", header.block_hash());
230+
panic!("Unexpected block connected: {:?}", block.block_hash());
231+
},
232+
Some(expected_block) => {
233+
assert_eq!(block.block_hash(), expected_block.header.block_hash());
234+
assert_eq!(height, expected_block.height);
235+
},
236+
}
237+
}
238+
239+
fn filtered_block_connected(&self, header: &BlockHeader, _txdata: &chain::transaction::TransactionData, height: u32) {
240+
match self.expected_filtered_blocks_connected.borrow_mut().pop_front() {
241+
None => {
242+
panic!("Unexpected filtered block connected: {:?}", header.block_hash());
215243
},
216244
Some(expected_block) => {
217245
assert_eq!(header.block_hash(), expected_block.header.block_hash());
@@ -244,6 +272,11 @@ impl Drop for MockChainListener {
244272
panic!("Expected blocks connected: {:?}", expected_blocks_connected);
245273
}
246274

275+
let expected_filtered_blocks_connected = self.expected_filtered_blocks_connected.borrow();
276+
if !expected_filtered_blocks_connected.is_empty() {
277+
panic!("Expected filtered_blocks connected: {:?}", expected_filtered_blocks_connected);
278+
}
279+
247280
let expected_blocks_disconnected = self.expected_blocks_disconnected.borrow();
248281
if !expected_blocks_disconnected.is_empty() {
249282
panic!("Expected blocks disconnected: {:?}", expected_blocks_disconnected);

0 commit comments

Comments
 (0)