github.com/palisadeinc/bor@v0.0.0-20230615125219-ab7196213d15/eth/catalyst/api.go (about)

     1  // Copyright 2020 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  	"crypto/sha256"
    22  	"encoding/binary"
    23  	"errors"
    24  	"fmt"
    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/log"
    34  	"github.com/ethereum/go-ethereum/node"
    35  	"github.com/ethereum/go-ethereum/rpc"
    36  )
    37  
    38  // Register adds catalyst APIs to the full node.
    39  func Register(stack *node.Node, backend *eth.Ethereum) error {
    40  	log.Warn("Catalyst mode enabled", "protocol", "eth")
    41  	stack.RegisterAPIs([]rpc.API{
    42  		{
    43  			Namespace:     "engine",
    44  			Version:       "1.0",
    45  			Service:       NewConsensusAPI(backend),
    46  			Public:        true,
    47  			Authenticated: true,
    48  		},
    49  		{
    50  			Namespace:     "engine",
    51  			Version:       "1.0",
    52  			Service:       NewConsensusAPI(backend),
    53  			Public:        true,
    54  			Authenticated: false,
    55  		},
    56  	})
    57  	return nil
    58  }
    59  
    60  type ConsensusAPI struct {
    61  	eth          *eth.Ethereum
    62  	remoteBlocks *headerQueue  // Cache of remote payloads received
    63  	localBlocks  *payloadQueue // Cache of local payloads generated
    64  }
    65  
    66  // NewConsensusAPI creates a new consensus api for the given backend.
    67  // The underlying blockchain needs to have a valid terminal total difficulty set.
    68  func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI {
    69  	if eth.BlockChain().Config().TerminalTotalDifficulty == nil {
    70  		panic("Catalyst started without valid total difficulty")
    71  	}
    72  	return &ConsensusAPI{
    73  		eth:          eth,
    74  		remoteBlocks: newHeaderQueue(),
    75  		localBlocks:  newPayloadQueue(),
    76  	}
    77  }
    78  
    79  // ForkchoiceUpdatedV1 has several responsibilities:
    80  // If the method is called with an empty head block:
    81  // 		we return success, which can be used to check if the catalyst mode is enabled
    82  // If the total difficulty was not reached:
    83  // 		we return INVALID
    84  // If the finalizedBlockHash is set:
    85  // 		we check if we have the finalizedBlockHash in our db, if not we start a sync
    86  // We try to set our blockchain to the headBlock
    87  // If there are payloadAttributes:
    88  // 		we try to assemble a block with the payloadAttributes and return its payloadID
    89  func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, payloadAttributes *beacon.PayloadAttributesV1) (beacon.ForkChoiceResponse, error) {
    90  	log.Trace("Engine API request received", "method", "ForkchoiceUpdated", "head", update.HeadBlockHash, "finalized", update.FinalizedBlockHash, "safe", update.SafeBlockHash)
    91  	if update.HeadBlockHash == (common.Hash{}) {
    92  		log.Warn("Forkchoice requested update to zero hash")
    93  		return beacon.STATUS_INVALID, nil // TODO(karalabe): Why does someone send us this?
    94  	}
    95  	// Check whether we have the block yet in our database or not. If not, we'll
    96  	// need to either trigger a sync, or to reject this forkchoice update for a
    97  	// reason.
    98  	block := api.eth.BlockChain().GetBlockByHash(update.HeadBlockHash)
    99  	if block == nil {
   100  		// If the head hash is unknown (was not given to us in a newPayload request),
   101  		// we cannot resolve the header, so not much to do. This could be extended in
   102  		// the future to resolve from the `eth` network, but it's an unexpected case
   103  		// that should be fixed, not papered over.
   104  		header := api.remoteBlocks.get(update.HeadBlockHash)
   105  		if header == nil {
   106  			log.Warn("Forkchoice requested unknown head", "hash", update.HeadBlockHash)
   107  			return beacon.STATUS_SYNCING, nil
   108  		}
   109  		// Header advertised via a past newPayload request. Start syncing to it.
   110  		// Before we do however, make sure any legacy sync in switched off so we
   111  		// don't accidentally have 2 cycles running.
   112  		if merger := api.eth.Merger(); !merger.TDDReached() {
   113  			merger.ReachTTD()
   114  			api.eth.Downloader().Cancel()
   115  		}
   116  		log.Info("Forkchoice requested sync to new head", "number", header.Number, "hash", header.Hash())
   117  		if err := api.eth.Downloader().BeaconSync(api.eth.SyncMode(), header); err != nil {
   118  			return beacon.STATUS_SYNCING, err
   119  		}
   120  		return beacon.STATUS_SYNCING, nil
   121  	}
   122  	// Block is known locally, just sanity check that the beacon client does not
   123  	// attempt to push us back to before the merge.
   124  	if block.Difficulty().BitLen() > 0 || block.NumberU64() == 0 {
   125  		var (
   126  			td  = api.eth.BlockChain().GetTd(update.HeadBlockHash, block.NumberU64())
   127  			ptd = api.eth.BlockChain().GetTd(block.ParentHash(), block.NumberU64()-1)
   128  			ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty
   129  		)
   130  		if td == nil || (block.NumberU64() > 0 && ptd == nil) {
   131  			log.Error("TDs unavailable for TTD check", "number", block.NumberU64(), "hash", update.HeadBlockHash, "td", td, "parent", block.ParentHash(), "ptd", ptd)
   132  			return beacon.STATUS_INVALID, errors.New("TDs unavailable for TDD check")
   133  		}
   134  		if td.Cmp(ttd) < 0 || (block.NumberU64() > 0 && ptd.Cmp(ttd) > 0) {
   135  			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)))
   136  			return beacon.ForkChoiceResponse{PayloadStatus: beacon.PayloadStatusV1{Status: beacon.INVALIDTERMINALBLOCK}, PayloadID: nil}, nil
   137  		}
   138  	}
   139  
   140  	if rawdb.ReadCanonicalHash(api.eth.ChainDb(), block.NumberU64()) != update.HeadBlockHash {
   141  		// Block is not canonical, set head.
   142  		if err := api.eth.BlockChain().SetChainHead(block); err != nil {
   143  			return beacon.STATUS_INVALID, err
   144  		}
   145  	} else {
   146  		// If the head block is already in our canonical chain, the beacon client is
   147  		// probably resyncing. Ignore the update.
   148  		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())
   149  	}
   150  	api.eth.SetSynced()
   151  
   152  	// If the beacon client also advertised a finalized block, mark the local
   153  	// chain final and completely in PoS mode.
   154  	if update.FinalizedBlockHash != (common.Hash{}) {
   155  		if merger := api.eth.Merger(); !merger.PoSFinalized() {
   156  			merger.FinalizePoS()
   157  		}
   158  		// TODO (MariusVanDerWijden): If the finalized block is not in our canonical tree, somethings wrong
   159  		finalBlock := api.eth.BlockChain().GetBlockByHash(update.FinalizedBlockHash)
   160  		if finalBlock == nil {
   161  			log.Warn("Final block not available in database", "hash", update.FinalizedBlockHash)
   162  			return beacon.STATUS_INVALID, errors.New("final block not available")
   163  		} else if rawdb.ReadCanonicalHash(api.eth.ChainDb(), finalBlock.NumberU64()) != update.FinalizedBlockHash {
   164  			log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", update.HeadBlockHash)
   165  			return beacon.STATUS_INVALID, errors.New("final block not canonical")
   166  		}
   167  	}
   168  	// TODO (MariusVanDerWijden): Check if the safe block hash is in our canonical tree, if not somethings wrong
   169  	if update.SafeBlockHash != (common.Hash{}) {
   170  		safeBlock := api.eth.BlockChain().GetBlockByHash(update.SafeBlockHash)
   171  		if safeBlock == nil {
   172  			log.Warn("Safe block not available in database")
   173  			return beacon.STATUS_INVALID, errors.New("safe head not available")
   174  		}
   175  		if rawdb.ReadCanonicalHash(api.eth.ChainDb(), safeBlock.NumberU64()) != update.SafeBlockHash {
   176  			log.Warn("Safe block not in canonical chain")
   177  			return beacon.STATUS_INVALID, errors.New("safe head not canonical")
   178  		}
   179  	}
   180  	// If payload generation was requested, create a new block to be potentially
   181  	// sealed by the beacon client. The payload will be requested later, and we
   182  	// might replace it arbitrarily many times in between.
   183  	if payloadAttributes != nil {
   184  		log.Info("Creating new payload for sealing")
   185  		start := time.Now()
   186  
   187  		data, err := api.assembleBlock(update.HeadBlockHash, payloadAttributes)
   188  		if err != nil {
   189  			log.Error("Failed to create sealing payload", "err", err)
   190  			return api.validForkChoiceResponse(nil), err // valid setHead, invalid payload
   191  		}
   192  		id := computePayloadId(update.HeadBlockHash, payloadAttributes)
   193  		api.localBlocks.put(id, data)
   194  
   195  		log.Info("Created payload for sealing", "id", id, "elapsed", time.Since(start))
   196  		return api.validForkChoiceResponse(&id), nil
   197  	}
   198  	return api.validForkChoiceResponse(nil), nil
   199  }
   200  
   201  // validForkChoiceResponse returns the ForkChoiceResponse{VALID}
   202  // with the latest valid hash and an optional payloadID.
   203  func (api *ConsensusAPI) validForkChoiceResponse(id *beacon.PayloadID) beacon.ForkChoiceResponse {
   204  	currentHash := api.eth.BlockChain().CurrentBlock().Hash()
   205  	return beacon.ForkChoiceResponse{
   206  		PayloadStatus: beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &currentHash},
   207  		PayloadID:     id,
   208  	}
   209  }
   210  
   211  // ExchangeTransitionConfigurationV1 checks the given configuration against
   212  // the configuration of the node.
   213  func (api *ConsensusAPI) ExchangeTransitionConfigurationV1(config beacon.TransitionConfigurationV1) (*beacon.TransitionConfigurationV1, error) {
   214  	if config.TerminalTotalDifficulty == nil {
   215  		return nil, errors.New("invalid terminal total difficulty")
   216  	}
   217  	ttd := api.eth.BlockChain().Config().TerminalTotalDifficulty
   218  	if ttd.Cmp(config.TerminalTotalDifficulty.ToInt()) != 0 {
   219  		log.Warn("Invalid TTD configured", "geth", ttd, "beacon", config.TerminalTotalDifficulty)
   220  		return nil, fmt.Errorf("invalid ttd: execution %v consensus %v", ttd, config.TerminalTotalDifficulty)
   221  	}
   222  
   223  	if config.TerminalBlockHash != (common.Hash{}) {
   224  		if hash := api.eth.BlockChain().GetCanonicalHash(uint64(config.TerminalBlockNumber)); hash == config.TerminalBlockHash {
   225  			return &beacon.TransitionConfigurationV1{
   226  				TerminalTotalDifficulty: (*hexutil.Big)(ttd),
   227  				TerminalBlockHash:       config.TerminalBlockHash,
   228  				TerminalBlockNumber:     config.TerminalBlockNumber,
   229  			}, nil
   230  		}
   231  		return nil, fmt.Errorf("invalid terminal block hash")
   232  	}
   233  	return &beacon.TransitionConfigurationV1{TerminalTotalDifficulty: (*hexutil.Big)(ttd)}, nil
   234  }
   235  
   236  // GetPayloadV1 returns a cached payload by id.
   237  func (api *ConsensusAPI) GetPayloadV1(payloadID beacon.PayloadID) (*beacon.ExecutableDataV1, error) {
   238  	log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID)
   239  	data := api.localBlocks.get(payloadID)
   240  	if data == nil {
   241  		return nil, &beacon.UnknownPayload
   242  	}
   243  	return data, nil
   244  }
   245  
   246  // NewPayloadV1 creates an Eth1 block, inserts it in the chain, and returns the status of the chain.
   247  func (api *ConsensusAPI) NewPayloadV1(params beacon.ExecutableDataV1) (beacon.PayloadStatusV1, error) {
   248  	log.Trace("Engine API request received", "method", "ExecutePayload", "number", params.Number, "hash", params.BlockHash)
   249  	block, err := beacon.ExecutableDataToBlock(params)
   250  	if err != nil {
   251  		log.Debug("Invalid NewPayload params", "params", params, "error", err)
   252  		return beacon.PayloadStatusV1{Status: beacon.INVALIDBLOCKHASH}, nil
   253  	}
   254  	// If we already have the block locally, ignore the entire execution and just
   255  	// return a fake success.
   256  	if block := api.eth.BlockChain().GetBlockByHash(params.BlockHash); block != nil {
   257  		log.Warn("Ignoring already known beacon payload", "number", params.Number, "hash", params.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)))
   258  		hash := block.Hash()
   259  		return beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &hash}, nil
   260  	}
   261  	// If the parent is missing, we - in theory - could trigger a sync, but that
   262  	// would also entail a reorg. That is problematic if multiple sibling blocks
   263  	// are being fed to us, and even more so, if some semi-distant uncle shortens
   264  	// our live chain. As such, payload execution will not permit reorgs and thus
   265  	// will not trigger a sync cycle. That is fine though, if we get a fork choice
   266  	// update after legit payload executions.
   267  	parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1)
   268  	if parent == nil {
   269  		// Stash the block away for a potential forced forckchoice update to it
   270  		// at a later time.
   271  		api.remoteBlocks.put(block.Hash(), block.Header())
   272  
   273  		// Although we don't want to trigger a sync, if there is one already in
   274  		// progress, try to extend if with the current payload request to relieve
   275  		// some strain from the forkchoice update.
   276  		if err := api.eth.Downloader().BeaconExtend(api.eth.SyncMode(), block.Header()); err == nil {
   277  			log.Debug("Payload accepted for sync extension", "number", params.Number, "hash", params.BlockHash)
   278  			return beacon.PayloadStatusV1{Status: beacon.SYNCING}, nil
   279  		}
   280  		// Either no beacon sync was started yet, or it rejected the delivered
   281  		// payload as non-integratable on top of the existing sync. We'll just
   282  		// have to rely on the beacon client to forcefully update the head with
   283  		// a forkchoice update request.
   284  		log.Warn("Ignoring payload with missing parent", "number", params.Number, "hash", params.BlockHash, "parent", params.ParentHash)
   285  		return beacon.PayloadStatusV1{Status: beacon.ACCEPTED}, nil
   286  	}
   287  	// We have an existing parent, do some sanity checks to avoid the beacon client
   288  	// triggering too early
   289  	var (
   290  		td  = api.eth.BlockChain().GetTd(parent.Hash(), parent.NumberU64())
   291  		ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty
   292  	)
   293  	if td.Cmp(ttd) < 0 {
   294  		log.Warn("Ignoring pre-merge payload", "number", params.Number, "hash", params.BlockHash, "td", td, "ttd", ttd)
   295  		return beacon.PayloadStatusV1{Status: beacon.INVALIDTERMINALBLOCK}, nil
   296  	}
   297  	if block.Time() <= parent.Time() {
   298  		log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time())
   299  		return api.invalid(errors.New("invalid timestamp")), nil
   300  	}
   301  	if !api.eth.BlockChain().HasBlockAndState(block.ParentHash(), block.NumberU64()-1) {
   302  		api.remoteBlocks.put(block.Hash(), block.Header())
   303  		log.Warn("State not available, ignoring new payload")
   304  		return beacon.PayloadStatusV1{Status: beacon.ACCEPTED}, nil
   305  	}
   306  	log.Trace("Inserting block without sethead", "hash", block.Hash(), "number", block.Number)
   307  	if err := api.eth.BlockChain().InsertBlockWithoutSetHead(block); err != nil {
   308  		log.Warn("NewPayloadV1: inserting block failed", "error", err)
   309  		return api.invalid(err), nil
   310  	}
   311  	// We've accepted a valid payload from the beacon client. Mark the local
   312  	// chain transitions to notify other subsystems (e.g. downloader) of the
   313  	// behavioral change.
   314  	if merger := api.eth.Merger(); !merger.TDDReached() {
   315  		merger.ReachTTD()
   316  		api.eth.Downloader().Cancel()
   317  	}
   318  	hash := block.Hash()
   319  	return beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &hash}, nil
   320  }
   321  
   322  // computePayloadId computes a pseudo-random payloadid, based on the parameters.
   323  func computePayloadId(headBlockHash common.Hash, params *beacon.PayloadAttributesV1) beacon.PayloadID {
   324  	// Hash
   325  	hasher := sha256.New()
   326  	hasher.Write(headBlockHash[:])
   327  	binary.Write(hasher, binary.BigEndian, params.Timestamp)
   328  	hasher.Write(params.Random[:])
   329  	hasher.Write(params.SuggestedFeeRecipient[:])
   330  	var out beacon.PayloadID
   331  	copy(out[:], hasher.Sum(nil)[:8])
   332  	return out
   333  }
   334  
   335  // invalid returns a response "INVALID" with the latest valid hash set to the current head.
   336  func (api *ConsensusAPI) invalid(err error) beacon.PayloadStatusV1 {
   337  	currentHash := api.eth.BlockChain().CurrentHeader().Hash()
   338  	errorMsg := err.Error()
   339  	return beacon.PayloadStatusV1{Status: beacon.INVALID, LatestValidHash: &currentHash, ValidationError: &errorMsg}
   340  }
   341  
   342  // assembleBlock creates a new block and returns the "execution
   343  // data" required for beacon clients to process the new block.
   344  func (api *ConsensusAPI) assembleBlock(parentHash common.Hash, params *beacon.PayloadAttributesV1) (*beacon.ExecutableDataV1, error) {
   345  	log.Info("Producing block", "parentHash", parentHash)
   346  	block, err := api.eth.Miner().GetSealingBlock(parentHash, params.Timestamp, params.SuggestedFeeRecipient, params.Random)
   347  	if err != nil {
   348  		return nil, err
   349  	}
   350  	return beacon.BlockToExecutableData(block), nil
   351  }
   352  
   353  // Used in tests to add a the list of transactions from a block to the tx pool.
   354  func (api *ConsensusAPI) insertTransactions(txs types.Transactions) error {
   355  	for _, tx := range txs {
   356  		api.eth.TxPool().AddLocal(tx)
   357  	}
   358  	return nil
   359  }