Skip to content
Closed
Show file tree
Hide file tree
Changes from 33 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
1 change: 1 addition & 0 deletions op-acceptance-tests/tests/lite_mode/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.test
127 changes: 127 additions & 0 deletions op-acceptance-tests/tests/lite_mode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Lite Mode Acceptance Tests

This directory contains acceptance tests for the lite mode feature in op-node.

## What is Lite Mode?

Lite mode is a new sync mode where the op-node sources safe/finalized heads from a remote RPC endpoint instead of deriving them from L1. It's designed for resource-constrained nodes that want to sync quickly without performing L1 derivation.

### Key Characteristics

- **Disables L1 derivation pipeline** for safe head progression
- **Polls a remote RPC endpoint** for safe/finalized blocks
- **Imports blocks from remote** and promotes them locally
- **CL sync (P2P gossip)** still handles unsafe blocks
- **Lower resource requirements** compared to full derivation

## Test Structure

### Test Files

- `init_test.go` - Test setup and configuration
- `lite_mode_test.go` - Test cases for lite mode functionality

### Test Cases

1. **TestLiteModeBasicSync** - Verifies basic safe head synchronization
- Tests that lite mode verifier syncs safe heads from sequencer
- Ensures safe head progression matches the sequencer

2. **TestLiteModeFinalizedSync** - Verifies finalized head synchronization
- Tests that lite mode verifier syncs finalized heads from sequencer
- Ensures finalized head progression matches the sequencer

3. **TestLiteModeUnsafeViaP2P** - Verifies P2P gossip still works
- Tests that unsafe blocks are still received via P2P
- Ensures CL sync remains functional in lite mode

4. **TestLiteModeContinuousSync** - Verifies continuous operation
- Tests that lite mode continues to sync over multiple rounds
- Ensures the verifier stays in sync with the sequencer

## Implementation Details

### Configuration

The lite mode tests use a custom system preset that:

1. Creates a standard single-chain multi-node setup (sequencer + verifier)
2. Configures the verifier to run in lite mode
3. Sets the verifier's remote RPC to the sequencer's RPC endpoint
4. Maintains P2P connections for unsafe block sync

### Code Organization

#### Preset Layer (`op-devstack/presets/`)

- **`lite_mode.go`** - Defines the `LiteMode` preset and `NewLiteMode()` constructor
- **`cl_config.go`** - Contains `WithLiteMode()` option for configuring lite mode

#### System Layer (`op-devstack/sysgo/`)

- **`system_lite_mode.go`** - Defines `LiteModeSystem()` that creates the test infrastructure
- **`l2_cl.go`** - Extended `L2CLConfig` with `LiteModeEnabled` and `LiteModeRemoteRPC` fields
- **`l2_cl_opnode.go`** - Modified to use lite mode config from `L2CLConfig`

### How It Works

1. **System Creation**: `LiteModeSystem()` creates a minimal system with sequencer
2. **Dynamic Configuration**: After sequencer is created, retrieves its RPC URL
3. **Verifier Setup**: Creates verifier CL node with lite mode enabled, pointing to sequencer RPC
4. **P2P Setup**: Connects nodes via P2P for unsafe block gossip
5. **Test Execution**: Tests verify sync behavior across different safety levels

## Running the Tests

```bash
# Run all lite mode tests
go test ./op-acceptance-tests/tests/lite_mode/...

# Run a specific test
go test ./op-acceptance-tests/tests/lite_mode/ -run TestLiteModeBasicSync

# Run with verbose output
go test ./op-acceptance-tests/tests/lite_mode/... -v
```

## Design Decisions

### Why AfterDeploy Hook?

The system uses `stack.AfterDeploy()` to configure the lite mode verifier because:
- The sequencer must be created first to get its RPC endpoint
- The verifier needs the sequencer's RPC URL for lite mode configuration
- This ensures proper ordering of node creation and configuration

### Why Keep P2P Connections?

Even in lite mode, P2P connections are maintained because:
- Unsafe blocks are still received via P2P gossip
- This ensures the node can participate in consensus layer sync
- It provides a more complete syncing experience

### Configuration Approach

The implementation supports two configuration methods:
1. **Programmatic**: Via `L2CLConfig` fields (preferred for tests)
2. **Environment Variables**: Via `OP_NODE_ROLLUP_LITE_MODE*` (backward compatible)

