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

op-supervisor: initialize cross-safe starting point #12841

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
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
13 changes: 12 additions & 1 deletion op-e2e/actions/helpers/l2_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,18 @@ func NewL2Verifier(t Testing, log log.Logger, l1 derive.L1Fetcher,
}

if interopBackend != nil {
sys.Register("interop", interop.NewInteropDeriver(log, cfg, ctx, interopBackend, eng), opts)
l2GenRef, err := eng.L2BlockRefByNumber(ctx, 0)
require.NoError(t, err)
l1GenRef, err := l1.L1BlockRefByNumber(ctx, 0)
require.NoError(t, err)
anchor := interop.AnchorPoint{
CrossSafe: l2GenRef.BlockRef(),
DerivedFrom: l1GenRef,
}
loadAnchor := interop.AnchorPointFn(func(ctx context.Context) (interop.AnchorPoint, error) {
return anchor, nil
})
sys.Register("interop", interop.NewInteropDeriver(log, cfg, ctx, interopBackend, eng, loadAnchor), opts)
}

metrics := &testutils.TestDerivationMetrics{}
Expand Down
27 changes: 26 additions & 1 deletion op-node/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"sync/atomic"
"time"

"github.com/ethereum-optimism/optimism/op-node/rollup/interop"

"github.com/hashicorp/go-multierror"
"github.com/libp2p/go-libp2p/core/peer"

Expand Down Expand Up @@ -425,10 +427,33 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config) error {
n.safeDB = safedb.Disabled
}
n.l2Driver = driver.NewDriver(n.eventSys, n.eventDrain, &cfg.Driver, &cfg.Rollup, n.l2Source, n.l1Source,
n.supervisor, n.beacon, n, n, n.log, n.metrics, cfg.ConfigPersistence, n.safeDB, &cfg.Sync, sequencerConductor, altDA)
n.supervisor, n, n.beacon, n, n, n.log, n.metrics, cfg.ConfigPersistence, n.safeDB, &cfg.Sync, sequencerConductor, altDA)
return nil
}

// LoadAnchorPoint implements the interop.AnchorPointLoader interface,
// to determine the trusted starting point to initialize the op-supervisor with.
// This data is lazy-loaded, as the block-contents are not always available on node startup (e.g. during EL sync).
func (n *OpNode) LoadAnchorPoint(ctx context.Context) (interop.AnchorPoint, error) {
cfg := &n.cfg.Rollup
l2Ref, err := retry.Do(ctx, 3, retry.Exponential(), func() (eth.L2BlockRef, error) {
return n.l2Source.L2BlockRefByHash(ctx, cfg.Genesis.L2.Hash)
})
if err != nil {
return interop.AnchorPoint{}, fmt.Errorf("failed to fetch L2 anchor block: %w", err)
}
l1Ref, err := retry.Do(ctx, 3, retry.Exponential(), func() (eth.L1BlockRef, error) {
return n.l1Source.L1BlockRefByHash(ctx, cfg.Genesis.L1.Hash)
})
if err != nil {
return interop.AnchorPoint{}, fmt.Errorf("failed to fetch L1 anchor block: %w", err)
}
return interop.AnchorPoint{
CrossSafe: l2Ref.BlockRef(),
DerivedFrom: l1Ref,
}, nil
}

