Skip to content

Commit 6f20e8a

Browse files
committed
fix: FilterIter detects reorgs
1 parent 14a2863 commit 6f20e8a

File tree

2 files changed

+81
-71
lines changed

2 files changed

+81
-71
lines changed

crates/bitcoind_rpc/src/bip158.rs

+81-70
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ pub struct FilterIter<'c, C> {
3131
spks: Vec<ScriptBuf>,
3232
// local cp
3333
cp: Option<CheckPoint>,
34-
// blocks map
35-
blocks: BTreeMap<Height, BlockHash>,
34+
// map of height -> (hash, is_match)
35+
blocks: BTreeMap<Height, (BlockHash, bool)>,
36+
// initial height
37+
start: Height,
3638
// best height counter
3739
height: Height,
3840
// stop height
@@ -47,6 +49,7 @@ impl<'c, C: RpcApi> FilterIter<'c, C> {
4749
spks: vec![],
4850
cp: None,
4951
blocks: BTreeMap::new(),
52+
start: height,
5053
height,
5154
stop: 0,
5255
}
@@ -69,22 +72,14 @@ impl<'c, C: RpcApi> FilterIter<'c, C> {
6972
self.spks.push(spk);
7073
}
7174

72-
/// Get the next filter and increment the current best height.
73-
///
74-
/// Returns `Ok(None)` when the stop height is exceeded.
75-
fn next_filter(&mut self) -> Result<Option<NextFilter>, Error> {
76-
if self.height > self.stop {
77-
return Ok(None);
78-
}
79-
let height = self.height;
80-
let hash = match self.blocks.get(&height) {
81-
Some(h) => *h,
82-
None => self.client.get_block_hash(height as u64)?,
83-
};
84-
let filter_bytes = self.client.get_block_filter(&hash)?.filter;
85-
let filter = BlockFilter::new(&filter_bytes);
86-
self.height += 1;
87-
Ok(Some((BlockId { height, hash }, filter)))
75+
/// Get the block hash by `height` if it is found in the blocks map.
76+
fn get_block_hash(&self, height: &Height) -> Option<BlockHash> {
77+
Some(self.blocks.get(height)?.0)
78+
}
79+
80+
/// Insert a (non-matching) block height and hash into the blocks map.
81+
fn insert_block(&mut self, height: Height, hash: BlockHash) {
82+
self.blocks.insert(height, (hash, false));
8883
}
8984

9085
/// Get the remote tip.
@@ -93,33 +88,17 @@ impl<'c, C: RpcApi> FilterIter<'c, C> {
9388
/// [`FilterIter`].
9489
pub fn get_tip(&mut self) -> Result<Option<BlockId>, Error> {
9590
let tip_hash = self.client.get_best_block_hash()?;
96-
let mut header = self.client.get_block_header_info(&tip_hash)?;
91+
let header = self.client.get_block_header_info(&tip_hash)?;
9792
let tip_height = header.height as u32;
9893
if self.height >= tip_height {
9994
// nothing to do
10095
return Ok(None);
10196
}
102-
self.blocks.insert(tip_height, tip_hash);
10397

104-
// if we have a checkpoint we use a lookback of ten blocks
105-
// to ensure consistency of the local chain
98+
// start scanning from point of agreement + 1
10699
if let Some(cp) = self.cp.as_ref() {
107-
// adjust start height to point of agreement + 1
108100
let base = self.find_base_with(cp.clone())?;
109-
self.height = base.height + 1;
110-
111-
for _ in 0..9 {
112-
let hash = match header.previous_block_hash {
113-
Some(hash) => hash,
114-
None => break,
115-
};
116-
header = self.client.get_block_header_info(&hash)?;
117-
let height = header.height as u32;
118-
if height < self.height {
119-
break;
120-
}
121-
self.blocks.insert(height, hash);
122-
}
101+
self.height = base.height.saturating_add(1);
123102
}
124103

125104
self.stop = tip_height;
@@ -131,9 +110,6 @@ impl<'c, C: RpcApi> FilterIter<'c, C> {
131110
}
132111
}
133112

134-
/// Alias for a compact filter and associated block id.
135-
type NextFilter = (BlockId, BlockFilter);
136-
137113
/// Event inner type
138114
#[derive(Debug, Clone)]
139115
pub struct EventInner {
@@ -171,27 +147,53 @@ impl<C: RpcApi> Iterator for FilterIter<'_, C> {
171147
type Item = Result<Event, Error>;
172148

173149
fn next(&mut self) -> Option<Self::Item> {
174-
(|| -> Result<_, Error> {
175-
// if the next filter matches any of our watched spks, get the block
176-
// and return it, inserting relevant block ids along the way
177-
self.next_filter()?.map_or(Ok(None), |(block, filter)| {
178-
let height = block.height;
179-
let hash = block.hash;
180-
181-
if self.spks.is_empty() {
182-
Err(Error::NoScripts)
183-
} else if filter
184-
.match_any(&hash, self.spks.iter().map(|script| script.as_bytes()))
185-
.map_err(Error::Bip158)?
186-
{
187-
let block = self.client.get_block(&hash)?;
188-
self.blocks.insert(height, hash);
189-
let inner = EventInner { height, block };
190-
Ok(Some(Event::Block(inner)))
191-
} else {
192-
Ok(Some(Event::NoMatch(height)))
150+
(|| -> Result<Option<_>, Error> {
151+
if self.height > self.stop {
152+
return Ok(None);
153+
}
154+
// Fetch next filter
155+
let mut height = self.height;
156+
let mut hash = self.client.get_block_hash(height as _)?;
157+
loop {
158+
let header = self.client.get_block_header(&hash)?;
159+
let prev_height = height.saturating_sub(1);
160+
let prev_hash = match self.get_block_hash(&prev_height) {
161+
Some(hash) => hash,
162+
None => break,
163+
};
164+
if header.prev_blockhash == prev_hash {
165+
break;
193166
}
194-
})
167+
// reorg detected, keep backtracking
168+
height = height.saturating_sub(1);
169+
hash = self.client.get_block_hash(height as _)?;
170+
}
171+
let filter_bytes = self.client.get_block_filter(&hash)?.filter;
172+
let filter = BlockFilter::new(&filter_bytes);
173+
174+
// record the scanned block
175+
self.insert_block(height, hash);
176+
// increment best height
177+
self.height = height.saturating_add(1);
178+
179+
// If the filter matches any of our watched SPKs, fetch the full
180+
// block, and record the matching block entry.
181+
if self.spks.is_empty() {
182+
Err(Error::NoScripts)
183+
} else if filter
184+
.match_any(&hash, self.spks.iter().map(|s| s.as_bytes()))
185+
.map_err(Error::Bip158)?
186+
{
187+
let block = self.client.get_block(&hash)?;
188+
// update for matched block
189+
self.blocks.entry(height).and_modify(|(_, is_match)| {
190+
*is_match = true;
191+
});
192+
let inner = EventInner { height, block };
193+
Ok(Some(Event::Block(inner)))
194+
} else {
195+
Ok(Some(Event::NoMatch(height)))
196+
}
195197
})()
196198
.transpose()
197199
}
@@ -202,36 +204,45 @@ impl<C: RpcApi> FilterIter<'_, C> {
202204
fn find_base_with(&mut self, mut cp: CheckPoint) -> Result<BlockId, Error> {
203205
loop {
204206
let height = cp.height();
205-
let fetched_hash = match self.blocks.get(&height) {
206-
Some(hash) => *hash,
207+
let fetched_hash = match self.get_block_hash(&height) {
208+
Some(hash) => hash,
207209
None if height == 0 => cp.hash(),
208210
_ => self.client.get_block_hash(height as _)?,
209211
};
210212
if cp.hash() == fetched_hash {
211213
// ensure this block also exists in self
212-
self.blocks.insert(height, cp.hash());
214+
self.insert_block(height, cp.hash());
213215
return Ok(cp.block_id());
214216
}
215217
// remember conflicts
216-
self.blocks.insert(height, fetched_hash);
218+
self.insert_block(height, fetched_hash);
217219
cp = cp.prev().expect("must break before genesis");
218220
}
219221
}
220222

221223
/// Returns a chain update from the newly scanned blocks.
222224
///
223225
/// Returns `None` if this [`FilterIter`] was not constructed using a [`CheckPoint`], or
224-
/// if no blocks have been fetched for example by using [`get_tip`](Self::get_tip).
226+
/// if not all events have been emitted (by calling `next`).
225227
pub fn chain_update(&mut self) -> Option<CheckPoint> {
226-
if self.cp.is_none() || self.blocks.is_empty() {
228+
if self.cp.is_none() || self.blocks.is_empty() || self.height <= self.stop {
227229
return None;
228230
}
229231

230-
// note: to connect with the local chain we must guarantee that `self.blocks.first()`
231-
// is also the point of agreement with `self.cp`.
232+
// We return blocks up to and including the initial height, all of the matching blocks,
233+
// and blocks in the terminal range.
234+
let tail_range = self.stop.saturating_sub(9)..=self.stop;
232235
Some(
233-
CheckPoint::from_block_ids(self.blocks.iter().map(BlockId::from))
234-
.expect("blocks must be in order"),
236+
CheckPoint::from_block_ids(self.blocks.iter().filter_map(
237+
|(&height, &(hash, is_match))| {
238+
if height <= self.start || is_match || tail_range.contains(&height) {
239+
Some(BlockId { height, hash })
240+
} else {
241+
None
242+
}
243+
},
244+
))
245+
.expect("blocks must be in order"),
235246
)
236247
}
237248
}

crates/bitcoind_rpc/tests/test_filter_iter.rs

-1
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,6 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
400400

401401
// Test that while a reorg is detected we delay incrementing the best height
402402
#[test]
403-
#[ignore]
404403
fn repeat_reorgs() -> anyhow::Result<()> {
405404
const MINE_TO: u32 = 11;
406405

0 commit comments

Comments
 (0)