Skip to content

Commit 30e25d1

Browse files
committed
feat(bitcoind_rpc): Various fixes for FilterIter
* Remove `MAX_REORG_DEPTH` check and error variant. For the scenario where the `FilterIter` is of a different network than the receiver, the genesis block will be emitted, thus making the receiver error out. * Fix `Iterator::next` impl so that we only mutate internal state after we are certain that no more errors will be returned. * `Error::NoScripts` is no longer returned since I could not justify a rationale for it. * Refactor `Iterator::next` impl be cleaner.
1 parent c2ae4da commit 30e25d1

File tree

2 files changed

+95
-168
lines changed

2 files changed

+95
-168
lines changed

crates/bitcoind_rpc/src/bip158.rs

Lines changed: 95 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use bitcoin::{
1515
bip158::{self, BlockFilter},
1616
Block, BlockHash, ScriptBuf,
1717
};
18-
use bitcoincore_rpc;
1918
use bitcoincore_rpc::RpcApi;
19+
use bitcoincore_rpc::{self, jsonrpc};
2020

2121
/// Block height
2222
type Height = u32;
@@ -44,9 +44,6 @@ pub struct FilterIter<'c, C> {
4444
}
4545

4646
impl<'c, C: RpcApi> FilterIter<'c, C> {
47-
/// Hard cap on how far to walk back when a reorg is detected.
48-
const MAX_REORG_DEPTH: u32 = 100;
49-
5047
/// Construct [`FilterIter`] from a given `client` and start `height`.
5148
pub fn new_with_height(client: &'c C, height: u32) -> Self {
5249
Self {
@@ -148,82 +145,7 @@ impl<C: RpcApi> Iterator for FilterIter<'_, C> {
148145
type Item = Result<Event, Error>;
149146

150147
fn next(&mut self) -> Option<Self::Item> {
151-
(|| -> Result<Option<_>, Error> {
152-
if self.height > self.stop {
153-
return Ok(None);
154-
}
155-
// Fetch next header.
156-
let mut height = self.height;
157-
let mut hash = self.client.get_block_hash(height as _)?;
158-
let mut header = self.client.get_block_header(&hash)?;
159-
160-
// Detect and resolve reorgs: either block at height changed, or its parent changed.
161-
let stored_hash = self.blocks.get(&height).copied();
162-
let prev_hash = height
163-
.checked_sub(1)
164-
.and_then(|height| self.blocks.get(&height).copied());
165-
166-
// If we've seen this height before but the hash has changed, or parent changed, trigger
167-
// reorg.
168-
let reorg_detected = if let Some(old_hash) = stored_hash {
169-
old_hash != hash
170-
} else if let Some(expected_prev) = prev_hash {
171-
header.prev_blockhash != expected_prev
172-
} else {
173-
false
174-
};
175-
176-
// Reorg detected, rewind to last known-good ancestor.
177-
if reorg_detected {
178-
let mut reorg_depth = 0;
179-
loop {
180-
if reorg_depth >= Self::MAX_REORG_DEPTH || height == 0 {
181-
return Err(Error::ReorgDepthExceeded);
182-
}
183-
184-
height = height.saturating_sub(1);
185-
hash = self.client.get_block_hash(height as _)?;
186-
header = self.client.get_block_header(&hash)?;
187-
188-
let prev_height = height.saturating_sub(1);
189-
if let Some(prev_hash) = self.blocks.get(&prev_height) {
190-
if header.prev_blockhash == *prev_hash {
191-
break;
192-
}
193-
}
194-
195-
reorg_depth += 1;
196-
}
197-
198-
self.blocks.split_off(&height);
199-
self.matched.split_off(&height);
200-
}
201-
202-
let filter_bytes = self.client.get_block_filter(&hash)?.filter;
203-
let filter = BlockFilter::new(&filter_bytes);
204-
205-
// record the scanned block
206-
self.blocks.insert(height, hash);
207-
// increment best height
208-
self.height = height.saturating_add(1);
209-
210-
// If the filter matches any of our watched SPKs, fetch the full
211-
// block, and record the matching block entry.
212-
if self.spks.is_empty() {
213-
Err(Error::NoScripts)
214-
} else if filter
215-
.match_any(&hash, self.spks.iter().map(|s| s.as_bytes()))
216-
.map_err(Error::Bip158)?
217-
{
218-
let block = self.client.get_block(&hash)?;
219-
self.matched.insert(height);
220-
let inner = EventInner { height, block };
221-
Ok(Some(Event::Block(inner)))
222-
} else {
223-
Ok(Some(Event::NoMatch(height)))
224-
}
225-
})()
226-
.transpose()
148+
self.next_event().transpose()
227149
}
228150
}
229151

@@ -274,6 +196,99 @@ impl<C: RpcApi> FilterIter<'_, C> {
274196
.expect("blocks must be in order"),
275197
)
276198
}
199+
200+
fn next_event(&mut self) -> Result<Option<Event>, Error> {
201+
let (height, hash) = match self.find_next_block()? {
202+
None => return Ok(None),
203+
Some((height, _)) if height > self.stop => return Ok(None),
204+
Some(block) => block,
205+
};
206+
207+
// Emit and increment `height` (which should really be `next_height`).
208+
let is_match = BlockFilter::new(&self.client.get_block_filter(&hash)?.filter)
209+
.match_any(&hash, self.spks.iter().map(ScriptBuf::as_ref))
210+
.map_err(Error::Bip158)?;
211+
212+
let event = if is_match {
213+
Event::Block(EventInner {
214+
height,
215+
block: self.client.get_block(&hash)?,
216+
})
217+
} else {
218+
Event::NoMatch(height)
219+
};
220+
221+
// Mutate internal state at the end, once we are sure there are no more errors.
222+
if is_match {
223+
self.matched.insert(height);
224+
}
225+
self.matched.split_off(&height);
226+
self.blocks.split_off(&height);
227+
self.blocks.insert(height, hash);
228+
self.height = height.saturating_add(1);
229+
self.cp = self
230+
.cp
231+
.as_ref()
232+
.and_then(|cp| cp.range(..=cp.height()).next());
233+
234+
Ok(Some(event))
235+
}
236+
237+
/// Non-mutating method that finds the next block which connects with our previously-emitted
238+
/// history.
239+
fn find_next_block(&self) -> Result<Option<(Height, BlockHash)>, bitcoincore_rpc::Error> {
240+
let mut height = self.height;
241+
242+
// Search blocks backwards until we find a block which connects with something the consumer
243+
// has already seen.
244+
let hash = loop {
245+
let hash = match self.client.get_block_hash(height as _) {
246+
Ok(hash) => hash,
247+
Err(bitcoincore_rpc::Error::JsonRpc(jsonrpc::Error::Rpc(rpc_err)))
248+
// -8: Out of bounds, -5: Not found
249+
if rpc_err.code == -8 || rpc_err.code == -5 =>
250+
{
251+
return Ok(None)
252+
}
253+
Err(err) => return Err(err),
254+
};
255+
let header = self.client.get_block_header(&hash)?;
256+
257+
let prev_height = match height.checked_sub(1) {
258+
Some(prev_height) => prev_height,
259+
// Always emit the genesis block as it cannot change.
260+
None => break hash,
261+
};
262+
263+
let prev_hash_remote = header.prev_blockhash;
264+
if let Some(&prev_hash) = self.blocks.get(&prev_height) {
265+
if prev_hash == prev_hash_remote {
266+
break hash;
267+
}
268+
height = prev_height;
269+
continue;
270+
}
271+
272+
let maybe_prev_cp = self
273+
.cp
274+
.as_ref()
275+
.and_then(|cp| cp.range(..=prev_height).next());
276+
if let Some(prev_cp) = maybe_prev_cp {
277+
if prev_cp.height() != prev_height {
278+
// Try again at a height that the consumer can compare against.
279+
height = prev_cp.height();
280+
continue;
281+
}
282+
if prev_cp.hash() != prev_hash_remote {
283+
height = prev_height;
284+
continue;
285+
}
286+
}
287+
break hash;
288+
};
289+
290+
Ok(Some((height, hash)))
291+
}
277292
}
278293

