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  }