1
+ import enum
1
2
import operator
2
3
import random
3
4
@@ -60,6 +61,10 @@ def mk_header_chain(base_header, length):
60
61
previous_header = next_header
61
62
62
63
64
+ def get_score (genesis_header , children ):
65
+ return sum (header .difficulty for header in children ) + genesis_header .difficulty
66
+
67
+
63
68
def test_headerdb_get_canonical_head_not_found (headerdb ):
64
69
with pytest .raises (CanonicalHeadNotFound ):
65
70
headerdb .get_canonical_head ()
@@ -89,16 +94,16 @@ def test_headerdb_persist_disconnected_headers(headerdb, genesis_header):
89
94
90
95
headers = mk_header_chain (genesis_header , length = 10 )
91
96
92
- score_at_pseudo_genesis = 154618822656
93
97
# This is the score that we would reach at the tip if we persist the entire chain.
94
98
# 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 )
96
100
97
101
pseudo_genesis = headers [7 ]
102
+ pseudo_genesis_score = get_score (genesis_header , headers [0 :8 ])
98
103
99
104
assert headerdb .get_header_chain_gaps () == GENESIS_CHAIN_GAPS
100
105
# 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 )
102
107
assert headerdb .get_header_chain_gaps () == (((1 , 7 ),), 9 )
103
108
104
109
assert_headers_eq (headerdb .get_canonical_head (), pseudo_genesis )
@@ -118,88 +123,130 @@ def test_headerdb_persist_disconnected_headers(headerdb, genesis_header):
118
123
headerdb .get_block_header_by_hash (headers [2 ].hash )
119
124
120
125
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 ()
135
133
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
140
134
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 ):
146
213
headerdb .persist_header (genesis_header )
147
214
chain_a = mk_header_chain (genesis_header , length = 10 )
148
215
chain_b = mk_header_chain (genesis_header , length = 10 )
149
216
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 } " )
186
224
187
225
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 )
193
226
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" )
203
250
204
251
205
252
@pytest .mark .parametrize (
0 commit comments