cmd/geth: implement dev mode for post-merge (#27327)
This change adds back the 'geth --dev' mode of operation, using a cl-mocker. --------- Co-authored-by: Martin Holst Swende <martin@swende.se> Co-authored-by: rjl493456442 <garyrong0905@gmail.com> Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com>
This commit is contained in:
co-authored by
Martin Holst Swende
rjl493456442
lightclient
parent
ab0e0f3517
commit
ea782809f7
+20
-9
@@ -407,7 +407,22 @@ func (api *ConsensusAPI) GetPayloadV2(payloadID engine.PayloadID) (*engine.Execu
|
||||
|
||||
func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) {
|
||||
log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID)
|
||||
data := api.localBlocks.get(payloadID)
|
||||
data := api.localBlocks.get(payloadID, false)
|
||||
if data == nil {
|
||||
return nil, engine.UnknownPayload
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// getFullPayload returns a cached payload by it. The difference is that this
|
||||
// function always expects a non-empty payload, but can also return empty one
|
||||
// if no transaction is executable.
|
||||
//
|
||||
// Note, this function is not a part of standard engine API, meant to be used
|
||||
// by consensus client mock in dev mode.
|
||||
func (api *ConsensusAPI) getFullPayload(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) {
|
||||
log.Trace("Engine API request received", "method", "GetFullPayload", "id", payloadID)
|
||||
data := api.localBlocks.get(payloadID, true)
|
||||
if data == nil {
|
||||
return nil, engine.UnknownPayload
|
||||
}
|
||||
@@ -715,8 +730,8 @@ func (api *ConsensusAPI) ExchangeCapabilities([]string) []string {
|
||||
return caps
|
||||
}
|
||||
|
||||
// GetPayloadBodiesV1 implements engine_getPayloadBodiesByHashV1 which allows for retrieval of a list
|
||||
// of block bodies by the engine api.
|
||||
// GetPayloadBodiesByHashV1 implements engine_getPayloadBodiesByHashV1 which
|
||||
// allows for retrieval of a list of block bodies by the engine api.
|
||||
func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engine.ExecutionPayloadBodyV1 {
|
||||
var bodies = make([]*engine.ExecutionPayloadBodyV1, len(hashes))
|
||||
for i, hash := range hashes {
|
||||
@@ -726,8 +741,8 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engin
|
||||
return bodies
|
||||
}
|
||||
|
||||
// GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range
|
||||
// of block bodies by the engine api.
|
||||
// GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which
|
||||
// allows for retrieval of a range of block bodies by the engine api.
|
||||
func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBodyV1, error) {
|
||||
if start == 0 || count == 0 {
|
||||
return nil, engine.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count))
|
||||
@@ -753,23 +768,19 @@ func getBody(block *types.Block) *engine.ExecutionPayloadBodyV1 {
|
||||
if block == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
body = block.Body()
|
||||
txs = make([]hexutil.Bytes, len(body.Transactions))
|
||||
withdrawals = body.Withdrawals
|
||||
)
|
||||
|
||||
for j, tx := range body.Transactions {
|
||||
data, _ := tx.MarshalBinary()
|
||||
txs[j] = hexutil.Bytes(data)
|
||||
}
|
||||
|
||||
// Post-shanghai withdrawals MUST be set to empty slice instead of nil
|
||||
if withdrawals == nil && block.Header().WithdrawalsHash != nil {
|
||||
withdrawals = make([]*types.Withdrawal, 0)
|
||||
}
|
||||
|
||||
return &engine.ExecutionPayloadBodyV1{
|
||||
TransactionData: txs,
|
||||
Withdrawals: withdrawals,
|
||||
|
||||
@@ -73,7 +73,7 @@ func (q *payloadQueue) put(id engine.PayloadID, payload *miner.Payload) {
|
||||
}
|
||||
|
||||
// get retrieves a previously stored payload item or nil if it does not exist.
|
||||
func (q *payloadQueue) get(id engine.PayloadID) *engine.ExecutionPayloadEnvelope {
|
||||
func (q *payloadQueue) get(id engine.PayloadID, full bool) *engine.ExecutionPayloadEnvelope {
|
||||
q.lock.RLock()
|
||||
defer q.lock.RUnlock()
|
||||
|
||||
@@ -82,7 +82,10 @@ func (q *payloadQueue) get(id engine.PayloadID) *engine.ExecutionPayloadEnvelope
|
||||
return nil // no more items
|
||||
}
|
||||
if item.id == id {
|
||||
return item.payload.Resolve()
|
||||
if !full {
|
||||
return item.payload.Resolve()
|
||||
}
|
||||
return item.payload.ResolveFull()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package catalyst
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/engine"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// withdrawalQueue implements a FIFO queue which holds withdrawals that are
|
||||
// pending inclusion.
|
||||
type withdrawalQueue struct {
|
||||
pending chan *types.Withdrawal
|
||||
}
|
||||
|
||||
// add queues a withdrawal for future inclusion.
|
||||
func (w *withdrawalQueue) add(withdrawal *types.Withdrawal) error {
|
||||
select {
|
||||
case w.pending <- withdrawal:
|
||||
break
|
||||
default:
|
||||
return errors.New("withdrawal queue full")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// gatherPending returns a number of queued withdrawals up to a maximum count.
|
||||
func (w *withdrawalQueue) gatherPending(maxCount int) []*types.Withdrawal {
|
||||
withdrawals := []*types.Withdrawal{}
|
||||
for {
|
||||
select {
|
||||
case withdrawal := <-w.pending:
|
||||
withdrawals = append(withdrawals, withdrawal)
|
||||
if len(withdrawals) == maxCount {
|
||||
break
|
||||
}
|
||||
default:
|
||||
return withdrawals
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SimulatedBeacon struct {
|
||||
shutdownCh chan struct{}
|
||||
eth *eth.Ethereum
|
||||
period uint64
|
||||
withdrawals withdrawalQueue
|
||||
|
||||
feeRecipient common.Address
|
||||
feeRecipientLock sync.Mutex // lock gates concurrent access to the feeRecipient
|
||||
|
||||
engineAPI *ConsensusAPI
|
||||
curForkchoiceState engine.ForkchoiceStateV1
|
||||
lastBlockTime uint64
|
||||
}
|
||||
|
||||
func NewSimulatedBeacon(period uint64, eth *eth.Ethereum) (*SimulatedBeacon, error) {
|
||||
chainConfig := eth.APIBackend.ChainConfig()
|
||||
if !chainConfig.IsDevMode {
|
||||
return nil, errors.New("incompatible pre-existing chain configuration")
|
||||
}
|
||||
block := eth.BlockChain().CurrentBlock()
|
||||
current := engine.ForkchoiceStateV1{
|
||||
HeadBlockHash: block.Hash(),
|
||||
SafeBlockHash: block.Hash(),
|
||||
FinalizedBlockHash: block.Hash(),
|
||||
}
|
||||
engineAPI := NewConsensusAPI(eth)
|
||||
|
||||
// if genesis block, send forkchoiceUpdated to trigger transition to PoS
|
||||
if block.Number.Sign() == 0 {
|
||||
if _, err := engineAPI.ForkchoiceUpdatedV2(current, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &SimulatedBeacon{
|
||||
eth: eth,
|
||||
period: period,
|
||||
shutdownCh: make(chan struct{}),
|
||||
engineAPI: engineAPI,
|
||||
lastBlockTime: block.Time,
|
||||
curForkchoiceState: current,
|
||||
withdrawals: withdrawalQueue{make(chan *types.Withdrawal, 20)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *SimulatedBeacon) setFeeRecipient(feeRecipient common.Address) {
|
||||
c.feeRecipientLock.Lock()
|
||||
c.feeRecipient = feeRecipient
|
||||
c.feeRecipientLock.Unlock()
|
||||
}
|
||||
|
||||
// Start invokes the SimulatedBeacon life-cycle function in a goroutine.
|
||||
func (c *SimulatedBeacon) Start() error {
|
||||
if c.period == 0 {
|
||||
go c.loopOnDemand()
|
||||
} else {
|
||||
go c.loop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop halts the SimulatedBeacon service.
|
||||
func (c *SimulatedBeacon) Stop() error {
|
||||
close(c.shutdownCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sealBlock initiates payload building for a new block and creates a new block
|
||||
// with the completed payload.
|
||||
func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal) error {
|
||||
tstamp := uint64(time.Now().Unix())
|
||||
if tstamp <= c.lastBlockTime {
|
||||
tstamp = c.lastBlockTime + 1
|
||||
}
|
||||
c.feeRecipientLock.Lock()
|
||||
feeRecipient := c.feeRecipient
|
||||
c.feeRecipientLock.Unlock()
|
||||
|
||||
fcResponse, err := c.engineAPI.ForkchoiceUpdatedV2(c.curForkchoiceState, &engine.PayloadAttributes{
|
||||
Timestamp: tstamp,
|
||||
SuggestedFeeRecipient: feeRecipient,
|
||||
Withdrawals: withdrawals,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calling forkchoice update: %v", err)
|
||||
}
|
||||
|
||||
envelope, err := c.engineAPI.getFullPayload(*fcResponse.PayloadID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error retrieving payload: %v", err)
|
||||
}
|
||||
payload := envelope.ExecutionPayload
|
||||
|
||||
// mark the payload as canon
|
||||
if _, err = c.engineAPI.NewPayloadV2(*payload); err != nil {
|
||||
return fmt.Errorf("failed to mark payload as canonical: %v", err)
|
||||
}
|
||||
c.curForkchoiceState = engine.ForkchoiceStateV1{
|
||||
HeadBlockHash: payload.BlockHash,
|
||||
SafeBlockHash: payload.BlockHash,
|
||||
FinalizedBlockHash: payload.BlockHash,
|
||||
}
|
||||
// mark the block containing the payload as canonical
|
||||
if _, err = c.engineAPI.ForkchoiceUpdatedV2(c.curForkchoiceState, nil); err != nil {
|
||||
return fmt.Errorf("failed to mark block as canonical: %v", err)
|
||||
}
|
||||
c.lastBlockTime = payload.Timestamp
|
||||
return nil
|
||||
}
|
||||
|
||||
// loopOnDemand runs the block production loop for "on-demand" configuration (period = 0)
|
||||
func (c *SimulatedBeacon) loopOnDemand() {
|
||||
var (
|
||||
newTxs = make(chan core.NewTxsEvent)
|
||||
sub = c.eth.TxPool().SubscribeNewTxsEvent(newTxs)
|
||||
)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.shutdownCh:
|
||||
return
|
||||
case w := <-c.withdrawals.pending:
|
||||
withdrawals := append(c.withdrawals.gatherPending(9), w)
|
||||
if err := c.sealBlock(withdrawals); err != nil {
|
||||
log.Error("Error performing sealing-work", "err", err)
|
||||
return
|
||||
}
|
||||
case <-newTxs:
|
||||
withdrawals := c.withdrawals.gatherPending(10)
|
||||
if err := c.sealBlock(withdrawals); err != nil {
|
||||
log.Error("Error performing sealing-work", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loopOnDemand runs the block production loop for non-zero period configuration
|
||||
func (c *SimulatedBeacon) loop() {
|
||||
timer := time.NewTimer(0)
|
||||
for {
|
||||
select {
|
||||
case <-c.shutdownCh:
|
||||
return
|
||||
case <-timer.C:
|
||||
withdrawals := c.withdrawals.gatherPending(10)
|
||||
if err := c.sealBlock(withdrawals); err != nil {
|
||||
log.Error("Error performing sealing-work", "err", err)
|
||||
return
|
||||
}
|
||||
timer.Reset(time.Second * time.Duration(c.period))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterSimulatedBeaconAPIs(stack *node.Node, sim *SimulatedBeacon) {
|
||||
stack.RegisterAPIs([]rpc.API{
|
||||
{
|
||||
Namespace: "dev",
|
||||
Service: &api{sim},
|
||||
Version: "1.0",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package catalyst
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
type api struct {
|
||||
simBeacon *SimulatedBeacon
|
||||
}
|
||||
|
||||
func (a *api) AddWithdrawal(ctx context.Context, withdrawal *types.Withdrawal) error {
|
||||
return a.simBeacon.withdrawals.add(withdrawal)
|
||||
}
|
||||
|
||||
func (a *api) SetFeeRecipient(ctx context.Context, feeRecipient common.Address) {
|
||||
a.simBeacon.setFeeRecipient(feeRecipient)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package catalyst
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||
"github.com/ethereum/go-ethereum/eth/ethconfig"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
func startSimulatedBeaconEthService(t *testing.T, genesis *core.Genesis) (*node.Node, *eth.Ethereum, *SimulatedBeacon) {
|
||||
t.Helper()
|
||||
|
||||
n, err := node.New(&node.Config{
|
||||
P2P: p2p.Config{
|
||||
ListenAddr: "127.0.0.1:8545",
|
||||
NoDiscovery: true,
|
||||
MaxPeers: 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("can't create node:", err)
|
||||
}
|
||||
|
||||
ethcfg := ðconfig.Config{Genesis: genesis, SyncMode: downloader.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256}
|
||||
ethservice, err := eth.New(n, ethcfg)
|
||||
if err != nil {
|
||||
t.Fatal("can't create eth service:", err)
|
||||
}
|
||||
|
||||
simBeacon, err := NewSimulatedBeacon(1, ethservice)
|
||||
if err != nil {
|
||||
t.Fatal("can't create simulated beacon:", err)
|
||||
}
|
||||
|
||||
n.RegisterLifecycle(simBeacon)
|
||||
|
||||
if err := n.Start(); err != nil {
|
||||
t.Fatal("can't start node:", err)
|
||||
}
|
||||
|
||||
ethservice.SetSynced()
|
||||
return n, ethservice, simBeacon
|
||||
}
|
||||
|
||||
// send 20 transactions, >10 withdrawals and ensure they are included in order
|
||||
// send enough transactions to fill multiple blocks
|
||||
func TestSimulatedBeaconSendWithdrawals(t *testing.T) {
|
||||
var withdrawals []types.Withdrawal
|
||||
txs := make(map[common.Hash]types.Transaction)
|
||||
|
||||
var (
|
||||
// testKey is a private key to use for funding a tester account.
|
||||
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
|
||||
// testAddr is the Ethereum address of the tester account.
|
||||
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
|
||||
)
|
||||
|
||||
// short period (1 second) for testing purposes
|
||||
var gasLimit uint64 = 10_000_000
|
||||
genesis := core.DeveloperGenesisBlock(gasLimit, testAddr)
|
||||
node, ethService, mock := startSimulatedBeaconEthService(t, genesis)
|
||||
_ = mock
|
||||
defer node.Close()
|
||||
|
||||
chainHeadCh := make(chan core.ChainHeadEvent, 10)
|
||||
subscription := ethService.BlockChain().SubscribeChainHeadEvent(chainHeadCh)
|
||||
defer subscription.Unsubscribe()
|
||||
|
||||
// generate some withdrawals
|
||||
for i := 0; i < 20; i++ {
|
||||
withdrawals = append(withdrawals, types.Withdrawal{Index: uint64(i)})
|
||||
if err := mock.withdrawals.add(&withdrawals[i]); err != nil {
|
||||
t.Fatal("addWithdrawal failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
// generate a bunch of transactions
|
||||
signer := types.NewEIP155Signer(ethService.BlockChain().Config().ChainID)
|
||||
for i := 0; i < 20; i++ {
|
||||
tx, err := types.SignTx(types.NewTransaction(uint64(i), common.Address{}, big.NewInt(1000), params.TxGas, big.NewInt(params.InitialBaseFee), nil), signer, testKey)
|
||||
if err != nil {
|
||||
t.Fatalf("error signing transaction, err=%v", err)
|
||||
}
|
||||
txs[tx.Hash()] = *tx
|
||||
|
||||
if err := ethService.APIBackend.SendTx(context.Background(), tx); err != nil {
|
||||
t.Fatal("SendTx failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
includedTxs := make(map[common.Hash]struct{})
|
||||
var includedWithdrawals []uint64
|
||||
|
||||
timer := time.NewTimer(12 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case evt := <-chainHeadCh:
|
||||
for _, includedTx := range evt.Block.Transactions() {
|
||||
includedTxs[includedTx.Hash()] = struct{}{}
|
||||
}
|
||||
for _, includedWithdrawal := range evt.Block.Withdrawals() {
|
||||
includedWithdrawals = append(includedWithdrawals, includedWithdrawal.Index)
|
||||
}
|
||||
|
||||
// ensure all withdrawals/txs included. this will take two blocks b/c number of withdrawals > 10
|
||||
if len(includedTxs) == len(txs) && len(includedWithdrawals) == len(withdrawals) && evt.Block.Number().Cmp(big.NewInt(2)) == 0 {
|
||||
return
|
||||
}
|
||||
case <-timer.C:
|
||||
t.Fatal("timed out without including all withdrawals/txs")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user