Browse Source

eth/catalyst: add catalyst API prototype (#22641)

This change adds the --catalyst flag, enabling an RPC API for eth2 integration.
In this initial version, catalyst mode also disables all peer-to-peer networking.

Co-authored-by: Mikhail Kalinin <noblesse.knight@gmail.com>
Co-authored-by: Felix Lange <fjl@twurst.com>
Guillaume Ballet 4 years ago
parent
commit
f79cce5de9

+ 12 - 1
cmd/geth/config.go

@@ -28,6 +28,7 @@ import (
 	"gopkg.in/urfave/cli.v1"
 
 	"github.com/ethereum/go-ethereum/cmd/utils"
+	"github.com/ethereum/go-ethereum/eth/catalyst"
 	"github.com/ethereum/go-ethereum/eth/ethconfig"
 	"github.com/ethereum/go-ethereum/internal/ethapi"
 	"github.com/ethereum/go-ethereum/metrics"
@@ -143,7 +144,17 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
 	if ctx.GlobalIsSet(utils.OverrideBerlinFlag.Name) {
 		cfg.Eth.OverrideBerlin = new(big.Int).SetUint64(ctx.GlobalUint64(utils.OverrideBerlinFlag.Name))
 	}
-	backend := utils.RegisterEthService(stack, &cfg.Eth)
+	backend, eth := utils.RegisterEthService(stack, &cfg.Eth)
+
+	// Configure catalyst.
+	if ctx.GlobalBool(utils.CatalystFlag.Name) {
+		if eth == nil {
+			utils.Fatalf("Catalyst does not work in light client mode.")
+		}
+		if err := catalyst.Register(stack, eth); err != nil {
+			utils.Fatalf("%v", err)
+		}
+	}
 
 	// Configure GraphQL if requested
 	if ctx.GlobalIsSet(utils.GraphQLEnabledFlag.Name) {

+ 1 - 0
cmd/geth/main.go

@@ -151,6 +151,7 @@ var (
 		utils.EVMInterpreterFlag,
 		utils.MinerNotifyFullFlag,
 		configFileFlag,
+		utils.CatalystFlag,
 	}
 
 	rpcFlags = []cli.Flag{

+ 1 - 0
cmd/geth/usage.go

@@ -235,6 +235,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{
 			utils.SnapshotFlag,
 			utils.BloomFilterSizeFlag,
 			cli.HelpFlag,
+			utils.CatalystFlag,
 		},
 	},
 }

+ 13 - 5
cmd/utils/flags.go

@@ -755,6 +755,11 @@ var (
 		Usage: "External EVM configuration (default = built-in interpreter)",
 		Value: "",
 	}
+
+	CatalystFlag = cli.BoolFlag{
+		Name:  "catalyst",
+		Usage: "Catalyst mode (eth2 integration testing)",
+	}
 )
 
 // MakeDataDir retrieves the currently requested data directory, terminating
@@ -1186,10 +1191,11 @@ func SetP2PConfig(ctx *cli.Context, cfg *p2p.Config) {
 		cfg.NetRestrict = list
 	}
 
-	if ctx.GlobalBool(DeveloperFlag.Name) {
+	if ctx.GlobalBool(DeveloperFlag.Name) || ctx.GlobalBool(CatalystFlag.Name) {
 		// --dev mode can't use p2p networking.
 		cfg.MaxPeers = 0
-		cfg.ListenAddr = ":0"
+		cfg.ListenAddr = ""
+		cfg.NoDial = true
 		cfg.NoDiscovery = true
 		cfg.DiscoveryV5 = false
 	}
@@ -1693,14 +1699,16 @@ func SetDNSDiscoveryDefaults(cfg *ethconfig.Config, genesis common.Hash) {
 }
 
 // RegisterEthService adds an Ethereum client to the stack.
-func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) ethapi.Backend {
+// The second return value is the full node instance, which may be nil if the
+// node is running as a light client.
+func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend, *eth.Ethereum) {
 	if cfg.SyncMode == downloader.LightSync {
 		backend, err := les.New(stack, cfg)
 		if err != nil {
 			Fatalf("Failed to register the Ethereum service: %v", err)
 		}
 		stack.RegisterAPIs(tracers.APIs(backend.ApiBackend))
-		return backend.ApiBackend
+		return backend.ApiBackend, nil
 	}
 	backend, err := eth.New(stack, cfg)
 	if err != nil {
@@ -1713,7 +1721,7 @@ func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) ethapi.Backend
 		}
 	}
 	stack.RegisterAPIs(tracers.APIs(backend.APIBackend))
-	return backend.APIBackend
+	return backend.APIBackend, backend
 }
 
 // RegisterEthStatsService configures the Ethereum Stats daemon and adds it to

+ 2 - 0
consensus/ethash/consensus.go

