Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions op-acceptance-tests/acceptance-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,17 @@ gates:
tests:
- package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/custom_gas_token
timeout: 10m

- id: supernode
description: "Supernode tests - tests for the op-supernode multi-chain consensus layer."
tests:
- package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/supernode
timeout: 10m

- id: supernode-interop
inherits:
- supernode
description: "Supernode interop tests - tests for supernode's cross-chain message verification."
tests:
- package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/supernode/interop
timeout: 15m
58 changes: 45 additions & 13 deletions op-acceptance-tests/tests/supernode/advance_multiple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,18 @@ import (
"github.com/stretchr/testify/require"
)

// TestCLAdvanceMultiple verifies two L2 chains advance when using a shared CL
// TestTwoChainProgress confirms that two L2 chains advance when using a shared CL
// it confirms:
// - the two L2 chains are on different chains
// - the two L2 chains are different
// - the two CLs are using the same supernode
// - the two CLs are advancing
func TestCLAdvanceMultiple(gt *testing.T) {
// - the two CLs are advancing unsafe and safe heads
func TestTwoChainProgress(gt *testing.T) {
t := devtest.ParallelT(gt)
sys := presets.NewTwoL2(t)

blockTime := sys.L2A.Escape().RollupConfig().BlockTime
waitTime := time.Duration(blockTime+1) * time.Second

// Check L2A advances
numA := sys.L2ACL.SyncStatus().UnsafeL2.Number
numB := sys.L2BCL.SyncStatus().UnsafeL2.Number

// Check that the two CLs are on different chains
require.NotEqual(t, sys.L2ACL.ChainID(), sys.L2BCL.ChainID())

Expand All @@ -36,11 +32,47 @@ func TestCLAdvanceMultiple(gt *testing.T) {
require.NoError(t, err)
require.Equal(t, uA.Scheme, uB.Scheme)
require.Equal(t, uA.Host, uB.Host)
require.Equal(t, uA.Port(), uB.Port())

// Record initial sync status
statusA := sys.L2ACL.SyncStatus()
statusB := sys.L2BCL.SyncStatus()

t.Logger().Info("initial sync status",
"chainA_unsafe", statusA.UnsafeL2.Number,
"chainA_safe", statusA.SafeL2.Number,
"chainB_unsafe", statusB.UnsafeL2.Number,
"chainB_safe", statusB.SafeL2.Number,
)

// unsafe heads should advance
t.Require().Eventually(func() bool {
newStatusA := sys.L2ACL.SyncStatus()
newStatusB := sys.L2BCL.SyncStatus()
return newStatusA.UnsafeL2.Number > statusA.UnsafeL2.Number &&
newStatusB.UnsafeL2.Number > statusB.UnsafeL2.Number
}, 30*time.Second, waitTime, "chains should advance unsafe heads")

// safe heads should advance
t.Require().Eventually(func() bool {
newStatusA := sys.L2ACL.SyncStatus()
newStatusB := sys.L2BCL.SyncStatus()
t.Logger().Info("waiting for safe head progression",
"chainA_safe", newStatusA.SafeL2.Number,
"chainB_safe", newStatusB.SafeL2.Number,
)
return newStatusA.SafeL2.Number > statusA.SafeL2.Number &&
newStatusB.SafeL2.Number > statusB.SafeL2.Number
}, 60*time.Second, waitTime, "chains should advance safe heads")

require.Eventually(t, func() bool {
newA := sys.L2ACL.SyncStatus().UnsafeL2.Number
newB := sys.L2BCL.SyncStatus().UnsafeL2.Number
return newA > numA && newB > numB
}, 30*time.Second, waitTime)
// Log final status
finalStatusA := sys.L2ACL.SyncStatus()
finalStatusB := sys.L2BCL.SyncStatus()
t.Logger().Info("final sync status",
"chainA_unsafe", finalStatusA.UnsafeL2.Number,
"chainA_safe", finalStatusA.SafeL2.Number,
"chainB_unsafe", finalStatusB.UnsafeL2.Number,
"chainB_safe", finalStatusB.SafeL2.Number,
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package activation

import (
"testing"
"time"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-service/eth"
)

// TestSupernodeInteropActivationAfterGenesis tests behavior when interop is activated
// AFTER genesis. This verifies that VerifiedAt (via superroot_atTimestamp) returns
// verified data for timestamps both before and after the activation boundary.
func TestSupernodeInteropActivationAfterGenesis(gt *testing.T) {
t := devtest.ParallelT(gt)
sys := presets.NewTwoL2SupernodeInterop(t, InteropActivationDelay)

genesisTime := sys.GenesisTime
activationTime := sys.InteropActivationTime
blockTime := sys.L2A.Escape().RollupConfig().BlockTime

// Select timestamps before and after activation
// Pre-activation: one block after genesis (before interop is active)
// Post-activation: one block after activation (after interop is active)
preActivationTs := genesisTime + blockTime
postActivationTs := activationTime + blockTime

t.Logger().Info("testing interop activation boundary",
"genesis_time", genesisTime,
"activation_time", activationTime,
"pre_activation_ts", preActivationTs,
"post_activation_ts", postActivationTs,
"block_time", blockTime,
)

ctx := t.Ctx()
snClient := sys.SuperNodeClient()

// Wait for both timestamps to be verified via SuperRootAtTimestamp
// Pre-activation timestamps are auto-verified (interop wasn't active yet)
// Post-activation timestamps require interop verification
var preActivationResp, postActivationResp eth.SuperRootAtTimestampResponse
t.Require().Eventually(func() bool {
var err error

// Check pre-activation timestamp
preActivationResp, err = snClient.SuperRootAtTimestamp(ctx, preActivationTs)
if err != nil {
t.Logger().Debug("superroot_atTimestamp error for pre-activation", "timestamp", preActivationTs, "err", err)
return false
}
preVerified := preActivationResp.Data != nil

// Check post-activation timestamp
postActivationResp, err = snClient.SuperRootAtTimestamp(ctx, postActivationTs)
if err != nil {
t.Logger().Debug("superroot_atTimestamp error for post-activation", "timestamp", postActivationTs, "err", err)
return false
}
postVerified := postActivationResp.Data != nil

t.Logger().Debug("waiting for both timestamps to be verified",
"pre_activation_ts", preActivationTs,
"pre_verified", preVerified,
"post_activation_ts", postActivationTs,
"post_verified", postVerified,
)

return preVerified && postVerified
}, 90*time.Second, time.Second, "both pre and post activation timestamps should be verified")

t.Logger().Info("activation boundary test complete",
"pre_activation_ts", preActivationTs,
"pre_activation_super_root", preActivationResp.Data.SuperRoot,
"post_activation_ts", postActivationTs,
"post_activation_super_root", postActivationResp.Data.SuperRoot,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package activation

import (
"os"
"testing"

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

// InteropActivationDelay is the delay in seconds from genesis to interop activation.
// This is set to 20 seconds to allow several blocks to be produced before interop kicks in.
const InteropActivationDelay = uint64(20)

// TestMain creates a two-L2 setup with a shared supernode that has interop enabled
// AFTER genesis (delayed by InteropActivationDelay seconds).
// This allows testing that safety proceeds normally before interop activation.
func TestMain(m *testing.M) {
// Set the L2CL kind to supernode for all tests in this package
_ = os.Setenv("DEVSTACK_L2CL_KIND", "supernode")
presets.DoMain(m, presets.WithTwoL2SupernodeInterop(InteropActivationDelay))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package interop

import (
"testing"
"time"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-service/eth"
)

// TestSupernodeInteropActivationAtGenesis tests behavior when interop is activated
// at genesis time (timestamp 0 offset). This verifies the first few timestamps are
// processed correctly with interop verification from the very beginning.
// Also verifies that VerifiedAt (via superroot_atTimestamp) works correctly.
func TestSupernodeInteropActivationAtGenesis(gt *testing.T) {
t := devtest.ParallelT(gt)
sys := presets.NewTwoL2SupernodeInterop(t, 0)

genesisTime := sys.L2A.Escape().RollupConfig().Genesis.L2Time
blockTime := sys.L2A.Escape().RollupConfig().BlockTime

t.Logger().Info("testing interop activation at genesis",
"genesis_time", genesisTime,
"block_time", blockTime,
)

// Create a SuperNodeClient to call superroot_atTimestamp (which uses VerifiedAt internally)
ctx := t.Ctx()
snClient := sys.SuperNodeClient()

// The first timestamp to be verified should be genesis + blockTime
// (genesis block doesn't have L1 data recorded in safeDB yet)
targetTimestamp := genesisTime + blockTime
t.Logger().Info("checking VerifiedAt for first block after genesis", "timestamp", targetTimestamp)

// Wait for interop to verify the first block after genesis
var genesisResp eth.SuperRootAtTimestampResponse
t.Require().Eventually(func() bool {
var err error
genesisResp, err = snClient.SuperRootAtTimestamp(ctx, targetTimestamp)
if err != nil {
t.Logger().Warn("superroot_atTimestamp error, retrying", "timestamp", targetTimestamp, "err", err)
return false
}
if genesisResp.Data == nil {
t.Logger().Debug("waiting for interop to verify first block", "timestamp", targetTimestamp)
return false
}
return true
}, 60*time.Second, time.Second, "VerifiedAt should return data for first block after genesis (interop-verified)")

t.Logger().Info("genesis activation verified",
"timestamp", targetTimestamp,
"verified_required_l1", genesisResp.Data.VerifiedRequiredL1,
"super_root", genesisResp.Data.SuperRoot,
)
}
101 changes: 101 additions & 0 deletions op-acceptance-tests/tests/supernode/interop/cross_message_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package interop

import (
"math/rand"
"testing"
"time"

"github.com/ethereum/go-ethereum/common"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-service/bigs"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testutils"
"github.com/ethereum-optimism/optimism/op-service/txintent"
)

// TestSupernodeInteropBidirectionalMessages tests sending messages in both directions
// (A->B and B->A) to verify the supernode handles bidirectional interop correctly.
// All messages are valid, and no interruptions to the chains are expected.
func TestSupernodeInteropBidirectionalMessages(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewTwoL2SupernodeInterop(t, 0)

// Create funded EOAs on both chains
alice := sys.FunderA.NewFundedEOA(eth.OneEther)
bob := sys.FunderB.NewFundedEOA(eth.OneEther)

// Deploy event loggers on both chains
eventLoggerA := alice.DeployEventLogger()
eventLoggerB := bob.DeployEventLogger()

// Sync chains
sys.L2B.CatchUpTo(sys.L2A)
sys.L2A.CatchUpTo(sys.L2B)

rng := rand.New(rand.NewSource(54321))

// Send A -> B message
initTriggerAtoB := randomInitTrigger(rng, eventLoggerA, 2, 10)
initTxAtoB, initReceiptAtoB := alice.SendInitMessage(initTriggerAtoB)
sys.L2B.WaitForBlock()
_, execReceiptAtoB := bob.SendExecMessage(initTxAtoB, 0)

t.Logger().Info("A->B message sent",
"init_block", initReceiptAtoB.BlockNumber,
"exec_block", execReceiptAtoB.BlockNumber,
)

// Send B -> A message
initTriggerBtoA := randomInitTrigger(rng, eventLoggerB, 2, 10)
initTxBtoA, initReceiptBtoA := bob.SendInitMessage(initTriggerBtoA)
sys.L2A.WaitForBlock()
_, execReceiptBtoA := alice.SendExecMessage(initTxBtoA, 0)

t.Logger().Info("B->A message sent",
"init_block", initReceiptBtoA.BlockNumber,
"exec_block", execReceiptBtoA.BlockNumber,
)

// Wait for all messages to become safe
blockTime := sys.L2A.Escape().RollupConfig().BlockTime
timeout := time.Duration(blockTime*25+60) * time.Second

t.Require().Eventually(func() bool {
statusA := sys.L2ACL.SyncStatus()
statusB := sys.L2BCL.SyncStatus()

// All blocks should be safe
return statusA.SafeL2.Number > bigs.Uint64Strict(initReceiptAtoB.BlockNumber) &&
statusA.SafeL2.Number > bigs.Uint64Strict(execReceiptBtoA.BlockNumber) &&
statusB.SafeL2.Number > bigs.Uint64Strict(execReceiptAtoB.BlockNumber) &&
statusB.SafeL2.Number > bigs.Uint64Strict(initReceiptBtoA.BlockNumber)
}, timeout, time.Second, "bidirectional messages should become safe")

t.Logger().Info("bidirectional messages processed successfully")
}

// randomInitTrigger creates a random init trigger for testing.
func randomInitTrigger(rng *rand.Rand, eventLoggerAddress common.Address, topicCount, dataLen int) *txintent.InitTrigger {
if topicCount > 4 {
topicCount = 4 // Max 4 topics in EVM logs
}
if topicCount < 1 {
topicCount = 1
}
if dataLen < 1 {
dataLen = 1
}

topics := make([][32]byte, topicCount)
for i := range topics {
copy(topics[i][:], testutils.RandomData(rng, 32))
}

return &txintent.InitTrigger{
Emitter: eventLoggerAddress,
Topics: topics,
OpaqueData: testutils.RandomData(rng, dataLen),
}
}
16 changes: 16 additions & 0 deletions op-acceptance-tests/tests/supernode/interop/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package interop

import (
"os"
"testing"

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

// TestMain creates a two-L2 setup with a shared supernode that has interop enabled.
// This allows testing of cross-chain message verification at each timestamp.
func TestMain(m *testing.M) {
// Set the L2CL kind to supernode for all tests in this package
_ = os.Setenv("DEVSTACK_L2CL_KIND", "supernode")
presets.DoMain(m, presets.WithTwoL2SupernodeInterop(0))
}
Loading