github.com/theQRL/go-zond@v0.2.1/zond/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 "errors" 22 "math/big" 23 "sync" 24 "time" 25 26 "github.com/theQRL/go-zond/beacon/engine" 27 "github.com/theQRL/go-zond/common" 28 "github.com/theQRL/go-zond/core/txpool" 29 "github.com/theQRL/go-zond/core/types" 30 "github.com/theQRL/go-zond/log" 31 "github.com/theQRL/go-zond/node" 32 "github.com/theQRL/go-zond/params" 33 "github.com/theQRL/go-zond/rpc" 34 "github.com/theQRL/go-zond/zond" 35 ) 36 37 const devEpochLength = 32 38 39 // withdrawalQueue implements a FIFO queue which holds withdrawals that are 40 // pending inclusion. 41 type withdrawalQueue struct { 42 pending chan *types.Withdrawal 43 } 44 45 // add queues a withdrawal for future inclusion. 46 func (w *withdrawalQueue) add(withdrawal *types.Withdrawal) error { 47 select { 48 case w.pending <- withdrawal: 49 break 50 default: 51 return errors.New("withdrawal queue full") 52 } 53 return nil 54 } 55 56 // gatherPending returns a number of queued withdrawals up to a maximum count. 57 func (w *withdrawalQueue) gatherPending(maxCount int) []*types.Withdrawal { 58 withdrawals := []*types.Withdrawal{} 59 for { 60 select { 61 case withdrawal := <-w.pending: 62 withdrawals = append(withdrawals, withdrawal) 63 if len(withdrawals) == maxCount { 64 return withdrawals 65 } 66 default: 67 return withdrawals 68 } 69 } 70 } 71 72 type SimulatedBeacon struct { 73 shutdownCh chan struct{} 74 zond *zond.Zond 75 period uint64 76 withdrawals withdrawalQueue 77 78 feeRecipient common.Address 79 feeRecipientLock sync.Mutex // lock gates concurrent access to the feeRecipient 80 81 engineAPI *ConsensusAPI 82 curForkchoiceState engine.ForkchoiceStateV1 83 lastBlockTime uint64 84 } 85 86 // NewSimulatedBeacon constructs a new simulated beacon chain. 87 // Period sets the period in which blocks should be produced. 88 // 89 // - If period is set to 0, a block is produced on every transaction. 90 // via Commit, Fork and AdjustTime. 91 func NewSimulatedBeacon(period uint64, zond *zond.Zond) (*SimulatedBeacon, error) { 92 block := zond.BlockChain().CurrentBlock() 93 current := engine.ForkchoiceStateV1{ 94 HeadBlockHash: block.Hash(), 95 SafeBlockHash: block.Hash(), 96 FinalizedBlockHash: block.Hash(), 97 } 98 engineAPI := newConsensusAPIWithoutHeartbeat(zond) 99 100 // if genesis block, send forkchoiceUpdated to trigger transition to PoS 101 if block.Number.Sign() == 0 { 102 if _, err := engineAPI.ForkchoiceUpdatedV2(current, nil); err != nil { 103 return nil, err 104 } 105 } 106 return &SimulatedBeacon{ 107 zond: zond, 108 period: period, 109 shutdownCh: make(chan struct{}), 110 engineAPI: engineAPI, 111 lastBlockTime: block.Time, 112 curForkchoiceState: current, 113 withdrawals: withdrawalQueue{make(chan *types.Withdrawal, 20)}, 114 }, nil 115 } 116 117 func (c *SimulatedBeacon) setFeeRecipient(feeRecipient common.Address) { 118 c.feeRecipientLock.Lock() 119 c.feeRecipient = feeRecipient 120 c.feeRecipientLock.Unlock() 121 } 122 123 // Start invokes the SimulatedBeacon life-cycle function in a goroutine. 124 func (c *SimulatedBeacon) Start() error { 125 if c.period == 0 { 126 // if period is set to 0, do not mine at all 127 // this is used in the simulated backend where blocks 128 // are explicitly mined via Commit, AdjustTime and Fork 129 } else { 130 go c.loop() 131 } 132 return nil 133 } 134 135 // Stop halts the SimulatedBeacon service. 136 func (c *SimulatedBeacon) Stop() error { 137 close(c.shutdownCh) 138 return nil 139 } 140 141 // sealBlock initiates payload building for a new block and creates a new block 142 // with the completed payload. 143 func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp uint64) error { 144 if timestamp <= c.lastBlockTime { 145 timestamp = c.lastBlockTime + 1 146 } 147 c.feeRecipientLock.Lock() 148 feeRecipient := c.feeRecipient 149 c.feeRecipientLock.Unlock() 150 151 // Reset to CurrentBlock in case of the chain was rewound 152 if header := c.zond.BlockChain().CurrentBlock(); c.curForkchoiceState.HeadBlockHash != header.Hash() { 153 finalizedHash := c.finalizedBlockHash(header.Number.Uint64()) 154 c.setCurrentState(header.Hash(), *finalizedHash) 155 } 156 157 var random [32]byte 158 rand.Read(random[:]) 159 fcResponse, err := c.engineAPI.ForkchoiceUpdatedV2(c.curForkchoiceState, &engine.PayloadAttributes{ 160 Timestamp: timestamp, 161 SuggestedFeeRecipient: feeRecipient, 162 Withdrawals: withdrawals, 163 Random: random, 164 }) 165 if err != nil { 166 return err 167 } 168 if fcResponse == engine.STATUS_SYNCING { 169 return errors.New("chain rewind prevented invocation of payload creation") 170 } 171 envelope, err := c.engineAPI.getPayload(*fcResponse.PayloadID, true) 172 if err != nil { 173 return err 174 } 175 payload := envelope.ExecutionPayload 176 177 var finalizedHash common.Hash 178 if payload.Number%devEpochLength == 0 { 179 finalizedHash = payload.BlockHash 180 } else { 181 if fh := c.finalizedBlockHash(payload.Number); fh == nil { 182 return errors.New("chain rewind interrupted calculation of finalized block hash") 183 } else { 184 finalizedHash = *fh 185 } 186 } 187 188 // Mark the payload as canon 189 if _, err = c.engineAPI.NewPayloadV2(*payload); err != nil { 190 return err 191 } 192 c.setCurrentState(payload.BlockHash, finalizedHash) 193 // Mark the block containing the payload as canonical 194 if _, err = c.engineAPI.ForkchoiceUpdatedV2(c.curForkchoiceState, nil); err != nil { 195 return err 196 } 197 c.lastBlockTime = payload.Timestamp 198 return nil 199 } 200 201 // loop runs the block production loop for non-zero period configuration 202 func (c *SimulatedBeacon) loop() { 203 timer := time.NewTimer(0) 204 for { 205 select { 206 case <-c.shutdownCh: 207 return 208 case <-timer.C: 209 withdrawals := c.withdrawals.gatherPending(10) 210 if err := c.sealBlock(withdrawals, uint64(time.Now().Unix())); err != nil { 211 log.Warn("Error performing sealing work", "err", err) 212 } else { 213 timer.Reset(time.Second * time.Duration(c.period)) 214 } 215 } 216 } 217 } 218 219 // finalizedBlockHash returns the block hash of the finalized block corresponding 220 // to the given number or nil if doesn't exist in the chain. 221 func (c *SimulatedBeacon) finalizedBlockHash(number uint64) *common.Hash { 222 var finalizedNumber uint64 223 if number%devEpochLength == 0 { 224 finalizedNumber = number 225 } else { 226 finalizedNumber = (number - 1) / devEpochLength * devEpochLength 227 } 228 229 if finalizedBlock := c.zond.BlockChain().GetBlockByNumber(finalizedNumber); finalizedBlock != nil { 230 fh := finalizedBlock.Hash() 231 return &fh 232 } 233 return nil 234 } 235 236 // setCurrentState sets the current forkchoice state 237 func (c *SimulatedBeacon) setCurrentState(headHash, finalizedHash common.Hash) { 238 c.curForkchoiceState = engine.ForkchoiceStateV1{ 239 HeadBlockHash: headHash, 240 SafeBlockHash: headHash, 241 FinalizedBlockHash: finalizedHash, 242 } 243 } 244 245 // Commit seals a block on demand. 246 func (c *SimulatedBeacon) Commit() common.Hash { 247 withdrawals := c.withdrawals.gatherPending(10) 248 if err := c.sealBlock(withdrawals, uint64(time.Now().Unix())); err != nil { 249 log.Warn("Error performing sealing work", "err", err) 250 } 251 return c.zond.BlockChain().CurrentBlock().Hash() 252 } 253 254 // Rollback un-sends previously added transactions. 255 func (c *SimulatedBeacon) Rollback() { 256 // Flush all transactions from the transaction pools 257 maxUint256 := new(big.Int).Sub(new(big.Int).Lsh(common.Big1, 256), common.Big1) 258 c.zond.TxPool().SetGasTip(maxUint256) 259 // Set the gas tip back to accept new transactions 260 // TODO (Marius van der Wijden): set gas tip to parameter passed by config 261 c.zond.TxPool().SetGasTip(big.NewInt(params.GWei)) 262 } 263 264 // Fork sets the head to the provided hash. 265 func (c *SimulatedBeacon) Fork(parentHash common.Hash) error { 266 // Ensure no pending transactions. 267 c.zond.TxPool().Sync() 268 if len(c.zond.TxPool().Pending(txpool.PendingFilter{})) != 0 { 269 return errors.New("pending block dirty") 270 } 271 272 parent := c.zond.BlockChain().GetBlockByHash(parentHash) 273 if parent == nil { 274 return errors.New("parent not found") 275 } 276 return c.zond.BlockChain().SetHead(parent.NumberU64()) 277 } 278 279 // AdjustTime creates a new block with an adjusted timestamp. 280 func (c *SimulatedBeacon) AdjustTime(adjustment time.Duration) error { 281 if len(c.zond.TxPool().Pending(txpool.PendingFilter{})) != 0 { 282 return errors.New("could not adjust time on non-empty block") 283 } 284 parent := c.zond.BlockChain().CurrentBlock() 285 if parent == nil { 286 return errors.New("parent not found") 287 } 288 withdrawals := c.withdrawals.gatherPending(10) 289 return c.sealBlock(withdrawals, parent.Time+uint64(adjustment)) 290 } 291 292 func RegisterSimulatedBeaconAPIs(stack *node.Node, sim *SimulatedBeacon) { 293 api := &api{sim} 294 if sim.period == 0 { 295 // mine on demand if period is set to 0 296 go api.loop() 297 } 298 stack.RegisterAPIs([]rpc.API{ 299 { 300 Namespace: "dev", 301 Service: api, 302 }, 303 }) 304 }