@@ -315,6 +315,8 @@ func (ethash *Ethash) CalcDifficulty(chain consensus.ChainHeaderReader, time uin
 func CalcDifficulty(config *params.ChainConfig, time uint64, parent *types.Header) *big.Int {
 	next := new(big.Int).Add(parent.Number, big1)
 	switch {
+	case config.IsCatalyst(next):
+		return big.NewInt(1)
 	case config.IsMuirGlacier(next):
 		return calcDifficultyEip2384(time, parent)
 	case config.IsConstantinople(next):

+ 16 - 0
core/blockchain.go

@@ -1693,6 +1693,22 @@ func (bc *BlockChain) InsertChain(chain types.Blocks) (int, error) {
 	return n, err
 }
 
+// InsertChainWithoutSealVerification works exactly the same
+// except for seal verification, seal verification is omitted
+func (bc *BlockChain) InsertChainWithoutSealVerification(block *types.Block) (int, error) {
+	bc.blockProcFeed.Send(true)
+	defer bc.blockProcFeed.Send(false)
+
+	// Pre-checks passed, start the full block imports
+	bc.wg.Add(1)
+	bc.chainmu.Lock()
+	n, err := bc.insertChain(types.Blocks([]*types.Block{block}), false)
+	bc.chainmu.Unlock()
+	bc.wg.Done()
+
+	return n, err
+}
+
 // insertChain is the internal implementation of InsertChain, which assumes that
 // 1) chains are contiguous, and 2) The chain mutex is held.
 //

+ 1 - 0
eth/backend.go

@@ -223,6 +223,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
 	}); err != nil {
 		return nil, err
 	}
+
 	eth.miner = miner.New(eth, &config.Miner, chainConfig, eth.EventMux(), eth.engine, eth.isLocalBlock)
 	eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData))
 

+ 302 - 0
eth/catalyst/api.go

