github.com/jimmyx0x/go-ethereum@v1.10.28/eth/catalyst/api.go (about)

     1  // Copyright 2021 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  // Package catalyst implements the temporary eth1/eth2 RPC integration.
    18  package catalyst
    19  
    20  import (
    21  	"errors"
    22  	"fmt"
    23  	"math/big"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/ethereum/go-ethereum/common"
    28  	"github.com/ethereum/go-ethereum/common/hexutil"
    29  	"github.com/ethereum/go-ethereum/core/beacon"
    30  	"github.com/ethereum/go-ethereum/core/rawdb"
    31  	"github.com/ethereum/go-ethereum/core/types"
    32  	"github.com/ethereum/go-ethereum/eth"
    33  	"github.com/ethereum/go-ethereum/eth/downloader"
    34  	"github.com/ethereum/go-ethereum/log"
    35  	"github.com/ethereum/go-ethereum/miner"
    36  	"github.com/ethereum/go-ethereum/node"
    37  	"github.com/ethereum/go-ethereum/rpc"
    38  )
    39  
    40  // Register adds the engine API to the full node.
    41  func Register(stack *node.Node, backend *eth.Ethereum) error {
    42  	log.Warn("Engine API enabled", "protocol", "eth")
    43  	stack.RegisterAPIs([]rpc.API{
    44  		{
    45  			Namespace:     "engine",
    46  			Service:       NewConsensusAPI(backend),
    47  			Authenticated: true,
    48  		},
    49  	})
    50  	return nil
    51  }
    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  	// beaconUpdateStartupTimeout is the time to wait for a beacon client to get
    64  	// attached before starting to issue warnings.
    65  	beaconUpdateStartupTimeout = 30 * time.Second
    66  
    67  	// beaconUpdateExchangeTimeout is the max time allowed for a beacon client to
    68  	// do a transition config exchange before it's considered offline and the user
    69  	// is warned.
    70  	beaconUpdateExchangeTimeout = 2 * time.Minute
    71  
    72  	// beaconUpdateConsensusTimeout is the max time allowed for a beacon client
    73  	// to send a consensus update before it's considered offline and the user is
    74  	// warned.
    75  	beaconUpdateConsensusTimeout = 30 * time.Second
    76  
    77  	// beaconUpdateWarnFrequency is the frequency at which to warn the user that
    78  	// the beacon client is offline.
    79  	beaconUpdateWarnFrequency = 5 * time.Minute
    80  )
    81  
    82  type ConsensusAPI struct {
    83  	eth *eth.Ethereum
    84  
    85  	remoteBlocks *headerQueue  // Cache of remote payloads received
    86  	localBlocks  *payloadQueue // Cache of local payloads generated
    87  
    88  	// The forkchoice update and new payload method require us to return the
    89  	// latest valid hash in an invalid chain. To support that return, we need
    90  	// to track historical bad blocks as well as bad tipsets in case a chain
    91  	// is constantly built on it.
    92  	//
    93  	// There are a few important caveats in this mechanism:
    94  	//   - The bad block tracking is ephemeral, in-memory only. We must never
    95  	//     persist any bad block information to disk as a bug in Geth could end
    96  	//     up blocking a valid chain, even if a later Geth update would accept
    97  	//     it.
    98  	//   - Bad blocks will get forgotten after a certain threshold of import
    99  	//     attempts and will be retried. The rationale is that if the network
   100  	//     really-really-really tries to feed us a block, we should give it a
   101  	//     new chance, perhaps us being racey instead of the block being legit
   102  	//     bad (this happened in Geth at a point with import vs. pending race).
   103  	//   - Tracking all the blocks built on top of the bad one could be a bit
   104  	//     problematic, so we will only track the head chain segment of a bad
   105  	//     chain to allow discarding progressing bad chains and side chains,
   106  	//     without tracking too much bad data.
   107  	invalidBlocksHits map[common.Hash]int           // Ephemeral cache to track invalid blocks and their hit count
   108  	invalidTipsets    map[common.Hash]*types.Header // Ephemeral cache to track invalid tipsets and their bad ancestor
   109  	invalidLock       sync.Mutex                    // Protects the invalid maps from concurrent access
   110  
   111  	// Geth can appear to be stuck or do strange things if the beacon client is
   112  	// offline or is sending us strange data. Stash some update stats away so
   113  	// that we can warn the user and not have them open issues on our tracker.
   114  	lastTransitionUpdate time.Time
   115  	lastTransitionLock   sync.Mutex
   116  	lastForkchoiceUpdate time.Time
   117  	lastForkchoiceLock   sync.Mutex
   118  	lastNewPayloadUpdate time.Time
   119  	lastNewPayloadLock   sync.Mutex
   120  
   121  	forkchoiceLock sync.Mutex // Lock for the forkChoiceUpdated method
   122  	newPayloadLock sync.Mutex // Lock for the NewPayload method
   123  }
   124  
   125  // NewConsensusAPI creates a new consensus api for the given backend.
   126  // The underlying blockchain needs to have a valid terminal total difficulty set.
   127  func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI {
   128  	if eth.BlockChain().Config().TerminalTotalDifficulty == nil {
   129  		log.Warn("Engine API started but chain not configured for merge yet")
   130  	}
   131  	api := &ConsensusAPI{
   132  		eth:               eth,
   133  		remoteBlocks:      newHeaderQueue(),
   134  		localBlocks:       newPayloadQueue(),
   135  		invalidBlocksHits: make(map[common.Hash]int),
   136  		invalidTipsets:    make(map[common.Hash]*types.Header),
   137  	}
   138  	eth.Downloader().SetBadBlockCallback(api.setInvalidAncestor)
   139  	go api.heartbeat()
   140  
   141  	return api
   142  }
   143  
   144  // ForkchoiceUpdatedV1 has several responsibilities:
   145  //
   146  // We try to set our blockchain to the headBlock.
   147  //
   148  // If the method is called with an empty head block: we return success, which can be used
   149  // to check if the engine API is enabled.
   150  //
   151  // If the total difficulty was not reached: we return INVALID.
   152  //
   153  // If the finalizedBlockHash is set: we check if we have the finalizedBlockHash in our db,
   154  // if not we start a sync.
   155  //
   156  // If there are payloadAttributes: we try to assemble a block with the payloadAttributes
   157  // and return its payloadID.
   158  func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, payloadAttributes *beacon.PayloadAttributesV1) (beacon.ForkChoiceResponse, error) {
   159  	api.forkchoiceLock.Lock()
   160  	defer api.forkchoiceLock.Unlock()
   161  
   162  	log.Trace("Engine API request received", "method", "ForkchoiceUpdated", "head", update.HeadBlockHash, "finalized", update.FinalizedBlockHash, "safe", update.SafeBlockHash)
   163  	if update.HeadBlockHash == (common.Hash{}) {
   164  		log.Warn("Forkchoice requested update to zero hash")
   165  		return beacon.STATUS_INVALID, nil // TODO(karalabe): Why does someone send us this?
   166  	}
   167  	// Stash away the last update to warn the user if the beacon client goes offline
   168  	api.lastForkchoiceLock.Lock()
   169  	api.lastForkchoiceUpdate = time.Now()
   170  	api.lastForkchoiceLock.Unlock()
   171  
   172  	// Check whether we have the block yet in our database or not. If not, we'll
   173  	// need to either trigger a sync, or to reject this forkchoice update for a
   174  	// reason.
   175  	block := api.eth.BlockChain().GetBlockByHash(update.HeadBlockHash)
   176  	if block == nil {
   177  		// If this block was previously invalidated, keep rejecting it here too
   178  		if res := api.checkInvalidAncestor(update.HeadBlockHash, update.HeadBlockHash); res != nil {
   179  			return beacon.ForkChoiceResponse{PayloadStatus: *res, PayloadID: nil}, nil
   180  		}
   181  		// If the head hash is unknown (was not given to us in a newPayload request),
   182  		// we cannot resolve the header, so not much to do. This could be extended in
   183  		// the future to resolve from the `eth` network, but it's an unexpected case
   184  		// that should be fixed, not papered over.
   185  		header := api.remoteBlocks.get(update.HeadBlockHash)
   186  		if header == nil {
   187  			log.Warn("Forkchoice requested unknown head", "hash", update.HeadBlockHash)
   188  			return beacon.STATUS_SYNCING, nil
   189  		}
   190  		// Header advertised via a past newPayload request. Start syncing to it.
   191  		// Before we do however, make sure any legacy sync in switched off so we
   192  		// don't accidentally have 2 cycles running.
   193  		if merger := api.eth.Merger(); !merger.TDDReached() {
   194  			merger.ReachTTD()
   195  			api.eth.Downloader().Cancel()
   196  		}
   197  		log.Info("Forkchoice requested sync to new head", "number", header.Number, "hash", header.Hash())
   198  		if err := api.eth.Downloader().BeaconSync(api.eth.SyncMode(), header); err != nil {
   199  			return beacon.STATUS_SYNCING, err
   200  		}
   201  		return beacon.STATUS_SYNCING, nil
   202  	}
   203  	// Block is known locally, just sanity check that the beacon client does not
   204  	// attempt to push us back to before the merge.
   205  	if block.Difficulty().BitLen() > 0 || block.NumberU64() == 0 {
   206  		var (
   207  			td  = api.eth.BlockChain().GetTd(update.HeadBlockHash, block.NumberU64())
   208  			ptd = api.eth.BlockChain().GetTd(block.ParentHash(), block.NumberU64()-1)
   209  			ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty
   210  		)
   211  		if td == nil || (block.NumberU64() > 0 && ptd == nil) {
   212  			log.Error("TDs unavailable for TTD check", "number", block.NumberU64(), "hash", update.HeadBlockHash, "td", td, "parent", block.ParentHash(), "ptd", ptd)
   213  			return beacon.STATUS_INVALID, errors.New("TDs unavailable for TDD check")
   214  		}
   215  		if td.Cmp(ttd) < 0 {
   216  			log.Error("Refusing beacon update to pre-merge", "number", block.NumberU64(), "hash", update.HeadBlockHash, "diff", block.Difficulty(), "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)))
   217  			return beacon.ForkChoiceResponse{PayloadStatus: beacon.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil
   218  		}
   219  		if block.NumberU64() > 0 && ptd.Cmp(ttd) >= 0 {
   220  			log.Error("Parent block is already post-ttd", "number", block.NumberU64(), "hash", update.HeadBlockHash, "diff", block.Difficulty(), "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)))
   221  			return beacon.ForkChoiceResponse{PayloadStatus: beacon.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil
   222  		}
   223  	}
   224  	valid := func(id *beacon.PayloadID) beacon.ForkChoiceResponse {
   225  		return beacon.ForkChoiceResponse{
   226  			PayloadStatus: beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &update.HeadBlockHash},
   227  			PayloadID:     id,
   228  		}
   229  	}
   230  	if rawdb.ReadCanonicalHash(api.eth.ChainDb(), block.NumberU64()) != update.HeadBlockHash {
   231  		// Block is not canonical, set head.
   232  		if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil {
   233  			return beacon.ForkChoiceResponse{PayloadStatus: beacon.PayloadStatusV1{Status: beacon.INVALID, LatestValidHash: &latestValid}}, err
   234  		}
   235  	} else if api.eth.BlockChain().CurrentBlock().Hash() == update.HeadBlockHash {
   236  		// If the specified head matches with our local head, do nothing and keep
   237  		// generating the payload. It's a special corner case that a few slots are
   238  		// missing and we are requested to generate the payload in slot.
   239  	} else {
   240  		// If the head block is already in our canonical chain, the beacon client is
   241  		// probably resyncing. Ignore the update.
   242  		log.Info("Ignoring beacon update to old head", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().NumberU64())
   243  		return valid(nil), nil
   244  	}
   245  	api.eth.SetSynced()
   246  
   247  	// If the beacon client also advertised a finalized block, mark the local
   248  	// chain final and completely in PoS mode.
   249  	if update.FinalizedBlockHash != (common.Hash{}) {
   250  		if merger := api.eth.Merger(); !merger.PoSFinalized() {
   251  			merger.FinalizePoS()
   252  		}
   253  		// If the finalized block is not in our canonical tree, somethings wrong
   254  		finalBlock := api.eth.BlockChain().GetBlockByHash(update.FinalizedBlockHash)
   255  		if finalBlock == nil {
   256  			log.Warn("Final block not available in database", "hash", update.FinalizedBlockHash)
   257  			return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("final block not available in database"))
   258  		} else if rawdb.ReadCanonicalHash(api.eth.ChainDb(), finalBlock.NumberU64()) != update.FinalizedBlockHash {
   259  			log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", update.HeadBlockHash)
   260  			return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("final block not in canonical chain"))
   261  		}
   262  		// Set the finalized block
   263  		api.eth.BlockChain().SetFinalized(finalBlock)
   264  	}
   265  	// Check if the safe block hash is in our canonical tree, if not somethings wrong
   266  	if update.SafeBlockHash != (common.Hash{}) {
   267  		safeBlock := api.eth.BlockChain().GetBlockByHash(update.SafeBlockHash)
   268  		if safeBlock == nil {
   269  			log.Warn("Safe block not available in database")
   270  			return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("safe block not available in database"))
   271  		}
   272  		if rawdb.ReadCanonicalHash(api.eth.ChainDb(), safeBlock.NumberU64()) != update.SafeBlockHash {
   273  			log.Warn("Safe block not in canonical chain")
   274  			return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("safe block not in canonical chain"))
   275  		}
   276  		// Set the safe block
   277  		api.eth.BlockChain().SetSafe(safeBlock)
   278  	}
   279  	// If payload generation was requested, create a new block to be potentially
   280  	// sealed by the beacon client. The payload will be requested later, and we
   281  	// will replace it arbitrarily many times in between.
   282  	if payloadAttributes != nil {
   283  		args := &miner.BuildPayloadArgs{
   284  			Parent:       update.HeadBlockHash,
   285  			Timestamp:    payloadAttributes.Timestamp,
   286  			FeeRecipient: payloadAttributes.SuggestedFeeRecipient,
   287  			Random:       payloadAttributes.Random,
   288  		}
   289  		id := args.Id()
   290  		// If we already are busy generating this work, then we do not need
   291  		// to start a second process.
   292  		if api.localBlocks.has(id) {
   293  			return valid(&id), nil
   294  		}
   295  		payload, err := api.eth.Miner().BuildPayload(args)
   296  		if err != nil {
   297  			log.Error("Failed to build payload", "err", err)
   298  			return valid(nil), beacon.InvalidPayloadAttributes.With(err)
   299  		}
   300  		api.localBlocks.put(id, payload)
   301  		return valid(&id), nil
   302  	}
   303  	return valid(nil), nil
   304  }
   305  
   306  // ExchangeTransitionConfigurationV1 checks the given configuration against
   307  // the configuration of the node.
   308  func (api *ConsensusAPI) ExchangeTransitionConfigurationV1(config beacon.TransitionConfigurationV1) (*beacon.TransitionConfigurationV1, error) {
   309  	log.Trace("Engine API request received", "method", "ExchangeTransitionConfiguration", "ttd", config.TerminalTotalDifficulty)
   310  	if config.TerminalTotalDifficulty == nil {
   311  		return nil, errors.New("invalid terminal total difficulty")
   312  	}
   313  	// Stash away the last update to warn the user if the beacon client goes offline
   314  	api.lastTransitionLock.Lock()
   315  	api.lastTransitionUpdate = time.Now()
   316  	api.lastTransitionLock.Unlock()
   317  
   318  	ttd := api.eth.BlockChain().Config().TerminalTotalDifficulty
   319  	if ttd == nil || ttd.Cmp(config.TerminalTotalDifficulty.ToInt()) != 0 {
   320  		log.Warn("Invalid TTD configured", "geth", ttd, "beacon", config.TerminalTotalDifficulty)
   321  		return nil, fmt.Errorf("invalid ttd: execution %v consensus %v", ttd, config.TerminalTotalDifficulty)
   322  	}
   323  	if config.TerminalBlockHash != (common.Hash{}) {
   324  		if hash := api.eth.BlockChain().GetCanonicalHash(uint64(config.TerminalBlockNumber)); hash == config.TerminalBlockHash {
   325  			return &beacon.TransitionConfigurationV1{
   326  				TerminalTotalDifficulty: (*hexutil.Big)(ttd),
   327  				TerminalBlockHash:       config.TerminalBlockHash,
   328  				TerminalBlockNumber:     config.TerminalBlockNumber,
   329  			}, nil
   330  		}
   331  		return nil, fmt.Errorf("invalid terminal block hash")
   332  	}
   333  	return &beacon.TransitionConfigurationV1{TerminalTotalDifficulty: (*hexutil.Big)(ttd)}, nil
   334  }
   335  
   336  // GetPayloadV1 returns a cached payload by id.
   337  func (api *ConsensusAPI) GetPayloadV1(payloadID beacon.PayloadID) (*beacon.ExecutableDataV1, error) {
   338  	log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID)
   339  	data := api.localBlocks.get(payloadID)
   340  	if data == nil {
   341  		return nil, beacon.UnknownPayload
   342  	}
   343  	return data, nil
   344  }
   345  
   346  // NewPayloadV1 creates an Eth1 block, inserts it in the chain, and returns the status of the chain.
   347  func (api *ConsensusAPI) NewPayloadV1(params beacon.ExecutableDataV1) (beacon.PayloadStatusV1, error) {
   348  	// The locking here is, strictly, not required. Without these locks, this can happen:
   349  	//
   350  	// 1. NewPayload( execdata-N ) is invoked from the CL. It goes all the way down to
   351  	//      api.eth.BlockChain().InsertBlockWithoutSetHead, where it is blocked on
   352  	//      e.g database compaction.
   353  	// 2. The call times out on the CL layer, which issues another NewPayload (execdata-N) call.
   354  	//    Similarly, this also get stuck on the same place. Importantly, since the
   355  	//    first call has not gone through, the early checks for "do we already have this block"
   356  	//    will all return false.
   357  	// 3. When the db compaction ends, then N calls inserting the same payload are processed
   358  	//    sequentially.
   359  	// Hence, we use a lock here, to be sure that the previous call has finished before we
   360  	// check whether we already have the block locally.
   361  	api.newPayloadLock.Lock()
   362  	defer api.newPayloadLock.Unlock()
   363  
   364  	log.Trace("Engine API request received", "method", "ExecutePayload", "number", params.Number, "hash", params.BlockHash)
   365  	block, err := beacon.ExecutableDataToBlock(params)
   366  	if err != nil {
   367  		log.Debug("Invalid NewPayload params", "params", params, "error", err)
   368  		return beacon.PayloadStatusV1{Status: beacon.INVALIDBLOCKHASH}, nil
   369  	}
   370  	// Stash away the last update to warn the user if the beacon client goes offline
   371  	api.lastNewPayloadLock.Lock()
   372  	api.lastNewPayloadUpdate = time.Now()
   373  	api.lastNewPayloadLock.Unlock()
   374  
   375  	// If we already have the block locally, ignore the entire execution and just
   376  	// return a fake success.
   377  	if block := api.eth.BlockChain().GetBlockByHash(params.BlockHash); block != nil {
   378  		log.Warn("Ignoring already known beacon payload", "number", params.Number, "hash", params.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)))
   379  		hash := block.Hash()
   380  		return beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &hash}, nil
   381  	}
   382  	// If this block was rejected previously, keep rejecting it
   383  	if res := api.checkInvalidAncestor(block.Hash(), block.Hash()); res != nil {
   384  		return *res, nil
   385  	}
   386  	// If the parent is missing, we - in theory - could trigger a sync, but that
   387  	// would also entail a reorg. That is problematic if multiple sibling blocks
   388  	// are being fed to us, and even more so, if some semi-distant uncle shortens
   389  	// our live chain. As such, payload execution will not permit reorgs and thus
   390  	// will not trigger a sync cycle. That is fine though, if we get a fork choice
   391  	// update after legit payload executions.
   392  	parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1)
   393  	if parent == nil {
   394  		return api.delayPayloadImport(block)
   395  	}
   396  	// We have an existing parent, do some sanity checks to avoid the beacon client
   397  	// triggering too early
   398  	var (
   399  		ptd  = api.eth.BlockChain().GetTd(parent.Hash(), parent.NumberU64())
   400  		ttd  = api.eth.BlockChain().Config().TerminalTotalDifficulty
   401  		gptd = api.eth.BlockChain().GetTd(parent.ParentHash(), parent.NumberU64()-1)
   402  	)
   403  	if ptd.Cmp(ttd) < 0 {
   404  		log.Warn("Ignoring pre-merge payload", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd)
   405  		return beacon.INVALID_TERMINAL_BLOCK, nil
   406  	}
   407  	if parent.Difficulty().BitLen() > 0 && gptd != nil && gptd.Cmp(ttd) >= 0 {
   408  		log.Error("Ignoring pre-merge parent block", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd)
   409  		return beacon.INVALID_TERMINAL_BLOCK, nil
   410  	}
   411  	if block.Time() <= parent.Time() {
   412  		log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time())
   413  		return api.invalid(errors.New("invalid timestamp"), parent.Header()), nil
   414  	}
   415  	// Another cornercase: if the node is in snap sync mode, but the CL client
   416  	// tries to make it import a block. That should be denied as pushing something
   417  	// into the database directly will conflict with the assumptions of snap sync
   418  	// that it has an empty db that it can fill itself.
   419  	if api.eth.SyncMode() != downloader.FullSync {
   420  		return api.delayPayloadImport(block)
   421  	}
   422  	if !api.eth.BlockChain().HasBlockAndState(block.ParentHash(), block.NumberU64()-1) {
   423  		api.remoteBlocks.put(block.Hash(), block.Header())
   424  		log.Warn("State not available, ignoring new payload")
   425  		return beacon.PayloadStatusV1{Status: beacon.ACCEPTED}, nil
   426  	}
   427  	log.Trace("Inserting block without sethead", "hash", block.Hash(), "number", block.Number)
   428  	if err := api.eth.BlockChain().InsertBlockWithoutSetHead(block); err != nil {
   429  		log.Warn("NewPayloadV1: inserting block failed", "error", err)
   430  
   431  		api.invalidLock.Lock()
   432  		api.invalidBlocksHits[block.Hash()] = 1
   433  		api.invalidTipsets[block.Hash()] = block.Header()
   434  		api.invalidLock.Unlock()
   435  
   436  		return api.invalid(err, parent.Header()), nil
   437  	}
   438  	// We've accepted a valid payload from the beacon client. Mark the local
   439  	// chain transitions to notify other subsystems (e.g. downloader) of the
   440  	// behavioral change.
   441  	if merger := api.eth.Merger(); !merger.TDDReached() {
   442  		merger.ReachTTD()
   443  		api.eth.Downloader().Cancel()
   444  	}
   445  	hash := block.Hash()
   446  	return beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &hash}, nil
   447  }
   448  
   449  // delayPayloadImport stashes the given block away for import at a later time,
   450  // either via a forkchoice update or a sync extension. This method is meant to
   451  // be called by the newpayload command when the block seems to be ok, but some
   452  // prerequisite prevents it from being processed (e.g. no parent, or snap sync).
   453  func (api *ConsensusAPI) delayPayloadImport(block *types.Block) (beacon.PayloadStatusV1, error) {
   454  	// Sanity check that this block's parent is not on a previously invalidated
   455  	// chain. If it is, mark the block as invalid too.
   456  	if res := api.checkInvalidAncestor(block.ParentHash(), block.Hash()); res != nil {
   457  		return *res, nil
   458  	}
   459  	// Stash the block away for a potential forced forkchoice update to it
   460  	// at a later time.
   461  	api.remoteBlocks.put(block.Hash(), block.Header())
   462  
   463  	// Although we don't want to trigger a sync, if there is one already in
   464  	// progress, try to extend if with the current payload request to relieve
   465  	// some strain from the forkchoice update.
   466  	if err := api.eth.Downloader().BeaconExtend(api.eth.SyncMode(), block.Header()); err == nil {
   467  		log.Debug("Payload accepted for sync extension", "number", block.NumberU64(), "hash", block.Hash())
   468  		return beacon.PayloadStatusV1{Status: beacon.SYNCING}, nil
   469  	}
   470  	// Either no beacon sync was started yet, or it rejected the delivered
   471  	// payload as non-integratable on top of the existing sync. We'll just
   472  	// have to rely on the beacon client to forcefully update the head with
   473  	// a forkchoice update request.
   474  	if api.eth.SyncMode() == downloader.FullSync {
   475  		// In full sync mode, failure to import a well-formed block can only mean
   476  		// that the parent state is missing and the syncer rejected extending the
   477  		// current cycle with the new payload.
   478  		log.Warn("Ignoring payload with missing parent", "number", block.NumberU64(), "hash", block.Hash(), "parent", block.ParentHash())
   479  	} else {
   480  		// In non-full sync mode (i.e. snap sync) all payloads are rejected until
   481  		// snap sync terminates as snap sync relies on direct database injections
   482  		// and cannot afford concurrent out-if-band modifications via imports.
   483  		log.Warn("Ignoring payload while snap syncing", "number", block.NumberU64(), "hash", block.Hash())
   484  	}
   485  	return beacon.PayloadStatusV1{Status: beacon.SYNCING}, nil
   486  }
   487  
   488  // setInvalidAncestor is a callback for the downloader to notify us if a bad block
   489  // is encountered during the async sync.
   490  func (api *ConsensusAPI) setInvalidAncestor(invalid *types.Header, origin *types.Header) {
   491  	api.invalidLock.Lock()
   492  	defer api.invalidLock.Unlock()
   493  
   494  	api.invalidTipsets[origin.Hash()] = invalid
   495  	api.invalidBlocksHits[invalid.Hash()]++
   496  }
   497  
   498  // checkInvalidAncestor checks whether the specified chain end links to a known
   499  // bad ancestor. If yes, it constructs the payload failure response to return.
   500  func (api *ConsensusAPI) checkInvalidAncestor(check common.Hash, head common.Hash) *beacon.PayloadStatusV1 {
   501  	api.invalidLock.Lock()
   502  	defer api.invalidLock.Unlock()
   503  
   504  	// If the hash to check is unknown, return valid
   505  	invalid, ok := api.invalidTipsets[check]
   506  	if !ok {
   507  		return nil
   508  	}
   509  	// If the bad hash was hit too many times, evict it and try to reprocess in
   510  	// the hopes that we have a data race that we can exit out of.
   511  	badHash := invalid.Hash()
   512  
   513  	api.invalidBlocksHits[badHash]++
   514  	if api.invalidBlocksHits[badHash] >= invalidBlockHitEviction {
   515  		log.Warn("Too many bad block import attempt, trying", "number", invalid.Number, "hash", badHash)
   516  		delete(api.invalidBlocksHits, badHash)
   517  
   518  		for descendant, badHeader := range api.invalidTipsets {
   519  			if badHeader.Hash() == badHash {
   520  				delete(api.invalidTipsets, descendant)
   521  			}
   522  		}
   523  		return nil
   524  	}
   525  	// Not too many failures yet, mark the head of the invalid chain as invalid
   526  	if check != head {
   527  		log.Warn("Marked new chain head as invalid", "hash", head, "badnumber", invalid.Number, "badhash", badHash)
   528  		for len(api.invalidTipsets) >= invalidTipsetsCap {
   529  			for key := range api.invalidTipsets {
   530  				delete(api.invalidTipsets, key)
   531  				break
   532  			}
   533  		}
   534  		api.invalidTipsets[head] = invalid
   535  	}
   536  	// If the last valid hash is the terminal pow block, return 0x0 for latest valid hash
   537  	lastValid := &invalid.ParentHash
   538  	if header := api.eth.BlockChain().GetHeader(invalid.ParentHash, invalid.Number.Uint64()-1); header != nil && header.Difficulty.Sign() != 0 {
   539  		lastValid = &common.Hash{}
   540  	}
   541  	failure := "links to previously rejected block"
   542  	return &beacon.PayloadStatusV1{
   543  		Status:          beacon.INVALID,
   544  		LatestValidHash: lastValid,
   545  		ValidationError: &failure,
   546  	}
   547  }
   548  
   549  // invalid returns a response "INVALID" with the latest valid hash supplied by latest or to the current head
   550  // if no latestValid block was provided.
   551  func (api *ConsensusAPI) invalid(err error, latestValid *types.Header) beacon.PayloadStatusV1 {
   552  	currentHash := api.eth.BlockChain().CurrentBlock().Hash()
   553  	if latestValid != nil {
   554  		// Set latest valid hash to 0x0 if parent is PoW block
   555  		currentHash = common.Hash{}
   556  		if latestValid.Difficulty.BitLen() == 0 {
   557  			// Otherwise set latest valid hash to parent hash
   558  			currentHash = latestValid.Hash()
   559  		}
   560  	}
   561  	errorMsg := err.Error()
   562  	return beacon.PayloadStatusV1{Status: beacon.INVALID, LatestValidHash: &currentHash, ValidationError: &errorMsg}
   563  }
   564  
   565  // heartbeat loops indefinitely, and checks if there have been beacon client updates
   566  // received in the last while. If not - or if they but strange ones - it warns the
   567  // user that something might be off with their consensus node.
   568  //
   569  // TODO(karalabe): Spin this goroutine down somehow
   570  func (api *ConsensusAPI) heartbeat() {
   571  	// Sleep a bit on startup since there's obviously no beacon client yet
   572  	// attached, so no need to print scary warnings to the user.
   573  	time.Sleep(beaconUpdateStartupTimeout)
   574  
   575  	var (
   576  		offlineLogged time.Time
   577  		ttd           = api.eth.BlockChain().Config().TerminalTotalDifficulty
   578  	)
   579  	// If the network is not yet merged/merging, don't bother continuing.
   580  	if ttd == nil {
   581  		return
   582  	}
   583  	for {
   584  		// Sleep a bit and retrieve the last known consensus updates
   585  		time.Sleep(5 * time.Second)
   586  
   587  		api.lastTransitionLock.Lock()
   588  		lastTransitionUpdate := api.lastTransitionUpdate
   589  		api.lastTransitionLock.Unlock()
   590  
   591  		api.lastForkchoiceLock.Lock()
   592  		lastForkchoiceUpdate := api.lastForkchoiceUpdate
   593  		api.lastForkchoiceLock.Unlock()
   594  
   595  		api.lastNewPayloadLock.Lock()
   596  		lastNewPayloadUpdate := api.lastNewPayloadUpdate
   597  		api.lastNewPayloadLock.Unlock()
   598  
   599  		// If there have been no updates for the past while, warn the user
   600  		// that the beacon client is probably offline
   601  		if api.eth.BlockChain().Config().TerminalTotalDifficultyPassed || api.eth.Merger().TDDReached() {
   602  			if time.Since(lastForkchoiceUpdate) <= beaconUpdateConsensusTimeout || time.Since(lastNewPayloadUpdate) <= beaconUpdateConsensusTimeout {
   603  				offlineLogged = time.Time{}
   604  				continue
   605  			}
   606  			if time.Since(lastTransitionUpdate) > beaconUpdateExchangeTimeout {
   607  				if time.Since(offlineLogged) > beaconUpdateWarnFrequency {
   608  					if lastTransitionUpdate.IsZero() {
   609  						log.Warn("Post-merge network, but no beacon client seen. Please launch one to follow the chain!")
   610  					} else {
   611  						log.Warn("Previously seen beacon client is offline. Please ensure it is operational to follow the chain!")
   612  					}
   613  					offlineLogged = time.Now()
   614  				}
   615  				continue
   616  			}
   617  			if time.Since(offlineLogged) > beaconUpdateWarnFrequency {
   618  				if lastForkchoiceUpdate.IsZero() && lastNewPayloadUpdate.IsZero() {
   619  					log.Warn("Beacon client online, but never received consensus updates. Please ensure your beacon client is operational to follow the chain!")
   620  				} else {
   621  					log.Warn("Beacon client online, but no consensus updates received in a while. Please fix your beacon client to follow the chain!")
   622  				}
   623  				offlineLogged = time.Now()
   624  			}
   625  			continue
   626  		}
   627  		if time.Since(lastTransitionUpdate) <= beaconUpdateExchangeTimeout {
   628  			offlineLogged = time.Time{}
   629  			continue
   630  		}
   631  		if time.Since(offlineLogged) > beaconUpdateWarnFrequency {
   632  			// Retrieve the last few blocks and make a rough estimate as
   633  			// to when the merge transition should happen
   634  			var (
   635  				chain = api.eth.BlockChain()
   636  				head  = chain.CurrentHeader()
   637  				htd   = chain.GetTd(head.Hash(), head.Number.Uint64())
   638  			)
   639  			if htd.Cmp(ttd) >= 0 {
   640  				if lastTransitionUpdate.IsZero() {
   641  					log.Warn("Merge already reached, but no beacon client seen. Please launch one to follow the chain!")
   642  				} else {
   643  					log.Warn("Merge already reached, but previously seen beacon client is offline. Please ensure it is operational to follow the chain!")
   644  				}
   645  				offlineLogged = time.Now()
   646  				continue
   647  			}
   648  			var eta time.Duration
   649  			if head.Number.Uint64() > 0 {
   650  				// Accumulate the last 64 difficulties to estimate the growth
   651  				var (
   652  					deltaDiff uint64
   653  					deltaTime uint64
   654  					current   = head
   655  				)
   656  				for i := 0; i < 64; i++ {
   657  					parent := chain.GetHeader(current.ParentHash, current.Number.Uint64()-1)
   658  					if parent == nil {
   659  						break
   660  					}
   661  					deltaDiff += current.Difficulty.Uint64()
   662  					deltaTime += current.Time - parent.Time
   663  					current = parent
   664  				}
   665  				// Estimate an ETA based on the block times and the difficulty growth
   666  				if deltaTime > 0 {
   667  					growth := deltaDiff / deltaTime
   668  					left := new(big.Int).Sub(ttd, htd)
   669  					eta = time.Duration(new(big.Int).Div(left, new(big.Int).SetUint64(growth+1)).Uint64()) * time.Second
   670  				}
   671  			}
   672  			message := "Merge is configured, but previously seen beacon client is offline. Please ensure it is operational before the transition arrives!"
   673  			if lastTransitionUpdate.IsZero() {
   674  				message = "Merge is configured, but no beacon client seen. Please ensure you have one available before the transition arrives!"
   675  			}
   676  			if eta < time.Second {
   677  				log.Warn(message)
   678  			} else {
   679  				log.Warn(message, "eta", common.PrettyAge(time.Now().Add(-eta))) // weird hack, but duration formatted doesn't handle days
   680  			}
   681  			offlineLogged = time.Now()
   682  		}
   683  	}
   684  }