diff --git a/Cargo.toml b/Cargo.toml index b190ba88fb..3b0c090b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ "crates/electrum", "crates/esplora", "crates/bitcoind_rpc", - "crates/hwi", + "crates/testenv", "example-crates/example_cli", "example-crates/example_electrum", "example-crates/example_esplora", diff --git a/crates/bitcoind_rpc/Cargo.toml b/crates/bitcoind_rpc/Cargo.toml index 9532dd6c93..763667cbf2 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -19,7 +19,7 @@ bitcoincore-rpc = { version = "0.17" } bdk_chain = { path = "../chain", version = "0.9", default-features = false } [dev-dependencies] -bitcoind = { version = "0.33", features = ["25_0"] } +bdk_testenv = { path = "../testenv", default_features = false } anyhow = { version = "1" } [features] diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 52d7093010..2161db0df4 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -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 { - 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
, - ) -> anyhow::Result> { - 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> { - 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> { - let start_height = self.client.get_block_count()?; - self.invalidate_blocks(count)?; - - let res = (0..count) - .map(|_| self.mine_empty_block()) - .collect::, _>>()?; - assert_eq!( - self.client.get_block_count()?, - start_height, - "reorg should not result in height change" - ); - Ok(res) - } - - fn send(&self, address: &Address, amount: Amount) -> anyhow::Result { - 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::, _>>()?; + 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::>(); - // 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::::new({ let mut index = SpkTxOutIndex::::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::::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::>(); - 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::>(), - env.client + env.rpc_client() .get_raw_mempool()? .into_iter() .collect::>(), @@ -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)?; diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index cf72e3e43b..33086b7235 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -15,3 +15,8 @@ readme = "README.md" bdk_chain = { path = "../chain", version = "0.9.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 diff --git a/crates/electrum/src/electrum_ext.rs b/crates/electrum/src/electrum_ext.rs index 96c4880c4f..5501b14956 100644 --- a/crates/electrum/src/electrum_ext.rs +++ b/crates/electrum/src/electrum_ext.rs @@ -189,7 +189,7 @@ impl ElectrumExt for A { ) -> Result<(ElectrumUpdate, BTreeMap), 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::>(); let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new(); diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index 6fbe8d5b7f..749349d664 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -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"] } diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index baae1d11b0..6c3c9cf1f9 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -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 { - 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
, - ) -> anyhow::Result> { - 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)); diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index 54c367e76c..6225a6a6ba 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -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 { - 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 { - 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
, - ) -> anyhow::Result> { - 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 index 0000000000..99681a2f87 --- /dev/null +++ b/crates/testenv/Cargo.toml @@ -0,0 +1,19 @@ +[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] +# For no-std, remember to enable the bitcoin/no-std feature +# bitcoin = { version = "0.30", default-features = false } +bitcoincore-rpc = { version = "0.17" } +bdk_chain = { path = "../chain", version = "0.9", 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 index 0000000000..06527fb712 --- /dev/null +++ b/crates/testenv/README.md @@ -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 index 0000000000..6f28dec75f --- /dev/null +++ b/crates/testenv/src/lib.rs @@ -0,0 +1,233 @@ +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 { + 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 electrsd = match std::env::var_os("ELECTRS_EXE") { + Some(env_electrs_exe) => electrsd::ElectrsD::new(env_electrs_exe, &bitcoind), + None => { + let mut electrsd_conf = electrsd::Conf::default(); + electrsd_conf.http_enabled = true; + 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 { + let electrsd = match std::env::var_os("ELECTRS_EXE") { + Some(env_electrs_exe) => electrsd::ElectrsD::new(env_electrs_exe, &self.bitcoind), + None => { + let mut electrsd_conf = electrsd::Conf::default(); + electrsd_conf.http_enabled = true; + 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
, + ) -> anyhow::Result> { + 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> { + 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> { + let start_height = self.bitcoind.client.get_block_count()?; + self.invalidate_blocks(count)?; + + let res = (0..count) + .map(|_| self.mine_empty_block()) + .collect::, _>>()?; + 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, amount: Amount) -> anyhow::Result { + let txid = self + .bitcoind + .client + .send_to_address(address, amount, None, None, None, None, None, None)?; + Ok(txid) + } +}