Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d340a84
docs: add lite mode design document
karlfloersch Sep 30, 2025
dd13439
test: add sync tester test runner script
Oct 1, 2025
65895da
docs: add comprehensive testing section to lite mode design
Oct 1, 2025
a003344
feat(op-node): add lite mode configuration and CLI flags
Oct 1, 2025
93e4375
feat(op-node): disable derivation in lite mode
Oct 1, 2025
ef2604e
feat(op-node): add LiteModeSync component
Oct 1, 2025
b177d39
feat(op-node): integrate LiteModeSync into Driver
Oct 1, 2025
aa52865
fix(op-node): fix compilation errors in lite mode integration
Oct 1, 2025
6ab4d94
docs(lite-mode): update environment variable names in test documentation
Oct 1, 2025
e1ac4b2
feat(devstack): add lite mode support to test infrastructure
Oct 1, 2025
aa9c015
fix(devstack): add missing os import for lite mode support
Oct 1, 2025
d0b113a
fix(lite-mode): handle missing eth_syncing method gracefully
Oct 1, 2025
513f8c2
fix(lite-mode): remove eth_syncing check and fix close panic
Oct 1, 2025
1857f2c
feat(lite-mode): fix unsafe head initialization and complete implemen…
Oct 1, 2025
dfd2e98
refactor(lite-mode): use FindL2Heads for initialization instead of ma…
Oct 1, 2025
4a00e95
feat(lite-mode): add acceptance tests and fix sync issues
Oct 1, 2025
f9210f2
refactor(lite-mode): move derivation disable to SyncStep for clarity
Oct 1, 2025
250276a
refactor(lite-mode): swap order and move catch-up logic to updateFina…
Oct 1, 2025
4554e4d
refactor(lite-mode): simplify safe head sync with backward-walking al…
Oct 1, 2025
96138c3
refactor(lite-mode): address review feedback
Oct 1, 2025
19eb5ac
feat(lite-mode): optimize block verification with header-only fetches
Oct 1, 2025
37a66f1
fix(lite-mode): use event system instead of direct engine calls
Oct 1, 2025
9a8d214
refactor(test): separate lite mode into dedicated test function
Oct 1, 2025
8a3dd82
refactor(lite-mode): extract block hash lookup into helper function
Oct 2, 2025
466b351
refactor(lite-mode): remove redundant warning when block lookup fails
Oct 2, 2025
d4b3556
fix(lite-mode): return error when remote safe is behind local finalized
Oct 2, 2025
cfaf487
refactor(lite-mode): integrate sync into SyncStep instead of separate…
Oct 2, 2025
d53ffc8
fix(lite-mode): use direct engine method calls instead of events and …
Oct 2, 2025
2bf330e
fix(lite-mode): wait for L1 finalization before checking L2 finalized
Oct 2, 2025
064a4c2
perf(lite-mode): prevent CPU starvation of P2P unsafe block processing
Oct 2, 2025
25f08ce
fix(lite-mode): disable challenger in lite mode tests to prevent flakes
Oct 2, 2025
311da5f
fix(lite-mode): prevent duplicate block insertions causing reorgs
Oct 2, 2025
fb9bb40
chore(lite-mode): remove polling logic and unused documentation
Oct 3, 2025
bc73f3a
chore: remove .gitignore from lite_mode tests
Oct 3, 2025
913bbc6
refactor: rename lite mode to tip mode throughout codebase
Oct 3, 2025
ea139b2
fix: correct tip mode environment variable naming
Oct 3, 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
493 changes: 493 additions & 0 deletions LITE_MODE.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/bin/bash
# Script to run the sync tester external EL test
# This test validates op-node syncing against external execution layer endpoints

set -e

# Configuration
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="${TEST_DIR}/test_run_$(date +%Y%m%d_%H%M%S).log"
LITE_MODE_RPC="${OP_NODE_LITE_MODE_RPC:-}"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo "=========================================="
echo "OP Stack Sync Tester - External EL"
echo "=========================================="
echo "Test Directory: ${TEST_DIR}"
echo "Log File: ${LOG_FILE}"
if [ -n "${LITE_MODE_RPC}" ]; then
echo -e "${YELLOW}Lite Mode: ENABLED${NC}"
echo "Lite Mode RPC: ${LITE_MODE_RPC}"
else
echo "Lite Mode: DISABLED (standard derivation)"
fi
echo "=========================================="
echo ""

cd "${TEST_DIR}"

# Run the test
echo "Starting test..."
CIRCLECI_PARAMETERS_SYNC_TEST_OP_NODE_DISPATCH=true \
TAILSCALE_NETWORKING=true \
NETWORK_PRESET=op-sepolia \
GOMAXPROCS=5 \
OP_NODE_LITE_MODE_RPC="${LITE_MODE_RPC}" \
go test -run '^TestSyncTesterExtEL$' -v -count=1 2>&1 | tee "${LOG_FILE}"

