diff --git a/clmock/api.go b/clmock/api.go new file mode 100644 index 0000000000000..e09a5147d4f45 --- /dev/null +++ b/clmock/api.go @@ -0,0 +1,22 @@ +package clmock + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +type API struct { + mock *CLMock +} + +func (api *API) AddWithdrawal(ctx context.Context, withdrawal *types.Withdrawal) error { + return api.mock.addWithdrawal(*withdrawal) +} + +func (api *API) SetFeeRecipient(ctx context.Context, feeRecipient *common.Address) { + api.mock.mu.Lock() + api.mock.feeRecipient = *feeRecipient + api.mock.mu.Unlock() +} diff --git a/clmock/clmock.go b/clmock/clmock.go new file mode 100644 index 0000000000000..d20bb6c4eb380 --- /dev/null +++ b/clmock/clmock.go @@ -0,0 +1,222 @@ +// 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 . + +package clmock + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/catalyst" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" +) + +type CLMock struct { + ctx context.Context + cancel context.CancelFunc + eth *eth.Ethereum + period time.Duration + withdrawals []*types.Withdrawal + feeRecipient common.Address + // mu controls access to the feeRecipient/withdrawals which can be modified by the dev-mode RPC API methods + mu sync.Mutex + nextWithdrawalIdx uint64 +} + +func NewCLMock(eth *eth.Ethereum) *CLMock { + chainConfig := eth.APIBackend.ChainConfig() + if chainConfig.Dev == nil { + log.Crit("incompatible pre-existing chain configuration") + } + + return &CLMock{ + eth: eth, + period: time.Duration(chainConfig.Dev.Period) * time.Second, + withdrawals: []*types.Withdrawal{}, + feeRecipient: common.Address{}, + } +} + +// Start invokes the clmock life-cycle function in a goroutine +func (c *CLMock) Start() error { + c.ctx, c.cancel = context.WithCancel(context.Background()) + go c.loop() + return nil +} + +// Stop halts the clmock service +func (c *CLMock) Stop() error { + c.cancel() + return nil +} + +func (c *CLMock) addWithdrawal(w types.Withdrawal) error { + c.mu.Lock() + defer c.mu.Unlock() + + if w.Index < c.nextWithdrawalIdx { + return fmt.Errorf("withdrawal has index (%d) less than or equal to latest received withdrawal index (%d)", w.Index, c.nextWithdrawalIdx-1) + } + c.nextWithdrawalIdx = w.Index + 1 + c.withdrawals = append(c.withdrawals, &w) + return nil +} + +// remove up to 10 withdrawals from the withdrawal queue +func (c *CLMock) popWithdrawals() []*types.Withdrawal { + c.mu.Lock() + defer c.mu.Unlock() + + var popCount int + if len(c.withdrawals) >= 10 { + popCount = 10 + } else { + popCount = len(c.withdrawals) + } + + popped := make([]*types.Withdrawal, popCount) + copy(popped[:], c.withdrawals[0:popCount]) + c.withdrawals = append([]*types.Withdrawal{}, c.withdrawals[popCount:]...) + return popped +} + +// loop manages the lifecycle of clmock. +// it drives block production, taking the role of a CL client and interacting with Geth via public engine/eth APIs +func (c *CLMock) loop() { + var ( + ticker = time.NewTicker(time.Millisecond * 100) + lastBlockTime = time.Now() + engineAPI = catalyst.NewConsensusAPI(c.eth) + header = c.eth.BlockChain().CurrentHeader() + curForkchoiceState = engine.ForkchoiceStateV1{ + HeadBlockHash: header.Hash(), + SafeBlockHash: header.Hash(), + FinalizedBlockHash: header.Hash(), + } + ) + + // if genesis block, send forkchoiceUpdated to trigger transition to PoS + if header.Number.BitLen() == 0 { + if _, err := engineAPI.ForkchoiceUpdatedV2(curForkchoiceState, nil); err != nil { + log.Crit("failed to initiate PoS transition for genesis via Forkchoiceupdated", "err", err) + } + } + + for { + select { + case <-c.ctx.Done(): + break + case curTime := <-ticker.C: + if curTime.Unix() > lastBlockTime.Add(c.period).Unix() { + c.mu.Lock() + feeRecipient := c.feeRecipient + c.mu.Unlock() + + payloadAttr := &engine.PayloadAttributes{ + Timestamp: uint64(curTime.Unix()), + Random: common.Hash{}, // TODO: make this configurable? + SuggestedFeeRecipient: feeRecipient, + Withdrawals: c.popWithdrawals(), + } + + // trigger block building + fcState, err := engineAPI.ForkchoiceUpdatedV2(curForkchoiceState, payloadAttr) + if err != nil { + log.Crit("failed to trigger block building via forkchoiceupdated", "err", err) + } + + var payload *engine.ExecutableData + + var ( + restartPayloadBuilding bool + // building a payload times out after SECONDS_PER_SLOT (12s on mainnet). + // trigger building a new payload if this amount of time elapses w/o any transactions or withdrawals + // having been received. + payloadTimeout = time.NewTimer(12 * time.Second) + // interval to poll the pending state to detect if transactions have arrived, and proceed if they have + // (or if there are pending withdrawals to include) + buildTicker = time.NewTicker(100 * time.Millisecond) + ) + for { + select { + case <-buildTicker.C: + pendingHeader, err := c.eth.APIBackend.HeaderByNumber(context.Background(), rpc.PendingBlockNumber) + if err != nil { + log.Crit("failed to get pending block header", "err", err) + } + // don't build a block if we don't have pending txs or withdrawals + if pendingHeader.TxHash == types.EmptyTxsHash && len(payloadAttr.Withdrawals) == 0 { + continue + } + payload, err = engineAPI.GetPayloadV1(*fcState.PayloadID) + if err != nil { + log.Crit("error retrieving payload", "err", err) + } + // Don't build a block if it doesn't contain transactions or withdrawals. + // Somehow, txs can arrive, be detected by this routine, but be missed by the miner payload builder. + // So this last clause prevents empty blocks from being built. + if len(payload.Transactions) == 0 && len(payloadAttr.Withdrawals) == 0 { + restartPayloadBuilding = true + } + case <-payloadTimeout.C: + restartPayloadBuilding = true + case <-c.ctx.Done(): + return + } + break + } + if restartPayloadBuilding { + continue + } + // mark the payload as the one we have chosen + if _, err = engineAPI.NewPayloadV2(*payload); err != nil { + log.Crit("failed to mark payload as canonical", "err", err) + } + + newForkchoiceState := &engine.ForkchoiceStateV1{ + HeadBlockHash: payload.BlockHash, + SafeBlockHash: payload.BlockHash, + FinalizedBlockHash: payload.BlockHash, + } + // mark the block containing the payload as canonical + _, err = engineAPI.ForkchoiceUpdatedV2(*newForkchoiceState, nil) + if err != nil { + log.Crit("failed to mark block as canonical", "err", err) + } + lastBlockTime = time.Unix(int64(payload.Timestamp), 0) + curForkchoiceState = *newForkchoiceState + } + } + } +} + +func RegisterAPIs(stack *node.Node, c *CLMock) { + stack.RegisterAPIs([]rpc.API{ + { + Namespace: "dev", + Service: &API{c}, + Version: "1.0", + }, + }) +} diff --git a/clmock/clmock_test.go b/clmock/clmock_test.go new file mode 100644 index 0000000000000..3107814f8106c --- /dev/null +++ b/clmock/clmock_test.go @@ -0,0 +1,139 @@ +// 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 . + +package clmock + +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 startEthService(t *testing.T, genesis *core.Genesis) (*node.Node, *eth.Ethereum, *CLMock) { + 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) + } + + clmock := NewCLMock(ethservice) + + n.RegisterLifecycle(clmock) + + if err := n.Start(); err != nil { + t.Fatal("can't start node:", err) + } + + ethservice.SetSynced() + return n, ethservice, clmock +} + +// send 20 transactions, >10 withdrawals and ensure they are included in order +// send enough transactions to fill multiple blocks +func TestCLMockSendWithdrawals(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 + period := 1 + var gasLimit uint64 = 10_000_000 + genesis := core.DeveloperGenesisBlock(uint64(period), gasLimit, testAddr) + node, ethService, mock := startEthService(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.addWithdrawal(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") + } + } +} diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 42ded493224bb..eee3012717584 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet" + "github.com/ethereum/go-ethereum/clmock" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/ethconfig" @@ -189,6 +190,14 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { if ctx.IsSet(utils.SyncTargetFlag.Name) && cfg.Eth.SyncMode == downloader.FullSync { utils.RegisterFullSyncTester(stack, eth, ctx.Path(utils.SyncTargetFlag.Name)) } + + // Start the dev mode if requested + if ctx.IsSet(utils.DeveloperFlag.Name) { + mock := clmock.NewCLMock(eth) + clmock.RegisterAPIs(stack, mock) + stack.RegisterLifecycle(mock) + } + return stack, backend } diff --git a/cmd/geth/consolecmd.go b/cmd/geth/consolecmd.go index aeee6f9c9e404..c7ca963371111 100644 --- a/cmd/geth/consolecmd.go +++ b/cmd/geth/consolecmd.go @@ -72,6 +72,7 @@ func localConsole(ctx *cli.Context) error { prepare(ctx) stack, backend := makeFullNode(ctx) startNode(ctx, stack, backend, true) + defer stack.Close() // Attach to the newly started node and create the JavaScript console. diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 2289a72a197b9..c744c32c64100 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -127,8 +127,8 @@ var ( utils.NodeKeyHexFlag, utils.DNSDiscoveryFlag, utils.DeveloperFlag, - utils.DeveloperPeriodFlag, utils.DeveloperGasLimitFlag, + utils.DeveloperPeriodFlag, utils.VMEnableDebugFlag, utils.NetworkIdFlag, utils.EthStatsURLFlag, @@ -323,6 +323,7 @@ func geth(ctx *cli.Context) error { defer stack.Close() startNode(ctx, stack, backend, false) + stack.Wait() return nil } @@ -408,7 +409,7 @@ func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend, isCon } // 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 if ctx.String(utils.SyncModeFlag.Name) == "light" { utils.Fatalf("Light clients do not support mining") diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 1ebc998a40ed3..0a85e1a2845dc 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -162,6 +162,7 @@ var ( DeveloperPeriodFlag = &cli.IntFlag{ Name: "dev.period", Usage: "Block period to use in developer mode (0 = mine only if transaction pending)", + Value: 12, Category: flags.DevCategory, } DeveloperGasLimitFlag = &cli.Uint64Flag{ diff --git a/core/genesis.go b/core/genesis.go index 1e56845d8ad3b..bdd34b8261b36 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -552,19 +552,17 @@ func DefaultSepoliaGenesisBlock() *Genesis { // DeveloperGenesisBlock returns the 'geth --dev' genesis block. func DeveloperGenesisBlock(period uint64, gasLimit uint64, faucet common.Address) *Genesis { // Override the default period to the user requested one - config := *params.AllCliqueProtocolChanges - config.Clique = ¶ms.CliqueConfig{ + config := *params.AllDevChainProtocolChanges + config.Dev = ¶ms.DeveloperModeConfig{ Period: period, - Epoch: config.Clique.Epoch, } // Assemble and return the genesis with the precompiles and faucet pre-funded return &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), + Difficulty: big.NewInt(0), Alloc: map[common.Address]GenesisAccount{ common.BytesToAddress([]byte{1}): {Balance: big.NewInt(1)}, // ECRecover common.BytesToAddress([]byte{2}): {Balance: big.NewInt(1)}, // SHA256 diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index c171709586e6f..f47564dc855e5 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -30,6 +30,7 @@ var Modules = map[string]string{ "txpool": TxpoolJs, "les": LESJs, "vflux": VfluxJs, + "dev": DevJs, } 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 + }), + ], +}); +` diff --git a/miner/miner_test.go b/miner/miner_test.go index 67d038d684766..585684fd82bdd 100644 --- a/miner/miner_test.go +++ b/miner/miner_test.go @@ -31,8 +31,10 @@ import ( "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "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/event" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" ) @@ -245,6 +247,34 @@ func waitForMiningState(t *testing.T, m *Miner, mining bool) { 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)) { // Create Ethash config config := Config{ @@ -252,7 +282,7 @@ func createMiner(t *testing.T) (*Miner, *event.TypeMux, func(skipMiner bool)) { } // Create chainConfig 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) if err != nil { t.Fatalf("can't create new chain config: %v", err) diff --git a/node/node.go b/node/node.go index e8494ac3b29e1..7ccebe505a8cb 100644 --- a/node/node.go +++ b/node/node.go @@ -484,9 +484,20 @@ func (n *Node) startRPC() error { } // Configure authenticated API if len(openAPIs) != len(allAPIs) { - jwtSecret, err := n.obtainJWTSecret(n.config.JWTSecret) - if err != nil { - return err + var ( + jwtSecret []byte + err error + ) + if n.config.DataDir == "" { + // TODO: make dev-mode and jwt secret flags mutually exclusive + + // in dev-mode we use a preconfigured jwt secret + jwtSecret = make([]byte, 32) + } else { + jwtSecret, err = n.obtainJWTSecret(n.config.JWTSecret) + if err != nil { + return err + } } if err := initAuth(n.config.AuthPort, jwtSecret); err != nil { return err diff --git a/params/config.go b/params/config.go index 455abe206239e..55b81f183945d 100644 --- a/params/config.go +++ b/params/config.go @@ -134,6 +134,26 @@ var ( 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, + } + // AllCliqueProtocolChanges contains every protocol change (EIPs) introduced // and accepted by the Ethereum core developers into the Clique consensus. AllCliqueProtocolChanges = &ChainConfig{ @@ -275,8 +295,9 @@ type ChainConfig struct { TerminalTotalDifficultyPassed bool `json:"terminalTotalDifficultyPassed,omitempty"` // Various consensus engines - Ethash *EthashConfig `json:"ethash,omitempty"` - Clique *CliqueConfig `json:"clique,omitempty"` + Ethash *EthashConfig `json:"ethash,omitempty"` + Clique *CliqueConfig `json:"clique,omitempty"` + Dev *DeveloperModeConfig `json:"dev,omitempty"` } // EthashConfig is the consensus engine configs for proof-of-work based sealing. @@ -298,6 +319,16 @@ func (c *CliqueConfig) String() string { return "clique" } +// DeveloperModeConfig is the genesis config for dev mode +type DeveloperModeConfig struct { + Period uint64 `json:"period"` // Number of seconds between blocks to enforce +} + +// String implements the stringer interface, returning the consensus engine details. +func (c *DeveloperModeConfig) String() string { + return "dev" +} + // Description returns a human-readable description of ChainConfig. func (c *ChainConfig) Description() string { var banner string