Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit eda5cb0

Browse files
authored
Merge pull request #1930 from carver/gap-filling-test
Handle a lot more scenarios of filling header gaps, created by checkpoints
2 parents c9534e8 + f8b0ca7 commit eda5cb0

File tree

7 files changed

+794
-70
lines changed

7 files changed

+794
-70
lines changed

eth/db/chain.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import (
55
Dict,
66
Iterable,
7+
Sequence,
78
Tuple,
89
Type,
910
)
@@ -94,14 +95,14 @@ def get_block_uncles(self, uncles_hash: Hash32) -> Tuple[BlockHeaderAPI, ...]:
9495
def _decanonicalize_old_headers(
9596
cls,
9697
db: DatabaseAPI,
97-
new_canonical_headers: Tuple[BlockHeaderAPI, ...]
98+
numbers_to_decanonicalize: Sequence[BlockNumber],
9899
) -> Tuple[BlockHeaderAPI, ...]:
99100
old_canonical_headers = []
100101

101102
# remove transaction lookups for blocks that are no longer canonical
102-
for h in new_canonical_headers:
103+
for block_number in numbers_to_decanonicalize:
103104
try:
104-
old_hash = cls._get_canonical_block_hash(db, h.block_number)
105+
old_hash = cls._get_canonical_block_hash(db, block_number)
105106
except HeaderNotFound:
106107
# no old block, and no more possible
107108
break

eth/db/chain_gaps.py

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import enum
2-
from typing import Tuple
2+
from typing import Iterable, Tuple
33

44
from eth_typing import BlockNumber
5+
from eth_utils import (
6+
ValidationError,
7+
to_tuple,
8+
)
59

610
from eth.exceptions import GapTrackingCorrupted
711
from eth.typing import BlockRange, ChainGaps
@@ -12,17 +16,87 @@ class GapChange(enum.Enum):
1216
NewGap = enum.auto()
1317
GapFill = enum.auto()
1418
GapSplit = enum.auto()
15-
GapShrink = enum.auto()
19+
GapLeftShrink = enum.auto()
20+
GapRightShrink = enum.auto()
1621
TailWrite = enum.auto()
1722

1823

19-
GAP_WRITES = (GapChange.GapFill, GapChange.GapSplit, GapChange.GapShrink)
24+
GAP_WRITES = (
25+
GapChange.GapFill,
26+
GapChange.GapSplit,
27+
GapChange.GapLeftShrink,
28+
GapChange.GapRightShrink,
29+
)
2030
GENESIS_CHAIN_GAPS = ((), BlockNumber(1))
2131

2232
GapInfo = Tuple[GapChange, ChainGaps]
2333

2434

25-
def calculate_gaps(newly_persisted: BlockNumber, base_gaps: ChainGaps) -> GapInfo:
35+
@to_tuple
36+
def _join_overlapping_gaps(unjoined_gaps: Tuple[BlockRange, ...]) -> Iterable[BlockRange]:
37+
"""
38+
After introducing a new gap, join any that overlap.
39+
Input must already be sorted.
40+
"""
41+
unyielded_low = None
42+
unyielded_high = None
43+
for low, high in unjoined_gaps:
44+
if unyielded_high is not None:
45+
if low < unyielded_low:
46+
raise ValidationError(f"Unsorted input! {unjoined_gaps!r}")
47+
elif unyielded_low <= low <= unyielded_high + 1:
48+
unyielded_high = max(high, unyielded_high)
49+
continue
50+
else:
51+
yield unyielded_low, unyielded_high
52+
53+
unyielded_low = low
54+
unyielded_high = high
55+
56+
if unyielded_high is not None:
57+
yield unyielded_low, unyielded_high
58+
59+
60+
def reopen_gap(decanonicalized: BlockNumber, base_gaps: ChainGaps) -> ChainGaps:
61+
"""
62+
Add a new gap, for a header that was decanonicalized.
63+
"""
64+
current_gaps, tip_child = base_gaps
65+
66+
if tip_child <= decanonicalized:
67+
return base_gaps
68+
69+
new_raw_gaps = current_gaps + ((decanonicalized, decanonicalized), )
70+
71+
# join overlapping gaps
72+
joined_gaps = _join_overlapping_gaps(sorted(new_raw_gaps))
73+
74+
# is the last gap overlapping with the tip child? if so, merge it
75+
if joined_gaps[-1][1] + 1 >= tip_child:
76+
return joined_gaps[:-1], joined_gaps[-1][0]
77+
else:
78+
return joined_gaps, tip_child
79+
80+
81+
def is_block_number_in_gap(block_number: BlockNumber, gaps: ChainGaps) -> bool:
82+
"""
83+
Check if a block number is found in the given gaps
84+
"""
85+
gap_ranges, tip_child = gaps
86+
for low, high in gap_ranges:
87+
if low > block_number:
88+
return False
89+
elif high >= block_number:
90+
return True
91+
# this range was below the block number, continue looking at the next range
92+
93+
return block_number >= tip_child
94+
95+
96+
def fill_gap(newly_persisted: BlockNumber, base_gaps: ChainGaps) -> GapInfo:
97+
"""
98+
Remove a gap, for a new header that was canonicalized.
99+
"""
26100

27101
current_gaps, tip_child = base_gaps
28102

@@ -45,11 +119,12 @@ def calculate_gaps(newly_persisted: BlockNumber, base_gaps: ChainGaps) -> GapInf
45119
]
46120

47121
if len(matching_gaps) > 1:
122+
first_match, second_match, *_ = matching_gaps
48123
raise GapTrackingCorrupted(
49124
"Corrupted chain gap tracking",
50125
f"No. {newly_persisted} appears to be missing in multiple gaps",
51-
f"1st gap goes from {matching_gaps[0][1][0]} to {matching_gaps[0][1][1]}"
52-
f"2nd gap goes from {matching_gaps[1][1][0]} to {matching_gaps[1][1][1]}"
126+
f"1st gap is {first_match[1]}, 2nd gap is {second_match[1]}",
127+
f"all matching gaps: {matching_gaps}",
53128
)
54129
elif len(matching_gaps) == 0:
55130
# Looks like we are just overwriting an existing header.
@@ -62,11 +137,11 @@ def calculate_gaps(newly_persisted: BlockNumber, base_gaps: ChainGaps) -> GapInf
62137
elif newly_persisted == gap[0]:
63138
# we are shrinking the gap at the start
64139
updated_center = ((BlockNumber(gap[0] + 1), gap[1],),)
65-
gap_change = GapChange.GapShrink
140+
gap_change = GapChange.GapLeftShrink
66141
elif newly_persisted == gap[1]:
67142
# we are shrinking the gap at the tail
68143
updated_center = ((gap[0], BlockNumber(gap[1] - 1),),)
69-
gap_change = GapChange.GapShrink
144+
gap_change = GapChange.GapRightShrink
70145
elif gap[0] < newly_persisted < gap[1]:
71146
# we are dividing the gap
72147
first_new_gap = (gap[0], BlockNumber(newly_persisted - 1))

0 commit comments

Comments
 (0)