use bitcoin::consensus::encode::serialize; use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::sha256d; use bitcoin::{Address, PackedLockTime, Script, Sequence, Transaction, Txid, Witness}; pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType; use bitcoincore_rpc::jsonrpc::serde_json::{self, json}; pub use bitcoincore_rpc::{Auth, Client as RpcClient, Error as RpcError, RpcApi}; use core::ops::Deref; use core::str::FromStr; use core::time::Duration; use electrsd::bitcoind::BitcoinD; use electrsd::{bitcoind, ElectrsD}; pub use electrum_client::{Client as ElectrumClient, ElectrumApi}; #[allow(unused_imports)] use log::{debug, error, info, log_enabled, trace, Level}; use std::env; pub struct TestClient { pub bitcoind: BitcoinD, pub electrsd: ElectrsD, } impl TestClient { pub fn new(bitcoind_exe: String, electrs_exe: String) -> Self { debug!("launching {} and {}", &bitcoind_exe, &electrs_exe); let mut conf = bitcoind::Conf::default(); conf.view_stdout = log_enabled!(Level::Debug); let bitcoind = BitcoinD::with_conf(bitcoind_exe, &conf).unwrap(); let mut conf = electrsd::Conf::default(); conf.view_stderr = log_enabled!(Level::Debug); conf.http_enabled = cfg!(feature = "esplora"); let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &conf).unwrap(); let node_address = bitcoind.client.get_new_address(None, None).unwrap(); bitcoind .client .generate_to_address(101, &node_address) .unwrap(); let mut test_client = TestClient { bitcoind, electrsd }; TestClient::wait_for_block(&mut test_client, 101); test_client } fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) { // wait for electrs to index the tx exponential_backoff_poll(|| { self.electrsd.trigger().unwrap(); trace!("wait_for_tx {}", txid); self.electrsd .client .script_get_history(monitor_script) .unwrap() .iter() .position(|entry| entry.tx_hash == txid) }); } fn wait_for_block(&mut self, min_height: usize) { self.electrsd.client.block_headers_subscribe().unwrap(); loop { let header = exponential_backoff_poll(|| { self.electrsd.trigger().unwrap(); self.electrsd.client.ping().unwrap(); self.electrsd.client.block_headers_pop().unwrap() }); if header.height >= min_height { break; } } } pub fn bump_fee(&mut self, txid: &Txid) -> Txid { let tx = self.get_raw_transaction_info(txid, None).unwrap(); assert!( tx.confirmations.is_none(), "Can't bump tx {} because it's already confirmed", txid ); let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap(); let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap(); let monitor_script = Script::from_hex(&mut tx.vout[0].script_pub_key.hex.to_hex()).unwrap(); self.wait_for_tx(new_txid, &monitor_script); debug!("Bumped {}, new txid {}", txid, new_txid); new_txid } pub fn generate_manually(&mut self, txs: Vec) -> String { use bitcoin::blockdata::block::{Block, BlockHeader}; use bitcoin::blockdata::script::Builder; use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut}; use bitcoin::hash_types::{BlockHash, TxMerkleNode}; use bitcoin::hashes::Hash; let block_template: serde_json::Value = self .call("getblocktemplate", &[json!({"rules": ["segwit"]})]) .unwrap(); trace!("getblocktemplate: {:#?}", block_template); let header = BlockHeader { version: block_template["version"].as_i64().unwrap() as i32, prev_blockhash: BlockHash::from_hex( block_template["previousblockhash"].as_str().unwrap(), ) .unwrap(), merkle_root: TxMerkleNode::all_zeros(), time: block_template["curtime"].as_u64().unwrap() as u32, bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(), nonce: 0, }; debug!("header: {:#?}", header); let height = block_template["height"].as_u64().unwrap() as i64; let witness_reserved_value: Vec = sha256d::Hash::all_zeros().as_ref().into(); // burn block subsidy and fees, not a big deal let mut coinbase_tx = Transaction { version: 1, lock_time: PackedLockTime(0), input: vec![TxIn { previous_output: OutPoint::null(), script_sig: Builder::new().push_int(height).into_script(), sequence: Sequence(0xFFFFFFFF), witness: Witness::from_vec(vec![witness_reserved_value]), }], output: vec![], }; let mut txdata = vec![coinbase_tx.clone()]; txdata.extend_from_slice(&txs); let mut block = Block { header, txdata }; if let Some(witness_root) = block.witness_root() { let witness_commitment = Block::compute_witness_commitment( &witness_root, &coinbase_tx.input[0] .witness .last() .expect("Should contain the witness reserved value"), ); // now update and replace the coinbase tx let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed]; coinbase_witness_commitment_script.extend_from_slice(&witness_commitment); coinbase_tx.output.push(TxOut { value: 0, script_pubkey: coinbase_witness_commitment_script.into(), }); } block.txdata[0] = coinbase_tx; // set merkle root if let Some(merkle_root) = block.compute_merkle_root() { block.header.merkle_root = merkle_root; } assert!(block.check_merkle_root()); assert!(block.check_witness_commitment()); // now do PoW :) let target = block.header.target(); while block.header.validate_pow(&target).is_err() { block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces } let block_hex: String = serialize(&block).to_hex(); debug!("generated block hex: {}", block_hex); self.electrsd.client.block_headers_subscribe().unwrap(); let submit_result: serde_json::Value = self.call("submitblock", &[block_hex.into()]).unwrap(); debug!("submitblock: {:?}", submit_result); assert!( submit_result.is_null(), "submitblock error: {:?}", submit_result.as_str() ); self.wait_for_block(height as usize); block.header.block_hash().to_hex() } pub fn generate(&mut self, num_blocks: u64, address: Option
) -> u32 { let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap()); let hashes = self.generate_to_address(num_blocks, &address).unwrap(); let best_hash = hashes.last().unwrap(); let height = self.get_block_info(best_hash).unwrap().height; self.wait_for_block(height); debug!("Generated blocks to new height {}", height); height as u32 } pub fn invalidate(&mut self, num_blocks: u64) { self.electrsd.client.block_headers_subscribe().unwrap(); let best_hash = self.get_best_block_hash().unwrap(); let initial_height = self.get_block_info(&best_hash).unwrap().height; let mut to_invalidate = best_hash; for i in 1..=num_blocks { trace!( "Invalidating block {}/{} ({})", i, num_blocks, to_invalidate ); self.invalidate_block(&to_invalidate).unwrap(); to_invalidate = self.get_best_block_hash().unwrap(); } self.wait_for_block(initial_height - num_blocks as usize); debug!( "Invalidated {} blocks to new height of {}", num_blocks, initial_height - num_blocks as usize ); } pub fn reorg(&mut self, num_blocks: u64) { self.invalidate(num_blocks); self.generate(num_blocks, None); } pub fn get_node_address(&self, address_type: Option) -> Address { Address::from_str( &self .get_new_address(None, address_type) .unwrap() .to_string(), ) .unwrap() } } pub fn get_electrum_url() -> String { env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string()) } impl Deref for TestClient { type Target = RpcClient; fn deref(&self) -> &Self::Target { &self.bitcoind.client } } impl Default for TestClient { fn default() -> Self { let bitcoind_exe = env::var("BITCOIND_EXE") .ok() .or(bitcoind::downloaded_exe_path().ok()) .expect( "you should provide env var BITCOIND_EXE or specifiy a bitcoind version feature", ); let electrs_exe = env::var("ELECTRS_EXE") .ok() .or(electrsd::downloaded_exe_path()) .expect( "you should provide env var ELECTRS_EXE or specifiy a electrsd version feature", ); Self::new(bitcoind_exe, electrs_exe) } } fn exponential_backoff_poll(mut poll: F) -> T where F: FnMut() -> Option, { let mut delay = Duration::from_millis(64); loop { match poll() { Some(data) => break data, None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0), None => {} } std::thread::sleep(delay); } }