]> Untitled Git - bdk/commitdiff
chore: extract `TestEnv` into separate crate
authorWei Chen <wzc110@gmail.com>
Fri, 2 Feb 2024 10:33:18 +0000 (18:33 +0800)
committerWei Chen <wzc110@gmail.com>
Fri, 22 Mar 2024 09:59:35 +0000 (17:59 +0800)
`TestEnv` is extracted into its own crate to serve as a framework
for testing other block explorer APIs.

Cargo.toml
crates/bitcoind_rpc/Cargo.toml
crates/bitcoind_rpc/tests/test_emitter.rs
crates/electrum/Cargo.toml
crates/electrum/src/electrum_ext.rs
crates/esplora/Cargo.toml
crates/esplora/tests/async_ext.rs
crates/esplora/tests/blocking_ext.rs
crates/testenv/Cargo.toml [new file with mode: 0644]
crates/testenv/README.md [new file with mode: 0644]
crates/testenv/src/lib.rs [new file with mode: 0644]

index b190ba88fbec23acd53d6504268cba44b8700599..53ceb7cd47a97c52882f2faeaee5f730c2839b53 100644 (file)
@@ -8,6 +8,7 @@ members = [
     "crates/esplora",
     "crates/bitcoind_rpc",
     "crates/hwi",
+    "crates/testenv",
     "example-crates/example_cli",
     "example-crates/example_electrum",
     "example-crates/example_esplora",
index 3af59864aa0a87db258f362e789b09a31f468f66..880b57867c4207c8353a7cc93e4e812252c52866 100644 (file)
@@ -19,7 +19,7 @@ bitcoincore-rpc = { version = "0.17" }
 bdk_chain = { path = "../chain", version = "0.11", default-features = false }
 
 [dev-dependencies]
-bitcoind = { version = "0.33", features = ["25_0"] }
+bdk_testenv = { path = "../testenv", default_features = false }
 anyhow = { version = "1" }
 
 [features]
index 52d7093010ac0c006ec2fe1388798a286b783c15..2161db0df4c2c5863265b8ebc248a025df7b4dee 100644 (file)
@@ -2,160 +2,14 @@ use std::collections::{BTreeMap, BTreeSet};
 
 use bdk_bitcoind_rpc::Emitter;
 use bdk_chain::{
-    bitcoin::{Address, Amount, BlockHash, Txid},
+    bitcoin::{Address, Amount, Txid},
     keychain::Balance,
     local_chain::{self, CheckPoint, LocalChain},
     Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
 };
-use bitcoin::{
-    address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
-    secp256k1::rand::random, Block, CompactTarget, OutPoint, ScriptBuf, ScriptHash, Transaction,
-    TxIn, TxOut, WScriptHash,
-};
-use bitcoincore_rpc::{
-    bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
-    RpcApi,
-};
-
-struct TestEnv {
-    #[allow(dead_code)]
-    daemon: bitcoind::BitcoinD,
-    client: bitcoincore_rpc::Client,
-}
-
-impl TestEnv {
-    fn new() -> anyhow::Result<Self> {
-        let daemon = match std::env::var_os("TEST_BITCOIND") {
-            Some(bitcoind_path) => bitcoind::BitcoinD::new(bitcoind_path),
-            None => bitcoind::BitcoinD::from_downloaded(),
-        }?;
-        let client = bitcoincore_rpc::Client::new(
-            &daemon.rpc_url(),
-            bitcoincore_rpc::Auth::CookieFile(daemon.params.cookie_file.clone()),
-        )?;
-        Ok(Self { daemon, client })
-    }
-
-    fn mine_blocks(
-        &self,
-        count: usize,
-        address: Option<Address>,
-    ) -> anyhow::Result<Vec<BlockHash>> {
-        let coinbase_address = match address {
-            Some(address) => address,
-            None => self.client.get_new_address(None, None)?.assume_checked(),
-        };
-        let block_hashes = self
-            .client
-            .generate_to_address(count as _, &coinbase_address)?;
-        Ok(block_hashes)
-    }
-
-    fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
-        let bt = self.client.get_block_template(
-            GetBlockTemplateModes::Template,
-            &[GetBlockTemplateRules::SegWit],
-            &[],
-        )?;
-
-        let txdata = vec![Transaction {
-            version: 1,
-            lock_time: bitcoin::absolute::LockTime::from_height(0)?,
-            input: vec![TxIn {
-                previous_output: bitcoin::OutPoint::default(),
-                script_sig: ScriptBuf::builder()
-                    .push_int(bt.height as _)
-                    // randomn number so that re-mining creates unique block
-                    .push_int(random())
-                    .into_script(),
-                sequence: bitcoin::Sequence::default(),
-                witness: bitcoin::Witness::new(),
-            }],
-            output: vec![TxOut {
-                value: 0,
-                script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
-            }],
-        }];
-
-        let bits: [u8; 4] = bt
-            .bits
-            .clone()
-            .try_into()
-            .expect("rpc provided us with invalid bits");
-
-        let mut block = Block {
-            header: Header {
-                version: bitcoin::block::Version::default(),
-                prev_blockhash: bt.previous_block_hash,
-                merkle_root: TxMerkleNode::all_zeros(),
-                time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
-                bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
-                nonce: 0,
-            },
-            txdata,
-        };
-
-        block.header.merkle_root = block.compute_merkle_root().expect("must compute");
-
-        for nonce in 0..=u32::MAX {
-            block.header.nonce = nonce;
-            if block.header.target().is_met_by(block.block_hash()) {
-                break;
-            }
-        }
-
-        self.client.submit_block(&block)?;
-        Ok((bt.height as usize, block.block_hash()))
-    }
-
-    fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
-        let mut hash = self.client.get_best_block_hash()?;
-        for _ in 0..count {
-            let prev_hash = self.client.get_block_info(&hash)?.previousblockhash;
-            self.client.invalidate_block(&hash)?;
-            match prev_hash {
-                Some(prev_hash) => hash = prev_hash,
-                None => break,
-            }
-        }
-        Ok(())
-    }
-
-    fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
-        let start_height = self.client.get_block_count()?;
-        self.invalidate_blocks(count)?;
-
-        let res = self.mine_blocks(count, None);
-        assert_eq!(
-            self.client.get_block_count()?,
-            start_height,
-            "reorg should not result in height change"
-        );
-        res
-    }
-
-    fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
-        let start_height = self.client.get_block_count()?;
-        self.invalidate_blocks(count)?;
-
-        let res = (0..count)
-            .map(|_| self.mine_empty_block())
-            .collect::<Result<Vec<_>, _>>()?;
-        assert_eq!(
-            self.client.get_block_count()?,
-            start_height,
-            "reorg should not result in height change"
-        );
-        Ok(res)
-    }
-
-    fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
-        let txid = self
-            .client
-            .send_to_address(address, amount, None, None, None, None, None, None)?;
-        Ok(txid)
-    }
-}
+use bdk_testenv::TestEnv;
+use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
+use bitcoincore_rpc::RpcApi;
 
 /// Ensure that blocks are emitted in order even after reorg.
 ///
