]> Untitled Git - bdk/commitdiff
[bdk_chain_redesign] Introduce `ChainOracle` and `TxIndex` traits
author志宇 <hello@evanlinjin.me>
Fri, 24 Mar 2023 07:47:39 +0000 (15:47 +0800)
committer志宇 <hello@evanlinjin.me>
Sun, 26 Mar 2023 03:03:35 +0000 (11:03 +0800)
The chain oracle keeps track of the best chain, while the transaction
index indexes transaction data in relation to script pubkeys.

This commit also includes initial work on `IndexedTxGraph`.

crates/chain/src/chain_data.rs
crates/chain/src/keychain.rs
crates/chain/src/keychain/txout_index.rs
crates/chain/src/sparse_chain.rs
crates/chain/src/spk_txout_index.rs
crates/chain/src/tx_data_traits.rs
crates/chain/src/tx_graph.rs

index ec76dbb7dba3882ce03e1e2b170f4c8cd6116487..147ce2402736f8fb9b61a9714807939c1404992a 100644 (file)
@@ -5,6 +5,15 @@ use crate::{
     BlockAnchor, COINBASE_MATURITY,
 };
 
+/// Represents an observation of some chain data.
+#[derive(Debug, Clone, Copy)]
+pub enum Observation<A> {
+    /// The chain data is seen in a block identified by `A`.
+    InBlock(A),
+    /// The chain data is seen at this given unix timestamp.
+    SeenAt(u64),
+}
+
 /// Represents the height at which a transaction is confirmed.
 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(
index 92d72841fa165a0a81a8fd34a80dd2fab692a6e0..dd419db56439addb2e260b3d4918c3275e0d6178 100644 (file)
@@ -19,7 +19,7 @@ use crate::{
     collections::BTreeMap,
     sparse_chain::ChainPosition,
     tx_graph::TxGraph,
-    ForEachTxOut,
+    ForEachTxOut, TxIndexAdditions,
 };
 
 #[cfg(feature = "miniscript")]
@@ -85,6 +85,12 @@ impl<K: Ord> DerivationAdditions<K> {
     }
 }
 
+impl<K: Ord> TxIndexAdditions for DerivationAdditions<K> {
+    fn append_additions(&mut self, other: Self) {
+        self.append(other)
+    }
+}
+
 impl<K> Default for DerivationAdditions<K> {
     fn default() -> Self {
         Self(Default::default())
index feb71edb45abffca489f4276410e31bf31300bcc..b60e0584c27f6c8fdf7502684464c7d84e7daf0a 100644 (file)
@@ -1,7 +1,7 @@
 use crate::{
     collections::*,
     miniscript::{Descriptor, DescriptorPublicKey},
-    ForEachTxOut, SpkTxOutIndex,
+    ForEachTxOut, SpkTxOutIndex, TxIndex,
 };
 use alloc::{borrow::Cow, vec::Vec};
 use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut};
@@ -88,6 +88,22 @@ impl<K> Deref for KeychainTxOutIndex<K> {
     }
 }
 
+impl<K: Clone + Ord + Debug> TxIndex for KeychainTxOutIndex<K> {
+    type Additions = DerivationAdditions<K>;
+
+    fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
+        self.scan_txout(outpoint, txout)
+    }
+
+    fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::Additions {
+        self.scan(tx)
+    }
+
+    fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool {
+        self.is_relevant(tx)
+    }
+}
+
 impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     /// Scans an object for relevant outpoints, which are stored and indexed internally.
     ///
index a449638d894ae4d954de93bcc9bd6bb57176d5b3..eb6e3e2ade4b4865976c19e6ed5888439143075f 100644 (file)
@@ -311,7 +311,7 @@ use core::{
     ops::{Bound, RangeBounds},
 };
 
-use crate::{collections::*, tx_graph::TxGraph, BlockId, FullTxOut, TxHeight};
+use crate::{collections::*, tx_graph::TxGraph, BlockId, ChainOracle, FullTxOut, TxHeight};
 use bitcoin::{hashes::Hash, BlockHash, OutPoint, Txid};
 
 /// This is a non-monotone structure that tracks relevant [`Txid`]s that are ordered by chain
@@ -456,6 +456,14 @@ impl<P: core::fmt::Debug> core::fmt::Display for UpdateError<P> {
 #[cfg(feature = "std")]
 impl<P: core::fmt::Debug> std::error::Error for UpdateError<P> {}
 
+impl<P: ChainPosition> ChainOracle for SparseChain<P> {
+    type Error = ();
+
+    fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
+        Ok(self.checkpoint_at(height).map(|b| b.hash))
+    }
+}
+
 impl<P: ChainPosition> SparseChain<P> {
     /// Creates a new chain from a list of block hashes and heights. The caller must guarantee they
     /// are in the same chain.
index 7f46604fc3bf1677cc1296a5dac26bbfc4f5ed9f..3ce6c06c8c20bc39d3a15d34cebe985a63a76b24 100644 (file)
@@ -2,7 +2,7 @@ use core::ops::RangeBounds;
 
 use crate::{
     collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
-    ForEachTxOut,
+    ForEachTxOut, TxIndex,
 };
 use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid};
 
