@@ -50,12 +50,47 @@ func Register(stack *node.Node, backend *eth.Ethereum) error {
5050 return nil
5151}
5252
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+
5364type ConsensusAPI struct {
54- eth * eth.Ethereum
65+ eth * eth.Ethereum
66+
5567 remoteBlocks * headerQueue // Cache of remote payloads received
5668 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
5994}
6095
6196// NewConsensusAPI creates a new consensus api for the given backend.
@@ -64,11 +99,16 @@ func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI {
6499 if eth .BlockChain ().Config ().TerminalTotalDifficulty == nil {
65100 log .Warn ("Engine API started but chain not configured for merge yet" )
66101 }
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 ),
71108 }
109+ eth .Downloader ().SetBadBlockCallback (api .setInvalidAncestor )
110+
111+ return api
72112}
73113
74114// ForkchoiceUpdatedV1 has several responsibilities:
@@ -96,6 +136,10 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, pa
96136 // reason.
97137 block := api .eth .BlockChain ().GetBlockByHash (update .HeadBlockHash )
98138 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+ }
99143 // If the head hash is unknown (was not given to us in a newPayload request),
100144 // we cannot resolve the header, so not much to do. This could be extended in
101145 // 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
266310 hash := block .Hash ()
267311 return beacon.PayloadStatusV1 {Status : beacon .VALID , LatestValidHash : & hash }, nil
268312 }
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+ }
269317 // If the parent is missing, we - in theory - could trigger a sync, but that
270318 // would also entail a reorg. That is problematic if multiple sibling blocks
271319 // 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
293341 }
294342 if block .Time () <= parent .Time () {
295343 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
297345 }
298346 // Another cornercase: if the node is in snap sync mode, but the CL client
299347 // 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
310358 log .Trace ("Inserting block without sethead" , "hash" , block .Hash (), "number" , block .Number )
311359 if err := api .eth .BlockChain ().InsertBlockWithoutSetHead (block ); err != nil {
312360 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
314368 }
315369 // We've accepted a valid payload from the beacon client. Mark the local
316370 // chain transitions to notify other subsystems (e.g. downloader) of the
@@ -339,8 +393,13 @@ func computePayloadId(headBlockHash common.Hash, params *beacon.PayloadAttribute
339393// delayPayloadImport stashes the given block away for import at a later time,
340394// either via a forkchoice update or a sync extension. This method is meant to
341395// 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).
343397func (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+ }
344403 // Stash the block away for a potential forced forkchoice update to it
345404 // at a later time.
346405 api .remoteBlocks .put (block .Hash (), block .Header ())
@@ -360,14 +419,70 @@ func (api *ConsensusAPI) delayPayloadImport(block *types.Block) (beacon.PayloadS
360419 return beacon.PayloadStatusV1 {Status : beacon .ACCEPTED }, nil
361420}
362421
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+
363478// invalid returns a response "INVALID" with the latest valid hash supplied by latest or to the current head
364479// 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 {
366481 currentHash := api .eth .BlockChain ().CurrentBlock ().Hash ()
367482 if latestValid != nil {
368483 // Set latest valid hash to 0x0 if parent is PoW block
369484 currentHash = common.Hash {}
370- if latestValid .Difficulty () .BitLen () == 0 {
485+ if latestValid .Difficulty .BitLen () == 0 {
371486 // Otherwise set latest valid hash to parent hash
372487 currentHash = latestValid .Hash ()
373488 }
0 commit comments