Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion crates/bitcoind_rpc/examples/filter_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ fn main() -> anyhow::Result<()> {
for (_, desc) in graph.index.keychains() {
spks.extend(SpkIterator::new_with_range(desc, 0..SPK_COUNT).map(|(_, s)| s));
}
let iter = FilterIter::new(&rpc_client, chain.tip(), spks);
const FINAL_DEPTH: u32 = 100;

let start_height = chain.tip().height().saturating_sub(FINAL_DEPTH);

let iter = FilterIter::new(&rpc_client, chain.tip(), start_height, spks);

let start = Instant::now();

Expand Down
30 changes: 25 additions & 5 deletions crates/bitcoind_rpc/src/bip158.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ use bitcoincore_rpc::{json::GetBlockHeaderResult, RpcApi};
/// [`bitcoincore_rpc::Client`].
/// * Collect the script pubkeys (SPKs) you want to watch. These will usually correspond to wallet
/// addresses that have been handed out for receiving payments.
/// * Construct `FilterIter` with the RPC client, SPKs, and [`CheckPoint`]. The checkpoint tip
/// informs `FilterIter` of the height to begin scanning from. An error is thrown if `FilterIter`
/// is unable to find a common ancestor with the remote node.
/// * Construct `FilterIter` with the RPC client, SPKs, [`CheckPoint`] and `start_height`.
/// The checkpoint tip informs `FilterIter` of the height to begin scanning from.
/// In case of deep reorgs where no common ancestor is found above `start_height`,
/// scanning resumes from `start_height`.
/// * Scan blocks by calling `next` in a loop and processing the [`Event`]s. If a filter matched any
/// of the watched scripts, then the relevant [`Block`] is returned. Note that false positives may
/// occur. `FilterIter` will continue to yield events until it reaches the latest chain tip.
Expand All @@ -38,20 +39,26 @@ pub struct FilterIter<'a> {
cp: CheckPoint<BlockHash>,
/// Header info, contains the prev and next hashes for each header.
header: Option<GetBlockHeaderResult>,
/// Earliest height to resume scanning from in case of deep reorgs.
start_height: u32,
}

impl<'a> FilterIter<'a> {
/// Construct [`FilterIter`] with checkpoint, RPC client and SPKs.
/// Construct [`FilterIter`] with checkpoint, RPC client, start height and SPKs.
/// `start_height` is the earliest height to resume scanning from in case
/// a reorg invalidates the local tip.
pub fn new(
client: &'a bitcoincore_rpc::Client,
cp: CheckPoint,
start_height: u32,
spks: impl IntoIterator<Item = ScriptBuf>,
) -> Self {
Self {
client,
spks: spks.into_iter().collect(),
cp,
header: None,
start_height,
}
}

Expand All @@ -60,14 +67,27 @@ impl<'a> FilterIter<'a> {
/// Error if no agreement header is found.
fn find_base(&self) -> Result<GetBlockHeaderResult, Error> {
for cp in self.cp.iter() {
if cp.height() < self.start_height {
break;
}

match self.client.get_block_header_info(&cp.hash()) {
Err(e) if is_not_found(&e) => continue,
Ok(header) if header.confirmations <= 0 => continue,
Ok(header) => return Ok(header),
Err(e) => return Err(Error::Rpc(e)),
}
}
Err(Error::ReorgDepthExceeded)

let hash = self.client.get_block_hash(self.start_height.into())?;

let header = self.client.get_block_header_info(&hash)?;

if header.confirmations <= 0 {
return Err(Error::ReorgDepthExceeded);
}

Ok(header)
}
}

Expand Down
30 changes: 23 additions & 7 deletions crates/bitcoind_rpc/tests/test_filter_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ fn filter_iter_matches_blocks() -> anyhow::Result<()> {
let cp = CheckPoint::new(0, genesis_hash);

let client = ClientExt::get_rpc_client(&env)?;
let iter = FilterIter::new(&client, cp, [addr.script_pubkey()]);
const FINAL_DEPTH: u32 = 100;
let tip_height = ClientExt::get_rpc_client(&env)?.get_block_count()? as u32;
let start_height = tip_height.saturating_sub(FINAL_DEPTH);

let iter = FilterIter::new(&client, cp, start_height, [addr.script_pubkey()]);

for res in iter {
let event = res?;
Expand All @@ -58,15 +62,27 @@ fn filter_iter_matches_blocks() -> anyhow::Result<()> {
}

#[test]
fn filter_iter_error_wrong_network() -> anyhow::Result<()> {
fn filter_iter_recovers_from_start_height() -> anyhow::Result<()> {
let env = testenv()?;
let _ = env.mine_blocks(10, None)?;

// Try to initialize FilterIter with a CP on the wrong network
// Wrong checkpoint hash
let cp = CheckPoint::new(0, bitcoin::hashes::Hash::hash(b"wrong-hash"));

let client = ClientExt::get_rpc_client(&env)?;
let mut iter = FilterIter::new(&client, cp, [ScriptBuf::new()]);
assert!(matches!(iter.next(), Some(Err(Error::ReorgDepthExceeded))));

// Recovery should happen from genesis/start_height
let mut iter = FilterIter::new(
&client,
cp,
0,
[ScriptBuf::new()],
);

// Iterator should successfully continue
let event = iter.next().unwrap()?;

assert_eq!(event.height(), 1);

Ok(())
}
Expand All @@ -87,7 +103,7 @@ fn filter_iter_detects_reorgs() -> anyhow::Result<()> {

let spk = ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa")?;
let client = ClientExt::get_rpc_client(&env)?;
let mut iter = FilterIter::new(&client, cp, [spk]);
let mut iter = FilterIter::new(&client, cp, 0, [spk]);

// Process events to height (MINE_TO - 1)
loop {
Expand Down Expand Up @@ -138,7 +154,7 @@ fn event_checkpoint_connects_to_local_chain() -> anyhow::Result<()> {

// Construct iter
let client = ClientExt::get_rpc_client(&env)?;
let mut iter = FilterIter::new(&client, cp, vec![ScriptBuf::new()]);
let mut iter = FilterIter::new(&client, cp, 0, [ScriptBuf::new()]);

// Now reorg 3 blocks (14, 15, 16)
let new_hashes: BTreeMap<u32, BlockHash> = (14..=16).zip(env.reorg(3)?).collect();
Expand Down
4 changes: 1 addition & 3 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
comment_width = 100
format_code_in_doc_comments = true
wrap_comments = true