github.com/ethw3/go-ethereuma@v0.0.0-20221013053120-c14602a4c23c/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/ethw3/go-ethereuma/common" 30 "github.com/ethw3/go-ethereuma/common/hexutil" 31 "github.com/ethw3/go-ethereuma/core/beacon" 32 "github.com/ethw3/go-ethereuma/core/rawdb" 33 "github.com/ethw3/go-ethereuma/core/types" 34 "github.com/ethw3/go-ethereuma/eth" 35 "github.com/ethw3/go-ethereuma/eth/downloader" 36 "github.com/ethw3/go-ethereuma/log" 37 "github.com/ethw3/go-ethereuma/node" 38 "github.com/ethw3/go-ethereuma/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 // 147 // we return success, which can be used to check if the engine API is enabled 148 // 149 // If the total difficulty was not reached: 150 // 151 // we return INVALID 152 // 153 // If the finalizedBlockHash is set: 154 // 155 // we check if we have the finalizedBlockHash in our db, if not we start a sync 156 // 157 // We try to set our blockchain to the headBlock 158 // If there are payloadAttributes: 159 // 160 // we try to assemble a block with the payloadAttributes and return its payloadID 161 func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, payloadAttributes *beacon.PayloadAttributesV1) (beacon.ForkChoiceResponse, error) { 162 api.forkchoiceLock.Lock() 163 defer api.forkchoiceLock.Unlock() 164 165 log.Trace("Engine API request received", "method", "ForkchoiceUpdated", "head", update.HeadBlockHash, "finalized", update.FinalizedBlockHash, "safe", update.SafeBlockHash) 166 if update.HeadBlockHash == (common.Hash{}) { 167 log.Warn("Forkchoice requested update to zero hash") 168 return beacon.STATUS_INVALID, nil // TODO(karalabe): Why does someone send us this? 169 } 170 // Stash away the last update to warn the user if the beacon client goes offline 171 api.lastForkchoiceLock.Lock() 172 api.lastForkchoiceUpdate = time.Now() 173 api.lastForkchoiceLock.Unlock() 174 175 // Check whether we have the block yet in our database or not. If not, we'll 176 // need to either trigger a sync, or to reject this forkchoice update for a 177 // reason. 178 block := api.eth.BlockChain().GetBlockByHash(update.HeadBlockHash) 179 if block == nil { 180 // If this block was previously invalidated, keep rejecting it here too 181 if res := api.checkInvalidAncestor(update.HeadBlockHash, update.HeadBlockHash); res != nil { 182 return beacon.ForkChoiceResponse{PayloadStatus: *res, PayloadID: nil}, nil 183 } 184 // If the head hash is unknown (was not given to us in a newPayload request), 185 // we cannot resolve the header, so not much to do. This could be extended in 186 // the future to resolve from the `eth` network, but it's an unexpected case 187 // that should be fixed, not papered over. 188 header := api.remoteBlocks.get(update.HeadBlockHash) 189 if header == nil { 190 log.Warn("Forkchoice requested unknown head", "hash", update.HeadBlockHash) 191 return beacon.STATUS_SYNCING, nil 192 } 193 // Header advertised via a past newPayload request. Start syncing to it. 194 // Before we do however, make sure any legacy sync in switched off so we 195 // don't accidentally have 2 cycles running. 196 if merger := api.eth.Merger(); !merger.TDDReached() { 197 merger.ReachTTD() 198 api.eth.Downloader().Cancel() 199 } 200 log.Info("Forkchoice requested sync to new head", "number", header.Number, "hash", header.Hash()) 201 if err := api.eth.Downloader().BeaconSync(api.eth.SyncMode(), header); err != nil { 202 return beacon.STATUS_SYNCING, err 203 } 204 return beacon.STATUS_SYNCING, nil 205 } 206 // Block is known locally, just sanity check that the beacon client does not 207 // attempt to push us back to before the merge. 208 if block.Difficulty().BitLen() > 0 || block.NumberU64() == 0 { 209 var ( 210 td = api.eth.BlockChain().GetTd(update.HeadBlockHash, block.NumberU64()) 211 ptd = api.eth.BlockChain().GetTd(block.ParentHash(), block.NumberU64()-1) 212 ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty 213 ) 214 if td == nil || (block.NumberU64() > 0 && ptd == nil) { 215 log.Error("TDs unavailable for TTD check", "number", block.NumberU64(), "hash", update.HeadBlockHash, "td", td, "parent", block.ParentHash(), "ptd", ptd) 216 return beacon.STATUS_INVALID, errors.New("TDs unavailable for TDD check") 217 } 218 if td.Cmp(ttd) < 0 { 219 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))) 220 return beacon.ForkChoiceResponse{PayloadStatus: beacon.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil 221 } 222 if block.NumberU64() > 0 && ptd.Cmp(ttd) >= 0 { 223 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))) 224 return beacon.ForkChoiceResponse{PayloadStatus: beacon.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil 225 } 226 } 227 valid := func(id *beacon.PayloadID) beacon.ForkChoiceResponse { 228 return beacon.ForkChoiceResponse{ 229 PayloadStatus: beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &update.HeadBlockHash}, 230 PayloadID: id, 231 } 232 } 233 if rawdb.ReadCanonicalHash(api.eth.ChainDb(), block.NumberU64()) != update.HeadBlockHash { 234 // Block is not canonical, set head. 235 if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil { 236 return beacon.ForkChoiceResponse{PayloadStatus: beacon.PayloadStatusV1{Status: beacon.INVALID, LatestValidHash: &latestValid}}, err 237 } 238 } else if api.eth.BlockChain().CurrentBlock().Hash() == update.HeadBlockHash { 239 // If the specified head matches with our local head, do nothing and keep 240 // generating the payload. It's a special corner case that a few slots are 241 // missing and we are requested to generate the payload in slot. 242 } else { 243 // If the head block is already in our canonical chain, the beacon client is 244 // probably resyncing. Ignore the update. 245 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()) 246 return valid(nil), nil 247 } 248 api.eth.SetSynced() 249 250 // If the beacon client also advertised a finalized block, mark the local 251 // chain final and completely in PoS mode. 252 if update.FinalizedBlockHash != (common.Hash{}) { 253 if merger := api.eth.Merger(); !merger.PoSFinalized() { 254 merger.FinalizePoS() 255 } 256 // If the finalized block is not in our canonical tree, somethings wrong 257 finalBlock := api.eth.BlockChain().GetBlockByHash(update.FinalizedBlockHash) 258 if finalBlock == nil { 259 log.Warn("Final block not available in database", "hash", update.FinalizedBlockHash) 260 return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("final block not available in database")) 261 } else if rawdb.ReadCanonicalHash(api.eth.ChainDb(), finalBlock.NumberU64()) != update.FinalizedBlockHash { 262 log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", update.HeadBlockHash) 263 return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("final block not in canonical chain")) 264 } 265 // Set the finalized block 266 api.eth.BlockChain().SetFinalized(finalBlock) 267 } 268 // Check if the safe block hash is in our canonical tree, if not somethings wrong 269 if update.SafeBlockHash != (common.Hash{}) { 270 safeBlock := api.eth.BlockChain().GetBlockByHash(update.SafeBlockHash) 271 if safeBlock == nil { 272 log.Warn("Safe block not available in database") 273 return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("safe block not available in database")) 274 } 275 if rawdb.ReadCanonicalHash(api.eth.ChainDb(), safeBlock.NumberU64()) != update.SafeBlockHash { 276 log.Warn("Safe block not in canonical chain") 277 return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("safe block not in canonical chain")) 278 } 279 // Set the safe block 280 api.eth.BlockChain().SetSafe(safeBlock) 281 } 282 // If payload generation was requested, create a new block to be potentially 283 // sealed by the beacon client. The payload will be requested later, and we 284 // might replace it arbitrarily many times in between. 285 if payloadAttributes != nil { 286 // Create an empty block first which can be used as a fallback 287 empty, err := api.eth.Miner().GetSealingBlockSync(update.HeadBlockHash, payloadAttributes.Timestamp, payloadAttributes.SuggestedFeeRecipient, payloadAttributes.Random, true) 288 if err != nil { 289 log.Error("Failed to create empty sealing payload", "err", err) 290 return valid(nil), beacon.InvalidPayloadAttributes.With(err) 291 } 292 // Send a request to generate a full block in the background. 293 // The result can be obtained via the returned channel. 294 resCh, err := api.eth.Miner().GetSealingBlockAsync(update.HeadBlockHash, payloadAttributes.Timestamp, payloadAttributes.SuggestedFeeRecipient, payloadAttributes.Random, false) 295 if err != nil { 296 log.Error("Failed to create async sealing payload", "err", err) 297 return valid(nil), beacon.InvalidPayloadAttributes.With(err) 298 } 299 id := computePayloadId(update.HeadBlockHash, payloadAttributes) 300 api.localBlocks.put(id, &payload{empty: empty, result: resCh}) 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 log.Trace("Engine API request received", "method", "ExecutePayload", "number", params.Number, "hash", params.BlockHash) 349 block, err := beacon.ExecutableDataToBlock(params) 350 if err != nil { 351 log.Debug("Invalid NewPayload params", "params", params, "error", err) 352 return beacon.PayloadStatusV1{Status: beacon.INVALIDBLOCKHASH}, nil 353 } 354 // Stash away the last update to warn the user if the beacon client goes offline 355 api.lastNewPayloadLock.Lock() 356 api.lastNewPayloadUpdate = time.Now() 357 api.lastNewPayloadLock.Unlock() 358 359 // If we already have the block locally, ignore the entire execution and just 360 // return a fake success. 361 if block := api.eth.BlockChain().GetBlockByHash(params.BlockHash); block != nil { 362 log.Warn("Ignoring already known beacon payload", "number", params.Number, "hash", params.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0))) 363 hash := block.Hash() 364 return beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &hash}, nil 365 } 366 // If this block was rejected previously, keep rejecting it 367 if res := api.checkInvalidAncestor(block.Hash(), block.Hash()); res != nil { 368 return *res, nil 369 } 370 // If the parent is missing, we - in theory - could trigger a sync, but that 371 // would also entail a reorg. That is problematic if multiple sibling blocks 372 // are being fed to us, and even more so, if some semi-distant uncle shortens 373 // our live chain. As such, payload execution will not permit reorgs and thus 374 // will not trigger a sync cycle. That is fine though, if we get a fork choice 375 // update after legit payload executions. 376 parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1) 377 if parent == nil { 378 return api.delayPayloadImport(block) 379 } 380 // We have an existing parent, do some sanity checks to avoid the beacon client 381 // triggering too early 382 var ( 383 ptd = api.eth.BlockChain().GetTd(parent.Hash(), parent.NumberU64()) 384 ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty 385 gptd = api.eth.BlockChain().GetTd(parent.ParentHash(), parent.NumberU64()-1) 386 ) 387 if ptd.Cmp(ttd) < 0 { 388 log.Warn("Ignoring pre-merge payload", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd) 389 return beacon.INVALID_TERMINAL_BLOCK, nil 390 } 391 if parent.Difficulty().BitLen() > 0 && gptd != nil && gptd.Cmp(ttd) >= 0 { 392 log.Error("Ignoring pre-merge parent block", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd) 393 return beacon.INVALID_TERMINAL_BLOCK, nil 394 } 395 if block.Time() <= parent.Time() { 396 log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time()) 397 return api.invalid(errors.New("invalid timestamp"), parent.Header()), nil 398 } 399 // Another cornercase: if the node is in snap sync mode, but the CL client 400 // tries to make it import a block. That should be denied as pushing something 401 // into the database directly will conflict with the assumptions of snap sync 402 // that it has an empty db that it can fill itself. 403 if api.eth.SyncMode() != downloader.FullSync { 404 return api.delayPayloadImport(block) 405 } 406 if !api.eth.BlockChain().HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { 407 api.remoteBlocks.put(block.Hash(), block.Header()) 408 log.Warn("State not available, ignoring new payload") 409 return beacon.PayloadStatusV1{Status: beacon.ACCEPTED}, nil 410 } 411 log.Trace("Inserting block without sethead", "hash", block.Hash(), "number", block.Number) 412 if err := api.eth.BlockChain().InsertBlockWithoutSetHead(block); err != nil { 413 log.Warn("NewPayloadV1: inserting block failed", "error", err) 414 415 api.invalidLock.Lock() 416 api.invalidBlocksHits[block.Hash()] = 1 417 api.invalidTipsets[block.Hash()] = block.Header() 418 api.invalidLock.Unlock() 419 420 return api.invalid(err, parent.Header()), nil 421 } 422 // We've accepted a valid payload from the beacon client. Mark the local 423 // chain transitions to notify other subsystems (e.g. downloader) of the 424 // behavioral change. 425 if merger := api.eth.Merger(); !merger.TDDReached() { 426 merger.ReachTTD() 427 api.eth.Downloader().Cancel() 428 } 429 hash := block.Hash() 430 return beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &hash}, nil 431 } 432 433 // computePayloadId computes a pseudo-random payloadid, based on the parameters. 434 func computePayloadId(headBlockHash common.Hash, params *beacon.PayloadAttributesV1) beacon.PayloadID { 435 // Hash 436 hasher := sha256.New() 437 hasher.Write(headBlockHash[:]) 438 binary.Write(hasher, binary.BigEndian, params.Timestamp) 439 hasher.Write(params.Random[:]) 440 hasher.Write(params.SuggestedFeeRecipient[:]) 441 var out beacon.PayloadID 442 copy(out[:], hasher.Sum(nil)[:8]) 443 return out 444 } 445 446 // delayPayloadImport stashes the given block away for import at a later time, 447 // either via a forkchoice update or a sync extension. This method is meant to 448 // be called by the newpayload command when the block seems to be ok, but some 449 // prerequisite prevents it from being processed (e.g. no parent, or snap sync). 450 func (api *ConsensusAPI) delayPayloadImport(block *types.Block) (beacon.PayloadStatusV1, error) { 451 // Sanity check that this block's parent is not on a previously invalidated 452 // chain. If it is, mark the block as invalid too. 453 if res := api.checkInvalidAncestor(block.ParentHash(), block.Hash()); res != nil { 454 return *res, nil 455 } 456 // Stash the block away for a potential forced forkchoice update to it 457 // at a later time. 458 api.remoteBlocks.put(block.Hash(), block.Header()) 459 460 // Although we don't want to trigger a sync, if there is one already in 461 // progress, try to extend if with the current payload request to relieve 462 // some strain from the forkchoice update. 463 if err := api.eth.Downloader().BeaconExtend(api.eth.SyncMode(), block.Header()); err == nil { 464 log.Debug("Payload accepted for sync extension", "number", block.NumberU64(), "hash", block.Hash()) 465 return beacon.PayloadStatusV1{Status: beacon.SYNCING}, nil 466 } 467 // Either no beacon sync was started yet, or it rejected the delivered 468 // payload as non-integratable on top of the existing sync. We'll just 469 // have to rely on the beacon client to forcefully update the head with 470 // a forkchoice update request. 471 if api.eth.SyncMode() == downloader.FullSync { 472 // In full sync mode, failure to import a well-formed block can only mean 473 // that the parent state is missing and the syncer rejected extending the 474 // current cycle with the new payload. 475 log.Warn("Ignoring payload with missing parent", "number", block.NumberU64(), "hash", block.Hash(), "parent", block.ParentHash()) 476 } else { 477 // In non-full sync mode (i.e. snap sync) all payloads are rejected until 478 // snap sync terminates as snap sync relies on direct database injections 479 // and cannot afford concurrent out-if-band modifications via imports. 480 log.Warn("Ignoring payload while snap syncing", "number", block.NumberU64(), "hash", block.Hash()) 481 } 482 return beacon.PayloadStatusV1{Status: beacon.SYNCING}, nil 483 } 484 485 // setInvalidAncestor is a callback for the downloader to notify us if a bad block 486 // is encountered during the async sync. 487 func (api *ConsensusAPI) setInvalidAncestor(invalid *types.Header, origin *types.Header) { 488 api.invalidLock.Lock() 489 defer api.invalidLock.Unlock() 490 491 api.invalidTipsets[origin.Hash()] = invalid 492 api.invalidBlocksHits[invalid.Hash()]++ 493 } 494 495 // checkInvalidAncestor checks whether the specified chain end links to a known 496 // bad ancestor. If yes, it constructs the payload failure response to return. 497 func (api *ConsensusAPI) checkInvalidAncestor(check common.Hash, head common.Hash) *beacon.PayloadStatusV1 { 498 api.invalidLock.Lock() 499 defer api.invalidLock.Unlock() 500 501 // If the hash to check is unknown, return valid 502 invalid, ok := api.invalidTipsets[check] 503 if !ok { 504 return nil 505 } 506 // If the bad hash was hit too many times, evict it and try to reprocess in 507 // the hopes that we have a data race that we can exit out of. 508 badHash := invalid.Hash() 509 510 api.invalidBlocksHits[badHash]++ 511 if api.invalidBlocksHits[badHash] >= invalidBlockHitEviction { 512 log.Warn("Too many bad block import attempt, trying", "number", invalid.Number, "hash", badHash) 513 delete(api.invalidBlocksHits, badHash) 514 515 for descendant, badHeader := range api.invalidTipsets { 516 if badHeader.Hash() == badHash { 517 delete(api.invalidTipsets, descendant) 518 } 519 } 520 return nil 521 } 522 // Not too many failures yet, mark the head of the invalid chain as invalid 523 if check != head { 524 log.Warn("Marked new chain head as invalid", "hash", head, "badnumber", invalid.Number, "badhash", badHash) 525 for len(api.invalidTipsets) >= invalidTipsetsCap { 526 for key := range api.invalidTipsets { 527 delete(api.invalidTipsets, key) 528 break 529 } 530 } 531 api.invalidTipsets[head] = invalid 532 } 533 // If the last valid hash is the terminal pow block, return 0x0 for latest valid hash 534 lastValid := &invalid.ParentHash 535 if header := api.eth.BlockChain().GetHeader(invalid.ParentHash, invalid.Number.Uint64()-1); header != nil && header.Difficulty.Sign() != 0 { 536 lastValid = &common.Hash{} 537 } 538 failure := "links to previously rejected block" 539 return &beacon.PayloadStatusV1{ 540 Status: beacon.INVALID, 541 LatestValidHash: lastValid, 542 ValidationError: &failure, 543 } 544 } 545 546 // invalid returns a response "INVALID" with the latest valid hash supplied by latest or to the current head 547 // if no latestValid block was provided. 548 func (api *ConsensusAPI) invalid(err error, latestValid *types.Header) beacon.PayloadStatusV1 { 549 currentHash := api.eth.BlockChain().CurrentBlock().Hash() 550 if latestValid != nil { 551 // Set latest valid hash to 0x0 if parent is PoW block 552 currentHash = common.Hash{} 553 if latestValid.Difficulty.BitLen() == 0 { 554 // Otherwise set latest valid hash to parent hash 555 currentHash = latestValid.Hash() 556 } 557 } 558 errorMsg := err.Error() 559 return beacon.PayloadStatusV1{Status: beacon.INVALID, LatestValidHash: ¤tHash, ValidationError: &errorMsg} 560 } 561 562 // heartbeat loops indefinitely, and checks if there have been beacon client updates 563 // received in the last while. If not - or if they but strange ones - it warns the 564 // user that something might be off with their consensus node. 565 // 566 // TODO(karalabe): Spin this goroutine down somehow 567 func (api *ConsensusAPI) heartbeat() { 568 // Sleep a bit on startup since there's obviously no beacon client yet 569 // attached, so no need to print scary warnings to the user. 570 time.Sleep(beaconUpdateStartupTimeout) 571 572 var ( 573 offlineLogged time.Time 574 ) 575 for { 576 // Sleep a bit and retrieve the last known consensus updates 577 time.Sleep(5 * time.Second) 578 579 if api.eth.BlockChain().Config().EthPoWForkSupport { 580 continue 581 } 582 // If the network is not yet merged/merging, don't bother scaring the user 583 ttd := api.eth.BlockChain().Config().TerminalTotalDifficulty 584 if ttd == nil { 585 continue 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 if time.Since(lastTransitionUpdate) > beaconUpdateExchangeTimeout { 604 if time.Since(offlineLogged) > beaconUpdateWarnFrequency { 605 if lastTransitionUpdate.IsZero() { 606 log.Warn("Post-merge network, but no beacon client seen. Please launch one to follow the chain!") 607 } else { 608 log.Warn("Previously seen beacon client is offline. Please ensure it is operational to follow the chain!") 609 } 610 offlineLogged = time.Now() 611 } 612 continue 613 } 614 if time.Since(offlineLogged) > beaconUpdateWarnFrequency { 615 if lastForkchoiceUpdate.IsZero() && lastNewPayloadUpdate.IsZero() { 616 log.Warn("Beacon client online, but never received consensus updates. Please ensure your beacon client is operational to follow the chain!") 617 } else { 618 log.Warn("Beacon client online, but no consensus updates received in a while. Please fix your beacon client to follow the chain!") 619 } 620 offlineLogged = time.Now() 621 } 622 continue 623 } else { 624 offlineLogged = time.Time{} 625 } 626 } else { 627 if time.Since(lastTransitionUpdate) > beaconUpdateExchangeTimeout { 628 if time.Since(offlineLogged) > beaconUpdateWarnFrequency { 629 // Retrieve the last few blocks and make a rough estimate as 630 // to when the merge transition should happen 631 var ( 632 chain = api.eth.BlockChain() 633 head = chain.CurrentBlock() 634 htd = chain.GetTd(head.Hash(), head.NumberU64()) 635 eta time.Duration 636 ) 637 if head.NumberU64() > 0 && htd.Cmp(ttd) < 0 { 638 // Accumulate the last 64 difficulties to estimate the growth 639 var diff float64 640 641 block := head 642 for i := 0; i < 64; i++ { 643 diff += float64(block.Difficulty().Uint64()) 644 if parent := chain.GetBlock(block.ParentHash(), block.NumberU64()-1); parent == nil { 645 break 646 } else { 647 block = parent 648 } 649 } 650 // Estimate an ETA based on the block times and the difficulty growth 651 growth := diff / float64(head.Time()-block.Time()+1) // +1 to avoid div by zero 652 if growth > 0 { 653 if left := new(big.Int).Sub(ttd, htd); left.IsUint64() { 654 eta = time.Duration(float64(left.Uint64())/growth) * time.Second 655 } else { 656 eta = time.Duration(new(big.Int).Div(left, big.NewInt(int64(growth))).Uint64()) * time.Second 657 } 658 } 659 } 660 var message string 661 if htd.Cmp(ttd) > 0 { 662 if lastTransitionUpdate.IsZero() { 663 message = "Merge already reached, but no beacon client seen. Please launch one to follow the chain!" 664 } else { 665 message = "Merge already reached, but previously seen beacon client is offline. Please ensure it is operational to follow the chain!" 666 } 667 } else { 668 if lastTransitionUpdate.IsZero() { 669 message = "Merge is configured, but no beacon client seen. Please ensure you have one available before the transition arrives!" 670 } else { 671 message = "Merge is configured, but previously seen beacon client is offline. Please ensure it is operational before the transition arrives!" 672 } 673 } 674 if eta == 0 { 675 log.Warn(message) 676 } else { 677 log.Warn(message, "eta", common.PrettyAge(time.Now().Add(-eta))) // weird hack, but duration formatted doesn't handle days 678 } 679 offlineLogged = time.Now() 680 } 681 continue 682 } else { 683 offlineLogged = time.Time{} 684 } 685 } 686 } 687 }