Skip to content

Commit 32da7e1

Browse files
committed
chain(fix): conflict resolution for txs with same last_seen
1 parent 2867e88 commit 32da7e1

File tree

2 files changed

+149
-3
lines changed

2 files changed

+149
-3
lines changed

crates/chain/src/tx_graph.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,26 @@ impl<A: Anchor> TxGraph<A> {
693693
if conflicting_tx.last_seen_unconfirmed > *last_seen {
694694
return Ok(None);
695695
}
696+
if conflicting_tx.last_seen_unconfirmed == *last_seen {
697+
// Check if conflicting tx has higher absolute fee and fee rate
698+
if let Ok(fee) = self.calculate_fee(tx) {
699+
if let Ok(conflicting_fee) = self.calculate_fee(&conflicting_tx) {
700+
let fee_rate = fee as f32 / tx.weight().to_vbytes_ceil() as f32;
701+
let conflicting_fee_rate = conflicting_fee as f32
702+
/ conflicting_tx.weight().to_vbytes_ceil() as f32;
703+
704+
if conflicting_fee > fee && conflicting_fee_rate > fee_rate {
705+
return Ok(None);
706+
}
707+
}
708+
}
709+
710+
// If fee rates cannot be distinguished, then conflicting tx has priority if txid of
711+
// conflicting tx > txid of original tx
712+
if conflicting_tx.txid() > tx.txid() {
713+
return Ok(None);
714+
}
715+
}
696716
}
697717

698718
Ok(Some(ChainPosition::Unconfirmed(*last_seen)))

crates/chain/tests/test_indexed_tx_graph.rs

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ use bdk_chain::{
77
indexed_tx_graph::{self, IndexedTxGraph},
88
keychain::{self, Balance, KeychainTxOutIndex},
99
local_chain::LocalChain,
10-
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
10+
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor, SpkIterator,
1111
};
1212
use bitcoin::{
13-
secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
13+
hashes::Hash, secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn,
14+
TxOut,
1415
};
15-
use miniscript::Descriptor;
16+
use common::*;
17+
use miniscript::{Descriptor, DescriptorPublicKey};
1618

1719
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
1820
/// in topological order.
@@ -471,3 +473,127 @@ fn test_list_owned_txouts() {
471473
);
472474
}
473475
}
476+
477+
#[allow(unused)]
478+
pub fn single_descriptor_setup() -> (
479+
LocalChain,
480+
IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>,
481+
Descriptor<DescriptorPublicKey>,
482+
) {
483+
let local_chain = (0..10)
484+
.map(|i| (i as u32, BlockHash::hash(format!("Block {}", i).as_bytes())))
485+
.collect::<BTreeMap<u32, BlockHash>>();
486+
let local_chain = LocalChain::from(local_chain);
487+
488+
let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
489+
490+
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::default();
491+
492+
graph.index.add_keychain((), desc_1.clone());
493+
graph.index.set_lookahead_for_all(100);
494+
495+
(local_chain, graph, desc_1)
496+
}
497+
498+
#[allow(unused)]
499+
pub fn setup_conflicts(
500+
spk_iter: &mut SpkIterator<&Descriptor<DescriptorPublicKey>>,
501+
) -> (Transaction, Transaction, Transaction) {
502+
let tx1 = Transaction {
503+
output: vec![TxOut {
504+
script_pubkey: spk_iter.next().unwrap().1,
505+
value: 40000,
506+
}],
507+
..new_tx(0)
508+
};
509+
510+
let tx_conflict_1 = Transaction {
511+
input: vec![TxIn {
512+
previous_output: OutPoint::new(tx1.txid(), 0),
513+
..Default::default()
514+
}],
515+
output: vec![TxOut {
516+
script_pubkey: spk_iter.next().unwrap().1,
517+
value: 20000,
518+
}],
519+
..new_tx(0)
520+
};
521+
522+
let tx_conflict_2 = Transaction {
523+
input: vec![TxIn {
524+
previous_output: OutPoint::new(tx1.txid(), 0),
525+
..Default::default()
526+
}],
527+
output: vec![TxOut {
528+
script_pubkey: spk_iter.next().unwrap().1,
529+
value: 30000,
530+
}],
531+
..new_tx(0)
532+
};
533+
534+
(tx1, tx_conflict_1, tx_conflict_2)
535+
}
536+
537+
/// Test conflicts for two mempool tx, with same `seen_at` time.
538+
#[test]
539+
fn test_unconfirmed_conflicts_at_same_last_seen() {
540+
let (local_chain, mut graph, desc) = single_descriptor_setup();
541+
let mut spk_iter = SpkIterator::new(&desc);
542+
let (parent_tx, tx_conflict_1, tx_conflict_2) = setup_conflicts(&mut spk_iter);
543+
544+
// Parent confirms at height 2.
545+
let _ = graph.insert_relevant_txs(
546+
[&parent_tx].iter().map(|tx| {
547+
(
548+
*tx,
549+
[ConfirmationHeightAnchor {
550+
anchor_block: (2, *local_chain.blocks().get(&2).unwrap()).into(),
551+
confirmation_height: 2,
552+
}],
553+
)
554+
}),
555+
None,
556+
);
557+
558+
// Both conflicts are in mempool at same `seen_at`
559+
let _ = graph.insert_relevant_txs(
560+
[&tx_conflict_1, &tx_conflict_2]
561+
.iter()
562+
.map(|tx| (*tx, None)),
563+
Some(100),
564+
);
565+
566+
let txouts = graph
567+
.graph()
568+
.filter_chain_txouts(
569+
&local_chain,
570+
local_chain.tip().unwrap().block_id(),
571+
graph.index.outpoints().iter().cloned(),
572+
)
573+
.collect::<Vec<_>>();
574+
575+
let utxos = graph
576+
.graph()
577+
.filter_chain_unspents(
578+
&local_chain,
579+
local_chain.tip().unwrap().block_id(),
580+
graph.index.outpoints().iter().cloned(),
581+
)
582+
.collect::<Vec<_>>();
583+
584+
assert_eq!(txouts.len(), 2);
585+
assert_eq!(
586+
txouts
587+
.iter()
588+
.filter(|(_, txout)| matches!(txout.chain_position, ChainPosition::Unconfirmed(100)))
589+
.count(),
590+
1
591+
);
592+
assert_eq!(
593+
utxos
594+
.iter()
595+
.filter(|(_, txout)| matches!(txout.chain_position, ChainPosition::Unconfirmed(100)))
596+
.count(),
597+
1
598+
);
599+
}

0 commit comments

Comments
 (0)