diff --git a/.circleci/config.yml b/.circleci/config.yml index a209d32849a..eb7bc83998c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1073,6 +1073,7 @@ jobs: default: "go-tests-short-ci" machine: true resource_class: <> + circleci_ip_ranges: true steps: - checkout-from-workspace - run: diff --git a/op-acceptance-tests/tests/sync_tester_ext_el/init_test.go b/op-acceptance-tests/tests/sync_tester_ext_el/init_test.go new file mode 100644 index 00000000000..c28d966bab8 --- /dev/null +++ b/op-acceptance-tests/tests/sync_tester_ext_el/init_test.go @@ -0,0 +1,39 @@ +package sync_tester_ext_el + +import ( + "os" + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/compat" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +var ( + InitialL2Block = uint64(32012748) +) + +func TestMain(m *testing.M) { + L2NetworkName := "op-sepolia" + L1ChainID := eth.ChainIDFromUInt64(11155111) + + L2ELEndpoint := "https://ci-sepolia-l2.optimism.io" + L1CLBeaconEndpoint := "https://ci-sepolia-beacon.optimism.io" + L1ELEndpoint := "https://ci-sepolia-l1.optimism.io" + + // Endpoints when running with Tailscale networking + if os.Getenv("TAILSCALE_NETWORKING") == "true" { + L2ELEndpoint = "https://proxyd-l2-sepolia.primary.client.dev.oplabs.cloud" + L1CLBeaconEndpoint = "https://beacon-api-proxy-sepolia.primary.client.dev.oplabs.cloud" + L1ELEndpoint = "https://proxyd-l1-sepolia.primary.client.dev.oplabs.cloud" + } + + presets.DoMain(m, presets.WithMinimalExternalELWithSuperchainRegistry(L1CLBeaconEndpoint, L1ELEndpoint, L2ELEndpoint, L1ChainID, L2NetworkName, eth.FCUState{ + Latest: InitialL2Block, + Safe: InitialL2Block, + Finalized: InitialL2Block, + }), + presets.WithCompatibleTypes(compat.SysGo), + ) + +} diff --git a/op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go b/op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go new file mode 100644 index 00000000000..0d797e529bb --- /dev/null +++ b/op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go @@ -0,0 +1,52 @@ +package sync_tester_ext_el + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +func TestSyncTesterExtEL(gt *testing.T) { + t := devtest.SerialT(gt) + + sys := presets.NewMinimalExternalELWithExternalL1(t) + require := t.Require() + + // Test that we can get chain IDs from L2CL node + l2CLChainID := sys.L2CL.ID().ChainID() + require.Equal(eth.ChainIDFromUInt64(11155420), l2CLChainID, "L2CL should be on chain 11155420") + + // Test that the network started successfully + require.NotNil(sys.L1EL, "L1 EL node should be available") + require.NotNil(sys.L2CL, "L2 CL node should be available") + require.NotNil(sys.SyncTester, "SyncTester should be available") + + // Test that we can get sync status from L2CL node + l2CLSyncStatus := sys.L2CL.SyncStatus() + require.NotNil(l2CLSyncStatus, "L2CL should have sync status") + + blocksToSync := uint64(20) + targetBlock := InitialL2Block + blocksToSync + sys.L2CL.Reached(types.LocalUnsafe, targetBlock, 500) + + l2CLSyncStatus = sys.L2CL.SyncStatus() + require.NotNil(l2CLSyncStatus, "L2CL should have sync status") + + unsafeL2Ref := l2CLSyncStatus.UnsafeL2 + blk := sys.L2EL.BlockRefByNumber(unsafeL2Ref.Number) + require.Equal(unsafeL2Ref.Hash, blk.Hash, "L2EL should be on the same block as L2CL") + + stSessions := sys.SyncTester.ListSessions() + require.Equal(len(stSessions), 1, "expect exactly one session") + + stSession := sys.SyncTester.GetSession(stSessions[0]) + require.GreaterOrEqual(stSession.CurrentState.Latest, stSession.InitialState.Latest+blocksToSync, "SyncTester session Latest should be on the same block as L2CL") + require.GreaterOrEqual(stSession.CurrentState.Safe, stSession.InitialState.Safe+blocksToSync, "SyncTester session Safe should be on the same block as L2CL") + + t.Logger().Info("SyncTester ExtEL test completed successfully", + "l2cl_chain_id", l2CLChainID, + "l2cl_sync_status", l2CLSyncStatus) +} diff --git a/op-devstack/presets/minimal_external_el.go b/op-devstack/presets/minimal_external_el.go new file mode 100644 index 00000000000..a14bc897abc --- /dev/null +++ b/op-devstack/presets/minimal_external_el.go @@ -0,0 +1,60 @@ +package presets + +import ( + "github.com/ethereum/go-ethereum/log" + + "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-service/eth" +) + +type MinimalExternalEL struct { + Log log.Logger + T devtest.T + ControlPlane stack.ControlPlane + + L1Network *dsl.L1Network + L1EL *dsl.L1ELNode + + L2Chain *dsl.L2Network + L2CL *dsl.L2CLNode + L2EL *dsl.L2ELNode + + SyncTester *dsl.SyncTester +} + +func (m *MinimalExternalEL) L2Networks() []*dsl.L2Network { + return []*dsl.L2Network{ + m.L2Chain, + } +} + +func WithMinimalExternalELWithSuperchainRegistry(l1CLBeaconRPC, l1ELRPC, l2ELRPC string, l1ChainID eth.ChainID, networkName string, fcu eth.FCUState) stack.CommonOption { + return stack.MakeCommon(sysgo.DefaultMinimalExternalELSystemWithEndpointAndSuperchainRegistry(&sysgo.DefaultMinimalExternalELSystemIDs{}, l1CLBeaconRPC, l1ELRPC, l2ELRPC, l1ChainID, networkName, fcu)) +} + +func NewMinimalExternalELWithExternalL1(t devtest.T) *MinimalExternalEL { + system := shim.NewSystem(t) + orch := Orchestrator() + orch.Hydrate(system) + + l2 := system.L2Network(match.Assume(t, match.L2ChainA)) + verifierCL := l2.L2CLNode(match.FirstL2CL) + syncTester := l2.SyncTester(match.Assume(t, match.FirstSyncTester)) + + return &MinimalExternalEL{ + Log: t.Logger(), + T: t, + ControlPlane: orch.ControlPlane(), + L1Network: dsl.NewL1Network(system.L1Network(match.FirstL1Network)), + L1EL: dsl.NewL1ELNode(system.L1Network(match.FirstL1Network).L1ELNode(match.FirstL1EL)), + L2Chain: dsl.NewL2Network(l2, orch.ControlPlane()), + L2CL: dsl.NewL2CLNode(verifierCL, orch.ControlPlane()), + L2EL: dsl.NewL2ELNode(l2.L2ELNode(match.FirstL2EL), orch.ControlPlane()), + SyncTester: dsl.NewSyncTester(syncTester), + } +} diff --git a/op-devstack/sysgo/faucet.go b/op-devstack/sysgo/faucet.go index 0d8f80717aa..f746ade136b 100644 --- a/op-devstack/sysgo/faucet.go +++ b/op-devstack/sysgo/faucet.go @@ -23,6 +23,10 @@ type FaucetService struct { } func (n *FaucetService) hydrate(system stack.ExtensibleSystem) { + if n == nil || n.service == nil { + return + } + require := system.T().Require() for faucetID, chainID := range n.service.Faucets() { diff --git a/op-devstack/sysgo/l1_nodes.go b/op-devstack/sysgo/l1_nodes.go index 272b15ccef0..750182edbc1 100644 --- a/op-devstack/sysgo/l1_nodes.go +++ b/op-devstack/sysgo/l1_nodes.go @@ -113,3 +113,24 @@ func WithL1Nodes(l1ELID stack.L1ELNodeID, l1CLID stack.L1CLNodeID) stack.Option[ require.True(orch.l1CLs.SetIfMissing(l1CLID, l1CLNode), "must not already exist") }) } + +// WithExtL1Nodes initializes L1 EL and CL nodes that connect to external RPC endpoints +func WithExtL1Nodes(l1ELID stack.L1ELNodeID, l1CLID stack.L1CLNodeID, elRPCEndpoint string, clRPCEndpoint string) stack.Option[*Orchestrator] { + return stack.AfterDeploy(func(orch *Orchestrator) { + require := orch.P().Require() + + // Create L1 EL node with external RPC + l1ELNode := &L1ELNode{ + id: l1ELID, + userRPC: elRPCEndpoint, + } + require.True(orch.l1ELs.SetIfMissing(l1ELID, l1ELNode), "must not already exist") + + // Create L1 CL node with external RPC + l1CLNode := &L1CLNode{ + id: l1CLID, + beaconHTTPAddr: clRPCEndpoint, + } + require.True(orch.l1CLs.SetIfMissing(l1CLID, l1CLNode), "must not already exist") + }) +} diff --git a/op-devstack/sysgo/l2_cl_opnode.go b/op-devstack/sysgo/l2_cl_opnode.go index d0377b63aa8..d290496a57c 100644 --- a/op-devstack/sysgo/l2_cl_opnode.go +++ b/op-devstack/sysgo/l2_cl_opnode.go @@ -256,7 +256,7 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L L2EngineJWTSecret: jwtSecret, }, Beacon: &config.L1BeaconEndpointConfig{ - BeaconAddr: l1CL.beacon.BeaconAddr(), + BeaconAddr: l1CL.beaconHTTPAddr, }, Driver: driver.Config{ SequencerEnabled: cfg.IsSequencer, diff --git a/op-devstack/sysgo/l2_network_superchain_registry.go b/op-devstack/sysgo/l2_network_superchain_registry.go new file mode 100644 index 00000000000..f395d2e813d --- /dev/null +++ b/op-devstack/sysgo/l2_network_superchain_registry.go @@ -0,0 +1,86 @@ +package sysgo + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/core" + + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/superutil" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/depset" +) + +// WithL2NetworkFromSuperchainRegistry creates an L2 network using the rollup config from the superchain registry +func WithL2NetworkFromSuperchainRegistry(l2NetworkID stack.L2NetworkID, networkName string) stack.Option[*Orchestrator] { + return stack.BeforeDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), l2NetworkID)) + require := p.Require() + + // Load the rollup config from the superchain registry + rollupCfg, err := chaincfg.GetRollupConfig(networkName) + require.NoError(err, "failed to load rollup config for network %s", networkName) + + // Get the chain config from the superchain registry + chainCfg := chaincfg.ChainByName(networkName) + require.NotNil(chainCfg, "chain config not found for network %s", networkName) + + // Load the chain config using superutil + paramsChainConfig, err := superutil.LoadOPStackChainConfigFromChainID(chainCfg.ChainID) + require.NoError(err, "failed to load chain config for network %s", networkName) + + // Create a genesis config from the chain config + genesis := &core.Genesis{ + Config: paramsChainConfig, + } + + // Create the L2 network + l2Net := &L2Network{ + id: l2NetworkID, + l1ChainID: eth.ChainIDFromBig(rollupCfg.L1ChainID), + genesis: genesis, + rollupCfg: rollupCfg, + keys: orch.keys, + } + + require.True(orch.l2Nets.SetIfMissing(l2NetworkID.ChainID(), l2Net), + fmt.Sprintf("must not already exist: %s", l2NetworkID)) + }) +} + +// WithL2NetworkFromSuperchainRegistryWithDependencySet creates an L2 network using the rollup config from the superchain registry +// and also sets up the dependency set for interop support +func WithL2NetworkFromSuperchainRegistryWithDependencySet(l2NetworkID stack.L2NetworkID, networkName string) stack.Option[*Orchestrator] { + return stack.Combine( + WithL2NetworkFromSuperchainRegistry(l2NetworkID, networkName), + stack.BeforeDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), l2NetworkID)) + require := p.Require() + + // Load the dependency set from the superchain registry + chainCfg := chaincfg.ChainByName(networkName) + require.NotNil(chainCfg, "chain config not found for network %s", networkName) + + _, err := depset.FromRegistry(eth.ChainIDFromUInt64(chainCfg.ChainID)) + if err != nil { + // If dependency set is not available, that's okay - it's optional + p.Logger().Info("No dependency set available for network", "network", networkName, "err", err) + return + } + + // Create a cluster to hold the dependency set + clusterID := stack.ClusterID(networkName) + + // Create a minimal full config set with just the dependency set + // This is a simplified approach - in a real implementation you might want + // to create a proper FullConfigSetMerged + cluster := &Cluster{ + id: clusterID, + cfgset: depset.FullConfigSetMerged{}, // Empty for now + } + + orch.clusters.Set(clusterID, cluster) + }), + ) +} diff --git a/op-devstack/sysgo/sync_tester.go b/op-devstack/sysgo/sync_tester.go index 2e15316dc6c..ace022fd639 100644 --- a/op-devstack/sysgo/sync_tester.go +++ b/op-devstack/sysgo/sync_tester.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/stack" "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/endpoint" + "github.com/ethereum-optimism/optimism/op-service/eth" oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" "github.com/ethereum-optimism/optimism/op-sync-tester/config" @@ -88,3 +89,43 @@ func WithSyncTester(syncTesterID stack.SyncTesterID, l2ELs []stack.L2ELNodeID) s orch.syncTester = &SyncTesterService{id: syncTesterID, service: srv} }) } + +func WithSyncTesterWithExternalEndpoint(syncTesterID stack.SyncTesterID, endpointRPC string, chainID eth.ChainID) stack.Option[*Orchestrator] { + return stack.AfterDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), syncTesterID)) + + require := p.Require() + + require.Nil(orch.syncTester, "can only support a single sync-tester-service in sysgo") + + syncTesters := make(map[sttypes.SyncTesterID]*stconf.SyncTesterEntry) + + // Create a sync tester entry with the external endpoint + id := sttypes.SyncTesterID(fmt.Sprintf("dev-sync-tester-%s", chainID)) + syncTesters[id] = &stconf.SyncTesterEntry{ + ELRPC: endpoint.MustRPC{Value: endpoint.URL(endpointRPC)}, + ChainID: chainID, + } + + cfg := &config.Config{ + RPC: oprpc.CLIConfig{ + ListenAddr: "127.0.0.1", + }, + SyncTesters: &stconf.Config{ + SyncTesters: syncTesters, + }, + } + logger := p.Logger() + srv, err := synctester.FromConfig(p.Ctx(), cfg, logger) + require.NoError(err, "must setup sync tester service") + require.NoError(srv.Start(p.Ctx())) + p.Cleanup(func() { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // force-quit + logger.Info("Closing sync tester") + _ = srv.Stop(ctx) + logger.Info("Closed sync tester") + }) + orch.syncTester = &SyncTesterService{id: syncTesterID, service: srv} + }) +} diff --git a/op-devstack/sysgo/system_synctester_ext.go b/op-devstack/sysgo/system_synctester_ext.go new file mode 100644 index 00000000000..4d6ef8e211f --- /dev/null +++ b/op-devstack/sysgo/system_synctester_ext.go @@ -0,0 +1,94 @@ +package sysgo + +import ( + "fmt" + "math/big" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" +) + +type DefaultMinimalExternalELSystemIDs struct { + L1 stack.L1NetworkID + L1EL stack.L1ELNodeID + L1CL stack.L1CLNodeID + + L2 stack.L2NetworkID + L2CL stack.L2CLNodeID + L2EL stack.L2ELNodeID + + SyncTester stack.SyncTesterID +} + +func NewDefaultMinimalExternalELSystemIDs(l1ID, l2ID eth.ChainID) DefaultMinimalExternalELSystemIDs { + ids := DefaultMinimalExternalELSystemIDs{ + L1: stack.L1NetworkID(l1ID), + L1EL: stack.NewL1ELNodeID("l1", l1ID), + L1CL: stack.NewL1CLNodeID("l1", l1ID), + L2: stack.L2NetworkID(l2ID), + L2CL: stack.NewL2CLNodeID("verifier", l2ID), + L2EL: stack.NewL2ELNodeID("sync-tester-el", l2ID), + SyncTester: stack.NewSyncTesterID("sync-tester", l2ID), + } + return ids +} + +// DefaultMinimalExternalELSystemWithEndpointAndSuperchainRegistry creates a minimal external EL system +// using a network from the superchain registry instead of the deployer +func DefaultMinimalExternalELSystemWithEndpointAndSuperchainRegistry(dest *DefaultMinimalExternalELSystemIDs, l1CLBeaconRPC, l1ELRPC, l2ELRPC string, l1ChainID eth.ChainID, networkName string, fcu eth.FCUState) stack.Option[*Orchestrator] { + chainCfg := chaincfg.ChainByName(networkName) + if chainCfg == nil { + panic(fmt.Sprintf("network %s not found in superchain registry", networkName)) + } + l2ChainID := eth.ChainIDFromUInt64(chainCfg.ChainID) + + ids := NewDefaultMinimalExternalELSystemIDs(l1ChainID, l2ChainID) + + opt := stack.Combine[*Orchestrator]() + opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { + o.P().Logger().Info("Setting up with superchain registry network", "network", networkName) + })) + + opt.Add(WithMnemonicKeys(devkeys.TestMnemonic)) + + // Skip deployer since we're using external L1 and superchain registry for L2 config + // Create L1 network record for external L1 + opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { + chainID, _ := ids.L1.ChainID().Uint64() + l1Net := &L1Network{ + id: ids.L1, + genesis: &core.Genesis{ + Config: ¶ms.ChainConfig{ + ChainID: big.NewInt(int64(chainID)), + }, + }, + blockTime: 12, + } + o.l1Nets.Set(ids.L1.ChainID(), l1Net) + })) + + opt.Add(WithExtL1Nodes(ids.L1EL, ids.L1CL, l1ELRPC, l1CLBeaconRPC)) + + // Use superchain registry instead of deployer + opt.Add(WithL2NetworkFromSuperchainRegistryWithDependencySet( + stack.L2NetworkID(l2ChainID), + networkName, + )) + + // Add SyncTester service with external endpoint + opt.Add(WithSyncTesterWithExternalEndpoint(ids.SyncTester, l2ELRPC, l2ChainID)) + + // Add SyncTesterL2ELNode as the L2EL replacement for real-world EL endpoint + opt.Add(WithSyncTesterL2ELNode(ids.L2EL, ids.L2EL, fcu)) + opt.Add(WithL2CLNode(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL)) + + opt.Add(stack.Finally(func(orch *Orchestrator) { + *dest = ids + })) + + return opt +} diff --git a/op-sync-tester/synctester/backend/sync_tester.go b/op-sync-tester/synctester/backend/sync_tester.go index c1373332122..201dcbf8b05 100644 --- a/op-sync-tester/synctester/backend/sync_tester.go +++ b/op-sync-tester/synctester/backend/sync_tester.go @@ -93,7 +93,7 @@ func (s *SyncTester) fetchSession(ctx context.Context) (*eth.SyncTesterSession, return nil, fmt.Errorf("session already deleted: %s", session.SessionID) } if existing, ok := s.sessions[session.SessionID]; ok { - s.log.Info("Using existing session", "session", existing) + s.log.Debug("Using existing session", "session", existing) return existing, nil } else { s.storeSession(session)