From a8400359ed32a21b078c92430563bfc758bba406 Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Wed, 13 Dec 2023 15:10:38 -0500 Subject: [PATCH] test(electrum): added scan and reorg tests Added scan and reorg tests to check electrum functionality using `TestEnv`. --- crates/electrum/tests/test_electrum.rs | 238 +++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 crates/electrum/tests/test_electrum.rs diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs new file mode 100644 index 0000000000..7d18526e92 --- /dev/null +++ b/crates/electrum/tests/test_electrum.rs @@ -0,0 +1,238 @@ +use anyhow::Result; +use bdk_chain::{ + bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}, + keychain::Balance, + local_chain::LocalChain, + ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex, +}; +use bdk_electrum::{ElectrumExt, ElectrumUpdate}; +use electrsd::bitcoind::bitcoincore_rpc::RpcApi; +use electrum_client::ElectrumApi; +use std::time::Duration; +use testenv::TestEnv; + +fn wait_for_block(env: &TestEnv, client: &electrum_client::Client) -> Result<()> { + client.block_headers_subscribe()?; + let mut delay = Duration::from_millis(64); + + loop { + env.electrsd.trigger()?; + client.ping()?; + if client.block_headers_pop()?.is_some() { + return Ok(()); + } + + if delay.as_millis() < 512 { + delay = delay.mul_f32(2.0); + } + std::thread::sleep(delay); + } +} + +fn get_balance( + recv_chain: &LocalChain, + recv_graph: &IndexedTxGraph>, +) -> Result { + let chain_tip = recv_chain.tip().block_id(); + let outpoints = recv_graph.index.outpoints().clone(); + let balance = recv_graph + .graph() + .balance(recv_chain, chain_tip, outpoints, |_, _| true); + Ok(balance) +} + +/// Ensure that [`ElectrumExt`] can sync properly. +/// +/// 1. Mine 101 blocks. +/// 2. Send a tx. +/// 3. Mine extra block to confirm sent tx. +/// 4. Check [`Balance`] to ensure tx is confirmed. +#[test] +fn scan_detects_confirmed_tx() -> Result<()> { + const SEND_AMOUNT: Amount = Amount::from_sat(10_000); + + let env = TestEnv::new()?; + let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; + + // Setup addresses. + let addr_to_mine = env + .bitcoind + .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, bdk_chain::bitcoin::Network::Regtest)?; + + // Setup receiver. + let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.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()); + recv_index + }); + + // Mine some blocks. + env.mine_blocks(101, Some(addr_to_mine))?; + + // Create transaction that is tracked by our receiver. + env.send(&addr_to_track, SEND_AMOUNT)?; + + // Mine a block to confirm sent tx. + env.mine_blocks(1, None)?; + + // Sync up to tip. + wait_for_block(&env, &client)?; + let ElectrumUpdate { + chain_update, + relevant_txids, + } = client.scan_without_keychain(recv_chain.tip(), [spk_to_track], None, None, 5)?; + + let missing = relevant_txids.missing_full_txs(recv_graph.graph()); + let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; + let _ = recv_chain.apply_update(chain_update); + let _ = recv_graph.apply_update(graph_update); + + // Check to see if tx is confirmed. + assert_eq!( + get_balance(&recv_chain, &recv_graph)?, + Balance { + confirmed: SEND_AMOUNT.to_sat(), + ..Balance::default() + }, + ); + + Ok(()) +} + +#[test] +fn test_reorg_is_detected_in_electrsd() -> Result<()> { + let env = TestEnv::new()?; + let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; + + // Mine some blocks. + env.mine_blocks(101, None)?; + wait_for_block(&env, &client)?; + let height = env.bitcoind.client.get_block_count()?; + let blocks = (0..=height) + .map(|i| env.bitcoind.client.get_block_hash(i)) + .collect::, _>>()?; + + // Perform reorg on six blocks. + env.reorg(6)?; + wait_for_block(&env, &client)?; + let reorged_height = env.bitcoind.client.get_block_count()?; + let reorged_blocks = (0..=height) + .map(|i| env.bitcoind.client.get_block_hash(i)) + .collect::, _>>()?; + + assert_eq!(height, reorged_height); + + // Block hashes should not be equal on the six reorged blocks. + 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(()) +} + +/// Ensure that confirmed txs that are reorged become unconfirmed. +/// +/// 1. Mine 101 blocks. +/// 2. Mine 11 blocks with a confirmed tx in each. +/// 3. Perform 11 separate reorgs on each block with a confirmed tx. +/// 4. Check [`Balance`] after each reorg to ensure unconfirmed amount is correct. +#[test] +fn tx_can_become_unconfirmed_after_reorg() -> Result<()> { + const REORG_COUNT: usize = 11; + const SEND_AMOUNT: Amount = Amount::from_sat(10_000); + + let env = TestEnv::new()?; + let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; + + // Setup addresses. + let addr_to_mine = env + .bitcoind + .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, bdk_chain::bitcoin::Network::Regtest)?; + + // Setup receiver. + let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.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()); + recv_index + }); + + // Mine some blocks. + env.mine_blocks(101, Some(addr_to_mine))?; + + // Create transactions that are tracked by our receiver. + for _ in 0..REORG_COUNT { + env.send(&addr_to_track, SEND_AMOUNT)?; + env.mine_blocks(1, None)?; + } + + // Sync up to tip. + wait_for_block(&env, &client)?; + let ElectrumUpdate { + chain_update, + relevant_txids, + } = client.scan_without_keychain(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?; + + let missing = relevant_txids.missing_full_txs(recv_graph.graph()); + let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; + let _ = recv_chain.apply_update(chain_update); + let _ = recv_graph.apply_update(graph_update); + + // Check if initial balance is correct. + assert_eq!( + get_balance(&recv_chain, &recv_graph)?, + Balance { + confirmed: SEND_AMOUNT.to_sat() * REORG_COUNT as u64, + ..Balance::default() + }, + "initial balance must be correct", + ); + + // Perform reorgs with different depths. + for depth in 1..=REORG_COUNT { + env.reorg_empty_blocks(depth)?; + + wait_for_block(&env, &client)?; + let ElectrumUpdate { + chain_update, + relevant_txids, + } = client.scan_without_keychain( + recv_chain.tip(), + [spk_to_track.clone()], + None, + None, + 5, + )?; + + let missing = relevant_txids.missing_full_txs(recv_graph.graph()); + let graph_update = + relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; + let _ = recv_chain.apply_update(chain_update); + let _ = recv_graph.apply_update(graph_update); + + assert_eq!( + get_balance(&recv_chain, &recv_graph)?, + Balance { + confirmed: SEND_AMOUNT.to_sat() * (REORG_COUNT - depth) as u64, + trusted_pending: SEND_AMOUNT.to_sat() * depth as u64, + ..Balance::default() + }, + "reorg_count: {}", + depth, + ); + } + + Ok(()) +}