]> Untitled Git - bdk/commitdiff
[bdk_chain_redesign] Initial work on `LocalChain`.
author志宇 <hello@evanlinjin.me>
Wed, 29 Mar 2023 14:45:01 +0000 (22:45 +0800)
committer志宇 <hello@evanlinjin.me>
Thu, 30 Mar 2023 05:07:13 +0000 (13:07 +0800)
crates/chain/src/chain_data.rs
crates/chain/src/indexed_tx_graph.rs
crates/chain/src/lib.rs
crates/chain/src/local_chain.rs [new file with mode: 0644]
crates/chain/src/tx_data_traits.rs

index df5a5e9c76071337d509ef1e116863a19b249706..6c1c2c3a68c95d044e68d89c6342dadceef7c9f2 100644 (file)
@@ -11,6 +11,7 @@ pub enum ObservedIn<A> {
     /// The chain data is seen in a block identified by `A`.
     Block(A),
     /// The chain data is seen in mempool at this given timestamp.
+    /// TODO: Call this `Unconfirmed`.
     Mempool(u64),
 }
 
index b2b206cfcad0bdeee11b2034cb61bc2f94e39593..21852150a2b3beafd96342d08985be1f663aaf01 100644 (file)
@@ -68,7 +68,7 @@ impl<A: BlockAnchor, D: TxIndexAdditions> TxIndexAdditions for IndexedAdditions<
 
 pub struct IndexedTxGraph<A, I> {
     graph: TxGraph<A>,
-    index: I,
+    index: I, // [TODO] Make public
     last_height: u32,
 }
 
@@ -219,6 +219,7 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
         self.last_height
     }
 
+    // [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,
index 5284409790e4279f51d1d475a53d44e5ae31965d..9319d4accb8e38460c33056148b9a86df22f76da 100644 (file)
@@ -26,6 +26,7 @@ mod chain_data;
 pub use chain_data::*;
 pub mod indexed_tx_graph;
 pub mod keychain;
+pub mod local_chain;
 pub mod sparse_chain;
 mod tx_data_traits;
 pub mod tx_graph;
diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs
new file mode 100644 (file)
index 0000000..5bcb524
--- /dev/null
@@ -0,0 +1,140 @@
+use core::convert::Infallible;
+
+use alloc::{collections::BTreeMap, vec::Vec};
+use bitcoin::BlockHash;
+
+use crate::{BlockId, ChainOracle};
+
+#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct LocalChain {
+    blocks: BTreeMap<u32, BlockHash>,
+}
+
+// [TODO] We need a cache/snapshot thing for chain oracle.
+// * Minimize calls to remotes.
+// * Can we cache it forever? Should we drop stuff?
+// * Assume anything deeper than (i.e. 10) blocks won't be reorged.
+// * Is this a cache on txs or block? or both?
+// [TODO] Parents of children are confirmed if children are confirmed.
+impl ChainOracle for LocalChain {
+    type Error = Infallible;
+
+    fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
+        Ok(self.blocks.get(&height).cloned())
+    }
+}
+
+impl AsRef<BTreeMap<u32, BlockHash>> for LocalChain {
+    fn as_ref(&self) -> &BTreeMap<u32, BlockHash> {
+        &self.blocks
+    }
+}
+
+impl From<LocalChain> for BTreeMap<u32, BlockHash> {
+    fn from(value: LocalChain) -> Self {
+        value.blocks
+    }
+}
+
+impl LocalChain {
+    pub fn tip(&self) -> Option<BlockId> {
+        self.blocks
+            .iter()
+            .last()
+            .map(|(&height, &hash)| BlockId { height, hash })
+    }
+
+    /// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights
+    /// are to be re-filled.
+    pub fn determine_changeset<U>(&self, update: &U) -> Result<ChangeSet, UpdateError>
+    where
+        U: AsRef<BTreeMap<u32, BlockHash>>,
+    {
+        let update = update.as_ref();
+        let update_tip = match update.keys().last().cloned() {
+            Some(tip) => tip,
+            None => return Ok(ChangeSet::default()),
+        };
+
+        // this is the latest height where both the update and local chain has the same block hash
+        let agreement_height = update
+            .iter()
+            .rev()
+            .find(|&(u_height, u_hash)| self.blocks.get(u_height) == Some(u_hash))
+            .map(|(&height, _)| height);
+
+        // the lower bound of the range to invalidate
+        let invalidate_lb = match agreement_height {
+            Some(height) if height == update_tip => u32::MAX,
+            Some(height) => height + 1,
+            None => 0,
+        };
+
+        // the first block's height to invalidate in the local chain
+        let invalidate_from = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h);
+
+        // the first block of height to invalidate (if any) should be represented in the update
+        if let Some(first_invalid) = invalidate_from {
+            if !update.contains_key(&first_invalid) {
+                return Err(UpdateError::NotConnected(first_invalid));
+            }
+        }
+
+        let invalidated_heights = invalidate_from
+            .into_iter()
+            .flat_map(|from_height| self.blocks.range(from_height..).map(|(h, _)| h));
+
+        // invalidated heights must all exist in the update
+        let mut missing_heights = Vec::<u32>::new();
+        for invalidated_height in invalidated_heights {
+            if !update.contains_key(invalidated_height) {
+                missing_heights.push(*invalidated_height);
+            }
+        }
+        if !missing_heights.is_empty() {
+            return Err(UpdateError::MissingHeightsInUpdate(missing_heights));
+        }
+
+        let mut changeset = BTreeMap::<u32, BlockHash>::new();
+        for (height, new_hash) in update {
+            let original_hash = self.blocks.get(height);
+            if Some(new_hash) != original_hash {
+                changeset.insert(*height, *new_hash);
+            }
+        }
+        Ok(ChangeSet(changeset))
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct ChangeSet(pub BTreeMap<u32, BlockHash>);
+
+/// Represents an update failure of [`LocalChain`].j
+#[derive(Clone, Debug, PartialEq)]
+pub enum UpdateError {
+    /// The update cannot be applied to the chain because the chain suffix it represents did not
+    /// connect to the existing chain. This error case contains the checkpoint height to include so
+    /// that the chains can connect.
+    NotConnected(u32),
+    /// If the update results in displacements of original blocks, the update should include all new
+    /// block hashes that have displaced the original block hashes. This error case contains the
+    /// heights of all missing block hashes in the update.
+    MissingHeightsInUpdate(Vec<u32>),
+}
+
+impl core::fmt::Display for UpdateError {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            UpdateError::NotConnected(heights) => write!(
+                f,
+                "the update cannot connect with the chain, try include blockhash at height {}",
+                heights
+            ),
+            UpdateError::MissingHeightsInUpdate(missing_heights) => write!(
+                f,
+                "block hashes of these heights must be included in the update to succeed: {:?}",
+                missing_heights
+            ),
+        }
+    }
+}
index 2ffb9a608ecd920209b4d771e54d5c4fef74fb30..485e3f70398d7c3cd459b94b3630960ce3dbcb36 100644 (file)
@@ -58,6 +58,10 @@ 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;
@@ -71,6 +75,8 @@ pub trait ChainOracle {
     }
 }
 
+// [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;