Skip to content

Commit 4beff35

Browse files
committed
PR feedback
1 parent bddc718 commit 4beff35

File tree

3 files changed

+129
-97
lines changed

3 files changed

+129
-97
lines changed

eth/abc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
Iterable,
1313
Iterator,
1414
MutableMapping,
15+
NamedTuple,
1516
Optional,
1617
Sequence,
1718
Tuple,
1819
Type,
1920
TypeVar,
2021
Union,
21-
NamedTuple,
2222
)
2323
from uuid import UUID
2424

eth/db/header.py

+5-20
Original file line numberDiff line numberDiff line change
@@ -298,33 +298,18 @@ def _handle_gap_change(cls,
298298
return
299299

300300
if gap_change is GapChange.GapFill:
301-
expected_child = cls._get_canonical_head(db)
301+
next_child_number = BlockNumber(header.block_number + 1)
302+
expected_child = cls._get_canonical_block_header_by_number(db, next_child_number)
302303
if header.hash != expected_child.parent_hash:
303304
# We are trying to close a gap with an uncle. Reject!
304305
raise ValidationError(f"{header} is not the parent of {expected_child}")
305306

306307
# We implicitly assert that persisted headers are canonical here.
307308
# This assertion is made when persisting headers that are known to be part of a gap
308309
# 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-
310+
# What if this assertion is later found to be false? At gap fill time, we can detect if the
311+
# chains don't link (and raise a ValidationError). Also, when a true canonical header is
312+
# added eventually, we need to canonicalize all the true headers.
328313
for ancestor in cls._find_new_ancestors(db, header, genesis_parent_hash):
329314
cls._add_block_number_to_hash_lookup(db, ancestor)
330315

tests/database/test_header_db.py

+123-76
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import enum
12
import operator
23
import 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+
6368
def 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

Comments
 (0)