-
Notifications
You must be signed in to change notification settings - Fork 329
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
chore: extract
TestEnv
into separate crate
`TestEnv` is extracted into its own crate to serve as a framework for testing other block explorer APIs.
1 parent
8db76d5
commit e8dc8d3
Showing
11 changed files
with
378 additions
and
333 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |