|
| 1 | +use anyhow::Result; |
| 2 | +use bdk_chain::{ |
| 3 | + bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}, |
| 4 | + keychain::Balance, |
| 5 | + local_chain::LocalChain, |
| 6 | + ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex, |
| 7 | +}; |
| 8 | +use bdk_electrum::{ElectrumExt, ElectrumUpdate}; |
| 9 | +use bdk_testenv::TestEnv; |
| 10 | +use electrsd::bitcoind::bitcoincore_rpc::RpcApi; |
| 11 | + |
| 12 | +fn get_balance( |
| 13 | + recv_chain: &LocalChain, |
| 14 | + recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>, |
| 15 | +) -> Result<Balance> { |
| 16 | + let chain_tip = recv_chain.tip().block_id(); |
| 17 | + let outpoints = recv_graph.index.outpoints().clone(); |
| 18 | + let balance = recv_graph |
| 19 | + .graph() |
| 20 | + .balance(recv_chain, chain_tip, outpoints, |_, _| true); |
| 21 | + Ok(balance) |
| 22 | +} |
| 23 | + |
| 24 | +/// Ensure that [`ElectrumExt`] can sync properly. |
| 25 | +/// |
| 26 | +/// 1. Mine 101 blocks. |
| 27 | +/// 2. Send a tx. |
| 28 | +/// 3. Mine extra block to confirm sent tx. |
| 29 | +/// 4. Check [`Balance`] to ensure tx is confirmed. |
| 30 | +#[test] |
| 31 | +fn scan_detects_confirmed_tx() -> Result<()> { |
| 32 | + const SEND_AMOUNT: Amount = Amount::from_sat(10_000); |
| 33 | + |
| 34 | + let env = TestEnv::new()?; |
| 35 | + let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; |
| 36 | + |
| 37 | + // Setup addresses. |
| 38 | + let addr_to_mine = env |
| 39 | + .bitcoind |
| 40 | + .client |
| 41 | + .get_new_address(None, None)? |
| 42 | + .assume_checked(); |
| 43 | + let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros()); |
| 44 | + let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?; |
| 45 | + |
| 46 | + // Setup receiver. |
| 47 | + let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); |
| 48 | + let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({ |
| 49 | + let mut recv_index = SpkTxOutIndex::default(); |
| 50 | + recv_index.insert_spk((), spk_to_track.clone()); |
| 51 | + recv_index |
| 52 | + }); |
| 53 | + |
| 54 | + // Mine some blocks. |
| 55 | + env.mine_blocks(101, Some(addr_to_mine))?; |
| 56 | + |
| 57 | + // Create transaction that is tracked by our receiver. |
| 58 | + env.send(&addr_to_track, SEND_AMOUNT)?; |
| 59 | + |
| 60 | + // Mine a block to confirm sent tx. |
| 61 | + env.mine_blocks(1, None)?; |
| 62 | + |
| 63 | + // Sync up to tip. |
| 64 | + env.wait_until_electrum_sees_block()?; |
| 65 | + let ElectrumUpdate { |
| 66 | + chain_update, |
| 67 | + relevant_txids, |
| 68 | + } = client.sync(recv_chain.tip(), [spk_to_track], None, None, 5)?; |
| 69 | + |
| 70 | + let missing = relevant_txids.missing_full_txs(recv_graph.graph()); |
| 71 | + let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; |
| 72 | + let _ = recv_chain |
| 73 | + .apply_update(chain_update) |
| 74 | + .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; |
| 75 | + let _ = recv_graph.apply_update(graph_update); |
| 76 | + |
| 77 | + // Check to see if tx is confirmed. |
| 78 | + assert_eq!( |
| 79 | + get_balance(&recv_chain, &recv_graph)?, |
| 80 | + Balance { |
| 81 | + confirmed: SEND_AMOUNT.to_sat(), |
| 82 | + ..Balance::default() |
| 83 | + }, |
| 84 | + ); |
| 85 | + |
| 86 | + Ok(()) |
| 87 | +} |
| 88 | + |
| 89 | +#[test] |
| 90 | +fn test_reorg_is_detected_in_electrsd() -> Result<()> { |
| 91 | + let env = TestEnv::new()?; |
| 92 | + |
| 93 | + // Mine some blocks. |
| 94 | + env.mine_blocks(101, None)?; |
| 95 | + env.wait_until_electrum_sees_block()?; |
| 96 | + let height = env.bitcoind.client.get_block_count()?; |
| 97 | + let blocks = (0..=height) |
| 98 | + .map(|i| env.bitcoind.client.get_block_hash(i)) |
| 99 | + .collect::<Result<Vec<_>, _>>()?; |
| 100 | + |
| 101 | + // Perform reorg on six blocks. |
| 102 | + env.reorg(6)?; |
| 103 | + env.wait_until_electrum_sees_block()?; |
| 104 | + let reorged_height = env.bitcoind.client.get_block_count()?; |
| 105 | + let reorged_blocks = (0..=height) |
| 106 | + .map(|i| env.bitcoind.client.get_block_hash(i)) |
| 107 | + .collect::<Result<Vec<_>, _>>()?; |
| 108 | + |
| 109 | + assert_eq!(height, reorged_height); |
| 110 | + |
| 111 | + // Block hashes should not be equal on the six reorged blocks. |
| 112 | + for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() { |
| 113 | + match i <= height as usize - 6 { |
| 114 | + true => assert_eq!(block, reorged_block), |
| 115 | + false => assert_ne!(block, reorged_block), |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + Ok(()) |
| 120 | +} |
| 121 | + |
| 122 | +/// Ensure that confirmed txs that are reorged become unconfirmed. |
| 123 | +/// |
| 124 | +/// 1. Mine 101 blocks. |
| 125 | +/// 2. Mine 8 blocks with a confirmed tx in each. |
| 126 | +/// 3. Perform 8 separate reorgs on each block with a confirmed tx. |
| 127 | +/// 4. Check [`Balance`] after each reorg to ensure unconfirmed amount is correct. |
| 128 | +#[test] |
| 129 | +fn tx_can_become_unconfirmed_after_reorg() -> Result<()> { |
| 130 | + const REORG_COUNT: usize = 8; |
| 131 | + const SEND_AMOUNT: Amount = Amount::from_sat(10_000); |
| 132 | + |
| 133 | + let env = TestEnv::new()?; |
| 134 | + let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; |
| 135 | + |
| 136 | + // Setup addresses. |
| 137 | + let addr_to_mine = env |
| 138 | + .bitcoind |
| 139 | + .client |
| 140 | + .get_new_address(None, None)? |
| 141 | + .assume_checked(); |
| 142 | + let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros()); |
| 143 | + let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?; |
| 144 | + |
| 145 | + // Setup receiver. |
| 146 | + let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); |
| 147 | + let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({ |
| 148 | + let mut recv_index = SpkTxOutIndex::default(); |
| 149 | + recv_index.insert_spk((), spk_to_track.clone()); |
| 150 | + recv_index |
| 151 | + }); |
| 152 | + |
| 153 | + // Mine some blocks. |
| 154 | + env.mine_blocks(101, Some(addr_to_mine))?; |
| 155 | + |
| 156 | + // Create transactions that are tracked by our receiver. |
| 157 | + for _ in 0..REORG_COUNT { |
| 158 | + env.send(&addr_to_track, SEND_AMOUNT)?; |
| 159 | + env.mine_blocks(1, None)?; |
| 160 | + } |
| 161 | + |
| 162 | + // Sync up to tip. |
| 163 | + env.wait_until_electrum_sees_block()?; |
| 164 | + let ElectrumUpdate { |
| 165 | + chain_update, |
| 166 | + relevant_txids, |
| 167 | + } = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?; |
| 168 | + |
| 169 | + let missing = relevant_txids.missing_full_txs(recv_graph.graph()); |
| 170 | + let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; |
| 171 | + let _ = recv_chain |
| 172 | + .apply_update(chain_update) |
| 173 | + .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; |
| 174 | + let _ = recv_graph.apply_update(graph_update.clone()); |
| 175 | + |
| 176 | + // Retain a snapshot of all anchors before reorg process. |
| 177 | + let initial_anchors = graph_update.all_anchors(); |
| 178 | + |
| 179 | + // Check if initial balance is correct. |
| 180 | + assert_eq!( |
| 181 | + get_balance(&recv_chain, &recv_graph)?, |
| 182 | + Balance { |
| 183 | + confirmed: SEND_AMOUNT.to_sat() * REORG_COUNT as u64, |
| 184 | + ..Balance::default() |
| 185 | + }, |
| 186 | + "initial balance must be correct", |
| 187 | + ); |
| 188 | + |
| 189 | + // Perform reorgs with different depths. |
| 190 | + for depth in 1..=REORG_COUNT { |
| 191 | + env.reorg_empty_blocks(depth)?; |
| 192 | + |
| 193 | + env.wait_until_electrum_sees_block()?; |
| 194 | + let ElectrumUpdate { |
| 195 | + chain_update, |
| 196 | + relevant_txids, |
| 197 | + } = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?; |
| 198 | + |
| 199 | + let missing = relevant_txids.missing_full_txs(recv_graph.graph()); |
| 200 | + let graph_update = |
| 201 | + relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; |
| 202 | + let _ = recv_chain |
| 203 | + .apply_update(chain_update) |
| 204 | + .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; |
| 205 | + |
| 206 | + // Check to see if a new anchor is added during current reorg. |
| 207 | + if !initial_anchors.is_superset(graph_update.all_anchors()) { |
| 208 | + println!("New anchor added at reorg depth {}", depth); |
| 209 | + } |
| 210 | + let _ = recv_graph.apply_update(graph_update); |
| 211 | + |
| 212 | + assert_eq!( |
| 213 | + get_balance(&recv_chain, &recv_graph)?, |
| 214 | + Balance { |
| 215 | + confirmed: SEND_AMOUNT.to_sat() * (REORG_COUNT - depth) as u64, |
| 216 | + trusted_pending: SEND_AMOUNT.to_sat() * depth as u64, |
| 217 | + ..Balance::default() |
| 218 | + }, |
| 219 | + "reorg_count: {}", |
| 220 | + depth, |
| 221 | + ); |
| 222 | + } |
| 223 | + |
| 224 | + Ok(()) |
| 225 | +} |
0 commit comments