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