@@ -0,0 +1,302 @@
+// Copyright 2020 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 implements the temporary eth1/eth2 RPC integration.
+package catalyst
+
+import (
+	"errors"
+	"fmt"
+	"math/big"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/core/state"
+	"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"
+	chainParams "github.com/ethereum/go-ethereum/params"
+	"github.com/ethereum/go-ethereum/rpc"
+	"github.com/ethereum/go-ethereum/trie"
+)
+
+// Register adds catalyst APIs to the node.
+func Register(stack *node.Node, backend *eth.Ethereum) error {
+	chainconfig := backend.BlockChain().Config()
+	if chainconfig.CatalystBlock == nil {
+		return errors.New("catalystBlock is not set in genesis config")
+	} else if chainconfig.CatalystBlock.Sign() != 0 {
+		return errors.New("catalystBlock of genesis config must be zero")
+	}
+
+	log.Warn("Catalyst mode enabled")
+	stack.RegisterAPIs([]rpc.API{
+		{
+			Namespace: "consensus",
+			Version:   "1.0",
+			Service:   newConsensusAPI(backend),
+			Public:    true,
+		},
+	})
+	return nil
+}
+
+type consensusAPI struct {
+	eth *eth.Ethereum
+}
+
+func newConsensusAPI(eth *eth.Ethereum) *consensusAPI {
+	return &consensusAPI{eth: eth}
+}
+
+// blockExecutionEnv gathers all the data required to execute
+// a block, either when assembling it or when inserting it.
+type blockExecutionEnv struct {
+	chain   *core.BlockChain
+	state   *state.StateDB
+	tcount  int
+	gasPool *core.GasPool
+
+	header   *types.Header
+	txs      []*types.Transaction
+	receipts []*types.Receipt
+}
+
+func (env *blockExecutionEnv) commitTransaction(tx *types.Transaction, coinbase common.Address) error {
+	vmconfig := *env.chain.GetVMConfig()
+	receipt, err := core.ApplyTransaction(env.chain.Config(), env.chain, &coinbase, env.gasPool, env.state, env.header, tx, &env.header.GasUsed, vmconfig)
+	if err != nil {
+		return err
+	}
+	env.txs = append(env.txs, tx)
+	env.receipts = append(env.receipts, receipt)
+	return nil
+}
+
+func (api *consensusAPI) makeEnv(parent *types.Block, header *types.Header) (*blockExecutionEnv, error) {
+	state, err := api.eth.BlockChain().StateAt(parent.Root())
+	if err != nil {
+		return nil, err
+	}
+	env := &blockExecutionEnv{
+		chain:   api.eth.BlockChain(),
+		state:   state,
+		header:  header,
+		gasPool: new(core.GasPool).AddGas(header.GasLimit),
+	}
+	return env, nil
+}
+
+// AssembleBlock creates a new block, inserts it into the chain, and returns the "execution
+// data" required for eth2 clients to process the new block.
+func (api *consensusAPI) AssembleBlock(params assembleBlockParams) (*executableData, error) {
+	log.Info("Producing block", "parentHash", params.ParentHash)
+
+	bc := api.eth.BlockChain()
+	parent := bc.GetBlockByHash(params.ParentHash)
+	pool := api.eth.TxPool()
+
+	if parent.Time() >= params.Timestamp {
+		return nil, fmt.Errorf("child timestamp lower than parent's: %d >= %d", parent.Time(), params.Timestamp)
+	}
+	if now := uint64(time.Now().Unix()); params.Timestamp > now+1 {
+		wait := time.Duration(params.Timestamp-now) * time.Second
+		log.Info("Producing block too far in the future", "wait", common.PrettyDuration(wait))
+		time.Sleep(wait)
+	}
+
+	pending, err := pool.Pending()
+	if err != nil {
+		return nil, err
+	}
+
+	coinbase, err := api.eth.Etherbase()
+	if err != nil {
+		return nil, err
+	}
+	num := parent.Number()
+	header := &types.Header{
+		ParentHash: parent.Hash(),
+		Number:     num.Add(num, common.Big1),
+		Coinbase:   coinbase,
+		GasLimit:   parent.GasLimit(), // Keep the gas limit constant in this prototype
+		Extra:      []byte{},
+		Time:       params.Timestamp,
+	}
+	err = api.eth.Engine().Prepare(bc, header)
+	if err != nil {
+		return nil, err
+	}
+
+	env, err := api.makeEnv(parent, header)
+	if err != nil {
+		return nil, err
+	}
+
+	var (
+		signer       = types.MakeSigner(bc.Config(), header.Number)
+		txHeap       = types.NewTransactionsByPriceAndNonce(signer, pending)
+		transactions []*types.Transaction
+	)
+	for {
+		if env.gasPool.Gas() < chainParams.TxGas {
+			log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", chainParams.TxGas)
+			break
+		}
+		tx := txHeap.Peek()
+		if tx == nil {
+			break
+		}
+
+		// The sender is only for logging purposes, and it doesn't really matter if it's correct.
+		from, _ := types.Sender(signer, tx)
+
+		// Execute the transaction
+		env.state.Prepare(tx.Hash(), common.Hash{}, env.tcount)
+		err = env.commitTransaction(tx, coinbase)
+		switch err {
+		case core.ErrGasLimitReached:
+			// Pop the current out-of-gas transaction without shifting in the next from the account
+			log.Trace("Gas limit exceeded for current block", "sender", from)
+			txHeap.Pop()
+
+		case core.ErrNonceTooLow:
+			// New head notification data race between the transaction pool and miner, shift
+			log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce())
+			txHeap.Shift()
+
+		case core.ErrNonceTooHigh:
+			// Reorg notification data race between the transaction pool and miner, skip account =
+			log.Trace("Skipping account with high nonce", "sender", from, "nonce", tx.Nonce())
+			txHeap.Pop()
+
+		case nil:
+			// Everything ok, collect the logs and shift in the next transaction from the same account
+			env.tcount++
+			txHeap.Shift()
+			transactions = append(transactions, tx)
+
+		default:
+			// Strange error, discard the transaction and get the next in line (note, the
+			// nonce-too-high clause will prevent us from executing in vain).
+			log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
+			txHeap.Shift()
+		}
+	}
+
+	// Create the block.
+	block, err := api.eth.Engine().FinalizeAndAssemble(bc, header, env.state, transactions, nil /* uncles */, env.receipts)
+	if err != nil {
+		return nil, err
+	}
+	return &executableData{
+		BlockHash:    block.Hash(),
+		ParentHash:   block.ParentHash(),
+		Miner:        block.Coinbase(),
+		StateRoot:    block.Root(),
+		Number:       block.NumberU64(),
+		GasLimit:     block.GasLimit(),
+		GasUsed:      block.GasUsed(),
+		Timestamp:    block.Time(),
+		ReceiptRoot:  block.ReceiptHash(),
+		LogsBloom:    block.Bloom().Bytes(),
+		Transactions: encodeTransactions(block.Transactions()),
+	}, nil
+}
+
+func encodeTransactions(txs []*types.Transaction) [][]byte {
+	var enc = make([][]byte, len(txs))
+	for i, tx := range txs {
+		enc[i], _ = tx.MarshalBinary()
+	}
+	return enc
+}
+
+func decodeTransactions(enc [][]byte) ([]*types.Transaction, error) {
+	var txs = make([]*types.Transaction, len(enc))
+	for i, encTx := range enc {
+		var tx types.Transaction
+		if err := tx.UnmarshalBinary(encTx); err != nil {
+			return nil, fmt.Errorf("invalid transaction %d: %v", i, err)
+		}
+		txs[i] = &tx
+	}
+	return txs, nil
+}
+
+func insertBlockParamsToBlock(params executableData) (*types.Block, error) {
+	txs, err := decodeTransactions(params.Transactions)
+	if err != nil {
+		return nil, err
+	}
+
+	number := big.NewInt(0)
+	number.SetUint64(params.Number)
+	header := &types.Header{
+		ParentHash:  params.ParentHash,
+		UncleHash:   types.EmptyUncleHash,
+		Coinbase:    params.Miner,
+		Root:        params.StateRoot,
+		TxHash:      types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)),
+		ReceiptHash: params.ReceiptRoot,
+		Bloom:       types.BytesToBloom(params.LogsBloom),
+		Difficulty:  big.NewInt(1),
+		Number:      number,
+		GasLimit:    params.GasLimit,
+		GasUsed:     params.GasUsed,
+		Time:        params.Timestamp,
+	}
+	block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */)
+	return block, nil
+}
+
+// NewBlock creates an Eth1 block, inserts it in the chain, and either returns true,
+// or false + an error. This is a bit redundant for go, but simplifies things on the
+// eth2 side.
+func (api *consensusAPI) NewBlock(params executableData) (*newBlockResponse, error) {
+	parent := api.eth.BlockChain().GetBlockByHash(params.ParentHash)
+	if parent == nil {
+		return &newBlockResponse{false}, fmt.Errorf("could not find parent %x", params.ParentHash)
+	}
+	block, err := insertBlockParamsToBlock(params)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = api.eth.BlockChain().InsertChainWithoutSealVerification(block)
+	return &newBlockResponse{err == nil}, err
+}
+
+// Used in tests to add a the list of transactions from a block to the tx pool.
+func (api *consensusAPI) addBlockTxs(block *types.Block) error {
+	for _, tx := range block.Transactions() {
+		api.eth.TxPool().AddLocal(tx)
+	}
+	return nil
+}
+
+// FinalizeBlock is called to mark a block as synchronized, so
+// that data that is no longer needed can be removed.
+func (api *consensusAPI) FinalizeBlock(blockHash common.Hash) (*genericResponse, error) {
+	return &genericResponse{true}, nil
+}
+
+// SetHead is called to perform a force choice.
+func (api *consensusAPI) SetHead(newHead common.Hash) (*genericResponse, error) {
+	return &genericResponse{true}, nil
+}

