Commit dee44ec6 authored by Axel Kingsley's avatar Axel Kingsley Committed by GitHub

Interop: Update Inputs (rebased) (#12204)

* op-node,op-supervisor: feed local-unsafe/local-safe/l1-finalized data to supervisor

* op-node,op-service,op-e2e: wip, fix interop op-node tests

* post-rebase compilation fixes

* BlockRef

* op-supervisor: fix service test, cleanup todo

* op-supervisor: link TODO comments to issue

* interop: fix e2e action test

---------
Co-authored-by: default avatarprotolambda <proto@protolambda.com>
parent 2c24e652
package interop package interop
import ( import (
"context"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-e2e/actions/helpers"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "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-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/rollup/interop" "github.com/ethereum-optimism/optimism/op-node/rollup/interop"
"github.com/ethereum-optimism/optimism/op-node/rollup/sync" "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/testlog"
"github.com/ethereum-optimism/optimism/op-service/testutils" "github.com/ethereum-optimism/optimism/op-service/testutils"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "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) { func TestInteropVerifier(gt *testing.T) {
t := helpers.NewDefaultTesting(gt) t := helpers.NewDefaultTesting(gt)
...@@ -26,14 +29,14 @@ func TestInteropVerifier(gt *testing.T) { ...@@ -26,14 +29,14 @@ func TestInteropVerifier(gt *testing.T) {
// The state genesis in this test is pre-interop however. // The state genesis in this test is pre-interop however.
sd.RollupCfg.InteropTime = new(uint64) sd.RollupCfg.InteropTime = new(uint64)
logger := testlog.Logger(t, log.LevelDebug) logger := testlog.Logger(t, log.LevelDebug)
seqMockBackend := &testutils.MockInteropBackend{} seqMockBackend := &testutils.FakeInteropBackend{}
l1Miner, seqEng, seq := helpers.SetupSequencerTest(t, sd, logger, l1Miner, seqEng, seq := helpers.SetupSequencerTest(t, sd, logger,
helpers.WithVerifierOpts(helpers.WithInteropBackend(seqMockBackend))) helpers.WithVerifierOpts(helpers.WithInteropBackend(seqMockBackend)))
batcher := helpers.NewL2Batcher(logger, sd.RollupCfg, helpers.DefaultBatcherCfg(dp), batcher := helpers.NewL2Batcher(logger, sd.RollupCfg, helpers.DefaultBatcherCfg(dp),
seq.RollupClient(), l1Miner.EthClient(), seqEng.EthClient(), seqEng.EngineClient(t, sd.RollupCfg)) seq.RollupClient(), l1Miner.EthClient(), seqEng.EthClient(), seqEng.EngineClient(t, sd.RollupCfg))
verMockBackend := &testutils.MockInteropBackend{} verMockBackend := &testutils.FakeInteropBackend{}
_, ver := helpers.SetupVerifier(t, sd, logger, _, ver := helpers.SetupVerifier(t, sd, logger,
l1Miner.L1Client(t, sd.RollupCfg), l1Miner.BlobStore(), &sync.Config{}, l1Miner.L1Client(t, sd.RollupCfg), l1Miner.BlobStore(), &sync.Config{},
helpers.WithInteropBackend(verMockBackend)) helpers.WithInteropBackend(verMockBackend))
...@@ -42,12 +45,21 @@ func TestInteropVerifier(gt *testing.T) { ...@@ -42,12 +45,21 @@ func TestInteropVerifier(gt *testing.T) {
ver.ActL2PipelineFull(t) ver.ActL2PipelineFull(t)
l2ChainID := types.ChainIDFromBig(sd.RollupCfg.L2ChainID) 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 // create an unsafe L2 block
seq.ActL2StartBlock(t) seq.ActL2StartBlock(t)
seq.ActL2EndBlock(t) seq.ActL2EndBlock(t)
seq.ActL2PipelineFull(t) seq.ActL2PipelineFull(t)
seqMockBackend.AssertExpectations(t)
status := seq.SyncStatus() status := seq.SyncStatus()
require.Equal(t, uint64(1), status.UnsafeL2.Number) require.Equal(t, uint64(1), status.UnsafeL2.Number)
require.Equal(t, uint64(0), status.CrossUnsafeL2.Number) require.Equal(t, uint64(0), status.CrossUnsafeL2.Number)
...@@ -56,10 +68,16 @@ func TestInteropVerifier(gt *testing.T) { ...@@ -56,10 +68,16 @@ func TestInteropVerifier(gt *testing.T) {
// promote it to cross-unsafe in the backend // promote it to cross-unsafe in the backend
// and see if the node picks up on it // 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.ActInteropBackendCheck(t)
seq.ActL2PipelineFull(t) seq.ActL2PipelineFull(t)
seqMockBackend.AssertExpectations(t)
status = seq.SyncStatus() status = seq.SyncStatus()
require.Equal(t, uint64(1), status.UnsafeL2.Number) require.Equal(t, uint64(1), status.UnsafeL2.Number)
require.Equal(t, uint64(1), status.CrossUnsafeL2.Number, "cross unsafe now") require.Equal(t, uint64(1), status.CrossUnsafeL2.Number, "cross unsafe now")
...@@ -74,10 +92,20 @@ func TestInteropVerifier(gt *testing.T) { ...@@ -74,10 +92,20 @@ func TestInteropVerifier(gt *testing.T) {
l1Miner.ActL1EndBlock(t) l1Miner.ActL1EndBlock(t)
// Sync the L1 block, to verify the L2 block as local-safe. // 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) seq.ActL1HeadSignal(t)
l1Head := seq.SyncStatus().HeadL1
seq.ActL2PipelineFull(t) seq.ActL2PipelineFull(t)
seqMockBackend.AssertExpectations(t)
status = seq.SyncStatus() status = seq.SyncStatus()
require.Equal(t, uint64(1), status.UnsafeL2.Number) require.Equal(t, uint64(1), status.UnsafeL2.Number)
...@@ -86,10 +114,23 @@ func TestInteropVerifier(gt *testing.T) { ...@@ -86,10 +114,23 @@ func TestInteropVerifier(gt *testing.T) {
require.Equal(t, uint64(0), status.SafeL2.Number) require.Equal(t, uint64(0), status.SafeL2.Number)
// Now mark it as cross-safe // 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.ActInteropBackendCheck(t)
seq.ActL2PipelineFull(t) seq.ActL2PipelineFull(t)
seqMockBackend.AssertExpectations(t)
status = seq.SyncStatus() status = seq.SyncStatus()
require.Equal(t, uint64(1), status.UnsafeL2.Number) require.Equal(t, uint64(1), status.UnsafeL2.Number)
...@@ -98,12 +139,30 @@ func TestInteropVerifier(gt *testing.T) { ...@@ -98,12 +139,30 @@ func TestInteropVerifier(gt *testing.T) {
require.Equal(t, uint64(1), status.SafeL2.Number, "cross-safe reached") require.Equal(t, uint64(1), status.SafeL2.Number, "cross-safe reached")
require.Equal(t, uint64(0), status.FinalizedL2.Number) 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. // 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.UnsafeViewFn = func(ctx context.Context, chainID types.ChainID, request types.ReferenceView) (types.ReferenceView, error) {
verMockBackend.ExpectCheckBlock(l2ChainID, 1, types.LocalUnsafe, nil) // for the local safe check 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.ActL1HeadSignal(t)
ver.ActL2PipelineFull(t) ver.ActL2PipelineFull(t)
verMockBackend.AssertExpectations(t)
status = ver.SyncStatus() status = ver.SyncStatus()
require.Equal(t, uint64(1), status.UnsafeL2.Number, "synced the block") require.Equal(t, uint64(1), status.UnsafeL2.Number, "synced the block")
require.Equal(t, uint64(0), status.CrossUnsafeL2.Number, "not cross-verified yet") require.Equal(t, uint64(0), status.CrossUnsafeL2.Number, "not cross-verified yet")
...@@ -111,13 +170,16 @@ func TestInteropVerifier(gt *testing.T) { ...@@ -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.SafeL2.Number, "not yet cross-safe")
require.Equal(t, uint64(0), status.FinalizedL2.Number) 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 // signal that L1 finalized; the cross-safe block we have should get finalized too
l1Miner.ActL1SafeNext(t) l1Miner.ActL1SafeNext(t)
l1Miner.ActL1FinalizeNext(t) l1Miner.ActL1FinalizeNext(t)
seq.ActL1SafeSignal(t) seq.ActL1SafeSignal(t)
seq.ActL1FinalizedSignal(t) seq.ActL1FinalizedSignal(t)
seq.ActL2PipelineFull(t) seq.ActL2PipelineFull(t)
seqMockBackend.AssertExpectations(t)
status = seq.SyncStatus() status = seq.SyncStatus()
require.Equal(t, uint64(1), status.FinalizedL2.Number, "finalized the block") require.Equal(t, uint64(1), status.FinalizedL2.Number, "finalized the block")
......
...@@ -229,6 +229,22 @@ func (ev PromoteFinalizedEvent) String() string { ...@@ -229,6 +229,22 @@ func (ev PromoteFinalizedEvent) String() string {
return "promote-finalized" 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. // CrossUpdateRequestEvent triggers update events to be emitted, repeating the current state.
type CrossUpdateRequestEvent struct { type CrossUpdateRequestEvent struct {
CrossUnsafe bool CrossUnsafe bool
...@@ -419,8 +435,11 @@ func (d *EngDeriver) OnEvent(ev event.Event) bool { ...@@ -419,8 +435,11 @@ func (d *EngDeriver) OnEvent(ev event.Event) bool {
return true return true
} }
d.ec.SetFinalizedHead(x.Ref) d.ec.SetFinalizedHead(x.Ref)
d.emitter.Emit(FinalizedUpdateEvent(x))
// Try to apply the forkchoice changes // Try to apply the forkchoice changes
d.emitter.Emit(TryUpdateEngineEvent{}) d.emitter.Emit(TryUpdateEngineEvent{})
case RequestFinalizedUpdateEvent:
d.emitter.Emit(FinalizedUpdateEvent{Ref: d.ec.Finalized()})
case CrossUpdateRequestEvent: case CrossUpdateRequestEvent:
if x.CrossUnsafe { if x.CrossUnsafe {
d.emitter.Emit(CrossUnsafeUpdateEvent{ d.emitter.Emit(CrossUnsafeUpdateEvent{
......
...@@ -2,6 +2,7 @@ package interop ...@@ -2,6 +2,7 @@ package interop
import ( import (
"context" "context"
"fmt"
"sync" "sync"
"time" "time"
...@@ -11,19 +12,28 @@ import ( ...@@ -11,19 +12,28 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup" "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/engine"
"github.com/ethereum-optimism/optimism/op-node/rollup/event" "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-service/eth"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
) )
const checkBlockTimeout = time.Second * 10 const rpcTimeout = time.Second * 10
type InteropBackend interface { type InteropBackend interface {
CheckBlock(ctx context.Context, UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error)
chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (types.SafetyLevel, 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 { type L2Source interface {
L2BlockRefByNumber(context.Context, uint64) (eth.L2BlockRef, error) 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, // InteropDeriver watches for update events (either real changes to block safety,
...@@ -39,11 +49,6 @@ type InteropDeriver struct { ...@@ -39,11 +49,6 @@ type InteropDeriver struct {
driverCtx context.Context 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 backend InteropBackend
l2 L2Source l2 L2Source
...@@ -62,7 +67,6 @@ func NewInteropDeriver(log log.Logger, cfg *rollup.Config, ...@@ -62,7 +67,6 @@ func NewInteropDeriver(log log.Logger, cfg *rollup.Config,
cfg: cfg, cfg: cfg,
chainID: types.ChainIDFromBig(cfg.L2ChainID), chainID: types.ChainIDFromBig(cfg.L2ChainID),
driverCtx: driverCtx, driverCtx: driverCtx,
derivedFrom: make(map[common.Hash]eth.L1BlockRef),
backend: backend, backend: backend,
l2: l2, l2: l2,
} }
...@@ -78,87 +82,178 @@ func (d *InteropDeriver) OnEvent(ev event.Event) bool { ...@@ -78,87 +82,178 @@ func (d *InteropDeriver) OnEvent(ev event.Event) bool {
switch x := ev.(type) { switch x := ev.(type) {
case engine.UnsafeUpdateEvent: 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: case engine.CrossUnsafeUpdateEvent:
if err := d.onCrossUnsafe(x); err != nil {
d.log.Error("Failed to process cross-unsafe update", "err", err)
}
case engine.CrossSafeUpdateEvent:
if err := d.onCrossSafeUpdateEvent(x); err != nil {
d.log.Error("Failed to process cross-safe update", "err", err)
}
case engine.FinalizedUpdateEvent:
if err := d.onFinalizedUpdate(x); err != nil {
d.log.Error("Failed to process finalized update", "err", err)
}
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 { if x.CrossUnsafe.Number >= x.LocalUnsafe.Number {
break // nothing left to promote return nil // nothing left to promote
} }
// Pre-interop the engine itself handles promotion to cross-unsafe. // Pre-interop the engine itself handles promotion to cross-unsafe.
// Check if the next block (still unsafe) can be promoted to cross-unsafe. // Start checking cross-unsafe once the local-unsafe block is in the interop update.
if !d.cfg.IsInterop(d.cfg.TimestampForBlock(x.CrossUnsafe.Number + 1)) { if !d.cfg.IsInterop(x.LocalUnsafe.Time) {
return false return nil
} }
ctx, cancel := context.WithTimeout(d.driverCtx, checkBlockTimeout) ctx, cancel := context.WithTimeout(d.driverCtx, rpcTimeout)
defer cancel() defer cancel()
candidate, err := d.l2.L2BlockRefByNumber(ctx, x.CrossUnsafe.Number+1) view := types.ReferenceView{
if err != nil { Local: x.LocalUnsafe.ID(),
d.log.Warn("Failed to fetch next cross-unsafe candidate", "err", err) Cross: x.CrossUnsafe.ID(),
break
} }
blockSafety, err := d.backend.CheckBlock(ctx, d.chainID, candidate.Hash, candidate.Number) result, err := d.backend.UnsafeView(ctx, d.chainID, view)
if err != nil { if err != nil {
d.log.Warn("Failed to check interop safety of unsafe block", "err", err) return fmt.Errorf("failed to check unsafe-level view: %w", err)
break
} }
switch blockSafety { if result.Cross.Number == x.CrossUnsafe.Number {
case types.CrossUnsafe, types.CrossSafe, types.Finalized: // supervisor is in sync with op-node
// Hold off on promoting higher than cross-unsafe, return nil
// this will happen once we verify it to be local-safe first.
d.emitter.Emit(engine.PromoteCrossUnsafeEvent{Ref: candidate})
} }
case engine.LocalSafeUpdateEvent: if result.Cross.Number < x.CrossUnsafe.Number {
d.log.Debug("Local safe update event", "block", x.Ref.Hash, "derivedFrom", x.DerivedFrom) d.log.Warn("op-supervisor is behind known cross-unsafe block", "supervisor", result.Cross, "known", x.CrossUnsafe)
d.derivedFrom[x.Ref.Hash] = x.DerivedFrom return nil
d.emitter.Emit(engine.RequestCrossSafeEvent{}) }
case engine.CrossSafeUpdateEvent: 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 { if x.CrossSafe.Number >= x.LocalSafe.Number {
break // nothing left to promote return nil // nothing left to promote
} }
// Pre-interop the engine itself handles promotion to cross-safe. // 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. // Start checking cross-safe once the local-safe block is in the interop update.
if !d.cfg.IsInterop(d.cfg.TimestampForBlock(x.CrossSafe.Number + 1)) { if !d.cfg.IsInterop(x.LocalSafe.Time) {
return false return nil
} }
ctx, cancel := context.WithTimeout(d.driverCtx, checkBlockTimeout) ctx, cancel := context.WithTimeout(d.driverCtx, rpcTimeout)
defer cancel() defer cancel()
candidate, err := d.l2.L2BlockRefByNumber(ctx, x.CrossSafe.Number+1) view := types.ReferenceView{
Local: x.LocalSafe.ID(),
Cross: x.CrossSafe.ID(),
}
result, err := d.backend.SafeView(ctx, d.chainID, view)
if err != nil { if err != nil {
d.log.Warn("Failed to fetch next cross-safe candidate", "err", err) return fmt.Errorf("failed to check safe-level view: %w", err)
break }
if result.Cross.Number == x.CrossSafe.Number {
// supervisor is in sync with op-node
return nil
} }
blockSafety, err := d.backend.CheckBlock(ctx, d.chainID, candidate.Hash, candidate.Number) 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 { if err != nil {
d.log.Warn("Failed to check interop safety of local-safe block", "err", err) return fmt.Errorf("failed to get derived-from of %s: %w", result.Cross, err)
break }
} ref, err := d.l2.L2BlockRefByHash(ctx, result.Cross.Hash)
derivedFrom, ok := d.derivedFrom[candidate.Hash] if err != nil {
if !ok { return fmt.Errorf("failed to get block ref of %s: %w", result.Cross, err)
d.log.Warn("Unknown block candidate source, cannot promote block safety", "block", candidate, "safety", blockSafety) }
break
}
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{ d.emitter.Emit(engine.PromoteSafeEvent{
Ref: candidate, Ref: ref,
DerivedFrom: derivedFrom, DerivedFrom: derivedFrom,
}) })
d.emitter.Emit(engine.PromoteFinalizedEvent{ d.emitter.Emit(engine.RequestFinalizedUpdateEvent{})
Ref: candidate, 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)
} }
// no reorg support yet; the safe L2 head will finalize eventually, no exceptions // Check if we can finalize something new
default: if finalized.Number == x.Ref.Number {
return false // supervisor is in sync with op-node
return nil
} }
return true 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
} }
...@@ -6,17 +6,18 @@ import ( ...@@ -6,17 +6,18 @@ import (
"math/rand" // nosemgrep "math/rand" // nosemgrep
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-node/rollup" "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/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/testlog"
"github.com/ethereum-optimism/optimism/op-service/testutils" "github.com/ethereum-optimism/optimism/op-service/testutils"
supervisortypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" supervisortypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
) )
var _ InteropBackend = (*testutils.MockInteropBackend)(nil)
func TestInteropDeriver(t *testing.T) { func TestInteropDeriver(t *testing.T) {
logger := testlog.Logger(t, log.LevelInfo) logger := testlog.Logger(t, log.LevelInfo)
l2Source := &testutils.MockL2Client{} l2Source := &testutils.MockL2Client{}
...@@ -31,25 +32,34 @@ func TestInteropDeriver(t *testing.T) { ...@@ -31,25 +32,34 @@ func TestInteropDeriver(t *testing.T) {
interopDeriver.AttachEmitter(emitter) interopDeriver.AttachEmitter(emitter)
rng := rand.New(rand.NewSource(123)) 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{}) emitter.ExpectOnce(engine.RequestCrossUnsafeEvent{})
interopDeriver.OnEvent(engine.UnsafeUpdateEvent{ unsafeHead := testutils.RandomL2BlockRef(rng)
Ref: testutils.RandomL2BlockRef(rng), interopBackend.ExpectUpdateLocalUnsafe(chainID, unsafeHead, nil)
}) interopDeriver.OnEvent(engine.UnsafeUpdateEvent{Ref: unsafeHead})
emitter.AssertExpectations(t) emitter.AssertExpectations(t)
interopBackend.AssertExpectations(t)
}) })
t.Run("establish cross-unsafe", func(t *testing.T) { t.Run("establish cross-unsafe", func(t *testing.T) {
crossUnsafe := testutils.RandomL2BlockRef(rng) oldCrossUnsafe := testutils.RandomL2BlockRef(rng)
firstLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, crossUnsafe, crossUnsafe.L1Origin) nextCrossUnsafe := testutils.NextRandomL2Ref(rng, 2, oldCrossUnsafe, oldCrossUnsafe.L1Origin)
lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, firstLocalUnsafe, firstLocalUnsafe.L1Origin) lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, nextCrossUnsafe, nextCrossUnsafe.L1Origin)
interopBackend.ExpectCheckBlock( localView := supervisortypes.ReferenceView{
chainID, firstLocalUnsafe.Number, supervisortypes.CrossUnsafe, nil) 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{ emitter.ExpectOnce(engine.PromoteCrossUnsafeEvent{
Ref: firstLocalUnsafe, Ref: nextCrossUnsafe,
}) })
l2Source.ExpectL2BlockRefByNumber(firstLocalUnsafe.Number, firstLocalUnsafe, nil)
interopDeriver.OnEvent(engine.CrossUnsafeUpdateEvent{ interopDeriver.OnEvent(engine.CrossUnsafeUpdateEvent{
CrossUnsafe: crossUnsafe, CrossUnsafe: oldCrossUnsafe,
LocalUnsafe: lastLocalUnsafe, LocalUnsafe: lastLocalUnsafe,
}) })
interopBackend.AssertExpectations(t) interopBackend.AssertExpectations(t)
...@@ -57,53 +67,62 @@ func TestInteropDeriver(t *testing.T) { ...@@ -57,53 +67,62 @@ func TestInteropDeriver(t *testing.T) {
l2Source.AssertExpectations(t) l2Source.AssertExpectations(t)
}) })
t.Run("deny cross-unsafe", func(t *testing.T) { t.Run("deny cross-unsafe", func(t *testing.T) {
crossUnsafe := testutils.RandomL2BlockRef(rng) oldCrossUnsafe := testutils.RandomL2BlockRef(rng)
firstLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, crossUnsafe, crossUnsafe.L1Origin) nextCrossUnsafe := testutils.NextRandomL2Ref(rng, 2, oldCrossUnsafe, oldCrossUnsafe.L1Origin)
lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, firstLocalUnsafe, firstLocalUnsafe.L1Origin) lastLocalUnsafe := testutils.NextRandomL2Ref(rng, 2, nextCrossUnsafe, nextCrossUnsafe.L1Origin)
interopBackend.ExpectCheckBlock( localView := supervisortypes.ReferenceView{
chainID, firstLocalUnsafe.Number, supervisortypes.LocalUnsafe, nil) Local: lastLocalUnsafe.ID(),
l2Source.ExpectL2BlockRefByNumber(firstLocalUnsafe.Number, firstLocalUnsafe, nil) 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{ interopDeriver.OnEvent(engine.CrossUnsafeUpdateEvent{
CrossUnsafe: crossUnsafe, CrossUnsafe: oldCrossUnsafe,
LocalUnsafe: lastLocalUnsafe, LocalUnsafe: lastLocalUnsafe,
}) })
interopBackend.AssertExpectations(t) interopBackend.AssertExpectations(t)
// no cross-unsafe promote event is expected emitter.AssertExpectations(t) // no promote-cross-unsafe event expected
emitter.AssertExpectations(t)
l2Source.AssertExpectations(t) 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) derivedFrom := testutils.RandomBlockRef(rng)
localSafe := testutils.RandomL2BlockRef(rng) localSafe := testutils.RandomL2BlockRef(rng)
emitter.ExpectOnce(engine.RequestCrossSafeEvent{}) interopBackend.ExpectUpdateLocalSafe(chainID, derivedFrom, localSafe, nil)
interopDeriver.OnEvent(engine.LocalSafeUpdateEvent{ interopDeriver.OnEvent(engine.LocalSafeUpdateEvent{
Ref: localSafe, Ref: localSafe,
DerivedFrom: derivedFrom, DerivedFrom: derivedFrom,
}) })
require.Contains(t, interopDeriver.derivedFrom, localSafe.Hash)
require.Equal(t, derivedFrom, interopDeriver.derivedFrom[localSafe.Hash])
emitter.AssertExpectations(t) emitter.AssertExpectations(t)
interopBackend.AssertExpectations(t)
}) })
t.Run("establish cross-safe", func(t *testing.T) { t.Run("establish cross-safe", func(t *testing.T) {
derivedFrom := testutils.RandomBlockRef(rng) derivedFrom := testutils.RandomBlockRef(rng)
crossSafe := testutils.RandomL2BlockRef(rng) oldCrossSafe := testutils.RandomL2BlockRef(rng)
firstLocalSafe := testutils.NextRandomL2Ref(rng, 2, crossSafe, crossSafe.L1Origin) nextCrossSafe := testutils.NextRandomL2Ref(rng, 2, oldCrossSafe, oldCrossSafe.L1Origin)
lastLocalSafe := testutils.NextRandomL2Ref(rng, 2, firstLocalSafe, firstLocalSafe.L1Origin) lastLocalSafe := testutils.NextRandomL2Ref(rng, 2, nextCrossSafe, nextCrossSafe.L1Origin)
emitter.ExpectOnce(engine.RequestCrossSafeEvent{}) localView := supervisortypes.ReferenceView{
// The local safe block must be known, for the derived-from mapping to work Local: lastLocalSafe.ID(),
interopDeriver.OnEvent(engine.LocalSafeUpdateEvent{ Cross: oldCrossSafe.ID(),
Ref: firstLocalSafe, }
DerivedFrom: derivedFrom, supervisorView := supervisortypes.ReferenceView{
}) Local: lastLocalSafe.ID(),
interopBackend.ExpectCheckBlock( Cross: nextCrossSafe.ID(),
chainID, firstLocalSafe.Number, supervisortypes.CrossSafe, nil) }
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{ emitter.ExpectOnce(engine.PromoteSafeEvent{
Ref: firstLocalSafe, Ref: nextCrossSafe,
DerivedFrom: derivedFrom, DerivedFrom: derivedFrom,
}) })
l2Source.ExpectL2BlockRefByNumber(firstLocalSafe.Number, firstLocalSafe, nil) emitter.ExpectOnce(engine.RequestFinalizedUpdateEvent{})
interopDeriver.OnEvent(engine.CrossSafeUpdateEvent{ interopDeriver.OnEvent(engine.CrossSafeUpdateEvent{
CrossSafe: crossSafe, CrossSafe: oldCrossSafe,
LocalSafe: lastLocalSafe, LocalSafe: lastLocalSafe,
}) })
interopBackend.AssertExpectations(t) interopBackend.AssertExpectations(t)
...@@ -111,26 +130,54 @@ func TestInteropDeriver(t *testing.T) { ...@@ -111,26 +130,54 @@ func TestInteropDeriver(t *testing.T) {
l2Source.AssertExpectations(t) l2Source.AssertExpectations(t)
}) })
t.Run("deny cross-safe", func(t *testing.T) { t.Run("deny cross-safe", func(t *testing.T) {
derivedFrom := testutils.RandomBlockRef(rng) oldCrossSafe := testutils.RandomL2BlockRef(rng)
crossSafe := testutils.RandomL2BlockRef(rng) nextCrossSafe := testutils.NextRandomL2Ref(rng, 2, oldCrossSafe, oldCrossSafe.L1Origin)
firstLocalSafe := testutils.NextRandomL2Ref(rng, 2, crossSafe, crossSafe.L1Origin) lastLocalSafe := testutils.NextRandomL2Ref(rng, 2, nextCrossSafe, nextCrossSafe.L1Origin)
lastLocalSafe := testutils.NextRandomL2Ref(rng, 2, firstLocalSafe, firstLocalSafe.L1Origin) localView := supervisortypes.ReferenceView{
emitter.ExpectOnce(engine.RequestCrossSafeEvent{}) Local: lastLocalSafe.ID(),
// The local safe block must be known, for the derived-from mapping to work Cross: oldCrossSafe.ID(),
interopDeriver.OnEvent(engine.LocalSafeUpdateEvent{ }
Ref: firstLocalSafe, supervisorView := supervisortypes.ReferenceView{
DerivedFrom: derivedFrom, Local: lastLocalSafe.ID(),
}) Cross: oldCrossSafe.ID(), // stay on old cross-safe
interopBackend.ExpectCheckBlock( }
chainID, firstLocalSafe.Number, supervisortypes.LocalSafe, nil) interopBackend.ExpectSafeView(chainID, localView, supervisorView, nil)
l2Source.ExpectL2BlockRefByNumber(firstLocalSafe.Number, firstLocalSafe, nil)
interopDeriver.OnEvent(engine.CrossSafeUpdateEvent{ interopDeriver.OnEvent(engine.CrossSafeUpdateEvent{
CrossSafe: crossSafe, CrossSafe: oldCrossSafe,
LocalSafe: lastLocalSafe, LocalSafe: lastLocalSafe,
}) })
interopBackend.AssertExpectations(t) interopBackend.AssertExpectations(t)
// no cross-safe promote event is expected emitter.AssertExpectations(t) // no promote-cross-safe event expected
emitter.AssertExpectations(t)
l2Source.AssertExpectations(t) 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)
})
} }
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/op-service/client" "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" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
) )
...@@ -65,20 +66,48 @@ func (cl *SupervisorClient) AddL2RPC( ...@@ -65,20 +66,48 @@ func (cl *SupervisorClient) AddL2RPC(
return result return result
} }
func (cl *SupervisorClient) CheckBlock(ctx context.Context, func (cl *SupervisorClient) UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) {
chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (types.SafetyLevel, error) { var result types.ReferenceView
var result types.SafetyLevel err := cl.client.CallContext(ctx, &result, "supervisor_unsafeView", (*hexutil.U256)(&chainID), unsafe)
err := cl.client.CallContext(
ctx,
&result,
"supervisor_checkBlock",
(*hexutil.U256)(&chainID), blockHash, hexutil.Uint64(blockNumber))
if err != nil { 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 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() { func (cl *SupervisorClient) Close() {
cl.client.Close() cl.client.Close()
} }
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)
}
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
) )
...@@ -14,13 +15,89 @@ type MockInteropBackend struct { ...@@ -14,13 +15,89 @@ type MockInteropBackend struct {
Mock mock.Mock Mock mock.Mock
} }
func (m *MockInteropBackend) ExpectCheckBlock(chainID types.ChainID, blockNumber uint64, safety types.SafetyLevel, err error) { func (m *MockInteropBackend) UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) {
m.Mock.On("CheckBlock", chainID, blockNumber).Once().Return(safety, &err) 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) { func (m *MockInteropBackend) ExpectUnsafeView(chainID types.ChainID, unsafe types.ReferenceView, result types.ReferenceView, err error) {
result := m.Mock.MethodCalled("CheckBlock", chainID, blockNumber) m.Mock.On("UnsafeView", chainID, unsafe).Once().Return(result, &err)
return result.Get(0).(types.SafetyLevel), *result.Get(1).(*error) }
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) { func (m *MockInteropBackend) AssertExpectations(t mock.TestingT) {
......
...@@ -226,3 +226,12 @@ func (su *SupervisorBackend) CheckBlock(chainID *hexutil.U256, blockHash common. ...@@ -226,3 +226,12 @@ func (su *SupervisorBackend) CheckBlock(chainID *hexutil.U256, blockHash common.
safest := su.db.Safest(types.ChainID(*chainID), uint64(blockNumber), 0) safest := su.db.Safest(types.ChainID(*chainID), uint64(blockNumber), 0)
return safest, nil 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
}
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "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/frontend"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
) )
...@@ -55,6 +56,10 @@ func (m *MockBackend) CheckBlock(chainID *hexutil.U256, blockHash common.Hash, b ...@@ -55,6 +56,10 @@ func (m *MockBackend) CheckBlock(chainID *hexutil.U256, blockHash common.Hash, b
return types.CrossUnsafe, nil 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 { func (m *MockBackend) Close() error {
return nil return nil
} }
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
) )
...@@ -19,6 +20,13 @@ type QueryBackend interface { ...@@ -19,6 +20,13 @@ type QueryBackend interface {
CheckMessage(identifier types.Identifier, payloadHash common.Hash) (types.SafetyLevel, error) CheckMessage(identifier types.Identifier, payloadHash common.Hash) (types.SafetyLevel, error)
CheckMessages(messages []types.Message, minSafety types.SafetyLevel) error CheckMessages(messages []types.Message, minSafety types.SafetyLevel) error
CheckBlock(chainID *hexutil.U256, blockHash common.Hash, blockNumber hexutil.Uint64) (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 { type Backend interface {
...@@ -44,9 +52,24 @@ func (q *QueryFrontend) CheckMessages( ...@@ -44,9 +52,24 @@ func (q *QueryFrontend) CheckMessages(
return q.Supervisor.CheckMessages(messages, minSafety) return q.Supervisor.CheckMessages(messages, minSafety)
} }
// CheckBlock checks the safety-level of an L2 block as a whole. func (q *QueryFrontend) UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) {
func (q *QueryFrontend) CheckBlock(chainID *hexutil.U256, blockHash common.Hash, blockNumber hexutil.Uint64) (types.SafetyLevel, error) { // TODO(#12358): attach to backend
return q.Supervisor.CheckBlock(chainID, blockHash, blockNumber) 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 { type AdminFrontend struct {
...@@ -67,3 +90,19 @@ func (a *AdminFrontend) Stop(ctx context.Context) error { ...@@ -67,3 +90,19 @@ func (a *AdminFrontend) Stop(ctx context.Context) error {
func (a *AdminFrontend) AddL2RPC(ctx context.Context, rpc string) error { func (a *AdminFrontend) AddL2RPC(ctx context.Context, rpc string) error {
return a.Supervisor.AddL2RPC(ctx, rpc) 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)
}
...@@ -5,12 +5,9 @@ import ( ...@@ -5,12 +5,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-supervisor/config"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/dial"
...@@ -19,6 +16,7 @@ import ( ...@@ -19,6 +16,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/oppprof" "github.com/ethereum-optimism/optimism/op-service/oppprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum-optimism/optimism/op-service/testlog" "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" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
) )
...@@ -62,8 +60,14 @@ func TestSupervisorService(t *testing.T) { ...@@ -62,8 +60,14 @@ func TestSupervisorService(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
var dest types.SafetyLevel var dest types.SafetyLevel
err = cl.CallContext(ctx, &dest, "supervisor_checkBlock", err = cl.CallContext(ctx, &dest, "supervisor_checkMessage",
(*hexutil.U256)(uint256.NewInt(1)), common.Hash{0xab}, hexutil.Uint64(123)) types.Identifier{
Origin: common.Address{0xaa},
BlockNumber: 123,
LogIndex: 42,
Timestamp: 1234567,
ChainID: types.ChainID{0xbb},
}, common.Hash{0xcc})
cancel() cancel()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, types.CrossUnsafe, dest, "expecting mock to return cross-unsafe") require.Equal(t, types.CrossUnsafe, dest, "expecting mock to return cross-unsafe")
......
...@@ -11,6 +11,8 @@ import ( ...@@ -11,6 +11,8 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/op-service/eth"
) )
type ExecutingMessage struct { type ExecutingMessage struct {
...@@ -160,3 +162,12 @@ func (id ChainID) ToUInt32() (uint32, error) { ...@@ -160,3 +162,12 @@ func (id ChainID) ToUInt32() (uint32, error) {
} }
return uint32(v64), nil 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)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment