@@ -139,8 +139,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
139139/// block-based chain-sources). Therefore it's better to be conservative when setting the tip (use
140140/// an earlier tip rather than a later tip) otherwise the caller may accidentally skip blocks when
141141/// alternating between chain-sources.
142- #[ doc( hidden) ]
143- pub async fn init_chain_update (
142+ async fn init_chain_update (
144143 client : & esplora_client:: AsyncClient ,
145144 local_tip : & CheckPoint ,
146145) -> Result < BTreeMap < u32 , BlockHash > , Error > {
@@ -183,8 +182,7 @@ pub async fn init_chain_update(
183182///
184183/// A checkpoint is considered "missing" if an anchor (of `anchors`) points to a height without an
185184/// existing checkpoint/block under `local_tip` or `update_blocks`.
186- #[ doc( hidden) ]
187- pub async fn finalize_chain_update < A : Anchor > (
185+ async fn finalize_chain_update < A : Anchor > (
188186 client : & esplora_client:: AsyncClient ,
189187 local_tip : & CheckPoint ,
190188 anchors : & BTreeSet < ( A , Txid ) > ,
@@ -243,8 +241,7 @@ pub async fn finalize_chain_update<A: Anchor>(
243241
244242/// This performs a full scan to get an update for the [`TxGraph`] and
245243/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
246- #[ doc( hidden) ]
247- pub async fn full_scan_for_index_and_graph < K : Ord + Clone + Send > (
244+ async fn full_scan_for_index_and_graph < K : Ord + Clone + Send > (
248245 client : & esplora_client:: AsyncClient ,
249246 keychain_spks : BTreeMap <
250247 K ,
@@ -339,8 +336,7 @@ pub async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
339336 Ok ( ( graph, last_active_indexes) )
340337}
341338
342- #[ doc( hidden) ]
343- pub async fn sync_for_index_and_graph (
339+ async fn sync_for_index_and_graph (
344340 client : & esplora_client:: AsyncClient ,
345341 misc_spks : impl IntoIterator < IntoIter = impl Iterator < Item = ScriptBuf > + Send > + Send ,
346342 txids : impl IntoIterator < IntoIter = impl Iterator < Item = Txid > + Send > + Send ,
@@ -414,3 +410,184 @@ pub async fn sync_for_index_and_graph(
414410
415411 Ok ( graph)
416412}
413+
414+ #[ cfg( test) ]
415+ mod test {
416+ use std:: { collections:: BTreeSet , time:: Duration } ;
417+
418+ use bdk_chain:: {
419+ bitcoin:: { hashes:: Hash , Txid } ,
420+ local_chain:: LocalChain ,
421+ BlockId ,
422+ } ;
423+ use bdk_testenv:: TestEnv ;
424+ use electrsd:: bitcoind:: bitcoincore_rpc:: RpcApi ;
425+ use esplora_client:: Builder ;
426+
427+ use crate :: async_ext:: { finalize_chain_update, init_chain_update} ;
428+
429+ macro_rules! h {
430+ ( $index: literal) => { {
431+ bdk_chain:: bitcoin:: hashes:: Hash :: hash( $index. as_bytes( ) )
432+ } } ;
433+ }
434+
435+ /// Ensure that update does not remove heights (from original), and all anchor heights are included.
436+ #[ tokio:: test]
437+ pub async fn test_finalize_chain_update ( ) -> anyhow:: Result < ( ) > {
438+ struct TestCase < ' a > {
439+ name : & ' a str ,
440+ /// Initial blockchain height to start the env with.
441+ initial_env_height : u32 ,
442+ /// Initial checkpoint heights to start with.
443+ initial_cps : & ' a [ u32 ] ,
444+ /// The final blockchain height of the env.
445+ final_env_height : u32 ,
446+ /// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
447+ /// the blockhash from the env.
448+ anchors : & ' a [ ( u32 , Txid ) ] ,
449+ }
450+
451+ let test_cases = [
452+ TestCase {
453+ name : "chain_extends" ,
454+ initial_env_height : 60 ,
455+ initial_cps : & [ 59 , 60 ] ,
456+ final_env_height : 90 ,
457+ anchors : & [ ] ,
458+ } ,
459+ TestCase {
460+ name : "introduce_older_heights" ,
461+ initial_env_height : 50 ,
462+ initial_cps : & [ 10 , 15 ] ,
463+ final_env_height : 50 ,
464+ anchors : & [ ( 11 , h ! ( "A" ) ) , ( 14 , h ! ( "B" ) ) ] ,
465+ } ,
466+ TestCase {
467+ name : "introduce_older_heights_after_chain_extends" ,
468+ initial_env_height : 50 ,
469+ initial_cps : & [ 10 , 15 ] ,
470+ final_env_height : 100 ,
471+ anchors : & [ ( 11 , h ! ( "A" ) ) , ( 14 , h ! ( "B" ) ) ] ,
472+ } ,
473+ ] ;
474+
475+ for ( i, t) in test_cases. into_iter ( ) . enumerate ( ) {
476+ println ! ( "[{}] running test case: {}" , i, t. name) ;
477+
478+ let env = TestEnv :: new ( ) ?;
479+ let base_url = format ! ( "http://{}" , & env. electrsd. esplora_url. clone( ) . unwrap( ) ) ;
480+ let client = Builder :: new ( base_url. as_str ( ) ) . build_async ( ) ?;
481+
482+ // set env to `initial_env_height`
483+ if let Some ( to_mine) = t
484+ . initial_env_height
485+ . checked_sub ( env. make_checkpoint_tip ( ) . height ( ) )
486+ {
487+ env. mine_blocks ( to_mine as _ , None ) ?;
488+ }
489+ while client. get_height ( ) . await ? < t. initial_env_height {
490+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
491+ }
492+
493+ // craft initial `local_chain`
494+ let local_chain = {
495+ let ( mut chain, _) = LocalChain :: from_genesis_hash ( env. genesis_hash ( ) ?) ;
496+ let chain_tip = chain. tip ( ) ;
497+ let update_blocks = init_chain_update ( & client, & chain_tip) . await ?;
498+ let update_anchors = t
499+ . initial_cps
500+ . iter ( )
501+ . map ( |& height| -> anyhow:: Result < _ > {
502+ Ok ( (
503+ BlockId {
504+ height,
505+ hash : env. bitcoind . client . get_block_hash ( height as _ ) ?,
506+ } ,
507+ Txid :: all_zeros ( ) ,
508+ ) )
509+ } )
510+ . collect :: < anyhow:: Result < BTreeSet < _ > > > ( ) ?;
511+ let chain_update =
512+ finalize_chain_update ( & client, & chain_tip, & update_anchors, update_blocks)
513+ . await ?;
514+ chain. apply_update ( chain_update) ?;
515+ chain
516+ } ;
517+ println ! ( "local chain height: {}" , local_chain. tip( ) . height( ) ) ;
518+
519+ // extend env chain
520+ if let Some ( to_mine) = t
521+ . final_env_height
522+ . checked_sub ( env. make_checkpoint_tip ( ) . height ( ) )
523+ {
524+ env. mine_blocks ( to_mine as _ , None ) ?;
525+ }
526+ while client. get_height ( ) . await ? < t. final_env_height {
527+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
528+ }
529+
530+ // craft update
531+ let update = {
532+ let local_tip = local_chain. tip ( ) ;
533+ let update_blocks = init_chain_update ( & client, & local_tip) . await ?;
534+ let update_anchors = t
535+ . anchors
536+ . iter ( )
537+ . map ( |& ( height, txid) | -> anyhow:: Result < _ > {
538+ Ok ( (
539+ BlockId {
540+ height,
541+ hash : env. bitcoind . client . get_block_hash ( height as _ ) ?,
542+ } ,
543+ txid,
544+ ) )
545+ } )
546+ . collect :: < anyhow:: Result < _ > > ( ) ?;
547+ finalize_chain_update ( & client, & local_tip, & update_anchors, update_blocks) . await ?
548+ } ;
549+
550+ // apply update
551+ let mut updated_local_chain = local_chain. clone ( ) ;
552+ updated_local_chain. apply_update ( update) ?;
553+ println ! (
554+ "updated local chain height: {}" ,
555+ updated_local_chain. tip( ) . height( )
556+ ) ;
557+
558+ assert ! (
559+ {
560+ let initial_heights = local_chain
561+ . iter_checkpoints( )
562+ . map( |cp| cp. height( ) )
563+ . collect:: <BTreeSet <_>>( ) ;
564+ let updated_heights = updated_local_chain
565+ . iter_checkpoints( )
566+ . map( |cp| cp. height( ) )
567+ . collect:: <BTreeSet <_>>( ) ;
568+ updated_heights. is_superset( & initial_heights)
569+ } ,
570+ "heights from the initial chain must all be in the updated chain" ,
571+ ) ;
572+
573+ assert ! (
574+ {
575+ let exp_anchor_heights = t
576+ . anchors
577+ . iter( )
578+ . map( |( h, _) | * h)
579+ . chain( t. initial_cps. iter( ) . copied( ) )
580+ . collect:: <BTreeSet <_>>( ) ;
581+ let anchor_heights = updated_local_chain
582+ . iter_checkpoints( )
583+ . map( |cp| cp. height( ) )
584+ . collect:: <BTreeSet <_>>( ) ;
585+ anchor_heights. is_superset( & exp_anchor_heights)
586+ } ,
587+ "anchor heights must all be in updated chain" ,
588+ ) ;
589+ }
590+
591+ Ok ( ( ) )
592+ }
593+ }
0 commit comments