func (n *OpNode) initRPCServer(cfg *Config) error {
server, err := newRPCServer(&cfg.RPC, &cfg.Rollup, n.l2Source.L2Client, n.l2Driver, n.safeDB, n.log, n.appVersion, n.metrics)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion op-node/rollup/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ func NewDriver(
l2 L2Chain,
l1 L1Chain,
supervisor interop.InteropBackend, // may be nil pre-interop.
anchorLoader interop.AnchorPointLoader, // may be nil pre-interop.
l1Blobs derive.L1BlobsFetcher,
altSync AltSync,
network Network,
Expand All @@ -182,7 +183,7 @@ func NewDriver(
// It will then be ready to pick up verification work
// as soon as we reach the upgrade time (if the upgrade is not already active).
if cfg.InteropTime != nil {
interopDeriver := interop.NewInteropDeriver(log, cfg, driverCtx, supervisor, l2)
interopDeriver := interop.NewInteropDeriver(log, cfg, driverCtx, supervisor, l2, anchorLoader)
sys.Register("interop", interopDeriver, opts)
}

Expand Down
49 changes: 42 additions & 7 deletions op-node/rollup/interop/interop.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package interop

import (
"context"
"errors"
"fmt"
"sync"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"

"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
Expand All @@ -29,6 +31,8 @@ type InteropBackend interface {

CrossDerivedFrom(ctx context.Context, chainID types.ChainID, derived eth.BlockID) (eth.L1BlockRef, error)

InitializeCrossSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.BlockRef, derived eth.BlockRef) error

UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.BlockRef) error
UpdateLocalSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.BlockRef) error
UpdateFinalizedL1(ctx context.Context, chainID types.ChainID, finalized eth.L1BlockRef) error
Expand All @@ -45,6 +49,21 @@ type L2Source interface {
L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error)
}

type AnchorPoint struct {
CrossSafe eth.BlockRef
DerivedFrom eth.BlockRef
}

type AnchorPointLoader interface {
LoadAnchorPoint(ctx context.Context) (AnchorPoint, error)
}

type AnchorPointFn func(ctx context.Context) (AnchorPoint, error)

func (fn AnchorPointFn) LoadAnchorPoint(ctx context.Context) (AnchorPoint, error) {
return fn(ctx)
}

// InteropDeriver watches for update events (either real changes to block safety,
// or updates published upon request), checks if there is some local data to cross-verify,
// and then checks with the interop-backend, to try to promote to cross-verified safety.
Expand All @@ -61,6 +80,8 @@ type InteropDeriver struct {
backend InteropBackend
l2 L2Source

anchorLoader AnchorPointLoader

emitter event.Emitter

mu sync.Mutex
Expand All @@ -70,14 +91,15 @@ var _ event.Deriver = (*InteropDeriver)(nil)
var _ event.AttachEmitter = (*InteropDeriver)(nil)

func NewInteropDeriver(log log.Logger, cfg *rollup.Config,
driverCtx context.Context, backend InteropBackend, l2 L2Source) *InteropDeriver {
driverCtx context.Context, backend InteropBackend, l2 L2Source, anchorLoader AnchorPointLoader) *InteropDeriver {
return &InteropDeriver{
log: log,
cfg: cfg,
chainID: types.ChainIDFromBig(cfg.L2ChainID),
driverCtx: driverCtx,
backend: backend,
l2: l2,
log: log,
cfg: cfg,
chainID: types.ChainIDFromBig(cfg.L2ChainID),
driverCtx: driverCtx,
backend: backend,
l2: l2,
anchorLoader: anchorLoader,
}
}

Expand Down Expand Up @@ -216,6 +238,19 @@ func (d *InteropDeriver) onCrossSafeUpdateEvent(x engine.CrossSafeUpdateEvent) e
}
result, err := d.backend.SafeView(ctx, d.chainID, view)
if err != nil {
var e rpc.Error
if errors.As(err, &e) && e.ErrorCode() == types.SupervisorUninitializedCrossSafeErrCode {
anchor, err := d.anchorLoader.LoadAnchorPoint(ctx)
if err != nil {
return fmt.Errorf("unable to initialize op-supervisor, failed to get anchorLoader-point: %w", err)
}
d.log.Warn("op-supervisor chain was not initialized, initializing now", "err", err,
"anchorDerivedFrom", anchor.DerivedFrom, "anchorCrossSafe", anchor.CrossSafe)
if err := d.backend.InitializeCrossSafe(ctx, d.chainID, anchor.DerivedFrom, anchor.CrossSafe); err != nil {
return fmt.Errorf("failed to initialize cross-safe: %w", err)
}
return nil
}
return fmt.Errorf("failed to check safe-level view: %w", err)
}
if result.Cross.Number == x.CrossSafe.Number {
Expand Down
53 changes: 41 additions & 12 deletions op-node/rollup/interop/interop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,30 @@ func TestInteropDeriver(t *testing.T) {
L2ChainID: big.NewInt(42),
}
chainID := supervisortypes.ChainIDFromBig(cfg.L2ChainID)
interopDeriver := NewInteropDeriver(logger, cfg, context.Background(), interopBackend, l2Source)
interopDeriver.AttachEmitter(emitter)

rng := rand.New(rand.NewSource(123))
genesisL1 := testutils.RandomBlockRef(rng)
genesisL2 := testutils.RandomL2BlockRef(rng)
anchor := AnchorPoint{
CrossSafe: genesisL2.BlockRef(),
DerivedFrom: genesisL1,
}
loadAnchor := AnchorPointFn(func(ctx context.Context) (AnchorPoint, error) {
return anchor, nil
})
interopDeriver := NewInteropDeriver(logger, cfg, context.Background(), interopBackend, l2Source, loadAnchor)
interopDeriver.AttachEmitter(emitter)

t.Run("local-unsafe blocks push to supervisor and trigger cross-unsafe attempts", func(t *testing.T) {
emitter.ExpectOnce(engine.RequestCrossUnsafeEvent{})
unsafeHead := testutils.RandomL2BlockRef(rng)
unsafeHead := testutils.NextRandomL2Ref(rng, 2, genesisL2, genesisL2.L1Origin)
interopBackend.ExpectUpdateLocalUnsafe(chainID, unsafeHead.BlockRef(), nil)
interopDeriver.OnEvent(engine.UnsafeUpdateEvent{Ref: unsafeHead})
emitter.AssertExpectations(t)
interopBackend.AssertExpectations(t)
})
t.Run("establish cross-unsafe", func(t *testing.T) {
oldCrossUnsafe := testutils.RandomL2BlockRef(rng)
oldCrossUnsafe := testutils.NextRandomL2Ref(rng, 2, genesisL2, genesisL2.L1Origin)
nextCrossUnsafe := testutils.NextRandomL2Ref(rng, 2, oldCrossUnsafe, oldCrossUnsafe.L1Origin)
lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, nextCrossUnsafe, nextCrossUnsafe.L1Origin)
localView := supervisortypes.ReferenceView{
Expand All @@ -68,7 +78,7 @@ func TestInteropDeriver(t *testing.T) {
l2Source.AssertExpectations(t)
})
t.Run("deny cross-unsafe", func(t *testing.T) {
oldCrossUnsafe := testutils.RandomL2BlockRef(rng)
oldCrossUnsafe := testutils.NextRandomL2Ref(rng, 2, genesisL2, genesisL2.L1Origin)
nextCrossUnsafe := testutils.NextRandomL2Ref(rng, 2, oldCrossUnsafe, oldCrossUnsafe.L1Origin)
lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, nextCrossUnsafe, nextCrossUnsafe.L1Origin)
localView := supervisortypes.ReferenceView{
Expand All @@ -91,8 +101,8 @@ func TestInteropDeriver(t *testing.T) {
})
t.Run("local-safe blocks push to supervisor and trigger cross-safe attempts", func(t *testing.T) {
emitter.ExpectOnce(engine.RequestCrossSafeEvent{})
derivedFrom := testutils.RandomBlockRef(rng)
localSafe := testutils.RandomL2BlockRef(rng)
derivedFrom := testutils.NextRandomRef(rng, genesisL1)
localSafe := testutils.NextRandomL2Ref(rng, 2, genesisL2, genesisL2.L1Origin)
interopBackend.ExpectUpdateLocalSafe(chainID, derivedFrom, localSafe.BlockRef(), nil)
interopDeriver.OnEvent(engine.InteropPendingSafeChangedEvent{
Ref: localSafe,
Expand All @@ -101,9 +111,28 @@ func TestInteropDeriver(t *testing.T) {
emitter.AssertExpectations(t)
interopBackend.AssertExpectations(t)
})
t.Run("initialize cross-safe", func(t *testing.T) {
oldCrossSafe := testutils.NextRandomL2Ref(rng, 2, genesisL2, genesisL2.L1Origin)
nextCrossSafe := testutils.NextRandomL2Ref(rng, 2, oldCrossSafe, oldCrossSafe.L1Origin)
lastLocalSafe := testutils.NextRandomL2Ref(rng, 2, nextCrossSafe, nextCrossSafe.L1Origin)
localView := supervisortypes.ReferenceView{
Local: lastLocalSafe.ID(),
Cross: oldCrossSafe.ID(),
}
supervisorView := supervisortypes.ReferenceView{}
interopBackend.ExpectSafeView(chainID, localView, supervisorView, supervisortypes.ErrUninitializedCrossSafeErr)
interopBackend.ExpectInitializeCrossSafe(chainID, anchor.DerivedFrom, anchor.CrossSafe, nil)
interopDeriver.OnEvent(engine.CrossSafeUpdateEvent{
CrossSafe: oldCrossSafe,
LocalSafe: lastLocalSafe,
})
interopBackend.AssertExpectations(t)
emitter.AssertExpectations(t)
l2Source.AssertExpectations(t)
})
t.Run("establish cross-safe", func(t *testing.T) {
derivedFrom := testutils.RandomBlockRef(rng)
oldCrossSafe := testutils.RandomL2BlockRef(rng)
derivedFrom := testutils.NextRandomRef(rng, genesisL1)
oldCrossSafe := testutils.NextRandomL2Ref(rng, 2, genesisL2, genesisL2.L1Origin)
nextCrossSafe := testutils.NextRandomL2Ref(rng, 2, oldCrossSafe, oldCrossSafe.L1Origin)
lastLocalSafe := testutils.NextRandomL2Ref(rng, 2, nextCrossSafe, nextCrossSafe.L1Origin)
localView := supervisortypes.ReferenceView{
Expand Down Expand Up @@ -135,7 +164,7 @@ func TestInteropDeriver(t *testing.T) {
l2Source.AssertExpectations(t)
})
t.Run("deny cross-safe", func(t *testing.T) {
oldCrossSafe := testutils.RandomL2BlockRef(rng)
oldCrossSafe := testutils.NextRandomL2Ref(rng, 2, genesisL2, genesisL2.L1Origin)
nextCrossSafe := testutils.NextRandomL2Ref(rng, 2, oldCrossSafe, oldCrossSafe.L1Origin)
lastLocalSafe := testutils.NextRandomL2Ref(rng, 2, nextCrossSafe, nextCrossSafe.L1Origin)
localView := supervisortypes.ReferenceView{
Expand Down Expand Up @@ -166,7 +195,7 @@ func TestInteropDeriver(t *testing.T) {
interopBackend.AssertExpectations(t)
})
t.Run("next L2 finalized block", func(t *testing.T) {
oldFinalizedL2 := testutils.RandomL2BlockRef(rng)
oldFinalizedL2 := testutils.NextRandomL2Ref(rng, 2, genesisL2, genesisL2.L1Origin)
intermediateL2 := testutils.NextRandomL2Ref(rng, 2, oldFinalizedL2, oldFinalizedL2.L1Origin)
nextFinalizedL2 := testutils.NextRandomL2Ref(rng, 2, intermediateL2, intermediateL2.L1Origin)
emitter.ExpectOnce(engine.PromoteFinalizedEvent{
Expand All @@ -179,7 +208,7 @@ func TestInteropDeriver(t *testing.T) {
interopBackend.AssertExpectations(t)
})
t.Run("keep L2 finalized block", func(t *testing.T) {
oldFinalizedL2 := testutils.RandomL2BlockRef(rng)
oldFinalizedL2 := testutils.NextRandomL2Ref(rng, 2, genesisL2, genesisL2.L1Origin)
interopBackend.ExpectFinalized(chainID, oldFinalizedL2.ID(), nil)
interopDeriver.OnEvent(engine.FinalizedUpdateEvent{Ref: oldFinalizedL2})
emitter.AssertExpectations(t) // no PromoteFinalizedEvent
Expand Down
10 changes: 10 additions & 0 deletions op-service/sources/supervisor_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ func (cl *SupervisorClient) CrossDerivedFrom(ctx context.Context, chainID types.
return result, err
}

func (cl *SupervisorClient) InitializeCrossSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.BlockRef, derived eth.BlockRef) error {
return cl.client.CallContext(
ctx,
nil,
"supervisor_initializeCrossSafe",
chainID,
derivedFrom,
derived)
}

func (cl *SupervisorClient) UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.BlockRef) error {
return cl.client.CallContext(
ctx,
Expand Down
9 changes: 9 additions & 0 deletions op-service/testutils/mock_interop_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ type MockInteropBackend struct {
Mock mock.Mock
}

func (m *MockInteropBackend) InitializeCrossSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.BlockRef, derived eth.BlockRef) error {
result := m.Mock.MethodCalled("InitializeCrossSafe", chainID, derivedFrom, derived)
return *result.Get(0).(*error)
}

func (m *MockInteropBackend) ExpectInitializeCrossSafe(chainID types.ChainID, derivedFrom eth.BlockRef, derived eth.BlockRef, err error) {
m.Mock.On("InitializeCrossSafe", chainID, derivedFrom, derived).Once().Return(&err)
}

func (m *MockInteropBackend) UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) {
result := m.Mock.MethodCalled("UnsafeView", chainID, unsafe)
return result.Get(0).(types.ReferenceView), *result.Get(1).(*error)
Expand Down
7 changes: 7 additions & 0 deletions op-supervisor/supervisor/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@ func (su *SupervisorBackend) SafeView(ctx context.Context, chainID types.ChainID
}
_, crossSafe, err := su.chainDBs.CrossSafe(chainID)
if err != nil {
if errors.Is(err, types.ErrFuture) { // no data? -> uninitialized cross-safe
return types.ReferenceView{}, types.ErrUninitializedCrossSafeErr
}
return types.ReferenceView{}, fmt.Errorf("failed to get cross-safe head: %w", err)
}

Expand Down Expand Up @@ -421,6 +424,10 @@ func (su *SupervisorBackend) CrossDerivedFrom(ctx context.Context, chainID types
// Update methods
// ----------------------------

func (u *SupervisorBackend) InitializeCrossSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.BlockRef, derived eth.BlockRef) error {
return u.chainDBs.InitializeCrossSafe(chainID, derivedFrom, derived)
}

func (su *SupervisorBackend) UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.BlockRef) error {
ch, ok := su.chainProcessors.Get(chainID)
if !ok {
Expand Down
12 changes: 10 additions & 2 deletions op-supervisor/supervisor/backend/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,14 @@ func TestBackendLifetime(t *testing.T) {

src := &testutils.MockL1Source{}

blockXFrom := eth.BlockRef{
Hash: common.Hash{0x01, 0x01, 0xaa},
Number: 1234,
ParentHash: common.Hash{0x01, 0x00, 0xaa},
Time: 9000,
}
blockX := eth.BlockRef{
Hash: common.Hash{0xaa},
Hash: common.Hash{0x02, 0, 0xaa},
Number: 0,
ParentHash: common.Hash{}, // genesis has no parent hash
Time: 10000,
Expand Down Expand Up @@ -93,6 +99,8 @@ func TestBackendLifetime(t *testing.T) {
_, err = b.UnsafeView(context.Background(), chainA, types.ReferenceView{})
require.ErrorIs(t, err, types.ErrFuture, "no data yet, need local-unsafe")

require.NoError(t, b.InitializeCrossSafe(context.Background(), chainA, blockXFrom, blockX))

src.ExpectL1BlockRefByNumber(0, blockX, nil)
src.ExpectFetchReceipts(blockX.Hash, &testutils.MockBlockInfo{
InfoHash: blockX.Hash,
Expand Down Expand Up @@ -121,7 +129,7 @@ func TestBackendLifetime(t *testing.T) {
proc.ProcessToHead()

_, err = b.UnsafeView(context.Background(), chainA, types.ReferenceView{})
require.ErrorIs(t, err, types.ErrFuture, "still no data yet, need cross-unsafe")
require.NoError(t, err, "cross-unsafe should default to cross-safe")

err = b.chainDBs.UpdateCrossUnsafe(chainA, types.BlockSeal{
Hash: blockX.Hash,
Expand Down
1 change: 1 addition & 0 deletions op-supervisor/supervisor/backend/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type LogStorage interface {
}

type LocalDerivedFromStorage interface {
IsEmpty() bool
First() (derivedFrom types.BlockSeal, derived types.BlockSeal, err error)
Latest() (derivedFrom types.BlockSeal, derived types.BlockSeal, err error)
AddDerived(derivedFrom eth.BlockRef, derived eth.BlockRef) error
Expand Down
Loading