diff --git a/crates/chain/src/canonical_task.rs b/crates/chain/src/canonical_task.rs index 0cb8ffbf4..bbcbdc2d5 100644 --- a/crates/chain/src/canonical_task.rs +++ b/crates/chain/src/canonical_task.rs @@ -119,7 +119,10 @@ 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() { + if !self.is_canonicalized(txid) + && !tx.is_coinbase() + && !self.is_evicted_leftover(txid) + { let observed_in = ObservedIn::Block(height); self.mark_canonical( txid, @@ -259,6 +262,18 @@ impl<'g, A: Anchor> CanonicalTask<'g, A> { self.canonical.contains_key(&txid) || self.not_canonical.contains(&txid) } + fn is_evicted_leftover(&self, txid: Txid) -> bool { + let node = self + .tx_graph + .get_tx_node(txid) + .expect("leftover transaction must exist"); + match (node.last_seen, node.last_evicted) { + (_, None) => false, + (Some(seen), Some(evicted)) => evicted >= seen, + (None, Some(_)) => true, + } + } + fn mark_canonical(&mut self, txid: Txid, tx: Arc, reason: CanonicalReason) { let starting_txid = txid; let mut is_starting_tx = true; diff --git a/crates/chain/tests/test_canonical_view.rs b/crates/chain/tests/test_canonical_view.rs index 2cbe02103..09693c328 100644 --- a/crates/chain/tests/test_canonical_view.rs +++ b/crates/chain/tests/test_canonical_view.rs @@ -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 = + [(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" + ); +}