github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/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 "strconv" 24 "sync" 25 "time" 26 27 "github.com/ethereum/go-ethereum/beacon/engine" 28 "github.com/ethereum/go-ethereum/common" 29 "github.com/ethereum/go-ethereum/common/hexutil" 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/internal/version" 35 "github.com/ethereum/go-ethereum/log" 36 "github.com/ethereum/go-ethereum/miner" 37 "github.com/ethereum/go-ethereum/node" 38 "github.com/ethereum/go-ethereum/params" 39 "github.com/ethereum/go-ethereum/params/forks" 40 "github.com/ethereum/go-ethereum/rpc" 41 ) 42 43 // Register adds the engine API to the full node. 44 func Register(stack *node.Node, backend *eth.Ethereum) error { 45 log.Warn("Engine API enabled", "protocol", "eth") 46 stack.RegisterAPIs([]rpc.API{ 47 { 48 Namespace: "engine", 49 Service: NewConsensusAPI(backend), 50 Authenticated: true, 51 }, 52 }) 53 return nil 54 } 55 56 const ( 57 // invalidBlockHitEviction is the number of times an invalid block can be 58 // referenced in forkchoice update or new payload before it is attempted 59 // to be reprocessed again. 60 invalidBlockHitEviction = 128 61 62 // invalidTipsetsCap is the max number of recent block hashes tracked that 63 // have lead to some bad ancestor block. It's just an OOM protection. 64 invalidTipsetsCap = 512 65 66 // beaconUpdateStartupTimeout is the time to wait for a beacon client to get 67 // attached before starting to issue warnings. 68 beaconUpdateStartupTimeout = 30 * time.Second 69 70 // beaconUpdateConsensusTimeout is the max time allowed for a beacon client 71 // to send a consensus update before it's considered offline and the user is 72 // warned. 73 beaconUpdateConsensusTimeout = 2 * time.Minute 74 75 // beaconUpdateWarnFrequency is the frequency at which to warn the user that 76 // the beacon client is offline. 77 beaconUpdateWarnFrequency = 5 * time.Minute 78 ) 79 80 // All methods provided over the engine endpoint. 81 var caps = []string{ 82 "engine_forkchoiceUpdatedV1", 83 "engine_forkchoiceUpdatedV2", 84 "engine_forkchoiceUpdatedV3", 85 "engine_exchangeTransitionConfigurationV1", 86 "engine_getPayloadV1", 87 "engine_getPayloadV2", 88 "engine_getPayloadV3", 89 "engine_newPayloadV1", 90 "engine_newPayloadV2", 91 "engine_newPayloadV3", 92 "engine_getPayloadBodiesByHashV1", 93 "engine_getPayloadBodiesByRangeV1", 94 "engine_getClientVersionV1", 95 } 96 97 type ConsensusAPI struct { 98 eth *eth.Ethereum 99 100 remoteBlocks *headerQueue // Cache of remote payloads received 101 localBlocks *payloadQueue // Cache of local payloads generated 102 103 // The forkchoice update and new payload method require us to return the 104 // latest valid hash in an invalid chain. To support that return, we need 105 // to track historical bad blocks as well as bad tipsets in case a chain 106 // is constantly built on it. 107 // 108 // There are a few important caveats in this mechanism: 109 // - The bad block tracking is ephemeral, in-memory only. We must never 110 // persist any bad block information to disk as a bug in Geth could end 111 // up blocking a valid chain, even if a later Geth update would accept 112 // it. 113 // - Bad blocks will get forgotten after a certain threshold of import 114 // attempts and will be retried. The rationale is that if the network 115 // really-really-really tries to feed us a block, we should give it a 116 // new chance, perhaps us being racey instead of the block being legit 117 // bad (this happened in Geth at a point with import vs. pending race). 118 // - Tracking all the blocks built on top of the bad one could be a bit 119 // problematic, so we will only track the head chain segment of a bad 120 // chain to allow discarding progressing bad chains and side chains, 121 // without tracking too much bad data. 122 invalidBlocksHits map[common.Hash]int // Ephemeral cache to track invalid blocks and their hit count 123 invalidTipsets map[common.Hash]*types.Header // Ephemeral cache to track invalid tipsets and their bad ancestor 124 invalidLock sync.Mutex // Protects the invalid maps from concurrent access 125 126 // Geth can appear to be stuck or do strange things if the beacon client is 127 // offline or is sending us strange data. Stash some update stats away so 128 // that we can warn the user and not have them open issues on our tracker. 129 lastTransitionUpdate time.Time 130 lastTransitionLock sync.Mutex 131 lastForkchoiceUpdate time.Time 132 lastForkchoiceLock sync.Mutex 133 lastNewPayloadUpdate time.Time 134 lastNewPayloadLock sync.Mutex 135 136 forkchoiceLock sync.Mutex // Lock for the forkChoiceUpdated method 137 newPayloadLock sync.Mutex // Lock for the NewPayload method 138 } 139 140 // NewConsensusAPI creates a new consensus api for the given backend. 141 // The underlying blockchain needs to have a valid terminal total difficulty set. 142 func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI { 143 api := newConsensusAPIWithoutHeartbeat(eth) 144 go api.heartbeat() 145 return api 146 } 147 148 // newConsensusAPIWithoutHeartbeat creates a new consensus api for the SimulatedBeacon Node. 149 func newConsensusAPIWithoutHeartbeat(eth *eth.Ethereum) *ConsensusAPI { 150 if eth.BlockChain().Config().TerminalTotalDifficulty == nil { 151 log.Warn("Engine API started but chain not configured for merge yet") 152 } 153 api := &ConsensusAPI{ 154 eth: eth, 155 remoteBlocks: newHeaderQueue(), 156 localBlocks: newPayloadQueue(), 157 invalidBlocksHits: make(map[common.Hash]int), 158 invalidTipsets: make(map[common.Hash]*types.Header), 159 } 160 eth.Downloader().SetBadBlockCallback(api.setInvalidAncestor) 161 return api 162 } 163 164 // ForkchoiceUpdatedV1 has several responsibilities: 165 // 166 // We try to set our blockchain to the headBlock. 167 // 168 // If the method is called with an empty head block: we return success, which can be used 169 // to check if the engine API is enabled. 170 // 171 // If the total difficulty was not reached: we return INVALID. 172 // 173 // If the finalizedBlockHash is set: we check if we have the finalizedBlockHash in our db, 174 // if not we start a sync. 175 // 176 // If there are payloadAttributes: we try to assemble a block with the payloadAttributes 177 // and return its payloadID. 178 func (api *ConsensusAPI) ForkchoiceUpdatedV1(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { 179 if payloadAttributes != nil { 180 if payloadAttributes.Withdrawals != nil || payloadAttributes.BeaconRoot != nil { 181 return engine.STATUS_INVALID, engine.InvalidParams.With(errors.New("withdrawals and beacon root not supported in V1")) 182 } 183 if api.eth.BlockChain().Config().IsShanghai(api.eth.BlockChain().Config().LondonBlock, payloadAttributes.Timestamp) { 184 return engine.STATUS_INVALID, engine.InvalidParams.With(errors.New("forkChoiceUpdateV1 called post-shanghai")) 185 } 186 } 187 return api.forkchoiceUpdated(update, payloadAttributes, engine.PayloadV1, false) 188 } 189 190 // ForkchoiceUpdatedV2 is equivalent to V1 with the addition of withdrawals in the payload 191 // attributes. It supports both PayloadAttributesV1 and PayloadAttributesV2. 192 func (api *ConsensusAPI) ForkchoiceUpdatedV2(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { 193 if params != nil { 194 if params.BeaconRoot != nil { 195 return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("unexpected beacon root")) 196 } 197 switch api.eth.BlockChain().Config().LatestFork(params.Timestamp) { 198 case forks.Paris: 199 if params.Withdrawals != nil { 200 return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("withdrawals before shanghai")) 201 } 202 case forks.Shanghai: 203 if params.Withdrawals == nil { 204 return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("missing withdrawals")) 205 } 206 default: 207 return engine.STATUS_INVALID, engine.UnsupportedFork.With(errors.New("forkchoiceUpdatedV2 must only be called with paris and shanghai payloads")) 208 } 209 } 210 return api.forkchoiceUpdated(update, params, engine.PayloadV2, false) 211 } 212 213 // ForkchoiceUpdatedV3 is equivalent to V2 with the addition of parent beacon block root 214 // in the payload attributes. It supports only PayloadAttributesV3. 215 func (api *ConsensusAPI) ForkchoiceUpdatedV3(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { 216 if params != nil { 217 if params.Withdrawals == nil { 218 return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("missing withdrawals")) 219 } 220 if params.BeaconRoot == nil { 221 return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("missing beacon root")) 222 } 223 if api.eth.BlockChain().Config().LatestFork(params.Timestamp) != forks.Cancun { 224 return engine.STATUS_INVALID, engine.UnsupportedFork.With(errors.New("forkchoiceUpdatedV3 must only be called for cancun payloads")) 225 } 226 } 227 // TODO(matt): the spec requires that fcu is applied when called on a valid 228 // hash, even if params are wrong. To do this we need to split up 229 // forkchoiceUpdate into a function that only updates the head and then a 230 // function that kicks off block construction. 231 return api.forkchoiceUpdated(update, params, engine.PayloadV3, false) 232 } 233 234 func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes, payloadVersion engine.PayloadVersion, simulatorMode bool) (engine.ForkChoiceResponse, error) { 235 api.forkchoiceLock.Lock() 236 defer api.forkchoiceLock.Unlock() 237 238 log.Trace("Engine API request received", "method", "ForkchoiceUpdated", "head", update.HeadBlockHash, "finalized", update.FinalizedBlockHash, "safe", update.SafeBlockHash) 239 if update.HeadBlockHash == (common.Hash{}) { 240 log.Warn("Forkchoice requested update to zero hash") 241 return engine.STATUS_INVALID, nil // TODO(karalabe): Why does someone send us this? 242 } 243 // Stash away the last update to warn the user if the beacon client goes offline 244 api.lastForkchoiceLock.Lock() 245 api.lastForkchoiceUpdate = time.Now() 246 api.lastForkchoiceLock.Unlock() 247 248 // Check whether we have the block yet in our database or not. If not, we'll 249 // need to either trigger a sync, or to reject this forkchoice update for a 250 // reason. 251 block := api.eth.BlockChain().GetBlockByHash(update.HeadBlockHash) 252 if block == nil { 253 // If this block was previously invalidated, keep rejecting it here too 254 if res := api.checkInvalidAncestor(update.HeadBlockHash, update.HeadBlockHash); res != nil { 255 return engine.ForkChoiceResponse{PayloadStatus: *res, PayloadID: nil}, nil 256 } 257 // If the head hash is unknown (was not given to us in a newPayload request), 258 // we cannot resolve the header, so not much to do. This could be extended in 259 // the future to resolve from the `eth` network, but it's an unexpected case 260 // that should be fixed, not papered over. 261 header := api.remoteBlocks.get(update.HeadBlockHash) 262 if header == nil { 263 log.Warn("Forkchoice requested unknown head", "hash", update.HeadBlockHash) 264 return engine.STATUS_SYNCING, nil 265 } 266 // If the finalized hash is known, we can direct the downloader to move 267 // potentially more data to the freezer from the get go. 268 finalized := api.remoteBlocks.get(update.FinalizedBlockHash) 269 270 // Header advertised via a past newPayload request. Start syncing to it. 271 context := []interface{}{"number", header.Number, "hash", header.Hash()} 272 if update.FinalizedBlockHash != (common.Hash{}) { 273 if finalized == nil { 274 context = append(context, []interface{}{"finalized", "unknown"}...) 275 } else { 276 context = append(context, []interface{}{"finalized", finalized.Number}...) 277 } 278 } 279 log.Info("Forkchoice requested sync to new head", context...) 280 if err := api.eth.Downloader().BeaconSync(api.eth.SyncMode(), header, finalized); err != nil { 281 return engine.STATUS_SYNCING, err 282 } 283 return engine.STATUS_SYNCING, nil 284 } 285 // Block is known locally, just sanity check that the beacon client does not 286 // attempt to push us back to before the merge. 287 if block.Difficulty().BitLen() > 0 || block.NumberU64() == 0 { 288 var ( 289 td = api.eth.BlockChain().GetTd(update.HeadBlockHash, block.NumberU64()) 290 ptd = api.eth.BlockChain().GetTd(block.ParentHash(), block.NumberU64()-1) 291 ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty 292 ) 293 if td == nil || (block.NumberU64() > 0 && ptd == nil) { 294 log.Error("TDs unavailable for TTD check", "number", block.NumberU64(), "hash", update.HeadBlockHash, "td", td, "parent", block.ParentHash(), "ptd", ptd) 295 return engine.STATUS_INVALID, errors.New("TDs unavailable for TDD check") 296 } 297 if td.Cmp(ttd) < 0 { 298 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))) 299 return engine.ForkChoiceResponse{PayloadStatus: engine.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil 300 } 301 if block.NumberU64() > 0 && ptd.Cmp(ttd) >= 0 { 302 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))) 303 return engine.ForkChoiceResponse{PayloadStatus: engine.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil 304 } 305 } 306 valid := func(id *engine.PayloadID) engine.ForkChoiceResponse { 307 return engine.ForkChoiceResponse{ 308 PayloadStatus: engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &update.HeadBlockHash}, 309 PayloadID: id, 310 } 311 } 312 if rawdb.ReadCanonicalHash(api.eth.ChainDb(), block.NumberU64()) != update.HeadBlockHash { 313 // Block is not canonical, set head. 314 if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil { 315 return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err 316 } 317 } else if api.eth.BlockChain().CurrentBlock().Hash() == update.HeadBlockHash { 318 // If the specified head matches with our local head, do nothing and keep 319 // generating the payload. It's a special corner case that a few slots are 320 // missing and we are requested to generate the payload in slot. 321 } else { 322 // If the head block is already in our canonical chain, the beacon client is 323 // probably resyncing. Ignore the update. 324 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().Number) 325 return valid(nil), nil 326 } 327 api.eth.SetSynced() 328 329 // If the beacon client also advertised a finalized block, mark the local 330 // chain final and completely in PoS mode. 331 if update.FinalizedBlockHash != (common.Hash{}) { 332 // If the finalized block is not in our canonical tree, something is wrong 333 finalBlock := api.eth.BlockChain().GetBlockByHash(update.FinalizedBlockHash) 334 if finalBlock == nil { 335 log.Warn("Final block not available in database", "hash", update.FinalizedBlockHash) 336 return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not available in database")) 337 } else if rawdb.ReadCanonicalHash(api.eth.ChainDb(), finalBlock.NumberU64()) != update.FinalizedBlockHash { 338 log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", update.HeadBlockHash) 339 return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not in canonical chain")) 340 } 341 // Set the finalized block 342 api.eth.BlockChain().SetFinalized(finalBlock.Header()) 343 } 344 // Check if the safe block hash is in our canonical tree, if not something is wrong 345 if update.SafeBlockHash != (common.Hash{}) { 346 safeBlock := api.eth.BlockChain().GetBlockByHash(update.SafeBlockHash) 347 if safeBlock == nil { 348 log.Warn("Safe block not available in database") 349 return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("safe block not available in database")) 350 } 351 if rawdb.ReadCanonicalHash(api.eth.ChainDb(), safeBlock.NumberU64()) != update.SafeBlockHash { 352 log.Warn("Safe block not in canonical chain") 353 return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("safe block not in canonical chain")) 354 } 355 // Set the safe block 356 api.eth.BlockChain().SetSafe(safeBlock.Header()) 357 } 358 // If payload generation was requested, create a new block to be potentially 359 // sealed by the beacon client. The payload will be requested later, and we 360 // will replace it arbitrarily many times in between. 361 if payloadAttributes != nil { 362 args := &miner.BuildPayloadArgs{ 363 Parent: update.HeadBlockHash, 364 Timestamp: payloadAttributes.Timestamp, 365 FeeRecipient: payloadAttributes.SuggestedFeeRecipient, 366 Random: payloadAttributes.Random, 367 Withdrawals: payloadAttributes.Withdrawals, 368 BeaconRoot: payloadAttributes.BeaconRoot, 369 Version: payloadVersion, 370 } 371 id := args.Id() 372 // If we already are busy generating this work, then we do not need 373 // to start a second process. 374 if api.localBlocks.has(id) { 375 return valid(&id), nil 376 } 377 // If the beacon chain is ran by a simulator, then transaction insertion, 378 // block insertion and block production will happen without any timing 379 // delay between them. This will cause flaky simulator executions due to 380 // the transaction pool running its internal reset operation on a back- 381 // ground thread. To avoid the racey behavior - in simulator mode - the 382 // pool will be explicitly blocked on its reset before continuing to the 383 // block production below. 384 if simulatorMode { 385 if err := api.eth.TxPool().Sync(); err != nil { 386 log.Error("Failed to sync transaction pool", "err", err) 387 return valid(nil), engine.InvalidPayloadAttributes.With(err) 388 } 389 } 390 payload, err := api.eth.Miner().BuildPayload(args) 391 if err != nil { 392 log.Error("Failed to build payload", "err", err) 393 return valid(nil), engine.InvalidPayloadAttributes.With(err) 394 } 395 api.localBlocks.put(id, payload) 396 return valid(&id), nil 397 } 398 return valid(nil), nil 399 } 400 401 // ExchangeTransitionConfigurationV1 checks the given configuration against 402 // the configuration of the node. 403 func (api *ConsensusAPI) ExchangeTransitionConfigurationV1(config engine.TransitionConfigurationV1) (*engine.TransitionConfigurationV1, error) { 404 log.Trace("Engine API request received", "method", "ExchangeTransitionConfiguration", "ttd", config.TerminalTotalDifficulty) 405 if config.TerminalTotalDifficulty == nil { 406 return nil, errors.New("invalid terminal total difficulty") 407 } 408 // Stash away the last update to warn the user if the beacon client goes offline 409 api.lastTransitionLock.Lock() 410 api.lastTransitionUpdate = time.Now() 411 api.lastTransitionLock.Unlock() 412 413 ttd := api.eth.BlockChain().Config().TerminalTotalDifficulty 414 if ttd == nil || ttd.Cmp(config.TerminalTotalDifficulty.ToInt()) != 0 { 415 log.Warn("Invalid TTD configured", "geth", ttd, "beacon", config.TerminalTotalDifficulty) 416 return nil, fmt.Errorf("invalid ttd: execution %v consensus %v", ttd, config.TerminalTotalDifficulty) 417 } 418 if config.TerminalBlockHash != (common.Hash{}) { 419 if hash := api.eth.BlockChain().GetCanonicalHash(uint64(config.TerminalBlockNumber)); hash == config.TerminalBlockHash { 420 return &engine.TransitionConfigurationV1{ 421 TerminalTotalDifficulty: (*hexutil.Big)(ttd), 422 TerminalBlockHash: config.TerminalBlockHash, 423 TerminalBlockNumber: config.TerminalBlockNumber, 424 }, nil 425 } 426 return nil, errors.New("invalid terminal block hash") 427 } 428 return &engine.TransitionConfigurationV1{TerminalTotalDifficulty: (*hexutil.Big)(ttd)}, nil 429 } 430 431 // GetPayloadV1 returns a cached payload by id. 432 func (api *ConsensusAPI) GetPayloadV1(payloadID engine.PayloadID) (*engine.ExecutableData, error) { 433 if !payloadID.Is(engine.PayloadV1) { 434 return nil, engine.UnsupportedFork 435 } 436 data, err := api.getPayload(payloadID, false) 437 if err != nil { 438 return nil, err 439 } 440 return data.ExecutionPayload, nil 441 } 442 443 // GetPayloadV2 returns a cached payload by id. 444 func (api *ConsensusAPI) GetPayloadV2(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { 445 if !payloadID.Is(engine.PayloadV1, engine.PayloadV2) { 446 return nil, engine.UnsupportedFork 447 } 448 return api.getPayload(payloadID, false) 449 } 450 451 // GetPayloadV3 returns a cached payload by id. 452 func (api *ConsensusAPI) GetPayloadV3(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { 453 if !payloadID.Is(engine.PayloadV3) { 454 return nil, engine.UnsupportedFork 455 } 456 return api.getPayload(payloadID, false) 457 } 458 459 func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID, full bool) (*engine.ExecutionPayloadEnvelope, error) { 460 log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID) 461 data := api.localBlocks.get(payloadID, full) 462 if data == nil { 463 return nil, engine.UnknownPayload 464 } 465 return data, nil 466 } 467 468 // NewPayloadV1 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. 469 func (api *ConsensusAPI) NewPayloadV1(params engine.ExecutableData) (engine.PayloadStatusV1, error) { 470 if params.Withdrawals != nil { 471 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("withdrawals not supported in V1")) 472 } 473 return api.newPayload(params, nil, nil) 474 } 475 476 // NewPayloadV2 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. 477 func (api *ConsensusAPI) NewPayloadV2(params engine.ExecutableData) (engine.PayloadStatusV1, error) { 478 if api.eth.BlockChain().Config().IsCancun(api.eth.BlockChain().Config().LondonBlock, params.Timestamp) { 479 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("can't use newPayloadV2 post-cancun")) 480 } 481 if api.eth.BlockChain().Config().LatestFork(params.Timestamp) == forks.Shanghai { 482 if params.Withdrawals == nil { 483 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil withdrawals post-shanghai")) 484 } 485 } else { 486 if params.Withdrawals != nil { 487 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("non-nil withdrawals pre-shanghai")) 488 } 489 } 490 if params.ExcessBlobGas != nil { 491 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("non-nil excessBlobGas pre-cancun")) 492 } 493 if params.BlobGasUsed != nil { 494 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("non-nil blobGasUsed pre-cancun")) 495 } 496 return api.newPayload(params, nil, nil) 497 } 498 499 // NewPayloadV3 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. 500 func (api *ConsensusAPI) NewPayloadV3(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (engine.PayloadStatusV1, error) { 501 if params.Withdrawals == nil { 502 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil withdrawals post-shanghai")) 503 } 504 if params.ExcessBlobGas == nil { 505 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil excessBlobGas post-cancun")) 506 } 507 if params.BlobGasUsed == nil { 508 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil blobGasUsed post-cancun")) 509 } 510 511 if versionedHashes == nil { 512 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil versionedHashes post-cancun")) 513 } 514 if beaconRoot == nil { 515 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil beaconRoot post-cancun")) 516 } 517 518 if api.eth.BlockChain().Config().LatestFork(params.Timestamp) != forks.Cancun { 519 return engine.PayloadStatusV1{Status: engine.INVALID}, engine.UnsupportedFork.With(errors.New("newPayloadV3 must only be called for cancun payloads")) 520 } 521 return api.newPayload(params, versionedHashes, beaconRoot) 522 } 523 524 func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (engine.PayloadStatusV1, error) { 525 // The locking here is, strictly, not required. Without these locks, this can happen: 526 // 527 // 1. NewPayload( execdata-N ) is invoked from the CL. It goes all the way down to 528 // api.eth.BlockChain().InsertBlockWithoutSetHead, where it is blocked on 529 // e.g database compaction. 530 // 2. The call times out on the CL layer, which issues another NewPayload (execdata-N) call. 531 // Similarly, this also get stuck on the same place. Importantly, since the 532 // first call has not gone through, the early checks for "do we already have this block" 533 // will all return false. 534 // 3. When the db compaction ends, then N calls inserting the same payload are processed 535 // sequentially. 536 // Hence, we use a lock here, to be sure that the previous call has finished before we 537 // check whether we already have the block locally. 538 api.newPayloadLock.Lock() 539 defer api.newPayloadLock.Unlock() 540 541 log.Trace("Engine API request received", "method", "NewPayload", "number", params.Number, "hash", params.BlockHash) 542 block, err := engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot) 543 if err != nil { 544 bgu := "nil" 545 if params.BlobGasUsed != nil { 546 bgu = strconv.Itoa(int(*params.BlobGasUsed)) 547 } 548 ebg := "nil" 549 if params.BlobGasUsed != nil { 550 ebg = strconv.Itoa(int(*params.ExcessBlobGas)) 551 } 552 log.Warn("Invalid NewPayload params", 553 "params.Number", params.Number, 554 "params.ParentHash", params.ParentHash, 555 "params.BlockHash", params.BlockHash, 556 "params.StateRoot", params.StateRoot, 557 "params.FeeRecipient", params.FeeRecipient, 558 "params.LogsBloom", common.PrettyBytes(params.LogsBloom), 559 "params.Random", params.Random, 560 "params.GasLimit", params.GasLimit, 561 "params.GasUsed", params.GasUsed, 562 "params.Timestamp", params.Timestamp, 563 "params.ExtraData", common.PrettyBytes(params.ExtraData), 564 "params.BaseFeePerGas", params.BaseFeePerGas, 565 "params.BlobGasUsed", bgu, 566 "params.ExcessBlobGas", ebg, 567 "len(params.Transactions)", len(params.Transactions), 568 "len(params.Withdrawals)", len(params.Withdrawals), 569 "beaconRoot", beaconRoot, 570 "error", err) 571 return api.invalid(err, nil), nil 572 } 573 // Stash away the last update to warn the user if the beacon client goes offline 574 api.lastNewPayloadLock.Lock() 575 api.lastNewPayloadUpdate = time.Now() 576 api.lastNewPayloadLock.Unlock() 577 578 // If we already have the block locally, ignore the entire execution and just 579 // return a fake success. 580 if block := api.eth.BlockChain().GetBlockByHash(params.BlockHash); block != nil { 581 log.Warn("Ignoring already known beacon payload", "number", params.Number, "hash", params.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0))) 582 hash := block.Hash() 583 return engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &hash}, nil 584 } 585 // If this block was rejected previously, keep rejecting it 586 if res := api.checkInvalidAncestor(block.Hash(), block.Hash()); res != nil { 587 return *res, nil 588 } 589 // If the parent is missing, we - in theory - could trigger a sync, but that 590 // would also entail a reorg. That is problematic if multiple sibling blocks 591 // are being fed to us, and even more so, if some semi-distant uncle shortens 592 // our live chain. As such, payload execution will not permit reorgs and thus 593 // will not trigger a sync cycle. That is fine though, if we get a fork choice 594 // update after legit payload executions. 595 parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1) 596 if parent == nil { 597 return api.delayPayloadImport(block), nil 598 } 599 // We have an existing parent, do some sanity checks to avoid the beacon client 600 // triggering too early 601 var ( 602 ptd = api.eth.BlockChain().GetTd(parent.Hash(), parent.NumberU64()) 603 ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty 604 gptd = api.eth.BlockChain().GetTd(parent.ParentHash(), parent.NumberU64()-1) 605 ) 606 if ptd.Cmp(ttd) < 0 { 607 log.Warn("Ignoring pre-merge payload", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd) 608 return engine.INVALID_TERMINAL_BLOCK, nil 609 } 610 if parent.Difficulty().BitLen() > 0 && gptd != nil && gptd.Cmp(ttd) >= 0 { 611 log.Error("Ignoring pre-merge parent block", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd) 612 return engine.INVALID_TERMINAL_BLOCK, nil 613 } 614 if block.Time() <= parent.Time() { 615 log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time()) 616 return api.invalid(errors.New("invalid timestamp"), parent.Header()), nil 617 } 618 // Another corner case: if the node is in snap sync mode, but the CL client 619 // tries to make it import a block. That should be denied as pushing something 620 // into the database directly will conflict with the assumptions of snap sync 621 // that it has an empty db that it can fill itself. 622 if api.eth.SyncMode() != downloader.FullSync { 623 return api.delayPayloadImport(block), nil 624 } 625 if !api.eth.BlockChain().HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { 626 api.remoteBlocks.put(block.Hash(), block.Header()) 627 log.Warn("State not available, ignoring new payload") 628 return engine.PayloadStatusV1{Status: engine.ACCEPTED}, nil 629 } 630 log.Trace("Inserting block without sethead", "hash", block.Hash(), "number", block.Number()) 631 if err := api.eth.BlockChain().InsertBlockWithoutSetHead(block); err != nil { 632 log.Warn("NewPayloadV1: inserting block failed", "error", err) 633 634 api.invalidLock.Lock() 635 api.invalidBlocksHits[block.Hash()] = 1 636 api.invalidTipsets[block.Hash()] = block.Header() 637 api.invalidLock.Unlock() 638 639 return api.invalid(err, parent.Header()), nil 640 } 641 hash := block.Hash() 642 return engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &hash}, nil 643 } 644 645 // delayPayloadImport stashes the given block away for import at a later time, 646 // either via a forkchoice update or a sync extension. This method is meant to 647 // be called by the newpayload command when the block seems to be ok, but some 648 // prerequisite prevents it from being processed (e.g. no parent, or snap sync). 649 func (api *ConsensusAPI) delayPayloadImport(block *types.Block) engine.PayloadStatusV1 { 650 // Sanity check that this block's parent is not on a previously invalidated 651 // chain. If it is, mark the block as invalid too. 652 if res := api.checkInvalidAncestor(block.ParentHash(), block.Hash()); res != nil { 653 return *res 654 } 655 // Stash the block away for a potential forced forkchoice update to it 656 // at a later time. 657 api.remoteBlocks.put(block.Hash(), block.Header()) 658 659 // Although we don't want to trigger a sync, if there is one already in 660 // progress, try to extend it with the current payload request to relieve 661 // some strain from the forkchoice update. 662 err := api.eth.Downloader().BeaconExtend(api.eth.SyncMode(), block.Header()) 663 if err == nil { 664 log.Debug("Payload accepted for sync extension", "number", block.NumberU64(), "hash", block.Hash()) 665 return engine.PayloadStatusV1{Status: engine.SYNCING} 666 } 667 // Either no beacon sync was started yet, or it rejected the delivered 668 // payload as non-integratable on top of the existing sync. We'll just 669 // have to rely on the beacon client to forcefully update the head with 670 // a forkchoice update request. 671 if api.eth.SyncMode() == downloader.FullSync { 672 // In full sync mode, failure to import a well-formed block can only mean 673 // that the parent state is missing and the syncer rejected extending the 674 // current cycle with the new payload. 675 log.Warn("Ignoring payload with missing parent", "number", block.NumberU64(), "hash", block.Hash(), "parent", block.ParentHash(), "reason", err) 676 } else { 677 // In non-full sync mode (i.e. snap sync) all payloads are rejected until 678 // snap sync terminates as snap sync relies on direct database injections 679 // and cannot afford concurrent out-if-band modifications via imports. 680 log.Warn("Ignoring payload while snap syncing", "number", block.NumberU64(), "hash", block.Hash(), "reason", err) 681 } 682 return engine.PayloadStatusV1{Status: engine.SYNCING} 683 } 684 685 // setInvalidAncestor is a callback for the downloader to notify us if a bad block 686 // is encountered during the async sync. 687 func (api *ConsensusAPI) setInvalidAncestor(invalid *types.Header, origin *types.Header) { 688 api.invalidLock.Lock() 689 defer api.invalidLock.Unlock() 690 691 api.invalidTipsets[origin.Hash()] = invalid 692 api.invalidBlocksHits[invalid.Hash()]++ 693 } 694 695 // checkInvalidAncestor checks whether the specified chain end links to a known 696 // bad ancestor. If yes, it constructs the payload failure response to return. 697 func (api *ConsensusAPI) checkInvalidAncestor(check common.Hash, head common.Hash) *engine.PayloadStatusV1 { 698 api.invalidLock.Lock() 699 defer api.invalidLock.Unlock() 700 701 // If the hash to check is unknown, return valid 702 invalid, ok := api.invalidTipsets[check] 703 if !ok { 704 return nil 705 } 706 // If the bad hash was hit too many times, evict it and try to reprocess in 707 // the hopes that we have a data race that we can exit out of. 708 badHash := invalid.Hash() 709 710 api.invalidBlocksHits[badHash]++ 711 if api.invalidBlocksHits[badHash] >= invalidBlockHitEviction { 712 log.Warn("Too many bad block import attempt, trying", "number", invalid.Number, "hash", badHash) 713 delete(api.invalidBlocksHits, badHash) 714 715 for descendant, badHeader := range api.invalidTipsets { 716 if badHeader.Hash() == badHash { 717 delete(api.invalidTipsets, descendant) 718 } 719 } 720 return nil 721 } 722 // Not too many failures yet, mark the head of the invalid chain as invalid 723 if check != head { 724 log.Warn("Marked new chain head as invalid", "hash", head, "badnumber", invalid.Number, "badhash", badHash) 725 for len(api.invalidTipsets) >= invalidTipsetsCap { 726 for key := range api.invalidTipsets { 727 delete(api.invalidTipsets, key) 728 break 729 } 730 } 731 api.invalidTipsets[head] = invalid 732 } 733 // If the last valid hash is the terminal pow block, return 0x0 for latest valid hash 734 lastValid := &invalid.ParentHash 735 if header := api.eth.BlockChain().GetHeader(invalid.ParentHash, invalid.Number.Uint64()-1); header != nil && header.Difficulty.Sign() != 0 { 736 lastValid = &common.Hash{} 737 } 738 failure := "links to previously rejected block" 739 return &engine.PayloadStatusV1{ 740 Status: engine.INVALID, 741 LatestValidHash: lastValid, 742 ValidationError: &failure, 743 } 744 } 745 746 // invalid returns a response "INVALID" with the latest valid hash supplied by latest. 747 func (api *ConsensusAPI) invalid(err error, latestValid *types.Header) engine.PayloadStatusV1 { 748 var currentHash *common.Hash 749 if latestValid != nil { 750 if latestValid.Difficulty.BitLen() != 0 { 751 // Set latest valid hash to 0x0 if parent is PoW block 752 currentHash = &common.Hash{} 753 } else { 754 // Otherwise set latest valid hash to parent hash 755 h := latestValid.Hash() 756 currentHash = &h 757 } 758 } 759 errorMsg := err.Error() 760 return engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: currentHash, ValidationError: &errorMsg} 761 } 762 763 // heartbeat loops indefinitely, and checks if there have been beacon client updates 764 // received in the last while. If not - or if they but strange ones - it warns the 765 // user that something might be off with their consensus node. 766 // 767 // TODO(karalabe): Spin this goroutine down somehow 768 func (api *ConsensusAPI) heartbeat() { 769 // Sleep a bit on startup since there's obviously no beacon client yet 770 // attached, so no need to print scary warnings to the user. 771 time.Sleep(beaconUpdateStartupTimeout) 772 773 // If the network is not yet merged/merging, don't bother continuing. 774 if api.eth.BlockChain().Config().TerminalTotalDifficulty == nil { 775 return 776 } 777 778 var offlineLogged time.Time 779 780 for { 781 // Sleep a bit and retrieve the last known consensus updates 782 time.Sleep(5 * time.Second) 783 784 api.lastTransitionLock.Lock() 785 lastTransitionUpdate := api.lastTransitionUpdate 786 api.lastTransitionLock.Unlock() 787 788 api.lastForkchoiceLock.Lock() 789 lastForkchoiceUpdate := api.lastForkchoiceUpdate 790 api.lastForkchoiceLock.Unlock() 791 792 api.lastNewPayloadLock.Lock() 793 lastNewPayloadUpdate := api.lastNewPayloadUpdate 794 api.lastNewPayloadLock.Unlock() 795 796 // If there have been no updates for the past while, warn the user 797 // that the beacon client is probably offline 798 if time.Since(lastForkchoiceUpdate) <= beaconUpdateConsensusTimeout || time.Since(lastNewPayloadUpdate) <= beaconUpdateConsensusTimeout { 799 offlineLogged = time.Time{} 800 continue 801 } 802 if time.Since(offlineLogged) > beaconUpdateWarnFrequency { 803 if lastForkchoiceUpdate.IsZero() && lastNewPayloadUpdate.IsZero() { 804 if lastTransitionUpdate.IsZero() { 805 log.Warn("Post-merge network, but no beacon client seen. Please launch one to follow the chain!") 806 } else { 807 log.Warn("Beacon client online, but never received consensus updates. Please ensure your beacon client is operational to follow the chain!") 808 } 809 } else { 810 log.Warn("Beacon client online, but no consensus updates received in a while. Please fix your beacon client to follow the chain!") 811 } 812 offlineLogged = time.Now() 813 } 814 continue 815 } 816 } 817 818 // ExchangeCapabilities returns the current methods provided by this node. 819 func (api *ConsensusAPI) ExchangeCapabilities([]string) []string { 820 return caps 821 } 822 823 // GetClientVersionV1 exchanges client version data of this node. 824 func (api *ConsensusAPI) GetClientVersionV1(info engine.ClientVersionV1) []engine.ClientVersionV1 { 825 log.Trace("Engine API request received", "method", "GetClientVersionV1", "info", info.String()) 826 commit := make([]byte, 4) 827 if vcs, ok := version.VCS(); ok { 828 commit = common.FromHex(vcs.Commit)[0:4] 829 } 830 return []engine.ClientVersionV1{ 831 { 832 Code: engine.ClientCode, 833 Name: engine.ClientName, 834 Version: params.VersionWithMeta, 835 Commit: hexutil.Encode(commit), 836 }, 837 } 838 } 839 840 // GetPayloadBodiesByHashV1 implements engine_getPayloadBodiesByHashV1 which allows for retrieval of a list 841 // of block bodies by the engine api. 842 func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engine.ExecutionPayloadBodyV1 { 843 bodies := make([]*engine.ExecutionPayloadBodyV1, len(hashes)) 844 for i, hash := range hashes { 845 block := api.eth.BlockChain().GetBlockByHash(hash) 846 bodies[i] = getBody(block) 847 } 848 return bodies 849 } 850 851 // GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range 852 // of block bodies by the engine api. 853 func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBodyV1, error) { 854 if start == 0 || count == 0 { 855 return nil, engine.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count)) 856 } 857 if count > 1024 { 858 return nil, engine.TooLargeRequest.With(fmt.Errorf("requested count too large: %v", count)) 859 } 860 // limit count up until current 861 current := api.eth.BlockChain().CurrentBlock().Number.Uint64() 862 last := uint64(start) + uint64(count) - 1 863 if last > current { 864 last = current 865 } 866 bodies := make([]*engine.ExecutionPayloadBodyV1, 0, uint64(count)) 867 for i := uint64(start); i <= last; i++ { 868 block := api.eth.BlockChain().GetBlockByNumber(i) 869 bodies = append(bodies, getBody(block)) 870 } 871 return bodies, nil 872 } 873 874 func getBody(block *types.Block) *engine.ExecutionPayloadBodyV1 { 875 if block == nil { 876 return nil 877 } 878 879 var ( 880 body = block.Body() 881 txs = make([]hexutil.Bytes, len(body.Transactions)) 882 withdrawals = body.Withdrawals 883 ) 884 885 for j, tx := range body.Transactions { 886 txs[j], _ = tx.MarshalBinary() 887 } 888 889 // Post-shanghai withdrawals MUST be set to empty slice instead of nil 890 if withdrawals == nil && block.Header().WithdrawalsHash != nil { 891 withdrawals = make([]*types.Withdrawal, 0) 892 } 893 894 return &engine.ExecutionPayloadBodyV1{ 895 TransactionData: txs, 896 Withdrawals: withdrawals, 897 } 898 }