Skip to content

Commit 1a77944

Browse files
committed
test(bitcoind_rpc): test reorg between next() calls
1 parent bbe369e commit 1a77944

File tree

1 file changed

+167
-10
lines changed

1 file changed

+167
-10
lines changed

crates/bitcoind_rpc/tests/test_filter_iter.rs

Lines changed: 167 additions & 10 deletions
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 {
@@ -267,8 +267,12 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
267267
// 5. Instantiate FilterIter at start height 104
268268
println!("STEP: Instantiating FilterIter");
269269
// Start processing from height 104
270-
let start_height = 104;
271-
let mut iter = FilterIter::new_with_height(client, start_height);
270+
let checkpoint = CheckPoint::from_block_ids([BlockId {
271+
height: 103,
272+
hash: client.get_block_hash(103)?,
273+
}])
274+
.unwrap();
275+
let mut iter = FilterIter::new_with_checkpoint(client, checkpoint);
272276
iter.add_spk(spk_to_watch.clone());
273277
let initial_tip = iter.get_tip()?.expect("Should get initial tip");
274278
assert_eq!(initial_tip.height, 105);
@@ -277,14 +281,13 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
277281
// 6. Iterate once processing block A
278282
println!("STEP: Iterating once (original block A)");
279283
let event_a = iter.next().expect("Iterator should have item A")?;
280-
// println!("First event: {:?}", event_a);
281284
match event_a {
282285
Event::Block(EventInner { height, block }) => {
283286
assert_eq!(height, 104);
284287
assert_eq!(block.block_hash(), hash_104);
285288
assert!(block.txdata.iter().any(|tx| tx.compute_txid() == txid_a));
286289
}
287-
_ => panic!("Expected relevant tx at block A 102"),
290+
_ => panic!("Expected relevant tx at block A 104"),
288291
}
289292

290293
// 7. Simulate Reorg (Invalidate blocks B and A)
@@ -388,16 +391,170 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
388391
}
389392

390393
// Check chain update tip
391-
// println!("STEP: Checking chain_update");
392-
let final_update = iter.chain_update();
393-
assert!(
394-
final_update.is_none(),
395-
"We didn't instantiate FilterIter with a checkpoint"
394+
let final_update = iter.chain_update().expect("Should return a checkpoint");
395+
396+
let block_104 = final_update.get(104).expect("Expected block at height 104");
397+
assert_eq!(
398+
block_104.hash(),
399+
hash_104_prime,
400+
"Checkpoint should contain replacement block A′ at height 104"
401+
);
402+
403+
let block_105 = final_update.get(105).expect("Expected block at height 105");
404+
assert_eq!(
405+
block_105.hash(),
406+
hash_105_prime,
407+
"Checkpoint should contain replacement block B′ at height 105"
396408
);
397409

398410
Ok(())
399411
}
400412

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

0 commit comments

Comments
 (0)