Skip to content

Commit 4ae06b4

Browse files
Musab1258ValuedMammal
authored andcommitted
test(bitcoind_rpc): update FilterIter tests
Co-authored-by: valued mammal <valuedmammal@protonmail.com>
1 parent 63923c6 commit 4ae06b4

File tree

1 file changed

+278
-4
lines changed

1 file changed

+278
-4
lines changed

crates/bitcoind_rpc/tests/test_filter_iter.rs

Lines changed: 278 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
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};
42
use bdk_core::{BlockId, CheckPoint};
53
use bdk_testenv::{anyhow, bitcoind, block_id, TestEnv};
4+
use bitcoin::{constants, Address, Amount, Network, ScriptBuf};
65
use bitcoincore_rpc::RpcApi;
76

87
fn testenv() -> anyhow::Result<TestEnv> {
@@ -100,6 +99,7 @@ fn get_tip_and_chain_update() -> anyhow::Result<()> {
10099
let cp = CheckPoint::from_block_ids(test.chain).unwrap();
101100
let mut iter = FilterIter::new_with_checkpoint(env.rpc_client(), cp);
102101
assert_eq!(iter.get_tip().unwrap(), Some(new_tip));
102+
for _res in iter.by_ref() {}
103103
let update_cp = iter.chain_update().unwrap();
104104
let mut update_blocks: Vec<_> = update_cp.iter().map(|cp| cp.block_id()).collect();
105105
update_blocks.reverse();
@@ -111,7 +111,6 @@ fn get_tip_and_chain_update() -> anyhow::Result<()> {
111111

112112
#[test]
113113
fn filter_iter_returns_matched_blocks() -> anyhow::Result<()> {
114-
use bdk_bitcoind_rpc::bip158::{Event, EventInner};
115114
let env = testenv()?;
116115
let rpc = env.rpc_client();
117116
while rpc.get_block_count()? < 101 {
@@ -163,3 +162,278 @@ fn filter_iter_error_no_scripts() -> anyhow::Result<()> {
163162

164163
Ok(())
165164
}
165+
166+
#[test]
167+
#[allow(clippy::print_stdout)]
168+
fn filter_iter_handles_reorg() -> anyhow::Result<()> {
169+
let env = testenv()?;
170+
let client = env.rpc_client();
171+
172+
// 1. Initial setup & mining
173+
println!("STEP: Initial mining (target height 102 for maturity)");
174+
175+
let expected_initial_height = 102;
176+
while env.rpc_client().get_block_count()? < expected_initial_height {
177+
let _ = env.mine_blocks(1, None)?;
178+
}
179+
// *****************************
180+
// Check the expected initial height
181+
assert_eq!(
182+
client.get_block_count()?,
183+
expected_initial_height,
184+
"Block count should be {} after initial mine",
185+
expected_initial_height
186+
);
187+
188+
// 2. Create watched script
189+
println!("STEP: Creating watched script");
190+
// Ensure address and spk_to_watch are defined here *****
191+
// ******************************************************************
192+
let spk_to_watch = ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa")?;
193+
let address = Address::from_script(&spk_to_watch, Network::Regtest)?;
194+
println!("Watching SPK: {}", spk_to_watch.to_hex_string());
195+
196+
// Create 2 txs to be confirmed at consecutive heights.
197+
// We have to choose our UTXOs now to make sure one doesn't get invalidated
198+
// later by a reorg.
199+
let unspent = client.list_unspent(None, None, None, None, None)?;
200+
assert!(unspent.len() >= 2);
201+
use bdk_testenv::bitcoincore_rpc::bitcoincore_rpc_json::CreateRawTransactionInput;
202+
let unspent_1 = &unspent[0];
203+
let unspent_2 = &unspent[1];
204+
let utxo_1 = CreateRawTransactionInput {
205+
txid: unspent_1.txid,
206+
vout: unspent_1.vout,
207+
sequence: None,
208+
};
209+
let utxo_2 = CreateRawTransactionInput {
210+
txid: unspent_2.txid,
211+
vout: unspent_2.vout,
212+
sequence: None,
213+
};
214+
215+
// create tx 1
216+
println!("STEP: Creating transactions to send");
217+
let to_send = Amount::ONE_BTC;
218+
let fee = Amount::from_sat(1_000);
219+
let change_addr = client.get_new_address(None, None)?.assume_checked();
220+
let change_amt = unspent_1.amount - to_send - fee;
221+
let out = [
222+
(address.to_string(), to_send),
223+
(change_addr.to_string(), change_amt),
224+
]
225+
.into();
226+
let to_send = Amount::ONE_BTC * 2;
227+
let tx = client.create_raw_transaction(&[utxo_1], &out, None, None)?;
228+
let res = client.sign_raw_transaction_with_wallet(&tx, None, None)?;
229+
let tx_1 = res.transaction()?;
230+
// create tx 2
231+
let change_addr = client.get_new_address(None, None)?.assume_checked();
232+
let change_amt = unspent_2.amount - to_send - fee;
233+
let out = [
234+
(address.to_string(), to_send),
235+
(change_addr.to_string(), change_amt),
236+
]
237+
.into();
238+
let tx = client.create_raw_transaction(&[utxo_2], &out, None, None)?;
239+
let res = client.sign_raw_transaction_with_wallet(&tx, None, None)?;
240+
let tx_2 = res.transaction()?;
241+
242+
// let mine_to: u32 = 103;
243+
244+
println!("STEP: Mining to height {}", 103);
245+
while env.rpc_client().get_block_count()? < 103 {
246+
let _ = env.mine_blocks(1, None)?;
247+
}
248+
249+
// 3. Mine block A WITH relevant tx
250+
println!("STEP: Sending tx for original block A");
251+
let txid_a = client.send_raw_transaction(&tx_1)?;
252+
println!("STEP: Mining original block A");
253+
let hash_104 = env.mine_blocks(1, None)?[0];
254+
255+
// 4. Mine block B WITH relevant tx 2
256+
println!("STEP: Sending tx 2 for original block B");
257+
let txid_b = client.send_raw_transaction(&tx_2)?;
258+
println!("STEP: Mining original block B");
259+
let hash_105 = env.mine_blocks(1, None)?[0];
260+
261+
assert_eq!(
262+
client.get_block_count()?,
263+
105,
264+
"Block count should be 105 after mining block B"
265+
);
266+
267+
// 5. Instantiate FilterIter at start height 104
268+
println!("STEP: Instantiating FilterIter");
269+
// Start processing from height 104
270+
let start_height = 104;
271+
let mut iter = FilterIter::new_with_height(client, start_height);
272+
iter.add_spk(spk_to_watch.clone());
273+
let initial_tip = iter.get_tip()?.expect("Should get initial tip");
274+
assert_eq!(initial_tip.height, 105);
275+
assert_eq!(initial_tip.hash, hash_105);
276+
277+
// 6. Iterate once processing block A
278+
println!("STEP: Iterating once (original block A)");
279+
let event_a = iter.next().expect("Iterator should have item A")?;
280+
// println!("First event: {:?}", event_a);
281+
match event_a {
282+
Event::Block(EventInner { height, block }) => {
283+
assert_eq!(height, 104);
284+
assert_eq!(block.block_hash(), hash_104);
285+
assert!(block.txdata.iter().any(|tx| tx.compute_txid() == txid_a));
286+
}
287+
_ => panic!("Expected relevant tx at block A 102"),
288+
}
289+
290+
// 7. Simulate Reorg (Invalidate blocks B and A)
291+
println!("STEP: Invalidating original blocks B and A");
292+
println!("Invalidating blocks B ({}) and A ({})", hash_105, hash_104);
293+
client.invalidate_block(&hash_105)?;
294+
client.invalidate_block(&hash_104)?;
295+
// We should see 2 unconfirmed txs in mempool
296+
let raw_mempool = client.get_raw_mempool()?;
297+
assert_eq!(raw_mempool.len(), 2);
298+
println!(
299+
"{} txs in mempool at height {}",
300+
raw_mempool.len(),
301+
client.get_block_count()?
302+
);
303+
304+
// 8. Mine Replacement Blocks WITH relevant txs
305+
// First mine Block A'
306+
println!("STEP: Mining replacement block A' (with send tx x2)");
307+
let hash_104_prime = env.mine_blocks(1, None)?[0];
308+
let height = client.get_block_count()?;
309+
println!("Block {} (A') hash: {}", height, hash_104_prime);
310+
assert_eq!(height, 104);
311+
assert_ne!(hash_104, hash_104_prime);
312+
313+
// Mine Block B' - empty or unrelated txs
314+
println!("STEP: Mining replacement block B' (no send tx)");
315+
let hash_105_prime = env.mine_blocks(1, None)?[0];
316+
let height = client.get_block_count()?;
317+
println!("Block {} (B') hash: {}", height, hash_105_prime);
318+
assert_eq!(height, 105);
319+
assert_ne!(hash_105, hash_105_prime);
320+
321+
// 9. Continue Iterating & Collect Events AFTER reorg
322+
// Iterator should now process heights 109 (A') and 110 (B').
323+
let mut post_reorg_events: Vec<Event> = vec![];
324+
325+
println!("STEP: Starting post-reorg iteration loop");
326+
println!("Continuing iteration after reorg...");
327+
for event_result in iter.by_ref() {
328+
let event = event_result?;
329+
println!(
330+
"Post-reorg event height: {}, matched: {}",
331+
event.height(),
332+
event.is_match(),
333+
);
334+
post_reorg_events.push(event);
335+
}
336+
337+
// 10. Assertions
338+
println!("STEP: Checking post-reorg assertions");
339+
340+
// Check for event post-reorg (Block A')
341+
let event_104_post = post_reorg_events.iter().find(|e| e.height() == 104);
342+
assert!(
343+
event_104_post.is_some(),
344+
"Should have yielded an event for post-reorg (Block A')"
345+
);
346+
match event_104_post.unwrap() {
347+
Event::Block(inner) => {
348+
assert_eq!(
349+
inner.block.block_hash(),
350+
hash_104_prime,
351+
"BUG: Iterator yielded wrong block for height 104! Expected A'"
352+
);
353+
assert!(
354+
inner
355+
.block
356+
.txdata
357+
.iter()
358+
.any(|tx| tx.compute_txid() == txid_a),
359+
"Expected relevant tx A"
360+
);
361+
assert!(
362+
inner
363+
.block
364+
.txdata
365+
.iter()
366+
.any(|tx| tx.compute_txid() == txid_b),
367+
"Expected relevant tx B"
368+
);
369+
}
370+
Event::NoMatch(..) => {
371+
panic!("Expected to match height 104");
372+
}
373+
}
374+
375+
// Check for event post-reorg (Block B')
376+
let event_105_post = post_reorg_events.iter().find(|e| e.height() == 105);
377+
assert!(
378+
event_105_post.is_some(),
379+
"Should have yielded an event for post-reorg (Block B')"
380+
);
381+
match event_105_post.unwrap() {
382+
Event::NoMatch(h) => {
383+
assert_eq!(*h, 105, "Should be NoMatch for block B'");
384+
}
385+
Event::Block(..) => {
386+
panic!("Expected NoMatch for block B'");
387+
}
388+
}
389+
390+
// 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"
396+
);
397+
398+
Ok(())
399+
}
400+
401+
// Test that while a reorg is detected we delay incrementing the best height
402+
#[test]
403+
#[ignore]
404+
fn repeat_reorgs() -> anyhow::Result<()> {
405+
const MINE_TO: u32 = 11;
406+
407+
let env = testenv()?;
408+
let rpc = env.rpc_client();
409+
while rpc.get_block_count()? < MINE_TO as u64 {
410+
let _ = env.mine_blocks(1, None)?;
411+
}
412+
413+
let spk = ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa")?;
414+
415+
let mut iter = FilterIter::new_with_height(env.rpc_client(), 1);
416+
iter.add_spk(spk);
417+
assert_eq!(iter.get_tip()?.unwrap().height, MINE_TO);
418+
419+
// Process events to height (MINE_TO - 1)
420+
loop {
421+
if iter.next().unwrap()?.height() == MINE_TO - 1 {
422+
break;
423+
}
424+
}
425+
426+
for _ in 0..3 {
427+
// Invalidate 2 blocks and remine to height = MINE_TO
428+
let _ = env.reorg(2)?;
429+
430+
// Call next. If we detect a reorg, we'll see no change in the event height
431+
assert_eq!(iter.next().unwrap()?.height(), MINE_TO - 1);
432+
}
433+
434+
// If no reorg, then height should increment normally from here on
435+
assert_eq!(iter.next().unwrap()?.height(), MINE_TO);
436+
assert!(iter.next().is_none());
437+
438+
Ok(())
439+
}

0 commit comments

Comments
 (0)