# Check exit code
EXIT_CODE=${PIPESTATUS[0]}

echo ""
echo "=========================================="
if [ ${EXIT_CODE} -eq 0 ]; then
echo -e "${GREEN}TEST PASSED ✓${NC}"
else
echo -e "${RED}TEST FAILED ✗${NC}"
fi
echo "Exit Code: ${EXIT_CODE}"
echo "Log File: ${LOG_FILE}"
echo "=========================================="

exit ${EXIT_CODE}
22 changes: 20 additions & 2 deletions op-devstack/sysgo/l2_cl_opnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/hex"
"flag"
"fmt"
"os"
"sync"
"time"

Expand Down Expand Up @@ -241,6 +242,19 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L
// Get the L2 engine address from the EL node (which can be a regular EL node or a SyncTesterEL)
l2EngineAddr := l2EL.EngineRPC()

// Check for lite mode environment variables
liteModeEnabled := false
liteModeRPC := ""
liteModePollInterval := time.Second
if os.Getenv("OP_NODE_ROLLUP_LITE_MODE") == "true" {
liteModeEnabled = true
liteModeRPC = os.Getenv("OP_NODE_ROLLUP_LITE_MODE_RPC")
if liteModeRPC == "" {
p.Require().FailNow("OP_NODE_ROLLUP_LITE_MODE enabled but OP_NODE_ROLLUP_LITE_MODE_RPC not set")
}
logger.Info("Lite mode enabled for op-node", "remote_rpc", liteModeRPC, "poll_interval", liteModePollInterval)
}

