Commit 4a5684ca authored by Sam Stokes's avatar Sam Stokes Committed by GitHub

Log when a hardfork is first activated (#10530)

* Log when a hardfork is first activated

* Initialize nextFork so duplicate logs are not created after op-node resets

* Move nextFork field from rollup.Config to rollup.ChainSpec

* Add unit tests for CheckForkActivation
parent 017faf22
package rollup
import (
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/log"
)
// maxChannelBankSize is the amount of memory space, in number of bytes,
// till the bank is pruned by removing channels, starting with the oldest channel.
// It's value is changed with the Fjord network upgrade.
......@@ -26,12 +31,36 @@ const SafeMaxRLPBytesPerChannel = maxRLPBytesPerChannelBedrock
// ChainSpec instead of reading the rollup configuration field directly.
const maxSequencerDriftFjord = 1800
type ForkName string
const (
Bedrock ForkName = "bedrock"
Regolith ForkName = "regolith"
Canyon ForkName = "canyon"
Delta ForkName = "delta"
Ecotone ForkName = "ecotone"
Fjord ForkName = "fjord"
Interop ForkName = "interop"
None ForkName = "none"
)
var nextFork = map[ForkName]ForkName{
Bedrock: Regolith,
Regolith: Canyon,
Canyon: Delta,
Delta: Ecotone,
Ecotone: Fjord,
Fjord: Interop,
Interop: None,
}
type ChainSpec struct {
config *Config
config *Config
currentFork ForkName
}
func NewChainSpec(config *Config) *ChainSpec {
return &ChainSpec{config}
return &ChainSpec{config: config}
}
// IsCanyon returns true if t >= canyon_time
......@@ -77,3 +106,56 @@ func (s *ChainSpec) MaxSequencerDrift(t uint64) uint64 {
}
return s.config.MaxSequencerDrift
}
func (s *ChainSpec) CheckForkActivation(log log.Logger, block eth.L2BlockRef) {
if s.currentFork == Interop {
return
}
if s.currentFork == "" {
// Initialize currentFork if it is not set yet
s.currentFork = Bedrock
if s.config.IsRegolith(block.Time) {
s.currentFork = Regolith
}
if s.config.IsCanyon(block.Time) {
s.currentFork = Canyon
}
if s.config.IsDelta(block.Time) {
s.currentFork = Delta
}
if s.config.IsEcotone(block.Time) {
s.currentFork = Ecotone
}
if s.config.IsFjord(block.Time) {
s.currentFork = Fjord
}
if s.config.IsInterop(block.Time) {
s.currentFork = Interop
}
log.Info("Current hardfork version detected", "forkName", s.currentFork)
return
}
foundActivationBlock := false
switch nextFork[s.currentFork] {
case Regolith:
foundActivationBlock = s.config.IsRegolithActivationBlock(block.Time)
case Canyon:
foundActivationBlock = s.config.IsCanyonActivationBlock(block.Time)
case Delta:
foundActivationBlock = s.config.IsDeltaActivationBlock(block.Time)
case Ecotone:
foundActivationBlock = s.config.IsEcotoneActivationBlock(block.Time)
case Fjord:
foundActivationBlock = s.config.IsFjordActivationBlock(block.Time)
case Interop:
foundActivationBlock = s.config.IsInteropActivationBlock(block.Time)
}
if foundActivationBlock {
s.currentFork = nextFork[s.currentFork]
log.Info("Detected hardfork activation block", "forkName", s.currentFork, "timestamp", block.Time, "blockNum", block.Number, "hash", block.Hash)
}
}
......@@ -5,8 +5,10 @@ import (
"testing"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slog"
)
func u64ptr(n uint64) *uint64 {
......@@ -142,3 +144,55 @@ func TestChainSpec_MaxSequencerDrift(t *testing.T) {
})
}
}
func TestCheckForkActivation(t *testing.T) {
tests := []struct {
name string
block eth.L2BlockRef
expectedCurrentFork ForkName
expectedLog string
}{
{
name: "Regolith activation",
block: eth.L2BlockRef{Time: 10, Number: 5, Hash: common.Hash{0x5}},
expectedCurrentFork: Regolith,
expectedLog: "Detected hardfork activation block",
},
{
name: "Still Regolith",
block: eth.L2BlockRef{Time: 11, Number: 6, Hash: common.Hash{0x6}},
expectedCurrentFork: Regolith,
expectedLog: "",
},
{
name: "Canyon activation",
block: eth.L2BlockRef{Time: 20, Number: 7, Hash: common.Hash{0x7}},
expectedCurrentFork: Canyon,
expectedLog: "Detected hardfork activation block",
},
{
name: "No more hardforks",
block: eth.L2BlockRef{Time: 700, Number: 8, Hash: common.Hash{0x8}},
expectedCurrentFork: Fjord,
expectedLog: "",
},
}
hasInfoLevel := testlog.NewLevelFilter(slog.LevelInfo)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lgr, logs := testlog.CaptureLogger(t, slog.LevelDebug)
chainSpec := NewChainSpec(&testConfig)
// First call initializes chainSpec.currentFork value
chainSpec.CheckForkActivation(lgr, eth.L2BlockRef{Time: tt.block.Time - 1, Number: 1, Hash: common.Hash{0x1}})
chainSpec.CheckForkActivation(lgr, tt.block)
require.Equal(t, tt.expectedCurrentFork, chainSpec.currentFork)
if tt.expectedLog != "" {
require.NotNil(t, logs.FindLog(
hasInfoLevel,
testlog.NewMessageContainsFilter(tt.expectedLog)))
}
})
}
}
......@@ -50,6 +50,7 @@ type EngineController struct {
metrics Metrics
syncMode sync.Mode
syncStatus syncStatusEnum
chainSpec *rollup.ChainSpec
rollupCfg *rollup.Config
elStart time.Time
clock clock.Clock
......@@ -85,6 +86,7 @@ func NewEngineController(engine ExecEngine, log log.Logger, metrics Metrics, rol
engine: engine,
log: log,
metrics: metrics,
chainSpec: rollup.NewChainSpec(rollupCfg),
rollupCfg: rollupCfg,
syncMode: syncMode,
syncStatus: syncStatus,
......@@ -149,6 +151,7 @@ func (e *EngineController) SetUnsafeHead(r eth.L2BlockRef) {
e.metrics.RecordL2Ref("l2_unsafe", r)
e.unsafeHead = r
e.needFCUCall = true
e.chainSpec.CheckForkActivation(e.log, r)
}
// SetBackupUnsafeL2Head implements LocalEngineControl.
......
......@@ -312,16 +312,16 @@ func (cfg *Config) Check() error {
return err
}
if err := checkFork(cfg.RegolithTime, cfg.CanyonTime, "regolith", "canyon"); err != nil {
if err := checkFork(cfg.RegolithTime, cfg.CanyonTime, Regolith, Canyon); err != nil {
return err
}
if err := checkFork(cfg.CanyonTime, cfg.DeltaTime, "canyon", "delta"); err != nil {
if err := checkFork(cfg.CanyonTime, cfg.DeltaTime, Canyon, Delta); err != nil {
return err
}
if err := checkFork(cfg.DeltaTime, cfg.EcotoneTime, "delta", "ecotone"); err != nil {
if err := checkFork(cfg.DeltaTime, cfg.EcotoneTime, Delta, Ecotone); err != nil {
return err
}
if err := checkFork(cfg.EcotoneTime, cfg.FjordTime, "ecotone", "fjord"); err != nil {
if err := checkFork(cfg.EcotoneTime, cfg.FjordTime, Ecotone, Fjord); err != nil {
return err
}
......@@ -354,7 +354,7 @@ func validatePlasmaConfig(cfg *Config) error {
}
// checkFork checks that fork A is before or at the same time as fork B
func checkFork(a, b *uint64, aName, bName string) error {
func checkFork(a, b *uint64, aName, bName ForkName) error {
if a == nil && b == nil {
return nil
}
......@@ -394,14 +394,6 @@ func (c *Config) IsEcotone(timestamp uint64) bool {
return c.EcotoneTime != nil && timestamp >= *c.EcotoneTime
}
// IsEcotoneActivationBlock returns whether the specified block is the first block subject to the
// Ecotone upgrade. Ecotone activation at genesis does not count.
func (c *Config) IsEcotoneActivationBlock(l2BlockTime uint64) bool {
return c.IsEcotone(l2BlockTime) &&
l2BlockTime >= c.BlockTime &&
!c.IsEcotone(l2BlockTime-c.BlockTime)
}
// IsFjord returns true if the Fjord hardfork is active at or past the given timestamp.
func (c *Config) IsFjord(timestamp uint64) bool {
return c.FjordTime != nil && timestamp >= *c.FjordTime
......@@ -420,6 +412,38 @@ func (c *Config) IsInterop(timestamp uint64) bool {
return c.InteropTime != nil && timestamp >= *c.InteropTime
}
func (c *Config) IsRegolithActivationBlock(l2BlockTime uint64) bool {
return c.IsRegolith(l2BlockTime) &&
l2BlockTime >= c.BlockTime &&
!c.IsRegolith(l2BlockTime-c.BlockTime)
}
func (c *Config) IsCanyonActivationBlock(l2BlockTime uint64) bool {
return c.IsCanyon(l2BlockTime) &&
l2BlockTime >= c.BlockTime &&
!c.IsCanyon(l2BlockTime-c.BlockTime)
}
func (c *Config) IsDeltaActivationBlock(l2BlockTime uint64) bool {
return c.IsDelta(l2BlockTime) &&
l2BlockTime >= c.BlockTime &&
!c.IsDelta(l2BlockTime-c.BlockTime)
}
// IsEcotoneActivationBlock returns whether the specified block is the first block subject to the
// Ecotone upgrade. Ecotone activation at genesis does not count.
func (c *Config) IsEcotoneActivationBlock(l2BlockTime uint64) bool {
return c.IsEcotone(l2BlockTime) &&
l2BlockTime >= c.BlockTime &&
!c.IsEcotone(l2BlockTime-c.BlockTime)
}
func (c *Config) IsInteropActivationBlock(l2BlockTime uint64) bool {
return c.IsInterop(l2BlockTime) &&
l2BlockTime >= c.BlockTime &&
!c.IsInterop(l2BlockTime-c.BlockTime)
}
// ForkchoiceUpdatedVersion returns the EngineAPIMethod suitable for the chain hard fork version.
func (c *Config) ForkchoiceUpdatedVersion(attr *eth.PayloadAttributes) eth.EngineAPIMethod {
if attr == nil {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment