bdk_bitcoind_rpc/
bip158.rs

1//! Compact block filters sync over RPC. For more details refer to [BIP157][0].
2//!
3//! This module is home to [`FilterIter`], a structure that returns bitcoin blocks by matching
4//! a list of script pubkeys against a [BIP158][1] [`BlockFilter`].
5//!
6//! [0]: https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki
7//! [1]: https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki
8
9use bdk_core::bitcoin;
10use bdk_core::{BlockId, CheckPoint};
11use bitcoin::{bip158::BlockFilter, Block, ScriptBuf};
12use bitcoincore_rpc;
13use bitcoincore_rpc::{json::GetBlockHeaderResult, RpcApi};
14
15/// Type that returns Bitcoin blocks by matching a list of script pubkeys (SPKs) against a
16/// [`bip158::BlockFilter`](bitcoin::bip158::BlockFilter).
17///
18/// * `FilterIter` talks to bitcoind via JSON-RPC interface, which is handled by the
19///   [`bitcoincore_rpc::Client`].
20/// * Collect the script pubkeys (SPKs) you want to watch. These will usually correspond to wallet
21///   addresses that have been handed out for receiving payments.
22/// * Construct `FilterIter` with the RPC client, SPKs, and [`CheckPoint`]. The checkpoint tip
23///   informs `FilterIter` of the height to begin scanning from. An error is thrown if `FilterIter`
24///   is unable to find a common ancestor with the remote node.
25/// * Scan blocks by calling `next` in a loop and processing the [`Event`]s. If a filter matched any
26///   of the watched scripts, then the relevant [`Block`] is returned. Note that false positives may
27///   occur. `FilterIter` will continue to yield events until it reaches the latest chain tip.
28///   Events contain the updated checkpoint `cp` which may be incorporated into the local chain
29///   state to stay in sync with the tip.
30#[derive(Debug)]
31pub struct FilterIter<'a> {
32    /// RPC client
33    client: &'a bitcoincore_rpc::Client,
34    /// SPK inventory
35    spks: Vec<ScriptBuf>,
36    /// checkpoint
37    cp: CheckPoint,
38    /// Header info, contains the prev and next hashes for each header.
39    header: Option<GetBlockHeaderResult>,
40}
41
42impl<'a> FilterIter<'a> {
43    /// Construct [`FilterIter`] with checkpoint, RPC client and SPKs.
44    pub fn new(
45        client: &'a bitcoincore_rpc::Client,
46        cp: CheckPoint,
47        spks: impl IntoIterator<Item = ScriptBuf>,
48    ) -> Self {
49        Self {
50            client,
51            spks: spks.into_iter().collect(),
52            cp,
53            header: None,
54        }
55    }
56
57    /// Return the agreement header with the remote node.
58    ///
59    /// Error if no agreement header is found.
60    fn find_base(&self) -> Result<GetBlockHeaderResult, Error> {
61        for cp in self.cp.iter() {
62            match self.client.get_block_header_info(&cp.hash()) {
63                Err(e) if is_not_found(&e) => continue,
64                Ok(header) if header.confirmations <= 0 => continue,
65                Ok(header) => return Ok(header),
66                Err(e) => return Err(Error::Rpc(e)),
67            }
68        }
69        Err(Error::ReorgDepthExceeded)
70    }
71}
72
73/// Event returned by [`FilterIter`].
74#[derive(Debug, Clone)]
75pub struct Event {
76    /// Checkpoint
77    pub cp: CheckPoint,
78    /// Block, will be `Some(..)` for matching blocks
79    pub block: Option<Block>,
80}
81
82impl Event {
83    /// Whether this event contains a matching block.
84    pub fn is_match(&self) -> bool {
85        self.block.is_some()
86    }
87
88    /// Return the height of the event.
89    pub fn height(&self) -> u32 {
90        self.cp.height()
91    }
92}
93
94impl Iterator for FilterIter<'_> {
95    type Item = Result<Event, Error>;
96
97    fn next(&mut self) -> Option<Self::Item> {
98        (|| -> Result<Option<_>, Error> {
99            let mut cp = self.cp.clone();
100
101            let header = match self.header.take() {
102                Some(header) => header,
103                // If no header is cached we need to locate a base of the local
104                // checkpoint from which the scan may proceed.
105                None => self.find_base()?,
106            };
107
108            let mut next_hash = match header.next_block_hash {
109                Some(hash) => hash,
110                None => return Ok(None),
111            };
112
113            let mut next_header = self.client.get_block_header_info(&next_hash)?;
114
115            // In case of a reorg, rewind by fetching headers of previous hashes until we find
116            // one with enough confirmations.
117            while next_header.confirmations < 0 {
118                let prev_hash = next_header
119                    .previous_block_hash
120                    .ok_or(Error::ReorgDepthExceeded)?;
121                let prev_header = self.client.get_block_header_info(&prev_hash)?;
122                next_header = prev_header;
123            }
124
125            next_hash = next_header.hash;
126            let next_height: u32 = next_header.height.try_into()?;
127
128            cp = cp.insert(BlockId {
129                height: next_height,
130                hash: next_hash,
131            });
132
133            let mut block = None;
134            let filter =
135                BlockFilter::new(self.client.get_block_filter(&next_hash)?.filter.as_slice());
136            if filter
137                .match_any(&next_hash, self.spks.iter().map(ScriptBuf::as_ref))
138                .map_err(Error::Bip158)?
139            {
140                block = Some(self.client.get_block(&next_hash)?);
141            }
142
143            // Store the next header
144            self.header = Some(next_header);
145            // Update self.cp
146            self.cp = cp.clone();
147
148            Ok(Some(Event { cp, block }))
149        })()
150        .transpose()
151    }
152}
153
154/// Error that may be thrown by [`FilterIter`].
155#[derive(Debug)]
156pub enum Error {
157    /// RPC error
158    Rpc(bitcoincore_rpc::Error),
159    /// `bitcoin::bip158` error
160    Bip158(bitcoin::bip158::Error),
161    /// Max reorg depth exceeded.
162    ReorgDepthExceeded,
163    /// Error converting an integer
164    TryFromInt(core::num::TryFromIntError),
165}
166
167impl core::fmt::Display for Error {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        match self {
170            Self::Rpc(e) => write!(f, "{e}"),
171            Self::Bip158(e) => write!(f, "{e}"),
172            Self::ReorgDepthExceeded => write!(f, "maximum reorg depth exceeded"),
173            Self::TryFromInt(e) => write!(f, "{e}"),
174        }
175    }
176}
177
178#[cfg(feature = "std")]
179impl std::error::Error for Error {}
180
181impl From<bitcoincore_rpc::Error> for Error {
182    fn from(e: bitcoincore_rpc::Error) -> Self {
183        Self::Rpc(e)
184    }
185}
186
187impl From<core::num::TryFromIntError> for Error {
188    fn from(e: core::num::TryFromIntError) -> Self {
189        Self::TryFromInt(e)
190    }
191}
192
193/// Whether the RPC error is a "not found" error (code: `-5`).
194fn is_not_found(e: &bitcoincore_rpc::Error) -> bool {
195    matches!(
196        e,
197        bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(e))
198        if e.code == -5
199    )
200}