github.com/aiyaya188/klaytn@v0.0.0-20220629133911-2c66fd5546f4/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 "sync" 26 "time" 27 28 "github.com/aiyaya188/klaytn/common" 29 "github.com/aiyaya188/klaytn/common/hexutil" 30 "github.com/aiyaya188/klaytn/core/beacon" 31 "github.com/aiyaya188/klaytn/core/rawdb" 32 "github.com/aiyaya188/klaytn/core/types" 33 "github.com/aiyaya188/klaytn/eth" 34 "github.com/aiyaya188/klaytn/log" 35 "github.com/aiyaya188/klaytn/node" 36 "github.com/aiyaya188/klaytn/rpc" 37 ) 38 39 // Register adds catalyst APIs to the full node. 40 func Register(stack *node.Node, backend *eth.Ethereum) error { 41 log.Warn("Catalyst mode enabled", "protocol", "eth") 42 stack.RegisterAPIs([]rpc.API{ 43 { 44 Namespace: "engine", 45 Version: "1.0", 46 Service: NewConsensusAPI(backend), 47 Authenticated: true, 48 }, 49 }) 50 return nil 51 } 52 53 type ConsensusAPI struct { 54 eth *eth.Ethereum 55 remoteBlocks *headerQueue // Cache of remote payloads received 56 localBlocks *payloadQueue // Cache of local payloads generated 57 // Lock for the forkChoiceUpdated method 58 forkChoiceLock sync.Mutex 59 } 60 61 // NewConsensusAPI creates a new consensus api for the given backend. 62 // The underlying blockchain needs to have a valid terminal total difficulty set. 63 func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI { 64 if eth.BlockChain().Config().TerminalTotalDifficulty == nil { 65 panic("Catalyst started without valid total difficulty") 66 } 67 return &ConsensusAPI{ 68 eth: eth, 69 remoteBlocks: newHeaderQueue(), 70 localBlocks: newPayloadQueue(), 71 } 72 } 73 74 // ForkchoiceUpdatedV1 has several responsibilities: 75 // If the method is called with an empty head block: 76 // we return success, which can be used to check if the catalyst mode is enabled 77 // If the total difficulty was not reached: 78 // we return INVALID 79 // If the finalizedBlockHash is set: 80 // we check if we have the finalizedBlockHash in our db, if not we start a sync 81 // We try to set our blockchain to the headBlock 82 // If there are payloadAttributes: 83 // we try to assemble a block with the payloadAttributes and return its payloadID 84 func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, payloadAttributes *beacon.PayloadAttributesV1) (beacon.ForkChoiceResponse, error) { 85 api.forkChoiceLock.Lock() 86 defer api.forkChoiceLock.Unlock() 87 88 log.Trace("Engine API request received", "method", "ForkchoiceUpdated", "head", update.HeadBlockHash, "finalized", update.FinalizedBlockHash, "safe", update.SafeBlockHash) 89 if update.HeadBlockHash == (common.Hash{}) { 90 log.Warn("Forkchoice requested update to zero hash") 91 return beacon.STATUS_INVALID, nil // TODO(karalabe): Why does someone send us this? 92 } 93 94 // Check whether we have the block yet in our database or not. If not, we'll 95 // need to either trigger a sync, or to reject this forkchoice update for a 96 // reason. 97 block := api.eth.BlockChain().GetBlockByHash(update.HeadBlockHash) 98 if block == nil { 99 // If the head hash is unknown (was not given to us in a newPayload request), 100 // we cannot resolve the header, so not much to do. This could be extended in 101 // the future to resolve from the `eth` network, but it's an unexpected case 102 // that should be fixed, not papered over. 103 header := api.remoteBlocks.get(update.HeadBlockHash) 104 if header == nil { 105 log.Warn("Forkchoice requested unknown head", "hash", update.HeadBlockHash) 106 return beacon.STATUS_SYNCING, nil 107 } 108 // Header advertised via a past newPayload request. Start syncing to it. 109 // Before we do however, make sure any legacy sync in switched off so we 110 // don't accidentally have 2 cycles running. 111 if merger := api.eth.Merger(); !merger.TDDReached() { 112 merger.ReachTTD() 113 api.eth.Downloader().Cancel() 114 } 115 log.Info("Forkchoice requested sync to new head", "number", header.Number, "hash", header.Hash()) 116 if err := api.eth.Downloader().BeaconSync(api.eth.SyncMode(), header); err != nil { 117 return beacon.STATUS_SYNCING, err 118 } 119 return beacon.STATUS_SYNCING, nil 120 } 121 // Block is known locally, just sanity check that the beacon client does not 122 // attempt to push us back to before the merge. 123 if block.Difficulty().BitLen() > 0 || block.NumberU64() == 0 { 124 var ( 125 td = api.eth.BlockChain().GetTd(update.HeadBlockHash, block.NumberU64()) 126 ptd = api.eth.BlockChain().GetTd(block.ParentHash(), block.NumberU64()-1) 127 ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty 128 ) 129 if td == nil || (block.NumberU64() > 0 && ptd == nil) { 130 log.Error("TDs unavailable for TTD check", "number", block.NumberU64(), "hash", update.HeadBlockHash, "td", td, "parent", block.ParentHash(), "ptd", ptd) 131 return beacon.STATUS_INVALID, errors.New("TDs unavailable for TDD check") 132 } 133 if td.Cmp(ttd) < 0 { 134 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))) 135 return beacon.ForkChoiceResponse{PayloadStatus: beacon.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil 136 } 137 if block.NumberU64() > 0 && ptd.Cmp(ttd) >= 0 { 138 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))) 139 return beacon.ForkChoiceResponse{PayloadStatus: beacon.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil 140 } 141 } 142 valid := func(id *beacon.PayloadID) beacon.ForkChoiceResponse { 143 return beacon.ForkChoiceResponse{ 144 PayloadStatus: beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &update.HeadBlockHash}, 145 PayloadID: id, 146 } 147 } 148 if rawdb.ReadCanonicalHash(api.eth.ChainDb(), block.NumberU64()) != update.HeadBlockHash { 149 // Block is not canonical, set head. 150 if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil { 151 return beacon.ForkChoiceResponse{PayloadStatus: beacon.PayloadStatusV1{Status: beacon.INVALID, LatestValidHash: &latestValid}}, err 152 } 153 } else if api.eth.BlockChain().CurrentBlock().Hash() == update.HeadBlockHash { 154 // If the specified head matches with our local head, do nothing and keep 155 // generating the payload. It's a special corner case that a few slots are 156 // missing and we are requested to generate the payload in slot. 157 } else { 158 // If the head block is already in our canonical chain, the beacon client is 159 // probably resyncing. Ignore the update. 160 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()) 161 return valid(nil), nil 162 } 163 api.eth.SetSynced() 164 165 // If the beacon client also advertised a finalized block, mark the local 166 // chain final and completely in PoS mode. 167 if update.FinalizedBlockHash != (common.Hash{}) { 168 if merger := api.eth.Merger(); !merger.PoSFinalized() { 169 merger.FinalizePoS() 170 } 171 // If the finalized block is not in our canonical tree, somethings wrong 172 finalBlock := api.eth.BlockChain().GetBlockByHash(update.FinalizedBlockHash) 173 if finalBlock == nil { 174 log.Warn("Final block not available in database", "hash", update.FinalizedBlockHash) 175 return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("final block not available in database")) 176 } else if rawdb.ReadCanonicalHash(api.eth.ChainDb(), finalBlock.NumberU64()) != update.FinalizedBlockHash { 177 log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", update.HeadBlockHash) 178 return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("final block not in canonical chain")) 179 } 180 // Set the finalized block 181 api.eth.BlockChain().SetFinalized(finalBlock) 182 } 183 // Check if the safe block hash is in our canonical tree, if not somethings wrong 184 if update.SafeBlockHash != (common.Hash{}) { 185 safeBlock := api.eth.BlockChain().GetBlockByHash(update.SafeBlockHash) 186 if safeBlock == nil { 187 log.Warn("Safe block not available in database") 188 return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("safe block not available in database")) 189 } 190 if rawdb.ReadCanonicalHash(api.eth.ChainDb(), safeBlock.NumberU64()) != update.SafeBlockHash { 191 log.Warn("Safe block not in canonical chain") 192 return beacon.STATUS_INVALID, beacon.InvalidForkChoiceState.With(errors.New("safe block not in canonical chain")) 193 } 194 } 195 // If payload generation was requested, create a new block to be potentially 196 // sealed by the beacon client. The payload will be requested later, and we 197 // might replace it arbitrarily many times in between. 198 if payloadAttributes != nil { 199 // Create an empty block first which can be used as a fallback 200 empty, err := api.eth.Miner().GetSealingBlockSync(update.HeadBlockHash, payloadAttributes.Timestamp, payloadAttributes.SuggestedFeeRecipient, payloadAttributes.Random, true) 201 if err != nil { 202 log.Error("Failed to create empty sealing payload", "err", err) 203 return valid(nil), beacon.InvalidPayloadAttributes.With(err) 204 } 205 // Send a request to generate a full block in the background. 206 // The result can be obtained via the returned channel. 207 resCh, err := api.eth.Miner().GetSealingBlockAsync(update.HeadBlockHash, payloadAttributes.Timestamp, payloadAttributes.SuggestedFeeRecipient, payloadAttributes.Random, false) 208 if err != nil { 209 log.Error("Failed to create async sealing payload", "err", err) 210 return valid(nil), beacon.InvalidPayloadAttributes.With(err) 211 } 212 id := computePayloadId(update.HeadBlockHash, payloadAttributes) 213 api.localBlocks.put(id, &payload{empty: empty, result: resCh}) 214 return valid(&id), nil 215 } 216 return valid(nil), nil 217 } 218 219 // ExchangeTransitionConfigurationV1 checks the given configuration against 220 // the configuration of the node. 221 func (api *ConsensusAPI) ExchangeTransitionConfigurationV1(config beacon.TransitionConfigurationV1) (*beacon.TransitionConfigurationV1, error) { 222 if config.TerminalTotalDifficulty == nil { 223 return nil, errors.New("invalid terminal total difficulty") 224 } 225 ttd := api.eth.BlockChain().Config().TerminalTotalDifficulty 226 if ttd.Cmp(config.TerminalTotalDifficulty.ToInt()) != 0 { 227 log.Warn("Invalid TTD configured", "geth", ttd, "beacon", config.TerminalTotalDifficulty) 228 return nil, fmt.Errorf("invalid ttd: execution %v consensus %v", ttd, config.TerminalTotalDifficulty) 229 } 230 231 if config.TerminalBlockHash != (common.Hash{}) { 232 if hash := api.eth.BlockChain().GetCanonicalHash(uint64(config.TerminalBlockNumber)); hash == config.TerminalBlockHash { 233 return &beacon.TransitionConfigurationV1{ 234 TerminalTotalDifficulty: (*hexutil.Big)(ttd), 235 TerminalBlockHash: config.TerminalBlockHash, 236 TerminalBlockNumber: config.TerminalBlockNumber, 237 }, nil 238 } 239 return nil, fmt.Errorf("invalid terminal block hash") 240 } 241 return &beacon.TransitionConfigurationV1{TerminalTotalDifficulty: (*hexutil.Big)(ttd)}, nil 242 } 243 244 // GetPayloadV1 returns a cached payload by id. 245 func (api *ConsensusAPI) GetPayloadV1(payloadID beacon.PayloadID) (*beacon.ExecutableDataV1, error) { 246 log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID) 247 data := api.localBlocks.get(payloadID) 248 if data == nil { 249 return nil, beacon.UnknownPayload 250 } 251 return data, nil 252 } 253 254 // NewPayloadV1 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. 255 func (api *ConsensusAPI) NewPayloadV1(params beacon.ExecutableDataV1) (beacon.PayloadStatusV1, error) { 256 log.Trace("Engine API request received", "method", "ExecutePayload", "number", params.Number, "hash", params.BlockHash) 257 block, err := beacon.ExecutableDataToBlock(params) 258 if err != nil { 259 log.Debug("Invalid NewPayload params", "params", params, "error", err) 260 return beacon.PayloadStatusV1{Status: beacon.INVALIDBLOCKHASH}, nil 261 } 262 // If we already have the block locally, ignore the entire execution and just 263 // return a fake success. 264 if block := api.eth.BlockChain().GetBlockByHash(params.BlockHash); block != nil { 265 log.Warn("Ignoring already known beacon payload", "number", params.Number, "hash", params.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0))) 266 hash := block.Hash() 267 return beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &hash}, nil 268 } 269 // If the parent is missing, we - in theory - could trigger a sync, but that 270 // would also entail a reorg. That is problematic if multiple sibling blocks 271 // are being fed to us, and even more so, if some semi-distant uncle shortens 272 // our live chain. As such, payload execution will not permit reorgs and thus 273 // will not trigger a sync cycle. That is fine though, if we get a fork choice 274 // update after legit payload executions. 275 parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1) 276 if parent == nil { 277 // Stash the block away for a potential forced forckchoice update to it 278 // at a later time. 279 api.remoteBlocks.put(block.Hash(), block.Header()) 280 281 // Although we don't want to trigger a sync, if there is one already in 282 // progress, try to extend if with the current payload request to relieve 283 // some strain from the forkchoice update. 284 if err := api.eth.Downloader().BeaconExtend(api.eth.SyncMode(), block.Header()); err == nil { 285 log.Debug("Payload accepted for sync extension", "number", params.Number, "hash", params.BlockHash) 286 return beacon.PayloadStatusV1{Status: beacon.SYNCING}, nil 287 } 288 // Either no beacon sync was started yet, or it rejected the delivered 289 // payload as non-integratable on top of the existing sync. We'll just 290 // have to rely on the beacon client to forcefully update the head with 291 // a forkchoice update request. 292 log.Warn("Ignoring payload with missing parent", "number", params.Number, "hash", params.BlockHash, "parent", params.ParentHash) 293 return beacon.PayloadStatusV1{Status: beacon.ACCEPTED}, nil 294 } 295 // We have an existing parent, do some sanity checks to avoid the beacon client 296 // triggering too early 297 var ( 298 ptd = api.eth.BlockChain().GetTd(parent.Hash(), parent.NumberU64()) 299 ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty 300 gptd = api.eth.BlockChain().GetTd(parent.ParentHash(), parent.NumberU64()-1) 301 ) 302 if ptd.Cmp(ttd) < 0 { 303 log.Warn("Ignoring pre-merge payload", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd) 304 return beacon.INVALID_TERMINAL_BLOCK, nil 305 } 306 if parent.Difficulty().BitLen() > 0 && gptd != nil && gptd.Cmp(ttd) >= 0 { 307 log.Error("Ignoring pre-merge parent block", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd) 308 return beacon.INVALID_TERMINAL_BLOCK, nil 309 } 310 if block.Time() <= parent.Time() { 311 log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time()) 312 return api.invalid(errors.New("invalid timestamp"), parent), nil 313 } 314 if !api.eth.BlockChain().HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { 315 api.remoteBlocks.put(block.Hash(), block.Header()) 316 log.Warn("State not available, ignoring new payload") 317 return beacon.PayloadStatusV1{Status: beacon.ACCEPTED}, nil 318 } 319 log.Trace("Inserting block without sethead", "hash", block.Hash(), "number", block.Number) 320 if err := api.eth.BlockChain().InsertBlockWithoutSetHead(block); err != nil { 321 log.Warn("NewPayloadV1: inserting block failed", "error", err) 322 return api.invalid(err, parent), nil 323 } 324 // We've accepted a valid payload from the beacon client. Mark the local 325 // chain transitions to notify other subsystems (e.g. downloader) of the 326 // behavioral change. 327 if merger := api.eth.Merger(); !merger.TDDReached() { 328 merger.ReachTTD() 329 api.eth.Downloader().Cancel() 330 } 331 hash := block.Hash() 332 return beacon.PayloadStatusV1{Status: beacon.VALID, LatestValidHash: &hash}, nil 333 } 334 335 // computePayloadId computes a pseudo-random payloadid, based on the parameters. 336 func computePayloadId(headBlockHash common.Hash, params *beacon.PayloadAttributesV1) beacon.PayloadID { 337 // Hash 338 hasher := sha256.New() 339 hasher.Write(headBlockHash[:]) 340 binary.Write(hasher, binary.BigEndian, params.Timestamp) 341 hasher.Write(params.Random[:]) 342 hasher.Write(params.SuggestedFeeRecipient[:]) 343 var out beacon.PayloadID 344 copy(out[:], hasher.Sum(nil)[:8]) 345 return out 346 } 347 348 // invalid returns a response "INVALID" with the latest valid hash supplied by latest or to the current head 349 // if no latestValid block was provided. 350 func (api *ConsensusAPI) invalid(err error, latestValid *types.Block) beacon.PayloadStatusV1 { 351 currentHash := api.eth.BlockChain().CurrentBlock().Hash() 352 if latestValid != nil { 353 // Set latest valid hash to 0x0 if parent is PoW block 354 currentHash = common.Hash{} 355 if latestValid.Difficulty().BitLen() == 0 { 356 // Otherwise set latest valid hash to parent hash 357 currentHash = latestValid.Hash() 358 } 359 } 360 errorMsg := err.Error() 361 return beacon.PayloadStatusV1{Status: beacon.INVALID, LatestValidHash: ¤tHash, ValidationError: &errorMsg} 362 }