github.com/ethereum/go-ethereum@v1.16.1/eth/catalyst/simulated_beacon.go (about) 1 // Copyright 2023 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 18 19 import ( 20 "crypto/rand" 21 "crypto/sha256" 22 "errors" 23 "fmt" 24 "math" 25 "sync" 26 "time" 27 28 "github.com/ethereum/go-ethereum/beacon/engine" 29 "github.com/ethereum/go-ethereum/common" 30 "github.com/ethereum/go-ethereum/core/txpool" 31 "github.com/ethereum/go-ethereum/core/types" 32 "github.com/ethereum/go-ethereum/crypto/kzg4844" 33 "github.com/ethereum/go-ethereum/eth" 34 "github.com/ethereum/go-ethereum/event" 35 "github.com/ethereum/go-ethereum/log" 36 "github.com/ethereum/go-ethereum/node" 37 "github.com/ethereum/go-ethereum/params" 38 "github.com/ethereum/go-ethereum/params/forks" 39 "github.com/ethereum/go-ethereum/rpc" 40 ) 41 42 const devEpochLength = 32 43 44 // withdrawalQueue implements a FIFO queue which holds withdrawals that are 45 // pending inclusion. 46 type withdrawalQueue struct { 47 pending types.Withdrawals 48 mu sync.Mutex 49 feed event.Feed 50 subs event.SubscriptionScope 51 } 52 53 type newWithdrawalsEvent struct{ Withdrawals types.Withdrawals } 54 55 // add queues a withdrawal for future inclusion. 56 func (w *withdrawalQueue) add(withdrawal *types.Withdrawal) error { 57 w.mu.Lock() 58 w.pending = append(w.pending, withdrawal) 59 w.mu.Unlock() 60 61 w.feed.Send(newWithdrawalsEvent{types.Withdrawals{withdrawal}}) 62 return nil 63 } 64 65 // pop dequeues the specified number of withdrawals from the queue. 66 func (w *withdrawalQueue) pop(count int) types.Withdrawals { 67 w.mu.Lock() 68 defer w.mu.Unlock() 69 70 count = min(count, len(w.pending)) 71 popped := w.pending[0:count] 72 w.pending = w.pending[count:] 73 74 return popped 75 } 76 77 // subscribe allows a listener to be updated when new withdrawals are added to 78 // the queue. 79 func (w *withdrawalQueue) subscribe(ch chan<- newWithdrawalsEvent) event.Subscription { 80 sub := w.feed.Subscribe(ch) 81 return w.subs.Track(sub) 82 } 83 84 // SimulatedBeacon drives an Ethereum instance as if it were a real beacon 85 // client. It can run in period mode where it mines a new block every period 86 // (seconds) or on every transaction via Commit, Fork and AdjustTime. 87 type SimulatedBeacon struct { 88 shutdownCh chan struct{} 89 eth *eth.Ethereum 90 period uint64 91 withdrawals withdrawalQueue 92 93 feeRecipient common.Address 94 feeRecipientLock sync.Mutex // lock gates concurrent access to the feeRecipient 95 96 engineAPI *ConsensusAPI 97 curForkchoiceState engine.ForkchoiceStateV1 98 lastBlockTime uint64 99 } 100 101 func payloadVersion(config *params.ChainConfig, time uint64) engine.PayloadVersion { 102 switch config.LatestFork(time) { 103 case forks.Prague, forks.Cancun: 104 return engine.PayloadV3 105 case forks.Paris, forks.Shanghai: 106 return engine.PayloadV2 107 } 108 panic("invalid fork, simulated beacon needs to be started post-merge") 109 } 110 111 // NewSimulatedBeacon constructs a new simulated beacon chain. 112 func NewSimulatedBeacon(period uint64, feeRecipient common.Address, eth *eth.Ethereum) (*SimulatedBeacon, error) { 113 block := eth.BlockChain().CurrentBlock() 114 current := engine.ForkchoiceStateV1{ 115 HeadBlockHash: block.Hash(), 116 SafeBlockHash: block.Hash(), 117 FinalizedBlockHash: block.Hash(), 118 } 119 engineAPI := newConsensusAPIWithoutHeartbeat(eth) 120 121 // if genesis block, send forkchoiceUpdated to trigger transition to PoS 122 if block.Number.Sign() == 0 { 123 version := payloadVersion(eth.BlockChain().Config(), block.Time) 124 if _, err := engineAPI.forkchoiceUpdated(current, nil, version, false); err != nil { 125 return nil, err 126 } 127 } 128 129 // cap the dev mode period to a reasonable maximum value to avoid 130 // overflowing the time.Duration (int64) that it will occupy 131 const maxPeriod = uint64(math.MaxInt64 / time.Second) 132 return &SimulatedBeacon{ 133 eth: eth, 134 period: min(period, maxPeriod), 135 shutdownCh: make(chan struct{}), 136 engineAPI: engineAPI, 137 lastBlockTime: block.Time, 138 curForkchoiceState: current, 139 feeRecipient: feeRecipient, 140 }, nil 141 } 142 143 func (c *SimulatedBeacon) setFeeRecipient(feeRecipient common.Address) { 144 c.feeRecipientLock.Lock() 145 c.feeRecipient = feeRecipient 146 c.feeRecipientLock.Unlock() 147 } 148 149 // Start invokes the SimulatedBeacon life-cycle function in a goroutine. 150 func (c *SimulatedBeacon) Start() error { 151 if c.period == 0 { 152 // if period is set to 0, do not mine at all 153 // this is used in the simulated backend where blocks 154 // are explicitly mined via Commit, AdjustTime and Fork 155 } else { 156 go c.loop() 157 } 158 return nil 159 } 160 161 // Stop halts the SimulatedBeacon service. 162 func (c *SimulatedBeacon) Stop() error { 163 close(c.shutdownCh) 164 return nil 165 } 166 167 // sealBlock initiates payload building for a new block and creates a new block 168 // with the completed payload. 169 func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp uint64) error { 170 if timestamp <= c.lastBlockTime { 171 timestamp = c.lastBlockTime + 1 172 } 173 c.feeRecipientLock.Lock() 174 feeRecipient := c.feeRecipient 175 c.feeRecipientLock.Unlock() 176 177 // Reset to CurrentBlock in case of the chain was rewound 178 if header := c.eth.BlockChain().CurrentBlock(); c.curForkchoiceState.HeadBlockHash != header.Hash() { 179 finalizedHash := c.finalizedBlockHash(header.Number.Uint64()) 180 c.setCurrentState(header.Hash(), *finalizedHash) 181 } 182 183 // Because transaction insertion, block insertion, and block production will 184 // happen without any timing delay between them in simulator mode and the 185 // transaction pool will be running its internal reset operation on a 186 // background thread, flaky executions can happen. To avoid the racey 187 // behavior, the pool will be explicitly blocked on its reset before 188 // continuing to the block production below. 189 if err := c.eth.APIBackend.TxPool().Sync(); err != nil { 190 return fmt.Errorf("failed to sync txpool: %w", err) 191 } 192 193 version := payloadVersion(c.eth.BlockChain().Config(), timestamp) 194 195 var random [32]byte 196 rand.Read(random[:]) 197 fcResponse, err := c.engineAPI.forkchoiceUpdated(c.curForkchoiceState, &engine.PayloadAttributes{ 198 Timestamp: timestamp, 199 SuggestedFeeRecipient: feeRecipient, 200 Withdrawals: withdrawals, 201 Random: random, 202 BeaconRoot: &common.Hash{}, 203 }, version, false) 204 if err != nil { 205 return err 206 } 207 if fcResponse == engine.STATUS_SYNCING { 208 return errors.New("chain rewind prevented invocation of payload creation") 209 } 210 211 // If the payload was already known, we can skip the rest of the process. 212 // This edge case is possible due to a race condition between seal and debug.setHead. 213 if fcResponse.PayloadStatus.Status == engine.VALID && fcResponse.PayloadID == nil { 214 return nil 215 } 216 217 envelope, err := c.engineAPI.getPayload(*fcResponse.PayloadID, true) 218 if err != nil { 219 return err 220 } 221 payload := envelope.ExecutionPayload 222 223 var finalizedHash common.Hash 224 if payload.Number%devEpochLength == 0 { 225 finalizedHash = payload.BlockHash 226 } else { 227 if fh := c.finalizedBlockHash(payload.Number); fh == nil { 228 return errors.New("chain rewind interrupted calculation of finalized block hash") 229 } else { 230 finalizedHash = *fh 231 } 232 } 233 234 var ( 235 blobHashes []common.Hash 236 beaconRoot *common.Hash 237 requests [][]byte 238 ) 239 // Compute post-shanghai fields 240 if version > engine.PayloadV2 { 241 // Independently calculate the blob hashes from sidecars. 242 blobHashes = make([]common.Hash, 0) 243 if envelope.BlobsBundle != nil { 244 hasher := sha256.New() 245 for _, commit := range envelope.BlobsBundle.Commitments { 246 var c kzg4844.Commitment 247 if len(commit) != len(c) { 248 return errors.New("invalid commitment length") 249 } 250 copy(c[:], commit) 251 blobHashes = append(blobHashes, kzg4844.CalcBlobHashV1(hasher, &c)) 252 } 253 } 254 beaconRoot = &common.Hash{} 255 requests = envelope.Requests 256 } 257 258 // Mark the payload as canon 259 _, err = c.engineAPI.newPayload(*payload, blobHashes, beaconRoot, requests, false) 260 if err != nil { 261 return err 262 } 263 c.setCurrentState(payload.BlockHash, finalizedHash) 264 265 // Mark the block containing the payload as canonical 266 if _, err = c.engineAPI.forkchoiceUpdated(c.curForkchoiceState, nil, version, false); err != nil { 267 return err 268 } 269 c.lastBlockTime = payload.Timestamp 270 return nil 271 } 272 273 // loop runs the block production loop for non-zero period configuration 274 func (c *SimulatedBeacon) loop() { 275 timer := time.NewTimer(0) 276 for { 277 select { 278 case <-c.shutdownCh: 279 return 280 case <-timer.C: 281 if err := c.sealBlock(c.withdrawals.pop(10), uint64(time.Now().Unix())); err != nil { 282 log.Warn("Error performing sealing work", "err", err) 283 } else { 284 timer.Reset(time.Second * time.Duration(c.period)) 285 } 286 } 287 } 288 } 289 290 // finalizedBlockHash returns the block hash of the finalized block corresponding 291 // to the given number or nil if doesn't exist in the chain. 292 func (c *SimulatedBeacon) finalizedBlockHash(number uint64) *common.Hash { 293 var finalizedNumber uint64 294 if number%devEpochLength == 0 { 295 finalizedNumber = number 296 } else { 297 finalizedNumber = (number - 1) / devEpochLength * devEpochLength 298 } 299 if finalizedBlock := c.eth.BlockChain().GetBlockByNumber(finalizedNumber); finalizedBlock != nil { 300 fh := finalizedBlock.Hash() 301 return &fh 302 } 303 return nil 304 } 305 306 // setCurrentState sets the current forkchoice state 307 func (c *SimulatedBeacon) setCurrentState(headHash, finalizedHash common.Hash) { 308 c.curForkchoiceState = engine.ForkchoiceStateV1{ 309 HeadBlockHash: headHash, 310 SafeBlockHash: headHash, 311 FinalizedBlockHash: finalizedHash, 312 } 313 } 314 315 // Commit seals a block on demand. 316 func (c *SimulatedBeacon) Commit() common.Hash { 317 withdrawals := c.withdrawals.pop(10) 318 if err := c.sealBlock(withdrawals, uint64(time.Now().Unix())); err != nil { 319 log.Warn("Error performing sealing work", "err", err) 320 } 321 return c.eth.BlockChain().CurrentBlock().Hash() 322 } 323 324 // Rollback un-sends previously added transactions. 325 func (c *SimulatedBeacon) Rollback() { 326 c.eth.TxPool().Clear() 327 } 328 329 // Fork sets the head to the provided hash. 330 func (c *SimulatedBeacon) Fork(parentHash common.Hash) error { 331 // Ensure no pending transactions. 332 c.eth.TxPool().Sync() 333 if len(c.eth.TxPool().Pending(txpool.PendingFilter{})) != 0 { 334 return errors.New("pending block dirty") 335 } 336 337 parent := c.eth.BlockChain().GetBlockByHash(parentHash) 338 if parent == nil { 339 return errors.New("parent not found") 340 } 341 _, err := c.eth.BlockChain().SetCanonical(parent) 342 return err 343 } 344 345 // AdjustTime creates a new block with an adjusted timestamp. 346 func (c *SimulatedBeacon) AdjustTime(adjustment time.Duration) error { 347 if len(c.eth.TxPool().Pending(txpool.PendingFilter{})) != 0 { 348 return errors.New("could not adjust time on non-empty block") 349 } 350 parent := c.eth.BlockChain().CurrentBlock() 351 if parent == nil { 352 return errors.New("parent not found") 353 } 354 withdrawals := c.withdrawals.pop(10) 355 return c.sealBlock(withdrawals, parent.Time+uint64(adjustment/time.Second)) 356 } 357 358 // RegisterSimulatedBeaconAPIs registers the simulated beacon's API with the 359 // stack. 360 func RegisterSimulatedBeaconAPIs(stack *node.Node, sim *SimulatedBeacon) { 361 api := newSimulatedBeaconAPI(sim) 362 stack.RegisterAPIs([]rpc.API{ 363 { 364 Namespace: "dev", 365 Service: api, 366 Version: "1.0", 367 }, 368 }) 369 }