+ 229 - 0
eth/catalyst/api_test.go

@@ -0,0 +1,229 @@
+// Copyright 2020 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 (
+	"math/big"
+	"testing"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/consensus/ethash"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/core/rawdb"
+	"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/ethconfig"
+	"github.com/ethereum/go-ethereum/node"
+	"github.com/ethereum/go-ethereum/params"
+)
+
+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)
+
+	testBalance = big.NewInt(2e10)
+)
+
+func generateTestChain() (*core.Genesis, []*types.Block) {
+	db := rawdb.NewMemoryDatabase()
+	config := params.AllEthashProtocolChanges
+	genesis := &core.Genesis{
+		Config:    config,
+		Alloc:     core.GenesisAlloc{testAddr: {Balance: testBalance}},
+		ExtraData: []byte("test genesis"),
+		Timestamp: 9000,
+	}
+	generate := func(i int, g *core.BlockGen) {
+		g.OffsetTime(5)
+		g.SetExtra([]byte("test"))
+	}
+	gblock := genesis.ToBlock(db)
+	engine := ethash.NewFaker()
+	blocks, _ := core.GenerateChain(config, gblock, engine, db, 10, generate)
+	blocks = append([]*types.Block{gblock}, blocks...)
+	return genesis, blocks
+}
+
+func generateTestChainWithFork(n int, fork int) (*core.Genesis, []*types.Block, []*types.Block) {
+	if fork >= n {
+		fork = n - 1
+	}
+	db := rawdb.NewMemoryDatabase()
+	//nolint:composites
+	config := &params.ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, big.NewInt(0), new(params.EthashConfig), nil}
+	genesis := &core.Genesis{
+		Config:    config,
+		Alloc:     core.GenesisAlloc{testAddr: {Balance: testBalance}},
+		ExtraData: []byte("test genesis"),
+		Timestamp: 9000,
+	}
+	generate := func(i int, g *core.BlockGen) {
+		g.OffsetTime(5)
+		g.SetExtra([]byte("test"))
+	}
+	generateFork := func(i int, g *core.BlockGen) {
+		g.OffsetTime(5)
+		g.SetExtra([]byte("testF"))
+	}
+	gblock := genesis.ToBlock(db)
+	engine := ethash.NewFaker()
+	blocks, _ := core.GenerateChain(config, gblock, engine, db, n, generate)
+	blocks = append([]*types.Block{gblock}, blocks...)
+	forkedBlocks, _ := core.GenerateChain(config, blocks[fork], engine, db, n-fork, generateFork)
+	return genesis, blocks, forkedBlocks
+}
+
+func TestEth2AssembleBlock(t *testing.T) {
+	genesis, blocks := generateTestChain()
+	n, ethservice := startEthService(t, genesis, blocks[1:9])
+	defer n.Close()
+
+	api := newConsensusAPI(ethservice)
+	signer := types.NewEIP155Signer(ethservice.BlockChain().Config().ChainID)
+	tx, err := types.SignTx(types.NewTransaction(0, blocks[8].Coinbase(), big.NewInt(1000), params.TxGas, nil, nil), signer, testKey)
+	if err != nil {
+		t.Fatalf("error signing transaction, err=%v", err)
+	}
+	ethservice.TxPool().AddLocal(tx)
+	blockParams := assembleBlockParams{
+		ParentHash: blocks[8].ParentHash(),
+		Timestamp:  blocks[8].Time(),
+	}
+	execData, err := api.AssembleBlock(blockParams)
+
+	if err != nil {
+		t.Fatalf("error producing block, err=%v", err)
+	}
+
+	if len(execData.Transactions) != 1 {
+		t.Fatalf("invalid number of transactions %d != 1", len(execData.Transactions))
+	}
+}
+
+func TestEth2AssembleBlockWithAnotherBlocksTxs(t *testing.T) {
+	genesis, blocks := generateTestChain()
+	n, ethservice := startEthService(t, genesis, blocks[1:9])
+	defer n.Close()
+
+	api := newConsensusAPI(ethservice)
+
+	// Put the 10th block's tx in the pool and produce a new block
+	api.addBlockTxs(blocks[9])
+	blockParams := assembleBlockParams{
+		ParentHash: blocks[9].ParentHash(),
+		Timestamp:  blocks[9].Time(),
+	}
+	execData, err := api.AssembleBlock(blockParams)
+	if err != nil {
+		t.Fatalf("error producing block, err=%v", err)
+	}
+
+	if len(execData.Transactions) != blocks[9].Transactions().Len() {
+		t.Fatalf("invalid number of transactions %d != 1", len(execData.Transactions))
+	}
+}
+
+func TestEth2NewBlock(t *testing.T) {
+	genesis, blocks, forkedBlocks := generateTestChainWithFork(10, 4)
+	n, ethservice := startEthService(t, genesis, blocks[1:5])
+	defer n.Close()
+
+	api := newConsensusAPI(ethservice)
+	for i := 5; i < 10; i++ {
+		p := executableData{
+			ParentHash:   ethservice.BlockChain().CurrentBlock().Hash(),
+			Miner:        blocks[i].Coinbase(),
+			StateRoot:    blocks[i].Root(),
+			GasLimit:     blocks[i].GasLimit(),
+			GasUsed:      blocks[i].GasUsed(),
+			Transactions: encodeTransactions(blocks[i].Transactions()),
+			ReceiptRoot:  blocks[i].ReceiptHash(),
+			LogsBloom:    blocks[i].Bloom().Bytes(),
+			BlockHash:    blocks[i].Hash(),
+			Timestamp:    blocks[i].Time(),
+			Number:       uint64(i),
+		}
+		success, err := api.NewBlock(p)
+		if err != nil || !success.Valid {
+			t.Fatalf("Failed to insert block: %v", err)
+		}
+	}
+
+	exp := ethservice.BlockChain().CurrentBlock().Hash()
+
+	// Introduce the fork point.
+	lastBlockNum := blocks[4].Number()
+	lastBlock := blocks[4]
+	for i := 0; i < 4; i++ {
+		lastBlockNum.Add(lastBlockNum, big.NewInt(1))
+		p := executableData{
+			ParentHash:   lastBlock.Hash(),
+			Miner:        forkedBlocks[i].Coinbase(),
+			StateRoot:    forkedBlocks[i].Root(),
+			Number:       lastBlockNum.Uint64(),
+			GasLimit:     forkedBlocks[i].GasLimit(),
+			GasUsed:      forkedBlocks[i].GasUsed(),
+			Transactions: encodeTransactions(blocks[i].Transactions()),
+			ReceiptRoot:  forkedBlocks[i].ReceiptHash(),
+			LogsBloom:    forkedBlocks[i].Bloom().Bytes(),
+			BlockHash:    forkedBlocks[i].Hash(),
+			Timestamp:    forkedBlocks[i].Time(),
+		}
+		success, err := api.NewBlock(p)
+		if err != nil || !success.Valid {
+			t.Fatalf("Failed to insert forked block #%d: %v", i, err)
+		}
+		lastBlock, err = insertBlockParamsToBlock(p)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	if ethservice.BlockChain().CurrentBlock().Hash() != exp {
+		t.Fatalf("Wrong head after inserting fork %x != %x", exp, ethservice.BlockChain().CurrentBlock().Hash())
+	}
+}
+
+// startEthService creates a full node instance for testing.
+func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) {
+	t.Helper()
+
+	n, err := node.New(&node.Config{})
+	if err != nil {
+		t.Fatal("can't create node:", err)
+	}
+
+	ethcfg := &ethconfig.Config{Genesis: genesis, Ethash: ethash.Config{PowMode: ethash.ModeFake}}
+	ethservice, err := eth.New(n, ethcfg)
+	if err != nil {
+		t.Fatal("can't create eth service:", err)
+	}
+	if err := n.Start(); err != nil {
+		t.Fatal("can't start node:", err)
+	}
+	if _, err := ethservice.BlockChain().InsertChain(blocks); err != nil {
+		n.Close()
+		t.Fatal("can't import test blocks:", err)
+	}
+	ethservice.SetEtherbase(testAddr)
+
+	return n, ethservice
+}

+ 70 - 0
eth/catalyst/api_types.go

@@ -0,0 +1,70 @@
+// Copyright 2020 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 (
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+)
+
+//go:generate go run github.com/fjl/gencodec -type assembleBlockParams -field-override assembleBlockParamsMarshaling -out gen_blockparams.go
+
+// Structure described at https://hackmd.io/T9x2mMA4S7us8tJwEB3FDQ
+type assembleBlockParams struct {
+	ParentHash common.Hash `json:"parentHash"    gencodec:"required"`
+	Timestamp  uint64      `json:"timestamp"     gencodec:"required"`
+}
+
+// JSON type overrides for assembleBlockParams.
+type assembleBlockParamsMarshaling struct {
+	Timestamp hexutil.Uint64
+}
+
+//go:generate go run github.com/fjl/gencodec -type executableData -field-override executableDataMarshaling -out gen_ed.go
+
+// Structure described at https://notes.ethereum.org/@n0ble/rayonism-the-merge-spec#Parameters1
+type executableData struct {
+	BlockHash    common.Hash    `json:"blockHash"     gencodec:"required"`
+	ParentHash   common.Hash    `json:"parentHash"    gencodec:"required"`
+	Miner        common.Address `json:"miner"         gencodec:"required"`
+	StateRoot    common.Hash    `json:"stateRoot"     gencodec:"required"`
+	Number       uint64         `json:"number"        gencodec:"required"`
+	GasLimit     uint64         `json:"gasLimit"      gencodec:"required"`
+	GasUsed      uint64         `json:"gasUsed"       gencodec:"required"`
+	Timestamp    uint64         `json:"timestamp"     gencodec:"required"`
+	ReceiptRoot  common.Hash    `json:"receiptsRoot"  gencodec:"required"`
+	LogsBloom    []byte         `json:"logsBloom"     gencodec:"required"`
+	Transactions [][]byte       `json:"transactions"  gencodec:"required"`
+}
+
+// JSON type overrides for executableData.
+type executableDataMarshaling struct {
+	Number       hexutil.Uint64
+	GasLimit     hexutil.Uint64
+	GasUsed      hexutil.Uint64
+	Timestamp    hexutil.Uint64
+	LogsBloom    hexutil.Bytes
+	Transactions []hexutil.Bytes
+}
+
+type newBlockResponse struct {
+	Valid bool `json:"valid"`
+}
+
+type genericResponse struct {
+	Success bool `json:"success"`
+}

