]> Untitled Git - bdk/commitdiff
feat(chain): Txs that conflict with relevant txs are also relevant
author志宇 <hello@evanlinjin.me>
Sat, 2 Aug 2025 06:16:26 +0000 (06:16 +0000)
committer志宇 <hello@evanlinjin.me>
Sat, 2 Aug 2025 06:16:26 +0000 (06:16 +0000)
Change behavior of {insert|apply}-if-relevant methods of `IndexedTxGraph`
to also consider txs that conflict with relevant txs as relevant.

Rationale:

It is useful to determine why something is evicted from the mempool.

For example, an incoming transaction may be evicted from the mempool due
to insufficient fees or a conflicting transaction is confirmed.

* Insufficient fees - the user may want to CPFP the tx.
* Conflicting tx is confirmed - the sender probably purposefully
  cancelled the tx. The user may want to forget about this tx once it
  reaches x confirmations.

The `IntentTracker` will make use of these relevant-conflicts.

A note about chain sources:

For some chain sources, obtaining relevant-conflicts is extremely
costly or downright impossible (i.e. Electrum, BIP-158 filters).

`bdk_bitcoind_rpc::Emitter` is still the most robust chain source to use.

crates/chain/src/indexed_tx_graph.rs

index 431ff29ebc681bbd97bdf19fd9f370c677ee6398..f0c1d121d02ef9e4b966af65f5f78f67b7e8e956 100644 (file)
@@ -67,6 +67,16 @@ impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
             indexer,
         }
     }
+
+    // If `tx` replaces a relevant tx, it should also be considered relevant.
+    fn is_tx_or_conflict_relevant(&self, tx: &Transaction) -> bool {
+        self.index.is_tx_relevant(tx)
+            || self
+                .graph
+                .direct_conflicts(tx)
+                .filter_map(|(_, txid)| self.graph.get_tx(txid))
+                .any(|tx| self.index.is_tx_relevant(&tx))
+    }
 }
 
 impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
@@ -239,8 +249,11 @@ where
 
     /// Batch insert transactions, filtering out those that are irrelevant.
     ///
-    /// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant
-    /// transactions in `txs` will be ignored. `txs` do not need to be in topological order.
+    /// `txs` do not need to be in topological order.
+    ///
+    /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`.
+    /// A transaction that conflicts with a relevant transaction is also considered relevant.
+    /// Irrelevant transactions in `txs` will be ignored.
     pub fn batch_insert_relevant<T: Into<Arc<Transaction>>>(
         &mut self,
         txs: impl IntoIterator<Item = (T, impl IntoIterator<Item = A>)>,
@@ -263,7 +276,7 @@ where
 
         let mut tx_graph = tx_graph::ChangeSet::default();
         for (tx, anchors) in txs {
-            if self.index.is_tx_relevant(&tx) {
+            if self.is_tx_or_conflict_relevant(&tx) {
                 let txid = tx.compute_txid();
                 tx_graph.merge(self.graph.insert_tx(tx.clone()));
                 for anchor in anchors {
@@ -278,7 +291,8 @@ where
     /// Batch insert unconfirmed transactions, filtering out those that are irrelevant.
     ///
     /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`.
-    /// Irrelevant transactions in `txs` will be ignored.
+    /// A transaction that conflicts with a relevant transaction is also considered relevant.
+    /// Irrelevant transactions in `unconfirmed_txs` will be ignored.
     ///
     /// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The
     /// *last seen* communicates when the transaction is last seen in the mempool which is used for
@@ -305,8 +319,9 @@ where
 
         let graph = self.graph.batch_insert_unconfirmed(
             txs.into_iter()
-                .filter(|(tx, _)| self.index.is_tx_relevant(tx))
-                .map(|(tx, seen_at)| (tx.clone(), seen_at)),
+                .filter(|(tx, _)| self.is_tx_or_conflict_relevant(tx))
+                .map(|(tx, seen_at)| (tx.clone(), seen_at))
+                .collect::<Vec<_>>(),
         );
 
         ChangeSet {
@@ -350,7 +365,8 @@ where
     /// Each inserted transaction's anchor will be constructed using [`TxPosInBlock`].
     ///
     /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`.
-    /// Irrelevant transactions in `txs` will be ignored.
+    /// A transaction that conflicts with a relevant transaction is also considered relevant.
+    /// Irrelevant transactions in `block` will be ignored.
     pub fn apply_block_relevant(
         &mut self,
         block: &Block,
@@ -363,7 +379,7 @@ where
         let mut changeset = ChangeSet::<A, I::ChangeSet>::default();
         for (tx_pos, tx) in block.txdata.iter().enumerate() {
             changeset.indexer.merge(self.index.index_tx(tx));
-            if self.index.is_tx_relevant(tx) {
+            if self.is_tx_or_conflict_relevant(tx) {
                 let txid = tx.compute_txid();
                 let anchor = TxPosInBlock {
                     block,