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