From 79412aaa06ef2a57ea6984c9e995c11c41714bee Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Thu, 12 Oct 2023 16:09:14 +0800 Subject: [PATCH] chore: extract `TestEnv` into separate crate `TestEnv` is extracted into its own crate to serve as a framework for testing other block explorer APIs. --- Cargo.toml | 1 + crates/bitcoind_rpc/Cargo.toml | 1 + crates/bitcoind_rpc/tests/test_emitter.rs | 152 +--------------------- crates/testenv/Cargo.toml | 20 +++ crates/testenv/src/lib.rs | 149 +++++++++++++++++++++ 5 files changed, 174 insertions(+), 149 deletions(-) create mode 100644 crates/testenv/Cargo.toml create mode 100644 crates/testenv/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 0e1efc902f..d9c9f2e97d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/electrum", "crates/esplora", "crates/bitcoind_rpc", + "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 7b91be1f3c..80e4a219c5 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -19,6 +19,7 @@ bitcoincore-rpc = { version = "0.17" } bdk_chain = { path = "../chain", version = "0.6", default-features = false } [dev-dependencies] +testenv = { path = "../testenv", version = "0.1.0", default_features = false } bitcoind = { version = "0.33", features = ["25_0"] } anyhow = { version = "1" } diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index f0bbd3d158..3264b96d79 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -7,155 +7,9 @@ use bdk_chain::{ 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 bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash}; +use bitcoincore_rpc::RpcApi; +use testenv::TestEnv; fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Update { let this_id = BlockId { diff --git a/crates/testenv/Cargo.toml b/crates/testenv/Cargo.toml new file mode 100644 index 0000000000..0d591b2b72 --- /dev/null +++ b/crates/testenv/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "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.6", default-features = false } +bdk_bitcoind_rpc = { path = "../bitcoind_rpc", version = "0.1.0", default-features = false } +bitcoind = { version = "0.33", features = ["25_0"] } +anyhow = { version = "1" } + +[features] +default = ["std"] +std = ["bitcoin/std", "bdk_chain/std"] +serde = ["bitcoin/serde", "bdk_chain/serde"] \ 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..7859ad9679 --- /dev/null +++ b/crates/testenv/src/lib.rs @@ -0,0 +1,149 @@ +use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid}; +use bitcoin::{ + address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash, + secp256k1::rand::random, Block, CompactTarget, ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, +}; +use bitcoincore_rpc::{ + bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules}, + RpcApi, +}; + +pub struct TestEnv { + #[allow(dead_code)] + pub daemon: bitcoind::BitcoinD, + pub client: bitcoincore_rpc::Client, +} + +impl TestEnv { + pub 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 }) + } + + pub 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) + } + + pub 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())) + } + + pub 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(()) + } + + pub 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 + } + + pub 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) + } + + pub 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) + } +}