|
1 | 1 | use bdk_bitcoind_rpc::bip158::{Event, EventInner, FilterIter};
|
2 | 2 | use bdk_core::{BlockId, CheckPoint};
|
| 3 | +use bdk_testenv::bitcoincore_rpc::bitcoincore_rpc_json::CreateRawTransactionInput; |
3 | 4 | use bdk_testenv::{anyhow, bitcoind, block_id, TestEnv};
|
4 | 5 | use bitcoin::{constants, Address, Amount, Network, ScriptBuf};
|
5 | 6 | use bitcoincore_rpc::RpcApi;
|
@@ -198,7 +199,6 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
|
198 | 199 | // later by a reorg.
|
199 | 200 | let unspent = client.list_unspent(None, None, None, None, None)?;
|
200 | 201 | assert!(unspent.len() >= 2);
|
201 |
| - use bdk_testenv::bitcoincore_rpc::bitcoincore_rpc_json::CreateRawTransactionInput; |
202 | 202 | let unspent_1 = &unspent[0];
|
203 | 203 | let unspent_2 = &unspent[1];
|
204 | 204 | let utxo_1 = CreateRawTransactionInput {
|
@@ -398,6 +398,151 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
|
398 | 398 | Ok(())
|
399 | 399 | }
|
400 | 400 |
|
| 401 | +#[test] |
| 402 | +#[allow(clippy::print_stdout)] |
| 403 | +fn filter_iter_handles_reorg_between_next_calls() -> anyhow::Result<()> { |
| 404 | + let env = testenv()?; |
| 405 | + let client = env.rpc_client(); |
| 406 | + |
| 407 | + // 1. Initial setup & mining |
| 408 | + println!("STEP: Initial mining (target height 102 for maturity)"); |
| 409 | + let expected_initial_height = 102; |
| 410 | + while env.rpc_client().get_block_count()? < expected_initial_height { |
| 411 | + let _ = env.mine_blocks(1, None)?; |
| 412 | + } |
| 413 | + assert_eq!( |
| 414 | + client.get_block_count()?, |
| 415 | + expected_initial_height, |
| 416 | + "Block count should be {} after initial mine", |
| 417 | + expected_initial_height |
| 418 | + ); |
| 419 | + |
| 420 | + // 2. Create watched script |
| 421 | + println!("STEP: Creating watched script"); |
| 422 | + let spk_to_watch = ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa")?; |
| 423 | + let address = Address::from_script(&spk_to_watch, Network::Regtest)?; |
| 424 | + println!("Watching SPK: {}", spk_to_watch.to_hex_string()); |
| 425 | + |
| 426 | + // 3. Create two transactions to be confirmed in consecutive blocks |
| 427 | + println!("STEP: Creating transactions to send"); |
| 428 | + let unspent = client.list_unspent(None, None, None, None, None)?; |
| 429 | + assert!(unspent.len() >= 2); |
| 430 | + let (utxo_1, utxo_2) = ( |
| 431 | + CreateRawTransactionInput { |
| 432 | + txid: unspent[0].txid, |
| 433 | + vout: unspent[0].vout, |
| 434 | + sequence: None, |
| 435 | + }, |
| 436 | + CreateRawTransactionInput { |
| 437 | + txid: unspent[1].txid, |
| 438 | + vout: unspent[1].vout, |
| 439 | + sequence: None, |
| 440 | + }, |
| 441 | + ); |
| 442 | + |
| 443 | + let fee = Amount::from_sat(1000); |
| 444 | + let to_send = Amount::from_sat(50_000); |
| 445 | + let change_1 = (unspent[0].amount - to_send - fee).to_sat(); |
| 446 | + let change_2 = (unspent[1].amount - to_send - fee).to_sat(); |
| 447 | + |
| 448 | + let make_tx = |utxo, change_amt| { |
| 449 | + let out = [ |
| 450 | + (address.to_string(), to_send), |
| 451 | + ( |
| 452 | + client |
| 453 | + .get_new_address(None, None)? |
| 454 | + .assume_checked() |
| 455 | + .to_string(), |
| 456 | + Amount::from_sat(change_amt), |
| 457 | + ), |
| 458 | + ] |
| 459 | + .into(); |
| 460 | + let tx = client.create_raw_transaction(&[utxo], &out, None, None)?; |
| 461 | + Ok::<_, anyhow::Error>( |
| 462 | + client |
| 463 | + .sign_raw_transaction_with_wallet(&tx, None, None)? |
| 464 | + .transaction()?, |
| 465 | + ) |
| 466 | + }; |
| 467 | + |
| 468 | + let tx_1 = make_tx(utxo_1, change_1)?; |
| 469 | + let tx_2 = make_tx(utxo_2.clone(), change_2)?; |
| 470 | + |
| 471 | + // 4. Mine up to height 103 |
| 472 | + println!("STEP: Mining to height 103"); |
| 473 | + while env.rpc_client().get_block_count()? < 103 { |
| 474 | + let _ = env.mine_blocks(1, None)?; |
| 475 | + } |
| 476 | + |
| 477 | + // 5. Send tx1 and tx2, mine block A and block B |
| 478 | + println!("STEP: Sending tx1 for block A"); |
| 479 | + let txid_a = client.send_raw_transaction(&tx_1)?; |
| 480 | + let hash_104 = env.mine_blocks(1, None)?[0]; |
| 481 | + |
| 482 | + println!("STEP: Sending tx2 for block B"); |
| 483 | + let _txid_b = client.send_raw_transaction(&tx_2)?; |
| 484 | + let hash_105 = env.mine_blocks(1, None)?[0]; |
| 485 | + |
| 486 | + // 6. Instantiate FilterIter and iterate once |
| 487 | + println!("STEP: Instantiating FilterIter"); |
| 488 | + let mut iter = FilterIter::new_with_height(client, 104); |
| 489 | + iter.add_spk(spk_to_watch.clone()); |
| 490 | + iter.get_tip()?; |
| 491 | + |
| 492 | + println!("STEP: Iterating once (original block A)"); |
| 493 | + let event_a = iter.next().expect("Expected block A")?; |
| 494 | + match event_a { |
| 495 | + Event::Block(EventInner { height, block }) => { |
| 496 | + assert_eq!(height, 104); |
| 497 | + assert_eq!(block.block_hash(), hash_104); |
| 498 | + assert!(block.txdata.iter().any(|tx| tx.compute_txid() == txid_a)); |
| 499 | + } |
| 500 | + _ => panic!("Expected match in block A"), |
| 501 | + } |
| 502 | + |
| 503 | + // 7. Simulate reorg at height 105 |
| 504 | + println!("STEP: Invalidating original block B"); |
| 505 | + client.invalidate_block(&hash_105)?; |
| 506 | + |
| 507 | + let unrelated_addr = client.get_new_address(None, None)?.assume_checked(); |
| 508 | + let input_amt = unspent[1].amount.to_sat(); |
| 509 | + let fee_sat = 2000; |
| 510 | + let change_sat = input_amt - to_send.to_sat() - fee_sat; |
| 511 | + assert!(change_sat > 500, "Change would be too small"); |
| 512 | + |
| 513 | + let change_addr = client.get_new_address(None, None)?.assume_checked(); |
| 514 | + let out = [ |
| 515 | + (unrelated_addr.to_string(), to_send), |
| 516 | + (change_addr.to_string(), Amount::from_sat(change_sat)), |
| 517 | + ] |
| 518 | + .into(); |
| 519 | + |
| 520 | + let tx_ds = { |
| 521 | + let tx = client.create_raw_transaction(&[utxo_2], &out, None, None)?; |
| 522 | + let res = client.sign_raw_transaction_with_wallet(&tx, None, None)?; |
| 523 | + res.transaction()? |
| 524 | + }; |
| 525 | + client.send_raw_transaction(&tx_ds)?; |
| 526 | + |
| 527 | + println!("STEP: Mining replacement block B'"); |
| 528 | + let _hash_105_prime = env.mine_blocks(1, None)?[0]; |
| 529 | + let new_tip = iter.get_tip()?.expect("Should have tip after reorg"); |
| 530 | + assert_eq!(new_tip.height, 105); |
| 531 | + assert_ne!(new_tip.hash, hash_105, "BUG: still sees old block B"); |
| 532 | + |
| 533 | + // 8. Iterate again — should detect reorg and yield NoMatch for B' |
| 534 | + println!("STEP: Iterating again (should detect reorg and yield B')"); |
| 535 | + let event_b_prime = iter.next().expect("Expected B'")?; |
| 536 | + match event_b_prime { |
| 537 | + Event::NoMatch(h) => { |
| 538 | + assert_eq!(h, 105); |
| 539 | + } |
| 540 | + Event::Block(_) => panic!("Expected NoMatch for B' (replacement)"), |
| 541 | + } |
| 542 | + |
| 543 | + Ok(()) |
| 544 | +} |
| 545 | + |
401 | 546 | // Test that while a reorg is detected we delay incrementing the best height
|
402 | 547 | #[test]
|
403 | 548 | fn repeat_reorgs() -> anyhow::Result<()> {
|
|
0 commit comments