From 9da25c5db79afdfbbae8654144f3426777538b53 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Mon, 31 Jan 2022 20:22:35 +0800 Subject: [PATCH] all: separate catalyst package (#24280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * all: seperate catalyst package * eth/catalyst: moved some methods, added docs * eth/catalyst, les/catalyst: add method docs * core, eth, les, miner: move common function to beacon package * eth/catalyst: goimported * cmd/utils, miner/stress/beacon: naming nitpicks Co-authored-by: Marius van der Wijden Co-authored-by: Péter Szilágyi --- cmd/utils/flags.go | 7 +- core/beacon/errors.go | 29 ++ .../beacon}/gen_blockparams.go | 2 +- {eth/catalyst => core/beacon}/gen_ed.go | 2 +- .../api_types.go => core/beacon/types.go | 85 ++++- eth/catalyst/api.go | 317 ++++-------------- eth/catalyst/api_test.go | 53 +-- eth/catalyst/queue.go | 14 +- les/catalyst/api.go | 178 ++++++++++ les/catalyst/api_test.go | 244 ++++++++++++++ miner/stress/beacon/main.go | 90 +++-- 11 files changed, 701 insertions(+), 320 deletions(-) create mode 100644 core/beacon/errors.go rename {eth/catalyst => core/beacon}/gen_blockparams.go (99%) rename {eth/catalyst => core/beacon}/gen_ed.go (99%) rename eth/catalyst/api_types.go => core/beacon/types.go (60%) create mode 100644 les/catalyst/api.go create mode 100644 les/catalyst/api_test.go diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 57d9b87958..586eeca400 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -45,7 +45,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" - "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/ethconfig" "github.com/ethereum/go-ethereum/eth/gasprice" @@ -56,6 +56,7 @@ import ( "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/flags" "github.com/ethereum/go-ethereum/les" + lescatalyst "github.com/ethereum/go-ethereum/les/catalyst" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics/exp" @@ -1724,7 +1725,7 @@ func RegisterEthService(stack *node.Node, cfg *ethconfig.Config, isCatalyst bool } stack.RegisterAPIs(tracers.APIs(backend.ApiBackend)) if isCatalyst { - if err := catalyst.RegisterLight(stack, backend); err != nil { + if err := lescatalyst.Register(stack, backend); err != nil { Fatalf("Failed to register the catalyst service: %v", err) } } @@ -1741,7 +1742,7 @@ func RegisterEthService(stack *node.Node, cfg *ethconfig.Config, isCatalyst bool } } if isCatalyst { - if err := catalyst.Register(stack, backend); err != nil { + if err := ethcatalyst.Register(stack, backend); err != nil { Fatalf("Failed to register the catalyst service: %v", err) } } diff --git a/core/beacon/errors.go b/core/beacon/errors.go new file mode 100644 index 0000000000..5b95c38a23 --- /dev/null +++ b/core/beacon/errors.go @@ -0,0 +1,29 @@ +// Copyright 2022 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 beacon + +import "github.com/ethereum/go-ethereum/rpc" + +var ( + VALID = GenericStringResponse{"VALID"} + SUCCESS = GenericStringResponse{"SUCCESS"} + INVALID = ForkChoiceResponse{Status: "INVALID", PayloadID: nil} + SYNCING = ForkChoiceResponse{Status: "SYNCING", PayloadID: nil} + GenericServerError = rpc.CustomError{Code: -32000, ValidationError: "Server error"} + UnknownPayload = rpc.CustomError{Code: -32001, ValidationError: "Unknown payload"} + InvalidTB = rpc.CustomError{Code: -32002, ValidationError: "Invalid terminal block"} +) diff --git a/eth/catalyst/gen_blockparams.go b/core/beacon/gen_blockparams.go similarity index 99% rename from eth/catalyst/gen_blockparams.go rename to core/beacon/gen_blockparams.go index ccf5c327ff..d3d569b7da 100644 --- a/eth/catalyst/gen_blockparams.go +++ b/core/beacon/gen_blockparams.go @@ -1,6 +1,6 @@ // Code generated by github.com/fjl/gencodec. DO NOT EDIT. -package catalyst +package beacon import ( "encoding/json" diff --git a/eth/catalyst/gen_ed.go b/core/beacon/gen_ed.go similarity index 99% rename from eth/catalyst/gen_ed.go rename to core/beacon/gen_ed.go index 46eb45808b..ac94f49a56 100644 --- a/eth/catalyst/gen_ed.go +++ b/core/beacon/gen_ed.go @@ -1,6 +1,6 @@ // Code generated by github.com/fjl/gencodec. DO NOT EDIT. -package catalyst +package beacon import ( "encoding/json" diff --git a/eth/catalyst/api_types.go b/core/beacon/types.go similarity index 60% rename from eth/catalyst/api_types.go rename to core/beacon/types.go index 07636239fa..d7f6ba535e 100644 --- a/eth/catalyst/api_types.go +++ b/core/beacon/types.go @@ -1,4 +1,4 @@ -// Copyright 2020 The go-ethereum Authors +// Copyright 2022 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 @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package catalyst +package beacon import ( "fmt" @@ -22,6 +22,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" ) //go:generate go run github.com/fjl/gencodec -type PayloadAttributesV1 -field-override payloadAttributesMarshaling -out gen_blockparams.go @@ -121,3 +123,82 @@ type ForkchoiceStateV1 struct { SafeBlockHash common.Hash `json:"safeBlockHash"` FinalizedBlockHash common.Hash `json:"finalizedBlockHash"` } + +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 +} + +// ExecutableDataToBlock constructs a block from executable data. +// It verifies that the following fields: +// len(extraData) <= 32 +// uncleHash = emptyUncleHash +// difficulty = 0 +// and that the blockhash of the constructed block matches the parameters. +func ExecutableDataToBlock(params ExecutableDataV1) (*types.Block, error) { + txs, err := decodeTransactions(params.Transactions) + if err != nil { + return nil, err + } + if len(params.ExtraData) > 32 { + return nil, fmt.Errorf("invalid extradata length: %v", len(params.ExtraData)) + } + header := &types.Header{ + ParentHash: params.ParentHash, + UncleHash: types.EmptyUncleHash, + Coinbase: params.FeeRecipient, + Root: params.StateRoot, + TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), + ReceiptHash: params.ReceiptsRoot, + Bloom: types.BytesToBloom(params.LogsBloom), + Difficulty: common.Big0, + Number: new(big.Int).SetUint64(params.Number), + GasLimit: params.GasLimit, + GasUsed: params.GasUsed, + Time: params.Timestamp, + BaseFee: params.BaseFeePerGas, + Extra: params.ExtraData, + MixDigest: params.Random, + } + block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */) + if block.Hash() != params.BlockHash { + return nil, fmt.Errorf("blockhash mismatch, want %x, got %x", params.BlockHash, block.Hash()) + } + return block, nil +} + +// BlockToExecutableData constructs the executableDataV1 structure by filling the +// fields from the given block. It assumes the given block is post-merge block. +func BlockToExecutableData(block *types.Block) *ExecutableDataV1 { + return &ExecutableDataV1{ + BlockHash: block.Hash(), + ParentHash: block.ParentHash(), + FeeRecipient: block.Coinbase(), + StateRoot: block.Root(), + Number: block.NumberU64(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + BaseFeePerGas: block.BaseFee(), + Timestamp: block.Time(), + ReceiptsRoot: block.ReceiptHash(), + LogsBloom: block.Bloom().Bytes(), + Transactions: encodeTransactions(block.Transactions()), + Random: block.MixDigest(), + ExtraData: block.Extra(), + } +} diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 1087496d16..a8b20d7589 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -20,29 +20,15 @@ package catalyst import ( "crypto/sha256" "encoding/binary" - "errors" "fmt" - "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/beacon" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/trie" -) - -var ( - VALID = GenericStringResponse{"VALID"} - SUCCESS = GenericStringResponse{"SUCCESS"} - INVALID = ForkChoiceResponse{Status: "INVALID", PayloadID: nil} - SYNCING = ForkChoiceResponse{Status: "SYNCING", PayloadID: nil} - GenericServerError = rpc.CustomError{Code: -32000, ValidationError: "Server error"} - UnknownPayload = rpc.CustomError{Code: -32001, ValidationError: "Unknown payload"} - InvalidTB = rpc.CustomError{Code: -32002, ValidationError: "Invalid terminal block"} ) // Register adds catalyst APIs to the full node. @@ -52,21 +38,7 @@ func Register(stack *node.Node, backend *eth.Ethereum) error { { Namespace: "engine", Version: "1.0", - Service: NewConsensusAPI(backend, nil), - Public: true, - }, - }) - return nil -} - -// RegisterLight adds catalyst APIs to the light client. -func RegisterLight(stack *node.Node, backend *les.LightEthereum) error { - log.Warn("Catalyst mode enabled", "protocol", "les") - stack.RegisterAPIs([]rpc.API{ - { - Namespace: "engine", - Version: "1.0", - Service: NewConsensusAPI(nil, backend), + Service: NewConsensusAPI(backend), Public: true, }, }) @@ -74,143 +46,86 @@ func RegisterLight(stack *node.Node, backend *les.LightEthereum) error { } type ConsensusAPI struct { - light bool eth *eth.Ethereum - les *les.LightEthereum preparedBlocks *payloadQueue // preparedBlocks caches payloads (*ExecutableDataV1) by payload ID (PayloadID) } -func NewConsensusAPI(eth *eth.Ethereum, les *les.LightEthereum) *ConsensusAPI { - if eth == nil { - if les.BlockChain().Config().TerminalTotalDifficulty == nil { - panic("Catalyst started without valid total difficulty") - } - } else { - if eth.BlockChain().Config().TerminalTotalDifficulty == nil { - panic("Catalyst started without valid total difficulty") - } +// NewConsensusAPI creates a new consensus api for the given backend. +// The underlying blockchain needs to have a valid terminal total difficulty set. +func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI { + if eth.BlockChain().Config().TerminalTotalDifficulty == nil { + panic("Catalyst started without valid total difficulty") } - return &ConsensusAPI{ - light: eth == nil, eth: eth, - les: les, preparedBlocks: newPayloadQueue(), } } -func (api *ConsensusAPI) GetPayloadV1(payloadID PayloadID) (*ExecutableDataV1, error) { - log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID) - data := api.preparedBlocks.get(payloadID) - if data == nil { - return nil, &UnknownPayload - } - return data, nil -} - -func (api *ConsensusAPI) ForkchoiceUpdatedV1(heads ForkchoiceStateV1, payloadAttributes *PayloadAttributesV1) (ForkChoiceResponse, error) { +// ForkchoiceUpdatedV1 has several responsibilities: +// If the method is called with an empty head block: +// we return success, which can be used to check if the catalyst mode is enabled +// If the total difficulty was not reached: +// we return INVALID +// If the finalizedBlockHash is set: +// we check if we have the finalizedBlockHash in our db, if not we start a sync +// We try to set our blockchain to the headBlock +// If there are payloadAttributes: +// we try to assemble a block with the payloadAttributes and return its payloadID +func (api *ConsensusAPI) ForkchoiceUpdatedV1(heads beacon.ForkchoiceStateV1, payloadAttributes *beacon.PayloadAttributesV1) (beacon.ForkChoiceResponse, error) { log.Trace("Engine API request received", "method", "ForkChoiceUpdated", "head", heads.HeadBlockHash, "finalized", heads.FinalizedBlockHash, "safe", heads.SafeBlockHash) if heads.HeadBlockHash == (common.Hash{}) { - return ForkChoiceResponse{Status: SUCCESS.Status, PayloadID: nil}, nil + return beacon.ForkChoiceResponse{Status: beacon.SUCCESS.Status, PayloadID: nil}, nil } if err := api.checkTerminalTotalDifficulty(heads.HeadBlockHash); err != nil { - if api.light { - if header := api.les.BlockChain().GetHeaderByHash(heads.HeadBlockHash); header == nil { - // TODO (MariusVanDerWijden) trigger sync - return SYNCING, nil - } - return INVALID, err - } else { - if block := api.eth.BlockChain().GetBlockByHash(heads.HeadBlockHash); block == nil { - // TODO (MariusVanDerWijden) trigger sync - return SYNCING, nil - } - return INVALID, err + if block := api.eth.BlockChain().GetBlockByHash(heads.HeadBlockHash); block == nil { + // TODO (MariusVanDerWijden) trigger sync + return beacon.SYNCING, nil } + return beacon.INVALID, err } // If the finalized block is set, check if it is in our blockchain if heads.FinalizedBlockHash != (common.Hash{}) { - if api.light { - if header := api.les.BlockChain().GetHeaderByHash(heads.FinalizedBlockHash); header == nil { - // TODO (MariusVanDerWijden) trigger sync - return SYNCING, nil - } - } else { - if block := api.eth.BlockChain().GetBlockByHash(heads.FinalizedBlockHash); block == nil { - // TODO (MariusVanDerWijden) trigger sync - return SYNCING, nil - } + if block := api.eth.BlockChain().GetBlockByHash(heads.FinalizedBlockHash); block == nil { + // TODO (MariusVanDerWijden) trigger sync + return beacon.SYNCING, nil } } // SetHead if err := api.setHead(heads.HeadBlockHash); err != nil { - return INVALID, err + return beacon.INVALID, err } // Assemble block (if needed). It only works for full node. - if !api.light && payloadAttributes != nil { + if payloadAttributes != nil { data, err := api.assembleBlock(heads.HeadBlockHash, payloadAttributes) if err != nil { - return INVALID, err + return beacon.INVALID, err } id := computePayloadId(heads.HeadBlockHash, payloadAttributes) api.preparedBlocks.put(id, data) log.Info("Created payload", "payloadID", id) - return ForkChoiceResponse{Status: SUCCESS.Status, PayloadID: &id}, nil + return beacon.ForkChoiceResponse{Status: beacon.SUCCESS.Status, PayloadID: &id}, nil } - return ForkChoiceResponse{Status: SUCCESS.Status, PayloadID: nil}, nil + return beacon.ForkChoiceResponse{Status: beacon.SUCCESS.Status, PayloadID: nil}, nil } -func computePayloadId(headBlockHash common.Hash, params *PayloadAttributesV1) PayloadID { - // Hash - hasher := sha256.New() - hasher.Write(headBlockHash[:]) - binary.Write(hasher, binary.BigEndian, params.Timestamp) - hasher.Write(params.Random[:]) - hasher.Write(params.SuggestedFeeRecipient[:]) - var out PayloadID - copy(out[:], hasher.Sum(nil)[:8]) - return out -} - -func (api *ConsensusAPI) invalid() ExecutePayloadResponse { - if api.light { - return ExecutePayloadResponse{Status: INVALID.Status, LatestValidHash: api.les.BlockChain().CurrentHeader().Hash()} +// GetPayloadV1 returns a cached payload by id. +func (api *ConsensusAPI) GetPayloadV1(payloadID beacon.PayloadID) (*beacon.ExecutableDataV1, error) { + log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID) + data := api.preparedBlocks.get(payloadID) + if data == nil { + return nil, &beacon.UnknownPayload } - return ExecutePayloadResponse{Status: INVALID.Status, LatestValidHash: api.eth.BlockChain().CurrentHeader().Hash()} + return data, nil } // ExecutePayloadV1 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. -func (api *ConsensusAPI) ExecutePayloadV1(params ExecutableDataV1) (ExecutePayloadResponse, error) { +func (api *ConsensusAPI) ExecutePayloadV1(params beacon.ExecutableDataV1) (beacon.ExecutePayloadResponse, error) { log.Trace("Engine API request received", "method", "ExecutePayload", params.BlockHash, "number", params.Number) - block, err := ExecutableDataToBlock(params) + block, err := beacon.ExecutableDataToBlock(params) if err != nil { return api.invalid(), err } - if api.light { - if !api.les.BlockChain().HasHeader(block.ParentHash(), block.NumberU64()-1) { - /* - TODO (MariusVanDerWijden) reenable once sync is merged - if err := api.eth.Downloader().BeaconSync(api.eth.SyncMode(), block.Header()); err != nil { - return SYNCING, err - } - */ - // TODO (MariusVanDerWijden) we should return nil here not empty hash - return ExecutePayloadResponse{Status: SYNCING.Status, LatestValidHash: common.Hash{}}, nil - } - parent := api.les.BlockChain().GetHeaderByHash(params.ParentHash) - td := api.les.BlockChain().GetTd(parent.Hash(), block.NumberU64()-1) - ttd := api.les.BlockChain().Config().TerminalTotalDifficulty - if td.Cmp(ttd) < 0 { - return api.invalid(), fmt.Errorf("can not execute payload on top of block with low td got: %v threshold %v", td, ttd) - } - if err = api.les.BlockChain().InsertHeader(block.Header()); err != nil { - return api.invalid(), err - } - if merger := api.merger(); !merger.TDDReached() { - merger.ReachTTD() - } - return ExecutePayloadResponse{Status: VALID.Status, LatestValidHash: block.Hash()}, nil - } if !api.eth.BlockChain().HasBlock(block.ParentHash(), block.NumberU64()-1) { /* TODO (MariusVanDerWijden) reenable once sync is merged @@ -219,7 +134,7 @@ func (api *ConsensusAPI) ExecutePayloadV1(params ExecutableDataV1) (ExecutePaylo } */ // TODO (MariusVanDerWijden) we should return nil here not empty hash - return ExecutePayloadResponse{Status: SYNCING.Status, LatestValidHash: common.Hash{}}, nil + return beacon.ExecutePayloadResponse{Status: beacon.SYNCING.Status, LatestValidHash: common.Hash{}}, nil } parent := api.eth.BlockChain().GetBlockByHash(params.ParentHash) td := api.eth.BlockChain().GetTd(parent.Hash(), block.NumberU64()-1) @@ -232,97 +147,39 @@ func (api *ConsensusAPI) ExecutePayloadV1(params ExecutableDataV1) (ExecutePaylo return api.invalid(), err } - if merger := api.merger(); !merger.TDDReached() { + if merger := api.eth.Merger(); !merger.TDDReached() { merger.ReachTTD() } - return ExecutePayloadResponse{Status: VALID.Status, LatestValidHash: block.Hash()}, nil + return beacon.ExecutePayloadResponse{Status: beacon.VALID.Status, LatestValidHash: block.Hash()}, 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(parentHash common.Hash, params *PayloadAttributesV1) (*ExecutableDataV1, error) { - if api.light { - return nil, errors.New("not supported") - } - log.Info("Producing block", "parentHash", parentHash) - block, err := api.eth.Miner().GetSealingBlock(parentHash, params.Timestamp, params.SuggestedFeeRecipient, params.Random) - if err != nil { - return nil, err - } - return BlockToExecutableData(block), nil -} - -func encodeTransactions(txs []*types.Transaction) [][]byte { - var enc = make([][]byte, len(txs)) - for i, tx := range txs { - enc[i], _ = tx.MarshalBinary() - } - return enc +// computePayloadId computes a pseudo-random payloadid, based on the parameters. +func computePayloadId(headBlockHash common.Hash, params *beacon.PayloadAttributesV1) beacon.PayloadID { + // Hash + hasher := sha256.New() + hasher.Write(headBlockHash[:]) + binary.Write(hasher, binary.BigEndian, params.Timestamp) + hasher.Write(params.Random[:]) + hasher.Write(params.SuggestedFeeRecipient[:]) + var out beacon.PayloadID + copy(out[:], hasher.Sum(nil)[:8]) + return out } -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 +// invalid returns a response "INVALID" with the latest valid hash set to the current head. +func (api *ConsensusAPI) invalid() beacon.ExecutePayloadResponse { + return beacon.ExecutePayloadResponse{Status: beacon.INVALID.Status, LatestValidHash: api.eth.BlockChain().CurrentHeader().Hash()} } -func ExecutableDataToBlock(params ExecutableDataV1) (*types.Block, error) { - txs, err := decodeTransactions(params.Transactions) +// assembleBlock creates a new block and returns the "execution +// data" required for beacon clients to process the new block. +func (api *ConsensusAPI) assembleBlock(parentHash common.Hash, params *beacon.PayloadAttributesV1) (*beacon.ExecutableDataV1, error) { + log.Info("Producing block", "parentHash", parentHash) + block, err := api.eth.Miner().GetSealingBlock(parentHash, params.Timestamp, params.SuggestedFeeRecipient, params.Random) if err != nil { return nil, err } - if len(params.ExtraData) > 32 { - return nil, fmt.Errorf("invalid extradata length: %v", len(params.ExtraData)) - } - header := &types.Header{ - ParentHash: params.ParentHash, - UncleHash: types.EmptyUncleHash, - Coinbase: params.FeeRecipient, - Root: params.StateRoot, - TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), - ReceiptHash: params.ReceiptsRoot, - Bloom: types.BytesToBloom(params.LogsBloom), - Difficulty: common.Big0, - Number: new(big.Int).SetUint64(params.Number), - GasLimit: params.GasLimit, - GasUsed: params.GasUsed, - Time: params.Timestamp, - BaseFee: params.BaseFeePerGas, - Extra: params.ExtraData, - MixDigest: params.Random, - } - block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */) - if block.Hash() != params.BlockHash { - return nil, fmt.Errorf("blockhash mismatch, want %x, got %x", params.BlockHash, block.Hash()) - } - return block, nil -} - -// BlockToExecutableData constructs the executableDataV1 structure by filling the -// fields from the given block. It assumes the given block is post-merge block. -func BlockToExecutableData(block *types.Block) *ExecutableDataV1 { - return &ExecutableDataV1{ - BlockHash: block.Hash(), - ParentHash: block.ParentHash(), - FeeRecipient: block.Coinbase(), - StateRoot: block.Root(), - Number: block.NumberU64(), - GasLimit: block.GasLimit(), - GasUsed: block.GasUsed(), - BaseFeePerGas: block.BaseFee(), - Timestamp: block.Time(), - ReceiptsRoot: block.ReceiptHash(), - LogsBloom: block.Bloom().Bytes(), - Transactions: encodeTransactions(block.Transactions()), - Random: block.MixDigest(), - ExtraData: block.Extra(), - } + return beacon.BlockToExecutableData(block), nil } // Used in tests to add a the list of transactions from a block to the tx pool. @@ -335,29 +192,17 @@ func (api *ConsensusAPI) insertTransactions(txs types.Transactions) error { func (api *ConsensusAPI) checkTerminalTotalDifficulty(head common.Hash) error { // shortcut if we entered PoS already - if api.merger().PoSFinalized() { - return nil - } - if api.light { - // make sure the parent has enough terminal total difficulty - header := api.les.BlockChain().GetHeaderByHash(head) - if header == nil { - return &GenericServerError - } - td := api.les.BlockChain().GetTd(header.Hash(), header.Number.Uint64()) - if td != nil && td.Cmp(api.les.BlockChain().Config().TerminalTotalDifficulty) < 0 { - return &InvalidTB - } + if api.eth.Merger().PoSFinalized() { return nil } // make sure the parent has enough terminal total difficulty newHeadBlock := api.eth.BlockChain().GetBlockByHash(head) if newHeadBlock == nil { - return &GenericServerError + return &beacon.GenericServerError } td := api.eth.BlockChain().GetTd(newHeadBlock.Hash(), newHeadBlock.NumberU64()) if td != nil && td.Cmp(api.eth.BlockChain().Config().TerminalTotalDifficulty) < 0 { - return &InvalidTB + return &beacon.InvalidTB } return nil } @@ -365,48 +210,22 @@ func (api *ConsensusAPI) checkTerminalTotalDifficulty(head common.Hash) error { // setHead is called to perform a force choice. func (api *ConsensusAPI) setHead(newHead common.Hash) error { log.Info("Setting head", "head", newHead) - if api.light { - headHeader := api.les.BlockChain().CurrentHeader() - if headHeader.Hash() == newHead { - return nil - } - newHeadHeader := api.les.BlockChain().GetHeaderByHash(newHead) - if newHeadHeader == nil { - return &GenericServerError - } - if err := api.les.BlockChain().SetChainHead(newHeadHeader); err != nil { - return err - } - // Trigger the transition if it's the first `NewHead` event. - if merger := api.merger(); !merger.PoSFinalized() { - merger.FinalizePoS() - } - return nil - } headBlock := api.eth.BlockChain().CurrentBlock() if headBlock.Hash() == newHead { return nil } newHeadBlock := api.eth.BlockChain().GetBlockByHash(newHead) if newHeadBlock == nil { - return &GenericServerError + return &beacon.GenericServerError } if err := api.eth.BlockChain().SetChainHead(newHeadBlock); err != nil { return err } // Trigger the transition if it's the first `NewHead` event. - if merger := api.merger(); !merger.PoSFinalized() { + if merger := api.eth.Merger(); !merger.PoSFinalized() { merger.FinalizePoS() } // TODO (MariusVanDerWijden) are we really synced now? api.eth.SetSynced() return nil } - -// Helper function, return the merger instance. -func (api *ConsensusAPI) merger() *consensus.Merger { - if api.light { - return api.les.Merger() - } - return api.eth.Merger() -} diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index b802fb05c8..b824d22f84 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -25,6 +25,7 @@ import ( "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/beacon" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -78,14 +79,14 @@ func TestEth2AssembleBlock(t *testing.T) { n, ethservice := startEthService(t, genesis, blocks) defer n.Close() - api := NewConsensusAPI(ethservice, nil) + api := NewConsensusAPI(ethservice) signer := types.NewEIP155Signer(ethservice.BlockChain().Config().ChainID) tx, err := types.SignTx(types.NewTransaction(uint64(10), blocks[9].Coinbase(), big.NewInt(1000), params.TxGas, big.NewInt(params.InitialBaseFee), nil), signer, testKey) if err != nil { t.Fatalf("error signing transaction, err=%v", err) } ethservice.TxPool().AddLocal(tx) - blockParams := PayloadAttributesV1{ + blockParams := beacon.PayloadAttributesV1{ Timestamp: blocks[9].Time() + 5, } execData, err := api.assembleBlock(blocks[9].Hash(), &blockParams) @@ -102,11 +103,11 @@ func TestEth2AssembleBlockWithAnotherBlocksTxs(t *testing.T) { n, ethservice := startEthService(t, genesis, blocks[:9]) defer n.Close() - api := NewConsensusAPI(ethservice, nil) + api := NewConsensusAPI(ethservice) // Put the 10th block's tx in the pool and produce a new block api.insertTransactions(blocks[9].Transactions()) - blockParams := PayloadAttributesV1{ + blockParams := beacon.PayloadAttributesV1{ Timestamp: blocks[8].Time() + 5, } execData, err := api.assembleBlock(blocks[8].Hash(), &blockParams) @@ -123,8 +124,8 @@ func TestSetHeadBeforeTotalDifficulty(t *testing.T) { n, ethservice := startEthService(t, genesis, blocks) defer n.Close() - api := NewConsensusAPI(ethservice, nil) - fcState := ForkchoiceStateV1{ + api := NewConsensusAPI(ethservice) + fcState := beacon.ForkchoiceStateV1{ HeadBlockHash: blocks[5].Hash(), SafeBlockHash: common.Hash{}, FinalizedBlockHash: common.Hash{}, @@ -141,14 +142,14 @@ func TestEth2PrepareAndGetPayload(t *testing.T) { n, ethservice := startEthService(t, genesis, blocks[:9]) defer n.Close() - api := NewConsensusAPI(ethservice, nil) + api := NewConsensusAPI(ethservice) // Put the 10th block's tx in the pool and produce a new block api.insertTransactions(blocks[9].Transactions()) - blockParams := PayloadAttributesV1{ + blockParams := beacon.PayloadAttributesV1{ Timestamp: blocks[8].Time() + 5, } - fcState := ForkchoiceStateV1{ + fcState := beacon.ForkchoiceStateV1{ HeadBlockHash: blocks[8].Hash(), SafeBlockHash: common.Hash{}, FinalizedBlockHash: common.Hash{}, @@ -166,7 +167,7 @@ func TestEth2PrepareAndGetPayload(t *testing.T) { t.Fatalf("invalid number of transactions %d != 1", len(execData.Transactions)) } // Test invalid payloadID - var invPayload PayloadID + var invPayload beacon.PayloadID copy(invPayload[:], payloadID[:]) invPayload[0] = ^invPayload[0] _, err = api.GetPayloadV1(invPayload) @@ -199,7 +200,7 @@ func TestInvalidPayloadTimestamp(t *testing.T) { ethservice.Merger().ReachTTD() defer n.Close() var ( - api = NewConsensusAPI(ethservice, nil) + api = NewConsensusAPI(ethservice) parent = ethservice.BlockChain().CurrentBlock() ) tests := []struct { @@ -215,12 +216,12 @@ func TestInvalidPayloadTimestamp(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("Timestamp test: %v", i), func(t *testing.T) { - params := PayloadAttributesV1{ + params := beacon.PayloadAttributesV1{ Timestamp: test.time, Random: crypto.Keccak256Hash([]byte{byte(123)}), SuggestedFeeRecipient: parent.Coinbase(), } - fcState := ForkchoiceStateV1{ + fcState := beacon.ForkchoiceStateV1{ HeadBlockHash: parent.Hash(), SafeBlockHash: common.Hash{}, FinalizedBlockHash: common.Hash{}, @@ -242,7 +243,7 @@ func TestEth2NewBlock(t *testing.T) { defer n.Close() var ( - api = NewConsensusAPI(ethservice, nil) + api = NewConsensusAPI(ethservice) parent = preMergeBlocks[len(preMergeBlocks)-1] // This EVM code generates a log when the contract is created. @@ -260,13 +261,13 @@ func TestEth2NewBlock(t *testing.T) { tx, _ := types.SignTx(types.NewContractCreation(nonce, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey) ethservice.TxPool().AddLocal(tx) - execData, err := api.assembleBlock(parent.Hash(), &PayloadAttributesV1{ + execData, err := api.assembleBlock(parent.Hash(), &beacon.PayloadAttributesV1{ Timestamp: parent.Time() + 5, }) if err != nil { t.Fatalf("Failed to create the executable data %v", err) } - block, err := ExecutableDataToBlock(*execData) + block, err := beacon.ExecutableDataToBlock(*execData) if err != nil { t.Fatalf("Failed to convert executable data to block %v", err) } @@ -278,7 +279,7 @@ func TestEth2NewBlock(t *testing.T) { t.Fatalf("Chain head shouldn't be updated") } checkLogEvents(t, newLogCh, rmLogsCh, 0, 0) - fcState := ForkchoiceStateV1{ + fcState := beacon.ForkchoiceStateV1{ HeadBlockHash: block.Hash(), SafeBlockHash: block.Hash(), FinalizedBlockHash: block.Hash(), @@ -300,13 +301,13 @@ func TestEth2NewBlock(t *testing.T) { ) parent = preMergeBlocks[len(preMergeBlocks)-1] for i := 0; i < 10; i++ { - execData, err := api.assembleBlock(parent.Hash(), &PayloadAttributesV1{ + execData, err := api.assembleBlock(parent.Hash(), &beacon.PayloadAttributesV1{ Timestamp: parent.Time() + 6, }) if err != nil { t.Fatalf("Failed to create the executable data %v", err) } - block, err := ExecutableDataToBlock(*execData) + block, err := beacon.ExecutableDataToBlock(*execData) if err != nil { t.Fatalf("Failed to convert executable data to block %v", err) } @@ -318,7 +319,7 @@ func TestEth2NewBlock(t *testing.T) { t.Fatalf("Chain head shouldn't be updated") } - fcState := ForkchoiceStateV1{ + fcState := beacon.ForkchoiceStateV1{ HeadBlockHash: block.Hash(), SafeBlockHash: block.Hash(), FinalizedBlockHash: block.Hash(), @@ -412,7 +413,7 @@ func TestFullAPI(t *testing.T) { ethservice.Merger().ReachTTD() defer n.Close() var ( - api = NewConsensusAPI(ethservice, nil) + api = NewConsensusAPI(ethservice) parent = ethservice.BlockChain().CurrentBlock() // This EVM code generates a log when the contract is created. logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") @@ -423,12 +424,12 @@ func TestFullAPI(t *testing.T) { tx, _ := types.SignTx(types.NewContractCreation(nonce, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey) ethservice.TxPool().AddLocal(tx) - params := PayloadAttributesV1{ + params := beacon.PayloadAttributesV1{ Timestamp: parent.Time() + 1, Random: crypto.Keccak256Hash([]byte{byte(i)}), SuggestedFeeRecipient: parent.Coinbase(), } - fcState := ForkchoiceStateV1{ + fcState := beacon.ForkchoiceStateV1{ HeadBlockHash: parent.Hash(), SafeBlockHash: common.Hash{}, FinalizedBlockHash: common.Hash{}, @@ -437,7 +438,7 @@ func TestFullAPI(t *testing.T) { if err != nil { t.Fatalf("error preparing payload, err=%v", err) } - if resp.Status != SUCCESS.Status { + if resp.Status != beacon.SUCCESS.Status { t.Fatalf("error preparing payload, invalid status: %v", resp.Status) } payloadID := computePayloadId(parent.Hash(), ¶ms) @@ -449,10 +450,10 @@ func TestFullAPI(t *testing.T) { if err != nil { t.Fatalf("can't execute payload: %v", err) } - if execResp.Status != VALID.Status { + if execResp.Status != beacon.VALID.Status { t.Fatalf("invalid status: %v", execResp.Status) } - fcState = ForkchoiceStateV1{ + fcState = beacon.ForkchoiceStateV1{ HeadBlockHash: payload.BlockHash, SafeBlockHash: payload.ParentHash, FinalizedBlockHash: payload.ParentHash, diff --git a/eth/catalyst/queue.go b/eth/catalyst/queue.go index db373a6c79..aa2ce7823d 100644 --- a/eth/catalyst/queue.go +++ b/eth/catalyst/queue.go @@ -16,7 +16,11 @@ package catalyst -import "sync" +import ( + "sync" + + "github.com/ethereum/go-ethereum/core/beacon" +) // maxTrackedPayloads is the maximum number of prepared payloads the execution // engine tracks before evicting old ones. Ideally we should only ever track the @@ -26,8 +30,8 @@ const maxTrackedPayloads = 10 // payloadQueueItem represents an id->payload tuple to store until it's retrieved // or evicted. type payloadQueueItem struct { - id PayloadID - payload *ExecutableDataV1 + id beacon.PayloadID + payload *beacon.ExecutableDataV1 } // payloadQueue tracks the latest handful of constructed payloads to be retrieved @@ -46,7 +50,7 @@ func newPayloadQueue() *payloadQueue { } // put inserts a new payload into the queue at the given id. -func (q *payloadQueue) put(id PayloadID, data *ExecutableDataV1) { +func (q *payloadQueue) put(id beacon.PayloadID, data *beacon.ExecutableDataV1) { q.lock.Lock() defer q.lock.Unlock() @@ -58,7 +62,7 @@ func (q *payloadQueue) put(id PayloadID, data *ExecutableDataV1) { } // get retrieves a previously stored payload item or nil if it does not exist. -func (q *payloadQueue) get(id PayloadID) *ExecutableDataV1 { +func (q *payloadQueue) get(id beacon.PayloadID) *beacon.ExecutableDataV1 { q.lock.RLock() defer q.lock.RUnlock() diff --git a/les/catalyst/api.go b/les/catalyst/api.go new file mode 100644 index 0000000000..5f5193c3bb --- /dev/null +++ b/les/catalyst/api.go @@ -0,0 +1,178 @@ +// Copyright 2022 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 catalyst implements the temporary eth1/eth2 RPC integration. +package catalyst + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/beacon" + "github.com/ethereum/go-ethereum/les" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" +) + +// Register adds catalyst APIs to the light client. +func Register(stack *node.Node, backend *les.LightEthereum) error { + log.Warn("Catalyst mode enabled", "protocol", "les") + stack.RegisterAPIs([]rpc.API{ + { + Namespace: "engine", + Version: "1.0", + Service: NewConsensusAPI(backend), + Public: true, + }, + }) + return nil +} + +type ConsensusAPI struct { + les *les.LightEthereum +} + +// NewConsensusAPI creates a new consensus api for the given backend. +// The underlying blockchain needs to have a valid terminal total difficulty set. +func NewConsensusAPI(les *les.LightEthereum) *ConsensusAPI { + if les.BlockChain().Config().TerminalTotalDifficulty == nil { + panic("Catalyst started without valid total difficulty") + } + return &ConsensusAPI{les: les} +} + +// ForkchoiceUpdatedV1 has several responsibilities: +// If the method is called with an empty head block: +// we return success, which can be used to check if the catalyst mode is enabled +// If the total difficulty was not reached: +// we return INVALID +// If the finalizedBlockHash is set: +// we check if we have the finalizedBlockHash in our db, if not we start a sync +// We try to set our blockchain to the headBlock +// If there are payloadAttributes: +// we return an error since block creation is not supported in les mode +func (api *ConsensusAPI) ForkchoiceUpdatedV1(heads beacon.ForkchoiceStateV1, payloadAttributes *beacon.PayloadAttributesV1) (beacon.ForkChoiceResponse, error) { + if heads.HeadBlockHash == (common.Hash{}) { + return beacon.ForkChoiceResponse{Status: beacon.SUCCESS.Status, PayloadID: nil}, nil + } + if err := api.checkTerminalTotalDifficulty(heads.HeadBlockHash); err != nil { + if header := api.les.BlockChain().GetHeaderByHash(heads.HeadBlockHash); header == nil { + // TODO (MariusVanDerWijden) trigger sync + return beacon.SYNCING, nil + } + return beacon.INVALID, err + } + // If the finalized block is set, check if it is in our blockchain + if heads.FinalizedBlockHash != (common.Hash{}) { + if header := api.les.BlockChain().GetHeaderByHash(heads.FinalizedBlockHash); header == nil { + // TODO (MariusVanDerWijden) trigger sync + return beacon.SYNCING, nil + } + } + // SetHead + if err := api.setHead(heads.HeadBlockHash); err != nil { + return beacon.INVALID, err + } + if payloadAttributes != nil { + return beacon.INVALID, errors.New("not supported") + } + return beacon.ForkChoiceResponse{Status: beacon.SUCCESS.Status, PayloadID: nil}, nil +} + +// GetPayloadV1 returns a cached payload by id. It's not supported in les mode. +func (api *ConsensusAPI) GetPayloadV1(payloadID beacon.PayloadID) (*beacon.ExecutableDataV1, error) { + return nil, &beacon.GenericServerError +} + +// ExecutePayloadV1 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. +func (api *ConsensusAPI) ExecutePayloadV1(params beacon.ExecutableDataV1) (beacon.ExecutePayloadResponse, error) { + block, err := beacon.ExecutableDataToBlock(params) + if err != nil { + return api.invalid(), err + } + if !api.les.BlockChain().HasHeader(block.ParentHash(), block.NumberU64()-1) { + /* + TODO (MariusVanDerWijden) reenable once sync is merged + if err := api.eth.Downloader().BeaconSync(api.eth.SyncMode(), block.Header()); err != nil { + return SYNCING, err + } + */ + // TODO (MariusVanDerWijden) we should return nil here not empty hash + return beacon.ExecutePayloadResponse{Status: beacon.SYNCING.Status, LatestValidHash: common.Hash{}}, nil + } + parent := api.les.BlockChain().GetHeaderByHash(params.ParentHash) + if parent == nil { + return api.invalid(), fmt.Errorf("could not find parent %x", params.ParentHash) + } + td := api.les.BlockChain().GetTd(parent.Hash(), block.NumberU64()-1) + ttd := api.les.BlockChain().Config().TerminalTotalDifficulty + if td.Cmp(ttd) < 0 { + return api.invalid(), fmt.Errorf("can not execute payload on top of block with low td got: %v threshold %v", td, ttd) + } + if err = api.les.BlockChain().InsertHeader(block.Header()); err != nil { + return api.invalid(), err + } + if merger := api.les.Merger(); !merger.TDDReached() { + merger.ReachTTD() + } + return beacon.ExecutePayloadResponse{Status: beacon.VALID.Status, LatestValidHash: block.Hash()}, nil +} + +// invalid returns a response "INVALID" with the latest valid hash set to the current head. +func (api *ConsensusAPI) invalid() beacon.ExecutePayloadResponse { + return beacon.ExecutePayloadResponse{Status: beacon.INVALID.Status, LatestValidHash: api.les.BlockChain().CurrentHeader().Hash()} +} + +func (api *ConsensusAPI) checkTerminalTotalDifficulty(head common.Hash) error { + // shortcut if we entered PoS already + if api.les.Merger().PoSFinalized() { + return nil + } + // make sure the parent has enough terminal total difficulty + header := api.les.BlockChain().GetHeaderByHash(head) + if header == nil { + return &beacon.GenericServerError + } + td := api.les.BlockChain().GetTd(header.Hash(), header.Number.Uint64()) + if td != nil && td.Cmp(api.les.BlockChain().Config().TerminalTotalDifficulty) < 0 { + return &beacon.InvalidTB + } + return nil +} + +// setHead is called to perform a force choice. +func (api *ConsensusAPI) setHead(newHead common.Hash) error { + log.Info("Setting head", "head", newHead) + + headHeader := api.les.BlockChain().CurrentHeader() + if headHeader.Hash() == newHead { + return nil + } + newHeadHeader := api.les.BlockChain().GetHeaderByHash(newHead) + if newHeadHeader == nil { + return &beacon.GenericServerError + } + if err := api.les.BlockChain().SetChainHead(newHeadHeader); err != nil { + return err + } + // Trigger the transition if it's the first `NewHead` event. + if merger := api.les.Merger(); !merger.PoSFinalized() { + merger.FinalizePoS() + } + return nil +} diff --git a/les/catalyst/api_test.go b/les/catalyst/api_test.go new file mode 100644 index 0000000000..c1cbf645cc --- /dev/null +++ b/les/catalyst/api_test.go @@ -0,0 +1,244 @@ +// 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 . + +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/beacon" + "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/downloader" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/les" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" +) + +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(2e18) +) + +func generatePreMergeChain(n int) (*core.Genesis, []*types.Header, []*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, + BaseFee: big.NewInt(params.InitialBaseFee), + } + gblock := genesis.ToBlock(db) + engine := ethash.NewFaker() + blocks, _ := core.GenerateChain(config, gblock, engine, db, n, nil) + totalDifficulty := big.NewInt(0) + + var headers []*types.Header + for _, b := range blocks { + totalDifficulty.Add(totalDifficulty, b.Difficulty()) + headers = append(headers, b.Header()) + } + config.TerminalTotalDifficulty = totalDifficulty + + return genesis, headers, blocks +} + +func TestSetHeadBeforeTotalDifficulty(t *testing.T) { + genesis, headers, blocks := generatePreMergeChain(10) + n, lesService := startLesService(t, genesis, headers) + defer n.Close() + + api := NewConsensusAPI(lesService) + fcState := beacon.ForkchoiceStateV1{ + HeadBlockHash: blocks[5].Hash(), + SafeBlockHash: common.Hash{}, + FinalizedBlockHash: common.Hash{}, + } + if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err == nil { + t.Errorf("fork choice updated before total terminal difficulty should fail") + } +} + +func TestExecutePayloadV1(t *testing.T) { + genesis, headers, blocks := generatePreMergeChain(10) + n, lesService := startLesService(t, genesis, headers[:9]) + lesService.Merger().ReachTTD() + defer n.Close() + + api := NewConsensusAPI(lesService) + fcState := beacon.ForkchoiceStateV1{ + HeadBlockHash: blocks[8].Hash(), + SafeBlockHash: common.Hash{}, + FinalizedBlockHash: common.Hash{}, + } + if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil { + t.Errorf("Failed to update head %v", err) + } + block := blocks[9] + + fakeBlock := types.NewBlock(&types.Header{ + ParentHash: block.ParentHash(), + UncleHash: crypto.Keccak256Hash(nil), + Coinbase: block.Coinbase(), + Root: block.Root(), + TxHash: crypto.Keccak256Hash(nil), + ReceiptHash: crypto.Keccak256Hash(nil), + Bloom: block.Bloom(), + Difficulty: big.NewInt(0), + Number: block.Number(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + Time: block.Time(), + Extra: block.Extra(), + MixDigest: block.MixDigest(), + Nonce: types.BlockNonce{}, + BaseFee: block.BaseFee(), + }, nil, nil, nil, trie.NewStackTrie(nil)) + + _, err := api.ExecutePayloadV1(beacon.ExecutableDataV1{ + ParentHash: fakeBlock.ParentHash(), + FeeRecipient: fakeBlock.Coinbase(), + StateRoot: fakeBlock.Root(), + ReceiptsRoot: fakeBlock.ReceiptHash(), + LogsBloom: fakeBlock.Bloom().Bytes(), + Random: fakeBlock.MixDigest(), + Number: fakeBlock.NumberU64(), + GasLimit: fakeBlock.GasLimit(), + GasUsed: fakeBlock.GasUsed(), + Timestamp: fakeBlock.Time(), + ExtraData: fakeBlock.Extra(), + BaseFeePerGas: fakeBlock.BaseFee(), + BlockHash: fakeBlock.Hash(), + Transactions: encodeTransactions(fakeBlock.Transactions()), + }) + if err != nil { + t.Errorf("Failed to execute payload %v", err) + } + headHeader := api.les.BlockChain().CurrentHeader() + if headHeader.Number.Uint64() != fakeBlock.NumberU64()-1 { + t.Fatal("Unexpected chain head update") + } + fcState = beacon.ForkchoiceStateV1{ + HeadBlockHash: fakeBlock.Hash(), + SafeBlockHash: common.Hash{}, + FinalizedBlockHash: common.Hash{}, + } + if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil { + t.Fatal("Failed to update head") + } + headHeader = api.les.BlockChain().CurrentHeader() + if headHeader.Number.Uint64() != fakeBlock.NumberU64() { + t.Fatal("Failed to update chain head") + } +} + +func TestEth2DeepReorg(t *testing.T) { + // TODO (MariusVanDerWijden) TestEth2DeepReorg is currently broken, because it tries to reorg + // before the totalTerminalDifficulty threshold + /* + genesis, preMergeBlocks := generatePreMergeChain(core.TriesInMemory * 2) + n, ethservice := startEthService(t, genesis, preMergeBlocks) + defer n.Close() + + var ( + api = NewConsensusAPI(ethservice, nil) + parent = preMergeBlocks[len(preMergeBlocks)-core.TriesInMemory-1] + head = ethservice.BlockChain().CurrentBlock().NumberU64() + ) + if ethservice.BlockChain().HasBlockAndState(parent.Hash(), parent.NumberU64()) { + t.Errorf("Block %d not pruned", parent.NumberU64()) + } + for i := 0; i < 10; i++ { + execData, err := api.assembleBlock(AssembleBlockParams{ + ParentHash: parent.Hash(), + Timestamp: parent.Time() + 5, + }) + if err != nil { + t.Fatalf("Failed to create the executable data %v", err) + } + block, err := ExecutableDataToBlock(ethservice.BlockChain().Config(), parent.Header(), *execData) + if err != nil { + t.Fatalf("Failed to convert executable data to block %v", err) + } + newResp, err := api.ExecutePayload(*execData) + if err != nil || newResp.Status != "VALID" { + t.Fatalf("Failed to insert block: %v", err) + } + if ethservice.BlockChain().CurrentBlock().NumberU64() != head { + t.Fatalf("Chain head shouldn't be updated") + } + if err := api.setHead(block.Hash()); err != nil { + t.Fatalf("Failed to set head: %v", err) + } + if ethservice.BlockChain().CurrentBlock().NumberU64() != block.NumberU64() { + t.Fatalf("Chain head should be updated") + } + parent, head = block, block.NumberU64() + } + */ +} + +// startEthService creates a full node instance for testing. +func startLesService(t *testing.T, genesis *core.Genesis, headers []*types.Header) (*node.Node, *les.LightEthereum) { + t.Helper() + + n, err := node.New(&node.Config{}) + if err != nil { + t.Fatal("can't create node:", err) + } + ethcfg := ðconfig.Config{ + Genesis: genesis, + Ethash: ethash.Config{PowMode: ethash.ModeFake}, + SyncMode: downloader.LightSync, + TrieDirtyCache: 256, + TrieCleanCache: 256, + LightPeers: 10, + } + lesService, err := les.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 := lesService.BlockChain().InsertHeaderChain(headers, 0); err != nil { + n.Close() + t.Fatal("can't import test headers:", err) + } + return n, lesService +} + +func encodeTransactions(txs []*types.Transaction) [][]byte { + var enc = make([][]byte, len(txs)) + for i, tx := range txs { + enc[i], _ = tx.MarshalBinary() + } + return enc +} diff --git a/miner/stress/beacon/main.go b/miner/stress/beacon/main.go index 6a6a0a7222..9fa63281c6 100644 --- a/miner/stress/beacon/main.go +++ b/miner/stress/beacon/main.go @@ -32,13 +32,15 @@ import ( "github.com/ethereum/go-ethereum/common/fdlimit" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/beacon" "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/catalyst" + ethcatalyst "github.com/ethereum/go-ethereum/eth/catalyst" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/les" + lescatalyst "github.com/ethereum/go-ethereum/les/catalyst" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/node" @@ -88,24 +90,26 @@ var ( type ethNode struct { typ nodetype - api *catalyst.ConsensusAPI - ethBackend *eth.Ethereum - lesBackend *les.LightEthereum stack *node.Node enode *enode.Node + api *ethcatalyst.ConsensusAPI + ethBackend *eth.Ethereum + lapi *lescatalyst.ConsensusAPI + lesBackend *les.LightEthereum } func newNode(typ nodetype, genesis *core.Genesis, enodes []*enode.Node) *ethNode { var ( err error - api *catalyst.ConsensusAPI + api *ethcatalyst.ConsensusAPI + lapi *lescatalyst.ConsensusAPI stack *node.Node ethBackend *eth.Ethereum lesBackend *les.LightEthereum ) // Start the node and wait until it's up if typ == eth2LightClient { - stack, lesBackend, api, err = makeLightNode(genesis) + stack, lesBackend, lapi, err = makeLightNode(genesis) } else { stack, ethBackend, api, err = makeFullNode(genesis) } @@ -131,13 +135,14 @@ func newNode(typ nodetype, genesis *core.Genesis, enodes []*enode.Node) *ethNode typ: typ, api: api, ethBackend: ethBackend, + lapi: lapi, lesBackend: lesBackend, stack: stack, enode: enode, } } -func (n *ethNode) assembleBlock(parentHash common.Hash, parentTimestamp uint64) (*catalyst.ExecutableDataV1, error) { +func (n *ethNode) assembleBlock(parentHash common.Hash, parentTimestamp uint64) (*beacon.ExecutableDataV1, error) { if n.typ != eth2MiningNode { return nil, errors.New("invalid node type") } @@ -145,12 +150,12 @@ func (n *ethNode) assembleBlock(parentHash common.Hash, parentTimestamp uint64) if timestamp <= parentTimestamp { timestamp = parentTimestamp + 1 } - payloadAttribute := catalyst.PayloadAttributesV1{ + payloadAttribute := beacon.PayloadAttributesV1{ Timestamp: timestamp, Random: common.Hash{}, SuggestedFeeRecipient: common.HexToAddress("0xdeadbeef"), } - fcState := catalyst.ForkchoiceStateV1{ + fcState := beacon.ForkchoiceStateV1{ HeadBlockHash: parentHash, SafeBlockHash: common.Hash{}, FinalizedBlockHash: common.Hash{}, @@ -162,39 +167,62 @@ func (n *ethNode) assembleBlock(parentHash common.Hash, parentTimestamp uint64) return n.api.GetPayloadV1(*payload.PayloadID) } -func (n *ethNode) insertBlock(eb catalyst.ExecutableDataV1) error { +func (n *ethNode) insertBlock(eb beacon.ExecutableDataV1) error { if !eth2types(n.typ) { return errors.New("invalid node type") } - newResp, err := n.api.ExecutePayloadV1(eb) - if err != nil { - return err - } else if newResp.Status != "VALID" { - return errors.New("failed to insert block") + switch n.typ { + case eth2NormalNode, eth2MiningNode: + newResp, err := n.api.ExecutePayloadV1(eb) + if err != nil { + return err + } else if newResp.Status != "VALID" { + return errors.New("failed to insert block") + } + return nil + case eth2LightClient: + newResp, err := n.lapi.ExecutePayloadV1(eb) + if err != nil { + return err + } else if newResp.Status != "VALID" { + return errors.New("failed to insert block") + } + return nil + default: + return errors.New("undefined node") } - return nil } -func (n *ethNode) insertBlockAndSetHead(parent *types.Header, ed catalyst.ExecutableDataV1) error { +func (n *ethNode) insertBlockAndSetHead(parent *types.Header, ed beacon.ExecutableDataV1) error { if !eth2types(n.typ) { return errors.New("invalid node type") } if err := n.insertBlock(ed); err != nil { return err } - block, err := catalyst.ExecutableDataToBlock(ed) + block, err := beacon.ExecutableDataToBlock(ed) if err != nil { return err } - fcState := catalyst.ForkchoiceStateV1{ + fcState := beacon.ForkchoiceStateV1{ HeadBlockHash: block.ParentHash(), SafeBlockHash: common.Hash{}, FinalizedBlockHash: common.Hash{}, } - if _, err := n.api.ForkchoiceUpdatedV1(fcState, nil); err != nil { - return err + switch n.typ { + case eth2NormalNode, eth2MiningNode: + if _, err := n.api.ForkchoiceUpdatedV1(fcState, nil); err != nil { + return err + } + return nil + case eth2LightClient: + if _, err := n.lapi.ForkchoiceUpdatedV1(fcState, nil); err != nil { + return err + } + return nil + default: + return errors.New("undefined node") } - return nil } type nodeManager struct { @@ -290,7 +318,7 @@ func (mgr *nodeManager) run() { nodes = append(nodes, mgr.getNodes(eth2NormalNode)...) nodes = append(nodes, mgr.getNodes(eth2LightClient)...) for _, node := range append(nodes) { - fcState := catalyst.ForkchoiceStateV1{ + fcState := beacon.ForkchoiceStateV1{ HeadBlockHash: oldest.Hash(), SafeBlockHash: common.Hash{}, FinalizedBlockHash: oldest.Hash(), @@ -336,20 +364,16 @@ func (mgr *nodeManager) run() { log.Error("Failed to assemble the block", "err", err) continue } - block, _ := catalyst.ExecutableDataToBlock(*ed) + block, _ := beacon.ExecutableDataToBlock(*ed) nodes := mgr.getNodes(eth2MiningNode) nodes = append(nodes, mgr.getNodes(eth2NormalNode)...) + nodes = append(nodes, mgr.getNodes(eth2LightClient)...) for _, node := range nodes { if err := node.insertBlockAndSetHead(parentBlock.Header(), *ed); err != nil { log.Error("Failed to insert block", "type", node.typ, "err", err) } } - for _, node := range mgr.getNodes(eth2LightClient) { - if err := node.insertBlock(*ed); err != nil { - log.Error("Failed to insert block", "type", node.typ, "err", err) - } - } log.Info("Create and insert eth2 block", "number", ed.Number) parentBlock = block waitFinalise = append(waitFinalise, block) @@ -435,7 +459,7 @@ func makeGenesis(faucets []*ecdsa.PrivateKey) *core.Genesis { return genesis } -func makeFullNode(genesis *core.Genesis) (*node.Node, *eth.Ethereum, *catalyst.ConsensusAPI, error) { +func makeFullNode(genesis *core.Genesis) (*node.Node, *eth.Ethereum, *ethcatalyst.ConsensusAPI, error) { // Define the basic configurations for the Ethereum node datadir, _ := ioutil.TempDir("", "") @@ -483,10 +507,10 @@ func makeFullNode(genesis *core.Genesis) (*node.Node, *eth.Ethereum, *catalyst.C log.Crit("Failed to create the LES server", "err", err) } err = stack.Start() - return stack, ethBackend, catalyst.NewConsensusAPI(ethBackend, nil), err + return stack, ethBackend, ethcatalyst.NewConsensusAPI(ethBackend), err } -func makeLightNode(genesis *core.Genesis) (*node.Node, *les.LightEthereum, *catalyst.ConsensusAPI, error) { +func makeLightNode(genesis *core.Genesis) (*node.Node, *les.LightEthereum, *lescatalyst.ConsensusAPI, error) { // Define the basic configurations for the Ethereum node datadir, _ := ioutil.TempDir("", "") @@ -521,7 +545,7 @@ func makeLightNode(genesis *core.Genesis) (*node.Node, *les.LightEthereum, *cata return nil, nil, nil, err } err = stack.Start() - return stack, lesBackend, catalyst.NewConsensusAPI(nil, lesBackend), err + return stack, lesBackend, lescatalyst.NewConsensusAPI(lesBackend), err } func eth2types(typ nodetype) bool {