+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)]
})
}
+ 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,
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(())
+}