279294
/// Errors that may occur during a compact filters sync.
@@ -285,8 +300,6 @@ pub enum Error {
285300
NoScripts,
286301
/// `bitcoincore_rpc` error
287302
Rpc(bitcoincore_rpc::Error),
288-
/// `MAX_REORG_DEPTH` exceeded
289-
ReorgDepthExceeded,
290303
}
291304

292305
impl From<bitcoincore_rpc::Error> for Error {
@@ -301,7 +314,6 @@ impl fmt::Display for Error {
301314
Self::Bip158(e) => e.fmt(f),
302315
Self::NoScripts => write!(f, "no script pubkeys were provided to match with"),
303316
Self::Rpc(e) => e.fmt(f),
304-
Self::ReorgDepthExceeded => write!(f, "maximum reorg depth exceeded"),
305317
}
306318
}
307319
}

crates/bitcoind_rpc/tests/test_filter_iter.rs

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -146,24 +146,6 @@ fn filter_iter_returns_matched_blocks() -> anyhow::Result<()> {
146146
Ok(())
147147
}
148148

149-
#[test]
150-
fn filter_iter_error_no_scripts() -> anyhow::Result<()> {
151-
use bdk_bitcoind_rpc::bip158::Error;
152-
let env = testenv()?;
153-
let _ = env.mine_blocks(2, None)?;
154-
155-
let mut iter = FilterIter::new_with_height(env.rpc_client(), 1);
156-
assert_eq!(iter.get_tip()?.unwrap().height, 3);
157-
158-
// iterator should return three errors
159-
for _ in 0..3 {
160-
assert!(matches!(iter.next().unwrap(), Err(Error::NoScripts)));
161-
}
162-
assert!(iter.next().is_none());
163-
164-
Ok(())
165-
}
166-
167149
#[test]
168150
#[allow(clippy::print_stdout)]
169151
fn filter_iter_handles_reorg() -> anyhow::Result<()> {
@@ -593,70 +575,3 @@ fn repeat_reorgs() -> anyhow::Result<()> {
593575

594576
Ok(())
595577
}
596-
597-
#[test]
598-
#[allow(clippy::print_stdout)]
599-
fn filter_iter_max_reorg_depth() -> anyhow::Result<()> {
600-
use bdk_bitcoind_rpc::bip158::{Error, FilterIter};
601-
use bdk_chain::{
602-
bitcoin::{Address, Amount, Network, ScriptBuf},
603-
BlockId, CheckPoint,
604-
};
605-
606-
let env = testenv()?;
607-
let client = env.rpc_client();
608-
609-
const BASE_HEIGHT: u32 = 110;
610-
const REORG_LENGTH: u32 = 101;
611-
const START_HEIGHT: u32 = BASE_HEIGHT + 1;
612-
const FINAL_HEIGHT: u32 = START_HEIGHT + REORG_LENGTH - 1;
613-
614-
while client.get_block_count()? < BASE_HEIGHT as u64 {
615-
env.mine_blocks(1, None)?;
616-
}
617-
618-
let spk = ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa")?;
619-
let addr = Address::from_script(&spk, Network::Regtest)?;
620-
621-
// Mine blocks 111-211, each with a tx.
622-
for _ in 0..REORG_LENGTH {
623-
env.send(&addr, Amount::from_sat(1000))?;
624-
env.mine_blocks(1, None)?;
625-
}
626-
627-
// Create `CheckPoint` and build `FilterIter`.
628-
let cp = CheckPoint::from_block_ids([BlockId {
629-
height: BASE_HEIGHT,
630-
hash: client.get_block_hash(BASE_HEIGHT as u64)?,
631-
}])
632-
.unwrap();
633-
let mut iter = FilterIter::new_with_checkpoint(client, cp);
634-
iter.add_spk(spk.clone());
635-
iter.get_tip()?;
636-
637-
// Scan blocks 111–211 so their hashes reside in `self.blocks`.
638-
for res in iter.by_ref() {
639-
res?;
640-
}
641-
642-
// Invalidate blocks 111-211 and mine replacement blocks.
643-
for h in (START_HEIGHT..=FINAL_HEIGHT).rev() {
644-
let hash = client.get_block_hash(h as u64)?;
645-
client.invalidate_block(&hash)?;
646-
}
647-
648-
// Mine one extra block so the new tip (212) isn’t below `iter.height`.
649-
env.mine_blocks((REORG_LENGTH + 1) as usize, None)?;
650-
651-
iter.get_tip()?;
652-
match iter.next() {
653-
Some(Err(Error::ReorgDepthExceeded)) => {
654-
println!("SUCCESS: Detected ReorgDepthExceeded");
655-
}
656-
Some(Err(e)) => panic!("Expected ReorgDepthExceeded, got {:?}", e),
657-
Some(Ok(ev)) => panic!("Expected error, got event {:?}", ev),
658-
None => panic!("Expected error, got None"),
659-
}
660-
661-
Ok(())
662-
}

0 commit comments

Comments
 (0)