Skip to content

Commit bddc718

Browse files
committed
PR feedback
1 parent d458644 commit bddc718

File tree

3 files changed

+87
-17
lines changed

3 files changed

+87
-17
lines changed

eth/db/chain_gaps.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,20 @@ class GapChange(enum.Enum):
2424

2525
def calculate_gaps(newly_persisted: BlockNumber, base_gaps: ChainGaps) -> GapInfo:
2626

27-
current_gaps, known_missing_tip = base_gaps
27+
current_gaps, tip_child = base_gaps
2828

29-
if newly_persisted == known_missing_tip:
29+
if newly_persisted == tip_child:
3030
# This is adding a consecutive header at the very tail
3131
new_gaps = (current_gaps, BlockNumber(newly_persisted + 1))
3232
gap_change = GapChange.TailWrite
33-
elif newly_persisted > known_missing_tip:
33+
elif newly_persisted > tip_child:
3434
# We are creating a gap in the chain
3535
gap_end = BlockNumber(newly_persisted - 1)
3636
new_gaps = (
37-
current_gaps + ((known_missing_tip, gap_end),), BlockNumber(newly_persisted + 1)
37+
current_gaps + ((tip_child, gap_end),), BlockNumber(newly_persisted + 1)
3838
)
3939
gap_change = GapChange.NewGap
40-
elif newly_persisted < known_missing_tip:
40+
elif newly_persisted < tip_child:
4141
# We are patching a gap which may either shrink an existing gap or divide it
4242
matching_gaps = [
4343
(index, pair) for index, pair in enumerate(current_gaps)
@@ -78,7 +78,7 @@ def calculate_gaps(newly_persisted: BlockNumber, base_gaps: ChainGaps) -> GapInf
7878

7979
before_gap = current_gaps[:gap_index]
8080
after_gap = current_gaps[gap_index + 1:]
81-
new_gaps = (before_gap + updated_center + after_gap, known_missing_tip)
81+
new_gaps = (before_gap + updated_center + after_gap, tip_child)
8282

8383
else:
8484
raise Exception("Invariant")

eth/db/header.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -256,15 +256,7 @@ def _persist_header_chain(
256256
)
257257
score = cls._set_hash_scores_to_db(db, curr_chain_head, score)
258258
gap_change, gaps = cls._update_header_chain_gaps(db, curr_chain_head)
259-
if gap_change in GAP_WRITES:
260-
# The caller implicitly asserts that persisted headers are canonical here.
261-
# This assertion is made when persisting headers that are known to be part of a gap
262-
# in the canonical chain. While we do validate that the headers are valid, we can not
263-
# be 100 % sure that they will eventually lead to the expected child that fills the gap.
264-
# E.g. there is nothing preventing one from persisting an uncle as the final header to
265-
# close the gap, even if that will not be the parent of the next consecutive header.
266-
# Py-EVM makes the assumption that client code will check and prevent such a scenario.
267-
cls._add_block_number_to_hash_lookup(db, curr_chain_head)
259+
cls._handle_gap_change(db, gap_change, curr_chain_head, genesis_parent_hash)
268260

269261
orig_headers_seq = concat([(first_header,), headers_iterator])
270262
for parent, child in sliding_window(2, orig_headers_seq):
@@ -283,8 +275,7 @@ def _persist_header_chain(
283275

284276
score = cls._set_hash_scores_to_db(db, curr_chain_head, score)
285277
gap_change, gaps = cls._update_header_chain_gaps(db, curr_chain_head, gaps)
286-
if gap_change in GAP_WRITES:
287-
cls._add_block_number_to_hash_lookup(db, curr_chain_head)
278+
cls._handle_gap_change(db, gap_change, curr_chain_head, genesis_parent_hash)
288279
try:
289280
previous_canonical_head = cls._get_canonical_head_hash(db)
290281
head_score = cls._get_score(db, previous_canonical_head)
@@ -296,6 +287,47 @@ def _persist_header_chain(
296287

297288
return tuple(), tuple()
298289

290+
@classmethod
291+
def _handle_gap_change(cls,
292+
db: DatabaseAPI,
293+
gap_change: GapChange,
294+
header: BlockHeaderAPI,
295+
genesis_parent_hash: Hash32) -> None:
296+
297+
if gap_change not in GAP_WRITES:
298+
return
299+
300+
if gap_change is GapChange.GapFill:
301+
expected_child = cls._get_canonical_head(db)
302+
if header.hash != expected_child.parent_hash:
303+
# We are trying to close a gap with an uncle. Reject!
304+
raise ValidationError(f"{header} is not the parent of {expected_child}")
305+
306+
# We implicitly assert that persisted headers are canonical here.
307+
# This assertion is made when persisting headers that are known to be part of a gap
308+
# in the canonical chain.
309+
# What we don't know is if this header will eventually lead up to the upper end of the
310+
# gap (the checkpoint header) or if this is an alternative history. But we do ensure the
311+
# integrity eventually. For one, we check the final header that would close the gap and if
312+
# it does not match our expected child (the checkpoint header), we will reject it.
313+
# Additionally, if we have persisted a chain of alternative history under the wrong
314+
# assumption that it would be the canonical chain, but then at a later point it turns out it
315+
# isn't and we overwrite it with the correct canonical chain, we make sure to
316+
# recanonicalize all affected headers.
317+
# IMPORTANT: Not only do we have to take this into account when we fill a gap, but also
318+
# every time we write into a gap.
319+
# Suppose we have a gap of 10 headers, then we fill 1 -5 with uncles from chain B.
320+
# Now suppose we find the original chain A and attempt to write headers 1 - 10. At that
321+
# point, we are under the assumption our current gap spans from just 6 - 10 because we have
322+
# previously written headers 1 - 5 from chain B *believing* they are canonical which they
323+
# turn out not to be. That means by the time we write headers 1 -5 again (but from chain A)
324+
# we do not recognize it as writing to a known gap. Therefore by the time we write header 6,
325+
# when we finally realize that we are attempting to fill in a gap, we have to backtrack
326+
# the ancestors to add them to the canonical chain.
327+
328+
for ancestor in cls._find_new_ancestors(db, header, genesis_parent_hash):
329+
cls._add_block_number_to_hash_lookup(db, ancestor)
330+
299331
@classmethod
300332
def _set_as_canonical_chain_head(
301333
cls,

tests/database/test_header_db.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,44 @@ def test_can_patch_holes(headerdb, genesis_header):
142142
assert_headers_eq(headerdb.get_canonical_head(), pseudo_genesis)
143143

144144

145+
def test_gap_filling_handles_uncles_correctly(headerdb, genesis_header):
146+
headerdb.persist_header(genesis_header)
147+
chain_a = mk_header_chain(genesis_header, length=10)
148+
chain_b = mk_header_chain(genesis_header, length=10)
149+
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+
145183
def test_write_batch_that_patches_gap_and_adds_new_at_the_tip(headerdb, genesis_header):
146184
headerdb.persist_header(genesis_header)
147185
headers = mk_header_chain(genesis_header, length=10)

0 commit comments

Comments
 (0)