@@ -52,6 +52,25 @@ impl<I> Default for SpkTxOutIndex<I> {
     }
 }
 
+impl<I: Clone + Ord> TxIndex for SpkTxOutIndex<I> {
+    type Additions = BTreeSet<I>;
+
+    fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
+        self.scan_txout(outpoint, txout)
+            .cloned()
+            .into_iter()
+            .collect()
+    }
+
+    fn index_tx(&mut self, tx: &Transaction) -> Self::Additions {
+        self.scan(tx)
+    }
+
+    fn is_tx_relevant(&self, tx: &Transaction) -> bool {
+        self.is_relevant(tx)
+    }
+}
+
 /// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a
 /// compiler error[E0521]: "borrowed data escapes out of closure" when we attempt to take a
 /// reference out of the `ForEachTxOut` closure during scanning.
index 9b9facabeb936b6533098c48710f7e40db9f7c62..43ce487e067407d52a4ea2ac6ee6b528e58fb79e 100644 (file)
@@ -1,3 +1,4 @@
+use alloc::collections::BTreeSet;
 use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut};
 
 use crate::BlockId;
@@ -44,8 +45,79 @@ pub trait BlockAnchor:
     fn anchor_block(&self) -> BlockId;
 }
 
+impl<A: BlockAnchor> BlockAnchor for &'static A {
+    fn anchor_block(&self) -> BlockId {
+        <A as BlockAnchor>::anchor_block(self)
+    }
+}
+
 impl BlockAnchor for (u32, BlockHash) {
     fn anchor_block(&self) -> BlockId {
         (*self).into()
     }
 }
+
+/// Represents a service that tracks the best chain history.
+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))
+    }
+}
+
+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 changes to a [`TxIndex`] implementation.
+pub trait TxIndexAdditions: Default {
+    /// Append `other` on top of `self`.
+    fn append_additions(&mut self, other: Self);
+}
+
+impl<I: Ord> TxIndexAdditions for BTreeSet<I> {
+    fn append_additions(&mut self, mut other: Self) {
+        self.append(&mut other);
+    }
+}
+
+/// Represents an index of transaction data.
+pub trait TxIndex {
+    /// The resultant "additions" when new transaction data is indexed.
+    type Additions: TxIndexAdditions;
+
+    /// Scan and index the given `outpoint` and `txout`.
+    fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions;
+
+    /// Scan and index the given transaction.
+    fn index_tx(&mut self, tx: &Transaction) -> Self::Additions {
+        let txid = tx.txid();
+        tx.output
+            .iter()
+            .enumerate()
+            .map(|(vout, txout)| self.index_txout(OutPoint::new(txid, vout as _), txout))
+            .reduce(|mut acc, other| {
+                acc.append_additions(other);
+                acc
+            })
+            .unwrap_or_default()
+    }
+
+    /// A transaction is relevant if it contains a txout with a script_pubkey that we own, or if it
+    /// spends an already-indexed outpoint that we have previously indexed.
+    fn is_tx_relevant(&self, tx: &Transaction) -> bool;
+}
index 824b68e2077b197a59411e1f67fef02b9996e51e..daa7e1ba889309f6cd2d66ae35bb69f88c70f814 100644 (file)
 //! assert!(additions.is_empty());
 //! ```
 
-use crate::{collections::*, BlockAnchor, BlockId, ForEachTxOut};
+use crate::{
+    collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, Observation, TxIndex,
+    TxIndexAdditions,
+};
 use alloc::vec::Vec;
 use bitcoin::{OutPoint, Transaction, TxOut, Txid};
 use core::ops::{Deref, RangeInclusive};
@@ -209,6 +212,12 @@ impl<A> TxGraph<A> {
         })
     }
 
+    pub fn get_anchors_and_last_seen(&self, txid: Txid) -> Option<(&BTreeSet<A>, u64)> {
+        self.txs
+            .get(&txid)
+            .map(|(_, anchors, last_seen)| (anchors, *last_seen))
+    }
+
     /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction.
     /// Returns `Some(_)` if we have all the `TxOut`s being spent by `tx` in the graph (either as
     /// the full transactions or individual txouts). If the returned value is negative, then the
@@ -462,6 +471,75 @@ impl<A: BlockAnchor> TxGraph<A> {
         *update_last_seen = seen_at;
         self.determine_additions(&update)
     }