This dual approach ensures:
- Clean, testable code with explicit configuration
- Backward compatibility with existing environment-based setups

## Future Enhancements

Potential improvements to these tests:

1. **Reorg Testing** - Verify behavior during chain reorganizations
2. **Connection Failure** - Test recovery when remote RPC is unavailable
3. **Performance Metrics** - Measure sync speed vs. full derivation
4. **Multiple Verifiers** - Test multiple lite mode nodes syncing from same source
5. **Mixed Mode** - Test systems with both lite and full derivation verifiers

## Related Files

- `/root/optimism-2/op-node/rollup/driver/lite_mode.go` - Core lite mode implementation
- `/root/optimism-2/op-node/rollup/driver/driver.go` - Integration with driver
- `/root/optimism-2/op-node/rollup/sync/config.go` - Sync configuration
16 changes: 16 additions & 0 deletions op-acceptance-tests/tests/lite_mode/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lite_mode

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.WithLiteModeSystem(),
presets.WithConsensusLayerSync(),
presets.WithCompatibleTypes(compat.SysGo),
)
}
165 changes: 165 additions & 0 deletions op-acceptance-tests/tests/lite_mode/lite_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package lite_mode

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"
)

// TestLiteModeBasicSync verifies that a lite mode verifier can sync safe and finalized heads
// from the sequencer without running L1 derivation.
func TestLiteModeBasicSync(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewLiteMode(t)
require := t.Require()
logger := t.Logger()

logger.Info("Starting lite mode basic sync test")

// The sequencer should be producing blocks
initialSafeSeq := sys.L2CL.SafeL2BlockRef().Number
logger.Info("Initial sequencer state", "safe", initialSafeSeq)

// Wait for sequencer to advance safe head by at least 5 blocks
targetDelta := uint64(5)
dsl.CheckAll(t,
sys.L2CL.AdvancedFn(types.LocalSafe, targetDelta, 30),
)

newSafeSeq := sys.L2CL.SafeL2BlockRef().Number
logger.Info("Sequencer advanced", "old_safe", initialSafeSeq, "new_safe", newSafeSeq)
require.GreaterOrEqual(newSafeSeq, initialSafeSeq+targetDelta, "sequencer should have advanced safe head")

// The lite mode verifier should sync to match the sequencer's safe head
// Give it some time to poll and sync
logger.Info("Waiting for lite mode verifier to sync")
sys.L2CLB.Matched(sys.L2CL, types.LocalSafe, 30)

verifierSafe := sys.L2CLB.SafeL2BlockRef()
logger.Info("Lite mode verifier synced", "safe", verifierSafe.Number, "hash", verifierSafe.Hash)

// Verify the safe heads match
require.Equal(newSafeSeq, verifierSafe.Number, "lite mode verifier safe head should match sequencer")
require.Equal(sys.L2CL.SafeL2BlockRef().Hash, verifierSafe.Hash, "lite mode verifier safe hash should match sequencer")
}

// TestLiteModeFinalizedSync verifies that lite mode correctly syncs finalized heads.
func TestLiteModeFinalizedSync(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewLiteMode(t)
require := t.Require()
logger := t.Logger()

logger.Info("Starting lite mode finalized sync test")

// Wait for L1 to produce enough blocks for finalization
// L1 needs head > finalizedDistance (20) for any blocks to be finalized
logger.Info("Waiting for L1 to produce sufficient blocks for finalization...")
for i := 0; i < 30; i++ {
l1Head := sys.L1Network.WaitForBlock()
logger.Info("L1 block produced", "number", l1Head.Number)
if l1Head.Number >= 23 {
logger.Info("L1 has sufficient blocks for finalization")
break
}
}

// Wait for both nodes to advance finalized heads
initialFinSeq := sys.L2CL.HeadBlockRef(types.Finalized).Number
logger.Info("Initial sequencer finalized", "finalized", initialFinSeq)

// Wait for sequencer to advance finalized by at least 3 blocks
targetDelta := uint64(3)
sys.L2CL.Advanced(types.Finalized, targetDelta, 50)

newFinSeq := sys.L2CL.HeadBlockRef(types.Finalized).Number
logger.Info("Sequencer finalized advanced", "old_fin", initialFinSeq, "new_fin", newFinSeq)
require.GreaterOrEqual(newFinSeq, initialFinSeq+targetDelta, "sequencer should have advanced finalized head")

// The lite mode verifier should sync finalized head
logger.Info("Waiting for lite mode verifier to sync finalized")
sys.L2CLB.Matched(sys.L2CL, types.Finalized, 30)

verifierFin := sys.L2CLB.HeadBlockRef(types.Finalized)
logger.Info("Lite mode verifier finalized synced", "finalized", verifierFin.Number, "hash", verifierFin.Hash)

// Verify the finalized heads match
require.Equal(newFinSeq, verifierFin.Number, "lite mode verifier finalized head should match sequencer")
require.Equal(sys.L2CL.HeadBlockRef(types.Finalized).Hash, verifierFin.Hash, "lite mode verifier finalized hash should match sequencer")
}

