diff --git a/op-e2e/actions/interop/interop_test.go b/op-e2e/actions/interop/interop_test.go index 57c77aaec0b0..c345404907b0 100644 --- a/op-e2e/actions/interop/interop_test.go +++ b/op-e2e/actions/interop/interop_test.go @@ -1,22 +1,25 @@ package interop import ( + "context" "testing" - "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-node/rollup/interop" "github.com/ethereum-optimism/optimism/op-node/rollup/sync" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testutils" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) -var _ interop.InteropBackend = (*testutils.MockInteropBackend)(nil) +var _ interop.InteropBackend = (*testutils.FakeInteropBackend)(nil) func TestInteropVerifier(gt *testing.T) { t := helpers.NewDefaultTesting(gt) @@ -26,14 +29,14 @@ func TestInteropVerifier(gt *testing.T) { // The state genesis in this test is pre-interop however. sd.RollupCfg.InteropTime = new(uint64) logger := testlog.Logger(t, log.LevelDebug) - seqMockBackend := &testutils.MockInteropBackend{} + seqMockBackend := &testutils.FakeInteropBackend{} l1Miner, seqEng, seq := helpers.SetupSequencerTest(t, sd, logger, helpers.WithVerifierOpts(helpers.WithInteropBackend(seqMockBackend))) batcher := helpers.NewL2Batcher(logger, sd.RollupCfg, helpers.DefaultBatcherCfg(dp), seq.RollupClient(), l1Miner.EthClient(), seqEng.EthClient(), seqEng.EngineClient(t, sd.RollupCfg)) - verMockBackend := &testutils.MockInteropBackend{} + verMockBackend := &testutils.FakeInteropBackend{} _, ver := helpers.SetupVerifier(t, sd, logger, l1Miner.L1Client(t, sd.RollupCfg), l1Miner.BlobStore(), &sync.Config{}, helpers.WithInteropBackend(verMockBackend)) @@ -42,12 +45,21 @@ func TestInteropVerifier(gt *testing.T) { ver.ActL2PipelineFull(t) l2ChainID := types.ChainIDFromBig(sd.RollupCfg.L2ChainID) - seqMockBackend.ExpectCheckBlock(l2ChainID, 1, types.LocalUnsafe, nil) + seqMockBackend.UpdateLocalUnsafeFn = func(ctx context.Context, chainID types.ChainID, head eth.L2BlockRef) error { + require.Equal(t, chainID, l2ChainID) + require.Equal(t, uint64(1), head.Number) + return nil + } + seqMockBackend.UnsafeViewFn = func(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) { + require.Equal(t, chainID, l2ChainID) + require.Equal(t, uint64(1), unsafe.Local.Number) + require.Equal(t, uint64(0), unsafe.Cross.Number) + return unsafe, nil + } // create an unsafe L2 block seq.ActL2StartBlock(t) seq.ActL2EndBlock(t) seq.ActL2PipelineFull(t) - seqMockBackend.AssertExpectations(t) status := seq.SyncStatus() require.Equal(t, uint64(1), status.UnsafeL2.Number) require.Equal(t, uint64(0), status.CrossUnsafeL2.Number) @@ -56,10 +68,16 @@ func TestInteropVerifier(gt *testing.T) { // promote it to cross-unsafe in the backend // and see if the node picks up on it - seqMockBackend.ExpectCheckBlock(l2ChainID, 1, types.CrossUnsafe, nil) + seqMockBackend.UnsafeViewFn = func(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) { + require.Equal(t, chainID, l2ChainID) + require.Equal(t, uint64(1), unsafe.Local.Number) + require.Equal(t, uint64(0), unsafe.Cross.Number) + out := unsafe + out.Cross = unsafe.Local + return out, nil + } seq.ActInteropBackendCheck(t) seq.ActL2PipelineFull(t) - seqMockBackend.AssertExpectations(t) status = seq.SyncStatus() require.Equal(t, uint64(1), status.UnsafeL2.Number) require.Equal(t, uint64(1), status.CrossUnsafeL2.Number, "cross unsafe now") @@ -74,10 +92,20 @@ func TestInteropVerifier(gt *testing.T) { l1Miner.ActL1EndBlock(t) // Sync the L1 block, to verify the L2 block as local-safe. - seqMockBackend.ExpectCheckBlock(l2ChainID, 1, types.CrossUnsafe, nil) // not cross-safe yet + seqMockBackend.UpdateLocalUnsafeFn = nil + seqMockBackend.UpdateLocalSafeFn = func(ctx context.Context, chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef) error { + require.Equal(t, uint64(1), lastDerived.Number) + return nil + } + seqMockBackend.SafeViewFn = func(ctx context.Context, chainID types.ChainID, safe types.ReferenceView) (types.ReferenceView, error) { + require.Equal(t, chainID, l2ChainID) + require.Equal(t, uint64(1), safe.Local.Number) + require.Equal(t, uint64(0), safe.Cross.Number) + return safe, nil + } seq.ActL1HeadSignal(t) + l1Head := seq.SyncStatus().HeadL1 seq.ActL2PipelineFull(t) - seqMockBackend.AssertExpectations(t) status = seq.SyncStatus() require.Equal(t, uint64(1), status.UnsafeL2.Number) @@ -86,10 +114,23 @@ func TestInteropVerifier(gt *testing.T) { require.Equal(t, uint64(0), status.SafeL2.Number) // Now mark it as cross-safe - seqMockBackend.ExpectCheckBlock(l2ChainID, 1, types.CrossSafe, nil) + seqMockBackend.SafeViewFn = func(ctx context.Context, chainID types.ChainID, request types.ReferenceView) (types.ReferenceView, error) { + require.Equal(t, chainID, l2ChainID) + require.Equal(t, uint64(1), request.Local.Number) + require.Equal(t, uint64(0), request.Cross.Number) + out := request + out.Cross = request.Local + return out, nil + } + seqMockBackend.DerivedFromFn = func(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (eth.L1BlockRef, error) { + require.Equal(t, uint64(1), blockNumber) + return l1Head, nil + } + seqMockBackend.FinalizedFn = func(ctx context.Context, chainID types.ChainID) (eth.BlockID, error) { + return seq.RollupCfg.Genesis.L1, nil + } seq.ActInteropBackendCheck(t) seq.ActL2PipelineFull(t) - seqMockBackend.AssertExpectations(t) status = seq.SyncStatus() require.Equal(t, uint64(1), status.UnsafeL2.Number) @@ -98,12 +139,30 @@ func TestInteropVerifier(gt *testing.T) { require.Equal(t, uint64(1), status.SafeL2.Number, "cross-safe reached") require.Equal(t, uint64(0), status.FinalizedL2.Number) + verMockBackend.UpdateLocalUnsafeFn = func(ctx context.Context, chainID types.ChainID, head eth.L2BlockRef) error { + require.Equal(t, uint64(1), head.Number) + return nil + } + verMockBackend.UpdateLocalSafeFn = func(ctx context.Context, chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef) error { + require.Equal(t, uint64(1), lastDerived.Number) + require.Equal(t, l1Head.ID(), derivedFrom.ID()) + return nil + } // The verifier might not see the L2 block that was just derived from L1 as cross-verified yet. - verMockBackend.ExpectCheckBlock(l2ChainID, 1, types.LocalUnsafe, nil) // for the local unsafe check - verMockBackend.ExpectCheckBlock(l2ChainID, 1, types.LocalUnsafe, nil) // for the local safe check + verMockBackend.UnsafeViewFn = func(ctx context.Context, chainID types.ChainID, request types.ReferenceView) (types.ReferenceView, error) { + require.Equal(t, uint64(1), request.Local.Number) + require.Equal(t, uint64(0), request.Cross.Number) + // Don't promote the Cross value yet + return request, nil + } + verMockBackend.SafeViewFn = func(ctx context.Context, chainID types.ChainID, request types.ReferenceView) (types.ReferenceView, error) { + require.Equal(t, uint64(1), request.Local.Number) + require.Equal(t, uint64(0), request.Cross.Number) + // Don't promote the Cross value yet + return request, nil + } ver.ActL1HeadSignal(t) ver.ActL2PipelineFull(t) - verMockBackend.AssertExpectations(t) status = ver.SyncStatus() require.Equal(t, uint64(1), status.UnsafeL2.Number, "synced the block") require.Equal(t, uint64(0), status.CrossUnsafeL2.Number, "not cross-verified yet") @@ -111,13 +170,16 @@ func TestInteropVerifier(gt *testing.T) { require.Equal(t, uint64(0), status.SafeL2.Number, "not yet cross-safe") require.Equal(t, uint64(0), status.FinalizedL2.Number) + seqMockBackend.UpdateFinalizedL1Fn = func(ctx context.Context, chainID types.ChainID, finalized eth.L1BlockRef) error { + require.Equal(t, l1Head, finalized) + return nil + } // signal that L1 finalized; the cross-safe block we have should get finalized too l1Miner.ActL1SafeNext(t) l1Miner.ActL1FinalizeNext(t) seq.ActL1SafeSignal(t) seq.ActL1FinalizedSignal(t) seq.ActL2PipelineFull(t) - seqMockBackend.AssertExpectations(t) status = seq.SyncStatus() require.Equal(t, uint64(1), status.FinalizedL2.Number, "finalized the block") diff --git a/op-node/rollup/engine/events.go b/op-node/rollup/engine/events.go index b5e010280ebc..b05410a6dcef 100644 --- a/op-node/rollup/engine/events.go +++ b/op-node/rollup/engine/events.go @@ -229,6 +229,22 @@ func (ev PromoteFinalizedEvent) String() string { return "promote-finalized" } +// FinalizedUpdateEvent signals that a block has been marked as finalized. +type FinalizedUpdateEvent struct { + Ref eth.L2BlockRef +} + +func (ev FinalizedUpdateEvent) String() string { + return "finalized-update" +} + +// RequestFinalizedUpdateEvent signals that a FinalizedUpdateEvent is needed. +type RequestFinalizedUpdateEvent struct{} + +func (ev RequestFinalizedUpdateEvent) String() string { + return "request-finalized-update" +} + // CrossUpdateRequestEvent triggers update events to be emitted, repeating the current state. type CrossUpdateRequestEvent struct { CrossUnsafe bool @@ -419,8 +435,11 @@ func (d *EngDeriver) OnEvent(ev event.Event) bool { return true } d.ec.SetFinalizedHead(x.Ref) + d.emitter.Emit(FinalizedUpdateEvent(x)) // Try to apply the forkchoice changes d.emitter.Emit(TryUpdateEngineEvent{}) + case RequestFinalizedUpdateEvent: + d.emitter.Emit(FinalizedUpdateEvent{Ref: d.ec.Finalized()}) case CrossUpdateRequestEvent: if x.CrossUnsafe { d.emitter.Emit(CrossUnsafeUpdateEvent{ diff --git a/op-node/rollup/interop/interop.go b/op-node/rollup/interop/interop.go index 152020f09c70..904f93d94112 100644 --- a/op-node/rollup/interop/interop.go +++ b/op-node/rollup/interop/interop.go @@ -2,6 +2,7 @@ package interop import ( "context" + "fmt" "sync" "time" @@ -11,19 +12,28 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/engine" "github.com/ethereum-optimism/optimism/op-node/rollup/event" + "github.com/ethereum-optimism/optimism/op-node/rollup/finality" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) -const checkBlockTimeout = time.Second * 10 +const rpcTimeout = time.Second * 10 type InteropBackend interface { - CheckBlock(ctx context.Context, - chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (types.SafetyLevel, error) + UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) + SafeView(ctx context.Context, chainID types.ChainID, safe types.ReferenceView) (types.ReferenceView, error) + Finalized(ctx context.Context, chainID types.ChainID) (eth.BlockID, error) + + DerivedFrom(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (eth.L1BlockRef, error) + + UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.L2BlockRef) error + UpdateLocalSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef) error + UpdateFinalizedL1(ctx context.Context, chainID types.ChainID, finalized eth.L1BlockRef) error } type L2Source interface { L2BlockRefByNumber(context.Context, uint64) (eth.L2BlockRef, error) + L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) } // InteropDeriver watches for update events (either real changes to block safety, @@ -39,11 +49,6 @@ type InteropDeriver struct { driverCtx context.Context - // L2 blockhash -> derived from L1 block ref. - // Added to when a block is local-safe. - // Removed from when it is promoted to cross-safe. - derivedFrom map[common.Hash]eth.L1BlockRef - backend InteropBackend l2 L2Source @@ -58,13 +63,12 @@ var _ event.AttachEmitter = (*InteropDeriver)(nil) func NewInteropDeriver(log log.Logger, cfg *rollup.Config, driverCtx context.Context, backend InteropBackend, l2 L2Source) *InteropDeriver { return &InteropDeriver{ - log: log, - cfg: cfg, - chainID: types.ChainIDFromBig(cfg.L2ChainID), - driverCtx: driverCtx, - derivedFrom: make(map[common.Hash]eth.L1BlockRef), - backend: backend, - l2: l2, + log: log, + cfg: cfg, + chainID: types.ChainIDFromBig(cfg.L2ChainID), + driverCtx: driverCtx, + backend: backend, + l2: l2, } } @@ -78,87 +82,178 @@ func (d *InteropDeriver) OnEvent(ev event.Event) bool { switch x := ev.(type) { case engine.UnsafeUpdateEvent: - d.emitter.Emit(engine.RequestCrossUnsafeEvent{}) + d.onLocalUnsafeUpdate(x) + case engine.LocalSafeUpdateEvent: + d.onLocalSafeUpdate(x) + case finality.FinalizeL1Event: + d.onFinalizedL1(x) case engine.CrossUnsafeUpdateEvent: - if x.CrossUnsafe.Number >= x.LocalUnsafe.Number { - break // nothing left to promote - } - // Pre-interop the engine itself handles promotion to cross-unsafe. - // Check if the next block (still unsafe) can be promoted to cross-unsafe. - if !d.cfg.IsInterop(d.cfg.TimestampForBlock(x.CrossUnsafe.Number + 1)) { - return false - } - ctx, cancel := context.WithTimeout(d.driverCtx, checkBlockTimeout) - defer cancel() - candidate, err := d.l2.L2BlockRefByNumber(ctx, x.CrossUnsafe.Number+1) - if err != nil { - d.log.Warn("Failed to fetch next cross-unsafe candidate", "err", err) - break - } - blockSafety, err := d.backend.CheckBlock(ctx, d.chainID, candidate.Hash, candidate.Number) - if err != nil { - d.log.Warn("Failed to check interop safety of unsafe block", "err", err) - break + if err := d.onCrossUnsafe(x); err != nil { + d.log.Error("Failed to process cross-unsafe update", "err", err) } - switch blockSafety { - case types.CrossUnsafe, types.CrossSafe, types.Finalized: - // Hold off on promoting higher than cross-unsafe, - // this will happen once we verify it to be local-safe first. - d.emitter.Emit(engine.PromoteCrossUnsafeEvent{Ref: candidate}) - } - case engine.LocalSafeUpdateEvent: - d.log.Debug("Local safe update event", "block", x.Ref.Hash, "derivedFrom", x.DerivedFrom) - d.derivedFrom[x.Ref.Hash] = x.DerivedFrom - d.emitter.Emit(engine.RequestCrossSafeEvent{}) case engine.CrossSafeUpdateEvent: - if x.CrossSafe.Number >= x.LocalSafe.Number { - break // nothing left to promote - } - // Pre-interop the engine itself handles promotion to cross-safe. - // Check if the next block (not yet cross-safe) can be promoted to cross-safe. - if !d.cfg.IsInterop(d.cfg.TimestampForBlock(x.CrossSafe.Number + 1)) { - return false - } - ctx, cancel := context.WithTimeout(d.driverCtx, checkBlockTimeout) - defer cancel() - candidate, err := d.l2.L2BlockRefByNumber(ctx, x.CrossSafe.Number+1) - if err != nil { - d.log.Warn("Failed to fetch next cross-safe candidate", "err", err) - break - } - blockSafety, err := d.backend.CheckBlock(ctx, d.chainID, candidate.Hash, candidate.Number) - if err != nil { - d.log.Warn("Failed to check interop safety of local-safe block", "err", err) - break - } - derivedFrom, ok := d.derivedFrom[candidate.Hash] - if !ok { - d.log.Warn("Unknown block candidate source, cannot promote block safety", "block", candidate, "safety", blockSafety) - break + if err := d.onCrossSafeUpdateEvent(x); err != nil { + d.log.Error("Failed to process cross-safe update", "err", err) } - switch blockSafety { - case types.CrossSafe: - d.log.Info("Verified cross-safe block", "block", candidate, "derivedFrom", derivedFrom) - // TODO(#11673): once we have interop reorg support, we need to clean stale blocks also. - delete(d.derivedFrom, candidate.Hash) - d.emitter.Emit(engine.PromoteSafeEvent{ - Ref: candidate, - DerivedFrom: derivedFrom, - }) - case types.Finalized: - // TODO(#11673): once we have interop reorg support, we need to clean stale blocks also. - delete(d.derivedFrom, candidate.Hash) - d.emitter.Emit(engine.PromoteSafeEvent{ - Ref: candidate, - DerivedFrom: derivedFrom, - }) - d.emitter.Emit(engine.PromoteFinalizedEvent{ - Ref: candidate, - }) + case engine.FinalizedUpdateEvent: + if err := d.onFinalizedUpdate(x); err != nil { + d.log.Error("Failed to process finalized update", "err", err) } - // no reorg support yet; the safe L2 head will finalize eventually, no exceptions default: return false } return true } + +func (d *InteropDeriver) onLocalUnsafeUpdate(x engine.UnsafeUpdateEvent) { + d.log.Debug("Signaling unsafe L2 head update to interop backend", "head", x.Ref) + ctx, cancel := context.WithTimeout(d.driverCtx, rpcTimeout) + defer cancel() + if err := d.backend.UpdateLocalUnsafe(ctx, d.chainID, x.Ref); err != nil { + d.log.Warn("Failed to signal unsafe L2 head to interop backend", "head", x.Ref, "err", err) + // still continue to try and do a cross-unsafe update + } + // Now that the op-supervisor is aware of the new local-unsafe block, we want to check if cross-unsafe changed. + d.emitter.Emit(engine.RequestCrossUnsafeEvent{}) +} + +func (d *InteropDeriver) onLocalSafeUpdate(x engine.LocalSafeUpdateEvent) { + d.log.Debug("Signaling derived-from update to interop backend", "derivedFrom", x.DerivedFrom, "block", x.Ref) + ctx, cancel := context.WithTimeout(d.driverCtx, rpcTimeout) + defer cancel() + if err := d.backend.UpdateLocalSafe(ctx, d.chainID, x.DerivedFrom, x.Ref); err != nil { + d.log.Debug("Failed to signal derived-from update to interop backend", "derivedFrom", x.DerivedFrom, "block", x.Ref) + // still continue to try and do a cross-safe update + } + // Now that the op-supervisor is aware of the new local-safe block, we want to check if cross-safe changed. + d.emitter.Emit(engine.RequestCrossSafeEvent{}) +} + +func (d *InteropDeriver) onFinalizedL1(x finality.FinalizeL1Event) { + if !d.cfg.IsInterop(x.FinalizedL1.Time) { + return + } + d.log.Debug("Signaling finalized L1 update to interop backend", "finalized", x.FinalizedL1) + ctx, cancel := context.WithTimeout(d.driverCtx, rpcTimeout) + defer cancel() + if err := d.backend.UpdateFinalizedL1(ctx, d.chainID, x.FinalizedL1); err != nil { + d.log.Warn("Failed to signal finalized L1 block to interop backend", "finalized", x.FinalizedL1, "err", err) + } + // New L2 blocks may be ready to finalize now that the backend knows of new L1 finalized info. + d.emitter.Emit(engine.RequestFinalizedUpdateEvent{}) +} + +func (d *InteropDeriver) onCrossUnsafe(x engine.CrossUnsafeUpdateEvent) error { + if x.CrossUnsafe.Number >= x.LocalUnsafe.Number { + return nil // nothing left to promote + } + + // Pre-interop the engine itself handles promotion to cross-unsafe. + // Start checking cross-unsafe once the local-unsafe block is in the interop update. + if !d.cfg.IsInterop(x.LocalUnsafe.Time) { + return nil + } + ctx, cancel := context.WithTimeout(d.driverCtx, rpcTimeout) + defer cancel() + view := types.ReferenceView{ + Local: x.LocalUnsafe.ID(), + Cross: x.CrossUnsafe.ID(), + } + result, err := d.backend.UnsafeView(ctx, d.chainID, view) + if err != nil { + return fmt.Errorf("failed to check unsafe-level view: %w", err) + } + if result.Cross.Number == x.CrossUnsafe.Number { + // supervisor is in sync with op-node + return nil + } + if result.Cross.Number < x.CrossUnsafe.Number { + d.log.Warn("op-supervisor is behind known cross-unsafe block", "supervisor", result.Cross, "known", x.CrossUnsafe) + return nil + } + d.log.Info("New cross-unsafe block", "block", result.Cross.Number) + // Note: in the future we want to do reorg-checks, + // and initiate a reorg, if found to be on a conflicting chain. + ref, err := d.l2.L2BlockRefByHash(ctx, result.Cross.Hash) + if err != nil { + return fmt.Errorf("failed to get cross-unsafe block info of %s: %w", result.Cross, err) + } + d.emitter.Emit(engine.PromoteCrossUnsafeEvent{Ref: ref}) + + return nil +} + +func (d *InteropDeriver) onCrossSafeUpdateEvent(x engine.CrossSafeUpdateEvent) error { + if x.CrossSafe.Number >= x.LocalSafe.Number { + return nil // nothing left to promote + } + // Pre-interop the engine itself handles promotion to cross-safe. + // Start checking cross-safe once the local-safe block is in the interop update. + if !d.cfg.IsInterop(x.LocalSafe.Time) { + return nil + } + ctx, cancel := context.WithTimeout(d.driverCtx, rpcTimeout) + defer cancel() + view := types.ReferenceView{ + Local: x.LocalSafe.ID(), + Cross: x.CrossSafe.ID(), + } + result, err := d.backend.SafeView(ctx, d.chainID, view) + if err != nil { + return fmt.Errorf("failed to check safe-level view: %w", err) + } + if result.Cross.Number == x.CrossSafe.Number { + // supervisor is in sync with op-node + return nil + } + if result.Cross.Number < x.CrossSafe.Number { + d.log.Warn("op-supervisor is behind known cross-safe block", "supervisor", result.Cross, "known", x.CrossSafe) + // TODO: we may want to force set the cross-safe block in the engine, + // and then reset derivation, so this op-node can help get the supervisor back in sync. + return nil + } + derivedFrom, err := d.backend.DerivedFrom(ctx, d.chainID, result.Cross.Hash, result.Cross.Number) + if err != nil { + return fmt.Errorf("failed to get derived-from of %s: %w", result.Cross, err) + } + ref, err := d.l2.L2BlockRefByHash(ctx, result.Cross.Hash) + if err != nil { + return fmt.Errorf("failed to get block ref of %s: %w", result.Cross, err) + } + d.emitter.Emit(engine.PromoteSafeEvent{ + Ref: ref, + DerivedFrom: derivedFrom, + }) + d.emitter.Emit(engine.RequestFinalizedUpdateEvent{}) + return nil +} + +func (d *InteropDeriver) onFinalizedUpdate(x engine.FinalizedUpdateEvent) error { + // Note: we have to check interop fork, but finality may be pre-fork activation until we update. + // We may want to change this to only start checking finality once the local head is past the activation. + + ctx, cancel := context.WithTimeout(d.driverCtx, rpcTimeout) + defer cancel() + + finalized, err := d.backend.Finalized(ctx, d.chainID) + if err != nil { + return fmt.Errorf("failed to retrieve finalized L2 block from supervisor: %w", err) + } + // Check if we can finalize something new + if finalized.Number == x.Ref.Number { + // supervisor is in sync with op-node + return nil + } + if finalized.Number < x.Ref.Number { + d.log.Warn("op-supervisor is behind known finalized block", "supervisor", finalized, "known", x.Ref) + return nil + } + ref, err := d.l2.L2BlockRefByHash(ctx, finalized.Hash) + if err != nil { + return fmt.Errorf("failed to get block ref of %s: %w", finalized, err) + } + d.emitter.Emit(engine.PromoteFinalizedEvent{ + Ref: ref, + }) + return nil +} diff --git a/op-node/rollup/interop/interop_test.go b/op-node/rollup/interop/interop_test.go index a7aaedcae7a1..4078e4881a07 100644 --- a/op-node/rollup/interop/interop_test.go +++ b/op-node/rollup/interop/interop_test.go @@ -6,17 +6,18 @@ import ( "math/rand" // nosemgrep "testing" - "github.com/stretchr/testify/require" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/engine" + "github.com/ethereum-optimism/optimism/op-node/rollup/finality" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testutils" supervisortypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) +var _ InteropBackend = (*testutils.MockInteropBackend)(nil) + func TestInteropDeriver(t *testing.T) { logger := testlog.Logger(t, log.LevelInfo) l2Source := &testutils.MockL2Client{} @@ -31,25 +32,34 @@ func TestInteropDeriver(t *testing.T) { interopDeriver.AttachEmitter(emitter) rng := rand.New(rand.NewSource(123)) - t.Run("unsafe blocks trigger cross-unsafe check attempts", func(t *testing.T) { + t.Run("local-unsafe blocks push to supervisor and trigger cross-unsafe attempts", func(t *testing.T) { emitter.ExpectOnce(engine.RequestCrossUnsafeEvent{}) - interopDeriver.OnEvent(engine.UnsafeUpdateEvent{ - Ref: testutils.RandomL2BlockRef(rng), - }) + unsafeHead := testutils.RandomL2BlockRef(rng) + interopBackend.ExpectUpdateLocalUnsafe(chainID, unsafeHead, nil) + interopDeriver.OnEvent(engine.UnsafeUpdateEvent{Ref: unsafeHead}) emitter.AssertExpectations(t) + interopBackend.AssertExpectations(t) }) t.Run("establish cross-unsafe", func(t *testing.T) { - crossUnsafe := testutils.RandomL2BlockRef(rng) - firstLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, crossUnsafe, crossUnsafe.L1Origin) - lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, firstLocalUnsafe, firstLocalUnsafe.L1Origin) - interopBackend.ExpectCheckBlock( - chainID, firstLocalUnsafe.Number, supervisortypes.CrossUnsafe, nil) + oldCrossUnsafe := testutils.RandomL2BlockRef(rng) + nextCrossUnsafe := testutils.NextRandomL2Ref(rng, 2, oldCrossUnsafe, oldCrossUnsafe.L1Origin) + lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, nextCrossUnsafe, nextCrossUnsafe.L1Origin) + localView := supervisortypes.ReferenceView{ + Local: lastLocalUnsafe.ID(), + Cross: oldCrossUnsafe.ID(), + } + supervisorView := supervisortypes.ReferenceView{ + Local: lastLocalUnsafe.ID(), + Cross: nextCrossUnsafe.ID(), + } + interopBackend.ExpectUnsafeView( + chainID, localView, supervisorView, nil) + l2Source.ExpectL2BlockRefByHash(nextCrossUnsafe.Hash, nextCrossUnsafe, nil) emitter.ExpectOnce(engine.PromoteCrossUnsafeEvent{ - Ref: firstLocalUnsafe, + Ref: nextCrossUnsafe, }) - l2Source.ExpectL2BlockRefByNumber(firstLocalUnsafe.Number, firstLocalUnsafe, nil) interopDeriver.OnEvent(engine.CrossUnsafeUpdateEvent{ - CrossUnsafe: crossUnsafe, + CrossUnsafe: oldCrossUnsafe, LocalUnsafe: lastLocalUnsafe, }) interopBackend.AssertExpectations(t) @@ -57,53 +67,62 @@ func TestInteropDeriver(t *testing.T) { l2Source.AssertExpectations(t) }) t.Run("deny cross-unsafe", func(t *testing.T) { - crossUnsafe := testutils.RandomL2BlockRef(rng) - firstLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, crossUnsafe, crossUnsafe.L1Origin) - lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, firstLocalUnsafe, firstLocalUnsafe.L1Origin) - interopBackend.ExpectCheckBlock( - chainID, firstLocalUnsafe.Number, supervisortypes.LocalUnsafe, nil) - l2Source.ExpectL2BlockRefByNumber(firstLocalUnsafe.Number, firstLocalUnsafe, nil) + oldCrossUnsafe := testutils.RandomL2BlockRef(rng) + nextCrossUnsafe := testutils.NextRandomL2Ref(rng, 2, oldCrossUnsafe, oldCrossUnsafe.L1Origin) + lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, nextCrossUnsafe, nextCrossUnsafe.L1Origin) + localView := supervisortypes.ReferenceView{ + Local: lastLocalUnsafe.ID(), + Cross: oldCrossUnsafe.ID(), + } + supervisorView := supervisortypes.ReferenceView{ + Local: lastLocalUnsafe.ID(), + Cross: oldCrossUnsafe.ID(), // stuck on same cross-safe + } + interopBackend.ExpectUnsafeView( + chainID, localView, supervisorView, nil) interopDeriver.OnEvent(engine.CrossUnsafeUpdateEvent{ - CrossUnsafe: crossUnsafe, + CrossUnsafe: oldCrossUnsafe, LocalUnsafe: lastLocalUnsafe, }) interopBackend.AssertExpectations(t) - // no cross-unsafe promote event is expected - emitter.AssertExpectations(t) + emitter.AssertExpectations(t) // no promote-cross-unsafe event expected l2Source.AssertExpectations(t) }) - t.Run("register local-safe", func(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) - emitter.ExpectOnce(engine.RequestCrossSafeEvent{}) + interopBackend.ExpectUpdateLocalSafe(chainID, derivedFrom, localSafe, nil) interopDeriver.OnEvent(engine.LocalSafeUpdateEvent{ Ref: localSafe, DerivedFrom: derivedFrom, }) - require.Contains(t, interopDeriver.derivedFrom, localSafe.Hash) - require.Equal(t, derivedFrom, interopDeriver.derivedFrom[localSafe.Hash]) emitter.AssertExpectations(t) + interopBackend.AssertExpectations(t) }) t.Run("establish cross-safe", func(t *testing.T) { derivedFrom := testutils.RandomBlockRef(rng) - crossSafe := testutils.RandomL2BlockRef(rng) - firstLocalSafe := testutils.NextRandomL2Ref(rng, 2, crossSafe, crossSafe.L1Origin) - lastLocalSafe := testutils.NextRandomL2Ref(rng, 2, firstLocalSafe, firstLocalSafe.L1Origin) - emitter.ExpectOnce(engine.RequestCrossSafeEvent{}) - // The local safe block must be known, for the derived-from mapping to work - interopDeriver.OnEvent(engine.LocalSafeUpdateEvent{ - Ref: firstLocalSafe, - DerivedFrom: derivedFrom, - }) - interopBackend.ExpectCheckBlock( - chainID, firstLocalSafe.Number, supervisortypes.CrossSafe, nil) + oldCrossSafe := testutils.RandomL2BlockRef(rng) + 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{ + Local: lastLocalSafe.ID(), + Cross: nextCrossSafe.ID(), + } + interopBackend.ExpectSafeView(chainID, localView, supervisorView, nil) + interopBackend.ExpectDerivedFrom(chainID, nextCrossSafe.Hash, nextCrossSafe.Number, derivedFrom, nil) + l2Source.ExpectL2BlockRefByHash(nextCrossSafe.Hash, nextCrossSafe, nil) emitter.ExpectOnce(engine.PromoteSafeEvent{ - Ref: firstLocalSafe, + Ref: nextCrossSafe, DerivedFrom: derivedFrom, }) - l2Source.ExpectL2BlockRefByNumber(firstLocalSafe.Number, firstLocalSafe, nil) + emitter.ExpectOnce(engine.RequestFinalizedUpdateEvent{}) interopDeriver.OnEvent(engine.CrossSafeUpdateEvent{ - CrossSafe: crossSafe, + CrossSafe: oldCrossSafe, LocalSafe: lastLocalSafe, }) interopBackend.AssertExpectations(t) @@ -111,26 +130,54 @@ func TestInteropDeriver(t *testing.T) { l2Source.AssertExpectations(t) }) t.Run("deny cross-safe", func(t *testing.T) { - derivedFrom := testutils.RandomBlockRef(rng) - crossSafe := testutils.RandomL2BlockRef(rng) - firstLocalSafe := testutils.NextRandomL2Ref(rng, 2, crossSafe, crossSafe.L1Origin) - lastLocalSafe := testutils.NextRandomL2Ref(rng, 2, firstLocalSafe, firstLocalSafe.L1Origin) - emitter.ExpectOnce(engine.RequestCrossSafeEvent{}) - // The local safe block must be known, for the derived-from mapping to work - interopDeriver.OnEvent(engine.LocalSafeUpdateEvent{ - Ref: firstLocalSafe, - DerivedFrom: derivedFrom, - }) - interopBackend.ExpectCheckBlock( - chainID, firstLocalSafe.Number, supervisortypes.LocalSafe, nil) - l2Source.ExpectL2BlockRefByNumber(firstLocalSafe.Number, firstLocalSafe, nil) + oldCrossSafe := testutils.RandomL2BlockRef(rng) + 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{ + Local: lastLocalSafe.ID(), + Cross: oldCrossSafe.ID(), // stay on old cross-safe + } + interopBackend.ExpectSafeView(chainID, localView, supervisorView, nil) interopDeriver.OnEvent(engine.CrossSafeUpdateEvent{ - CrossSafe: crossSafe, + CrossSafe: oldCrossSafe, LocalSafe: lastLocalSafe, }) interopBackend.AssertExpectations(t) - // no cross-safe promote event is expected - emitter.AssertExpectations(t) + emitter.AssertExpectations(t) // no promote-cross-safe event expected l2Source.AssertExpectations(t) }) + t.Run("finalized L1 trigger cross-L2 finality check", func(t *testing.T) { + emitter.ExpectOnce(engine.RequestFinalizedUpdateEvent{}) + finalizedL1 := testutils.RandomBlockRef(rng) + interopBackend.ExpectUpdateFinalizedL1(chainID, finalizedL1, nil) + interopDeriver.OnEvent(finality.FinalizeL1Event{ + FinalizedL1: finalizedL1, + }) + emitter.AssertExpectations(t) + interopBackend.AssertExpectations(t) + }) + t.Run("next L2 finalized block", func(t *testing.T) { + oldFinalizedL2 := testutils.RandomL2BlockRef(rng) + intermediateL2 := testutils.NextRandomL2Ref(rng, 2, oldFinalizedL2, oldFinalizedL2.L1Origin) + nextFinalizedL2 := testutils.NextRandomL2Ref(rng, 2, intermediateL2, intermediateL2.L1Origin) + emitter.ExpectOnce(engine.PromoteFinalizedEvent{ + Ref: nextFinalizedL2, + }) + interopBackend.ExpectFinalized(chainID, nextFinalizedL2.ID(), nil) + l2Source.ExpectL2BlockRefByHash(nextFinalizedL2.Hash, nextFinalizedL2, nil) + interopDeriver.OnEvent(engine.FinalizedUpdateEvent{Ref: oldFinalizedL2}) + emitter.AssertExpectations(t) + interopBackend.AssertExpectations(t) + }) + t.Run("keep L2 finalized block", func(t *testing.T) { + oldFinalizedL2 := testutils.RandomL2BlockRef(rng) + interopBackend.ExpectFinalized(chainID, oldFinalizedL2.ID(), nil) + interopDeriver.OnEvent(engine.FinalizedUpdateEvent{Ref: oldFinalizedL2}) + emitter.AssertExpectations(t) // no PromoteFinalizedEvent + interopBackend.AssertExpectations(t) + }) } diff --git a/op-service/sources/supervisor_client.go b/op-service/sources/supervisor_client.go index ff702010daff..d9cb71fb45b2 100644 --- a/op-service/sources/supervisor_client.go +++ b/op-service/sources/supervisor_client.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) @@ -65,20 +66,48 @@ func (cl *SupervisorClient) AddL2RPC( return result } -func (cl *SupervisorClient) CheckBlock(ctx context.Context, - chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (types.SafetyLevel, error) { - var result types.SafetyLevel - err := cl.client.CallContext( - ctx, - &result, - "supervisor_checkBlock", - (*hexutil.U256)(&chainID), blockHash, hexutil.Uint64(blockNumber)) +func (cl *SupervisorClient) UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) { + var result types.ReferenceView + err := cl.client.CallContext(ctx, &result, "supervisor_unsafeView", (*hexutil.U256)(&chainID), unsafe) if err != nil { - return types.LocalUnsafe, fmt.Errorf("failed to check Block %s:%d (chain %s): %w", blockHash, blockNumber, chainID, err) + return types.ReferenceView{}, fmt.Errorf("failed to share unsafe block view %s (chain %s): %w", unsafe, chainID, err) } return result, nil } +func (cl *SupervisorClient) SafeView(ctx context.Context, chainID types.ChainID, safe types.ReferenceView) (types.ReferenceView, error) { + var result types.ReferenceView + err := cl.client.CallContext(ctx, &result, "supervisor_safeView", (*hexutil.U256)(&chainID), safe) + if err != nil { + return types.ReferenceView{}, fmt.Errorf("failed to share safe block view %s (chain %s): %w", safe, chainID, err) + } + return result, nil +} + +func (cl *SupervisorClient) Finalized(ctx context.Context, chainID types.ChainID) (eth.BlockID, error) { + var result eth.BlockID + err := cl.client.CallContext(ctx, &result, "supervisor_finalized", chainID) + return result, err +} + +func (cl *SupervisorClient) DerivedFrom(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (eth.L1BlockRef, error) { + var result eth.L1BlockRef + err := cl.client.CallContext(ctx, &result, "supervisor_derivedFrom", chainID, blockHash, blockNumber) + return result, err +} + +func (cl *SupervisorClient) UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.L2BlockRef) error { + return cl.client.CallContext(ctx, nil, "supervisor_updateLocalUnsafe", chainID, head) +} + +func (cl *SupervisorClient) UpdateLocalSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef) error { + return cl.client.CallContext(ctx, nil, "supervisor_updateLocalSafe", chainID, derivedFrom, lastDerived) +} + +func (cl *SupervisorClient) UpdateFinalizedL1(ctx context.Context, chainID types.ChainID, finalizedL1 eth.L1BlockRef) error { + return cl.client.CallContext(ctx, nil, "supervisor_updateFinalizedL1", chainID, finalizedL1) +} + func (cl *SupervisorClient) Close() { cl.client.Close() } diff --git a/op-service/testutils/fake_interop_backend.go b/op-service/testutils/fake_interop_backend.go new file mode 100644 index 000000000000..7c6287031229 --- /dev/null +++ b/op-service/testutils/fake_interop_backend.go @@ -0,0 +1,48 @@ +package testutils + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +type FakeInteropBackend struct { + UnsafeViewFn func(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) + SafeViewFn func(ctx context.Context, chainID types.ChainID, safe types.ReferenceView) (types.ReferenceView, error) + FinalizedFn func(ctx context.Context, chainID types.ChainID) (eth.BlockID, error) + DerivedFromFn func(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (eth.L1BlockRef, error) + UpdateLocalUnsafeFn func(ctx context.Context, chainID types.ChainID, head eth.L2BlockRef) error + UpdateLocalSafeFn func(ctx context.Context, chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef) error + UpdateFinalizedL1Fn func(ctx context.Context, chainID types.ChainID, finalized eth.L1BlockRef) error +} + +func (m *FakeInteropBackend) UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) { + return m.UnsafeViewFn(ctx, chainID, unsafe) +} + +func (m *FakeInteropBackend) SafeView(ctx context.Context, chainID types.ChainID, safe types.ReferenceView) (types.ReferenceView, error) { + return m.SafeViewFn(ctx, chainID, safe) +} + +func (m *FakeInteropBackend) Finalized(ctx context.Context, chainID types.ChainID) (eth.BlockID, error) { + return m.FinalizedFn(ctx, chainID) +} + +func (m *FakeInteropBackend) DerivedFrom(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (eth.L1BlockRef, error) { + return m.DerivedFromFn(ctx, chainID, blockHash, blockNumber) +} + +func (m *FakeInteropBackend) UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.L2BlockRef) error { + return m.UpdateLocalUnsafeFn(ctx, chainID, head) +} + +func (m *FakeInteropBackend) UpdateLocalSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef) error { + return m.UpdateLocalSafeFn(ctx, chainID, derivedFrom, lastDerived) +} + +func (m *FakeInteropBackend) UpdateFinalizedL1(ctx context.Context, chainID types.ChainID, finalized eth.L1BlockRef) error { + return m.UpdateFinalizedL1Fn(ctx, chainID, finalized) +} diff --git a/op-service/testutils/mock_interop_backend.go b/op-service/testutils/mock_interop_backend.go index 970627ff750f..4bb300a95ff2 100644 --- a/op-service/testutils/mock_interop_backend.go +++ b/op-service/testutils/mock_interop_backend.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) @@ -14,13 +15,89 @@ type MockInteropBackend struct { Mock mock.Mock } -func (m *MockInteropBackend) ExpectCheckBlock(chainID types.ChainID, blockNumber uint64, safety types.SafetyLevel, err error) { - m.Mock.On("CheckBlock", chainID, blockNumber).Once().Return(safety, &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) } -func (m *MockInteropBackend) CheckBlock(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (types.SafetyLevel, error) { - result := m.Mock.MethodCalled("CheckBlock", chainID, blockNumber) - return result.Get(0).(types.SafetyLevel), *result.Get(1).(*error) +func (m *MockInteropBackend) ExpectUnsafeView(chainID types.ChainID, unsafe types.ReferenceView, result types.ReferenceView, err error) { + m.Mock.On("UnsafeView", chainID, unsafe).Once().Return(result, &err) +} + +func (m *MockInteropBackend) OnUnsafeView(chainID types.ChainID, fn func(request types.ReferenceView) (result types.ReferenceView, err error)) { + var result types.ReferenceView + var err error + m.Mock.On("UnsafeView", chainID, mock.Anything).Run(func(args mock.Arguments) { + v := args[0].(types.ReferenceView) + result, err = fn(v) + }).Return(result, &err) +} + +func (m *MockInteropBackend) SafeView(ctx context.Context, chainID types.ChainID, safe types.ReferenceView) (types.ReferenceView, error) { + result := m.Mock.MethodCalled("SafeView", chainID, safe) + return result.Get(0).(types.ReferenceView), *result.Get(1).(*error) +} + +func (m *MockInteropBackend) ExpectSafeView(chainID types.ChainID, safe types.ReferenceView, result types.ReferenceView, err error) { + m.Mock.On("SafeView", chainID, safe).Once().Return(result, &err) +} + +func (m *MockInteropBackend) OnSafeView(chainID types.ChainID, fn func(request types.ReferenceView) (result types.ReferenceView, err error)) { + var result types.ReferenceView + var err error + m.Mock.On("SafeView", chainID, mock.Anything).Run(func(args mock.Arguments) { + v := args[0].(types.ReferenceView) + result, err = fn(v) + }).Return(result, &err) +} + +func (m *MockInteropBackend) Finalized(ctx context.Context, chainID types.ChainID) (eth.BlockID, error) { + result := m.Mock.MethodCalled("Finalized", chainID) + return result.Get(0).(eth.BlockID), *result.Get(1).(*error) +} + +func (m *MockInteropBackend) ExpectFinalized(chainID types.ChainID, result eth.BlockID, err error) { + m.Mock.On("Finalized", chainID).Once().Return(result, &err) +} + +func (m *MockInteropBackend) DerivedFrom(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (eth.L1BlockRef, error) { + result := m.Mock.MethodCalled("DerivedFrom", chainID, blockHash, blockNumber) + return result.Get(0).(eth.L1BlockRef), *result.Get(1).(*error) +} + +func (m *MockInteropBackend) ExpectDerivedFrom(chainID types.ChainID, blockHash common.Hash, blockNumber uint64, result eth.L1BlockRef, err error) { + m.Mock.On("DerivedFrom", chainID, blockHash, blockNumber).Once().Return(result, &err) +} + +func (m *MockInteropBackend) UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.L2BlockRef) error { + result := m.Mock.MethodCalled("UpdateLocalUnsafe", chainID, head) + return *result.Get(0).(*error) +} + +func (m *MockInteropBackend) ExpectUpdateLocalUnsafe(chainID types.ChainID, head eth.L2BlockRef, err error) { + m.Mock.On("UpdateLocalUnsafe", chainID, head).Once().Return(&err) +} + +func (m *MockInteropBackend) ExpectAnyUpdateLocalUnsafe(chainID types.ChainID, err error) { + m.Mock.On("UpdateLocalUnsafe", chainID, mock.Anything).Once().Return(&err) +} + +func (m *MockInteropBackend) UpdateLocalSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef) error { + result := m.Mock.MethodCalled("UpdateLocalSafe", chainID, derivedFrom, lastDerived) + return *result.Get(0).(*error) +} + +func (m *MockInteropBackend) ExpectUpdateLocalSafe(chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef, err error) { + m.Mock.On("UpdateLocalSafe", chainID, derivedFrom, lastDerived).Once().Return(&err) +} + +func (m *MockInteropBackend) UpdateFinalizedL1(ctx context.Context, chainID types.ChainID, finalized eth.L1BlockRef) error { + result := m.Mock.MethodCalled("UpdateFinalizedL1", chainID, finalized) + return *result.Get(0).(*error) +} + +func (m *MockInteropBackend) ExpectUpdateFinalizedL1(chainID types.ChainID, finalized eth.L1BlockRef, err error) { + m.Mock.On("UpdateFinalizedL1", chainID, finalized).Once().Return(&err) } func (m *MockInteropBackend) AssertExpectations(t mock.TestingT) { diff --git a/op-supervisor/supervisor/backend/backend.go b/op-supervisor/supervisor/backend/backend.go index 8216eaa9c0b5..563268ae3f3f 100644 --- a/op-supervisor/supervisor/backend/backend.go +++ b/op-supervisor/supervisor/backend/backend.go @@ -226,3 +226,12 @@ func (su *SupervisorBackend) CheckBlock(chainID *hexutil.U256, blockHash common. safest := su.db.Safest(types.ChainID(*chainID), uint64(blockNumber), 0) return safest, nil } + +func (su *SupervisorBackend) DerivedFrom( + ctx context.Context, + chainID types.ChainID, + blockHash common.Hash, + blockNumber uint64) (eth.BlockRef, error) { + // TODO(#12358): attach to backend + return eth.BlockRef{}, nil +} diff --git a/op-supervisor/supervisor/backend/mock.go b/op-supervisor/supervisor/backend/mock.go index e62c7b950b7c..99ec630ddc36 100644 --- a/op-supervisor/supervisor/backend/mock.go +++ b/op-supervisor/supervisor/backend/mock.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/frontend" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) @@ -55,6 +56,10 @@ func (m *MockBackend) CheckBlock(chainID *hexutil.U256, blockHash common.Hash, b return types.CrossUnsafe, nil } +func (m *MockBackend) DerivedFrom(ctx context.Context, t types.ChainID, parentHash common.Hash, n uint64) (eth.BlockRef, error) { + return eth.BlockRef{}, nil +} + func (m *MockBackend) Close() error { return nil } diff --git a/op-supervisor/supervisor/frontend/frontend.go b/op-supervisor/supervisor/frontend/frontend.go index 41fb84e511cd..b77b6b3edeeb 100644 --- a/op-supervisor/supervisor/frontend/frontend.go +++ b/op-supervisor/supervisor/frontend/frontend.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) @@ -19,6 +20,13 @@ type QueryBackend interface { CheckMessage(identifier types.Identifier, payloadHash common.Hash) (types.SafetyLevel, error) CheckMessages(messages []types.Message, minSafety types.SafetyLevel) error CheckBlock(chainID *hexutil.U256, blockHash common.Hash, blockNumber hexutil.Uint64) (types.SafetyLevel, error) + DerivedFrom(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (eth.BlockRef, error) +} + +type UpdatesBackend interface { + UpdateLocalUnsafe(chainID types.ChainID, head eth.BlockRef) + UpdateLocalSafe(chainID types.ChainID, derivedFrom eth.BlockRef, lastDerived eth.BlockRef) + UpdateFinalizedL1(chainID types.ChainID, finalized eth.BlockRef) } type Backend interface { @@ -44,9 +52,24 @@ func (q *QueryFrontend) CheckMessages( return q.Supervisor.CheckMessages(messages, minSafety) } -// CheckBlock checks the safety-level of an L2 block as a whole. -func (q *QueryFrontend) CheckBlock(chainID *hexutil.U256, blockHash common.Hash, blockNumber hexutil.Uint64) (types.SafetyLevel, error) { - return q.Supervisor.CheckBlock(chainID, blockHash, blockNumber) +func (q *QueryFrontend) UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) { + // TODO(#12358): attach to backend + return types.ReferenceView{}, nil +} + +func (q *QueryFrontend) SafeView(ctx context.Context, chainID types.ChainID, safe types.ReferenceView) (types.ReferenceView, error) { + // TODO(#12358): attach to backend + return types.ReferenceView{}, nil +} + +func (q *QueryFrontend) Finalized(ctx context.Context, chainID types.ChainID) (eth.BlockID, error) { + // TODO(#12358): attach to backend + return eth.BlockID{}, nil +} + +func (q *QueryFrontend) DerivedFrom(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (eth.BlockRef, error) { + // TODO(#12358): attach to backend + return eth.BlockRef{}, nil } type AdminFrontend struct { @@ -67,3 +90,19 @@ func (a *AdminFrontend) Stop(ctx context.Context) error { func (a *AdminFrontend) AddL2RPC(ctx context.Context, rpc string) error { return a.Supervisor.AddL2RPC(ctx, rpc) } + +type UpdatesFrontend struct { + Supervisor UpdatesBackend +} + +func (u *UpdatesFrontend) UpdateLocalUnsafe(chainID types.ChainID, head eth.BlockRef) { + u.Supervisor.UpdateLocalUnsafe(chainID, head) +} + +func (u *UpdatesFrontend) UpdateLocalSafe(chainID types.ChainID, derivedFrom eth.BlockRef, lastDerived eth.BlockRef) { + u.Supervisor.UpdateLocalSafe(chainID, derivedFrom, lastDerived) +} + +func (u *UpdatesFrontend) UpdateFinalizedL1(chainID types.ChainID, finalized eth.BlockRef) { + u.Supervisor.UpdateFinalizedL1(chainID, finalized) +} diff --git a/op-supervisor/supervisor/service_test.go b/op-supervisor/supervisor/service_test.go index 8cc4dcfa6678..a61b96937f6e 100644 --- a/op-supervisor/supervisor/service_test.go +++ b/op-supervisor/supervisor/service_test.go @@ -5,12 +5,9 @@ import ( "testing" "time" - "github.com/ethereum-optimism/optimism/op-supervisor/config" - "github.com/holiman/uint256" "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" "github.com/ethereum-optimism/optimism/op-service/dial" @@ -19,6 +16,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/oppprof" oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-supervisor/config" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) @@ -62,8 +60,14 @@ func TestSupervisorService(t *testing.T) { require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) var dest types.SafetyLevel - err = cl.CallContext(ctx, &dest, "supervisor_checkBlock", - (*hexutil.U256)(uint256.NewInt(1)), common.Hash{0xab}, hexutil.Uint64(123)) + err = cl.CallContext(ctx, &dest, "supervisor_checkMessage", + types.Identifier{ + Origin: common.Address{0xaa}, + BlockNumber: 123, + LogIndex: 42, + Timestamp: 1234567, + ChainID: types.ChainID{0xbb}, + }, common.Hash{0xcc}) cancel() require.NoError(t, err) require.Equal(t, types.CrossUnsafe, dest, "expecting mock to return cross-unsafe") diff --git a/op-supervisor/supervisor/types/types.go b/op-supervisor/supervisor/types/types.go index ea480afa8b3c..0224b9c29e93 100644 --- a/op-supervisor/supervisor/types/types.go +++ b/op-supervisor/supervisor/types/types.go @@ -11,6 +11,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/ethereum-optimism/optimism/op-service/eth" ) type ExecutingMessage struct { @@ -160,3 +162,12 @@ func (id ChainID) ToUInt32() (uint32, error) { } return uint32(v64), nil } + +type ReferenceView struct { + Local eth.BlockID `json:"local"` + Cross eth.BlockID `json:"cross"` +} + +func (v ReferenceView) String() string { + return fmt.Sprintf("View(local: %s, cross: %s)", v.Local, v.Cross) +}