From d8b075a3d005c2c791c6badfd59008a18310b124 Mon Sep 17 00:00:00 2001 From: protolambda Date: Tue, 24 Sep 2024 13:24:59 -0600 Subject: [PATCH] op-node,op-supervisor: feed local-unsafe/local-safe/l1-finalized data to supervisor --- op-node/rollup/engine/events.go | 19 ++ op-node/rollup/finality/finalizer.go | 6 + op-node/rollup/interop/interop.go | 263 ++++++++++++------ op-service/sources/supervisor_client.go | 47 +++- op-supervisor/supervisor/frontend/frontend.go | 45 ++- op-supervisor/supervisor/types/types.go | 11 + 6 files changed, 290 insertions(+), 101 deletions(-) diff --git a/op-node/rollup/engine/events.go b/op-node/rollup/engine/events.go index b5e010280ebc..0f4bfc30f21c 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{Ref: x.Ref}) // 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/finality/finalizer.go b/op-node/rollup/finality/finalizer.go index 046b94664e05..f158e58db4c7 100644 --- a/op-node/rollup/finality/finalizer.go +++ b/op-node/rollup/finality/finalizer.go @@ -253,6 +253,12 @@ func (fi *Finalizer) tryFinalize() { func (fi *Finalizer) onDerivedSafeBlock(l2Safe eth.L2BlockRef, derivedFrom eth.L1BlockRef) { fi.mu.Lock() defer fi.mu.Unlock() + + // TODO: stop finalizing blocks post-interop based on L1 local finality + //if !fi.cfg.IsInterop(fi.cfg.TimestampForBlock(l2Safe.Number)) { + // return nil + //} + // remember the last L2 block that we fully derived from the given finality data if len(fi.finalityData) == 0 || fi.finalityData[len(fi.finalityData)-1].L1Block.Number < derivedFrom.Number { // prune finality data if necessary, before appending any data. diff --git a/op-node/rollup/interop/interop.go b/op-node/rollup/interop/interop.go index 152020f09c70..da656bd55a33 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,168 @@ 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) + } + // 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) + } + // 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) { + 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) + } +} + +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. + // 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 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. + // 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 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 { + 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-service/sources/supervisor_client.go b/op-service/sources/supervisor_client.go index ff702010daff..a8b238b77981 100644 --- a/op-service/sources/supervisor_client.go +++ b/op-service/sources/supervisor_client.go @@ -3,6 +3,7 @@ package sources import ( "context" "fmt" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -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-supervisor/supervisor/frontend/frontend.go b/op-supervisor/supervisor/frontend/frontend.go index 41fb84e511cd..368aedcc0b1b 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.L1BlockRef, error) +} + +type UpdatesBackend interface { + UpdateLocalUnsafe(chainID types.ChainID, head eth.L2BlockRef) + UpdateLocalSafe(chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef) + UpdateFinalizedL1(chainID types.ChainID, finalized eth.L1BlockRef) } 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 + return types.ReferenceView{}, nil +} + +func (q *QueryFrontend) SafeView(ctx context.Context, chainID types.ChainID, safe types.ReferenceView) (types.ReferenceView, error) { + // TODO + return types.ReferenceView{}, nil +} + +func (q *QueryFrontend) Finalized(ctx context.Context, chainID types.ChainID) (eth.BlockID, error) { + // TODO + return eth.BlockID{}, nil +} + +func (q *QueryFrontend) DerivedFrom(ctx context.Context, chainID types.ChainID, blockHash common.Hash, blockNumber uint64) (eth.L1BlockRef, error) { + // TODO + return eth.L1BlockRef{}, 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.L2BlockRef) { + u.Supervisor.UpdateLocalUnsafe(chainID, head) +} + +func (u *UpdatesFrontend) UpdateLocalSafe(chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.L2BlockRef) { + u.Supervisor.UpdateLocalSafe(chainID, derivedFrom, lastDerived) +} + +func (u *UpdatesFrontend) UpdateFinalizedL1(chainID types.ChainID, finalized eth.L1BlockRef) { + u.Supervisor.UpdateFinalizedL1(chainID, finalized) +} 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) +}