Skip to content

Commit 8d076fc

Browse files
committed
test(bitcoind_rpc): test reorg between next() calls
1 parent a1eb192 commit 8d076fc

File tree

1 file changed

+146
-1
lines changed

1 file changed

+146
-1
lines changed

crates/bitcoind_rpc/tests/test_filter_iter.rs

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use bdk_bitcoind_rpc::bip158::{Event, EventInner, FilterIter};
22
use bdk_core::{BlockId, CheckPoint};
3+
use bdk_testenv::bitcoincore_rpc::bitcoincore_rpc_json::CreateRawTransactionInput;
34
use bdk_testenv::{anyhow, bitcoind, block_id, TestEnv};
45
use bitcoin::{constants, Address, Amount, Network, ScriptBuf};
56
use bitcoincore_rpc::RpcApi;
@@ -198,7 +199,6 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
198199
// later by a reorg.
199200
let unspent = client.list_unspent(None, None, None, None, None)?;
200201
assert!(unspent.len() >= 2);
201-
use bdk_testenv::bitcoincore_rpc::bitcoincore_rpc_json::CreateRawTransactionInput;
202202
let unspent_1 = &unspent[0];
203203
let unspent_2 = &unspent[1];
204204
let utxo_1 = CreateRawTransactionInput {
@@ -398,6 +398,151 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
398398
Ok(())
399399
}
400400

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+
401546
// Test that while a reorg is detected we delay incrementing the best height
402547
#[test]
403548
fn repeat_reorgs() -> anyhow::Result<()> {

0 commit comments

Comments
 (0)