Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ca2f695
docs: Add implementation plan for --safe-source=l2 feature
Oct 8, 2025
0f44862
feat(op-node): Add --safe-source=l2 config and flags (Phase 1)
Oct 8, 2025
a636c00
feat(op-node): Implement safe-source=l2 mode (Phases 2-4)
Oct 8, 2025
9f6fd1a
feat(op-node): Add payload fetching and reorg logic for safe-source=l2
Oct 8, 2025
a6ada8d
test(op-node): Add unit tests for safe-source=l2 mode
Oct 8, 2025
ee1789f
fix: address PR review comments
Oct 8, 2025
47d6dcd
fix: add ResetStepBackoff back to safe-source=l2 block
Oct 8, 2025
6eacf92
style: match EL sync comment style for safe-source=l2 block
Oct 8, 2025
b370957
test: remove config_test.go for SafeSource
Oct 8, 2025
f7731d8
test: add safe-source=l2 devstack preset and acceptance test
Oct 8, 2025
fbfa1ec
test: add sync_tester test for safe-source=l2
Oct 8, 2025
86c3c9c
test: add init_test.go for safe_source_l2
Oct 8, 2025
5df9133
fix(op-node): ensure safe-source=l2 runs periodically and handles mis…
Oct 8, 2025
a4a4f97
refactor: move safe-source=l2 logic to dedicated ticker
Oct 8, 2025
e0daad4
docs: remove implementation planning document
Oct 8, 2025
5022f92
fix: skip safe-source=l2 updates when EL is syncing
Oct 8, 2025
e2c9d92
refactor: rename safesource to safesourcel2 for consistency
Oct 8, 2025
34604e6
fix: address PR review comments
Oct 8, 2025
3221aa4
fix: fetch finalized before safe to maintain invariant
Oct 8, 2025
a8d157e
refactor: simplify SafeSource type and extract ticker logic
Oct 8, 2025
4755c5c
refactor: fetch unsafe head and rename payload insertion function
Oct 8, 2025
ec5f81c
refactor(safe-source-l2): improve reorg handling and simplify error h…
Oct 8, 2025
e13f62e
refactor(safe-source-l2): simplify syncSafeHeadFromL2 to rely on EL sync
Oct 10, 2025
da1b08a
fix(safe-source-l2): remove startup engine reset to prevent FCU to ge…
Oct 30, 2025
9541f7f
Fix unit test?
pcw109550 Nov 6, 2025
633779a
Rm unused method and unit tests
pcw109550 Nov 6, 2025
8be9545
Deflake
pcw109550 Nov 6, 2025
8590515
linter happy
pcw109550 Nov 6, 2025
5342a5d
safe source with sequencer
pcw109550 Nov 6, 2025
fbecf30
blast every logs
pcw109550 Nov 6, 2025
dc42477
disable derivation + seq works?
pcw109550 Nov 7, 2025
2ffc9bd
Do not FCU to genesis
pcw109550 Nov 11, 2025
f958ffd
revert back
pcw109550 Nov 11, 2025
8a9f55e
Fix seq + light node?
pcw109550 Nov 11, 2025
b73e69e
edge case
pcw109550 Nov 11, 2025
89c60af
try another approach
pcw109550 Nov 11, 2025
6811966
linter
pcw109550 Nov 11, 2025
bda342f
logs
pcw109550 Nov 11, 2025
6b18dee
make RR Sync work + allow initial Sync using syncmode=execution-layer
pcw109550 Nov 12, 2025
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: 17 additions & 0 deletions op-acceptance-tests/tests/safe_source_l2/seq/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package safe_source_l2

import (
"testing"

"github.com/ethereum-optimism/optimism/op-devstack/compat"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
)

