1+ use bdk_chain:: bitcoin:: hashes:: Hash ;
2+ use bdk_chain:: local_chain:: LocalChain ;
3+ use bdk_chain:: BlockId ;
14use bdk_esplora:: EsploraAsyncExt ;
25use electrsd:: bitcoind:: anyhow;
36use electrsd:: bitcoind:: bitcoincore_rpc:: RpcApi ;
@@ -10,6 +13,175 @@ use std::time::Duration;
1013use bdk_chain:: bitcoin:: { Address , Amount , Txid } ;
1114use bdk_testenv:: TestEnv ;
1215
16+ macro_rules! h {
17+ ( $index: literal) => { {
18+ bdk_chain:: bitcoin:: hashes:: Hash :: hash( $index. as_bytes( ) )
19+ } } ;
20+ }
21+
22+ /// Ensure that update does not remove heights (from original), and all anchor heights are included.
23+ #[ tokio:: test]
24+ pub async fn test_finalize_chain_update ( ) -> anyhow:: Result < ( ) > {
25+ struct TestCase < ' a > {
26+ name : & ' a str ,
27+ /// Initial blockchain height to start the env with.
28+ initial_env_height : u32 ,
29+ /// Initial checkpoint heights to start with.
30+ initial_cps : & ' a [ u32 ] ,
31+ /// The final blockchain height of the env.
32+ final_env_height : u32 ,
33+ /// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
34+ /// the blockhash from the env.
35+ anchors : & ' a [ ( u32 , Txid ) ] ,
36+ }
37+
38+ let test_cases = [
39+ TestCase {
40+ name : "chain_extends" ,
41+ initial_env_height : 60 ,
42+ initial_cps : & [ 59 , 60 ] ,
43+ final_env_height : 90 ,
44+ anchors : & [ ] ,
45+ } ,
46+ TestCase {
47+ name : "introduce_older_heights" ,
48+ initial_env_height : 50 ,
49+ initial_cps : & [ 10 , 15 ] ,
50+ final_env_height : 50 ,
51+ anchors : & [ ( 11 , h ! ( "A" ) ) , ( 14 , h ! ( "B" ) ) ] ,
52+ } ,
53+ TestCase {
54+ name : "introduce_older_heights_after_chain_extends" ,
55+ initial_env_height : 50 ,
56+ initial_cps : & [ 10 , 15 ] ,
57+ final_env_height : 100 ,
58+ anchors : & [ ( 11 , h ! ( "A" ) ) , ( 14 , h ! ( "B" ) ) ] ,
59+ } ,
60+ ] ;
61+
62+ for ( i, t) in test_cases. into_iter ( ) . enumerate ( ) {
63+ println ! ( "[{}] running test case: {}" , i, t. name) ;
64+
65+ let env = TestEnv :: new ( ) ?;
66+ let base_url = format ! ( "http://{}" , & env. electrsd. esplora_url. clone( ) . unwrap( ) ) ;
67+ let client = Builder :: new ( base_url. as_str ( ) ) . build_async ( ) ?;
68+
69+ // set env to `initial_env_height`
70+ if let Some ( to_mine) = t
71+ . initial_env_height
72+ . checked_sub ( env. make_checkpoint_tip ( ) . height ( ) )
73+ {
74+ env. mine_blocks ( to_mine as _ , None ) ?;
75+ }
76+ while client. get_height ( ) . await ? < t. initial_env_height {
77+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
78+ }
79+
80+ // craft initial `local_chain`
81+ let local_chain = {
82+ let ( mut chain, _) = LocalChain :: from_genesis_hash ( env. genesis_hash ( ) ?) ;
83+ let chain_tip = chain. tip ( ) ;
84+ let update_blocks = bdk_esplora:: init_chain_update ( & client, & chain_tip) . await ?;
85+ let update_anchors = t
86+ . initial_cps
87+ . iter ( )
88+ . map ( |& height| -> anyhow:: Result < _ > {
89+ Ok ( (
90+ BlockId {
91+ height,
92+ hash : env. bitcoind . client . get_block_hash ( height as _ ) ?,
93+ } ,
94+ Txid :: all_zeros ( ) ,
95+ ) )
96+ } )
97+ . collect :: < anyhow:: Result < BTreeSet < _ > > > ( ) ?;
98+ let chain_update = bdk_esplora:: finalize_chain_update (
99+ & client,
100+ & chain_tip,
101+ & update_anchors,
102+ update_blocks,
103+ )
104+ . await ?;
105+ chain. apply_update ( chain_update) ?;
106+ chain
107+ } ;
108+ println ! ( "local chain height: {}" , local_chain. tip( ) . height( ) ) ;
109+
110+ // extend env chain
111+ if let Some ( to_mine) = t
112+ . final_env_height
113+ . checked_sub ( env. make_checkpoint_tip ( ) . height ( ) )
114+ {
115+ env. mine_blocks ( to_mine as _ , None ) ?;
116+ }
117+ while client. get_height ( ) . await ? < t. final_env_height {
118+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
119+ }
120+
121+ // craft update
122+ let update = {
123+ let local_tip = local_chain. tip ( ) ;
124+ let update_blocks = bdk_esplora:: init_chain_update ( & client, & local_tip) . await ?;
125+ let update_anchors = t
126+ . anchors
127+ . iter ( )
128+ . map ( |& ( height, txid) | -> anyhow:: Result < _ > {
129+ Ok ( (
130+ BlockId {
131+ height,
132+ hash : env. bitcoind . client . get_block_hash ( height as _ ) ?,
133+ } ,
134+ txid,
135+ ) )
136+ } )
137+ . collect :: < anyhow:: Result < _ > > ( ) ?;
138+ bdk_esplora:: finalize_chain_update ( & client, & local_tip, & update_anchors, update_blocks)
139+ . await ?
140+ } ;
141+
142+ // apply update
143+ let mut updated_local_chain = local_chain. clone ( ) ;
144+ updated_local_chain. apply_update ( update) ?;
145+ println ! (
146+ "updated local chain height: {}" ,
147+ updated_local_chain. tip( ) . height( )
148+ ) ;
149+
150+ assert ! (
151+ {
152+ let initial_heights = local_chain
153+ . iter_checkpoints( )
154+ . map( |cp| cp. height( ) )
155+ . collect:: <BTreeSet <_>>( ) ;
156+ let updated_heights = updated_local_chain
157+ . iter_checkpoints( )
158+ . map( |cp| cp. height( ) )
159+ . collect:: <BTreeSet <_>>( ) ;
160+ updated_heights. is_superset( & initial_heights)
161+ } ,
162+ "heights from the initial chain must all be in the updated chain" ,
163+ ) ;
164+
165+ assert ! (
166+ {
167+ let exp_anchor_heights = t
168+ . anchors
169+ . iter( )
170+ . map( |( h, _) | * h)
171+ . chain( t. initial_cps. iter( ) . copied( ) )
172+ . collect:: <BTreeSet <_>>( ) ;
173+ let anchor_heights = updated_local_chain
174+ . iter_checkpoints( )
175+ . map( |cp| cp. height( ) )
176+ . collect:: <BTreeSet <_>>( ) ;
177+ anchor_heights. is_superset( & exp_anchor_heights)
178+ } ,
179+ "anchor heights must all be in updated chain" ,
180+ ) ;
181+ }
182+
183+ Ok ( ( ) )
184+ }
13185#[ tokio:: test]
14186pub async fn test_update_tx_graph_without_keychain ( ) -> anyhow:: Result < ( ) > {
15187 let env = TestEnv :: new ( ) ?;
0 commit comments