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  }