]> Untitled Git - bdk/commitdiff
fix(chain): skip evicted leftover transactions
authorNoah Joeris <noahjoeris@gmail.com>
Wed, 22 Jul 2026 19:23:11 +0000 (22:23 +0300)
committerNoah Joeris <noahjoeris@gmail.com>
Wed, 22 Jul 2026 19:23:11 +0000 (22:23 +0300)
crates/chain/src/canonical_task.rs
crates/chain/tests/test_canonical_view.rs

index 0cb8ffbf4a756a05855c1a84689d7ebc4d7dfb86..a7cb9230a27df1e85799e311cfed644985ca4feb 100644 (file)
@@ -119,7 +119,12 @@ impl<'g, A: Anchor> ChainQuery for CanonicalTask<'g, A> {
                 }
                 CanonicalStage::LeftOverTxs => {
                     if let Some((txid, tx, height)) = self.unprocessed_leftover_txs.pop_front() {
-                        if !self.is_canonicalized(txid) && !tx.is_coinbase() {
+                        let is_evicted = self
+                            .tx_graph
+                            .get_tx_node(txid)
+                            .expect("leftover transaction must exist")
+                            .is_evicted();
+                        if !self.is_canonicalized(txid) && !tx.is_coinbase() && !is_evicted {
                             let observed_in = ObservedIn::Block(height);
                             self.mark_canonical(
                                 txid,
index 2cbe021033a34dfd90d5845a0ae595f3f41be238..09693c3288836b44744aabb069668fa7b2eb15cc 100644 (file)
@@ -2,7 +2,7 @@
 
 use std::collections::BTreeMap;
 
-use bdk_chain::{local_chain::LocalChain, ConfirmationBlockTime, TxGraph};
+use bdk_chain::{local_chain::LocalChain, BlockId, ConfirmationBlockTime, TxGraph};
 use bdk_testenv::{hash, utils::new_tx};
 use bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
 
@@ -294,3 +294,47 @@ fn test_min_confirmations_multiple_transactions() {
     );
     assert_eq!(balance_high.untrusted_pending, Amount::ZERO);
 }
+
+/// Txs with stale anchor and that have `last_evicted >= last_seen` should be excluded from
+/// canonicalization.
+#[test]
+fn test_evicted_stale_anchored_tx_not_canonical() {
+    let blocks: BTreeMap<u32, BlockHash> =
+        [(0, hash!("genesis")), (1, hash!("b1")), (2, hash!("tip"))]
+            .into_iter()
+            .collect();
+    let chain = LocalChain::from_blocks(blocks).unwrap();
+
+    let mut tx_graph = TxGraph::default();
+    let tx = Transaction {
+        input: vec![TxIn {
+            previous_output: OutPoint::new(hash!("parent"), 0),
+            ..Default::default()
+        }],
+        output: vec![TxOut {
+            value: Amount::from_sat(50_000),
+            script_pubkey: ScriptBuf::new(),
+        }],
+        ..new_tx(1)
+    };
+    let txid = tx.compute_txid();
+    let _ = tx_graph.insert_tx(tx);
+    let _ = tx_graph.insert_anchor(
+        txid,
+        ConfirmationBlockTime {
+            block_id: BlockId {
+                height: 1,
+                hash: hash!("stale"),
+            },
+            confirmation_time: 123456,
+        },
+    );
+    let _ = tx_graph.insert_seen_at(txid, 100);
+    let _ = tx_graph.insert_evicted_at(txid, 200);
+
+    let view = chain.canonical_view(&tx_graph, chain.tip().block_id(), Default::default());
+    assert!(
+        !view.txs().any(|tx| tx.txid == txid),
+        "evicted leftover tx must not be canonical"
+    );
+}