From 6f824cf325c6c882b00742d814be1d72602f05a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 12 Jan 2024 01:25:17 +0800 Subject: [PATCH] test(esplora): introduce test cases for `update_local_chain` --- crates/esplora/tests/blocking_ext.rs | 190 ++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 1 deletion(-) diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index 50b19d1c..0959136c 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -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 { + 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::>() + }; + // 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::>(); + + 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::>(); + + 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(()) +}