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