Skip to content

Commit

Permalink
Snap Sync: consensus: handle legacy pre-bedrock header verification (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>
  • Loading branch information
protolambda and sebastianst authored Jan 22, 2024
1 parent d5f142e commit ec72ec6
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 23 deletions.
25 changes: 25 additions & 0 deletions consensus/beacon/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down
74 changes: 74 additions & 0 deletions consensus/beacon/oplegacy.go
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion core/chain_makers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion core/types/receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions core/types/receipt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = &params.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
Expand Down Expand Up @@ -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 <nil>", err)
}
Expand Down Expand Up @@ -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")
}
Expand Down
3 changes: 3 additions & 0 deletions eth/ethconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 29 additions & 18 deletions ethclient/ethclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -286,21 +288,28 @@ 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{})
if err != nil {
t.Fatalf("can't create new node: %v", err)
}
// Create Ethereum Service
var actualGenesis *core.Genesis
if enableHistoricalState {
actualGenesis = genesisForHistorical
} else {
actualGenesis = genesis
}
config := &ethconfig.Config{Genesis: actualGenesis}
if enableHistoricalState {
config.RollupHistoricalRPC = histAddr
Expand All @@ -310,37 +319,39 @@ 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)
}
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) {
Expand Down
10 changes: 10 additions & 0 deletions fork.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit ec72ec6

Please sign in to comment.