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