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