+ 46 - 0
eth/catalyst/gen_blockparams.go

@@ -0,0 +1,46 @@
+// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
+
+package catalyst
+
+import (
+	"encoding/json"
+	"errors"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+)
+
+var _ = (*assembleBlockParamsMarshaling)(nil)
+
+// MarshalJSON marshals as JSON.
+func (a assembleBlockParams) MarshalJSON() ([]byte, error) {
+	type assembleBlockParams struct {
+		ParentHash common.Hash    `json:"parentHash"    gencodec:"required"`
+		Timestamp  hexutil.Uint64 `json:"timestamp"     gencodec:"required"`
+	}
+	var enc assembleBlockParams
+	enc.ParentHash = a.ParentHash
+	enc.Timestamp = hexutil.Uint64(a.Timestamp)
+	return json.Marshal(&enc)
+}
+
+// UnmarshalJSON unmarshals from JSON.
+func (a *assembleBlockParams) UnmarshalJSON(input []byte) error {
+	type assembleBlockParams struct {
+		ParentHash *common.Hash    `json:"parentHash"    gencodec:"required"`
+		Timestamp  *hexutil.Uint64 `json:"timestamp"     gencodec:"required"`
+	}
+	var dec assembleBlockParams
+	if err := json.Unmarshal(input, &dec); err != nil {
+		return err
+	}
+	if dec.ParentHash == nil {
+		return errors.New("missing required field 'parentHash' for assembleBlockParams")
+	}
+	a.ParentHash = *dec.ParentHash
+	if dec.Timestamp == nil {
+		return errors.New("missing required field 'timestamp' for assembleBlockParams")
+	}
+	a.Timestamp = uint64(*dec.Timestamp)
+	return nil
+}

