Skip to content

Commit ff92f30

Browse files
committed
fix(rpc)!: improve checkpoint update
Change `Event` to a simple struct containing `cp` and optional `block`. The checkpoint is updated on each iteration whether or not it corresponds to a matching block. We use `CheckPoint::insert` which will also purge evicted blocks if needed. Change implementation of `find_base` to use `get_block_header_info` which helps to reduce the number of RPC calls. Add test `event_checkpoint_connects_to_local_chain` to check the expected events after a reorg, and check that intermediate updates can be applied to the local chain.
1 parent b302336 commit ff92f30

File tree

3 files changed

+84
-87
lines changed

3 files changed

+84
-87
lines changed

crates/bitcoind_rpc/examples/filter_iter.rs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,12 @@ fn main() -> anyhow::Result<()> {
6161
let start = Instant::now();
6262

6363
for res in iter {
64-
let event = res?;
65-
match event {
66-
Event::NoMatch { .. } => {}
67-
Event::Block { cp, ref block } => {
68-
// Apply relevant tx data
69-
let height = cp.height();
70-
let _ = graph.apply_block_relevant(block, height);
71-
// Update chain tip
72-
let _ = chain.apply_update(cp)?;
73-
println!("Matched block {height}");
74-
}
75-
Event::Tip { cp } => {
76-
let _ = chain.apply_update(cp)?;
77-
}
64+
let Event { cp, block } = res?;
65+
let height = cp.height();
66+
let _ = chain.apply_update(cp)?;
67+
if let Some(block) = block {
68+
let _ = graph.apply_block_relevant(&block, height);
69+
println!("Matched block {height}");
7870
}
7971
}
8072

crates/bitcoind_rpc/src/bip158.rs

Lines changed: 37 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -41,59 +41,40 @@ impl<'a> FilterIter<'a> {
4141
}
4242
}
4343

44-
/// Find the agreement height with the remote node and return the corresponding
45-
/// header info.
44+
/// Return the agreement header with the remote node.
4645
///
47-
/// Error if no agreement height is found.
46+
/// Error if no agreement header is found.
4847
fn find_base(&self) -> Result<GetBlockHeaderResult, Error> {
4948
for cp in self.cp.iter() {
50-
let height = cp.height();
51-
52-
let fetched_hash = self.client.get_block_hash(height as u64)?;
53-
54-
if fetched_hash == cp.hash() {
55-
return Ok(self.client.get_block_header_info(&fetched_hash)?);
49+
match self.client.get_block_header_info(&cp.hash()) {
50+
Err(e) if is_not_found(&e) => continue,
51+
Ok(header) if header.confirmations <= 0 => continue,
52+
Ok(header) => return Ok(header),
53+
Err(e) => return Err(Error::Rpc(e)),
5654
}
5755
}
58-
5956
Err(Error::ReorgDepthExceeded)
6057
}
6158
}
6259

63-
/// Kind of event produced by `FilterIter`.
60+
/// Event returned by [`FilterIter`].
6461
#[derive(Debug, Clone)]
65-
pub enum Event {
66-
/// Block
67-
Block {
68-
/// checkpoint
69-
cp: CheckPoint,
70-
/// block
71-
block: Block,
72-
},
73-
/// No match
74-
NoMatch {
75-
/// block id
76-
id: BlockId,
77-
},
78-
/// Tip
79-
Tip {
80-
/// checkpoint
81-
cp: CheckPoint,
82-
},
62+
pub struct Event {
63+
/// Checkpoint
64+
pub cp: CheckPoint,
65+
/// Block, will be `Some(..)` for matching blocks
66+
pub block: Option<Block>,
8367
}
8468

8569
impl Event {
8670
/// Whether this event contains a matching block.
8771
pub fn is_match(&self) -> bool {
88-
matches!(self, Event::Block { .. })
72+
self.block.is_some()
8973
}
9074

9175
/// Return the height of the event.
9276
pub fn height(&self) -> u32 {
93-
match self {
94-
Self::Block { cp, .. } | Self::Tip { cp } => cp.height(),
95-
Self::NoMatch { id } => id.height,
96-
}
77+
self.cp.height()
9778
}
9879
}
9980

@@ -106,15 +87,9 @@ impl Iterator for FilterIter<'_> {
10687

10788
let header = match self.header.take() {
10889
Some(header) => header,
109-
None => {
110-
// If no header is cached we need to locate a base of the local
111-
// checkpoint from which the scan may proceed.
112-
let header = self.find_base()?;
113-
let height: u32 = header.height.try_into()?;
114-
cp = cp.range(..=height).next().expect("we found a base");
115-
116-
header
117-
}
90+
// If no header is cached we need to locate a base of the local
91+
// checkpoint from which the scan may proceed.
92+
None => self.find_base()?,
11893
};
11994

12095
let mut next_hash = match header.next_block_hash {
@@ -126,58 +101,38 @@ impl Iterator for FilterIter<'_> {
126101

127102
// In case of a reorg, rewind by fetching headers of previous hashes until we find
128103
// one with enough confirmations.
129-
let mut reorg_ct: i32 = 0;
130104
while next_header.confirmations < 0 {
131105
let prev_hash = next_header
132106
.previous_block_hash
133107
.ok_or(Error::ReorgDepthExceeded)?;
134108
let prev_header = self.client.get_block_header_info(&prev_hash)?;
135109
next_header = prev_header;
136-
reorg_ct += 1;
137110
}
138111

139112
next_hash = next_header.hash;
140113
let next_height: u32 = next_header.height.try_into()?;
141114

142-
// Purge any no longer valid checkpoints.
143-
if reorg_ct.is_positive() {
144-
cp = cp
145-
.range(..=next_height)
146-
.next()
147-
.ok_or(Error::ReorgDepthExceeded)?;
148-
}
149-
let block_id = BlockId {
115+
cp = cp.insert(BlockId {
150116
height: next_height,
151117
hash: next_hash,
152-
};
153-
let filter_bytes = self.client.get_block_filter(&next_hash)?.filter;
154-
let filter = BlockFilter::new(&filter_bytes);
118+
});
155119

156-
let next_event = if filter
120+
let mut block = None;
121+
let filter =
122+
BlockFilter::new(self.client.get_block_filter(&next_hash)?.filter.as_slice());
123+
if filter
157124
.match_any(&next_hash, self.spks.iter().map(ScriptBuf::as_ref))
158125
.map_err(Error::Bip158)?
159126
{
160-
let block = self.client.get_block(&next_hash)?;
161-
cp = cp.insert(block_id);
162-
163-
Ok(Some(Event::Block {
164-
cp: cp.clone(),
165-
block,
166-
}))
167-
} else if next_header.next_block_hash.is_none() {
168-
cp = cp.insert(block_id);
169-
170-
Ok(Some(Event::Tip { cp: cp.clone() }))
171-
} else {
172-
Ok(Some(Event::NoMatch { id: block_id }))
173-
};
127+
block = Some(self.client.get_block(&next_hash)?);
128+
}
174129

175130
// Store the next header
176131
self.header = Some(next_header);
177132
// Update self.cp
178-
self.cp = cp;
133+
self.cp = cp.clone();
179134

180-
next_event
135+
Ok(Some(Event { cp, block }))
181136
})()
182137
.transpose()
183138
}
@@ -221,3 +176,12 @@ impl From<core::num::TryFromIntError> for Error {
221176
Self::TryFromInt(e)
222177
}
223178
}
179+
180+
/// Whether the RPC error is a "not found" error (code: `-5`).
181+
fn is_not_found(e: &bitcoincore_rpc::Error) -> bool {
182+
matches!(
183+
e,
184+
bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(e))
185+
if e.code == -5
186+
)
187+
}

