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:
parent
ab0e0f3517
commit
ea782809f7
@ -32,6 +32,8 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/accounts/scwallet"
|
"github.com/ethereum/go-ethereum/accounts/scwallet"
|
||||||
"github.com/ethereum/go-ethereum/accounts/usbwallet"
|
"github.com/ethereum/go-ethereum/accounts/usbwallet"
|
||||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
|
"github.com/ethereum/go-ethereum/eth/catalyst"
|
||||||
|
ethcatalyst "github.com/ethereum/go-ethereum/eth/catalyst"
|
||||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||||
"github.com/ethereum/go-ethereum/eth/ethconfig"
|
"github.com/ethereum/go-ethereum/eth/ethconfig"
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||||
@ -193,6 +195,22 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
|
|||||||
if ctx.IsSet(utils.SyncTargetFlag.Name) && cfg.Eth.SyncMode == downloader.FullSync {
|
if ctx.IsSet(utils.SyncTargetFlag.Name) && cfg.Eth.SyncMode == downloader.FullSync {
|
||||||
utils.RegisterFullSyncTester(stack, eth, ctx.Path(utils.SyncTargetFlag.Name))
|
utils.RegisterFullSyncTester(stack, eth, ctx.Path(utils.SyncTargetFlag.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the dev mode if requested, or launch the engine API for
|
||||||
|
// interacting with external consensus client.
|
||||||
|
if ctx.IsSet(utils.DeveloperFlag.Name) {
|
||||||
|
simBeacon, err := catalyst.NewSimulatedBeacon(ctx.Uint64(utils.DeveloperPeriodFlag.Name), eth)
|
||||||
|
if err != nil {
|
||||||
|
utils.Fatalf("failed to register dev mode catalyst service: %v", err)
|
||||||
|
}
|
||||||
|
catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon)
|
||||||
|
stack.RegisterLifecycle(simBeacon)
|
||||||
|
} else if cfg.Eth.SyncMode != downloader.LightSync {
|
||||||
|
err := ethcatalyst.Register(stack, eth)
|
||||||
|
if err != nil {
|
||||||
|
utils.Fatalf("failed to register catalyst service: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return stack, backend
|
return stack, backend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,8 +128,8 @@ var (
|
|||||||
utils.NodeKeyHexFlag,
|
utils.NodeKeyHexFlag,
|
||||||
utils.DNSDiscoveryFlag,
|
utils.DNSDiscoveryFlag,
|
||||||
utils.DeveloperFlag,
|
utils.DeveloperFlag,
|
||||||
utils.DeveloperPeriodFlag,
|
|
||||||
utils.DeveloperGasLimitFlag,
|
utils.DeveloperGasLimitFlag,
|
||||||
|
utils.DeveloperPeriodFlag,
|
||||||
utils.VMEnableDebugFlag,
|
utils.VMEnableDebugFlag,
|
||||||
utils.NetworkIdFlag,
|
utils.NetworkIdFlag,
|
||||||
utils.EthStatsURLFlag,
|
utils.EthStatsURLFlag,
|
||||||
@ -408,7 +408,7 @@ func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend, isCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start auxiliary services if enabled
|
// Start auxiliary services if enabled
|
||||||
if ctx.Bool(utils.MiningEnabledFlag.Name) || ctx.Bool(utils.DeveloperFlag.Name) {
|
if ctx.Bool(utils.MiningEnabledFlag.Name) {
|
||||||
// Mining only makes sense if a full Ethereum node is running
|
// Mining only makes sense if a full Ethereum node is running
|
||||||
if ctx.String(utils.SyncModeFlag.Name) == "light" {
|
if ctx.String(utils.SyncModeFlag.Name) == "light" {
|
||||||
utils.Fatalf("Light clients do not support mining")
|
utils.Fatalf("Light clients do not support mining")
|
||||||
|
@ -160,7 +160,7 @@ var (
|
|||||||
Usage: "Ephemeral proof-of-authority network with a pre-funded developer account, mining enabled",
|
Usage: "Ephemeral proof-of-authority network with a pre-funded developer account, mining enabled",
|
||||||
Category: flags.DevCategory,
|
Category: flags.DevCategory,
|
||||||
}
|
}
|
||||||
DeveloperPeriodFlag = &cli.IntFlag{
|
DeveloperPeriodFlag = &cli.Uint64Flag{
|
||||||
Name: "dev.period",
|
Name: "dev.period",
|
||||||
Usage: "Block period to use in developer mode (0 = mine only if transaction pending)",
|
Usage: "Block period to use in developer mode (0 = mine only if transaction pending)",
|
||||||
Category: flags.DevCategory,
|
Category: flags.DevCategory,
|
||||||
@ -1817,7 +1817,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
|
|||||||
log.Info("Using developer account", "address", developer.Address)
|
log.Info("Using developer account", "address", developer.Address)
|
||||||
|
|
||||||
// Create a new developer genesis block or reuse existing one
|
// Create a new developer genesis block or reuse existing one
|
||||||
cfg.Genesis = core.DeveloperGenesisBlock(uint64(ctx.Int(DeveloperPeriodFlag.Name)), ctx.Uint64(DeveloperGasLimitFlag.Name), developer.Address)
|
cfg.Genesis = core.DeveloperGenesisBlock(ctx.Uint64(DeveloperGasLimitFlag.Name), developer.Address)
|
||||||
if ctx.IsSet(DataDirFlag.Name) {
|
if ctx.IsSet(DataDirFlag.Name) {
|
||||||
// If datadir doesn't exist we need to open db in write-mode
|
// If datadir doesn't exist we need to open db in write-mode
|
||||||
// so leveldb can create files.
|
// so leveldb can create files.
|
||||||
@ -1892,9 +1892,6 @@ func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend
|
|||||||
Fatalf("Failed to create the LES server: %v", err)
|
Fatalf("Failed to create the LES server: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := ethcatalyst.Register(stack, backend); err != nil {
|
|
||||||
Fatalf("Failed to register the Engine API service: %v", err)
|
|
||||||
}
|
|
||||||
stack.RegisterAPIs(tracers.APIs(backend.APIBackend))
|
stack.RegisterAPIs(tracers.APIs(backend.APIBackend))
|
||||||
return backend.APIBackend, backend
|
return backend.APIBackend, backend
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester {
|
|||||||
t.Fatalf("failed to create node: %v", err)
|
t.Fatalf("failed to create node: %v", err)
|
||||||
}
|
}
|
||||||
ethConf := ðconfig.Config{
|
ethConf := ðconfig.Config{
|
||||||
Genesis: core.DeveloperGenesisBlock(15, 11_500_000, common.Address{}),
|
Genesis: core.DeveloperGenesisBlock(11_500_000, common.Address{}),
|
||||||
Miner: miner.Config{
|
Miner: miner.Config{
|
||||||
Etherbase: common.HexToAddress(testAddress),
|
Etherbase: common.HexToAddress(testAddress),
|
||||||
},
|
},
|
||||||
|
@ -554,21 +554,16 @@ func DefaultSepoliaGenesisBlock() *Genesis {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeveloperGenesisBlock returns the 'geth --dev' genesis block.
|
// DeveloperGenesisBlock returns the 'geth --dev' genesis block.
|
||||||
func DeveloperGenesisBlock(period uint64, gasLimit uint64, faucet common.Address) *Genesis {
|
func DeveloperGenesisBlock(gasLimit uint64, faucet common.Address) *Genesis {
|
||||||
// Override the default period to the user requested one
|
// Override the default period to the user requested one
|
||||||
config := *params.AllCliqueProtocolChanges
|
config := *params.AllDevChainProtocolChanges
|
||||||
config.Clique = ¶ms.CliqueConfig{
|
|
||||||
Period: period,
|
|
||||||
Epoch: config.Clique.Epoch,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assemble and return the genesis with the precompiles and faucet pre-funded
|
// Assemble and return the genesis with the precompiles and faucet pre-funded
|
||||||
return &Genesis{
|
return &Genesis{
|
||||||
Config: &config,
|
Config: &config,
|
||||||
ExtraData: append(append(make([]byte, 32), faucet[:]...), make([]byte, crypto.SignatureLength)...),
|
|
||||||
GasLimit: gasLimit,
|
GasLimit: gasLimit,
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
BaseFee: big.NewInt(params.InitialBaseFee),
|
||||||
Difficulty: big.NewInt(1),
|
Difficulty: big.NewInt(0),
|
||||||
Alloc: map[common.Address]GenesisAccount{
|
Alloc: map[common.Address]GenesisAccount{
|
||||||
common.BytesToAddress([]byte{1}): {Balance: big.NewInt(1)}, // ECRecover
|
common.BytesToAddress([]byte{1}): {Balance: big.NewInt(1)}, // ECRecover
|
||||||
common.BytesToAddress([]byte{2}): {Balance: big.NewInt(1)}, // SHA256
|
common.BytesToAddress([]byte{2}): {Balance: big.NewInt(1)}, // SHA256
|
||||||
|
@ -407,7 +407,22 @@ func (api *ConsensusAPI) GetPayloadV2(payloadID engine.PayloadID) (*engine.Execu
|
|||||||
|
|
||||||
func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) {
|
func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) {
|
||||||
log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID)
|
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 {
|
if data == nil {
|
||||||
return nil, engine.UnknownPayload
|
return nil, engine.UnknownPayload
|
||||||
}
|
}
|
||||||
@ -715,8 +730,8 @@ func (api *ConsensusAPI) ExchangeCapabilities([]string) []string {
|
|||||||
return caps
|
return caps
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPayloadBodiesV1 implements engine_getPayloadBodiesByHashV1 which allows for retrieval of a list
|
// GetPayloadBodiesByHashV1 implements engine_getPayloadBodiesByHashV1 which
|
||||||
// of block bodies by the engine api.
|
// allows for retrieval of a list of block bodies by the engine api.
|
||||||
func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engine.ExecutionPayloadBodyV1 {
|
func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engine.ExecutionPayloadBodyV1 {
|
||||||
var bodies = make([]*engine.ExecutionPayloadBodyV1, len(hashes))
|
var bodies = make([]*engine.ExecutionPayloadBodyV1, len(hashes))
|
||||||
for i, hash := range hashes {
|
for i, hash := range hashes {
|
||||||
@ -726,8 +741,8 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engin
|
|||||||
return bodies
|
return bodies
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range
|
// GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which
|
||||||
// of block bodies by the engine api.
|
// allows for retrieval of a range of block bodies by the engine api.
|
||||||
func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBodyV1, error) {
|
func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBodyV1, error) {
|
||||||
if start == 0 || count == 0 {
|
if start == 0 || count == 0 {
|
||||||
return nil, engine.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count))
|
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 {
|
if block == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
body = block.Body()
|
body = block.Body()
|
||||||
txs = make([]hexutil.Bytes, len(body.Transactions))
|
txs = make([]hexutil.Bytes, len(body.Transactions))
|
||||||
withdrawals = body.Withdrawals
|
withdrawals = body.Withdrawals
|
||||||
)
|
)
|
||||||
|
|
||||||
for j, tx := range body.Transactions {
|
for j, tx := range body.Transactions {
|
||||||
data, _ := tx.MarshalBinary()
|
data, _ := tx.MarshalBinary()
|
||||||
txs[j] = hexutil.Bytes(data)
|
txs[j] = hexutil.Bytes(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post-shanghai withdrawals MUST be set to empty slice instead of nil
|
// Post-shanghai withdrawals MUST be set to empty slice instead of nil
|
||||||
if withdrawals == nil && block.Header().WithdrawalsHash != nil {
|
if withdrawals == nil && block.Header().WithdrawalsHash != nil {
|
||||||
withdrawals = make([]*types.Withdrawal, 0)
|
withdrawals = make([]*types.Withdrawal, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &engine.ExecutionPayloadBodyV1{
|
return &engine.ExecutionPayloadBodyV1{
|
||||||
TransactionData: txs,
|
TransactionData: txs,
|
||||||
Withdrawals: withdrawals,
|
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.
|
// 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()
|
q.lock.RLock()
|
||||||
defer q.lock.RUnlock()
|
defer q.lock.RUnlock()
|
||||||
|
|
||||||
@ -82,8 +82,11 @@ func (q *payloadQueue) get(id engine.PayloadID) *engine.ExecutionPayloadEnvelope
|
|||||||
return nil // no more items
|
return nil // no more items
|
||||||
}
|
}
|
||||||
if item.id == id {
|
if item.id == id {
|
||||||
|
if !full {
|
||||||
return item.payload.Resolve()
|
return item.payload.Resolve()
|
||||||
}
|
}
|
||||||
|
return item.payload.ResolveFull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
231
eth/catalyst/simulated_beacon.go
Normal file
231
eth/catalyst/simulated_beacon.go
Normal file
@ -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",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
36
eth/catalyst/simulated_beacon_api.go
Normal file
36
eth/catalyst/simulated_beacon_api.go
Normal file
@ -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)
|
||||||
|
}
|
141
eth/catalyst/simulated_beacon_test.go
Normal file
141
eth/catalyst/simulated_beacon_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,7 @@ var Modules = map[string]string{
|
|||||||
"txpool": TxpoolJs,
|
"txpool": TxpoolJs,
|
||||||
"les": LESJs,
|
"les": LESJs,
|
||||||
"vflux": VfluxJs,
|
"vflux": VfluxJs,
|
||||||
|
"dev": DevJs,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CliqueJs = `
|
const CliqueJs = `
|
||||||
@ -886,3 +887,22 @@ web3._extend({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const DevJs = `
|
||||||
|
web3._extend({
|
||||||
|
property: 'dev',
|
||||||
|
methods:
|
||||||
|
[
|
||||||
|
new web3._extend.Method({
|
||||||
|
name: 'addWithdrawal',
|
||||||
|
call: 'dev_addWithdrawal',
|
||||||
|
params: 1
|
||||||
|
}),
|
||||||
|
new web3._extend.Method({
|
||||||
|
name: 'setFeeRecipient',
|
||||||
|
call: 'dev_setFeeRecipient',
|
||||||
|
params: 1
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
`
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/core/txpool/legacypool"
|
"github.com/ethereum/go-ethereum/core/txpool/legacypool"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
@ -252,6 +253,34 @@ func waitForMiningState(t *testing.T, m *Miner, mining bool) {
|
|||||||
t.Fatalf("Mining() == %t, want %t", state, mining)
|
t.Fatalf("Mining() == %t, want %t", state, mining)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func minerTestGenesisBlock(period uint64, gasLimit uint64, faucet common.Address) *core.Genesis {
|
||||||
|
config := *params.AllCliqueProtocolChanges
|
||||||
|
config.Clique = ¶ms.CliqueConfig{
|
||||||
|
Period: period,
|
||||||
|
Epoch: config.Clique.Epoch,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble and return the genesis with the precompiles and faucet pre-funded
|
||||||
|
return &core.Genesis{
|
||||||
|
Config: &config,
|
||||||
|
ExtraData: append(append(make([]byte, 32), faucet[:]...), make([]byte, crypto.SignatureLength)...),
|
||||||
|
GasLimit: gasLimit,
|
||||||
|
BaseFee: big.NewInt(params.InitialBaseFee),
|
||||||
|
Difficulty: big.NewInt(1),
|
||||||
|
Alloc: map[common.Address]core.GenesisAccount{
|
||||||
|
common.BytesToAddress([]byte{1}): {Balance: big.NewInt(1)}, // ECRecover
|
||||||
|
common.BytesToAddress([]byte{2}): {Balance: big.NewInt(1)}, // SHA256
|
||||||
|
common.BytesToAddress([]byte{3}): {Balance: big.NewInt(1)}, // RIPEMD
|
||||||
|
common.BytesToAddress([]byte{4}): {Balance: big.NewInt(1)}, // Identity
|
||||||
|
common.BytesToAddress([]byte{5}): {Balance: big.NewInt(1)}, // ModExp
|
||||||
|
common.BytesToAddress([]byte{6}): {Balance: big.NewInt(1)}, // ECAdd
|
||||||
|
common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul
|
||||||
|
common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing
|
||||||
|
common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b
|
||||||
|
faucet: {Balance: new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(9))},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
func createMiner(t *testing.T) (*Miner, *event.TypeMux, func(skipMiner bool)) {
|
func createMiner(t *testing.T) (*Miner, *event.TypeMux, func(skipMiner bool)) {
|
||||||
// Create Ethash config
|
// Create Ethash config
|
||||||
config := Config{
|
config := Config{
|
||||||
@ -259,7 +288,7 @@ func createMiner(t *testing.T) (*Miner, *event.TypeMux, func(skipMiner bool)) {
|
|||||||
}
|
}
|
||||||
// Create chainConfig
|
// Create chainConfig
|
||||||
chainDB := rawdb.NewMemoryDatabase()
|
chainDB := rawdb.NewMemoryDatabase()
|
||||||
genesis := core.DeveloperGenesisBlock(15, 11_500_000, common.HexToAddress("12345"))
|
genesis := minerTestGenesisBlock(15, 11_500_000, common.HexToAddress("12345"))
|
||||||
chainConfig, _, err := core.SetupGenesisBlock(chainDB, trie.NewDatabase(chainDB), genesis)
|
chainConfig, _, err := core.SetupGenesisBlock(chainDB, trie.NewDatabase(chainDB), genesis)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("can't create new chain config: %v", err)
|
t.Fatalf("can't create new chain config: %v", err)
|
||||||
|
@ -135,7 +135,7 @@ func (payload *Payload) ResolveEmpty() *engine.ExecutionPayloadEnvelope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResolveFull is basically identical to Resolve, but it expects full block only.
|
// ResolveFull is basically identical to Resolve, but it expects full block only.
|
||||||
// It's only used in tests.
|
// Don't call Resolve until ResolveFull returns, otherwise it might block forever.
|
||||||
func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope {
|
func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope {
|
||||||
payload.lock.Lock()
|
payload.lock.Lock()
|
||||||
defer payload.lock.Unlock()
|
defer payload.lock.Unlock()
|
||||||
@ -146,8 +146,17 @@ func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope {
|
|||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
// Wait the full payload construction. Note it might block
|
||||||
|
// forever if Resolve is called in the meantime which
|
||||||
|
// terminates the background construction process.
|
||||||
payload.cond.Wait()
|
payload.cond.Wait()
|
||||||
}
|
}
|
||||||
|
// Terminate the background payload construction
|
||||||
|
select {
|
||||||
|
case <-payload.stop:
|
||||||
|
default:
|
||||||
|
close(payload.stop)
|
||||||
|
}
|
||||||
return engine.BlockToExecutableData(payload.full, payload.fullFees)
|
return engine.BlockToExecutableData(payload.full, payload.fullFees)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,6 +135,27 @@ var (
|
|||||||
Clique: nil,
|
Clique: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AllDevChainProtocolChanges = &ChainConfig{
|
||||||
|
ChainID: big.NewInt(1337),
|
||||||
|
HomesteadBlock: big.NewInt(0),
|
||||||
|
EIP150Block: big.NewInt(0),
|
||||||
|
EIP155Block: big.NewInt(0),
|
||||||
|
EIP158Block: big.NewInt(0),
|
||||||
|
ByzantiumBlock: big.NewInt(0),
|
||||||
|
ConstantinopleBlock: big.NewInt(0),
|
||||||
|
PetersburgBlock: big.NewInt(0),
|
||||||
|
IstanbulBlock: big.NewInt(0),
|
||||||
|
MuirGlacierBlock: big.NewInt(0),
|
||||||
|
BerlinBlock: big.NewInt(0),
|
||||||
|
LondonBlock: big.NewInt(0),
|
||||||
|
ArrowGlacierBlock: big.NewInt(0),
|
||||||
|
GrayGlacierBlock: big.NewInt(0),
|
||||||
|
ShanghaiTime: newUint64(0),
|
||||||
|
TerminalTotalDifficulty: big.NewInt(0),
|
||||||
|
TerminalTotalDifficultyPassed: true,
|
||||||
|
IsDevMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
// AllCliqueProtocolChanges contains every protocol change (EIPs) introduced
|
// AllCliqueProtocolChanges contains every protocol change (EIPs) introduced
|
||||||
// and accepted by the Ethereum core developers into the Clique consensus.
|
// and accepted by the Ethereum core developers into the Clique consensus.
|
||||||
AllCliqueProtocolChanges = &ChainConfig{
|
AllCliqueProtocolChanges = &ChainConfig{
|
||||||
@ -282,6 +303,7 @@ type ChainConfig struct {
|
|||||||
// Various consensus engines
|
// Various consensus engines
|
||||||
Ethash *EthashConfig `json:"ethash,omitempty"`
|
Ethash *EthashConfig `json:"ethash,omitempty"`
|
||||||
Clique *CliqueConfig `json:"clique,omitempty"`
|
Clique *CliqueConfig `json:"clique,omitempty"`
|
||||||
|
IsDevMode bool `json:"isDev,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EthashConfig is the consensus engine configs for proof-of-work based sealing.
|
// EthashConfig is the consensus engine configs for proof-of-work based sealing.
|
||||||
|
Loading…
Reference in New Issue
Block a user