+ 117 - 0
eth/catalyst/gen_ed.go

@@ -0,0 +1,117 @@
+// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
+
+package catalyst
+
+import (
+	"encoding/json"
+	"errors"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+)
+
+var _ = (*executableDataMarshaling)(nil)
+
+// MarshalJSON marshals as JSON.
+func (e executableData) MarshalJSON() ([]byte, error) {
+	type executableData struct {
+		BlockHash    common.Hash     `json:"blockHash"     gencodec:"required"`
+		ParentHash   common.Hash     `json:"parentHash"    gencodec:"required"`
+		Miner        common.Address  `json:"miner"         gencodec:"required"`
+		StateRoot    common.Hash     `json:"stateRoot"     gencodec:"required"`
+		Number       hexutil.Uint64  `json:"number"        gencodec:"required"`
+		GasLimit     hexutil.Uint64  `json:"gasLimit"      gencodec:"required"`
+		GasUsed      hexutil.Uint64  `json:"gasUsed"       gencodec:"required"`
+		Timestamp    hexutil.Uint64  `json:"timestamp"     gencodec:"required"`
+		ReceiptRoot  common.Hash     `json:"receiptsRoot"  gencodec:"required"`
+		LogsBloom    hexutil.Bytes   `json:"logsBloom"     gencodec:"required"`
+		Transactions []hexutil.Bytes `json:"transactions"  gencodec:"required"`
+	}
+	var enc executableData
+	enc.BlockHash = e.BlockHash
+	enc.ParentHash = e.ParentHash
+	enc.Miner = e.Miner
+	enc.StateRoot = e.StateRoot
+	enc.Number = hexutil.Uint64(e.Number)
+	enc.GasLimit = hexutil.Uint64(e.GasLimit)
+	enc.GasUsed = hexutil.Uint64(e.GasUsed)
+	enc.Timestamp = hexutil.Uint64(e.Timestamp)
+	enc.ReceiptRoot = e.ReceiptRoot
+	enc.LogsBloom = e.LogsBloom
+	if e.Transactions != nil {
+		enc.Transactions = make([]hexutil.Bytes, len(e.Transactions))
+		for k, v := range e.Transactions {
+			enc.Transactions[k] = v
+		}
+	}
+	return json.Marshal(&enc)
+}
+
+// UnmarshalJSON unmarshals from JSON.
+func (e *executableData) UnmarshalJSON(input []byte) error {
+	type executableData struct {
+		BlockHash    *common.Hash    `json:"blockHash"     gencodec:"required"`
+		ParentHash   *common.Hash    `json:"parentHash"    gencodec:"required"`
+		Miner        *common.Address `json:"miner"         gencodec:"required"`
+		StateRoot    *common.Hash    `json:"stateRoot"     gencodec:"required"`
+		Number       *hexutil.Uint64 `json:"number"        gencodec:"required"`
+		GasLimit     *hexutil.Uint64 `json:"gasLimit"      gencodec:"required"`
+		GasUsed      *hexutil.Uint64 `json:"gasUsed"       gencodec:"required"`
+		Timestamp    *hexutil.Uint64 `json:"timestamp"     gencodec:"required"`
+		ReceiptRoot  *common.Hash    `json:"receiptsRoot"  gencodec:"required"`
+		LogsBloom    *hexutil.Bytes  `json:"logsBloom"     gencodec:"required"`
+		Transactions []hexutil.Bytes `json:"transactions"  gencodec:"required"`
+	}
+	var dec executableData
+	if err := json.Unmarshal(input, &dec); err != nil {
+		return err
+	}
+	if dec.BlockHash == nil {
+		return errors.New("missing required field 'blockHash' for executableData")
+	}
+	e.BlockHash = *dec.BlockHash
+	if dec.ParentHash == nil {
+		return errors.New("missing required field 'parentHash' for executableData")
+	}
+	e.ParentHash = *dec.ParentHash
+	if dec.Miner == nil {
+		return errors.New("missing required field 'miner' for executableData")
+	}
+	e.Miner = *dec.Miner
+	if dec.StateRoot == nil {
+		return errors.New("missing required field 'stateRoot' for executableData")
+	}
+	e.StateRoot = *dec.StateRoot
+	if dec.Number == nil {
+		return errors.New("missing required field 'number' for executableData")
+	}
+	e.Number = uint64(*dec.Number)
+	if dec.GasLimit == nil {
+		return errors.New("missing required field 'gasLimit' for executableData")
+	}
+	e.GasLimit = uint64(*dec.GasLimit)
+	if dec.GasUsed == nil {
+		return errors.New("missing required field 'gasUsed' for executableData")
+	}
+	e.GasUsed = uint64(*dec.GasUsed)
+	if dec.Timestamp == nil {
+		return errors.New("missing required field 'timestamp' for executableData")
+	}
+	e.Timestamp = uint64(*dec.Timestamp)
+	if dec.ReceiptRoot == nil {
+		return errors.New("missing required field 'receiptsRoot' for executableData")
+	}
+	e.ReceiptRoot = *dec.ReceiptRoot
+	if dec.LogsBloom == nil {
+		return errors.New("missing required field 'logsBloom' for executableData")
+	}
+	e.LogsBloom = *dec.LogsBloom
+	if dec.Transactions == nil {
+		return errors.New("missing required field 'transactions' for executableData")
+	}
+	e.Transactions = make([][]byte, len(dec.Transactions))
+	for k, v := range dec.Transactions {
+		e.Transactions[k] = v
+	}
+	return nil
+}

