|
1 |
| -use bitcoin::{constants, Address, Amount, Network, ScriptBuf}; |
2 |
| - |
3 |
| -use bdk_bitcoind_rpc::bip158::FilterIter; |
| 1 | +use bdk_bitcoind_rpc::bip158::{Event, EventInner, FilterIter}; |
4 | 2 | use bdk_core::{BlockId, CheckPoint};
|
5 | 3 | use bdk_testenv::{anyhow, bitcoind, block_id, TestEnv};
|
| 4 | +use bitcoin::{constants, Address, Amount, Network, ScriptBuf}; |
6 | 5 | use bitcoincore_rpc::RpcApi;
|
7 | 6 |
|
8 | 7 | fn testenv() -> anyhow::Result<TestEnv> {
|
@@ -163,3 +162,202 @@ fn filter_iter_error_no_scripts() -> anyhow::Result<()> {
|
163 | 162 |
|
164 | 163 | Ok(())
|
165 | 164 | }
|
| 165 | + |
| 166 | +#[test] |
| 167 | +fn filter_iter_handles_reorg_during_iteration() -> anyhow::Result<()> { |
| 168 | + let env = testenv()?; |
| 169 | + let client = env.rpc_client(); |
| 170 | + |
| 171 | + // 1. Initial setup & mining |
| 172 | + println!("STEP: Initial mining (target height 101 for maturity)"); |
| 173 | + |
| 174 | + let additional_blocks_to_mine = 100; // Mine 100 *additional* blocks |
| 175 | + let _ = env.mine_blocks(additional_blocks_to_mine, None)?; |
| 176 | + // ***************************** |
| 177 | + // Now check against the expected final height |
| 178 | + let expected_initial_height = 101; // Assuming genesis(0)+initial(1)+100 = 101 |
| 179 | + assert_eq!( |
| 180 | + client.get_block_count()?, |
| 181 | + expected_initial_height as u64, |
| 182 | + "Block count should be {} after initial mine", |
| 183 | + expected_initial_height |
| 184 | + ); |
| 185 | + |
| 186 | + // 2. Create watched script |
| 187 | + println!("STEP: Creating watched script"); |
| 188 | + // ***** FIX: Ensure address and spk_to_watch are defined here ***** |
| 189 | + let address = client |
| 190 | + .get_new_address( |
| 191 | + Some("reorg-test"), |
| 192 | + Some(bitcoincore_rpc::json::AddressType::Bech32), |
| 193 | + )? |
| 194 | + .assume_checked(); |
| 195 | + let spk_to_watch = address.script_pubkey(); |
| 196 | + // ****************************************************************** |
| 197 | + println!("Watching SPK: {}", spk_to_watch.to_hex_string()); |
| 198 | + |
| 199 | + // 3. Mine block A (now height 102) WITH relevant tx |
| 200 | + println!("STEP: Sending tx for original block A (now height 102)"); |
| 201 | + let txid_a = env.send(&address, Amount::from_sat(1000))?; |
| 202 | + println!("STEP: Mining original block A (now height 102)"); |
| 203 | + let hash_102a = env.mine_blocks(1, None)?[0]; |
| 204 | + println!("Block 102 (A) hash: {}", hash_102a); |
| 205 | + assert_eq!( |
| 206 | + client.get_block_count()?, |
| 207 | + 102, |
| 208 | + "Block count should be 102 after mining block A" |
| 209 | + ); |
| 210 | + |
| 211 | + // 4. Mine block B (now height 103) WITH relevant tx |
| 212 | + println!("STEP: Sending tx for original block B (now height 103)"); |
| 213 | + let txid_b = env.send(&address, Amount::from_sat(2000))?; |
| 214 | + println!("STEP: Mining original block B (now height 103)"); |
| 215 | + let hash_103b = env.mine_blocks(1, None)?[0]; |
| 216 | + println!("Block 103 (B) hash: {}", hash_103b); |
| 217 | + assert_eq!( |
| 218 | + client.get_block_count()?, |
| 219 | + 103, |
| 220 | + "Block count should be 103 after mining block B" |
| 221 | + ); |
| 222 | + |
| 223 | + // 5. Instantiate FilterIter (Start height remains 101) |
| 224 | + println!("STEP: Instantiating FilterIter"); |
| 225 | + let start_height = 101; // Start scan *after* the initial mine tip (height 101) |
| 226 | + let mut iter = FilterIter::new_with_height(client, start_height + 1); // Start processing from height 102 |
| 227 | + iter.add_spk(spk_to_watch.clone()); |
| 228 | + let initial_tip = iter.get_tip()?.expect("Should get initial tip"); |
| 229 | + assert_eq!(initial_tip.height, 103); // Tip is now block B |
| 230 | + assert_eq!(initial_tip.hash, hash_103b); |
| 231 | + |
| 232 | + // 6. Iterate once (process block A - now height 102) |
| 233 | + println!("STEP: Iterating once (original block A at height 102)"); |
| 234 | + let event_a_result = iter.next().expect("Iterator should have item A"); |
| 235 | + let event_a = event_a_result?; |
| 236 | + println!("First event: {:?}", event_a); |
| 237 | + match &event_a { |
| 238 | + Event::Block(EventInner { height, block }) => { |
| 239 | + assert_eq!(*height, 102); |
| 240 | + assert_eq!(block.block_hash(), hash_102a); |
| 241 | + assert!(block.txdata.iter().any(|tx| tx.compute_txid() == txid_a)); |
| 242 | + } |
| 243 | + _ => panic!("Expected block A"), |
| 244 | + } |
| 245 | + |
| 246 | + // 7. Simulate Reorg (Invalidate blocks 103B and 102A) |
| 247 | + println!("STEP: Invalidating blocks B (103) and A (102)"); |
| 248 | + println!( |
| 249 | + "Invalidating blocks B ({}) and A ({})", |
| 250 | + hash_103b, hash_102a |
| 251 | + ); |
| 252 | + client.invalidate_block(&hash_103b)?; |
| 253 | + client.invalidate_block(&hash_102a)?; |
| 254 | + // Current tip is now 101 |
| 255 | + |
| 256 | + // 8. Mine Replacement Blocks WITHOUT relevant txs |
| 257 | + // Block A' (height 102) - empty or unrelated txs |
| 258 | + println!("STEP: Mining replacement block A' (height 102, no send)"); |
| 259 | + println!("Mining replacement Block 102 (A') without relevant tx"); |
| 260 | + let hash_102a_prime = env.mine_blocks(1, None)?[0]; // Mine block 102 (A') |
| 261 | + println!("Block 102 (A') hash: {}", hash_102a_prime); |
| 262 | + assert_eq!(client.get_block_count()?, 102); |
| 263 | + assert_ne!(hash_102a, hash_102a_prime); |
| 264 | + |
| 265 | + // Block B' (height 103) - empty or unrelated txs |
| 266 | + println!("STEP: Mining replacement block B' (height 103, no send)"); |
| 267 | + println!("Mining replacement Block 103 (B') without relevant tx"); |
| 268 | + let hash_103b_prime = env.mine_blocks(1, None)?[0]; // Mine block 103 (B') |
| 269 | + println!("Block 103 (B') hash: {}", hash_103b_prime); |
| 270 | + assert_eq!(client.get_block_count()?, 103); |
| 271 | + assert_ne!(hash_103b, hash_103b_prime); |
| 272 | + |
| 273 | + // 9. Continue Iterating & Collect Events AFTER reorg |
| 274 | + // Iterator should now process heights 102 (A') and 103 (B'). |
| 275 | + let mut post_reorg_events: Vec<bdk_bitcoind_rpc::bip158::Event> = Vec::new(); |
| 276 | + |
| 277 | + println!("STEP: Starting post-reorg iteration loop"); |
| 278 | + println!("Continuing iteration after reorg..."); |
| 279 | + while let Some(event_result) = iter.next() { |
| 280 | + match event_result { |
| 281 | + Ok(event) => { |
| 282 | + println!("Post-reorg event: {:?}", event); |
| 283 | + post_reorg_events.push(event); |
| 284 | + } |
| 285 | + Err(e) => { |
| 286 | + // Print the error and fail the test immediately if an error occurs during iteration |
| 287 | + eprintln!("Error during post-reorg iteration: {:?}", e); |
| 288 | + return Err(anyhow::Error::msg(format!( |
| 289 | + "Iteration failed post-reorg: {:?}", |
| 290 | + e |
| 291 | + ))); |
| 292 | + } |
| 293 | + } |
| 294 | + } |
| 295 | + |
| 296 | + // 10. Assertions (Adjust heights) |
| 297 | + println!("STEP: Checking post-reorg assertions"); |
| 298 | + println!("Checking assertions (expecting failure)..."); |
| 299 | + |
| 300 | + // Check event for height 102 post-reorg (Block A') |
| 301 | + let event_102_post = post_reorg_events.iter().find(|e| e.height() == 102); |
| 302 | + assert!( |
| 303 | + event_102_post.is_some(), |
| 304 | + "Should have yielded an event for height 102 post-reorg (Block A')" |
| 305 | + ); |
| 306 | + match event_102_post.unwrap() { |
| 307 | + Event::Block(inner) => { |
| 308 | + assert_eq!( |
| 309 | + inner.block.block_hash(), |
| 310 | + hash_102a_prime, |
| 311 | + "BUG: Iterator yielded wrong block for height 102! Expected A', maybe got A?" |
| 312 | + ); |
| 313 | + } |
| 314 | + Event::NoMatch(h) => { |
| 315 | + assert_eq!(*h, 102, "Should be NoMatch for height 102"); |
| 316 | + } |
| 317 | + } |
| 318 | + |
| 319 | + // Check event for height 103 post-reorg (Block B') |
| 320 | + let event_103_post = post_reorg_events.iter().find(|e| e.height() == 103); |
| 321 | + assert!( |
| 322 | + event_103_post.is_some(), |
| 323 | + "Should have yielded an event for height 103 post-reorg (Block B')" |
| 324 | + ); |
| 325 | + match event_103_post.unwrap() { |
| 326 | + Event::Block(inner) => { |
| 327 | + assert_eq!( |
| 328 | + inner.block.block_hash(), |
| 329 | + hash_103b_prime, |
| 330 | + "BUG: Iterator yielded wrong block for height 103! Expected B', maybe got B?" |
| 331 | + ); |
| 332 | + assert!( |
| 333 | + !inner |
| 334 | + .block |
| 335 | + .txdata |
| 336 | + .iter() |
| 337 | + .any(|tx| tx.compute_txid() == txid_b), |
| 338 | + "BUG: Iterator yielded block for height 103 containing old txid_b!" |
| 339 | + ); |
| 340 | + } |
| 341 | + Event::NoMatch(h) => { |
| 342 | + assert_eq!(*h, 103, "Should be NoMatch for height 103"); |
| 343 | + } |
| 344 | + } |
| 345 | + |
| 346 | + // Check chain update tip (Adjust height) |
| 347 | + println!("STEP: Checking chain_update"); |
| 348 | + let final_update = iter.chain_update(); |
| 349 | + assert!(final_update.is_some(), "Should get a final chain update"); |
| 350 | + let final_tip_id = final_update.unwrap().block_id(); |
| 351 | + assert_eq!( |
| 352 | + final_tip_id.height, 103, |
| 353 | + "BUG: Final checkpoint height mismatch!" |
| 354 | + ); |
| 355 | + assert_eq!( |
| 356 | + final_tip_id.hash, hash_103b_prime, |
| 357 | + "BUG: Final checkpoint hash mismatch! Expected hash of B'." |
| 358 | + ); |
| 359 | + |
| 360 | + println!("If you see this, the test PASSED unexpectedly. The bug might already be fixed or the test logic is flawed."); |
| 361 | + |
| 362 | + Ok(()) |
| 363 | +} |
0 commit comments