Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snap Sync: consensus: handle legacy pre-bedrock header verification #182

Merged
merged 3 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
trianglesphere marked this conversation as resolved.
Show resolved Hide resolved
}

// 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))
trianglesphere marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading