1
1
import enum
2
+ from functools import partial
2
3
import operator
3
4
import random
4
5
6
+ from hypothesis import (
7
+ example ,
8
+ given ,
9
+ strategies as st ,
10
+ )
5
11
import pytest
6
12
7
- from eth_utils .toolz import accumulate
13
+ from eth_utils .toolz import (
14
+ accumulate ,
15
+ compose ,
16
+ sliding_window ,
17
+ )
8
18
9
19
from eth_utils import (
20
+ to_set ,
10
21
to_tuple ,
11
22
keccak ,
12
23
ValidationError ,
17
28
GENESIS_DIFFICULTY ,
18
29
GENESIS_GAS_LIMIT ,
19
30
)
20
- from eth .db .chain_gaps import GapChange , GENESIS_CHAIN_GAPS
31
+ from eth .db .chain_gaps import (
32
+ GapChange ,
33
+ GENESIS_CHAIN_GAPS ,
34
+ fill_gap ,
35
+ reopen_gap ,
36
+ is_block_number_in_gap ,
37
+ )
21
38
from eth .exceptions import (
22
39
CanonicalHeadNotFound ,
40
+ GapTrackingCorrupted ,
23
41
HeaderNotFound ,
24
42
ParentNotFound ,
25
43
)
@@ -37,7 +55,7 @@ def headerdb(base_db):
37
55
return HeaderDB (base_db )
38
56
39
57
40
- @pytest .fixture
58
+ @pytest .fixture ( scope = "module" )
41
59
def genesis_header ():
42
60
return BlockHeader (
43
61
difficulty = GENESIS_DIFFICULTY ,
@@ -123,6 +141,13 @@ def test_headerdb_persist_disconnected_headers(headerdb, genesis_header):
123
141
headerdb .get_block_header_by_hash (headers [2 ].hash )
124
142
125
143
144
+ def test_corrupt_gaps ():
145
+ with pytest .raises (GapTrackingCorrupted , match = "(1, 3)" ):
146
+ fill_gap (2 , (((1 , 3 ), (2 , 4 )), 6 ))
147
+ with pytest .raises (GapTrackingCorrupted , match = "(2, 4)" ):
148
+ fill_gap (2 , (((1 , 3 ), (2 , 4 )), 6 ))
149
+
150
+
126
151
class StepAction (enum .Enum ):
127
152
PERSIST_CHECKPOINT = enum .auto ()
128
153
PERSIST_HEADERS = enum .auto ()
@@ -132,6 +157,30 @@ class StepAction(enum.Enum):
132
157
VERIFY_PERSIST_RAISES = enum .auto ()
133
158
134
159
160
+ def _all_gap_numbers (chain_gaps , highest_block_number ):
161
+ """List all the missing headers, the block numbers in gaps"""
162
+ gap_ranges , tail = chain_gaps
163
+ for low , high in gap_ranges :
164
+ yield from range (low , high + 1 )
165
+ yield from range (tail , highest_block_number + 1 )
166
+
167
+
168
+ def _validate_gap_invariants (gaps ):
169
+ # 1. gaps are sorted
170
+ for low , high in gaps [0 ]:
171
+ assert high >= low , gaps
172
+
173
+ # 2. gaps are not overrlapping
174
+ for low_range , high_range in sliding_window (2 , gaps [0 ]):
175
+ # the top of the low range must not be sequential with the bottom of the high range
176
+ assert low_range [1 ] + 1 < high_range [0 ], gaps
177
+
178
+ # 3. final gap does not overlap with the tail
179
+ if len (gaps [0 ]):
180
+ final_gap_range = gaps [0 ][- 1 ]
181
+ assert final_gap_range [1 ] + 1 < gaps [1 ], gaps
182
+
183
+
135
184
@pytest .mark .parametrize (
136
185
'steps' ,
137
186
(
@@ -296,12 +345,12 @@ def _get_chain(id):
296
345
(GapChange .TailWrite , ((), 2 )),
297
346
(GapChange .NewGap , (((2 , 9 ),), 11 )),
298
347
(GapChange .GapSplit , (((2 , 4 ), (6 , 9 ),), 11 )),
299
- (GapChange .GapShrink , (((3 , 4 ), (6 , 9 ),), 11 )),
300
- (GapChange .GapShrink , (((3 , 3 ), (6 , 9 ),), 11 )),
348
+ (GapChange .GapLeftShrink , (((3 , 4 ), (6 , 9 ),), 11 )),
349
+ (GapChange .GapRightShrink , (((3 , 3 ), (6 , 9 ),), 11 )),
301
350
(GapChange .GapFill , (((6 , 9 ),), 11 )),
302
- (GapChange .GapShrink , (((6 , 8 ),), 11 )),
303
- (GapChange .GapShrink , (((6 , 7 ),), 11 )),
304
- (GapChange .GapShrink , (((6 , 6 ),), 11 )),
351
+ (GapChange .GapRightShrink , (((6 , 8 ),), 11 )),
352
+ (GapChange .GapRightShrink , (((6 , 7 ),), 11 )),
353
+ (GapChange .GapRightShrink , (((6 , 6 ),), 11 )),
305
354
(GapChange .GapFill , ((), 11 )),
306
355
)
307
356
),
@@ -325,6 +374,75 @@ def test_gap_tracking(headerdb, genesis_header, written_headers, evolving_gaps):
325
374
assert change == evolving_gaps [idx ][0 ]
326
375
327
376
377
+ @given (st .lists (
378
+ st .tuples (
379
+ # True to insert a header (ie~ remove a gap), False to remove a header (ie~ add a gap)
380
+ st .booleans (),
381
+ st .integers (min_value = 1 , max_value = 19 ), # constrain to try to cause collisions
382
+ )
383
+ ))
384
+ @example ([(True , 2 ), (True , 4 ), (False , 4 )])
385
+ def test_gap_continuity (changes ):
386
+ MAX_BLOCK_NUM = 21
387
+
388
+ # method to get all the block numbers that are in a gap right now
389
+ _all_missing = compose (set , partial (_all_gap_numbers , highest_block_number = MAX_BLOCK_NUM ))
390
+
391
+ @to_set
392
+ def _all_inserted (chain_gaps ):
393
+ """List all the inserted headers, the block numbers not in gaps"""
394
+ missing = _all_missing (chain_gaps )
395
+ for block_num in range (MAX_BLOCK_NUM + 1 ):
396
+ if block_num not in missing :
397
+ yield block_num
398
+
399
+ chain_gaps = GENESIS_CHAIN_GAPS
400
+
401
+ for do_insert , block_num in changes :
402
+ starts_inserted = _all_inserted (chain_gaps )
403
+ starts_missing = _all_missing (chain_gaps )
404
+
405
+ if do_insert :
406
+ to_insert = block_num
407
+ _ , chain_gaps = fill_gap (to_insert , chain_gaps )
408
+ assert not is_block_number_in_gap (to_insert , chain_gaps )
409
+
410
+ # Make sure that at most this one block number was filled
411
+ finished_inserted = _all_inserted (chain_gaps )
412
+ assert to_insert in finished_inserted
413
+ new_inserts = finished_inserted - starts_inserted
414
+ if block_num in starts_inserted :
415
+ assert new_inserts == set ()
416
+ else :
417
+ assert new_inserts == {block_num }
418
+
419
+ # Make sure that no new gaps were created
420
+ finished_missing = _all_missing (chain_gaps )
421
+ assert to_insert not in finished_missing
422
+ new_missing = finished_missing - starts_missing
423
+ assert new_missing == set ()
424
+ else :
425
+ to_remove = block_num
426
+ # Note that removing a header is inserting a gap
427
+ chain_gaps = reopen_gap (to_remove , chain_gaps )
428
+ assert is_block_number_in_gap (to_remove , chain_gaps )
429
+
430
+ # Make sure that no gaps were filled
431
+ finished_inserted = _all_inserted (chain_gaps )
432
+ new_inserts = finished_inserted - starts_inserted
433
+ assert new_inserts == set ()
434
+
435
+ # Make sure that at most this one block number gap was reopened
436
+ finished_missing = _all_missing (chain_gaps )
437
+ new_missing = finished_missing - starts_missing
438
+ if block_num in starts_missing :
439
+ assert new_missing == set ()
440
+ else :
441
+ assert new_missing == {block_num }
442
+
443
+ _validate_gap_invariants (chain_gaps )
444
+
445
+
328
446
@pytest .mark .parametrize (
329
447
'chain_length' ,
330
448
(0 , 1 , 2 , 3 ),
0 commit comments