From 32da7e12a18798207b4e475c03e7c80ba2bca05a Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Fri, 1 Sep 2023 17:58:47 +0800 Subject: [PATCH] chain(fix): conflict resolution for txs with same last_seen --- crates/chain/src/tx_graph.rs | 20 +++ crates/chain/tests/test_indexed_tx_graph.rs | 132 +++++++++++++++++++- 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 7f58f2031f..a3c59edbb1 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -693,6 +693,26 @@ impl TxGraph { if conflicting_tx.last_seen_unconfirmed > *last_seen { return Ok(None); } + if conflicting_tx.last_seen_unconfirmed == *last_seen { + // Check if conflicting tx has higher absolute fee and fee rate + if let Ok(fee) = self.calculate_fee(tx) { + if let Ok(conflicting_fee) = self.calculate_fee(&conflicting_tx) { + let fee_rate = fee as f32 / tx.weight().to_vbytes_ceil() as f32; + let conflicting_fee_rate = conflicting_fee as f32 + / conflicting_tx.weight().to_vbytes_ceil() as f32; + + if conflicting_fee > fee && conflicting_fee_rate > fee_rate { + return Ok(None); + } + } + } + + // If fee rates cannot be distinguished, then conflicting tx has priority if txid of + // conflicting tx > txid of original tx + if conflicting_tx.txid() > tx.txid() { + return Ok(None); + } + } } Ok(Some(ChainPosition::Unconfirmed(*last_seen))) diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 84506ec118..cad6dd3ebb 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -7,12 +7,14 @@ use bdk_chain::{ indexed_tx_graph::{self, IndexedTxGraph}, keychain::{self, Balance, KeychainTxOutIndex}, local_chain::LocalChain, - tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor, + tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor, SpkIterator, }; use bitcoin::{ - secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, + hashes::Hash, secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn, + TxOut, }; -use miniscript::Descriptor; +use common::*; +use miniscript::{Descriptor, DescriptorPublicKey}; /// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented /// in topological order. @@ -471,3 +473,127 @@ fn test_list_owned_txouts() { ); } } + +#[allow(unused)] +pub fn single_descriptor_setup() -> ( + LocalChain, + IndexedTxGraph>, + Descriptor, +) { + let local_chain = (0..10) + .map(|i| (i as u32, BlockHash::hash(format!("Block {}", i).as_bytes()))) + .collect::>(); + let local_chain = LocalChain::from(local_chain); + + let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap(); + + let mut graph = IndexedTxGraph::>::default(); + + graph.index.add_keychain((), desc_1.clone()); + graph.index.set_lookahead_for_all(100); + + (local_chain, graph, desc_1) +} + +#[allow(unused)] +pub fn setup_conflicts( + spk_iter: &mut SpkIterator<&Descriptor>, +) -> (Transaction, Transaction, Transaction) { + let tx1 = Transaction { + output: vec![TxOut { + script_pubkey: spk_iter.next().unwrap().1, + value: 40000, + }], + ..new_tx(0) + }; + + let tx_conflict_1 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx1.txid(), 0), + ..Default::default() + }], + output: vec![TxOut { + script_pubkey: spk_iter.next().unwrap().1, + value: 20000, + }], + ..new_tx(0) + }; + + let tx_conflict_2 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx1.txid(), 0), + ..Default::default() + }], + output: vec![TxOut { + script_pubkey: spk_iter.next().unwrap().1, + value: 30000, + }], + ..new_tx(0) + }; + + (tx1, tx_conflict_1, tx_conflict_2) +} + +/// Test conflicts for two mempool tx, with same `seen_at` time. +#[test] +fn test_unconfirmed_conflicts_at_same_last_seen() { + let (local_chain, mut graph, desc) = single_descriptor_setup(); + let mut spk_iter = SpkIterator::new(&desc); + let (parent_tx, tx_conflict_1, tx_conflict_2) = setup_conflicts(&mut spk_iter); + + // Parent confirms at height 2. + let _ = graph.insert_relevant_txs( + [&parent_tx].iter().map(|tx| { + ( + *tx, + [ConfirmationHeightAnchor { + anchor_block: (2, *local_chain.blocks().get(&2).unwrap()).into(), + confirmation_height: 2, + }], + ) + }), + None, + ); + + // Both conflicts are in mempool at same `seen_at` + let _ = graph.insert_relevant_txs( + [&tx_conflict_1, &tx_conflict_2] + .iter() + .map(|tx| (*tx, None)), + Some(100), + ); + + let txouts = graph + .graph() + .filter_chain_txouts( + &local_chain, + local_chain.tip().unwrap().block_id(), + graph.index.outpoints().iter().cloned(), + ) + .collect::>(); + + let utxos = graph + .graph() + .filter_chain_unspents( + &local_chain, + local_chain.tip().unwrap().block_id(), + graph.index.outpoints().iter().cloned(), + ) + .collect::>(); + + assert_eq!(txouts.len(), 2); + assert_eq!( + txouts + .iter() + .filter(|(_, txout)| matches!(txout.chain_position, ChainPosition::Unconfirmed(100))) + .count(), + 1 + ); + assert_eq!( + utxos + .iter() + .filter(|(_, txout)| matches!(txout.chain_position, ChainPosition::Unconfirmed(100))) + .count(), + 1 + ); +}