Skip to content

Commit

Permalink
Merge pull request ethereum#128 from ethereum-optimism/superchain-ver…
Browse files Browse the repository at this point in the history
…sion-signal

introduce engine_signalSuperchainV1
  • Loading branch information
protolambda authored Sep 14, 2023
2 parents 3cea832 + 92eb9d4 commit b84ba11
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 6 deletions.
1 change: 1 addition & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ var (
utils.RollupHistoricalRPCTimeoutFlag,
utils.RollupDisableTxPoolGossipFlag,
utils.RollupComputePendingBlock,
utils.RollupHaltOnIncompatibleProtocolVersionFlag,
configFileFlag,
}, utils.NetworkFlags, utils.DatabasePathFlags)

Expand Down
6 changes: 6 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,11 @@ var (
Usage: "By default the pending block equals the latest block to save resources and not leak txs from the tx-pool, this flag enables computing of the pending block from the tx-pool instead.",
Category: flags.RollupCategory,
}
RollupHaltOnIncompatibleProtocolVersionFlag = &cli.StringFlag{
Name: "beta.rollup.halt",
Usage: "Opt-in option to halt on incompatible protocol version requirements of the given level (major/minor/patch/none), as signaled through the Engine API by the rollup node",
Category: flags.RollupCategory,
}

// Metrics flags
MetricsEnabledFlag = &cli.BoolFlag{
Expand Down Expand Up @@ -1848,6 +1853,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
}
cfg.RollupDisableTxPoolGossip = ctx.Bool(RollupDisableTxPoolGossipFlag.Name)
cfg.RollupDisableTxPoolAdmission = cfg.RollupSequencerHTTP != "" && !ctx.Bool(RollupEnableTxPoolAdmissionFlag.Name)
cfg.RollupHaltOnIncompatibleProtocolVersion = ctx.String(RollupHaltOnIncompatibleProtocolVersionFlag.Name)
// Override any default configs for hard coded networks.
switch {
case ctx.Bool(MainnetFlag.Name):
Expand Down
33 changes: 33 additions & 0 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ type Ethereum struct {
lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase)

shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully

nodeCloser func() error
}

// New creates a new Ethereum object (including the
Expand Down Expand Up @@ -162,6 +164,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
bloomIndexer: core.NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms),
p2pServer: stack.Server(),
shutdownTracker: shutdowncheck.NewShutdownTracker(chainDb),
nodeCloser: stack.Close,
}

bcVersion := rawdb.ReadDatabaseVersion(chainDb)
Expand Down Expand Up @@ -575,3 +578,33 @@ func (s *Ethereum) Stop() error {

return nil
}

// HandleRequiredProtocolVersion handles the protocol version signal. This implements opt-in halting,
// the protocol version data is already logged and metered when signaled through the Engine API.
func (s *Ethereum) HandleRequiredProtocolVersion(required params.ProtocolVersion) error {
var needLevel int
switch s.config.RollupHaltOnIncompatibleProtocolVersion {
case "major":
needLevel = 3
case "minor":
needLevel = 2
case "patch":
needLevel = 1
default:
return nil // do not consider halting if not configured to
}
haveLevel := 0
switch params.OPStackSupport.Compare(required) {
case params.OutdatedMajor:
haveLevel = 3
case params.OutdatedMinor:
haveLevel = 2
case params.OutdatedPatch:
haveLevel = 1
}
if haveLevel >= needLevel { // halt if we opted in to do so at this granularity
log.Error("Opted to halt, unprepared for protocol change", "required", required, "local", params.OPStackSupport)
return s.nodeCloser()
}
return nil
}
7 changes: 6 additions & 1 deletion eth/catalyst/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,11 @@ func TestEth2DeepReorg(t *testing.T) {

// startEthService creates a full node instance for testing.
func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) {
ethcfg := &ethconfig.Config{Genesis: genesis, SyncMode: downloader.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256}
return startEthServiceWithConfigFn(t, blocks, ethcfg)
}