crates/bitcoind_rpc/tests/test_filter_iter.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,44 @@ fn filter_iter_detects_reorgs() -> anyhow::Result<()> {
112112

113113
Ok(())
114114
}
115+
116+
#[test]
117+
fn event_checkpoint_connects_to_local_chain() -> anyhow::Result<()> {
118+
use bitcoin::BlockHash;
119+
use std::collections::BTreeMap;
120+
let env = testenv()?;
121+
let _ = env.mine_blocks(15, None)?;
122+
123+
let cp = env.make_checkpoint_tip();
124+
let mut chain = bdk_chain::local_chain::LocalChain::from_tip(cp.clone())?;
125+
assert_eq!(chain.tip().height(), 16);
126+
let old_hashes: Vec<BlockHash> = [14, 15, 16]
127+
.into_iter()
128+
.map(|height| chain.get(height).unwrap().hash())
129+
.collect();
130+
131+
// Construct iter
132+
let mut iter = FilterIter::new(&env.bitcoind.client, cp, vec![ScriptBuf::new()]);
133+
134+
// Now reorg 3 blocks (14, 15, 16)
135+
let new_hashes: BTreeMap<u32, BlockHash> = (14..=16).zip(env.reorg(3)?).collect();
136+
137+
// Expect events from height 14 on...
138+
while let Some(event) = iter.next().transpose()? {
139+
let _ = chain
140+
.apply_update(event.cp)
141+
.expect("chain update should connect");
142+
}
143+
144+
for height in 14..=16 {
145+
let hash = chain.get(height).unwrap().hash();
146+
assert!(!old_hashes.contains(&hash), "Old hashes were reorged out");
147+
assert_eq!(
148+
new_hashes.get(&height),
149+
Some(&hash),
150+
"Chain must include new hashes"
151+
);
152+
}
153+
154+
Ok(())
155+
}

0 commit comments

Comments
 (0)