+
+    /// Determines whether a transaction of `txid` is in the best chain.
+    ///
+    /// TODO: Also return conflicting tx list, ordered by last_seen.
+    pub fn is_txid_in_best_chain<C>(&self, chain: C, txid: Txid) -> Result<bool, C::Error>
+    where
+        C: ChainOracle,
+    {
+        let (tx_node, anchors, &last_seen) = match self.txs.get(&txid) {
+            Some((tx, anchors, last_seen)) if !(anchors.is_empty() && *last_seen == 0) => {
+                (tx, anchors, last_seen)
+            }
+            _ => return Ok(false),
+        };
+
+        for block_id in anchors.iter().map(A::anchor_block) {
+            if chain.is_block_in_best_chain(block_id)? {
+                return Ok(true);
+            }
+        }
+
+        // The tx is not anchored to a block which is in the best chain, let's check whether we can
+        // ignore it by checking conflicts!
+        let tx = match tx_node {
+            TxNode::Whole(tx) => tx,
+            TxNode::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 :/
+                return Ok(false);
+            }
+        };
+
+        // [TODO] Is this logic correct? I do not think so, but it should be good enough for now!
+        let mut latest_last_seen = 0_u64;
+        for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx(txid)) {
+            for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) {
+                if chain.is_block_in_best_chain(block_id)? {
+                    // conflicting tx is in best chain, so the current tx cannot be in best chain!
+                    return Ok(false);
+                }
+            }
+            if conflicting_tx.last_seen > latest_last_seen {
+                latest_last_seen = conflicting_tx.last_seen;
+            }
+        }
+        if last_seen >= latest_last_seen {
+            Ok(true)
+        } else {
+            Ok(false)
+        }
+    }
+
+    /// Return true if `outpoint` exists in best chain and is unspent.
+    pub fn is_unspent<C>(&self, chain: C, outpoint: OutPoint) -> Result<bool, C::Error>
+    where
+        C: ChainOracle,
+    {
+        if !self.is_txid_in_best_chain(&chain, outpoint.txid)? {
+            return Ok(false);
+        }
+        if let Some(spends) = self.spends.get(&outpoint) {
+            for &txid in spends {
+                if self.is_txid_in_best_chain(&chain, txid)? {
+                    return Ok(false);
+                }
+            }
+        }
+        Ok(true)
+    }
 }
 
 impl<A> TxGraph<A> {
@@ -568,6 +646,108 @@ impl<A> TxGraph<A> {
     }
 }
 
+pub struct IndexedAdditions<A, D> {
+    pub graph_additions: Additions<A>,
+    pub index_delta: D,
+}
+
+impl<A, D: Default> Default for IndexedAdditions<A, D> {
+    fn default() -> Self {
+        Self {
+            graph_additions: Default::default(),
+            index_delta: Default::default(),
+        }
+    }
+}
+
+impl<A: BlockAnchor, D: TxIndexAdditions> TxIndexAdditions for IndexedAdditions<A, D> {
+    fn append_additions(&mut self, other: Self) {
+        let Self {
+            graph_additions,
+            index_delta,
+        } = other;
+        self.graph_additions.append(graph_additions);
+        self.index_delta.append_additions(index_delta);
+    }
+}
+
+pub struct IndexedTxGraph<A, I> {
+    graph: TxGraph<A>,
+    index: I,
+}
+
+impl<A, I: Default> Default for IndexedTxGraph<A, I> {
+    fn default() -> Self {
+        Self {
+            graph: Default::default(),
+            index: Default::default(),
+        }
+    }
+}
+
+impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
+    pub fn insert_txout(
+        &mut self,
+        outpoint: OutPoint,
+        txout: &TxOut,
+        observation: Observation<A>,
+    ) -> IndexedAdditions<A, I::Additions> {
+        IndexedAdditions {
+            graph_additions: {
+                let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone());
+                graph_additions.append(match observation {
+                    Observation::InBlock(anchor) => self.graph.insert_anchor(outpoint.txid, anchor),
+                    Observation::SeenAt(seen_at) => {
+                        self.graph.insert_seen_at(outpoint.txid, seen_at)
+                    }
+                });
+                graph_additions
+            },
+            index_delta: <I as TxIndex>::index_txout(&mut self.index, outpoint, txout),
+        }
+    }
+
+    pub fn insert_tx(
+        &mut self,
+        tx: &Transaction,
+        observation: Observation<A>,
+    ) -> IndexedAdditions<A, I::Additions> {
+        let txid = tx.txid();
+        IndexedAdditions {
+            graph_additions: {
+                let mut graph_additions = self.graph.insert_tx(tx.clone());
+                graph_additions.append(match observation {
+                    Observation::InBlock(anchor) => self.graph.insert_anchor(txid, anchor),
+                    Observation::SeenAt(seen_at) => self.graph.insert_seen_at(txid, seen_at),
+                });
+                graph_additions
+            },
+            index_delta: <I as TxIndex>::index_tx(&mut self.index, tx),
+        }
+    }
+
+    pub fn filter_and_insert_txs<'t, T>(
+        &mut self,
+        txs: T,
+        observation: Observation<A>,
+    ) -> IndexedAdditions<A, I::Additions>
+    where
+        T: Iterator<Item = &'t Transaction>,
+    {
+        txs.filter_map(|tx| {
+            if self.index.is_tx_relevant(tx) {
+                Some(self.insert_tx(tx, observation.clone()))
+            } else {
+                None
+            }
+        })
+        .fold(IndexedAdditions::default(), |mut acc, other| {
+            acc.append_additions(other);
+            acc
+        })
+    }
+}
+
 /// A structure that represents changes to a [`TxGraph`].
 ///
 /// It is named "additions" because [`TxGraph`] is monotone, so transactions can only be added and