From e8da007011c3c7e247c26a1c9984319229bb0afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 13 Jun 2025 13:07:37 +1000 Subject: [PATCH 1/2] fix(chain): Unconfirmed coinbase txs should never be canonical The logic in `CanonicalIter` will consider txs that are anchored to blocks not in the best chain since they still can appear in the mempool. However, coinbase txs can never be unconfirmed - which the old logic failed to exclude. --- crates/chain/src/canonical_iter.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/chain/src/canonical_iter.rs b/crates/chain/src/canonical_iter.rs index 58f266f89..204ead451 100644 --- a/crates/chain/src/canonical_iter.rs +++ b/crates/chain/src/canonical_iter.rs @@ -230,6 +230,10 @@ impl Iterator for CanonicalIter<'_, A, C> { } if let Some((txid, tx, last_seen)) = self.unprocessed_seen_txs.next() { + debug_assert!( + !tx.is_coinbase(), + "Coinbase txs must not have `last_seen` (in mempool) value" + ); if !self.is_canonicalized(txid) { let observed_in = ObservedIn::Mempool(last_seen); self.mark_canonical(txid, tx, CanonicalReason::from_observed_in(observed_in)); @@ -238,7 +242,7 @@ impl Iterator for CanonicalIter<'_, A, C> { } if let Some((txid, tx, height)) = self.unprocessed_leftover_txs.pop_front() { - if !self.is_canonicalized(txid) { + if !self.is_canonicalized(txid) && !tx.is_coinbase() { let observed_in = ObservedIn::Block(height); self.mark_canonical(txid, tx, CanonicalReason::from_observed_in(observed_in)); } From c84035e4d804943da7b3308411a1402b1ff6e2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 13 Jun 2025 13:12:59 +1000 Subject: [PATCH 2/2] test(chain): Add scenario: coinbase tx must not become unconfirmed --- crates/chain/tests/test_tx_graph_conflicts.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 862eb9b4b..71944e404 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -943,6 +943,28 @@ fn test_tx_conflict_handling() { confirmed: Amount::ZERO, }, }, + Scenario { + name: "coinbase tx must not become unconfirmed", + tx_templates: &[ + TxTemplate { + tx_name: "coinbase", + inputs: &[TxInTemplate::Coinbase], + outputs: &[TxOutTemplate::new(21_000, Some(0))], + // Stale block + anchors: &[block_id!(1, "B-prime")], + ..Default::default() + } + ], + exp_chain_txs: HashSet::from([]), + exp_chain_txouts: HashSet::from([]), + exp_unspents: HashSet::from([]), + exp_balance: Balance { + immature: Amount::ZERO, + trusted_pending: Amount::ZERO, + untrusted_pending: Amount::ZERO, + confirmed: Amount::ZERO, + } + } ]; for scenario in scenarios {