]> Untitled Git - bdk/commitdiff
test(esplora): introduce test cases for `update_local_chain`
author志宇 <hello@evanlinjin.me>
Thu, 11 Jan 2024 17:25:17 +0000 (01:25 +0800)
committer志宇 <hello@evanlinjin.me>
Fri, 19 Jan 2024 15:17:54 +0000 (23:17 +0800)
crates/esplora/tests/blocking_ext.rs

index 50b19d1ccd686d916df32b1f75ba154e161610c3..0959136cf9441fa1e877a3f434d739ed84fe091b 100644 (file)
@@ -1,15 +1,31 @@
+use bdk_chain::local_chain::LocalChain;
+use bdk_chain::BlockId;
 use bdk_esplora::EsploraExt;
 use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
 use electrsd::bitcoind::{self, anyhow, BitcoinD};
 use electrsd::{Conf, ElectrsD};
 use esplora_client::{self, BlockingClient, Builder};
-use std::collections::{BTreeMap, HashSet};
+use std::collections::{BTreeMap, BTreeSet, HashSet};
 use std::str::FromStr;
 use std::thread::sleep;
 use std::time::Duration;
 
 use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
 
+macro_rules! h {
+    ($index:literal) => {{
+        bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
+    }};
+}
+
+macro_rules! local_chain {
+    [ $(($height:expr, $block_hash:expr)), * ] => {{
+        #[allow(unused_mut)]
+        bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
+            .expect("chain must have genesis block")
+    }};
+}
+
 struct TestEnv {
     bitcoind: BitcoinD,
     #[allow(dead_code)]
@@ -39,6 +55,20 @@ impl TestEnv {
         })
     }
 
+    fn reset_electrsd(mut self) -> anyhow::Result<Self> {
+        let mut electrs_conf = Conf::default();
+        electrs_conf.http_enabled = true;
+        let electrs_exe =
+            electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
+        let electrsd = ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrs_conf)?;
+
+        let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
+        let client = Builder::new(base_url.as_str()).build_blocking()?;
+        self.electrsd = electrsd;
+        self.client = client;
+        Ok(self)
+    }
+
     fn mine_blocks(
         &self,
         count: usize,
@@ -202,3 +232,161 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
 
     Ok(())
 }