// TestLiteModeUnsafeViaP2P verifies that lite mode nodes still receive unsafe blocks via P2P gossip.
func TestLiteModeUnsafeViaP2P(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewLiteMode(t)
require := t.Require()
logger := t.Logger()

logger.Info("Starting lite mode unsafe P2P test")

// Verify P2P connection between nodes
sys.L2CLB.IsP2PConnected(sys.L2CL)
logger.Info("Lite mode verifier is P2P connected to sequencer")

// First, wait for the sequencer to produce some blocks and for safe heads to sync
initialSafeSeq := sys.L2CL.SafeL2BlockRef().Number
logger.Info("Initial sequencer safe head", "safe", initialSafeSeq)

// Wait for sequencer to advance safe head by at least 5 blocks
targetDelta := uint64(5)
dsl.CheckAll(t,
sys.L2CL.AdvancedFn(types.LocalSafe, targetDelta, 30),
)

newSafeSeq := sys.L2CL.SafeL2BlockRef().Number
logger.Info("Sequencer advanced safe head", "old_safe", initialSafeSeq, "new_safe", newSafeSeq)

// Wait for verifier to sync safe head via lite mode RPC
logger.Info("Waiting for verifier to sync safe head")
sys.L2CLB.Matched(sys.L2CL, types.LocalSafe, 60)
logger.Info("Verifier safe head synced")

// Now verify that unsafe blocks also sync via P2P (alongside safe head progression)
logger.Info("Waiting for verifier unsafe head to sync via P2P")
sys.L2CLB.Matched(sys.L2CL, types.LocalUnsafe, 60)

verifierUnsafe := sys.L2CLB.HeadBlockRef(types.LocalUnsafe)
seqUnsafe := sys.L2CL.HeadBlockRef(types.LocalUnsafe)
logger.Info("P2P sync complete", "sequencer", seqUnsafe.Number, "verifier", verifierUnsafe.Number)

// Verify unsafe heads match (P2P sync is working)
require.Equal(seqUnsafe.Number, verifierUnsafe.Number, "lite mode verifier unsafe head should match sequencer via P2P")
require.Equal(seqUnsafe.Hash, verifierUnsafe.Hash, "lite mode verifier unsafe hash should match sequencer")
}

// TestLiteModeContinuousSync verifies that lite mode continues to sync as the sequencer progresses.
func TestLiteModeContinuousSync(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewLiteMode(t)
require := t.Require()
logger := t.Logger()

logger.Info("Starting lite mode continuous sync test")

// Perform multiple rounds of sync to verify continuous operation
for i := 0; i < 3; i++ {
logger.Info("Continuous sync round", "round", i+1)

// Wait for sequencer to advance
sys.L2CL.Advanced(types.LocalSafe, 2, 30)

// Verify lite mode verifier keeps up
sys.L2CLB.Matched(sys.L2CL, types.LocalSafe, 30)

seqSafe := sys.L2CL.SafeL2BlockRef()
verSafe := sys.L2CLB.SafeL2BlockRef()
logger.Info("Sync round complete", "round", i+1, "seq_safe", seqSafe.Number, "ver_safe", verSafe.Number)

require.Equal(seqSafe.Hash, verSafe.Hash, "lite mode verifier should stay synced with sequencer")
}

logger.Info("Continuous sync test completed successfully")
}
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}
Loading