func TestMain(m *testing.M) {
presets.DoMain(m,
presets.WithSingleChainMultiNodeWithSafeSourceL2(true /* seqWithSafeSourceL2 */),
presets.WithCompatibleTypes(compat.SysGo),
// test should work w/wo WithExecutionLayerSyncOnVerifiers.
// presets.WithExecutionLayerSyncOnVerifiers(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package safe_source_l2

import (
"testing"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
)

// TestSafeSourceL2BasicSync verifies that a verifier node configured with --safe-source=l2
// can follow the safe head of another verifier without performing derivation.
func TestSafeSourceL2BasicSync(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewSingleChainMultiNodeWithSafeSourceL2(t)

// Advance the normal verifier's safe head
dsl.CheckAll(t,
sys.L2CL.AdvancedFn(types.LocalSafe, 5, 30),
)

// Verify the safe-source=l2 verifier matches the normal verifier
sys.L2CLB.Matched(sys.L2CL, types.LocalSafe, 30)

// Get sync status from both nodes
l2CLStatus := sys.L2CL.SyncStatus()
l2CLBStatus := sys.L2CLB.SyncStatus()

require := t.Require()
require.Equal(l2CLStatus.SafeL2.Hash, l2CLBStatus.SafeL2.Hash, "Safe heads should match")
require.Equal(l2CLStatus.SafeL2.Number, l2CLBStatus.SafeL2.Number, "Safe block numbers should match")
require.Equal(l2CLStatus.FinalizedL2.Hash, l2CLBStatus.FinalizedL2.Hash, "Finalized heads should match")
require.Equal(l2CLStatus.FinalizedL2.Number, l2CLBStatus.FinalizedL2.Number, "Finalized block numbers should match")

// Advance further to ensure continued sync
sys.L2CL.Advanced(types.LocalSafe, 5, 30)
sys.L2CLB.Matched(sys.L2CL, types.LocalSafe, 30)
}
17 changes: 17 additions & 0 deletions op-acceptance-tests/tests/safe_source_l2/ver/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package safe_source_l2

import (
"testing"

"github.com/ethereum-optimism/optimism/op-devstack/compat"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
)

func TestMain(m *testing.M) {
presets.DoMain(m,
presets.WithSingleChainMultiNodeWithSafeSourceL2(false /* seqWithSafeSourceL2 */),
presets.WithCompatibleTypes(compat.SysGo),
// test should work w/wo WithExecutionLayerSyncOnVerifiers.
// presets.WithExecutionLayerSyncOnVerifiers(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package safe_source_l2

import (
"testing"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
)

// TestSafeSourceL2BasicSync verifies that a verifier node configured with --safe-source=l2
// can follow the safe head of another verifier without performing derivation.
func TestSafeSourceL2BasicSync(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewSingleChainMultiNodeWithSafeSourceL2(t)

// Advance the normal verifier's safe head
dsl.CheckAll(t,
sys.L2CL.AdvancedFn(types.LocalSafe, 5, 30),
)

// Verify the safe-source=l2 verifier matches the normal verifier
sys.L2CLB.Matched(sys.L2CL, types.LocalSafe, 30)

// Get sync status from both nodes
l2CLStatus := sys.L2CL.SyncStatus()
l2CLBStatus := sys.L2CLB.SyncStatus()

require := t.Require()
require.Equal(l2CLStatus.SafeL2.Hash, l2CLBStatus.SafeL2.Hash, "Safe heads should match")
require.Equal(l2CLStatus.SafeL2.Number, l2CLBStatus.SafeL2.Number, "Safe block numbers should match")
require.Equal(l2CLStatus.FinalizedL2.Hash, l2CLBStatus.FinalizedL2.Hash, "Finalized heads should match")
require.Equal(l2CLStatus.FinalizedL2.Number, l2CLBStatus.FinalizedL2.Number, "Finalized block numbers should match")

// Advance further to ensure continued sync
sys.L2CL.Advanced(types.LocalSafe, 5, 30)
sys.L2CLB.Matched(sys.L2CL, types.LocalSafe, 30)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package sync_tester_safesourcel2

import (
"testing"

"github.com/ethereum-optimism/optimism/op-devstack/compat"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
)

func TestMain(m *testing.M) {
presets.DoMain(m,
presets.WithELSyncActive(),
presets.WithSimpleWithSyncTesterSafeSourceL2(),
presets.WithExecutionLayerSyncOnVerifiers(),
presets.WithCompatibleTypes(compat.SysGo),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package sync_tester_safesourcel2

import (
"testing"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
)

func TestSyncTesterSafeSourceL2(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewSimpleWithSyncTesterSafeSourceL2(t)
require := t.Require()
logger := t.Logger()
ctx := t.Ctx()

target := uint64(5)
dsl.CheckAll(t,
sys.L2CL.AdvancedFn(types.LocalUnsafe, target, 30),
sys.L2CL2.AdvancedFn(types.LocalUnsafe, target, 30),
)

// Stop L2CL2 which is using safe-source=l2
sys.L2CL2.Stop()

// Reset Sync Tester EL
sessionIDs := sys.SyncTester.ListSessions()
require.GreaterOrEqual(len(sessionIDs), 1, "at least one session")
sessionID := sessionIDs[0]
logger.Info("SyncTester EL", "sessionID", sessionID)
syncTesterClient := sys.SyncTester.Escape().APIWithSession(sessionID)
require.NoError(syncTesterClient.ResetSession(ctx))

// Wait for L2CL to advance more unsafe and safe blocks
sys.L2CL.Advanced(types.LocalUnsafe, target+5, 30)
sys.L2CL.Advanced(types.LocalSafe, target+3, 30)

// Restarting will allow L2CL2 to query safe head from L2CL via safe-source=l2
sys.L2CL2.Start()

// Wait until P2P is connected for unsafe head gossip
sys.L2CL2.IsP2PConnected(sys.L2CL)

// L2CL2 should catch up via safe-source=l2
target = uint64(20)
sys.L2CL.Reached(types.LocalSafe, target, 30)
sys.L2CL2.Reached(types.LocalSafe, target, 30)

sys.L2CL.Matched(sys.L2CL2, types.LocalSafe, 30)

logger.Info("SyncTester SafeSourceL2 test completed successfully")

logger.Info("### Safe ", "ver", sys.L2CL2.SafeL2BlockRef(), "seq", sys.L2CL.SafeL2BlockRef())
logger.Info("### Unsafe", "ver", sys.L2CL2.UnsafeHead(), "seq", sys.L2CL.UnsafeHead())
// Safe matches but unsafe gap still happen

sys.L2CL.Matched(sys.L2CL2, types.LocalUnsafe, 100)

logger.Info("### Safe ", "ver", sys.L2CL2.SafeL2BlockRef(), "seq", sys.L2CL.SafeL2BlockRef())
logger.Info("### Unsafe", "ver", sys.L2CL2.UnsafeHead(), "seq", sys.L2CL.UnsafeHead())
logger.Info("### Finalzed", "ver", sys.L2CL2.SyncStatus().FinalizedL2, "seq", sys.L2CL.SyncStatus().FinalizedL2)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EL never advanced since the underlying sync tester EL did not enable EL Syncing. Fixed

}
2 changes: 1 addition & 1 deletion op-devstack/presets/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func DoMain(m TestingM, opts ...stack.CommonOption) {
cfg := flags.ReadTestConfig()
logHandler := oplog.NewLogHandler(os.Stdout, cfg.LogConfig)
logHandler = logfilter.WrapFilterHandler(logHandler)
logHandler.(logfilter.FilterHandler).Set(logfilter.DefaultMute(logfilter.Level(log.LevelInfo).Show()))
// logHandler.(logfilter.FilterHandler).Set(logfilter.DefaultMute(logfilter.Level(log.LevelInfo).Show()))
logHandler = logfilter.WrapContextHandler(logHandler)
// The default can be changed using the WithLogFilters option which replaces this default
logger := log.NewLogger(logHandler)
Expand Down
43 changes: 43 additions & 0 deletions op-devstack/presets/simple_with_synctester_safesourcel2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package presets

import (
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
"github.com/ethereum-optimism/optimism/op-devstack/shim"
"github.com/ethereum-optimism/optimism/op-devstack/stack"
"github.com/ethereum-optimism/optimism/op-devstack/stack/match"
"github.com/ethereum-optimism/optimism/op-devstack/sysgo"
)

type SimpleWithSyncTesterSafeSourceL2 struct {
Minimal

SyncTester *dsl.SyncTester
SyncTesterL2EL *dsl.L2ELNode
L2CL2 *dsl.L2CLNode
}

func WithSimpleWithSyncTesterSafeSourceL2() stack.CommonOption {
return stack.MakeCommon(sysgo.DefaultSimpleSystemWithSyncTesterSafeSourceL2(&sysgo.DefaultSimpleSystemWithSyncTesterSafeSourceL2IDs{}))
}

func NewSimpleWithSyncTesterSafeSourceL2(t devtest.T) *SimpleWithSyncTesterSafeSourceL2 {
system := shim.NewSystem(t)
orch := Orchestrator()
orch.Hydrate(system)
minimal := minimalFromSystem(t, system, orch)
l2 := system.L2Network(match.L2ChainA)
syncTester := l2.SyncTester(match.FirstSyncTester)

// L2CL2 connected to L2EL initialized by sync tester, with safe-source=l2
l2CL2 := l2.L2CLNode(match.SecondL2CL)
// L2EL initialized by sync tester
syncTesterL2EL := l2.L2ELNode(match.SecondL2EL)

return &SimpleWithSyncTesterSafeSourceL2{
Minimal: *minimal,
SyncTester: dsl.NewSyncTester(syncTester),
SyncTesterL2EL: dsl.NewL2ELNode(syncTesterL2EL, orch.ControlPlane()),
L2CL2: dsl.NewL2CLNode(l2CL2, orch.ControlPlane()),
}
}
55 changes: 55 additions & 0 deletions op-devstack/presets/singlechain_multinode_safesourcel2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package presets

import (
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
"github.com/ethereum-optimism/optimism/op-devstack/shim"
"github.com/ethereum-optimism/optimism/op-devstack/stack"
"github.com/ethereum-optimism/optimism/op-devstack/stack/match"
"github.com/ethereum-optimism/optimism/op-devstack/sysgo"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
)

type SingleChainMultiNodeWithSafeSourceL2 struct {
Minimal

L2ELB *dsl.L2ELNode
L2CLB *dsl.L2CLNode
}

func WithSingleChainMultiNodeWithSafeSourceL2(seqWithSafeSourceL2 bool) stack.CommonOption {
return stack.MakeCommon(sysgo.DefaultSingleChainMultiNodeWithSafeSourceL2System(&sysgo.DefaultSingleChainMultiNodeWithSafeSourceL2SystemIDs{}, seqWithSafeSourceL2))
}

func NewSingleChainMultiNodeWithSafeSourceL2(t devtest.T) *SingleChainMultiNodeWithSafeSourceL2 {
preset := NewSingleChainMultiNodeWithSafeSourceL2WithoutCheck(t)
// Ensure the L2 sourced node is in sync with the sequencer before starting tests
dsl.CheckAll(t,
preset.L2CLB.MatchedFn(preset.L2CL, types.CrossSafe, 30),
preset.L2CLB.MatchedFn(preset.L2CL, types.LocalUnsafe, 30),
)
return preset
}

func NewSingleChainMultiNodeWithSafeSourceL2WithoutCheck(t devtest.T) *SingleChainMultiNodeWithSafeSourceL2 {
system := shim.NewSystem(t)
orch := Orchestrator()
orch.Hydrate(system)
minimal := minimalFromSystem(t, system, orch)
l2 := system.L2Network(match.Assume(t, match.L2ChainA))
verifierCL := l2.L2CLNode(match.Assume(t,
match.And(
match.Not(match.WithSequencerActive(t.Ctx())),
match.Not[stack.L2CLNodeID, stack.L2CLNode](minimal.L2CL.ID()),
)))
verifierEL := l2.L2ELNode(match.Assume(t,
match.And(
match.EngineFor(verifierCL),
match.Not[stack.L2ELNodeID, stack.L2ELNode](minimal.L2EL.ID()))))
preset := &SingleChainMultiNodeWithSafeSourceL2{
Minimal: *minimal,
L2ELB: dsl.NewL2ELNode(verifierEL, orch.ControlPlane()),
L2CLB: dsl.NewL2CLNode(verifierCL, orch.ControlPlane()),
}
return preset
}
7 changes: 7 additions & 0 deletions op-devstack/sysgo/l2_cl.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ type L2CLConfig struct {

// NoDiscovery is the flag to enable/disable discovery
NoDiscovery bool

// SafeSource determines where the safe head comes from (L1 derivation or L2 RPC)
SafeSource nodeSync.SafeSource
// SafeSourceL2RPC is the RPC endpoint for L2 safe source (when SafeSource is L2)
SafeSourceL2RPC string
}

func L2CLSequencer() L2CLOption {
Expand All @@ -56,6 +61,8 @@ func DefaultL2CLConfig() *L2CLConfig {
IndexingMode: false,
EnableReqRespSync: true,
NoDiscovery: false,
SafeSource: nodeSync.SafeSourceL1,
SafeSourceL2RPC: "",
}
}

Expand Down
2 changes: 2 additions & 0 deletions op-devstack/sysgo/l2_cl_opnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L
SyncMode: syncMode,
SkipSyncStartCheck: false,
SupportsPostFinalizationELSync: false,
SafeSource: cfg.SafeSource,
SafeSourceL2RPC: cfg.SafeSourceL2RPC,
},
ConfigPersistence: config.DisabledConfigPersistence{},
Metrics: opmetrics.CLIConfig{},
Expand Down
48 changes: 48 additions & 0 deletions op-devstack/sysgo/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,54 @@ func defaultMinimalSystemOpts(ids *DefaultMinimalSystemIDs, dest *DefaultMinimal
return opt
}

func DefaultMinimalSystemNoSeq(dest *DefaultMinimalSystemIDs) stack.Option[*Orchestrator] {
ids := NewDefaultMinimalSystemIDs(DefaultL1ID, DefaultL2AID)
ids.L2CL = stack.NewL2CLNodeID("verifier", DefaultL2AID)
ids.L2EL = stack.NewL2ELNodeID("verifier", DefaultL2AID)
return defaultMinimalSystemNoSeqOpts(&ids, dest)
}

func defaultMinimalSystemNoSeqOpts(ids *DefaultMinimalSystemIDs, dest *DefaultMinimalSystemIDs) stack.CombinedOption[*Orchestrator] {
opt := stack.Combine[*Orchestrator]()
opt.Add(stack.BeforeDeploy(func(o *Orchestrator) {
o.P().Logger().Info("Setting up")
}))

opt.Add(WithMnemonicKeys(devkeys.TestMnemonic))

opt.Add(WithDeployer(),
WithDeployerOptions(
WithLocalContractSources(),
WithCommons(ids.L1.ChainID()),
WithPrefundedL2(ids.L1.ChainID(), ids.L2.ChainID()),
),
)

opt.Add(WithL1Nodes(ids.L1EL, ids.L1CL))

opt.Add(WithL2ELNode(ids.L2EL))
opt.Add(WithL2CLNode(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL))

opt.Add(WithBatcher(ids.L2Batcher, ids.L1EL, ids.L2CL, ids.L2EL))
opt.Add(WithProposer(ids.L2Proposer, ids.L1EL, &ids.L2CL, nil))

opt.Add(WithFaucets([]stack.L1ELNodeID{ids.L1EL}, []stack.L2ELNodeID{ids.L2EL}))

opt.Add(WithTestSequencer(ids.TestSequencer, ids.L1CL, ids.L2CL, ids.L1EL, ids.L2EL))

opt.Add(WithL2Challenger(ids.L2Challenger, ids.L1EL, ids.L1CL, nil, nil, &ids.L2CL, []stack.L2ELNodeID{
ids.L2EL,
}))

opt.Add(WithL2MetricsDashboard())

opt.Add(stack.Finally(func(orch *Orchestrator) {
*dest = *ids
}))

return opt
}

// DefaultTwoL2System defines a minimal system with a single L1 and two L2 chains,
// without interop or supervisor: both L2s get their own ELs, and we attach L2CL nodes
// via the default L2CL selector (which can be set to supernode to share a single process).
Expand Down
Loading