Skip to content

Commit bea8e5a

Browse files
committed
fix: TxGraph::missing_blocks logic
Added additional tests for unnecessary duplicate heights
1 parent db15e03 commit bea8e5a

File tree

5 files changed

+191
-16
lines changed

5 files changed

+191
-16
lines changed

crates/chain/src/tx_data_traits.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ impl ForEachTxOut for Transaction {
3838

3939
/// Trait that "anchors" blockchain data to a specific block of height and hash.
4040
///
41+
/// [`Anchor`] implementations must be [`Ord`] by the anchor block's [`BlockId`] first.
42+
///
4143
/// I.e. If transaction A is anchored in block B, then if block B is in the best chain, we can
4244
/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean
4345
/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a

crates/chain/src/tx_graph.rs

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -601,23 +601,63 @@ impl<A: Anchor> TxGraph<A> {
601601
/// Find missing block heights of `chain`.
602602
///
603603
/// This works by scanning through anchors, and seeing whether the anchor block of the anchor
604-
/// exists in the [`LocalChain`].
605-
pub fn missing_blocks<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
606-
let mut last_block = Option::<BlockId>::None;
604+
/// exists in the [`LocalChain`]. The returned iterator does not output duplicate heights.
605+
pub fn missing_heights<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
606+
// Map of txids to skip.
607+
//
608+
// Usually, if a height of a tx anchor is missing from the chain, we would want to return
609+
// this height in the iterator. The exception is when the tx is confirmed in chain. All the
610+
// other missing-height anchors of this tx can be skipped.
611+
//
612+
// * Some(true) => skip all anchors of this txid
613+
// * Some(false) => do not skip anchors of this txid
614+
// * None => we do not know whether we can skip this txid
615+
let mut txids_to_skip = HashMap::<Txid, bool>::new();
616+
617+
// Keeps track of the last height emitted so we don't double up.
618+
let mut last_height_emitted = Option::<u32>::None;
619+
607620
self.anchors
608621
.iter()
609-
.map(|(a, _)| a.anchor_block())
610-
.filter(move |block| {
611-
if last_block.as_ref() == Some(block) {
612-
false
613-
} else {
614-
last_block = Some(*block);
622+
.filter(move |(_, txid)| {
623+
let skip = *txids_to_skip.entry(*txid).or_insert_with(|| {
624+
let tx_anchors = match self.txs.get(txid) {
625+
Some((_, anchors, _)) => anchors,
626+
None => return true,
627+
};
628+
let mut has_missing_height = false;
629+
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
630+
match chain.heights().get(&anchor_block.height) {
631+
None => {
632+
has_missing_height = true;
633+
continue;
634+
}
635+
Some(chain_hash) => {
636+
if chain_hash == &anchor_block.hash {
637+
return true;
638+
}
639+
}
640+
}
641+
}
642+
!has_missing_height
643+
});
644+
#[cfg(feature = "std")]
645+
debug_assert!({
646+
println!("txid={} skip={}", txid, skip);
615647
true
616-
}
648+
});
649+
!skip
617650
})
618-
.filter_map(|block| match chain.heights().get(&block.height) {
619-
Some(chain_hash) if *chain_hash == block.hash => None,
620-
_ => Some(block.height),
651+
.filter_map(move |(a, _)| {
652+
let anchor_block = a.anchor_block();
653+
if Some(anchor_block.height) != last_height_emitted
654+
&& !chain.heights().contains_key(&anchor_block.height)
655+
{
656+
last_height_emitted = Some(anchor_block.height);
657+
Some(anchor_block.height)
658+
} else {
659+
None
660+
}
621661
})
622662
}
623663

crates/chain/tests/test_tx_graph.rs

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use bdk_chain::{
44
collections::*,
55
local_chain::LocalChain,
66
tx_graph::{Additions, TxGraph},
7-
Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
7+
Anchor, Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
88
};
99
use bitcoin::{
1010
hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
@@ -822,3 +822,136 @@ fn test_additions_last_seen_append() {
822822
);
823823
}
824824
}
825+
826+
#[test]
827+
fn test_missing_blocks() {
828+
/// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.
829+
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, core::hash::Hash)]
830+
struct TestAnchor(BlockId);
831+
832+
impl Anchor for TestAnchor {
833+
fn anchor_block(&self) -> BlockId {
834+
self.0
835+
}
836+
}
837+
838+
struct Scenario<'a> {
839+
name: &'a str,
840+
graph: TxGraph<TestAnchor>,
841+
chain: LocalChain,
842+
exp_heights: &'a [u32],
843+
}
844+
845+
const fn new_anchor(height: u32, hash: BlockHash) -> TestAnchor {
846+
TestAnchor(BlockId { height, hash })
847+
}
848+
849+
fn new_scenario<'a>(
850+
name: &'a str,
851+
graph_anchors: &'a [(Txid, TestAnchor)],
852+
chain: &'a [(u32, BlockHash)],
853+
exp_heights: &'a [u32],
854+
) -> Scenario<'a> {
855+
Scenario {
856+
name,
857+
graph: {
858+
let mut g = TxGraph::default();
859+
for (txid, anchor) in graph_anchors {
860+
let _ = g.insert_anchor(*txid, anchor.clone());
861+
}
862+
g
863+
},
864+
chain: {
865+
let mut c = LocalChain::default();
866+
for (height, hash) in chain {
867+
let _ = c.insert_block(BlockId {
868+
height: *height,
869+
hash: *hash,
870+
});
871+
}
872+
c
873+
},
874+
exp_heights,
875+
}
876+
}
877+
878+
fn run(scenarios: &[Scenario]) {
879+
for scenario in scenarios {
880+
let Scenario {
881+
name,
882+
graph,
883+
chain,
884+
exp_heights,
885+
} = scenario;
886+
887+
let heights = graph.missing_heights(chain).collect::<Vec<_>>();
888+
assert_eq!(&heights, exp_heights, "scenario: {}", name);
889+
}
890+
}
891+
892+
run(&[
893+
new_scenario(
894+
"2 txs with the same anchor (2:B) which is missing from chain",
895+
&[
896+
(h!("tx_1"), new_anchor(2, h!("B"))),
897+
(h!("tx_2"), new_anchor(2, h!("B"))),
898+
],
899+
&[(1, h!("A")), (3, h!("C"))],
900+
&[2],
901+
),
902+
new_scenario(
903+
"2 txs with different anchors at the same height, one of the anchors is missing",
904+
&[
905+
(h!("tx_1"), new_anchor(2, h!("B1"))),
906+
(h!("tx_2"), new_anchor(2, h!("B2"))),
907+
],
908+
&[(1, h!("A")), (2, h!("B1"))],
909+
&[],
910+
),
911+
new_scenario(
912+
"tx with 2 anchors of same height which are missing from the chain",
913+
&[
914+
(h!("tx"), new_anchor(3, h!("C1"))),
915+
(h!("tx"), new_anchor(3, h!("C2"))),
916+
],
917+
&[(1, h!("A")), (4, h!("D"))],
918+
&[3],
919+
),
920+
new_scenario(
921+
"tx with 2 anchors at the same height, chain has this height but does not match either anchor",
922+
&[
923+
(h!("tx"), new_anchor(4, h!("D1"))),
924+
(h!("tx"), new_anchor(4, h!("D2"))),
925+
],
926+
&[(4, h!("D3")), (5, h!("E"))],
927+
&[],
928+
),
929+
new_scenario(
930+
"tx with 2 anchors at different heights, one anchor exists in chain, should return nothing",
931+
&[
932+
(h!("tx"), new_anchor(3, h!("C"))),
933+
(h!("tx"), new_anchor(4, h!("D"))),
934+
],
935+
&[(4, h!("D")), (5, h!("E"))],
936+
&[],
937+
),
938+
new_scenario(
939+
"tx with 2 anchors at different heights, first height is already in chain with different hash, iterator should only return 2nd height",
940+
&[
941+
(h!("tx"), new_anchor(5, h!("E1"))),
942+
(h!("tx"), new_anchor(6, h!("F1"))),
943+
],
944+
&[(4, h!("D")), (5, h!("E")), (7, h!("G"))],
945+
&[6],
946+
),
947+
new_scenario(
948+
"tx with 2 anchors at different heights, neither height is in chain, both heights should be returned",
949+
&[
950+
(h!("tx"), new_anchor(3, h!("C"))),
951+
(h!("tx"), new_anchor(4, h!("D"))),
952+
],
953+
&[(1, h!("A")), (2, h!("B"))],
954+
&[3, 4],
955+
),
956+
]);
957+
}

example-crates/wallet_esplora/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
5656

5757
let (update_graph, last_active_indices) =
5858
client.update_tx_graph(keychain_spks, None, None, STOP_GAP, PARALLEL_REQUESTS)?;
59-
let get_heights = wallet.tx_graph().missing_blocks(wallet.local_chain());
59+
let get_heights = wallet.tx_graph().missing_heights(wallet.local_chain());
6060
let chain_update = client.update_local_chain(prev_tip, get_heights)?;
6161
let update = LocalUpdate {
6262
last_active_indices,

example-crates/wallet_esplora_async/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5757
let (update_graph, last_active_indices) = client
5858
.update_tx_graph(keychain_spks, None, None, STOP_GAP, PARALLEL_REQUESTS)
5959
.await?;
60-
let get_heights = wallet.tx_graph().missing_blocks(wallet.local_chain());
60+
let get_heights = wallet.tx_graph().missing_heights(wallet.local_chain());
6161
let chain_update = client.update_local_chain(prev_tip, get_heights).await?;
6262
let update = LocalUpdate {
6363
last_active_indices,

0 commit comments

Comments
 (0)