@@ -166,17 +20,22 @@ impl TestEnv {
 #[test]
 pub fn test_sync_local_chain() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
-    let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
-    let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
+    let network_tip = env.rpc_client().get_block_count()?;
+    let (mut local_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
+    let mut emitter = Emitter::new(env.rpc_client(), local_chain.tip(), 0);
 
-    // mine some blocks and returned the actual block hashes
+    // Mine some blocks and return the actual block hashes.
+    // Because initializing `ElectrsD` already mines some blocks, we must include those too when
+    // returning block hashes.
     let exp_hashes = {
-        let mut hashes = vec![env.client.get_block_hash(0)?]; // include genesis block
-        hashes.extend(env.mine_blocks(101, None)?);
+        let mut hashes = (0..=network_tip)
+            .map(|height| env.rpc_client().get_block_hash(height))
+            .collect::<Result<Vec<_>, _>>()?;
+        hashes.extend(env.mine_blocks(101 - network_tip as usize, None)?);
         hashes
     };
 
-    // see if the emitter outputs the right blocks
+    // See if the emitter outputs the right blocks.
     println!("first sync:");
     while let Some(emission) = emitter.next_block()? {
         let height = emission.block_height();
@@ -207,7 +66,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
         "final local_chain state is unexpected",
     );
 
-    // perform reorg
+    // Perform reorg.
     let reorged_blocks = env.reorg(6)?;
     let exp_hashes = exp_hashes
         .iter()
@@ -216,7 +75,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
         .cloned()
         .collect::<Vec<_>>();
 
-    // see if the emitter outputs the right blocks
+    // See if the emitter outputs the right blocks.
     println!("after reorg:");
     let mut exp_height = exp_hashes.len() - reorged_blocks.len();
     while let Some(emission) = emitter.next_block()? {
@@ -272,16 +131,25 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
 
     println!("getting new addresses!");
-    let addr_0 = env.client.get_new_address(None, None)?.assume_checked();
-    let addr_1 = env.client.get_new_address(None, None)?.assume_checked();
-    let addr_2 = env.client.get_new_address(None, None)?.assume_checked();
+    let addr_0 = env
+        .rpc_client()
+        .get_new_address(None, None)?
+        .assume_checked();
+    let addr_1 = env
+        .rpc_client()
+        .get_new_address(None, None)?
+        .assume_checked();
+    let addr_2 = env
+        .rpc_client()
+        .get_new_address(None, None)?
+        .assume_checked();
     println!("got new addresses!");
 
     println!("mining block!");
     env.mine_blocks(101, None)?;
     println!("mined blocks!");
 
-    let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
+    let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
     let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
         let mut index = SpkTxOutIndex::<usize>::default();
         index.insert_spk(0, addr_0.script_pubkey());
@@ -290,7 +158,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
         index
     });
 
-    let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
+    let emitter = &mut Emitter::new(env.rpc_client(), chain.tip(), 0);
 
     while let Some(emission) = emitter.next_block()? {
         let height = emission.block_height();
@@ -306,7 +174,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
     let exp_txids = {
         let mut txids = BTreeSet::new();
         for _ in 0..3 {
-            txids.insert(env.client.send_to_address(
+            txids.insert(env.rpc_client().send_to_address(
                 &addr_0,
                 Amount::from_sat(10_000),
                 None,
@@ -342,7 +210,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
 
     // mine a block that confirms the 3 txs
     let exp_block_hash = env.mine_blocks(1, None)?[0];
-    let exp_block_height = env.client.get_block_info(&exp_block_hash)?.height as u32;
+    let exp_block_height = env.rpc_client().get_block_info(&exp_block_hash)?.height as u32;
     let exp_anchors = exp_txids
         .iter()
         .map({
@@ -386,10 +254,10 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
 
     let env = TestEnv::new()?;
     let mut emitter = Emitter::new(
-        &env.client,
+        env.rpc_client(),
         CheckPoint::new(BlockId {
             height: 0,
-            hash: env.client.get_block_hash(0)?,
+            hash: env.rpc_client().get_block_hash(0)?,
         }),
         EMITTER_START_HEIGHT as _,
     );
@@ -463,21 +331,24 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
 
     let env = TestEnv::new()?;
     let mut emitter = Emitter::new(
-        &env.client,
+        env.rpc_client(),
         CheckPoint::new(BlockId {
             height: 0,
-            hash: env.client.get_block_hash(0)?,
+            hash: env.rpc_client().get_block_hash(0)?,
         }),
         0,
     );
 
     // setup addresses
-    let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
+    let addr_to_mine = env
+        .rpc_client()
+        .get_new_address(None, None)?
+        .assume_checked();
     let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
     let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
 
     // setup receiver
-    let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
+    let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
     let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
         let mut recv_index = SpkTxOutIndex::default();
         recv_index.insert_spk((), spk_to_track.clone());
@@ -493,7 +364,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
 
         // lock outputs that send to `addr_to_track`
         let outpoints_to_lock = env
-            .client
+            .rpc_client()
             .get_transaction(&txid, None)?
             .transaction()?
             .output
@@ -502,7 +373,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
             .filter(|(_, txo)| txo.script_pubkey == spk_to_track)
             .map(|(vout, _)| OutPoint::new(txid, vout as _))
             .collect::<Vec<_>>();
-        env.client.lock_unspent(&outpoints_to_lock)?;
+        env.rpc_client().lock_unspent(&outpoints_to_lock)?;
 
         let _ = env.mine_blocks(1, None)?;
     }
@@ -551,16 +422,19 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
 
     let env = TestEnv::new()?;
     let mut emitter = Emitter::new(
-        &env.client,
+        env.rpc_client(),
         CheckPoint::new(BlockId {
             height: 0,
-            hash: env.client.get_block_hash(0)?,
+            hash: env.rpc_client().get_block_hash(0)?,
         }),
         0,
     );
 
     // mine blocks and sync up emitter
-    let addr = env.client.get_new_address(None, None)?.assume_checked();
+    let addr = env
+        .rpc_client()
+        .get_new_address(None, None)?
+        .assume_checked();
     env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
     while emitter.next_header()?.is_some() {}
 
@@ -613,16 +487,19 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
 
     let env = TestEnv::new()?;
     let mut emitter = Emitter::new(
-        &env.client,
+        env.rpc_client(),
         CheckPoint::new(BlockId {
             height: 0,
-            hash: env.client.get_block_hash(0)?,
+            hash: env.rpc_client().get_block_hash(0)?,
         }),
         0,
     );
 
     // mine blocks to get initial balance, sync emitter up to tip
-    let addr = env.client.get_new_address(None, None)?.assume_checked();
+    let addr = env
+        .rpc_client()
+        .get_new_address(None, None)?
+        .assume_checked();
     env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
     while emitter.next_header()?.is_some() {}
 
@@ -698,16 +575,19 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
 
     let env = TestEnv::new()?;
     let mut emitter = Emitter::new(
-        &env.client,
+        env.rpc_client(),
         CheckPoint::new(BlockId {
             height: 0,
-            hash: env.client.get_block_hash(0)?,
+            hash: env.rpc_client().get_block_hash(0)?,
         }),
         0,
     );
 
     // mine blocks to get initial balance
-    let addr = env.client.get_new_address(None, None)?.assume_checked();
+    let addr = env
+        .rpc_client()
+        .get_new_address(None, None)?
+        .assume_checked();
     env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
 
     // introduce mempool tx at each block extension
@@ -725,7 +605,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
             .into_iter()
             .map(|(tx, _)| tx.txid())
             .collect::<BTreeSet<_>>(),
-        env.client
+        env.rpc_client()
             .get_raw_mempool()?
             .into_iter()
             .collect::<BTreeSet<_>>(),
@@ -744,7 +624,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
         // emission.
         // TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
         let tx_introductions = dbg!(env
-            .client
+            .rpc_client()
             .get_raw_mempool_verbose()?
             .into_iter()
             .map(|(txid, entry)| (txid, entry.height as usize))
@@ -821,10 +701,10 @@ fn no_agreement_point() -> anyhow::Result<()> {
 
     // start height is 99
     let mut emitter = Emitter::new(
-        &env.client,
+        env.rpc_client(),
         CheckPoint::new(BlockId {
             height: 0,
-            hash: env.client.get_block_hash(0)?,
+            hash: env.rpc_client().get_block_hash(0)?,
         }),
         (PREMINE_COUNT - 2) as u32,
     );
@@ -842,12 +722,12 @@ fn no_agreement_point() -> anyhow::Result<()> {
     let block_hash_100a = block_header_100a.block_hash();
 
     // get hash for block 101a
-    let block_hash_101a = env.client.get_block_hash(101)?;
+    let block_hash_101a = env.rpc_client().get_block_hash(101)?;
 
     // invalidate blocks 99a, 100a, 101a
-    env.client.invalidate_block(&block_hash_99a)?;
-    env.client.invalidate_block(&block_hash_100a)?;
-    env.client.invalidate_block(&block_hash_101a)?;
+    env.rpc_client().invalidate_block(&block_hash_99a)?;
+    env.rpc_client().invalidate_block(&block_hash_100a)?;
+    env.rpc_client().invalidate_block(&block_hash_101a)?;
 
     // mine new blocks 99b, 100b, 101b
     env.mine_blocks(3, None)?;
index 80ba41a99f52f643db0332b88cf68d844db3ab5b..60f0f23cdf45d4057df93083ed8c9ede8977b3f3 100644 (file)
@@ -15,3 +15,8 @@ readme = "README.md"
 bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
 electrum-client = { version = "0.18" }
 #rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
+
+[dev-dependencies]
+bdk_testenv = { path = "../testenv", default-features = false }
+electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
+anyhow = "1"
\ No newline at end of file
index 96c4880c4f97bd737a581375c37de05738d48576..5501b149563941e0a1fcc169a3ffb9405856da17 100644 (file)
@@ -189,7 +189,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
     ) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
         let mut request_spks = keychain_spks
             .into_iter()
-            .map(|(k, s)| (k.clone(), s.into_iter()))
+            .map(|(k, s)| (k, s.into_iter()))
             .collect::<BTreeMap<K, _>>();
         let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
 
index 4de245f9f28dca940971a172bd743aa0e57cf29d..0a7c98c1f1316ede1f4a3ed33dd129525c97684b 100644 (file)
@@ -21,6 +21,9 @@ futures = { version = "0.3.26", optional = true }
 bitcoin = { version = "0.30.0", optional = true, default-features = false }
 miniscript = { version = "10.0.0", optional = true, default-features = false }
 
+[dev-dependencies]
+bdk_testenv = { path = "../testenv", default_features = false }
+
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
 electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
 tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
index baae1d11b05ee97c1426a830876df2c98fe7e80d..6c3c9cf1f993dde6b90f206cb7b0bbc7cd890042 100644 (file)
@@ -1,68 +1,21 @@
 use bdk_esplora::EsploraAsyncExt;
+use electrsd::bitcoind::anyhow;
 use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
-use electrsd::bitcoind::{self, anyhow, BitcoinD};
-use electrsd::{Conf, ElectrsD};
-use esplora_client::{self, AsyncClient, Builder};
+use esplora_client::{self, Builder};
 use std::collections::{BTreeMap, HashSet};
 use std::str::FromStr;
 use std::thread::sleep;
 use std::time::Duration;
 
-use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
-
-struct TestEnv {
-    bitcoind: BitcoinD,
-    #[allow(dead_code)]
-    electrsd: ElectrsD,
-    client: AsyncClient,
-}
-
-impl TestEnv {
-    fn new() -> Result<Self, anyhow::Error> {
-        let bitcoind_exe =
-            bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
-        let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
-
-        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, &bitcoind, &electrs_conf)?;
-
-        let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
-        let client = Builder::new(base_url.as_str()).build_async()?;
-
-        Ok(Self {
-            bitcoind,
-            electrsd,
-            client,
-        })
-    }
-
-    fn mine_blocks(
-        &self,
-        count: usize,
-        address: Option<Address>,
-    ) -> anyhow::Result<Vec<BlockHash>> {
-        let coinbase_address = match address {
-            Some(address) => address,
-            None => self
-                .bitcoind
-                .client
-                .get_new_address(None, None)?
-                .assume_checked(),
-        };
-        let block_hashes = self
-            .bitcoind
-            .client
-            .generate_to_address(count as _, &coinbase_address)?;
-        Ok(block_hashes)
-    }
-}
+use bdk_chain::bitcoin::{Address, Amount, Txid};
+use bdk_testenv::TestEnv;
 
 #[tokio::test]
 pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
+    let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+    let client = Builder::new(base_url.as_str()).build_async()?;
+
     let receive_address0 =
         Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
     let receive_address1 =
@@ -95,12 +48,11 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
         None,
     )?;
     let _block_hashes = env.mine_blocks(1, None)?;
-    while env.client.get_height().await.unwrap() < 102 {
+    while client.get_height().await.unwrap() < 102 {
         sleep(Duration::from_millis(10))
     }
 
-    let graph_update = env
-        .client
+    let graph_update = client
         .sync(
             misc_spks.into_iter(),
             vec![].into_iter(),
@@ -143,6 +95,8 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
 #[tokio::test]
 pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
+    let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+    let client = Builder::new(base_url.as_str()).build_async()?;
     let _block_hashes = env.mine_blocks(101, None)?;
 
     // Now let's test the gap limit. First of all get a chain of 10 addresses.
@@ -182,16 +136,16 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
         None,
     )?;
     let _block_hashes = env.mine_blocks(1, None)?;
-    while env.client.get_height().await.unwrap() < 103 {
+    while client.get_height().await.unwrap() < 103 {
         sleep(Duration::from_millis(10))
     }
 
     // A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
     // will.
-    let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 2, 1).await?;
     assert!(graph_update.full_txs().next().is_none());
     assert!(active_indices.is_empty());
-    let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
     assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
     assert_eq!(active_indices[&0], 3);
 
@@ -207,18 +161,18 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
         None,
     )?;
     let _block_hashes = env.mine_blocks(1, None)?;
-    while env.client.get_height().await.unwrap() < 104 {
+    while client.get_height().await.unwrap() < 104 {
         sleep(Duration::from_millis(10))
     }
 
     // A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
     // The last active indice won't be updated in the first case but will in the second one.
-    let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1).await?;
     let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
     assert_eq!(txs.len(), 1);
     assert!(txs.contains(&txid_4th_addr));
     assert_eq!(active_indices[&0], 3);
-    let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
+    let (graph_update, active_indices) = client.full_scan(keychains, 5, 1).await?;
     let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
     assert_eq!(txs.len(), 2);
     assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
index 54c367e76c9094a967ca666eeda92f8076985cae..6225a6a6ba73d56dcc51da2491b98f864c2d6258 100644 (file)
@@ -1,16 +1,16 @@
 use bdk_chain::local_chain::LocalChain;
 use bdk_chain::BlockId;
 use bdk_esplora::EsploraExt;
+use electrsd::bitcoind::anyhow;
 use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
-use electrsd::bitcoind::{self, anyhow, BitcoinD};
-use electrsd::{Conf, ElectrsD};
-use esplora_client::{self, BlockingClient, Builder};
+use esplora_client::{self, Builder};
 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};
+use bdk_chain::bitcoin::{Address, Amount, Txid};
+use bdk_testenv::TestEnv;
 
 macro_rules! h {
     ($index:literal) => {{
@@ -26,73 +26,12 @@ macro_rules! local_chain {
     }};
 }
 
-struct TestEnv {
-    bitcoind: BitcoinD,
-    #[allow(dead_code)]
-    electrsd: ElectrsD,
-    client: BlockingClient,
-}
-
-impl TestEnv {
-    fn new() -> Result<Self, anyhow::Error> {
-        let bitcoind_exe =
-            bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
-        let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
-
-        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, &bitcoind, &electrs_conf)?;
-
-        let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
-        let client = Builder::new(base_url.as_str()).build_blocking()?;
-
-        Ok(Self {
-            bitcoind,
-            electrsd,
-            client,
-        })
-    }
-
-    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,
-        address: Option<Address>,
-    ) -> anyhow::Result<Vec<BlockHash>> {
-        let coinbase_address = match address {
-            Some(address) => address,
-            None => self
-                .bitcoind
-                .client
-                .get_new_address(None, None)?
-                .assume_checked(),
-        };
-        let block_hashes = self
-            .bitcoind
-            .client
-            .generate_to_address(count as _, &coinbase_address)?;
-        Ok(block_hashes)
-    }
-}
-
 #[test]
 pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
+    let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+    let client = Builder::new(base_url.as_str()).build_blocking()?;
+
     let receive_address0 =
         Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
     let receive_address1 =
@@ -125,11 +64,11 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
         None,
     )?;
     let _block_hashes = env.mine_blocks(1, None)?;
-    while env.client.get_height().unwrap() < 102 {
+    while client.get_height().unwrap() < 102 {
         sleep(Duration::from_millis(10))
     }
 
-    let graph_update = env.client.sync(
+    let graph_update = client.sync(
         misc_spks.into_iter(),
         vec![].into_iter(),
         vec![].into_iter(),
@@ -171,6 +110,8 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
 #[test]
 pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
+    let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+    let client = Builder::new(base_url.as_str()).build_blocking()?;
     let _block_hashes = env.mine_blocks(101, None)?;
 
     // Now let's test the gap limit. First of all get a chain of 10 addresses.
@@ -210,16 +151,16 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
         None,
     )?;
     let _block_hashes = env.mine_blocks(1, None)?;
-    while env.client.get_height().unwrap() < 103 {
+    while client.get_height().unwrap() < 103 {
         sleep(Duration::from_millis(10))
     }
 
     // A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
     // will.
-    let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 2, 1)?;
     assert!(graph_update.full_txs().next().is_none());
     assert!(active_indices.is_empty());
-    let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
     assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
     assert_eq!(active_indices[&0], 3);
 
@@ -235,18 +176,18 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
         None,
     )?;
     let _block_hashes = env.mine_blocks(1, None)?;
-    while env.client.get_height().unwrap() < 104 {
+    while client.get_height().unwrap() < 104 {
         sleep(Duration::from_millis(10))
     }
 
     // A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
     // The last active indice won't be updated in the first case but will in the second one.
-    let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
+    let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1)?;
     let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
     assert_eq!(txs.len(), 1);
     assert!(txs.contains(&txid_4th_addr));
     assert_eq!(active_indices[&0], 3);
-    let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
+    let (graph_update, active_indices) = client.full_scan(keychains, 5, 1)?;
     let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
     assert_eq!(txs.len(), 2);
     assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
@@ -273,6 +214,8 @@ fn update_local_chain() -> anyhow::Result<()> {
     };
     // so new blocks can be seen by Electrs
     let env = env.reset_electrsd()?;
+    let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+    let client = Builder::new(base_url.as_str()).build_blocking()?;
 
     struct TestCase {
         name: &'static str,
@@ -375,8 +318,7 @@ fn update_local_chain() -> anyhow::Result<()> {
         println!("Case {}: {}", i, t.name);
         let mut chain = t.chain;
 
-        let update = env
-            .client
+        let update = client
             .update_local_chain(chain.tip(), t.request_heights.iter().copied())
             .map_err(|err| {
                 anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
diff --git a/crates/testenv/Cargo.toml b/crates/testenv/Cargo.toml
new file mode 100644 (file)
index 0000000..61cd169
--- /dev/null
@@ -0,0 +1,17 @@
+[package]
+name = "bdk_testenv"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+bitcoincore-rpc = { version = "0.17" }
+bdk_chain = { path = "../chain", version = "0.11", default-features = false }
+electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
+anyhow = { version = "1" }
+
+[features]
+default = ["std"]
+std = ["bdk_chain/std"]
+serde = ["bdk_chain/serde"]
\ No newline at end of file
diff --git a/crates/testenv/README.md b/crates/testenv/README.md
new file mode 100644 (file)
index 0000000..06527fb
--- /dev/null
@@ -0,0 +1,6 @@
+# BDK TestEnv
+
+This crate sets up a regtest environment with a single [`bitcoind`] node
+connected to an [`electrs`] instance. This framework provides the infrastructure
+for testing chain source crates, e.g., [`bdk_chain`], [`bdk_electrum`],
+[`bdk_esplora`], etc.
\ No newline at end of file
diff --git a/crates/testenv/src/lib.rs b/crates/testenv/src/lib.rs
new file mode 100644 (file)
index 0000000..fa940cd
--- /dev/null
@@ -0,0 +1,237 @@
+use bdk_chain::bitcoin::{
+    address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
+    secp256k1::rand::random, Address, Amount, Block, BlockHash, CompactTarget, ScriptBuf,
+    ScriptHash, Transaction, TxIn, TxOut, Txid,
+};
+use bitcoincore_rpc::{
+    bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
+    RpcApi,
+};
+use electrsd::electrum_client::ElectrumApi;
+use std::time::Duration;
+
+/// Struct for running a regtest environment with a single `bitcoind` node with an `electrs`
+/// instance connected to it.
+pub struct TestEnv {
+    pub bitcoind: electrsd::bitcoind::BitcoinD,
+    pub electrsd: electrsd::ElectrsD,
+}
+
+impl TestEnv {
+    /// Construct a new [`TestEnv`] instance with default configurations.
+    pub fn new() -> anyhow::Result<Self> {
+        let bitcoind = match std::env::var_os("BITCOIND_EXE") {
+            Some(bitcoind_path) => electrsd::bitcoind::BitcoinD::new(bitcoind_path),
+            None => {
+                let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path()
+                    .expect(
+                "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
+                );
+                electrsd::bitcoind::BitcoinD::with_conf(
+                    bitcoind_exe,
+                    &electrsd::bitcoind::Conf::default(),
+                )
+            }
+        }?;
+
+        let mut electrsd_conf = electrsd::Conf::default();
+        electrsd_conf.http_enabled = true;
+        let electrsd = match std::env::var_os("ELECTRS_EXE") {
+            Some(env_electrs_exe) => {
+                electrsd::ElectrsD::with_conf(env_electrs_exe, &bitcoind, &electrsd_conf)
+            }
+            None => {
+                let electrs_exe = electrsd::downloaded_exe_path()
+                    .expect("electrs version feature must be enabled");
+                electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf)
+            }
+        }?;
+
+        Ok(Self { bitcoind, electrsd })
+    }
+
+    /// Exposes the [`ElectrumApi`] calls from the Electrum client.
+    pub fn electrum_client(&self) -> &impl ElectrumApi {
+        &self.electrsd.client
+    }
+
+    /// Exposes the [`RpcApi`] calls from [`bitcoincore_rpc`].
+    pub fn rpc_client(&self) -> &impl RpcApi {
+        &self.bitcoind.client
+    }
+
+    // Reset `electrsd` so that new blocks can be seen.
+    pub fn reset_electrsd(mut self) -> anyhow::Result<Self> {
+        let mut electrsd_conf = electrsd::Conf::default();
+        electrsd_conf.http_enabled = true;
+        let electrsd = match std::env::var_os("ELECTRS_EXE") {
+            Some(env_electrs_exe) => {
+                electrsd::ElectrsD::with_conf(env_electrs_exe, &self.bitcoind, &electrsd_conf)
+            }
+            None => {
+                let electrs_exe = electrsd::downloaded_exe_path()
+                    .expect("electrs version feature must be enabled");
+                electrsd::ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrsd_conf)
+            }
+        }?;
+        self.electrsd = electrsd;
+        Ok(self)
+    }
+
+    /// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
+    /// `address`.
+    pub fn mine_blocks(
+        &self,
+        count: usize,
+        address: Option<Address>,
+    ) -> anyhow::Result<Vec<BlockHash>> {
+        let coinbase_address = match address {
+            Some(address) => address,
+            None => self
+                .bitcoind
+                .client
+                .get_new_address(None, None)?
+                .assume_checked(),
+        };
+        let block_hashes = self
+            .bitcoind
+            .client
+            .generate_to_address(count as _, &coinbase_address)?;
+        Ok(block_hashes)
+    }
+
+    /// Mine a block that is guaranteed to be empty even with transactions in the mempool.
+    pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
+        let bt = self.bitcoind.client.get_block_template(
+            GetBlockTemplateModes::Template,
+            &[GetBlockTemplateRules::SegWit],
+            &[],
+        )?;
+
+        let txdata = vec![Transaction {
+            version: 1,
+            lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
+            input: vec![TxIn {
+                previous_output: bdk_chain::bitcoin::OutPoint::default(),
+                script_sig: ScriptBuf::builder()
+                    .push_int(bt.height as _)
+                    // randomn number so that re-mining creates unique block
+                    .push_int(random())
+                    .into_script(),
+                sequence: bdk_chain::bitcoin::Sequence::default(),
+                witness: bdk_chain::bitcoin::Witness::new(),
+            }],
+            output: vec![TxOut {
+                value: 0,
+                script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
+            }],
+        }];
+
+        let bits: [u8; 4] = bt
+            .bits
+            .clone()
+            .try_into()
+            .expect("rpc provided us with invalid bits");
+
+        let mut block = Block {
+            header: Header {
+                version: bdk_chain::bitcoin::block::Version::default(),
+                prev_blockhash: bt.previous_block_hash,
+                merkle_root: TxMerkleNode::all_zeros(),
+                time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
+                bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
+                nonce: 0,
+            },
+            txdata,
+        };
+
+        block.header.merkle_root = block.compute_merkle_root().expect("must compute");
+
+        for nonce in 0..=u32::MAX {
+            block.header.nonce = nonce;
+            if block.header.target().is_met_by(block.block_hash()) {
+                break;
+            }
+        }
+
+        self.bitcoind.client.submit_block(&block)?;
+        Ok((bt.height as usize, block.block_hash()))
+    }
+
+    /// This method waits for the Electrum notification indicating that a new block has been mined.
+    pub fn wait_until_electrum_sees_block(&self) -> anyhow::Result<()> {
+        self.electrsd.client.block_headers_subscribe()?;
+        let mut delay = Duration::from_millis(64);
+
+        loop {
+            self.electrsd.trigger()?;
+            self.electrsd.client.ping()?;
+            if self.electrsd.client.block_headers_pop()?.is_some() {
+                return Ok(());
+            }
+
+            if delay.as_millis() < 512 {
+                delay = delay.mul_f32(2.0);
+            }
+            std::thread::sleep(delay);
+        }
+    }
+
+    /// Invalidate a number of blocks of a given size `count`.
+    pub fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
+        let mut hash = self.bitcoind.client.get_best_block_hash()?;
+        for _ in 0..count {
+            let prev_hash = self
+                .bitcoind
+                .client
+                .get_block_info(&hash)?
+                .previousblockhash;
+            self.bitcoind.client.invalidate_block(&hash)?;
+            match prev_hash {
+                Some(prev_hash) => hash = prev_hash,
+                None => break,
+            }
+        }
+        Ok(())
+    }
+
+    /// Reorg a number of blocks of a given size `count`.
+    /// Refer to [`TestEnv::mine_empty_block`] for more information.
+    pub fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
+        let start_height = self.bitcoind.client.get_block_count()?;
+        self.invalidate_blocks(count)?;
+
+        let res = self.mine_blocks(count, None);
+        assert_eq!(
+            self.bitcoind.client.get_block_count()?,
+            start_height,
+            "reorg should not result in height change"
+        );
+        res
+    }
+
+    /// Reorg with a number of empty blocks of a given size `count`.
+    pub fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
+        let start_height = self.bitcoind.client.get_block_count()?;
+        self.invalidate_blocks(count)?;
+
+        let res = (0..count)
+            .map(|_| self.mine_empty_block())
+            .collect::<Result<Vec<_>, _>>()?;
+        assert_eq!(
+            self.bitcoind.client.get_block_count()?,
+            start_height,
+            "reorg should not result in height change"
+        );
+        Ok(res)
+    }
+
+    /// Send a tx of a given `amount` to a given `address`.
+    pub fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
+        let txid = self
+            .bitcoind
+            .client
+            .send_to_address(address, amount, None, None, None, None, None, None)?;
+        Ok(txid)
+    }
+}