+ 13 - 6
params/config.go

@@ -244,16 +244,16 @@ var (
 	//
 	// This configuration is intentionally not using keyed fields to force anyone
 	// adding flags to the config to also have to set these fields.
-	AllEthashProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, new(EthashConfig), nil}
+	AllEthashProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, new(EthashConfig), nil}
 
 	// AllCliqueProtocolChanges contains every protocol change (EIPs) introduced
 	// and accepted by the Ethereum core developers into the Clique consensus.
 	//
 	// This configuration is intentionally not using keyed fields to force anyone
 	// adding flags to the config to also have to set these fields.
-	AllCliqueProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}}
+	AllCliqueProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}}
 
-	TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, new(EthashConfig), nil}
+	TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, new(EthashConfig), nil}
 	TestRules       = TestChainConfig.Rules(new(big.Int))
 )
 
@@ -326,8 +326,9 @@ type ChainConfig struct {
 	MuirGlacierBlock    *big.Int `json:"muirGlacierBlock,omitempty"`    // Eip-2384 (bomb delay) switch block (nil = no fork, 0 = already activated)
 	BerlinBlock         *big.Int `json:"berlinBlock,omitempty"`         // Berlin switch block (nil = no fork, 0 = already on berlin)
 
-	YoloV3Block *big.Int `json:"yoloV3Block,omitempty"` // YOLO v3: Gas repricings TODO @holiman add EIP references
-	EWASMBlock  *big.Int `json:"ewasmBlock,omitempty"`  // EWASM switch block (nil = no fork, 0 = already activated)
+	YoloV3Block   *big.Int `json:"yoloV3Block,omitempty"`   // YOLO v3: Gas repricings TODO @holiman add EIP references
+	EWASMBlock    *big.Int `json:"ewasmBlock,omitempty"`    // EWASM switch block (nil = no fork, 0 = already activated)
+	CatalystBlock *big.Int `json:"catalystBlock,omitempty"` // Catalyst switch block (nil = no fork, 0 = already on catalyst)
 
 	// Various consensus engines
 	Ethash *EthashConfig `json:"ethash,omitempty"`
@@ -440,6 +441,11 @@ func (c *ChainConfig) IsBerlin(num *big.Int) bool {
 	return isForked(c.BerlinBlock, num) || isForked(c.YoloV3Block, num)
 }
 
+// IsCatalyst returns whether num is either equal to the Merge fork block or greater.
+func (c *ChainConfig) IsCatalyst(num *big.Int) bool {
+	return isForked(c.CatalystBlock, num)
+}
+
 // IsEWASM returns whether num represents a block number after the EWASM fork
 func (c *ChainConfig) IsEWASM(num *big.Int) bool {
 	return isForked(c.EWASMBlock, num)
@@ -623,7 +629,7 @@ type Rules struct {
 	ChainID                                                 *big.Int
 	IsHomestead, IsEIP150, IsEIP155, IsEIP158               bool
 	IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool
-	IsBerlin                                                bool
+	IsBerlin, IsCatalyst                                    bool
 }
 
 // Rules ensures c's ChainID is not nil.
@@ -643,5 +649,6 @@ func (c *ChainConfig) Rules(num *big.Int) Rules {
 		IsPetersburg:     c.IsPetersburg(num),
 		IsIstanbul:       c.IsIstanbul(num),
 		IsBerlin:         c.IsBerlin(num),
+		IsCatalyst:       c.IsCatalyst(num),
 	}
 }