@@ -50,12 +50,47 @@ func Register(stack *node.Node, backend *eth.Ethereum) error {
50
50
return nil
51
51
}
52
52
53
+ const (
54
+ // invalidBlockHitEviction is the number of times an invalid block can be
55
+ // referenced in forkchoice update or new payload before it is attempted
56
+ // to be reprocessed again.
57
+ invalidBlockHitEviction = 128
58
+
59
+ // invalidTipsetsCap is the max number of recent block hashes tracked that
60
+ // have lead to some bad ancestor block. It's just an OOM protection.
61
+ invalidTipsetsCap = 512
62
+ )
63
+
53
64
type ConsensusAPI struct {
54
- eth * eth.Ethereum
65
+ eth * eth.Ethereum
66
+
55
67
remoteBlocks * headerQueue // Cache of remote payloads received
56
68
localBlocks * payloadQueue // Cache of local payloads generated
57
- // Lock for the forkChoiceUpdated method
58
- forkChoiceLock sync.Mutex
69
+
70
+ // The forkchoice update and new payload method require us to return the
71
+ // latest valid hash in an invalid chain. To support that return, we need
72
+ // to track historical bad blocks as well as bad tipsets in case a chain
73
+ // is constantly built on it.
74
+ //
75
+ // There are a few important caveats in this mechanism:
76
+ // - The bad block tracking is ephemeral, in-memory only. We must never
77
+ // persist any bad block information to disk as a bug in Geth could end
78
+ // up blocking a valid chain, even if a later Geth update would accept
79
+ // it.
80
+ // - Bad blocks will get forgotten after a certain threshold of import
81
+ // attempts and will be retried. The rationale is that if the network
82
+ // really-really-really tries to feed us a block, we should give it a
83
+ // new chance, perhaps us being racey instead of the block being legit
84
+ // bad (this happened in Geth at a point with import vs. pending race).
85
+ // - Tracking all the blocks built on top of the bad one could be a bit
86
+ // problematic, so we will only track the head chain segment of a bad
87
+ // chain to allow discarding progressing bad chains and side chains,
88
+ // without tracking too much bad data.
89
+ invalidBlocksHits map [common.Hash ]int // Emhemeral cache to track invalid blocks and their hit count
90
+ invalidTipsets map [common.Hash ]* types.Header // Ephemeral cache to track invalid tipsets and their bad ancestor
91
+ invalidLock sync.Mutex // Protects the invalid maps from concurrent access
92
+
93
+ forkChoiceLock sync.Mutex // Lock for the forkChoiceUpdated method
59
94
}
60
95
61
96
// NewConsensusAPI creates a new consensus api for the given backend.
@@ -64,11 +99,16 @@ func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI {
64
99
if eth .BlockChain ().Config ().TerminalTotalDifficulty == nil {
65
100
log .Warn ("Engine API started but chain not configured for merge yet" )
66
101
}
67
- return & ConsensusAPI {
68
- eth : eth ,
69
- remoteBlocks : newHeaderQueue (),
70
- localBlocks : newPayloadQueue (),
102
+ api := & ConsensusAPI {
103
+ eth : eth ,
104
+ remoteBlocks : newHeaderQueue (),
105
+ localBlocks : newPayloadQueue (),
106
+ invalidBlocksHits : make (map [common.Hash ]int ),
107
+ invalidTipsets : make (map [common.Hash ]* types.Header ),
71
108
}
109
+ eth .Downloader ().SetBadBlockCallback (api .setInvalidAncestor )
110
+
111
+ return api
72
112
}
73
113
74
114
// ForkchoiceUpdatedV1 has several responsibilities:
@@ -96,6 +136,10 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, pa
96
136
// reason.
97
137
block := api .eth .BlockChain ().GetBlockByHash (update .HeadBlockHash )
98
138
if block == nil {
139
+ // If this block was previously invalidated, keep rejecting it here too
140
+ if res := api .checkInvalidAncestor (update .HeadBlockHash , update .HeadBlockHash ); res != nil {
141
+ return beacon.ForkChoiceResponse {PayloadStatus : * res , PayloadID : nil }, nil
142
+ }
99
143
// If the head hash is unknown (was not given to us in a newPayload request),
100
144
// we cannot resolve the header, so not much to do. This could be extended in
101
145
// the future to resolve from the `eth` network, but it's an unexpected case
@@ -266,6 +310,10 @@ func (api *ConsensusAPI) NewPayloadV1(params beacon.ExecutableDataV1) (beacon.Pa
266
310
hash := block .Hash ()
267
311
return beacon.PayloadStatusV1 {Status : beacon .VALID , LatestValidHash : & hash }, nil
268
312
}
313
+ // If this block was rejected previously, keep rejecting it
314
+ if res := api .checkInvalidAncestor (block .Hash (), block .Hash ()); res != nil {
315
+ return * res , nil
316
+ }
269
317
// If the parent is missing, we - in theory - could trigger a sync, but that
270
318
// would also entail a reorg. That is problematic if multiple sibling blocks
271
319
// are being fed to us, and even more so, if some semi-distant uncle shortens
@@ -293,7 +341,7 @@ func (api *ConsensusAPI) NewPayloadV1(params beacon.ExecutableDataV1) (beacon.Pa
293
341
}
294
342
if block .Time () <= parent .Time () {
295
343
log .Warn ("Invalid timestamp" , "parent" , block .Time (), "block" , block .Time ())
296
- return api .invalid (errors .New ("invalid timestamp" ), parent ), nil
344
+ return api .invalid (errors .New ("invalid timestamp" ), parent . Header () ), nil
297
345
}
298
346
// Another cornercase: if the node is in snap sync mode, but the CL client
299
347
// tries to make it import a block. That should be denied as pushing something
@@ -310,7 +358,13 @@ func (api *ConsensusAPI) NewPayloadV1(params beacon.ExecutableDataV1) (beacon.Pa
310
358
log .Trace ("Inserting block without sethead" , "hash" , block .Hash (), "number" , block .Number )
311
359
if err := api .eth .BlockChain ().InsertBlockWithoutSetHead (block ); err != nil {
312
360
log .Warn ("NewPayloadV1: inserting block failed" , "error" , err )
313
- return api .invalid (err , parent ), nil
361
+
362
+ api .invalidLock .Lock ()
363
+ api .invalidBlocksHits [block .Hash ()] = 1
364
+ api .invalidTipsets [block .Hash ()] = block .Header ()
365
+ api .invalidLock .Unlock ()
366
+
367
+ return api .invalid (err , parent .Header ()), nil
314
368
}
315
369
// We've accepted a valid payload from the beacon client. Mark the local
316
370
// chain transitions to notify other subsystems (e.g. downloader) of the
@@ -339,8 +393,13 @@ func computePayloadId(headBlockHash common.Hash, params *beacon.PayloadAttribute
339
393
// delayPayloadImport stashes the given block away for import at a later time,
340
394
// either via a forkchoice update or a sync extension. This method is meant to
341
395
// be called by the newpayload command when the block seems to be ok, but some
342
- // prerequisite prevents it from being processed (e.g. no parent, or nap sync).
396
+ // prerequisite prevents it from being processed (e.g. no parent, or snap sync).
343
397
func (api * ConsensusAPI ) delayPayloadImport (block * types.Block ) (beacon.PayloadStatusV1 , error ) {
398
+ // Sanity check that this block's parent is not on a previously invalidated
399
+ // chain. If it is, mark the block as invalid too.
400
+ if res := api .checkInvalidAncestor (block .ParentHash (), block .Hash ()); res != nil {
401
+ return * res , nil
402
+ }
344
403
// Stash the block away for a potential forced forkchoice update to it
345
404
// at a later time.
346
405
api .remoteBlocks .put (block .Hash (), block .Header ())
@@ -360,14 +419,70 @@ func (api *ConsensusAPI) delayPayloadImport(block *types.Block) (beacon.PayloadS
360
419
return beacon.PayloadStatusV1 {Status : beacon .ACCEPTED }, nil
361
420
}
362
421
422
+ // setInvalidAncestor is a callback for the downloader to notify us if a bad block
423
+ // is encountered during the async sync.
424
+ func (api * ConsensusAPI ) setInvalidAncestor (invalid * types.Header , origin * types.Header ) {
425
+ api .invalidLock .Lock ()
426
+ defer api .invalidLock .Unlock ()
427
+
428
+ api .invalidTipsets [origin .Hash ()] = invalid
429
+ api .invalidBlocksHits [invalid .Hash ()]++
430
+ }
431
+
432
+ // checkInvalidAncestor checks whether the specified chain end links to a known
433
+ // bad ancestor. If yes, it constructs the payload failure response to return.
434
+ func (api * ConsensusAPI ) checkInvalidAncestor (check common.Hash , head common.Hash ) * beacon.PayloadStatusV1 {
435
+ api .invalidLock .Lock ()
436
+ defer api .invalidLock .Unlock ()
437
+
438
+ // If the hash to check is unknown, return valid
439
+ invalid , ok := api .invalidTipsets [check ]
440
+ if ! ok {
441
+ return nil
442
+ }
443
+ // If the bad hash was hit too many times, evict it and try to reprocess in
444
+ // the hopes that we have a data race that we can exit out of.
445
+ badHash := invalid .Hash ()
446
+
447
+ api .invalidBlocksHits [badHash ]++
448
+ if api .invalidBlocksHits [badHash ] >= invalidBlockHitEviction {
449
+ log .Warn ("Too many bad block import attempt, trying" , "number" , invalid .Number , "hash" , badHash )
450
+ delete (api .invalidBlocksHits , badHash )
451
+
452
+ for descendant , badHeader := range api .invalidTipsets {
453
+ if badHeader .Hash () == badHash {
454
+ delete (api .invalidTipsets , descendant )
455
+ }
456
+ }
457
+ return nil
458
+ }
459
+ // Not too many failures yet, mark the head of the invalid chain as invalid
460
+ if check != head {
461
+ log .Warn ("Marked new chain head as invalid" , "hash" , head , "badnumber" , invalid .Number , "badhash" , badHash )
462
+ for len (api .invalidTipsets ) >= invalidTipsetsCap {
463
+ for key := range api .invalidTipsets {
464
+ delete (api .invalidTipsets , key )
465
+ break
466
+ }
467
+ }
468
+ api .invalidTipsets [head ] = invalid
469
+ }
470
+ failure := "links to previously rejected block"
471
+ return & beacon.PayloadStatusV1 {
472
+ Status : beacon .INVALID ,
473
+ LatestValidHash : & invalid .ParentHash ,
474
+ ValidationError : & failure ,
475
+ }
476
+ }
477
+
363
478
// invalid returns a response "INVALID" with the latest valid hash supplied by latest or to the current head
364
479
// if no latestValid block was provided.
365
- func (api * ConsensusAPI ) invalid (err error , latestValid * types.Block ) beacon.PayloadStatusV1 {
480
+ func (api * ConsensusAPI ) invalid (err error , latestValid * types.Header ) beacon.PayloadStatusV1 {
366
481
currentHash := api .eth .BlockChain ().CurrentBlock ().Hash ()
367
482
if latestValid != nil {
368
483
// Set latest valid hash to 0x0 if parent is PoW block
369
484
currentHash = common.Hash {}
370
- if latestValid .Difficulty () .BitLen () == 0 {
485
+ if latestValid .Difficulty .BitLen () == 0 {
371
486
// Otherwise set latest valid hash to parent hash
372
487
currentHash = latestValid .Hash ()
373
488
}
0 commit comments