/// Find missing block heights of `chain`.
///
/// This works by scanning through anchors, and seeing whether the anchor block of the anchor
- /// exists in the [`LocalChain`].
- pub fn missing_blocks<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
- let mut last_block = Option::<BlockId>::None;
+ /// exists in the [`LocalChain`]. The returned iterator does not output duplicate heights.
+ pub fn missing_heights<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
+ // Map of txids to skip.
+ //
+ // Usually, if a height of a tx anchor is missing from the chain, we would want to return
+ // this height in the iterator. The exception is when the tx is confirmed in chain. All the
+ // other missing-height anchors of this tx can be skipped.
+ //
+ // * Some(true) => skip all anchors of this txid
+ // * Some(false) => do not skip anchors of this txid
+ // * None => we do not know whether we can skip this txid
+ let mut txids_to_skip = HashMap::<Txid, bool>::new();
+
+ // Keeps track of the last height emitted so we don't double up.
+ let mut last_height_emitted = Option::<u32>::None;
+
self.anchors
.iter()
- .map(|(a, _)| a.anchor_block())
- .filter(move |block| {
- if last_block.as_ref() == Some(block) {
- false
- } else {
- last_block = Some(*block);
+ .filter(move |(_, txid)| {
+ let skip = *txids_to_skip.entry(*txid).or_insert_with(|| {
+ let tx_anchors = match self.txs.get(txid) {
+ Some((_, anchors, _)) => anchors,
+ None => return true,
+ };
+ let mut has_missing_height = false;
+ for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
+ match chain.heights().get(&anchor_block.height) {
+ None => {
+ has_missing_height = true;
+ continue;
+ }
+ Some(chain_hash) => {
+ if chain_hash == &anchor_block.hash {
+ return true;
+ }
+ }
+ }
+ }
+ !has_missing_height
+ });
+ #[cfg(feature = "std")]
+ debug_assert!({
+ println!("txid={} skip={}", txid, skip);
true
- }
+ });
+ !skip
})
- .filter_map(|block| match chain.heights().get(&block.height) {
- Some(chain_hash) if *chain_hash == block.hash => None,
- _ => Some(block.height),
+ .filter_map(move |(a, _)| {
+ let anchor_block = a.anchor_block();
+ if Some(anchor_block.height) != last_height_emitted
+ && !chain.heights().contains_key(&anchor_block.height)
+ {
+ last_height_emitted = Some(anchor_block.height);
+ Some(anchor_block.height)
+ } else {
+ None
+ }
})
}
collections::*,
local_chain::LocalChain,
tx_graph::{Additions, TxGraph},
- Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
+ Anchor, Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
};
use bitcoin::{
hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
);
}
}
+
+#[test]
+fn test_missing_blocks() {
+ /// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.
+ #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, core::hash::Hash)]
+ struct TestAnchor(BlockId);
+
+ impl Anchor for TestAnchor {
+ fn anchor_block(&self) -> BlockId {
+ self.0
+ }
+ }
+
+ struct Scenario<'a> {
+ name: &'a str,
+ graph: TxGraph<TestAnchor>,
+ chain: LocalChain,
+ exp_heights: &'a [u32],
+ }
+
+ const fn new_anchor(height: u32, hash: BlockHash) -> TestAnchor {
+ TestAnchor(BlockId { height, hash })
+ }
+
+ fn new_scenario<'a>(
+ name: &'a str,
+ graph_anchors: &'a [(Txid, TestAnchor)],
+ chain: &'a [(u32, BlockHash)],
+ exp_heights: &'a [u32],
+ ) -> Scenario<'a> {
+ Scenario {
+ name,
+ graph: {
+ let mut g = TxGraph::default();
+ for (txid, anchor) in graph_anchors {
+ let _ = g.insert_anchor(*txid, anchor.clone());
+ }
+ g
+ },
+ chain: {
+ let mut c = LocalChain::default();
+ for (height, hash) in chain {
+ let _ = c.insert_block(BlockId {
+ height: *height,
+ hash: *hash,
+ });
+ }
+ c
+ },
+ exp_heights,
+ }
+ }
+
+ fn run(scenarios: &[Scenario]) {
+ for scenario in scenarios {
+ let Scenario {
+ name,
+ graph,
+ chain,
+ exp_heights,
+ } = scenario;
+
+ let heights = graph.missing_heights(chain).collect::<Vec<_>>();
+ assert_eq!(&heights, exp_heights, "scenario: {}", name);
+ }
+ }
+
+ run(&[
+ new_scenario(
+ "2 txs with the same anchor (2:B) which is missing from chain",
+ &[
+ (h!("tx_1"), new_anchor(2, h!("B"))),
+ (h!("tx_2"), new_anchor(2, h!("B"))),
+ ],
+ &[(1, h!("A")), (3, h!("C"))],
+ &[2],
+ ),
+ new_scenario(
+ "2 txs with different anchors at the same height, one of the anchors is missing",
+ &[
+ (h!("tx_1"), new_anchor(2, h!("B1"))),
+ (h!("tx_2"), new_anchor(2, h!("B2"))),
+ ],
+ &[(1, h!("A")), (2, h!("B1"))],
+ &[],
+ ),
+ new_scenario(
+ "tx with 2 anchors of same height which are missing from the chain",
+ &[
+ (h!("tx"), new_anchor(3, h!("C1"))),
+ (h!("tx"), new_anchor(3, h!("C2"))),
+ ],
+ &[(1, h!("A")), (4, h!("D"))],
+ &[3],
+ ),
+ new_scenario(
+ "tx with 2 anchors at the same height, chain has this height but does not match either anchor",
+ &[
+ (h!("tx"), new_anchor(4, h!("D1"))),
+ (h!("tx"), new_anchor(4, h!("D2"))),
+ ],
+ &[(4, h!("D3")), (5, h!("E"))],
+ &[],
+ ),
+ new_scenario(
+ "tx with 2 anchors at different heights, one anchor exists in chain, should return nothing",
+ &[
+ (h!("tx"), new_anchor(3, h!("C"))),
+ (h!("tx"), new_anchor(4, h!("D"))),
+ ],
+ &[(4, h!("D")), (5, h!("E"))],
+ &[],
+ ),
+ new_scenario(
+ "tx with 2 anchors at different heights, first height is already in chain with different hash, iterator should only return 2nd height",
+ &[
+ (h!("tx"), new_anchor(5, h!("E1"))),
+ (h!("tx"), new_anchor(6, h!("F1"))),
+ ],
+ &[(4, h!("D")), (5, h!("E")), (7, h!("G"))],
+ &[6],
+ ),
+ new_scenario(
+ "tx with 2 anchors at different heights, neither height is in chain, both heights should be returned",
+ &[
+ (h!("tx"), new_anchor(3, h!("C"))),
+ (h!("tx"), new_anchor(4, h!("D"))),
+ ],
+ &[(1, h!("A")), (2, h!("B"))],
+ &[3, 4],
+ ),
+ ]);
+}