Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions op-acceptance-tests/tests/sync/follow_l2/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,19 @@ func TestFollowL2_ReorgRecovery(gt *testing.T) {
)
}

func TestFollowL2_SafeAndFinalized(gt *testing.T) {
func TestFollowL2_Safe_Finalized_CurrentL1(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewSingleChainTwoVerifiersWithoutCheck(t)
logger := t.Logger()
require := t.Require()

// Takes about 2 minutes for L1 finalization
attempts := 70
target := uint64(3)

// L2CL is the sequencer with follow source, derivation disabled
// L2CL is the sequencer with EL follow source, derivation disabled
// L2CLB is the verifier without follow source, derivation enabled
// L2CLC is the verifier with follow source, derivation disabled
// L2CLC is the verifier with CL follow source, derivation disabled
// All verifiers must eventually advance unsafe, safe, finalized
checkMatchedAll := func(lvl types.SafetyLevel) {
dsl.CheckAll(t,
Expand All @@ -118,6 +120,15 @@ func TestFollowL2_SafeAndFinalized(gt *testing.T) {

checkMatchedAll(types.Finalized)
logger.Info("Finalized head followed source", "target", target)

// EL Following CL does not gain CurrentL1
require.Equal(sys.L2CL.SyncStatus().CurrentL1, eth.L1BlockRef{})
// CL Following CL passed genesis
require.NotEqual(sys.L2CLC.SyncStatus().CurrentL1, eth.L1BlockRef{})
// CL Following CL gain CurrentL1
attempts = 10
sys.L2CLC.CurrentL1Matched(sys.L2CLB, attempts)
logger.Info("CurrentL1 followed source", "currentL1", sys.L2CLC.SyncStatus().CurrentL1)
}

func TestFollowL2_WithoutCLP2P(gt *testing.T) {
Expand Down
14 changes: 14 additions & 0 deletions op-devstack/dsl/l2_cl.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,17 @@ func (cl *L2CLNode) UnsafeHead() *BlockRefResult {
func (cl *L2CLNode) SafeHead() *BlockRefResult {
return &BlockRefResult{T: cl.t, BlockRef: cl.HeadBlockRef(types.CrossSafe)}
}

func (cl *L2CLNode) CurrentL1Matched(refNode *L2CLNode, attempts int) {
cl.require.NoError(retry.Do0(cl.ctx, attempts, &retry.FixedStrategy{Dur: 1 * time.Second},
func() error {
currentL1 := cl.SyncStatus().CurrentL1
ref := refNode.SyncStatus().CurrentL1
if currentL1 == ref {
cl.log.Info("CurrentL1 reached", "currentL1", currentL1)
return nil
}
cl.log.Info("Chain sync status", "currentL1", currentL1.Number, "ref", ref)
return fmt.Errorf("expected currentL1 to match")
}))
}
4 changes: 2 additions & 2 deletions op-devstack/sysgo/l2_cl.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,13 @@ func WithL2CLNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack
}
}

func WithL2CLNodeFollowL2(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2ELID, l2ELFollowSourceID stack.L2ELNodeID, opts ...L2CLOption) stack.Option[*Orchestrator] {
func WithL2CLNodeFollowL2(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, l2FollowSourceID stack.IDWithChain, opts ...L2CLOption) stack.Option[*Orchestrator] {
switch os.Getenv("DEVSTACK_L2CL_KIND") {
case "kona":
panic("kona does not support following")
case "supernode":
panic("supernode does not support following")
default:
return WithOpNodeFollowL2(l2CLID, l1CLID, l1ELID, l2ELID, l2ELFollowSourceID, opts...)
return WithOpNodeFollowL2(l2CLID, l1CLID, l1ELID, l2ELID, l2FollowSourceID, opts...)
}
}
23 changes: 18 additions & 5 deletions op-devstack/sysgo/l2_cl_opnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,27 @@ func (n *OpNode) Stop() {
n.opNode = nil
}

func WithOpNodeFollowL2(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2ELID, l2ELFollowSourceID stack.L2ELNodeID, opts ...L2CLOption) stack.Option[*Orchestrator] {
func fetchFollowSourceRPC(orch *Orchestrator, p devtest.P, l2FollowSourceID stack.IDWithChain) string {
require := p.Require()
if l2ELFollowSourceID, ok := l2FollowSourceID.(stack.L2ELNodeID); ok {
l2ELFollowSource, ok := orch.l2ELs.Get(l2ELFollowSourceID)
require.True(ok, "l2 EL Follow Source required")
return l2ELFollowSource.UserRPC()
}
if l2CLFollowSourceID, ok := l2FollowSourceID.(stack.L2CLNodeID); ok {
l2CLFollowSource, ok := orch.l2CLs.Get(l2CLFollowSourceID)
require.True(ok, "l2 CL Follow Source required")
return l2CLFollowSource.UserRPC()
}
require.Failf("Invalid Follow Source", "l2 Follow Source does not implement L2CL nor L2EL: %s", l2FollowSourceID)
return ""
}

func WithOpNodeFollowL2(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, l2FollowSourceID stack.IDWithChain, opts ...L2CLOption) stack.Option[*Orchestrator] {
return stack.AfterDeploy(func(orch *Orchestrator) {
followSource := func(orch *Orchestrator) string {
p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), l2CLID))
require := p.Require()
l2ELFollowSource, ok := orch.l2ELs.Get(l2ELFollowSourceID)
require.True(ok, "l2 EL Follow Source required")
return l2ELFollowSource.UserRPC()
return fetchFollowSourceRPC(orch, p, l2FollowSourceID)
}(orch)
opts = append(opts, L2CLFollowSource(followSource))
withOpNode(l2CLID, l1CLID, l1ELID, l2ELID, opts...)(orch)
Expand Down
4 changes: 3 additions & 1 deletion op-devstack/sysgo/system_singlechain_twoverifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ func DefaultSingleChainTwoVerifiersFollowL2System(dest *DefaultSingleChainTwoVer
opt.Add(WithL2CLNode(ids.L2CLB, ids.L1CL, ids.L1EL, ids.L2ELB, L2CLVerifierDisableUnsafeOnly()))

opt.Add(WithL2ELNode(ids.L2EL))
// Follow EL
opt.Add(WithL2CLNodeFollowL2(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL, ids.L2ELB, L2CLSequencer()))

opt.Add(WithL2ELNode(ids.L2ELC))
opt.Add(WithL2CLNodeFollowL2(ids.L2CLC, ids.L1CL, ids.L1EL, ids.L2ELC, ids.L2ELB))
// Follow CL
opt.Add(WithL2CLNodeFollowL2(ids.L2CLC, ids.L1CL, ids.L1EL, ids.L2ELC, ids.L2CLB))

opt.Add(WithL2CLP2PConnection(ids.L2CL, ids.L2CLB))
opt.Add(WithL2ELP2PConnection(ids.L2EL, ids.L2ELB))
Expand Down
10 changes: 5 additions & 5 deletions op-node/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ type OpNode struct {
p2pSigner p2p.Signer // p2p gossip application messages will be signed with this signer
runCfg *runcfg.RuntimeConfig // runtime configurables

l2FollowSource *sources.L2Client // (Optional) L2 Follow source when derivation disabled
l2FollowSource *sources.FollowClient // (Optional) L2 Follow source when derivation disabled

safeDB closableSafeDB

Expand Down Expand Up @@ -604,7 +604,7 @@ func initL2(ctx context.Context, cfg *config.Config, node *OpNode) (*sources.Eng

var upstreamFollowSource driver.UpstreamFollowSource
if node.cfg.Sync.UnsafeOnly {
upstreamFollowSource = driver.NewL2ELFollowSource(node.l2FollowSource, node.l1Source)
upstreamFollowSource = driver.NewL2FollowSource(node.l2FollowSource, node.l1Source)
}

l2Driver := driver.NewDriver(node.eventSys, node.eventDrain, &cfg.Driver, &cfg.Rollup, cfg.L1ChainConfig, cfg.DependencySet, l2Source, node.l1Source, upstreamFollowSource,
Expand All @@ -620,14 +620,14 @@ func initL2(ctx context.Context, cfg *config.Config, node *OpNode) (*sources.Eng
return l2Source, sys, l2Driver, safeDB, nil
}

func initFollowSource(ctx context.Context, cfg *config.Config, node *OpNode) (*sources.L2Client, error) {
func initFollowSource(ctx context.Context, cfg *config.Config, node *OpNode) (*sources.FollowClient, error) {
rpcClient, rpcCfg, err := cfg.L2FollowSource.Setup(ctx, node.log, &node.cfg.Rollup, node.metrics)
if err != nil {
return nil, fmt.Errorf("failed to setup L2 follow source RPC client: %w", err)
}
l2FollowSource, err := sources.NewL2Client(rpcClient, node.log, node.metrics.L2FollowSourceCache, rpcCfg)
l2FollowSource, err := sources.NewFollowClient(rpcClient, node.log, node.metrics.L2FollowSourceCache, rpcCfg)
if err != nil {
return nil, fmt.Errorf("failed to create Eth client: %w", err)
return nil, fmt.Errorf("failed to create follow client: %w", err)
}
return l2FollowSource, nil
}
Expand Down
19 changes: 10 additions & 9 deletions op-node/rollup/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,19 +524,20 @@ func (s *Driver) followUpstream() {
if !s.upstreamFollowSource.CanFollowL2() {
return
}
eFinalized, err := s.upstreamFollowSource.L2BlockRefByLabel(s.driverCtx, eth.Finalized)
status, err := s.upstreamFollowSource.GetFollowStatus(s.driverCtx)
if err != nil {
s.log.Warn("Follow Upstream: Failed to fetch finalizedRef", "err", err)
s.log.Warn("Follow Upstream: Failed to fetch status", "err", err)
return
}
eSafe, err := s.upstreamFollowSource.L2BlockRefByLabel(s.driverCtx, eth.Safe)
if err != nil {
s.log.Warn("Follow Upstream: Failed to fetch safeRef", "err", err)
if status.FinalizedL2.Number > status.SafeL2.Number {
s.log.Warn("Follow Upstream: Invalid external state, finalized is ahead of safe", "safe", status.SafeL2.Number, "finalized", status.FinalizedL2.Number)
return
}
if eFinalized.Number > eSafe.Number {
s.log.Warn("Follow Upstream: Invalid external state, finalized is ahead of safe", "safe", eSafe, "finalized", eFinalized)
return
if (status.CurrentL1 == eth.L1BlockRef{}) {
s.log.Debug("Follow Upstream: CurrentL1 not supported")
} else {
s.log.Debug("Follow Upstream: Inject L1 Info", "currentL1", status.CurrentL1)
s.emitter.Emit(s.driverCtx, derive.DeriverL1StatusEvent{Origin: status.CurrentL1})
Comment thread
pcw109550 marked this conversation as resolved.
}
s.SyncDeriver.Engine.FollowSource(eSafe, eFinalized)
s.SyncDeriver.Engine.FollowSource(status.SafeL2, status.FinalizedL2)
}
30 changes: 11 additions & 19 deletions op-node/rollup/driver/follow_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,36 @@ type L1FollowSource interface {
L1BlockRefByNumber(ctx context.Context, num uint64) (eth.L1BlockRef, error)
}

// L2FollowSource provides access to L2 block references for upstream following.
type L2FollowSource interface {
L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
}

// UpstreamFollowSource combines L1 and L2 follow sources.
// L2 following may be optionally disabled.
type UpstreamFollowSource interface {
L2FollowSource
L1FollowSource
CanFollowL2() bool
GetFollowStatus(ctx context.Context) (*sources.FollowStatus, error)
}

type L2ELFollowSource struct {
l2Source *sources.L2Client
type L2FollowSource struct {
l2Source *sources.FollowClient
l1Source L1FollowSource
}

var _ UpstreamFollowSource = (*L2ELFollowSource)(nil)
var _ UpstreamFollowSource = (*L2FollowSource)(nil)

func NewL2ELFollowSource(client *sources.L2Client, l1Source L1FollowSource) *L2ELFollowSource {
func NewL2FollowSource(client *sources.FollowClient, l1Source L1FollowSource) *L2FollowSource {
if l1Source == nil {
panic("NewL2ELFollowSource: l1Source must not be nil")
panic("NewL2FollowSource: l1Source must not be nil")
}
return &L2ELFollowSource{l2Source: client, l1Source: l1Source}
return &L2FollowSource{l2Source: client, l1Source: l1Source}
}

func (fs *L2ELFollowSource) CanFollowL2() bool {
func (fs *L2FollowSource) CanFollowL2() bool {
return fs.l2Source != nil
}

func (fs *L2ELFollowSource) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) {
if fs.l2Source == nil {
return eth.L2BlockRef{}, errL2FollowSourceNotEnabled
}
return fs.l2Source.L2BlockRefByLabel(ctx, label)
func (fs *L2FollowSource) GetFollowStatus(ctx context.Context) (*sources.FollowStatus, error) {
return fs.l2Source.GetFollowStatus(ctx)
}

func (fs *L2ELFollowSource) L1BlockRefByNumber(ctx context.Context, num uint64) (eth.L1BlockRef, error) {
func (fs *L2FollowSource) L1BlockRefByNumber(ctx context.Context, num uint64) (eth.L1BlockRef, error) {
return fs.l1Source.L1BlockRefByNumber(ctx, num)
}
66 changes: 66 additions & 0 deletions op-service/sources/follow_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package sources

import (
"context"
"fmt"
"time"

"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources/caching"
"github.com/ethereum/go-ethereum/log"
)

type FollowClient struct {
l2Client *L2Client
rollupClient *RollupClient
followCL bool
}

type FollowStatus struct {
SafeL2 eth.L2BlockRef
FinalizedL2 eth.L2BlockRef
CurrentL1 eth.L1BlockRef
}

func NewFollowClient(client client.RPC, log log.Logger, metrics caching.Metrics, config *L2ClientConfig) (*FollowClient, error) {
l2Client, err := NewL2Client(client, log, metrics, config)
if err != nil {
return nil, fmt.Errorf("failed to create Eth client: %w", err)
}
rollupClient := NewRollupClient(client)
// Check the RPC is from CL
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, err = rollupClient.SyncStatus(ctx)
followCL := err == nil
if followCL {
log.Info("FollowClient: Following CL")
} else {
log.Info("FollowClient: Following EL")
}
return &FollowClient{l2Client: l2Client, rollupClient: rollupClient, followCL: followCL}, nil
}

func (s *FollowClient) GetFollowStatus(ctx context.Context) (*FollowStatus, error) {
if s.followCL {
status, err := s.rollupClient.SyncStatus(ctx)
if err != nil {
return nil, fmt.Errorf("Failed to fetch external syncStatus", "err", err)
}
return &FollowStatus{
FinalizedL2: status.FinalizedL2,
SafeL2: status.SafeL2,
CurrentL1: status.CurrentL1,
}, nil
}
eFinalized, err := s.l2Client.L2BlockRefByLabel(ctx, eth.Finalized)
if err != nil {
return nil, fmt.Errorf("Failed to fetch external finalizedRef", "err", err)
}
eSafe, err := s.l2Client.L2BlockRefByLabel(ctx, eth.Safe)
if err != nil {
return nil, fmt.Errorf("Failed to fetch external safeRef", "err", err)
}
return &FollowStatus{FinalizedL2: eFinalized, SafeL2: eSafe}, nil
}