1
1
import functools
2
- from typing import cast , Iterable , Tuple
2
+ from typing import (
3
+ cast ,
4
+ Iterable ,
5
+ Sequence ,
6
+ Tuple ,
7
+ )
3
8
4
9
import rlp
5
10
35
40
GAP_WRITES ,
36
41
GENESIS_CHAIN_GAPS ,
37
42
fill_gap ,
43
+ reopen_gap ,
38
44
)
39
45
from eth .exceptions import (
40
46
CanonicalHeadNotFound ,
47
+ CheckpointsMustBeCanonical ,
41
48
HeaderNotFound ,
42
49
ParentNotFound ,
43
50
)
@@ -218,10 +225,125 @@ def _persist_checkpoint_header(
218
225
header .hash ,
219
226
rlp .encode (header ),
220
227
)
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
+
221
237
previous_score = score - header .difficulty
222
238
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
225
347
226
348
@classmethod
227
349
def _persist_header_chain (
@@ -255,8 +377,10 @@ def _persist_header_chain(
255
377
rlp .encode (curr_chain_head ),
256
378
)
257
379
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 )
260
384
261
385
orig_headers_seq = concat ([(first_header ,), headers_iterator ])
262
386
for parent , child in sliding_window (2 , orig_headers_seq ):
@@ -274,8 +398,8 @@ def _persist_header_chain(
274
398
)
275
399
276
400
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 )
279
403
try :
280
404
previous_canonical_head = cls ._get_canonical_head_hash (db )
281
405
head_score = cls ._get_score (db , previous_canonical_head )
@@ -290,30 +414,82 @@ def _persist_header_chain(
290
414
@classmethod
291
415
def _handle_gap_change (cls ,
292
416
db : DatabaseAPI ,
293
- gap_change : GapChange ,
417
+ gap_info : GapInfo ,
294
418
header : BlockHeaderAPI ,
295
- genesis_parent_hash : Hash32 ) -> None :
419
+ genesis_parent_hash : Hash32 ) -> ChainGaps :
296
420
421
+ gap_change , gaps = gap_info
297
422
if gap_change not in GAP_WRITES :
298
- return
423
+ return gaps
299
424
300
425
# Check if this change will link up the chain to the right
301
426
if gap_change in (GapChange .GapFill , GapChange .GapRightShrink ):
302
427
next_child_number = BlockNumber (header .block_number + 1 )
303
428
expected_child = cls ._get_canonical_block_header_by_number (db , next_child_number )
304
429
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 )
307
441
308
442
# We implicitly assert that persisted headers are canonical here.
309
443
# This assertion is made when persisting headers that are known to be part of a gap
310
444
# in the canonical chain.
311
445
# What if this assertion is later found to be false? At gap fill time, we can detect if the
312
446
# chains don't link (and raise a ValidationError). Also, when a true canonical header is
313
447
# 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 :
315
486
cls ._add_block_number_to_hash_lookup (db , ancestor )
316
487
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
+
317
493
@classmethod
318
494
def _set_as_canonical_chain_head (
319
495
cls ,
@@ -327,6 +503,8 @@ def _set_as_canonical_chain_head(
327
503
328
504
:return: a tuple of the headers that are newly in the canonical chain, and the headers that
329
505
are no longer in the canonical chain
506
+ :raises CheckpointsMustBeCanonical: if trying to set a head that would
507
+ de-canonicalize a checkpoint
330
508
"""
331
509
try :
332
510
current_canonical_head = cls ._get_canonical_head_hash (db )
@@ -337,46 +515,48 @@ def _set_as_canonical_chain_head(
337
515
old_canonical_headers : Tuple [BlockHeaderAPI , ...]
338
516
339
517
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
341
519
# relatively expensive, it's better to skip them in this case, where we're
342
520
# extending the canonical chain by a header
343
521
new_canonical_headers = (header ,)
344
522
old_canonical_headers = ()
523
+ cls ._add_block_number_to_hash_lookup (db , header )
345
524
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 )
356
529
357
530
db .set (SchemaV1 .make_canonical_head_hash_lookup_key (), header .hash )
358
531
359
532
return new_canonical_headers , old_canonical_headers
360
533
361
534
@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 (
363
548
cls ,
364
549
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 :
370
553
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 )
372
555
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
375
558
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 )
380
560
381
561
@classmethod
382
562
@to_tuple
@@ -413,7 +593,11 @@ def _find_new_ancestors(cls,
413
593
if h .parent_hash == genesis_parent_hash :
414
594
break
415
595
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
417
601
418
602
@staticmethod
419
603
def _add_block_number_to_hash_lookup (db : DatabaseAPI , header : BlockHeaderAPI ) -> None :
0 commit comments