diff --git a/op-e2e/actions/helpers/l2_verifier.go b/op-e2e/actions/helpers/l2_verifier.go index 6f9d80169875..895f62caf2c5 100644 --- a/op-e2e/actions/helpers/l2_verifier.go +++ b/op-e2e/actions/helpers/l2_verifier.go @@ -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{} diff --git a/op-node/node/node.go b/op-node/node/node.go index 298c98aa2b18..3b6e6af66c67 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -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" @@ -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 { diff --git a/op-node/rollup/driver/driver.go b/op-node/rollup/driver/driver.go index 1fd751846cf3..c0cd7800ef0f 100644 --- a/op-node/rollup/driver/driver.go +++ b/op-node/rollup/driver/driver.go @@ -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, @@ -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) } diff --git a/op-node/rollup/interop/interop.go b/op-node/rollup/interop/interop.go index a4342b6a19f6..f926c7d66cff 100644 --- a/op-node/rollup/interop/interop.go +++ b/op-node/rollup/interop/interop.go @@ -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" @@ -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 @@ -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. @@ -61,6 +80,8 @@ type InteropDeriver struct { backend InteropBackend l2 L2Source + anchorLoader AnchorPointLoader + emitter event.Emitter mu sync.Mutex @@ -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, } } @@ -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 { diff --git a/op-node/rollup/interop/interop_test.go b/op-node/rollup/interop/interop_test.go index d4fb87f0dd92..b78e246de79d 100644 --- a/op-node/rollup/interop/interop_test.go +++ b/op-node/rollup/interop/interop_test.go @@ -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{ @@ -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{ @@ -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, @@ -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{ @@ -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{ @@ -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{ @@ -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 diff --git a/op-service/sources/supervisor_client.go b/op-service/sources/supervisor_client.go index d6191b9cfb20..feb7f3747933 100644 --- a/op-service/sources/supervisor_client.go +++ b/op-service/sources/supervisor_client.go @@ -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, diff --git a/op-service/testutils/mock_interop_backend.go b/op-service/testutils/mock_interop_backend.go index af6762c204cd..83b49d5fc5c6 100644 --- a/op-service/testutils/mock_interop_backend.go +++ b/op-service/testutils/mock_interop_backend.go @@ -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) diff --git a/op-supervisor/supervisor/backend/backend.go b/op-supervisor/supervisor/backend/backend.go index 7d368cb40f15..a9a1a7a70e97 100644 --- a/op-supervisor/supervisor/backend/backend.go +++ b/op-supervisor/supervisor/backend/backend.go @@ -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) } @@ -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 { diff --git a/op-supervisor/supervisor/backend/backend_test.go b/op-supervisor/supervisor/backend/backend_test.go index c104bf0bae5d..32c80ae505bd 100644 --- a/op-supervisor/supervisor/backend/backend_test.go +++ b/op-supervisor/supervisor/backend/backend_test.go @@ -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, @@ -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, @@ -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, diff --git a/op-supervisor/supervisor/backend/db/db.go b/op-supervisor/supervisor/backend/db/db.go index b667718759b7..f2072c3feacc 100644 --- a/op-supervisor/supervisor/backend/db/db.go +++ b/op-supervisor/supervisor/backend/db/db.go @@ -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 diff --git a/op-supervisor/supervisor/backend/db/fromda/db.go b/op-supervisor/supervisor/backend/db/fromda/db.go index 48d8564ced20..40871ab39299 100644 --- a/op-supervisor/supervisor/backend/db/fromda/db.go +++ b/op-supervisor/supervisor/backend/db/fromda/db.go @@ -101,6 +101,13 @@ func (db *DB) PreviousDerived(derived eth.BlockID) (prevDerived types.BlockSeal, return prev.derived, nil } +// IsEmpty returns true if the DB is empty, false if not. +func (db *DB) IsEmpty() bool { + db.rwLock.RLock() + defer db.rwLock.RUnlock() + return db.store.LastEntryIdx() < 0 +} + // Latest returns the last known values: // derivedFrom: the L1 block that the L2 block is safe for (not necessarily the first, multiple L2 blocks may be derived from the same L1 block). // derived: the L2 block that was derived (not necessarily the first, the L1 block may have been empty and repeated the last safe L2 block). diff --git a/op-supervisor/supervisor/backend/db/query.go b/op-supervisor/supervisor/backend/db/query.go index bbee01d71b4d..91d2f4d34da9 100644 --- a/op-supervisor/supervisor/backend/db/query.go +++ b/op-supervisor/supervisor/backend/db/query.go @@ -237,21 +237,9 @@ func (db *ChainsDB) CandidateCrossSafe(chain types.ChainID) (derivedFromScope, c crossDerivedFrom, crossDerived, err := xDB.Latest() if err != nil { if errors.Is(err, types.ErrFuture) { - // If we do not have any cross-safe block yet, then return the first local-safe block. - derivedFrom, derived, err := lDB.First() - if err != nil { - return eth.BlockRef{}, eth.BlockRef{}, fmt.Errorf("failed to find first local-safe block: %w", err) - } - // the first derivedFrom (L1 block) is unlikely to be the genesis block, - derivedFromRef, err := derivedFrom.WithParent(eth.BlockID{}) - if err != nil { - // if the first derivedFrom isn't the genesis block, just warn and continue anyway - db.logger.Warn("First DerivedFrom is not genesis block") - derivedFromRef = derivedFrom.ForceWithParent(eth.BlockID{}) - } - // the first derived must be the genesis block, panic otherwise - derivedRef := derived.MustWithParent(eth.BlockID{}) - return derivedFromRef, derivedRef, nil + // If we do not have any cross-safe block yet, then abort: InitializeCrossSafe should be called. + return eth.BlockRef{}, eth.BlockRef{}, + fmt.Errorf("no initial cross-safe value, awaiting InitializeCrossSafe: %w", err) } return eth.BlockRef{}, eth.BlockRef{}, err } diff --git a/op-supervisor/supervisor/backend/db/update.go b/op-supervisor/supervisor/backend/db/update.go index 7ae7fde58a8b..ffaac9211ebe 100644 --- a/op-supervisor/supervisor/backend/db/update.go +++ b/op-supervisor/supervisor/backend/db/update.go @@ -43,6 +43,25 @@ func (db *ChainsDB) Rewind(chain types.ChainID, headBlockNum uint64) error { return logDB.Rewind(headBlockNum) } +// InitializeCrossSafe sets the initial cross-safe block, for the node to start syncing from. +func (db *ChainsDB) InitializeCrossSafe(chainID types.ChainID, derivedFrom eth.BlockRef, derived eth.BlockRef) error { + crossDB, ok := db.crossDBs.Get(chainID) + if !ok { + return fmt.Errorf("cannot InitializeCrossSafe: %w: %v", types.ErrUnknownChain, chainID) + } + if crossDB.IsEmpty() { + db.logger.Info("No previous known cross-safe data, initializing now", + "chain", chainID, "crossSafe", derived, "derivedFrom", derivedFrom) + // This may error if there was another faster InitializeCrossSafe call, that is ok. + if err := crossDB.AddDerived(derivedFrom, derived); err != nil { + return fmt.Errorf("cannot InitializeCrossSafe, failed to add initial data: %w", err) + } + } else { + db.logger.Debug("Supervisor cross-safe data is already initialized") + } + return nil +} + func (db *ChainsDB) UpdateLocalSafe(chain types.ChainID, derivedFrom eth.BlockRef, lastDerived eth.BlockRef) error { localDB, ok := db.localDBs.Get(chain) if !ok { diff --git a/op-supervisor/supervisor/backend/mock.go b/op-supervisor/supervisor/backend/mock.go index b40c5209d5ef..bead01276576 100644 --- a/op-supervisor/supervisor/backend/mock.go +++ b/op-supervisor/supervisor/backend/mock.go @@ -66,6 +66,10 @@ func (m *MockBackend) CrossDerivedFrom(ctx context.Context, chainID types.ChainI return eth.BlockRef{}, nil } +func (m *MockBackend) InitializeCrossSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.BlockRef, derived eth.BlockRef) error { + return nil +} + func (m *MockBackend) UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.BlockRef) error { return nil } diff --git a/op-supervisor/supervisor/frontend/frontend.go b/op-supervisor/supervisor/frontend/frontend.go index 0a5b70a4799a..a884af4b10a8 100644 --- a/op-supervisor/supervisor/frontend/frontend.go +++ b/op-supervisor/supervisor/frontend/frontend.go @@ -24,6 +24,7 @@ type QueryBackend interface { } type UpdatesBackend interface { + 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.BlockRef, lastDerived eth.BlockRef) error UpdateFinalizedL1(ctx context.Context, chainID types.ChainID, finalized eth.BlockRef) error @@ -98,6 +99,10 @@ type UpdatesFrontend struct { var _ UpdatesBackend = (*UpdatesFrontend)(nil) +func (u *UpdatesFrontend) InitializeCrossSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.BlockRef, derived eth.BlockRef) error { + return u.Supervisor.InitializeCrossSafe(ctx, chainID, derivedFrom, derived) +} + func (u *UpdatesFrontend) UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.BlockRef) error { return u.Supervisor.UpdateLocalUnsafe(ctx, chainID, head) } diff --git a/op-supervisor/supervisor/types/error.go b/op-supervisor/supervisor/types/error.go index fc493fb87f91..d149a9aea181 100644 --- a/op-supervisor/supervisor/types/error.go +++ b/op-supervisor/supervisor/types/error.go @@ -1,6 +1,10 @@ package types -import "errors" +import ( + "errors" + + "github.com/ethereum/go-ethereum/rpc" +) var ( // ErrOutOfOrder happens when you try to add data to the DB, @@ -28,3 +32,10 @@ var ( // ErrNoRPCSource happens when a sub-service needs an RPC data source, but is not configured with one. ErrNoRPCSource = errors.New("no RPC client configured") ) + +const SupervisorUninitializedCrossSafeErrCode = -35400 + +var ErrUninitializedCrossSafeErr = &rpc.JsonError{ + Code: SupervisorUninitializedCrossSafeErrCode, + Message: "Uninitialized cross-safe data", +}