From ec72ec6a285e85761a21d6700af8c52513fed9eb Mon Sep 17 00:00:00 2001 From: protolambda Date: Mon, 22 Jan 2024 13:19:28 -0600 Subject: [PATCH] Snap Sync: consensus: handle legacy pre-bedrock header verification (#182) * consensus: handle legacy pre-bedrock header verification * consensus/beacon: Add parent hash check to OpLegacy.VerifyHeader VerifyHeaders will be dealt with in a follow-up. * optimism: fix historical-blocks chain-gen and TTD check --------- Co-authored-by: Sebastian Stammler --- consensus/beacon/consensus.go | 25 ++++++++++++ consensus/beacon/oplegacy.go | 74 +++++++++++++++++++++++++++++++++++ core/chain_makers.go | 2 +- core/types/receipt.go | 2 +- core/types/receipt_test.go | 14 +++++-- eth/ethconfig/config.go | 3 ++ ethclient/ethclient_test.go | 47 +++++++++++++--------- fork.yaml | 10 +++++ 8 files changed, 154 insertions(+), 23 deletions(-) create mode 100644 consensus/beacon/oplegacy.go diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index a7d0e58161..1db9d81a1d 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -58,6 +58,7 @@ var ( // is only used for necessary consensus checks. The legacy consensus engine can be any // engine implements the consensus interface (except the beacon itself). type Beacon struct { + // For migrated OP chains (OP mainnet, OP Goerli), ethone is a dummy legacy pre-Bedrock consensus ethone consensus.Engine // Original consensus engine used in eth1, e.g. ethash or clique } @@ -105,12 +106,27 @@ func errOut(n int, err error) chan error { return errs } +// OP-Stack Bedrock variant of splitHeaders: the total-terminal difficulty is terminated at bedrock transition, but also reset to 0. +// So just use the bedrock fork check to split the headers, to simplify the splitting. +// The returned slices are slices over the input. The input must be sorted. +func (beacon *Beacon) splitBedrockHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) ([]*types.Header, []*types.Header, error) { + for i, h := range headers { + if chain.Config().IsBedrock(h.Number) { + return headers[:i], headers[i:], nil + } + } + return headers, nil, nil +} + // splitHeaders splits the provided header batch into two parts according to // the configured ttd. It requires the parent of header batch along with its // td are stored correctly in chain. If ttd is not configured yet, all headers // will be treated legacy PoW headers. // Note, this function will not verify the header validity but just split them. func (beacon *Beacon) splitHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) ([]*types.Header, []*types.Header, error) { + if chain.Config().Optimism != nil { + return beacon.splitBedrockHeaders(chain, headers) + } // TTD is not defined yet, all headers should be in legacy format. ttd := chain.Config().TerminalTotalDifficulty if ttd == nil { @@ -446,6 +462,10 @@ func (beacon *Beacon) InnerEngine() consensus.Engine { return beacon.ethone } +func (beacon *Beacon) SwapInner(inner consensus.Engine) { + beacon.ethone = inner +} + // SetThreads updates the mining threads. Delegate the call // to the eth1 engine if it's threaded. func (beacon *Beacon) SetThreads(threads int) { @@ -461,6 +481,11 @@ func (beacon *Beacon) SetThreads(threads int) { // It depends on the parentHash already being stored in the database. // If the parentHash is not stored in the database a UnknownAncestor error is returned. func IsTTDReached(chain consensus.ChainHeaderReader, parentHash common.Hash, parentNumber uint64) (bool, error) { + if cfg := chain.Config(); cfg.Optimism != nil { + // If OP-Stack then bedrock activation number determines when TTD (eth Merge) has been reached. + // Note: some tests/utils will set parentNumber == max_uint64 as "parent" of the genesis block, this is fine. + return cfg.IsBedrock(new(big.Int).SetUint64(parentNumber + 1)), nil + } if chain.Config().TerminalTotalDifficulty == nil { return false, nil } diff --git a/consensus/beacon/oplegacy.go b/consensus/beacon/oplegacy.go new file mode 100644 index 0000000000..90d7e8678e --- /dev/null +++ b/consensus/beacon/oplegacy.go @@ -0,0 +1,74 @@ +package beacon + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" +) + +type OpLegacy struct{} + +func (o *OpLegacy) Author(header *types.Header) (common.Address, error) { + return header.Coinbase, nil +} + +func (o *OpLegacy) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header) error { + // redundant check to guarantee DB consistency + parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1) + if parent == nil { + return consensus.ErrUnknownAncestor + } + return nil // legacy chain is verified by block-hash reverse sync otherwise +} + +func (o *OpLegacy) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) { + quit := make(chan struct{}, 1) + result := make(chan error, len(headers)) + for _, h := range headers { + result <- o.VerifyHeader(chain, h) + } + return quit, result +} + +func (o *OpLegacy) VerifyUncles(chain consensus.ChainReader, block *types.Block) error { + return nil +} + +func (o *OpLegacy) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { + return fmt.Errorf("cannot prepare for legacy block header: %s (num %d)", header.Hash(), header.Number) +} + +func (o *OpLegacy) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) { + panic(fmt.Errorf("cannot finalize legacy block header: %s (num %d)", header.Hash(), header.Number)) +} + +func (o *OpLegacy) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) { + return nil, fmt.Errorf("cannot finalize and assemble for legacy block header: %s (num %d)", header.Hash(), header.Number) +} + +func (o *OpLegacy) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error { + return fmt.Errorf("cannot seal legacy block header: %s (num %d)", block.Hash(), block.Number()) +} + +func (o *OpLegacy) SealHash(header *types.Header) common.Hash { + panic(fmt.Errorf("cannot compute pow/poa seal-hash for legacy block header: %s (num %d)", header.Hash(), header.Number)) +} + +func (o *OpLegacy) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int { + return big.NewInt(0) +} + +func (o *OpLegacy) APIs(chain consensus.ChainHeaderReader) []rpc.API { + return nil +} + +func (o *OpLegacy) Close() error { + return nil +} + +var _ consensus.Engine = (*OpLegacy)(nil) diff --git a/core/chain_makers.go b/core/chain_makers.go index 90b5b1fcee..22c837e84f 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -319,7 +319,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse // to a chain, so the difficulty will be left unset (nil). Set it here to the // correct value. if b.header.Difficulty == nil { - if config.TerminalTotalDifficulty == nil { + if config.TerminalTotalDifficulty == nil && !config.IsOptimismBedrock(b.header.Number) { // Clique chain b.header.Difficulty = big.NewInt(2) } else { diff --git a/core/types/receipt.go b/core/types/receipt.go index edd90d9ab2..1b7d03459a 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -569,7 +569,7 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu logIndex++ } } - if config.Optimism != nil && len(txs) >= 2 { // need at least an info tx and a non-info tx + if config.Optimism != nil && len(txs) >= 2 && config.IsBedrock(new(big.Int).SetUint64(number)) { // need at least an info tx and a non-info tx l1BaseFee, costFunc, feeScalar, err := extractL1GasParams(config, time, txs[0].Data()) if err != nil { return err diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go index 5f6a125636..8eaae91057 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -34,8 +34,16 @@ import ( ) var ( + bedrockGenesisTestConfig = func() *params.ChainConfig { + conf := *params.AllCliqueProtocolChanges // copy the config + conf.Clique = nil + conf.TerminalTotalDifficultyPassed = true + conf.BedrockBlock = big.NewInt(0) + conf.Optimism = ¶ms.OptimismConfig{EIP1559Elasticity: 50, EIP1559Denominator: 10} + return &conf + }() ecotoneTestConfig = func() *params.ChainConfig { - conf := *params.OptimismTestConfig // copy the config + conf := *bedrockGenesisTestConfig // copy the config time := uint64(0) conf.EcotoneTime = &time return &conf @@ -774,7 +782,7 @@ func TestDeriveOptimismBedrockTxReceipts(t *testing.T) { // Re-derive receipts. baseFee := big.NewInt(1000) derivedReceipts := clearComputedFieldsOnReceipts(receipts) - err := Receipts(derivedReceipts).DeriveFields(params.OptimismTestConfig, blockHash, blockNumber.Uint64(), 0, baseFee, nil, txs) + err := Receipts(derivedReceipts).DeriveFields(bedrockGenesisTestConfig, blockHash, blockNumber.Uint64(), 0, baseFee, nil, txs) if err != nil { t.Fatalf("DeriveFields(...) = %v, want ", err) } @@ -802,7 +810,7 @@ func TestDeriveOptimismEcotoneTxReceipts(t *testing.T) { baseFee := big.NewInt(1000) derivedReceipts := clearComputedFieldsOnReceipts(receipts) // Should error out if we try to process this with a pre-Ecotone config - err := Receipts(derivedReceipts).DeriveFields(params.OptimismTestConfig, blockHash, blockNumber.Uint64(), 0, baseFee, nil, txs) + err := Receipts(derivedReceipts).DeriveFields(bedrockGenesisTestConfig, blockHash, blockNumber.Uint64(), 0, baseFee, nil, txs) if err == nil { t.Fatalf("expected error from deriving ecotone receipts with pre-ecotone config, got none") } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index ff164ed841..9b2edec62e 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -190,6 +190,9 @@ type Config struct { // Clique is allowed for now to live standalone, but ethash is forbidden and can // only exist on already merged networks. func CreateConsensusEngine(config *params.ChainConfig, db ethdb.Database) (consensus.Engine, error) { + if config.Optimism != nil { + return beacon.New(&beacon.OpLegacy{}), nil + } // If proof-of-authority is requested, set it up if config.Clique != nil { return beacon.New(clique.New(config.Clique, db)), nil diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index c2b8748110..2b6f19f447 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -29,6 +29,8 @@ import ( "time" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum" @@ -286,8 +288,21 @@ func newMockHistoricalBackend(t *testing.T) string { func newTestBackend(t *testing.T, enableHistoricalState bool) (*node.Node, []*types.Block) { histAddr := newMockHistoricalBackend(t) - // Generate test chain. - blocks := generateTestChain(enableHistoricalState) + var consensusEngine consensus.Engine + var actualGenesis *core.Genesis + var chainLength int + if enableHistoricalState { + actualGenesis = genesisForHistorical + consensusEngine = beacon.New(ethash.NewFaker()) + chainLength = 10 + } else { + actualGenesis = genesis + consensusEngine = ethash.NewFaker() + chainLength = 2 + } + + // Generate test chain + blocks := generateTestChain(consensusEngine, actualGenesis, chainLength) // Create node n, err := node.New(&node.Config{}) @@ -295,12 +310,6 @@ func newTestBackend(t *testing.T, enableHistoricalState bool) (*node.Node, []*ty t.Fatalf("can't create new node: %v", err) } // Create Ethereum Service - var actualGenesis *core.Genesis - if enableHistoricalState { - actualGenesis = genesisForHistorical - } else { - actualGenesis = genesis - } config := ðconfig.Config{Genesis: actualGenesis} if enableHistoricalState { config.RollupHistoricalRPC = histAddr @@ -310,6 +319,9 @@ func newTestBackend(t *testing.T, enableHistoricalState bool) (*node.Node, []*ty if err != nil { t.Fatalf("can't create new ethereum service: %v", err) } + if enableHistoricalState { // swap to the pre-bedrock consensus-engine that we used to generate the historical blocks + ethservice.BlockChain().Engine().(*beacon.Beacon).SwapInner(ethash.NewFaker()) + } // Import the test chain. if err := n.Start(); err != nil { t.Fatalf("can't start test node: %v", err) @@ -317,30 +329,29 @@ func newTestBackend(t *testing.T, enableHistoricalState bool) (*node.Node, []*ty if _, err := ethservice.BlockChain().InsertChain(blocks[1:]); err != nil { t.Fatalf("can't import test blocks: %v", err) } + if enableHistoricalState { + // Now that we have a filled DB, swap the pre-Bedrock consensus to OpLegacy, + // which does not support re-processing of pre-bedrock data. + ethservice.Engine().(*beacon.Beacon).SwapInner(&beacon.OpLegacy{}) + } return n, blocks } -func generateTestChain(enableHistoricalState bool) []*types.Block { +func generateTestChain(consensusEngine consensus.Engine, genesis *core.Genesis, length int) []*types.Block { generate := func(i int, g *core.BlockGen) { g.OffsetTime(5) g.SetExtra([]byte("test")) if i == 1 { // Test transactions are included in block #2. - if enableHistoricalState { + if genesis.Config.Optimism != nil && genesis.Config.IsBedrock(big.NewInt(1)) { g.AddTx(depositTx) } g.AddTx(testTx1) g.AddTx(testTx2) } } - var actualGenesis *core.Genesis - if enableHistoricalState { - actualGenesis = genesisForHistorical - } else { - actualGenesis = genesis - } - _, blocks, _ := core.GenerateChainWithGenesis(actualGenesis, ethash.NewFaker(), 2, generate) - return append([]*types.Block{actualGenesis.ToBlock()}, blocks...) + _, blocks, _ := core.GenerateChainWithGenesis(genesis, consensusEngine, length, generate) + return append([]*types.Block{genesis.ToBlock()}, blocks...) } func TestEthClientHistoricalBackend(t *testing.T) { diff --git a/fork.yaml b/fork.yaml index eb697a7ae8..a60ae411a0 100644 --- a/fork.yaml +++ b/fork.yaml @@ -80,8 +80,18 @@ def: description: | The Engine API is activated at the Merge transition, with a Total Terminal Difficulty (TTD). The rollup starts post-merge, and thus sets the TTD to 0. + The TTD is always "reached" starting at the bedrock block. globs: - "consensus/beacon/consensus.go" + - title: "Legacy OP-mainnet / OP-goerli header-verification support" + description: | + Pre-Bedrock OP-mainnet and OP-Goerli had differently formatted block-headers, loosely compatible with the geth types (since it was based on Clique). + However, due to differences like the extra-data length (97+ bytes), these legacy block-headers need special verification. + The pre-merge "consensus" fallback is set to this custom but basic verifier, to accept these headers when syncing a pre-bedrock part of the chain, + independent of any clique code or configuration (which may be removed from geth at a later point). + All the custom verifier has to do is accept the headers, as the headers are already verified by block-hash through the reverse-header-sync. + globs: + - "consensus/beacon/oplegacy.go" - title: "Engine API modifications" description: | The Engine API is extended to insert transactions into the block and optionally exclude the tx-pool,