Skip to content

Commit

Permalink
op-node,op-supervisor: feed local-unsafe/local-safe/l1-finalized data…
Browse files Browse the repository at this point in the history
… to supervisor
  • Loading branch information
protolambda committed Sep 27, 2024
1 parent 2007969 commit d8b075a
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 101 deletions.
19 changes: 19 additions & 0 deletions op-node/rollup/engine/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
6 changes: 6 additions & 0 deletions op-node/rollup/finality/finalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
263 changes: 174 additions & 89 deletions op-node/rollup/interop/interop.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package interop

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

Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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,
}
}

Expand All @@ -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
}
Loading

0 comments on commit d8b075a

Please sign in to comment.