+
+#[test]
+fn update_local_chain() -> anyhow::Result<()> {
+    const TIP_HEIGHT: u32 = 50;
+
+    let env = TestEnv::new()?;
+    let b = {
+        let bdc = &env.bitcoind.client;
+        assert_eq!(bdc.get_block_count()?, 1);
+        [(0, bdc.get_block_hash(0)?), (1, bdc.get_block_hash(1)?)]
+            .into_iter()
+            .chain((2..).zip(env.mine_blocks((TIP_HEIGHT - 1) as usize, None)?))
+            .collect::<BTreeMap<_, _>>()
+    };
+    // so new blocks can be seen by Electrs
+    let env = env.reset_electrsd()?;
+
+    struct TestCase {
+        name: &'static str,
+        chain: LocalChain,
+        heights: &'static [u32],
+        exp_update_heights: &'static [u32],
+    }
+
+    let test_cases = [
+        TestCase {
+            name: "request_later_blocks",
+            chain: local_chain![(0, b[&0]), (21, b[&21])],
+            heights: &[22, 25, 28],
+            exp_update_heights: &[21, 22, 25, 28],
+        },
+        TestCase {
+            name: "request_prev_blocks",
+            chain: local_chain![(0, b[&0]), (1, b[&1]), (5, b[&5])],
+            heights: &[4],
+            exp_update_heights: &[4, 5],
+        },
+        TestCase {
+            name: "request_prev_blocks_2",
+            chain: local_chain![(0, b[&0]), (1, b[&1]), (10, b[&10])],
+            heights: &[4, 6],
+            exp_update_heights: &[4, 6, 10],
+        },
+        TestCase {
+            name: "request_later_and_prev_blocks",
+            chain: local_chain![(0, b[&0]), (7, b[&7]), (11, b[&11])],
+            heights: &[8, 9, 15],
+            exp_update_heights: &[8, 9, 11, 15],
+        },
+        TestCase {
+            name: "request_tip_only",
+            chain: local_chain![(0, b[&0]), (5, b[&5]), (49, b[&49])],
+            heights: &[TIP_HEIGHT],
+            exp_update_heights: &[49],
+        },
+        TestCase {
+            name: "request_nothing",
+            chain: local_chain![(0, b[&0]), (13, b[&13]), (23, b[&23])],
+            heights: &[],
+            exp_update_heights: &[23],
+        },
+        TestCase {
+            name: "request_nothing_during_reorg",
+            chain: local_chain![(0, b[&0]), (13, b[&13]), (23, h!("23"))],
+            heights: &[],
+            exp_update_heights: &[13, 23],
+        },
+        TestCase {
+            name: "request_nothing_during_reorg_2",
+            chain: local_chain![(0, b[&0]), (21, b[&21]), (22, h!("22")), (23, h!("23"))],
+            heights: &[],
+            exp_update_heights: &[21, 22, 23],
+        },
+        TestCase {
+            name: "request_prev_blocks_during_reorg",
+            chain: local_chain![(0, b[&0]), (21, b[&21]), (22, h!("22")), (23, h!("23"))],
+            heights: &[17, 20],
+            exp_update_heights: &[17, 20, 21, 22, 23],
+        },
+        TestCase {
+            name: "request_later_blocks_during_reorg",
+            chain: local_chain![(0, b[&0]), (9, b[&9]), (22, h!("22")), (23, h!("23"))],
+            heights: &[25, 27],
+            exp_update_heights: &[9, 22, 23, 25, 27],
+        },
+        TestCase {
+            name: "request_later_blocks_during_reorg_2",
+            chain: local_chain![(0, b[&0]), (9, h!("9"))],
+            heights: &[10],
+            exp_update_heights: &[0, 9, 10],
+        },
+        TestCase {
+            name: "request_later_and_prev_blocks_during_reorg",
+            chain: local_chain![(0, b[&0]), (1, b[&1]), (9, h!("9"))],
+            heights: &[8, 11],
+            exp_update_heights: &[1, 8, 9, 11],
+        },
+    ];
+
+    for (i, t) in test_cases.into_iter().enumerate() {
+        println!("Case {}: {}", i, t.name);
+        let mut chain = t.chain;
+
+        let update = env
+            .client
+            .update_local_chain(chain.tip(), t.heights.iter().copied())
+            .map_err(|err| {
+                anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
+            })?;
+
+        let update_blocks = update
+            .tip
+            .iter()
+            .map(|cp| cp.block_id())
+            .collect::<BTreeSet<_>>();
+
+        let exp_update_blocks = t
+            .exp_update_heights
+            .iter()
+            .map(|&height| {
+                let hash = b[&height];
+                BlockId { height, hash }
+            })
+            .chain(
+                // Electrs Esplora `get_block` call fetches 10 blocks which is included in the
+                // update
+                b.range(TIP_HEIGHT - 9..)
+                    .map(|(&height, &hash)| BlockId { height, hash }),
+            )
+            .collect::<BTreeSet<_>>();
+
+        assert_eq!(
+            update_blocks, exp_update_blocks,
+            "[{}:{}] unexpected update",
+            i, t.name
+        );
+
+        let _ = chain
+            .apply_update(update)
+            .unwrap_or_else(|err| panic!("[{}:{}] update failed to apply: {}", i, t.name, err));
+
+        // all requested heights must exist in the final chain
+        for height in t.heights {
+            let exp_blockhash = b.get(height).expect("block must exist in bitcoind");
+            assert_eq!(
+                chain.blocks().get(height),
+                Some(exp_blockhash),
+                "[{}:{}] block {}:{} must exist in final chain",
+                i,
+                t.name,
+                height,
+                exp_blockhash
+            );
+        }
+    }
+
+    Ok(())
+}