From 284baf7b85e25d0ca1f6c275c4c629a4ce5d3eef Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Fri, 2 Feb 2024 18:35:24 +0800 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 | 225 +++++++++++++++++++++++++ 1 file changed, 225 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..8a7b3f43f9 --- /dev/null +++ b/crates/electrum/tests/test_electrum.rs @@ -0,0 +1,225 @@ +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 bdk_testenv::TestEnv; +use electrsd::bitcoind::bitcoincore_rpc::RpcApi; + +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. + env.wait_until_electrum_sees_block()?; + let ElectrumUpdate { + chain_update, + relevant_txids, + } = client.sync(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) + .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; + 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()?; + + // Mine some blocks. + env.mine_blocks(101, None)?; + env.wait_until_electrum_sees_block()?; + 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)?; + env.wait_until_electrum_sees_block()?; + 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 = 8; + 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. + env.wait_until_electrum_sees_block()?; + let ElectrumUpdate { + chain_update, + relevant_txids, + } = client.sync(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) + .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; + let _ = recv_graph.apply_update(graph_update.clone()); + + // Retain a snapshot of all anchors before reorg process. + let initial_anchors = graph_update.all_anchors(); + + // 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)?; + + env.wait_until_electrum_sees_block()?; + let ElectrumUpdate { + chain_update, + relevant_txids, + } = client.sync(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) + .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; + + // Check to see if a new anchor is added during current reorg. + if !initial_anchors.is_superset(graph_update.all_anchors()) { + println!("New anchor added at reorg depth {}", depth); + } + 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(()) +}