1+ import enum
12import operator
23import random
34
@@ -60,6 +61,10 @@ def mk_header_chain(base_header, length):
6061 previous_header = next_header
6162
6263
64+ def get_score (genesis_header , children ):
65+ return sum (header .difficulty for header in children ) + genesis_header .difficulty
66+
67+
6368def test_headerdb_get_canonical_head_not_found (headerdb ):
6469 with pytest .raises (CanonicalHeadNotFound ):
6570 headerdb .get_canonical_head ()
@@ -89,16 +94,16 @@ def test_headerdb_persist_disconnected_headers(headerdb, genesis_header):
8994
9095 headers = mk_header_chain (genesis_header , length = 10 )
9196
92- score_at_pseudo_genesis = 154618822656
9397 # This is the score that we would reach at the tip if we persist the entire chain.
9498 # But we test to reach it by building on top of a trusted score.
95- expected_score_at_tip = 188978561024
99+ expected_score_at_tip = get_score ( genesis_header , headers )
96100
97101 pseudo_genesis = headers [7 ]
102+ pseudo_genesis_score = get_score (genesis_header , headers [0 :8 ])
98103
99104 assert headerdb .get_header_chain_gaps () == GENESIS_CHAIN_GAPS
100105 # Persist the checkpoint header with a trusted score
101- headerdb .persist_checkpoint_header (pseudo_genesis , score_at_pseudo_genesis )
106+ headerdb .persist_checkpoint_header (pseudo_genesis , pseudo_genesis_score )
102107 assert headerdb .get_header_chain_gaps () == (((1 , 7 ),), 9 )
103108
104109 assert_headers_eq (headerdb .get_canonical_head (), pseudo_genesis )
@@ -118,88 +123,130 @@ def test_headerdb_persist_disconnected_headers(headerdb, genesis_header):
118123 headerdb .get_block_header_by_hash (headers [2 ].hash )
119124
120125
121- def test_can_patch_holes (headerdb , genesis_header ):
122- headerdb .persist_header (genesis_header )
123- headers = mk_header_chain (genesis_header , length = 10 )
124-
125- assert headerdb .get_header_chain_gaps () == GENESIS_CHAIN_GAPS
126- # Persist the checkpoint header with a trusted score
127- # headers[7] has block number 8 because `headers` doesn't include the genesis
128- pseudo_genesis = headers [7 ]
129- headerdb .persist_checkpoint_header (pseudo_genesis , 154618822656 )
130- assert headerdb .get_header_chain_gaps () == (((1 , 7 ),), 9 )
131- assert_headers_eq (headerdb .get_canonical_head (), pseudo_genesis )
132-
133- headerdb .persist_header_chain (headers [:7 ])
134- assert headerdb .get_header_chain_gaps () == ((), 9 )
126+ class StepAction (enum .Enum ):
127+ PERSIST_CHECKPOINT = enum .auto ()
128+ PERSIST_HEADERS = enum .auto ()
129+ VERIFY_GAPS = enum .auto ()
130+ VERIFY_CANONICAL_HEAD = enum .auto ()
131+ VERIFY_CANONICAL_HEADERS = enum .auto ()
132+ VERIFY_PERSIST_RAISES = enum .auto ()
135133
136- for number in range (1 , 9 ):
137- # Make sure we can lookup the headers by block number
138- header_by_number = headerdb .get_canonical_block_header_by_number (number )
139- assert header_by_number .block_number == headers [number - 1 ].block_number
140134
141- # Make sure patching the hole does not affect what our current head is
142- assert_headers_eq (headerdb .get_canonical_head (), pseudo_genesis )
143-
144-
145- def test_gap_filling_handles_uncles_correctly (headerdb , genesis_header ):
135+ @pytest .mark .parametrize (
136+ 'steps' ,
137+ (
138+ # Start patching gap with uncles, later overwrite with true chain
139+ (
140+ (StepAction .PERSIST_CHECKPOINT , 8 ),
141+ (StepAction .VERIFY_GAPS , (((1 , 7 ),), 9 )),
142+ (StepAction .VERIFY_CANONICAL_HEAD , 8 ),
143+ (StepAction .PERSIST_HEADERS , ('b' , lambda headers : headers [:3 ])),
144+ (StepAction .VERIFY_GAPS , (((4 , 7 ),), 9 )),
145+ (StepAction .VERIFY_CANONICAL_HEADERS , ('b' , lambda headers : headers [:3 ])),
146+ # Verify patching the gap does not affect what our current head is
147+ (StepAction .VERIFY_CANONICAL_HEAD , 8 ),
148+ (StepAction .PERSIST_HEADERS , ('a' , lambda headers : headers )),
149+ # It's important to verify all headers of a became canonical because there was a point
150+ # in time where the chain "thought" we already had filled 1 - 3 but they later turned
151+ # out to be uncles.
152+ (StepAction .VERIFY_CANONICAL_HEADERS , ('a' , lambda headers : headers )),
153+ (StepAction .VERIFY_GAPS , ((), 11 )),
154+ ),
155+ # Can not close gap with uncle chain
156+ (
157+ (StepAction .PERSIST_CHECKPOINT , 8 ),
158+ (StepAction .VERIFY_GAPS , (((1 , 7 ),), 9 )),
159+ (StepAction .VERIFY_CANONICAL_HEAD , 8 ),
160+ (StepAction .VERIFY_PERSIST_RAISES , ('b' , ValidationError , lambda h : h [:7 ])),
161+ (StepAction .VERIFY_GAPS , (((1 , 7 ),), 9 )),
162+ ),
163+ # Can not fill gaps non-sequentially (backwards from checkpoint)
164+ (
165+ (StepAction .PERSIST_CHECKPOINT , 4 ),
166+ (StepAction .VERIFY_GAPS , (((1 , 3 ),), 5 )),
167+ (StepAction .VERIFY_CANONICAL_HEAD , 4 ),
168+ (StepAction .VERIFY_PERSIST_RAISES , ('b' , ParentNotFound , lambda headers : [headers [2 ]])),
169+ (StepAction .VERIFY_PERSIST_RAISES , ('a' , ParentNotFound , lambda headers : [headers [2 ]])),
170+ (StepAction .VERIFY_GAPS , (((1 , 3 ),), 5 )),
171+ ),
172+ # Can close gap, when head has advanced from checkpoint header
173+ (
174+ (StepAction .PERSIST_CHECKPOINT , 4 ),
175+ (StepAction .PERSIST_HEADERS , ('a' , lambda headers : [headers [4 ]])),
176+ (StepAction .VERIFY_GAPS , (((1 , 3 ),), 6 )),
177+ (StepAction .VERIFY_CANONICAL_HEAD , 5 ),
178+ (StepAction .PERSIST_HEADERS , ('a' , lambda headers : headers [:3 ])),
179+ (StepAction .VERIFY_GAPS , ((), 6 )),
180+ ),
181+ # Can close gap that ends at checkpoint header
182+ (
183+ (StepAction .PERSIST_CHECKPOINT , 4 ),
184+ (StepAction .VERIFY_GAPS , (((1 , 3 ),), 5 )),
185+ (StepAction .VERIFY_CANONICAL_HEAD , 4 ),
186+ (StepAction .PERSIST_HEADERS , ('a' , lambda headers : headers [:3 ])),
187+ (StepAction .VERIFY_GAPS , ((), 5 )),
188+ ),
189+ # Open new gaps, while filling in previous gaps
190+ (
191+ (StepAction .PERSIST_CHECKPOINT , 4 ),
192+ (StepAction .VERIFY_GAPS , (((1 , 3 ),), 5 )),
193+ (StepAction .VERIFY_CANONICAL_HEAD , 4 ),
194+ (StepAction .PERSIST_HEADERS , ('a' , lambda headers : headers [:2 ])),
195+ (StepAction .VERIFY_GAPS , (((3 , 3 ),), 5 )),
196+ # Create another gap
197+ (StepAction .PERSIST_CHECKPOINT , 8 ),
198+ (StepAction .VERIFY_CANONICAL_HEAD , 8 ),
199+ (StepAction .VERIFY_GAPS , (((3 , 3 ), (5 , 7 ),), 9 )),
200+ # Work on the second gap but don't close
201+ (StepAction .PERSIST_HEADERS , ('a' , lambda headers : headers [4 :6 ])),
202+ (StepAction .VERIFY_GAPS , (((3 , 3 ), (7 , 7 )), 9 )),
203+ # Close first gap
204+ (StepAction .PERSIST_HEADERS , ('a' , lambda headers : [headers [2 ]])),
205+ (StepAction .VERIFY_GAPS , (((7 , 7 ),), 9 )),
206+ # Close second gap
207+ (StepAction .PERSIST_HEADERS , ('a' , lambda headers : [headers [6 ]])),
208+ (StepAction .VERIFY_GAPS , ((), 9 )),
209+ ),
210+ ),
211+ )
212+ def test_different_cases_of_patching_gaps (headerdb , genesis_header , steps ):
146213 headerdb .persist_header (genesis_header )
147214 chain_a = mk_header_chain (genesis_header , length = 10 )
148215 chain_b = mk_header_chain (genesis_header , length = 10 )
149216
150- assert headerdb .get_header_chain_gaps () == GENESIS_CHAIN_GAPS
151- # Persist the checkpoint header with a trusted score
152- # chain_a[7] has block number 8 because `chain_a` doesn't include the genesis
153- pseudo_genesis = chain_a [7 ]
154- headerdb .persist_checkpoint_header (pseudo_genesis , 154618822656 )
155- assert headerdb .get_header_chain_gaps () == (((1 , 7 ),), 9 )
156- assert_headers_eq (headerdb .get_canonical_head (), pseudo_genesis )
157-
158- # Start filling the gap with headers from `chain_b`. At this point, we do not yet know this is
159- # an alternative history not leading up to our expected checkpoint header.
160- headerdb .persist_header_chain (chain_b [:3 ])
161- assert headerdb .get_header_chain_gaps () == (((4 , 7 ),), 9 )
162-
163- with pytest .raises (ValidationError ):
164- headerdb .persist_header_chain (chain_b [3 :7 ])
165-
166- # That last persist did not write any headers
167- assert headerdb .get_header_chain_gaps () == (((4 , 7 ),), 9 )
168-
169- for number in range (1 , 4 ):
170- # Make sure we can lookup the headers by block number
171- header_by_number = headerdb .get_canonical_block_header_by_number (number )
172- assert header_by_number == chain_b [number - 1 ]
173-
174- # Make sure patching the hole does not affect what our current head is
175- assert_headers_eq (headerdb .get_canonical_head (), pseudo_genesis )
176-
177- # Now we fill the gap with the actual correct chain that does lead up to our checkpoint header
178- headerdb .persist_header_chain (chain_a )
179-
180- assert_is_canonical_chain (headerdb , chain_a )
181-
182-
183- def test_write_batch_that_patches_gap_and_adds_new_at_the_tip (headerdb , genesis_header ):
184- headerdb .persist_header (genesis_header )
185- headers = mk_header_chain (genesis_header , length = 10 )
217+ def _get_chain (id ):
218+ if chain_id == 'a' :
219+ return chain_a
220+ elif chain_id == 'b' :
221+ return chain_b
222+ else :
223+ raise Exception (f"Invalid chain id: { chain_id } " )
186224
187225 assert headerdb .get_header_chain_gaps () == GENESIS_CHAIN_GAPS
188- # Persist the checkpoint header with a trusted score
189- pseudo_genesis = headers [7 ]
190- headerdb .persist_checkpoint_header (pseudo_genesis , 154618822656 )
191- assert headerdb .get_header_chain_gaps () == (((1 , 7 ),), 9 )
192- assert_headers_eq (headerdb .get_canonical_head (), pseudo_genesis )
193226
194- headerdb .persist_header_chain (headers )
195- assert headerdb .get_header_chain_gaps () == ((), 11 )
196-
197- for number in range (1 , len (headers )):
198- # Make sure we can lookup the headers by block number
199- header_by_number = headerdb .get_canonical_block_header_by_number (number )
200- assert header_by_number .block_number == headers [number - 1 ].block_number
201- # Make sure patching the hole does not affect what our current head is
202- assert_headers_eq (headerdb .get_canonical_head (), headers [- 1 ])
227+ for step in steps :
228+ step_action , step_data = step
229+ if step_action is StepAction .PERSIST_CHECKPOINT :
230+ pseudo_genesis = chain_a [step_data - 1 ]
231+ pseudo_genesis_score = get_score (genesis_header , chain_a [0 :step_data ])
232+ headerdb .persist_checkpoint_header (pseudo_genesis , pseudo_genesis_score )
233+ elif step_action is StepAction .PERSIST_HEADERS :
234+ chain_id , selector_fn = step_data
235+ headerdb .persist_header_chain (selector_fn (_get_chain (chain_id )))
236+ elif step_action is StepAction .VERIFY_GAPS :
237+ assert headerdb .get_header_chain_gaps () == step_data
238+ elif step_action is StepAction .VERIFY_PERSIST_RAISES :
239+ chain_id , error , selector_fn = step_data
240+ with pytest .raises (error ):
241+ headerdb .persist_header_chain (selector_fn (_get_chain (chain_id )))
242+ elif step_action is StepAction .VERIFY_CANONICAL_HEAD :
243+ assert_headers_eq (headerdb .get_canonical_head (), chain_a [step_data - 1 ])
244+ elif step_action is StepAction .VERIFY_CANONICAL_HEADERS :
245+ chain_id , selector_fn = step_data
246+ for header in selector_fn (_get_chain (chain_id )):
247+ assert headerdb .get_canonical_block_header_by_number (header .block_number ) == header
248+ else :
249+ raise Exception ("Unknown step action" )
203250
204251
205252@pytest .mark .parametrize (
0 commit comments