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