11import functools
2- from typing import cast , Iterable , Tuple
2+ from typing import (
3+ cast ,
4+ Iterable ,
5+ Sequence ,
6+ Tuple ,
7+ )
38
49import rlp
510
3540 GAP_WRITES ,
3641 GENESIS_CHAIN_GAPS ,
3742 fill_gap ,
43+ reopen_gap ,
3844)
3945from eth .exceptions import (
4046 CanonicalHeadNotFound ,
47+ CheckpointsMustBeCanonical ,
4148 HeaderNotFound ,
4249 ParentNotFound ,
4350)
@@ -218,10 +225,125 @@ def _persist_checkpoint_header(
218225 header .hash ,
219226 rlp .encode (header ),
220227 )
228+
229+ # Add new checkpoint header
230+ previous_checkpoints = cls ._get_checkpoints (db )
231+ new_checkpoints = previous_checkpoints + (header .hash ,)
232+ db .set (
233+ SchemaV1 .make_checkpoint_headers_key (),
234+ b'' .join (new_checkpoints ),
235+ )
236+
221237 previous_score = score - header .difficulty
222238 cls ._set_hash_scores_to_db (db , header , previous_score )
223- cls ._set_as_canonical_chain_head (db , header , header .parent_hash )
224- cls ._update_header_chain_gaps (db , header )
239+ cls ._set_as_canonical_chain_head (db , header , GENESIS_PARENT_HASH )
240+ _ , gaps = cls ._update_header_chain_gaps (db , header )
241+
242+ # check if the parent block number exists, and is not a match for checkpoint.parent_hash
243+ parent_block_num = BlockNumber (header .block_number - 1 )
244+ try :
245+ parent_hash = cls ._get_canonical_block_hash (db , parent_block_num )
246+ except HeaderNotFound :
247+ # no parent to check
248+ pass
249+ else :
250+ # User is asserting that the checkpoint must be canonical, so if the parent doesn't
251+ # match, then the parent must not be canonical, and should be de-canonicalized.
252+ if parent_hash != header .parent_hash :
253+ # does the correct header exist in the database?
254+ try :
255+ true_parent = cls ._get_block_header_by_hash (db , header .parent_hash )
256+ except HeaderNotFound :
257+ # True parent unavailable, just delete the now non-canonical one
258+ cls ._decanonicalize_single (db , parent_block_num , gaps )
259+ else :
260+ # True parent should have already been canonicalized during
261+ # _set_as_canonical_chain_head()
262+ raise ValidationError (
263+ f"Why was a non-matching parent header { parent_hash } left as canonical "
264+ f"after _set_as_canonical_chain_head() and { true_parent } is available?"
265+ )
266+
267+ cls ._decanonicalize_descendant_orphans (db , header , new_checkpoints )
268+
269+ @classmethod
270+ def _decanonicalize_descendant_orphans (
271+ cls ,
272+ db : DatabaseAPI ,
273+ header : BlockHeaderAPI ,
274+ checkpoints : Tuple [Hash32 , ...]) -> None :
275+
276+ # Determine if any children need to be de-canonicalized because they are not children of
277+ # the new chain head
278+ new_gaps = starting_gaps = cls ._get_header_chain_gaps (db )
279+
280+ child_number = BlockNumber (header .block_number + 1 )
281+ try :
282+ child = cls ._get_canonical_block_header_by_number (db , child_number )
283+ except HeaderNotFound :
284+ # There is no canonical block here
285+ next_invalid_child = None
286+ else :
287+ if child .parent_hash != header .hash :
288+ if child .hash in checkpoints :
289+ raise CheckpointsMustBeCanonical (
290+ f"Trying to decanonicalize { child } while making { header } the chain tip"
291+ )
292+ else :
293+ next_invalid_child = child
294+ else :
295+ next_invalid_child = None
296+
297+ while next_invalid_child :
298+ # decanonicalize, and add gap for tracking
299+ db .delete (SchemaV1 .make_block_number_to_hash_lookup_key (child_number ))
300+ new_gaps = reopen_gap (child_number , new_gaps )
301+
302+ # find next child
303+ child_number = BlockNumber (child_number + 1 )
304+ try :
305+ # All contiguous children must now be made invalid
306+ next_invalid_child = cls ._get_canonical_block_header_by_number (db , child_number )
307+ except HeaderNotFound :
308+ # Found the end of this streak of canonical blocks
309+ break
310+ else :
311+ if next_invalid_child .hash in checkpoints :
312+ raise CheckpointsMustBeCanonical (
313+ f"Trying to decanonicalize { next_invalid_child } while making { header } the"
314+ " chain tip"
315+ )
316+
317+ if new_gaps != starting_gaps :
318+ db .set (
319+ SchemaV1 .make_header_chain_gaps_lookup_key (),
320+ rlp .encode (new_gaps , sedes = chain_gaps )
321+ )
322+
323+ @classmethod
324+ def _decanonicalize_single (
325+ cls ,
326+ db : DatabaseAPI ,
327+ block_num : BlockNumber ,
328+ base_gaps : ChainGaps ) -> ChainGaps :
329+ """
330+ A single block number was found to no longer be canonical. At doc-time,
331+ this only happens because it does not link up with a checkpoint header.
332+ So de-canonicalize this block number and insert a gap in the tracked
333+ chain gaps.
334+ """
335+
336+ db .delete (
337+ SchemaV1 .make_block_number_to_hash_lookup_key (block_num )
338+ )
339+
340+ new_gaps = reopen_gap (block_num , base_gaps )
341+ if new_gaps != base_gaps :
342+ db .set (
343+ SchemaV1 .make_header_chain_gaps_lookup_key (),
344+ rlp .encode (new_gaps , sedes = chain_gaps )
345+ )
346+ return new_gaps
225347
226348 @classmethod
227349 def _persist_header_chain (
@@ -255,8 +377,10 @@ def _persist_header_chain(
255377 rlp .encode (curr_chain_head ),
256378 )
257379 score = cls ._set_hash_scores_to_db (db , curr_chain_head , score )
258- gap_change , gaps = cls ._update_header_chain_gaps (db , curr_chain_head )
259- cls ._handle_gap_change (db , gap_change , curr_chain_head , genesis_parent_hash )
380+
381+ base_gaps = cls ._get_header_chain_gaps (db )
382+ gap_info = cls ._update_header_chain_gaps (db , curr_chain_head , base_gaps )
383+ gaps = cls ._handle_gap_change (db , gap_info , curr_chain_head , genesis_parent_hash )
260384
261385 orig_headers_seq = concat ([(first_header ,), headers_iterator ])
262386 for parent , child in sliding_window (2 , orig_headers_seq ):
@@ -274,8 +398,8 @@ def _persist_header_chain(
274398 )
275399
276400 score = cls ._set_hash_scores_to_db (db , curr_chain_head , score )
277- gap_change , gaps = cls ._update_header_chain_gaps (db , curr_chain_head , gaps )
278- cls ._handle_gap_change (db , gap_change , curr_chain_head , genesis_parent_hash )
401+ gap_info = cls ._update_header_chain_gaps (db , curr_chain_head , gaps )
402+ gaps = cls ._handle_gap_change (db , gap_info , curr_chain_head , genesis_parent_hash )
279403 try :
280404 previous_canonical_head = cls ._get_canonical_head_hash (db )
281405 head_score = cls ._get_score (db , previous_canonical_head )
@@ -290,30 +414,82 @@ def _persist_header_chain(
290414 @classmethod
291415 def _handle_gap_change (cls ,
292416 db : DatabaseAPI ,
293- gap_change : GapChange ,
417+ gap_info : GapInfo ,
294418 header : BlockHeaderAPI ,
295- genesis_parent_hash : Hash32 ) -> None :
419+ genesis_parent_hash : Hash32 ) -> ChainGaps :
296420
421+ gap_change , gaps = gap_info
297422 if gap_change not in GAP_WRITES :
298- return
423+ return gaps
299424
300425 # Check if this change will link up the chain to the right
301426 if gap_change in (GapChange .GapFill , GapChange .GapRightShrink ):
302427 next_child_number = BlockNumber (header .block_number + 1 )
303428 expected_child = cls ._get_canonical_block_header_by_number (db , next_child_number )
304429 if header .hash != expected_child .parent_hash :
305- # We are trying to close a gap with an uncle. Reject!
306- raise ValidationError (f"{ header } is not the parent of { expected_child } " )
430+ # Must not join a canonical chain that is not linked from parent to child
431+ # If the child is a checkpoint, reject this fill as an uncle.
432+ checkpoints = cls ._get_checkpoints (db )
433+ if expected_child .hash in checkpoints :
434+ raise CheckpointsMustBeCanonical (
435+ f"Cannot make { header } canonical, because it is not the parent of"
436+ f" declared checkpoint: { expected_child } "
437+ )
438+ else :
439+ # If the child is *not* a checkpoint, then re-open a gap in the chain
440+ gaps = cls ._decanonicalize_single (db , expected_child .block_number , gaps )
307441
308442 # We implicitly assert that persisted headers are canonical here.
309443 # This assertion is made when persisting headers that are known to be part of a gap
310444 # in the canonical chain.
311445 # What if this assertion is later found to be false? At gap fill time, we can detect if the
312446 # chains don't link (and raise a ValidationError). Also, when a true canonical header is
313447 # added eventually, we need to canonicalize all the true headers.
314- for ancestor in cls ._find_new_ancestors (db , header , genesis_parent_hash ):
448+ cls ._canonicalize_header (db , header , genesis_parent_hash )
449+ return gaps
450+
451+ @classmethod
452+ def _canonicalize_header (
453+ cls ,
454+ db : DatabaseAPI ,
455+ header : BlockHeaderAPI ,
456+ genesis_parent_hash : Hash32 ,
457+ ) -> Tuple [Tuple [BlockHeaderAPI , ...], Tuple [BlockHeaderAPI , ...]]:
458+ """
459+ Force this header to be canonical, and adjust its ancestors/descendants as necessary
460+
461+ :raises CheckpointsMustBeCanonical: if trying to set a head that would
462+ de-canonicalize a checkpoint
463+ """
464+ new_canonical_headers = cast (
465+ Tuple [BlockHeaderAPI , ...],
466+ tuple (reversed (cls ._find_new_ancestors (db , header , genesis_parent_hash )))
467+ )
468+ old_canonical_headers = cls ._find_headers_to_decanonicalize (
469+ db ,
470+ [h .block_number for h in new_canonical_headers ],
471+ )
472+
473+ # Reject if this would make a checkpoint non-canonical
474+ checkpoints = cls ._get_checkpoints (db )
475+ attempted_checkpoint_overrides = set (
476+ old for old in old_canonical_headers
477+ if old .hash in checkpoints
478+ )
479+ if len (attempted_checkpoint_overrides ):
480+ raise CheckpointsMustBeCanonical (
481+ f"Tried to switch chain away from checkpoint(s) { attempted_checkpoint_overrides !r} "
482+ f" by inserting new canonical headers { new_canonical_headers } "
483+ )
484+
485+ for ancestor in new_canonical_headers :
315486 cls ._add_block_number_to_hash_lookup (db , ancestor )
316487
488+ if len (new_canonical_headers ):
489+ cls ._decanonicalize_descendant_orphans (db , new_canonical_headers [- 1 ], checkpoints )
490+
491+ return new_canonical_headers , old_canonical_headers
492+
317493 @classmethod
318494 def _set_as_canonical_chain_head (
319495 cls ,
@@ -327,6 +503,8 @@ def _set_as_canonical_chain_head(
327503
328504 :return: a tuple of the headers that are newly in the canonical chain, and the headers that
329505 are no longer in the canonical chain
506+ :raises CheckpointsMustBeCanonical: if trying to set a head that would
507+ de-canonicalize a checkpoint
330508 """
331509 try :
332510 current_canonical_head = cls ._get_canonical_head_hash (db )
@@ -337,46 +515,48 @@ def _set_as_canonical_chain_head(
337515 old_canonical_headers : Tuple [BlockHeaderAPI , ...]
338516
339517 if current_canonical_head and header .parent_hash == current_canonical_head :
340- # the calls to _find_new_ancestors and _decanonicalize_old_headers are
518+ # the calls to _find_new_ancestors and _find_headers_to_decanonicalize are
341519 # relatively expensive, it's better to skip them in this case, where we're
342520 # extending the canonical chain by a header
343521 new_canonical_headers = (header ,)
344522 old_canonical_headers = ()
523+ cls ._add_block_number_to_hash_lookup (db , header )
345524 else :
346- new_canonical_headers = cast (
347- Tuple [BlockHeaderAPI , ...],
348- tuple (reversed (cls ._find_new_ancestors (db , header , genesis_parent_hash )))
349- )
350- old_canonical_headers = cls ._decanonicalize_old_headers (
351- db , new_canonical_headers
352- )
353-
354- for h in new_canonical_headers :
355- cls ._add_block_number_to_hash_lookup (db , h )
525+ (
526+ new_canonical_headers ,
527+ old_canonical_headers ,
528+ ) = cls ._canonicalize_header (db , header , genesis_parent_hash )
356529
357530 db .set (SchemaV1 .make_canonical_head_hash_lookup_key (), header .hash )
358531
359532 return new_canonical_headers , old_canonical_headers
360533
361534 @classmethod
362- def _decanonicalize_old_headers (
535+ def _get_checkpoints (cls , db : DatabaseAPI ) -> Tuple [Hash32 , ...]:
536+ concatenated_checkpoints = db .get (SchemaV1 .make_checkpoint_headers_key ())
537+ if concatenated_checkpoints is None :
538+ return ()
539+ else :
540+ return tuple (
541+ Hash32 (concatenated_checkpoints [index :index + 32 ])
542+ for index in range (0 , len (concatenated_checkpoints ), 32 )
543+ )
544+
545+ @classmethod
546+ @to_tuple
547+ def _find_headers_to_decanonicalize (
363548 cls ,
364549 db : DatabaseAPI ,
365- new_canonical_headers : Tuple [BlockHeaderAPI , ...]
366- ) -> Tuple [BlockHeaderAPI , ...]:
367- old_canonical_headers = []
368-
369- for h in new_canonical_headers :
550+ numbers_to_decanonicalize : Sequence [BlockNumber ]
551+ ) -> Iterable [BlockHeaderAPI ]:
552+ for block_number in numbers_to_decanonicalize :
370553 try :
371- old_canonical_hash = cls ._get_canonical_block_hash (db , h . block_number )
554+ old_canonical_hash = cls ._get_canonical_block_hash (db , block_number )
372555 except HeaderNotFound :
373- # no old_canonical block, and no more possible
374- break
556+ # no old_canonical block, but due to checkpointing, more may be possible
557+ continue
375558 else :
376- old_canonical_header = cls ._get_block_header_by_hash (db , old_canonical_hash )
377- old_canonical_headers .append (old_canonical_header )
378-
379- return tuple (old_canonical_headers )
559+ yield cls ._get_block_header_by_hash (db , old_canonical_hash )
380560
381561 @classmethod
382562 @to_tuple
@@ -413,7 +593,11 @@ def _find_new_ancestors(cls,
413593 if h .parent_hash == genesis_parent_hash :
414594 break
415595 else :
416- h = cls ._get_block_header_by_hash (db , h .parent_hash )
596+ try :
597+ h = cls ._get_block_header_by_hash (db , h .parent_hash )
598+ except HeaderNotFound :
599+ # We must have hit a checkpoint parent, return early
600+ break
417601
418602 @staticmethod
419603 def _add_block_number_to_hash_lookup (db : DatabaseAPI , header : BlockHeaderAPI ) -> None :
0 commit comments