Skip to content

Commit 9ded1d2

Browse files
committed
refactor(bip158)!: make constructor FilterIter::new_with_checkpoint fallible
The logic of `find_base` is moved inside the `new_with_checkpoint` constructor, which will error if a point of agreement (PoA) isn't found. As a result, `get_tip` is now only responsible for setting the stop height, which means it can be called multiple times on a single instance of FilterIter. `cp` field is removed from FilterIter struct, since it is now only used in the constructor to find a base and set the start height. There's no longer a need to remember conflicting blocks on the way to finding the PoA, now that we store all newly fetched headers during `next`.
1 parent a0d965f commit 9ded1d2

File tree

3 files changed

+34
-29
lines changed

3 files changed

+34
-29
lines changed

crates/bitcoind_rpc/examples/filter_iter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ fn main() -> anyhow::Result<()> {
5555
// Initialize block emitter
5656
let cp = chain.tip();
5757
let start_height = cp.height();
58-
let mut emitter = FilterIter::new_with_checkpoint(&rpc_client, cp);
58+
let mut emitter = FilterIter::new_with_checkpoint(&rpc_client, cp)?;
5959
for (_, desc) in graph.index.keychains() {
6060
let spks = SpkIterator::new_with_range(desc, 0..SPK_COUNT).map(|(_, spk)| spk);
6161
emitter.add_spks(spks);

crates/bitcoind_rpc/src/bip158.rs

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ pub struct FilterIter<'c, C> {
3333
client: &'c C,
3434
// SPK inventory
3535
spks: Vec<ScriptBuf>,
36-
// local cp
37-
cp: Option<CheckPoint>,
3836
// block headers
3937
headers: BTreeMap<Height, HashedHeader>,
4038
// heights of matching blocks
@@ -58,7 +56,6 @@ impl<'c, C: RpcApi> FilterIter<'c, C> {
5856
Self {
5957
client,
6058
spks: vec![],
61-
cp: None,
6259
headers: BTreeMap::new(),
6360
matched: BTreeSet::new(),
6461
height,
@@ -68,10 +65,19 @@ impl<'c, C: RpcApi> FilterIter<'c, C> {
6865
}
6966

7067
/// Construct [`FilterIter`] from a given `client` and [`CheckPoint`].
71-
pub fn new_with_checkpoint(client: &'c C, cp: CheckPoint) -> Self {
72-
let mut filter_iter = Self::new_with_height(client, cp.height());
73-
filter_iter.cp = Some(cp);
74-
filter_iter
68+
///
69+
/// # Errors
70+
///
71+
/// If no point of agreement is found between `cp` and the remote node, then
72+
/// a [`Error::ReorgDepthExceeded`] error is returned.
73+
pub fn new_with_checkpoint(client: &'c C, cp: CheckPoint) -> Result<Self, Error> {
74+
let mut iter = Self::new_with_height(client, cp.height());
75+
76+
// Start scanning from point of agreement + 1.
77+
let base = iter.find_base(cp.clone())?;
78+
iter.height = base.height.saturating_add(1);
79+
80+
Ok(iter)
7581
}
7682

7783
/// Extends `self` with an iterator of spks.
@@ -86,7 +92,9 @@ impl<'c, C: RpcApi> FilterIter<'c, C> {
8692

8793
/// Get the remote tip.
8894
///
89-
/// Returns `None` if the remote height is less than the height of this [`FilterIter`].
95+
/// This will set the stop height to that of the new tip.
96+
///
97+
/// Returns `None` if the remote height is not at least the height of this [`FilterIter`].
9098
pub fn get_tip(&mut self) -> Result<Option<BlockId>, Error> {
9199
let tip_hash = self.client.get_best_block_hash()?;
92100
let header = self.client.get_block_header_info(&tip_hash)?;
@@ -95,12 +103,6 @@ impl<'c, C: RpcApi> FilterIter<'c, C> {
95103
return Ok(None);
96104
}
97105

98-
// start scanning from point of agreement + 1
99-
if let Some(cp) = self.cp.as_ref() {
100-
let base = self.find_base_with(cp.clone())?;
101-
self.height = base.height.saturating_add(1);
102-
}
103-
104106
self.stop = tip_height;
105107

106108
Ok(Some(BlockId {
@@ -223,8 +225,14 @@ impl<C: RpcApi> Iterator for FilterIter<'_, C> {
223225
}
224226

225227
impl<C: RpcApi> FilterIter<'_, C> {
226-
/// Returns the point of agreement between `self` and the given `cp`.
227-
fn find_base_with(&mut self, mut cp: CheckPoint) -> Result<BlockId, Error> {
228+
/// Returns the point of agreement (PoA) between `self` and the given `cp`.
229+
///
230+
/// This ensures that the scan may proceed from a block that still exists
231+
/// in the best chain.
232+
///
233+
/// If no PoA is found between `cp` and the remote node, then a [`Error::ReorgDepthExceeded`]
234+
/// error is returned.
235+
fn find_base(&mut self, mut cp: CheckPoint) -> Result<BlockId, Error> {
228236
loop {
229237
let height = cp.height();
230238
let (fetched_hash, header) = match self.headers.get(&height).copied() {
@@ -240,24 +248,24 @@ impl<C: RpcApi> FilterIter<'_, C> {
240248
self.headers.insert(height, (fetched_hash, header));
241249
return Ok(cp.block_id());
242250
}
243-
// Remember conflicts.
244-
self.headers.insert(height, (fetched_hash, header));
245251
cp = cp.prev().ok_or(Error::ReorgDepthExceeded)?;
246252
}
247253
}
248254

249255
/// Returns a chain update from the newly scanned blocks.
250256
///
251-
/// Returns `None` if this [`FilterIter`] was not constructed using a [`CheckPoint`], or
252-
/// if not all events have been emitted (by calling `next`).
257+
/// This should only be called once all events have been consumed (by calling `next`).
258+
///
259+
/// Returns `None` if the height of this `FilterIter` is not yet past the stop height.
253260
pub fn chain_update(&self) -> Option<CheckPoint> {
254-
if self.cp.is_none() || self.headers.is_empty() || self.height <= self.stop {
261+
if self.headers.is_empty() || self.height <= self.stop {
255262
return None;
256263
}
257264

258-
// We return blocks up to and including the initial height, all of the matching blocks,
265+
// We return blocks up to the initial height, all of the matching blocks,
259266
// and blocks in the terminal range.
260267
let tail_range = (self.stop + 1).saturating_sub(Self::CHAIN_SUFFIX_LEN)..=self.stop;
268+
261269
Some(
262270
CheckPoint::from_block_ids(self.headers.iter().filter_map(|(&height, &(hash, _))| {
263271
if height <= self.start

crates/bitcoind_rpc/tests/test_filter_iter.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ fn get_tip_and_chain_update() -> anyhow::Result<()> {
9797
.into_iter()
9898
.for_each(|test| {
9999
let cp = CheckPoint::from_block_ids(test.chain).unwrap();
100-
let mut iter = FilterIter::new_with_checkpoint(env.rpc_client(), cp);
100+
let mut iter = FilterIter::new_with_checkpoint(env.rpc_client(), cp).unwrap();
101101
assert_eq!(iter.get_tip().unwrap(), Some(new_tip));
102102
for _res in iter.by_ref() {}
103103
let update_cp = iter.chain_update().unwrap();
@@ -177,11 +177,8 @@ fn filter_iter_error_wrong_network() -> anyhow::Result<()> {
177177
hash: bitcoin::hashes::Hash::hash(b"wrong-hash"),
178178
};
179179
let cp = CheckPoint::new(block_id);
180-
let mut iter = FilterIter::new_with_checkpoint(rpc, cp);
181-
let err = iter
182-
.get_tip()
183-
.expect_err("`get_tip` should fail to find PoA");
184-
assert!(matches!(err, Error::ReorgDepthExceeded));
180+
let res = FilterIter::new_with_checkpoint(rpc, cp);
181+
assert!(matches!(res, Err(Error::ReorgDepthExceeded)));
185182

186183
Ok(())
187184
}

0 commit comments

Comments
 (0)