]> Untitled Git - bdk/commitdiff
[bdk_chain_redesign] `chain_oracle::Cache`
author志宇 <hello@evanlinjin.me>
Wed, 5 Apr 2023 02:57:26 +0000 (10:57 +0800)
committer志宇 <hello@evanlinjin.me>
Wed, 5 Apr 2023 02:57:26 +0000 (10:57 +0800)
Introduce `chain_oracle::Cache` which is a cache for requests to the
chain oracle. `ChainOracle` has also been moved to the `chain_oracle`
module.

Introduce `get_tip_in_best_chain` method to the `ChainOracle` trait.
This allows for guaranteeing that chain state can be consistent across
operations with `IndexedTxGraph`.

crates/chain/src/chain_oracle.rs [new file with mode: 0644]
crates/chain/src/lib.rs
crates/chain/src/local_chain.rs
crates/chain/src/sparse_chain.rs
crates/chain/src/tx_data_traits.rs
crates/chain/src/tx_graph.rs

diff --git a/crates/chain/src/chain_oracle.rs b/crates/chain/src/chain_oracle.rs
new file mode 100644 (file)
index 0000000..ccf3bc0
--- /dev/null
@@ -0,0 +1,162 @@
+use core::{convert::Infallible, marker::PhantomData};
+
+use alloc::collections::BTreeMap;
+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.
+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)
+    }
+}
+
+/// 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>,
+    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)
+    }
+
+    /// Update the `tip_height` with the [`ChainOracle`]'s tip.
+    ///
+    /// `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(())
+    }
+
+    /// 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));
+        }
+
+        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")
+    }
+
+    /// 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")
+    }
+
+    /// 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")
+    }
+}
index 9319d4accb8e38460c33056148b9a86df22f76da..265276234163d772841d86fe85b12a704667e11e 100644 (file)
@@ -31,6 +31,8 @@ pub mod sparse_chain;
 mod tx_data_traits;
 pub mod tx_graph;
 pub use tx_data_traits::*;
+mod chain_oracle;
+pub use chain_oracle::*;
 
 #[doc(hidden)]
 pub mod example_utils;
index 5d459a15fd0779526daa4622ecc2a9aef99607a0..a1ca921b98b4ee58c7bb5a60fc29bd4e22914c42 100644 (file)
@@ -22,6 +22,14 @@ 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())
     }
@@ -153,7 +161,7 @@ impl Deref for ChangeSet {
     }
 }
 
-/// Represents an update failure of [`LocalChain`].j
+/// Represents an update failure of [`LocalChain`].
 #[derive(Clone, Debug, PartialEq)]
 pub enum UpdateError {
     /// The update cannot be applied to the chain because the chain suffix it represents did not
index eb6e3e2ade4b4865976c19e6ed5888439143075f..7f0b67e50d8f5e1da6f47911e5ca6295efddd51f 100644 (file)
 //! );
 //! ```
 use core::{
+    convert::Infallible,
     fmt::Debug,
     ops::{Bound, RangeBounds},
 };
@@ -457,7 +458,15 @@ impl<P: core::fmt::Debug> core::fmt::Display for UpdateError<P> {
 impl<P: core::fmt::Debug> std::error::Error for UpdateError<P> {}
 
 impl<P: ChainPosition> ChainOracle for SparseChain<P> {
-    type Error = ();
+    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))
index 0e2474c4433c61047c3a82bd59ef44255bfaa19f..366fc34b8526625b8c78ce7ec2173add5070724c 100644 (file)
@@ -56,38 +56,6 @@ impl BlockAnchor for (u32, BlockHash) {
     }
 }
 
-/// 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.
-pub trait ChainOracle {
-    /// Error type.
-    type Error: core::fmt::Debug;
-
-    /// 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_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)
-    }
-}
-
 /// Represents an index of transaction data.
 pub trait TxIndex {
     /// The resultant "additions" when new transaction data is indexed.
index bbdd7bdbe4de69e3c630bc5476c66030c806e7a0..893060ae3447b45a207928a2929ea12c25d356a9 100644 (file)
@@ -586,11 +586,12 @@ impl<A: Clone + Ord> TxGraph<A> {
 
 impl<A: BlockAnchor> TxGraph<A> {
     /// Get all heights that are relevant to the graph.
-    pub fn relevant_heights(&self) -> BTreeSet<u32> {
+    pub fn relevant_heights(&self) -> impl Iterator<Item = u32> + '_ {
+        let mut visited = HashSet::new();
         self.anchors
             .iter()
             .map(|(a, _)| a.anchor_block().height)
-            .collect()
+            .filter(move |&h| visited.insert(h))
     }
 
     /// Determines whether a transaction of `txid` is in the best chain.