Skip to content

Commit

Permalink
op-node,op-supervisor: feed local-unsafe/safe and finalized-l1 update…
Browse files Browse the repository at this point in the history
…s to op-supervisor, pull derived-from info from supervisor

ref view
  • Loading branch information
protolambda committed Sep 26, 2024
1 parent fe20a20 commit db067d4
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 103 deletions.
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
217 changes: 126 additions & 91 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,15 +12,23 @@ 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 {
Expand All @@ -39,11 +48,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 +62,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,88 +81,120 @@ 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.CrossFinalized:
// 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
if err := d.onCrossSafeUpdateEvent(x); err != nil {
d.log.Error("Failed to process cross-safe update", "err", err)
}
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
}
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.CrossFinalized: // TODO remove the cross-finalized/finalized difference
d.log.Info("Verified cross-finalized 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,
})
d.emitter.Emit(engine.PromoteFinalizedEvent{
Ref: candidate,
})
}
// 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()

blockSafety, err := d.backend.CheckBlock(ctx, d.chainID, x.LocalUnsafe.Hash, x.LocalUnsafe.Number)
if err != nil {
return fmt.Errorf("failed to check interop safety of unsafe block: %w", 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})
}
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
}

// TODO: we should just pull the latest local-safe cross-safe block from the op-supervisor,
// and trigger a derivation-reset if the local-safe block is older,
// or if the new cross-safe block is newer than the local-safe block.

ctx, cancel := context.WithTimeout(d.driverCtx, rpcTimeout)
defer cancel()
candidate, err := d.l2.L2BlockRefByNumber(ctx, x.CrossSafe.Number+1)
if err != nil {
return fmt.Errorf("failed to fetch next cross-safe candidate: %w", err)
}
blockSafety, err := d.backend.CheckBlock(ctx, d.chainID, candidate.Hash, candidate.Number)
if err != nil {
return fmt.Errorf("failed to check interop safety of local-safe block: %w", err)
}
if blockSafety == types.CrossSafe || blockSafety == types.Finalized {
derivedFrom, err := d.backend.DerivedFrom(ctx, d.chainID, candidate.Hash, candidate.Number)
if err != nil {
return fmt.Errorf("failed to get derived-from of %s: %w", candidate, err)
}
d.emitter.Emit(engine.PromoteSafeEvent{
Ref: candidate,
DerivedFrom: derivedFrom,
})
}
if blockSafety == types.Finalized {
d.emitter.Emit(engine.PromoteFinalizedEvent{
Ref: candidate,
})
}
return nil
}
47 changes: 38 additions & 9 deletions op-service/sources/supervisor_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.Unsafe, 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()
}
Loading

0 comments on commit db067d4

Please sign in to comment.