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 > {
@@ -100,6 +99,7 @@ fn get_tip_and_chain_update() -> anyhow::Result<()> {
100
99
let cp = CheckPoint :: from_block_ids ( test. chain ) . unwrap ( ) ;
101
100
let mut iter = FilterIter :: new_with_checkpoint ( env. rpc_client ( ) , cp) ;
102
101
assert_eq ! ( iter. get_tip( ) . unwrap( ) , Some ( new_tip) ) ;
102
+ for _res in iter. by_ref ( ) { }
103
103
let update_cp = iter. chain_update ( ) . unwrap ( ) ;
104
104
let mut update_blocks: Vec < _ > = update_cp. iter ( ) . map ( |cp| cp. block_id ( ) ) . collect ( ) ;
105
105
update_blocks. reverse ( ) ;
@@ -111,7 +111,6 @@ fn get_tip_and_chain_update() -> anyhow::Result<()> {
111
111
112
112
#[ test]
113
113
fn filter_iter_returns_matched_blocks ( ) -> anyhow:: Result < ( ) > {
114
- use bdk_bitcoind_rpc:: bip158:: { Event , EventInner } ;
115
114
let env = testenv ( ) ?;
116
115
let rpc = env. rpc_client ( ) ;
117
116
while rpc. get_block_count ( ) ? < 101 {
@@ -163,3 +162,278 @@ fn filter_iter_error_no_scripts() -> anyhow::Result<()> {
163
162
164
163
Ok ( ( ) )
165
164
}
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