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