From: Noah Joeris Date: Wed, 22 Jul 2026 19:23:11 +0000 (+0300) Subject: fix(chain): skip evicted leftover transactions X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/ui-kit/value/FromScriptException.UnrecognizedScript.html?a=commitdiff_plain;h=2a8512b15706e3f05a4a506af0293c2d33d12581;p=bdk fix(chain): skip evicted leftover transactions --- diff --git a/crates/chain/src/canonical_task.rs b/crates/chain/src/canonical_task.rs index 0cb8ffbf..a7cb9230 100644 --- a/crates/chain/src/canonical_task.rs +++ b/crates/chain/src/canonical_task.rs @@ -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, diff --git a/crates/chain/tests/test_canonical_view.rs b/crates/chain/tests/test_canonical_view.rs index 2cbe0210..09693c32 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" + ); +}