github.com/ethxdao/go-ethereum@v0.0.0-20221218102228-5ae34a9cc189/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/ethxdao/go-ethereum/common" 30 "github.com/ethxdao/go-ethereum/common/hexutil" 31 "github.com/ethxdao/go-ethereum/core/beacon" 32 "github.com/ethxdao/go-ethereum/core/rawdb" 33 "github.com/ethxdao/go-ethereum/core/types" 34 "github.com/ethxdao/go-ethereum/eth" 35 "github.com/ethxdao/go-ethereum/eth/downloader" 36 "github.com/ethxdao/go-ethereum/log" 37 "github.com/ethxdao/go-ethereum/node" 38 "github.com/ethxdao/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 // 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 the network is not yet merged/merging, don't bother scaring the user 580 ttd := api.eth.BlockChain().Config().TerminalTotalDifficulty 581 if ttd == nil { 582 continue 583 } 584 api.lastTransitionLock.Lock() 585 lastTransitionUpdate := api.lastTransitionUpdate 586 api.lastTransitionLock.Unlock() 587 588 //api.lastForkchoiceLock.Lock() 589 //lastForkchoiceUpdate := api.lastForkchoiceUpdate 590 //api.lastForkchoiceLock.Unlock() 591 // 592 //api.lastNewPayloadLock.Lock() 593 //lastNewPayloadUpdate := api.lastNewPayloadUpdate 594 //api.lastNewPayloadLock.Unlock() 595 596 // If there have been no updates for the past while, warn the user 597 // that the beacon client is probably offline 598 //if api.eth.BlockChain().Config().TerminalTotalDifficultyPassed || api.eth.Merger().TDDReached() { 599 // if time.Since(lastForkchoiceUpdate) > beaconUpdateConsensusTimeout && time.Since(lastNewPayloadUpdate) > beaconUpdateConsensusTimeout { 600 // if time.Since(lastTransitionUpdate) > beaconUpdateExchangeTimeout { 601 // if time.Since(offlineLogged) > beaconUpdateWarnFrequency { 602 // if lastTransitionUpdate.IsZero() { 603 // log.Warn("Post-merge network, but no beacon client seen. Please launch one to follow the chain!") 604 // } else { 605 // log.Warn("Previously seen beacon client is offline. Please ensure it is operational to follow the chain!") 606 // } 607 // offlineLogged = time.Now() 608 // } 609 // continue 610 // } 611 // if time.Since(offlineLogged) > beaconUpdateWarnFrequency { 612 // if lastForkchoiceUpdate.IsZero() && lastNewPayloadUpdate.IsZero() { 613 // log.Warn("Beacon client online, but never received consensus updates. Please ensure your beacon client is operational to follow the chain!") 614 // } else { 615 // log.Warn("Beacon client online, but no consensus updates received in a while. Please fix your beacon client to follow the chain!") 616 // } 617 // offlineLogged = time.Now() 618 // } 619 // continue 620 // } else { 621 // offlineLogged = time.Time{} 622 // } 623 //} else { 624 if time.Since(lastTransitionUpdate) > beaconUpdateExchangeTimeout { 625 if time.Since(offlineLogged) > beaconUpdateWarnFrequency { 626 // Retrieve the last few blocks and make a rough estimate as 627 // to when the merge transition should happen 628 var ( 629 chain = api.eth.BlockChain() 630 head = chain.CurrentBlock() 631 htd = chain.GetTd(head.Hash(), head.NumberU64()) 632 eta time.Duration 633 ) 634 if head.NumberU64() > 0 && htd.Cmp(ttd) < 0 { 635 // Accumulate the last 64 difficulties to estimate the growth 636 var diff float64 637 638 block := head 639 for i := 0; i < 64; i++ { 640 diff += float64(block.Difficulty().Uint64()) 641 if parent := chain.GetBlock(block.ParentHash(), block.NumberU64()-1); parent == nil { 642 break 643 } else { 644 block = parent 645 } 646 } 647 // Estimate an ETA based on the block times and the difficulty growth 648 growth := diff / float64(head.Time()-block.Time()+1) // +1 to avoid div by zero 649 if growth > 0 { 650 if left := new(big.Int).Sub(ttd, htd); left.IsUint64() { 651 eta = time.Duration(float64(left.Uint64())/growth) * time.Second 652 } else { 653 eta = time.Duration(new(big.Int).Div(left, big.NewInt(int64(growth))).Uint64()) * time.Second 654 } 655 } 656 } 657 var message string 658 if htd.Cmp(ttd) > 0 { 659 if lastTransitionUpdate.IsZero() { 660 message = "Merge already reached, but no beacon client seen. Please launch one to follow the chain!" 661 } else { 662 message = "Merge already reached, but previously seen beacon client is offline. Please ensure it is operational to follow the chain!" 663 } 664 } else { 665 if lastTransitionUpdate.IsZero() { 666 message = "Merge is configured, but no beacon client seen. Please ensure you have one available before the transition arrives!" 667 } else { 668 message = "Merge is configured, but previously seen beacon client is offline. Please ensure it is operational before the transition arrives!" 669 } 670 } 671 if eta == 0 { 672 log.Warn(message) 673 } else { 674 log.Warn(message, "eta", common.PrettyAge(time.Now().Add(-eta))) // weird hack, but duration formatted doesn't handle days 675 } 676 offlineLogged = time.Now() 677 } 678 continue 679 } else { 680 offlineLogged = time.Time{} 681 } 682 //} 683 } 684 }