nodeCfg := &config.Config{
L1: &config.L1EndpointConfig{
L1NodeAddr: l1EL.userRPC,
Expand All @@ -260,8 +274,11 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L
BeaconAddr: l1CL.beaconHTTPAddr,
},
Driver: driver.Config{
SequencerEnabled: cfg.IsSequencer,
SequencerConfDepth: 2,
SequencerEnabled: cfg.IsSequencer,
SequencerConfDepth: 2,
LiteModeEnabled: liteModeEnabled,
LiteModeRPC: liteModeRPC,
LiteModePollInterval: liteModePollInterval,
},
Rollup: *l2Net.rollupCfg,
DependencySet: depSet,
Expand All @@ -282,6 +299,7 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L
SyncMode: syncMode,
SkipSyncStartCheck: false,
SupportsPostFinalizationELSync: false,
LiteModeEnabled: liteModeEnabled,
},
ConfigPersistence: config.DisabledConfigPersistence{},
Metrics: opmetrics.CLIConfig{},
Expand Down
24 changes: 24 additions & 0 deletions op-node/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,27 @@ var (
EnvVars: prefixEnvVars("ROLLUP_LOAD_PROTOCOL_VERSIONS"),
Category: RollupCategory,
}
/* Lite Mode Flags */
LiteModeEnabled = &cli.BoolFlag{
Name: "rollup.lite-mode",
Usage: "Enable lite mode: disables L1 derivation and sources safe/finalized heads from external RPC",
EnvVars: prefixEnvVars("ROLLUP_LITE_MODE"),
Value: false,
Category: RollupCategory,
}
LiteModeRPC = &cli.StringFlag{
Name: "rollup.lite-mode-rpc",
Usage: "External L2 RPC endpoint to query for safe/finalized heads in lite mode (required if lite-mode is enabled)",
EnvVars: prefixEnvVars("ROLLUP_LITE_MODE_RPC"),
Category: RollupCategory,
}
LiteModePollInterval = &cli.DurationFlag{
Name: "rollup.lite-mode-poll-interval",
Usage: "Polling interval for querying safe/finalized heads from external RPC in lite mode",
EnvVars: prefixEnvVars("ROLLUP_LITE_MODE_POLL_INTERVAL"),
Value: time.Second * 1,
Category: RollupCategory,
}
SafeDBPath = &cli.StringFlag{
Name: "safedb.path",
Usage: "File path used to persist safe head update data. Disabled if not set.",
Expand Down Expand Up @@ -452,6 +473,9 @@ var optionalFlags = []cli.Flag{
HeartbeatURLFlag,
RollupHalt,
RollupLoadProtocolVersions,
LiteModeEnabled,
LiteModeRPC,
LiteModePollInterval,
ConductorEnabledFlag,
ConductorRpcFlag,
ConductorRpcTimeoutFlag,
Expand Down
15 changes: 15 additions & 0 deletions op-node/rollup/driver/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package driver

import "time"

type Config struct {
// VerifierConfDepth is the distance to keep from the L1 head when reading L1 data for L2 derivation.
VerifierConfDepth uint64 `json:"verifier_conf_depth"`
Expand All @@ -24,4 +26,17 @@ type Config struct {
// RecoverMode forces the sequencer to select the next L1 Origin exactly, and create an empty block,
// to be compatible with verifiers forcefully generating the same block while catching up the sequencing window timeout.
RecoverMode bool `json:"recover_mode"`

// LiteModeEnabled disables L1 derivation and sources safe/finalized heads from an external RPC.
// When enabled, the node will not perform derivation and will instead poll the remote RPC
// for safe and finalized block heads.
LiteModeEnabled bool `json:"lite_mode_enabled"`

// LiteModeRPC is the remote execution client RPC endpoint to query for safe/finalized heads.
// Only used when LiteModeEnabled is true.
LiteModeRPC string `json:"lite_mode_rpc"`

// LiteModePollInterval is the interval at which to poll the remote RPC for safe/finalized head updates.
// Defaults to 1 second if not specified.
LiteModePollInterval time.Duration `json:"lite_mode_poll_interval"`
}
59 changes: 59 additions & 0 deletions op-node/rollup/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup/sequencing"
"github.com/ethereum-optimism/optimism/op-node/rollup/status"
"github.com/ethereum-optimism/optimism/op-node/rollup/sync"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/event"
"github.com/ethereum-optimism/optimism/op-service/sources"
)

// NewDriver composes an events handler that tracks L1 state, triggers L2 Derivation, and optionally sequences new L2 blocks.
Expand Down Expand Up @@ -132,6 +134,42 @@ func NewDriver(
sequencer = sequencing.DisabledSequencer{}
}

// Initialize lite mode sync if enabled
var liteModeSync *LiteModeSync
if syncCfg.LiteModeEnabled {
if driverCfg.LiteModeRPC == "" {
log.Crit("Lite mode enabled but no remote RPC endpoint configured")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Should we panic? How are other critical misconfigurations handled?

}

log.Info("Initializing lite mode sync",
"remote_rpc", driverCfg.LiteModeRPC,
"poll_interval", driverCfg.LiteModePollInterval)

// Create remote L2 client (no JWT needed for external RPCs)
remoteRPC, err := client.NewRPC(driverCtx, log, driverCfg.LiteModeRPC)
if err != nil {
log.Crit("Failed to create remote RPC client for lite mode", "err", err)
}

// Create L2 client for the remote endpoint
// Use nil metrics since we don't need caching metrics for remote client
remoteClientCfg := sources.L2ClientDefaultConfig(cfg, true)
remoteL2Client, err := sources.NewL2Client(remoteRPC, log, nil, remoteClientCfg)
if err != nil {
log.Crit("Failed to create remote L2 client for lite mode", "err", err)
}

liteModeSync = NewLiteModeSync(
driverCtx,
log,
cfg,
remoteL2Client, // remoteEL
l2, // localEL
ec, // engine
driverCfg.LiteModePollInterval,
)
}

driverEmitter := sys.Register("driver", nil)
driver := &Driver{
StatusTracker: statusTracker,
Expand All @@ -149,6 +187,7 @@ func NewDriver(
sequencer: sequencer,
metrics: metrics,
altSync: altSync,
liteModeSync: liteModeSync,
}

return driver
Expand Down Expand Up @@ -181,6 +220,9 @@ type Driver struct {

sequencer sequencing.SequencerIface

// Lite mode sync component (nil if not in lite mode)
liteModeSync *LiteModeSync

metrics Metrics
log log.Logger

Expand All @@ -195,6 +237,12 @@ type Driver struct {
func (s *Driver) Start() error {
log.Info("Starting driver", "sequencerEnabled", s.driverConfig.SequencerEnabled,
"sequencerStopped", s.driverConfig.SequencerStopped, "recoverMode", s.driverConfig.RecoverMode)

// Start lite mode sync if enabled
if s.liteModeSync != nil {
s.liteModeSync.Start()
}

if s.driverConfig.SequencerEnabled {
if s.driverConfig.RecoverMode {
log.Warn("sequencer is in recover mode")
Expand All @@ -218,6 +266,10 @@ func (s *Driver) Close() error {
s.driverCancel()
s.wg.Wait()
s.sequencer.Close()
// Close lite mode sync if enabled
if s.liteModeSync != nil {
s.liteModeSync.Close()
}
return nil
}

Expand All @@ -229,6 +281,13 @@ func (s *Driver) eventLoop() {

defer s.driverCancel()

// In lite mode, emit a reset event at startup to properly initialize forkchoice state.
// This triggers FindL2Heads which handles the case where unsafe < finalized.
if s.liteModeSync != nil {
s.log.Info("Lite mode: requesting initial engine reset to establish forkchoice state")
s.emitter.Emit(s.driverCtx, engine.ResetEngineRequestEvent{})
}

@opsuperchain opsuperchain Oct 2, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Is there a more canonical way to do this? Where is this logic handled in the standard derivation process?

// reqStep requests a derivation step nicely, with a delay if this is a reattempt, or not at all if we already scheduled a reattempt.
reqStep := func() {
s.sched.RequestStep(s.driverCtx, false)
Expand Down
Loading