pub mod utils;
use bdk_chain::{
bitcoin::{
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget,
ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid,
},
local_chain::CheckPoint,
BlockId,
};
use bitcoincore_rpc::{
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
RpcApi,
};
use electrsd::bitcoind::anyhow::Context;
pub use electrsd;
pub use electrsd::bitcoind;
pub use electrsd::bitcoind::anyhow;
pub use electrsd::bitcoind::bitcoincore_rpc;
pub use electrsd::electrum_client;
use electrsd::electrum_client::ElectrumApi;
use std::time::Duration;
pub struct TestEnv {
pub bitcoind: electrsd::bitcoind::BitcoinD,
pub electrsd: electrsd::ElectrsD,
}
#[derive(Debug)]
pub struct Config<'a> {
pub bitcoind: bitcoind::Conf<'a>,
pub electrsd: electrsd::Conf<'a>,
}
impl Default for Config<'_> {
fn default() -> Self {
Self {
bitcoind: bitcoind::Conf::default(),
electrsd: {
let mut conf = electrsd::Conf::default();
conf.http_enabled = true;
conf
},
}
}
}
impl TestEnv {
pub fn new() -> anyhow::Result<Self> {
TestEnv::new_with_config(Config::default())
}
pub fn new_with_config(config: Config) -> anyhow::Result<Self> {
let bitcoind_exe = match std::env::var("BITCOIND_EXE") {
Ok(path) => path,
Err(_) => bitcoind::downloaded_exe_path().context(
"you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
)?,
};
let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &config.bitcoind)?;
let electrs_exe = match std::env::var("ELECTRS_EXE") {
Ok(path) => path,
Err(_) => electrsd::downloaded_exe_path()
.context("electrs version feature must be enabled")?,
};
let electrsd = electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &config.electrsd)?;
Ok(Self { bitcoind, electrsd })
}
pub fn electrum_client(&self) -> &impl ElectrumApi {
&self.electrsd.client
}
pub fn rpc_client(&self) -> &impl RpcApi {
&self.bitcoind.client
}
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)
}
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)
}
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: transaction::Version::ONE,
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 _)
.push_int(random())
.into_script(),
sequence: bdk_chain::bitcoin::Sequence::default(),
witness: bdk_chain::bitcoin::Witness::new(),
}],
output: vec![TxOut {
value: Amount::ZERO,
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()))
}
pub fn wait_until_electrum_sees_block(&self, timeout: Duration) -> anyhow::Result<()> {
self.electrsd.client.block_headers_subscribe()?;
let delay = Duration::from_millis(200);
let start = std::time::Instant::now();
while start.elapsed() < timeout {
self.electrsd.trigger()?;
self.electrsd.client.ping()?;
if self.electrsd.client.block_headers_pop()?.is_some() {
return Ok(());
}
std::thread::sleep(delay);
}
Err(anyhow::Error::msg(
"Timed out waiting for Electrsd to get block header",
))
}
pub fn wait_until_electrum_sees_txid(
&self,
txid: Txid,
timeout: Duration,
) -> anyhow::Result<()> {
let delay = Duration::from_millis(200);
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if self.electrsd.client.transaction_get(&txid).is_ok() {
return Ok(());
}
std::thread::sleep(delay);
}
Err(anyhow::Error::msg(
"Timed out waiting for Electrsd to get transaction",
))
}
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(())
}
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
}
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)
}
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)
}
pub fn make_checkpoint_tip(&self) -> CheckPoint {
CheckPoint::from_block_ids((0_u32..).map_while(|height| {
self.bitcoind
.client
.get_block_hash(height as u64)
.ok()
.map(|hash| BlockId { height, hash })
}))
.expect("must craft tip")
}
pub fn genesis_hash(&self) -> anyhow::Result<BlockHash> {
let hash = self.bitcoind.client.get_block_hash(0)?;
Ok(hash)
}
}
#[cfg(test)]
mod test {
use crate::TestEnv;
use core::time::Duration;
use electrsd::bitcoind::{anyhow::Result, bitcoincore_rpc::RpcApi};
#[test]
fn test_reorg_is_detected_in_electrsd() -> Result<()> {
let env = TestEnv::new()?;
env.mine_blocks(101, None)?;
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
let height = env.bitcoind.client.get_block_count()?;
let blocks = (0..=height)
.map(|i| env.bitcoind.client.get_block_hash(i))
.collect::<Result<Vec<_>, _>>()?;
env.reorg(6)?;
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
let reorged_height = env.bitcoind.client.get_block_count()?;
let reorged_blocks = (0..=height)
.map(|i| env.bitcoind.client.get_block_hash(i))
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(height, reorged_height);
for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() {
match i <= height as usize - 6 {
true => assert_eq!(block, reorged_block),
false => assert_ne!(block, reorged_block),
}
}
Ok(())
}
}