func startEthServiceWithConfigFn(t *testing.T, blocks []*types.Block, ethcfg *ethconfig.Config) (*node.Node, *eth.Ethereum) {
t.Helper()

n, err := node.New(&node.Config{
Expand All @@ -440,7 +445,7 @@ func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block)
t.Fatal("can't create node:", err)
}

ethcfg := &ethconfig.Config{Genesis: genesis, SyncMode: downloader.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256}
// default eth config is moved to startEthService
ethservice, err := eth.New(n, ethcfg)
if err != nil {
t.Fatal("can't create eth service:", err)
Expand Down
64 changes: 64 additions & 0 deletions eth/catalyst/superchain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package catalyst

import (
"fmt"

"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/params"
)

var (
requiredProtocolDeltaGauge = metrics.NewRegisteredGauge("superchain/required/delta", nil)
recommendedProtocolDeltaGauge = metrics.NewRegisteredGauge("superchain/recommended/delta", nil)
)

type SuperchainSignal struct {
Recommended params.ProtocolVersion `json:"recommended"`
Required params.ProtocolVersion `json:"required"`
}

func (api *ConsensusAPI) SignalSuperchainV1(signal *SuperchainSignal) (params.ProtocolVersion, error) {
if signal == nil {
log.Info("Received empty superchain version signal", "local", params.OPStackSupport)
return params.OPStackSupport, nil
}
// update metrics and log any warnings/info
requiredProtocolDeltaGauge.Update(int64(params.OPStackSupport.Compare(signal.Required)))
recommendedProtocolDeltaGauge.Update(int64(params.OPStackSupport.Compare(signal.Recommended)))
logger := log.New("local", params.OPStackSupport, "required", signal.Required, "recommended", signal.Recommended)
LogProtocolVersionSupport(logger, params.OPStackSupport, signal.Recommended, "recommended")
LogProtocolVersionSupport(logger, params.OPStackSupport, signal.Required, "required")

if err := api.eth.HandleRequiredProtocolVersion(signal.Required); err != nil {
log.Error("Failed to handle required protocol version", "err", err, "required", signal.Required)
return params.OPStackSupport, err
}

return params.OPStackSupport, nil
}

func LogProtocolVersionSupport(logger log.Logger, local, other params.ProtocolVersion, name string) {
switch local.Compare(other) {
case params.AheadMajor:
logger.Info(fmt.Sprintf("Ahead with major %s protocol version change", name))
case params.AheadMinor, params.AheadPatch, params.AheadPrerelease:
logger.Debug(fmt.Sprintf("Ahead with compatible %s protocol version change", name))
case params.Matching:
logger.Debug(fmt.Sprintf("Latest %s protocol version is supported", name))
case params.OutdatedMajor:
logger.Error(fmt.Sprintf("Outdated with major %s protocol change", name))
case params.OutdatedMinor:
logger.Warn(fmt.Sprintf("Outdated with minor backward-compatible %s protocol change", name))
case params.OutdatedPatch:
logger.Info(fmt.Sprintf("Outdated with support backward-compatible %s protocol change", name))
case params.OutdatedPrerelease:
logger.Debug(fmt.Sprintf("New %s protocol pre-release is available", name))
case params.DiffBuild:
logger.Debug(fmt.Sprintf("Ignoring %s protocolversion signal, local build is different", name))
case params.DiffVersionType:
logger.Warn(fmt.Sprintf("Failed to recognize %s protocol version signal version-type", name))
case params.EmptyVersion:
logger.Debug(fmt.Sprintf("No %s protocol version available to check", name))
}
}
100 changes: 100 additions & 0 deletions eth/catalyst/superchain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package catalyst

import (
"testing"
"time"

"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
)

func TestSignalSuperchainV1(t *testing.T) {
genesis, preMergeBlocks := generateMergeChain(2, false)
n, ethservice := startEthService(t, genesis, preMergeBlocks)
defer n.Close()
api := NewConsensusAPI(ethservice)
t.Run("matching", func(t *testing.T) {
out, err := api.SignalSuperchainV1(&SuperchainSignal{
Recommended: params.OPStackSupport,
Required: params.OPStackSupport,
})
if err != nil {
t.Fatalf("failed to process signal: %v", err)
}
if out != params.OPStackSupport {
t.Fatalf("expected %s but got %s", params.OPStackSupport, out)
}
})
t.Run("null_arg", func(t *testing.T) {
out, err := api.SignalSuperchainV1(nil)
if err != nil {
t.Fatalf("failed to process signal: %v", err)
}
if out != params.OPStackSupport {
t.Fatalf("expected %s but got %s", params.OPStackSupport, out)
}
})
}

func TestSignalSuperchainV1Halt(t *testing.T) {
testCases := []struct {
cfg string
bump string
halt bool
}{
{"none", "major", false},
{"major", "major", true},
{"minor", "major", true},
{"patch", "major", true},
{"major", "minor", false},
{"minor", "minor", true},
{"patch", "minor", true},
{"major", "patch", false},
{"minor", "patch", false},
{"patch", "patch", true},
}
for _, tc := range testCases {
t.Run(tc.cfg+"_"+tc.bump, func(t *testing.T) {
genesis, preMergeBlocks := generateMergeChain(2, false)
ethcfg := &ethconfig.Config{Genesis: genesis, SyncMode: downloader.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256}
ethcfg.RollupHaltOnIncompatibleProtocolVersion = tc.cfg // opt-in to halting (or not)
n, ethservice := startEthServiceWithConfigFn(t, preMergeBlocks, ethcfg)
defer n.Close() // close at the end, regardless of any prior (failed) closing
api := NewConsensusAPI(ethservice)
_, build, major, minor, patch, preRelease := params.OPStackSupport.Parse()
majorSignal, minorSignal, patchSignal := major, minor, patch
switch tc.bump {
case "major":
majorSignal += 1
case "minor":
minorSignal += 1
case "patch":
patchSignal += 1
}
out, err := api.SignalSuperchainV1(&SuperchainSignal{
Recommended: params.OPStackSupport, // required version change should be enough
Required: params.ProtocolVersionV0{Build: build, Major: majorSignal, Minor: minorSignal, Patch: patchSignal, PreRelease: preRelease}.Encode(),
})
if err != nil {
t.Fatalf("failed to process signal: %v", err)
}
if out != params.OPStackSupport {
t.Fatalf("expected %s but got %s", params.OPStackSupport, out)
}
closeErr := n.Close()
if tc.halt {
// assert no halt by closing, and not getting any error
if closeErr == nil {
t.Fatalf("expected not to have closed already, but just closed without error")
}
} else {
// assert halt by closing again, and seeing if things error
if closeErr == node.ErrNodeStopped {
t.Fatalf("expected to have already closed and get a ErrNodeStopped error, but got %v", closeErr)
}
}
})
}
}
11 changes: 6 additions & 5 deletions eth/ethconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,12 @@ type Config struct {
OverrideOptimismRegolith *uint64 `toml:",omitempty"`
OverrideOptimism *bool

RollupSequencerHTTP string
RollupHistoricalRPC string
RollupHistoricalRPCTimeout time.Duration
RollupDisableTxPoolGossip bool
RollupDisableTxPoolAdmission bool
RollupSequencerHTTP string
RollupHistoricalRPC string
RollupHistoricalRPCTimeout time.Duration
RollupDisableTxPoolGossip bool
RollupDisableTxPoolAdmission bool
RollupHaltOnIncompatibleProtocolVersion string
}

// CreateConsensusEngine creates a consensus engine for the given chain config.
Expand Down
Loading

0 comments on commit b84ba11

Please sign in to comment.