]> Untitled Git - bdk/commitdiff
[bdk_chain_redesign] Consistent `ChainOracle`
author志宇 <hello@evanlinjin.me>
Mon, 10 Apr 2023 05:03:51 +0000 (13:03 +0800)
committer志宇 <hello@evanlinjin.me>
Mon, 10 Apr 2023 05:03:51 +0000 (13:03 +0800)
The problem with the previous `ChainOracle` interface is that it had no
guarantee for consistency. For example, a block deemed to be part of the
"best chain" can be reorged out. So when `ChainOracle` is called
multiple times for an operation (such as getting the UTXO set), the
returned result may be inconsistent.

This PR changes `ChainOracle::is_block_in_chain` to take in another
input `static_block`, ensuring `block` is an ancestor of `static_block`.
Thus, if `static_block` is consistent across the operation, the result
will be consistent also.

`is_block_in_chain` now returns `Option<bool>`. The `None` case means
that the oracle implementation cannot determine whether block is an
ancestor of static block. `IndexedTxGraph::list_chain_txouts` handles
this case by checking child spends that are in chain, and if so, the
parent tx must be in chain too.

crates/chain/src/chain_data.rs
crates/chain/src/chain_oracle.rs
crates/chain/src/indexed_tx_graph.rs
crates/chain/src/local_chain.rs
crates/chain/src/sparse_chain.rs
crates/chain/src/tx_graph.rs

index 85f9107c1209456ba0dc3490bba78cedfee05b4a..5615b0947047dd372f86b7463bb456bcb1db35b9 100644 (file)
@@ -248,7 +248,7 @@ impl<A: BlockAnchor> FullTxOut<ObservedAs<A>> {
     /// [`ObservedAs<A>`] where `A` implements [`BlockAnchor`].
     ///
     /// [`is_mature`]: Self::is_mature
-    pub fn is_observed_as_mature(&self, tip: u32) -> bool {
+    pub fn is_observed_as_confirmed_and_mature(&self, tip: u32) -> bool {
         if !self.is_on_coinbase {
             return false;
         }
@@ -275,8 +275,8 @@ impl<A: BlockAnchor> FullTxOut<ObservedAs<A>> {
     /// being a [`ObservedAs<A>`] where `A` implements [`BlockAnchor`].
     ///
     /// [`is_spendable_at`]: Self::is_spendable_at
-    pub fn is_observed_as_spendable(&self, tip: u32) -> bool {
-        if !self.is_observed_as_mature(tip) {
+    pub fn is_observed_as_confirmed_and_spendable(&self, tip: u32) -> bool {
+        if !self.is_observed_as_confirmed_and_mature(tip) {
             return false;
         }
 
index ccf3bc09a57acc78345fd793fdb0aed62e0206ca..7e975ad2345d41c013bcc2c456ceefb462887ead 100644 (file)
-use core::{convert::Infallible, marker::PhantomData};
+use crate::collections::HashSet;
+use core::marker::PhantomData;
 
-use alloc::collections::BTreeMap;
+use alloc::{collections::VecDeque, vec::Vec};
 use bitcoin::BlockHash;
 
 use crate::BlockId;
 
-/// Represents a service that tracks the best chain history.
-/// TODO: How do we ensure the chain oracle is consistent across a single call?
-/// * We need to somehow lock the data! What if the ChainOracle is remote?
-/// * Get tip method! And check the tip still exists at the end! And every internal call
-///   does not go beyond the initial tip.
+/// Represents a service that tracks the blockchain.
+///
+/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`]
+/// is an ancestor of another "static block".
+///
+/// [`is_block_in_chain`]: Self::is_block_in_chain
 pub trait ChainOracle {
     /// Error type.
     type Error: core::fmt::Debug;
 
-    /// Get the height and hash of the tip in the best chain.
-    fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error>;
-
-    /// Returns the block hash (if any) of the given `height`.
-    fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error>;
-
-    /// Determines whether the block of [`BlockId`] exists in the best chain.
-    fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
-        Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash))
-    }
-}
-
-// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this?
-// Box<dyn ChainOracle>, Arc<dyn ChainOracle> ????? I will figure it out
-impl<C: ChainOracle> ChainOracle for &C {
-    type Error = C::Error;
-
-    fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error> {
-        <C as ChainOracle>::get_tip_in_best_chain(self)
-    }
-
-    fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
-        <C as ChainOracle>::get_block_in_best_chain(self, height)
-    }
-
-    fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
-        <C as ChainOracle>::is_block_in_best_chain(self, block_id)
-    }
+    /// Determines whether `block` of [`BlockId`] exists as an ancestor of `static_block`.
+    ///
+    /// If `None` is returned, it means the implementation cannot determine whether `block` exists.
+    fn is_block_in_chain(
+        &self,
+        block: BlockId,
+        static_block: BlockId,
+    ) -> Result<Option<bool>, Self::Error>;
 }
 
-/// This structure increases the performance of getting chain data.
-#[derive(Debug)]
-pub struct Cache<C> {
-    assume_final_depth: u32,
-    tip_height: u32,
-    cache: BTreeMap<u32, BlockHash>,
+/// A cache structure increases the performance of getting chain data.
+///
+/// A simple FIFO cache replacement policy is used. Something more efficient and advanced can be
+/// implemented later.
+#[derive(Debug, Default)]
+pub struct CacheBackend<C> {
+    cache: HashSet<(BlockHash, BlockHash)>,
+    fifo: VecDeque<(BlockHash, BlockHash)>,
     marker: PhantomData<C>,
 }
 
-impl<C> Cache<C> {
-    /// Creates a new [`Cache`].
-    ///
-    /// `assume_final_depth` represents the minimum number of blocks above the block in question
-    /// when we can assume the block is final (reorgs cannot happen). I.e. a value of 0 means the
-    /// tip is assumed to be final. The cache only caches blocks that are assumed to be final.
-    pub fn new(assume_final_depth: u32) -> Self {
-        Self {
-            assume_final_depth,
-            tip_height: 0,
-            cache: Default::default(),
-            marker: Default::default(),
-        }
-    }
-}
-
-impl<C: ChainOracle> Cache<C> {
-    /// This is the topmost (highest) block height that we assume as final (no reorgs possible).
-    ///
-    /// Blocks higher than this height are not cached.
-    pub fn assume_final_height(&self) -> u32 {
-        self.tip_height.saturating_sub(self.assume_final_depth)
+impl<C> CacheBackend<C> {
+    /// Get the number of elements in the cache.
+    pub fn cache_size(&self) -> usize {
+        self.cache.len()
     }
 
-    /// Update the `tip_height` with the [`ChainOracle`]'s tip.
+    /// Prunes the cache to reach the `max_size` target.
     ///
-    /// `tip_height` is used with `assume_final_depth` to determine whether we should cache a
-    /// certain block height (`tip_height` - `assume_final_depth`).
-    pub fn try_update_tip_height(&mut self, chain: C) -> Result<(), C::Error> {
-        let tip = chain.get_tip_in_best_chain()?;
-        if let Some(BlockId { height, .. }) = tip {
-            self.tip_height = height;
-        }
-        Ok(())
+    /// Returns pruned elements.
+    pub fn prune(&mut self, max_size: usize) -> Vec<(BlockHash, BlockHash)> {
+        let prune_count = self.cache.len().saturating_sub(max_size);
+        (0..prune_count)
+            .filter_map(|_| self.fifo.pop_front())
+            .filter(|k| self.cache.remove(k))
+            .collect()
     }
 
-    /// Get a block from the cache with the [`ChainOracle`] as fallback.
-    ///
-    /// If the block does not exist in cache, the logic fallbacks to fetching from the internal
-    /// [`ChainOracle`]. If the block is at or below the "assume final height", we will also store
-    /// the missing block in the cache.
-    pub fn try_get_block(&mut self, chain: C, height: u32) -> Result<Option<BlockHash>, C::Error> {
-        if let Some(&hash) = self.cache.get(&height) {
-            return Ok(Some(hash));
+    pub fn contains(&self, static_block: BlockId, block: BlockId) -> bool {
+        if static_block.height < block.height
+            || static_block.height == block.height && static_block.hash != block.hash
+        {
+            return false;
         }
 
-        let hash = chain.get_block_in_best_chain(height)?;
-
-        if hash.is_some() && height > self.tip_height {
-            self.tip_height = height;
-        }
-
-        // only cache block if at least as deep as `assume_final_depth`
-        let assume_final_height = self.tip_height.saturating_sub(self.assume_final_depth);
-        if height <= assume_final_height {
-            if let Some(hash) = hash {
-                self.cache.insert(height, hash);
-            }
-        }
-
-        Ok(hash)
-    }
-
-    /// Determines whether the block of `block_id` is in the chain using the cache.
-    ///
-    /// This uses [`try_get_block`] internally.
-    ///
-    /// [`try_get_block`]: Self::try_get_block
-    pub fn try_is_block_in_chain(&mut self, chain: C, block_id: BlockId) -> Result<bool, C::Error> {
-        match self.try_get_block(chain, block_id.height)? {
-            Some(hash) if hash == block_id.hash => Ok(true),
-            _ => Ok(false),
-        }
-    }
-}
-
-impl<C: ChainOracle<Error = Infallible>> Cache<C> {
-    /// Updates the `tip_height` with the [`ChainOracle`]'s tip.
-    ///
-    /// This is the no-error version of [`try_update_tip_height`].
-    ///
-    /// [`try_update_tip_height`]: Self::try_update_tip_height
-    pub fn update_tip_height(&mut self, chain: C) {
-        self.try_update_tip_height(chain)
-            .expect("chain oracle error is infallible")
+        self.cache.contains(&(static_block.hash, block.hash))
     }
 
-    /// Get a block from the cache with the [`ChainOracle`] as fallback.
-    ///
-    /// This is the no-error version of [`try_get_block`].
-    ///
-    /// [`try_get_block`]: Self::try_get_block
-    pub fn get_block(&mut self, chain: C, height: u32) -> Option<BlockHash> {
-        self.try_get_block(chain, height)
-            .expect("chain oracle error is infallible")
-    }
+    pub fn insert(&mut self, static_block: BlockId, block: BlockId) -> bool {
+        let cache_key = (static_block.hash, block.hash);
 
-    /// Determines whether the block at `block_id` is in the chain using the cache.
-    ///
-    /// This is the no-error version of [`try_is_block_in_chain`].
-    ///
-    /// [`try_is_block_in_chain`]: Self::try_is_block_in_chain
-    pub fn is_block_in_best_chain(&mut self, chain: C, block_id: BlockId) -> bool {
-        self.try_is_block_in_chain(chain, block_id)
-            .expect("chain oracle error is infallible")
+        if self.cache.insert(cache_key) {
+            self.fifo.push_back(cache_key);
+            true
+        } else {
+            false
+        }
     }
 }
index f50f454b3a6d22c410609d447cf2e1d4e2363674..dac05e72562bd098e100306bb42e3344d26b6f04 100644 (file)
@@ -5,7 +5,7 @@ use bitcoin::{OutPoint, Script, Transaction, TxOut};
 use crate::{
     keychain::Balance,
     tx_graph::{Additions, TxGraph, TxNode},
-    Append, BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex,
+    Append, BlockAnchor, BlockId, ChainOracle, FullTxOut, ObservedAs, TxIndex,
 };
 
 /// An outwards-facing view of a transaction that is part of the *best chain*'s history.
@@ -220,7 +220,8 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
     // [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`.
     pub fn try_list_chain_txs<'a, C>(
         &'a self,
-        chain: C,
+        chain: &'a C,
+        static_block: BlockId,
     ) -> impl Iterator<Item = Result<CanonicalTx<'a, Transaction, A>, C::Error>>
     where
         C: ChainOracle + 'a,
@@ -230,7 +231,7 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
             .filter(|tx| self.index.is_tx_relevant(tx))
             .filter_map(move |tx| {
                 self.graph
-                    .try_get_chain_position(&chain, tx.txid)
+                    .try_get_chain_position(chain, static_block, tx.txid)
                     .map(|v| {
                         v.map(|observed_in| CanonicalTx {
                             observed_as: observed_in,
@@ -243,18 +244,20 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
 
     pub fn list_chain_txs<'a, C>(
         &'a self,
-        chain: C,
+        chain: &'a C,
+        static_block: BlockId,
     ) -> impl Iterator<Item = CanonicalTx<'a, Transaction, A>>
     where
         C: ChainOracle<Error = Infallible> + 'a,
     {
-        self.try_list_chain_txs(chain)
+        self.try_list_chain_txs(chain, static_block)
             .map(|r| r.expect("error is infallible"))
     }
 
     pub fn try_list_chain_txouts<'a, C>(
         &'a self,
-        chain: C,
+        chain: &'a C,
+        static_block: BlockId,
     ) -> impl Iterator<Item = Result<FullTxOut<ObservedAs<A>>, C::Error>> + 'a
     where
         C: ChainOracle + 'a,
@@ -267,13 +270,17 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
 
                 let is_on_coinbase = graph_tx.is_coin_base();
 
-                let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) {
-                    Ok(Some(observed_at)) => observed_at.cloned(),
-                    Ok(None) => return None,
-                    Err(err) => return Some(Err(err)),
-                };
-
-                let spent_by = match self.graph.try_get_spend_in_chain(&chain, op) {
+                let chain_position =
+                    match self
+                        .graph
+                        .try_get_chain_position(chain, static_block, op.txid)
+                    {
+                        Ok(Some(observed_at)) => observed_at.cloned(),
+                        Ok(None) => return None,
+                        Err(err) => return Some(Err(err)),
+                    };
+
+                let spent_by = match self.graph.try_get_spend_in_chain(chain, static_block, op) {
                     Ok(Some((obs, txid))) => Some((obs.cloned(), txid)),
                     Ok(None) => None,
                     Err(err) => return Some(Err(err)),
@@ -293,41 +300,45 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
 
     pub fn list_chain_txouts<'a, C>(
         &'a self,
-        chain: C,
+        chain: &'a C,
+        static_block: BlockId,
     ) -> impl Iterator<Item = FullTxOut<ObservedAs<A>>> + 'a
     where
         C: ChainOracle<Error = Infallible> + 'a,
     {
-        self.try_list_chain_txouts(chain)
+        self.try_list_chain_txouts(chain, static_block)
             .map(|r| r.expect("error in infallible"))
     }
 
     /// Return relevant unspents.
     pub fn try_list_chain_utxos<'a, C>(
         &'a self,
-        chain: C,
+        chain: &'a C,
+        static_block: BlockId,
     ) -> impl Iterator<Item = Result<FullTxOut<ObservedAs<A>>, C::Error>> + 'a
     where
         C: ChainOracle + 'a,
     {
-        self.try_list_chain_txouts(chain)
+        self.try_list_chain_txouts(chain, static_block)
             .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none()))
     }
 
     pub fn list_chain_utxos<'a, C>(
         &'a self,
-        chain: C,
+        chain: &'a C,
+        static_block: BlockId,
     ) -> impl Iterator<Item = FullTxOut<ObservedAs<A>>> + 'a
     where
         C: ChainOracle<Error = Infallible> + 'a,
     {
-        self.try_list_chain_utxos(chain)
+        self.try_list_chain_utxos(chain, static_block)
             .map(|r| r.expect("error is infallible"))
     }
 
     pub fn try_balance<C, F>(
         &self,
-        chain: C,
+        chain: &C,
+        static_block: BlockId,
         tip: u32,
         mut should_trust: F,
     ) -> Result<Balance, C::Error>
@@ -340,13 +351,13 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
         let mut untrusted_pending = 0;
         let mut confirmed = 0;
 
-        for res in self.try_list_chain_txouts(&chain) {
+        for res in self.try_list_chain_txouts(chain, static_block) {
             let txout = res?;
 
             match &txout.chain_position {
                 ObservedAs::Confirmed(_) => {
                     if txout.is_on_coinbase {
-                        if txout.is_observed_as_mature(tip) {
+                        if txout.is_observed_as_confirmed_and_mature(tip) {
                             confirmed += txout.txout.value;
                         } else {
                             immature += txout.txout.value;
@@ -371,34 +382,45 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
         })
     }
 
-    pub fn balance<C, F>(&self, chain: C, tip: u32, should_trust: F) -> Balance
+    pub fn balance<C, F>(
+        &self,
+        chain: &C,
+        static_block: BlockId,
+        tip: u32,
+        should_trust: F,
+    ) -> Balance
     where
         C: ChainOracle<Error = Infallible>,
         F: FnMut(&Script) -> bool,
     {
-        self.try_balance(chain, tip, should_trust)
+        self.try_balance(chain, static_block, tip, should_trust)
             .expect("error is infallible")
     }
 
-    pub fn try_balance_at<C>(&self, chain: C, height: u32) -> Result<u64, C::Error>
+    pub fn try_balance_at<C>(
+        &self,
+        chain: &C,
+        static_block: BlockId,
+        height: u32,
+    ) -> Result<u64, C::Error>
     where
         C: ChainOracle,
     {
         let mut sum = 0;
-        for txo_res in self.try_list_chain_txouts(chain) {
+        for txo_res in self.try_list_chain_txouts(chain, static_block) {
             let txo = txo_res?;
-            if txo.is_observed_as_spendable(height) {
+            if txo.is_observed_as_confirmed_and_spendable(height) {
                 sum += txo.txout.value;
             }
         }
         Ok(sum)
     }
 
-    pub fn balance_at<C>(&self, chain: C, height: u32) -> u64
+    pub fn balance_at<C>(&self, chain: &C, static_block: BlockId, height: u32) -> u64
     where
         C: ChainOracle<Error = Infallible>,
     {
-        self.try_balance_at(chain, height)
+        self.try_balance_at(chain, static_block, height)
             .expect("error is infallible")
     }
 }
index e1b24ad072fd823550f0e9af5aae9c1fe0442a5a..20b54a2f2f8df4847ca76b866acbd805a74e27db 100644 (file)
@@ -22,16 +22,25 @@ pub struct LocalChain {
 impl ChainOracle for LocalChain {
     type Error = Infallible;
 
-    fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error> {
-        Ok(self
-            .blocks
-            .iter()
-            .last()
-            .map(|(&height, &hash)| BlockId { height, hash }))
-    }
-
-    fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
-        Ok(self.blocks.get(&height).cloned())
+    fn is_block_in_chain(
+        &self,
+        block: BlockId,
+        static_block: BlockId,
+    ) -> Result<Option<bool>, Self::Error> {
+        if block.height > static_block.height {
+            return Ok(None);
+        }
+        Ok(
+            match (
+                self.blocks.get(&block.height),
+                self.blocks.get(&static_block.height),
+            ) {
+                (Some(&hash), Some(&static_hash)) => {
+                    Some(hash == block.hash && static_hash == static_block.hash)
+                }
+                _ => None,
+            },
+        )
     }
 }
 
index b615f4aac464869d8b1a9b62944714bffbf47674..acc61601518bbcbbc3ba966f30797f7296b6ded6 100644 (file)
@@ -460,16 +460,20 @@ impl<P: core::fmt::Debug> std::error::Error for UpdateError<P> {}
 impl<P> ChainOracle for SparseChain<P> {
     type Error = Infallible;
 
-    fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error> {
-        Ok(self
-            .checkpoints
-            .iter()
-            .last()
-            .map(|(&height, &hash)| BlockId { height, hash }))
-    }
-
-    fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
-        Ok(self.checkpoint_at(height).map(|b| b.hash))
+    fn is_block_in_chain(
+        &self,
+        block: BlockId,
+        static_block: BlockId,
+    ) -> Result<Option<bool>, Self::Error> {
+        Ok(
+            match (
+                self.checkpoint_at(block.height),
+                self.checkpoint_at(static_block.height),
+            ) {
+                (Some(b), Some(static_b)) => Some(b == block && static_b == static_block),
+                _ => None,
+            },
+        )
     }
 }
 
index 0959456d15255686b7b0e662ec1c4e26a7831d41..e3afce0e9afdfdb39c0d73952af3fed970da46be 100644 (file)
@@ -55,7 +55,7 @@
 //! assert!(additions.is_empty());
 //! ```
 
-use crate::{collections::*, BlockAnchor, ChainOracle, ForEachTxOut, ObservedAs};
+use crate::{collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, ObservedAs};
 use alloc::vec::Vec;
 use bitcoin::{OutPoint, Transaction, TxOut, Txid};
 use core::{
@@ -596,7 +596,8 @@ impl<A: BlockAnchor> TxGraph<A> {
     /// TODO: Also return conflicting tx list, ordered by last_seen.
     pub fn try_get_chain_position<C>(
         &self,
-        chain: C,
+        chain: &C,
+        static_block: BlockId,
         txid: Txid,
     ) -> Result<Option<ObservedAs<&A>>, C::Error>
     where
@@ -610,8 +611,28 @@ impl<A: BlockAnchor> TxGraph<A> {
         };
 
         for anchor in anchors {
-            if chain.is_block_in_best_chain(anchor.anchor_block())? {
-                return Ok(Some(ObservedAs::Confirmed(anchor)));
+            match chain.is_block_in_chain(anchor.anchor_block(), static_block)? {
+                Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))),
+                Some(false) => continue,
+                // if we cannot determine whether block is in the best chain, we can check whether
+                // a spending transaction is confirmed in best chain, and if so, it is guaranteed
+                // that the tx being spent (this tx) is in the best chain
+                None => {
+                    let spending_anchors = self
+                        .spends
+                        .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX))
+                        .flat_map(|(_, spending_txids)| spending_txids)
+                        .filter_map(|spending_txid| self.txs.get(spending_txid))
+                        .flat_map(|(_, spending_anchors, _)| spending_anchors);
+                    for spending_anchor in spending_anchors {
+                        match chain
+                            .is_block_in_chain(spending_anchor.anchor_block(), static_block)?
+                        {
+                            Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))),
+                            _ => continue,
+                        }
+                    }
+                }
             }
         }
 
@@ -620,8 +641,7 @@ impl<A: BlockAnchor> TxGraph<A> {
         let tx = match tx_node {
             TxNodeInternal::Whole(tx) => tx,
             TxNodeInternal::Partial(_) => {
-                // [TODO] Unfortunately, we can't iterate over conflicts of partial txs right now!
-                // [TODO] So we just assume the partial tx does not exist in the best chain :/
+                // Partial transactions (outputs only) cannot have conflicts.
                 return Ok(None);
             }
         };
@@ -629,8 +649,8 @@ impl<A: BlockAnchor> TxGraph<A> {
         // If a conflicting tx is in the best chain, or has `last_seen` higher than this tx, then
         // this tx cannot exist in the best chain
         for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) {
-            for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) {
-                if chain.is_block_in_best_chain(block_id)? {
+            for block in conflicting_tx.anchors.iter().map(A::anchor_block) {
+                if chain.is_block_in_chain(block, static_block)? == Some(true) {
                     // conflicting tx is in best chain, so the current tx cannot be in best chain!
                     return Ok(None);
                 }
@@ -643,31 +663,37 @@ impl<A: BlockAnchor> TxGraph<A> {
         Ok(Some(ObservedAs::Unconfirmed(last_seen)))
     }
 
-    pub fn get_chain_position<C>(&self, chain: C, txid: Txid) -> Option<ObservedAs<&A>>
+    pub fn get_chain_position<C>(
+        &self,
+        chain: &C,
+        static_block: BlockId,
+        txid: Txid,
+    ) -> Option<ObservedAs<&A>>
     where
         C: ChainOracle<Error = Infallible>,
     {
-        self.try_get_chain_position(chain, txid)
+        self.try_get_chain_position(chain, static_block, txid)
             .expect("error is infallible")
     }
 
     pub fn try_get_spend_in_chain<C>(
         &self,
-        chain: C,
+        chain: &C,
+        static_block: BlockId,
         outpoint: OutPoint,
     ) -> Result<Option<(ObservedAs<&A>, Txid)>, C::Error>
     where
         C: ChainOracle,
     {
         if self
-            .try_get_chain_position(&chain, outpoint.txid)?
+            .try_get_chain_position(chain, static_block, outpoint.txid)?
             .is_none()
         {
             return Ok(None);
         }
         if let Some(spends) = self.spends.get(&outpoint) {
             for &txid in spends {
-                if let Some(observed_at) = self.try_get_chain_position(&chain, txid)? {
+                if let Some(observed_at) = self.try_get_chain_position(chain, static_block, txid)? {
                     return Ok(Some((observed_at, txid)));
                 }
             }
@@ -675,11 +701,16 @@ impl<A: BlockAnchor> TxGraph<A> {
         Ok(None)
     }
 
-    pub fn get_chain_spend<C>(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedAs<&A>, Txid)>
+    pub fn get_chain_spend<C>(
+        &self,
+        chain: &C,
+        static_block: BlockId,
+        outpoint: OutPoint,
+    ) -> Option<(ObservedAs<&A>, Txid)>
     where
         C: ChainOracle<Error = Infallible>,
     {
-        self.try_get_spend_in_chain(chain, outpoint)
+        self.try_get_spend_in_chain(chain, static_block, outpoint)
             .expect("error is infallible")
     }
 }