diff --git a/op-devstack/stack/capabilities.go b/op-devstack/stack/capabilities.go new file mode 100644 index 00000000000..7075fa0eabf --- /dev/null +++ b/op-devstack/stack/capabilities.go @@ -0,0 +1,134 @@ +package stack + +import ( + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +// Capability interfaces define shared behaviors across component types. +// These enable polymorphic operations without requiring components to +// implement interfaces with incompatible ID() method signatures. +// +// For example, RollupBoostNode and OPRBuilderNode both provide L2 EL +// functionality but can't implement L2ELNode because their ID() methods +// return different types. The L2ELCapable interface captures the shared +// L2 EL behavior, allowing code to work with any L2 EL-like component. + +// L2ELCapable is implemented by any component that provides L2 execution layer functionality. +// This includes L2ELNode, RollupBoostNode, and OPRBuilderNode. +// +// Components implementing this interface can: +// - Execute L2 transactions +// - Provide engine API access for consensus layer integration +type L2ELCapable interface { + L2EthClient() apis.L2EthClient + L2EngineClient() apis.EngineClient + ELNode +} + +// L2ELCapableKinds returns all ComponentKinds that implement L2ELCapable. +func L2ELCapableKinds() []ComponentKind { + return []ComponentKind{ + KindL2ELNode, + KindRollupBoostNode, + KindOPRBuilderNode, + } +} + +// L1ELCapable is implemented by any component that provides L1 execution layer functionality. +type L1ELCapable interface { + ELNode +} + +// L1ELCapableKinds returns all ComponentKinds that implement L1ELCapable. +func L1ELCapableKinds() []ComponentKind { + return []ComponentKind{ + KindL1ELNode, + } +} + +// Verify that expected types implement capability interfaces. +// These are compile-time checks. +var ( + _ L2ELCapable = (L2ELNode)(nil) + _ L2ELCapable = (RollupBoostNode)(nil) + _ L2ELCapable = (OPRBuilderNode)(nil) +) + +// Registry helper functions for capability-based lookups. + +// RegistryFindByCapability returns all components that implement the given capability interface. +// This iterates over all components and performs a type assertion. +func RegistryFindByCapability[T any](r *Registry) []T { + var result []T + r.Range(func(id ComponentID, component any) bool { + if capable, ok := component.(T); ok { + result = append(result, capable) + } + return true + }) + return result +} + +// RegistryFindByCapabilityOnChain returns all components on a specific chain +// that implement the given capability interface. +func RegistryFindByCapabilityOnChain[T any](r *Registry, chainID eth.ChainID) []T { + var result []T + r.RangeByChainID(chainID, func(id ComponentID, component any) bool { + if capable, ok := component.(T); ok { + result = append(result, capable) + } + return true + }) + return result +} + +// RegistryFindByKinds returns all components of the specified kinds. +// This is useful when you know which kinds implement a capability. +func RegistryFindByKinds(r *Registry, kinds []ComponentKind) []any { + var result []any + for _, kind := range kinds { + result = append(result, r.GetByKind(kind)...) + } + return result +} + +// RegistryFindByKindsTyped returns all components of the specified kinds, +// cast to the expected type. Components that don't match are skipped. +func RegistryFindByKindsTyped[T any](r *Registry, kinds []ComponentKind) []T { + var result []T + for _, kind := range kinds { + for _, component := range r.GetByKind(kind) { + if typed, ok := component.(T); ok { + result = append(result, typed) + } + } + } + return result +} + +// FindL2ELCapable returns all L2 EL-capable components in the registry. +// This is a convenience function that finds L2ELNode, RollupBoostNode, and OPRBuilderNode. +func FindL2ELCapable(r *Registry) []L2ELCapable { + return RegistryFindByKindsTyped[L2ELCapable](r, L2ELCapableKinds()) +} + +// FindL2ELCapableOnChain returns all L2 EL-capable components on a specific chain. +func FindL2ELCapableOnChain(r *Registry, chainID eth.ChainID) []L2ELCapable { + return RegistryFindByCapabilityOnChain[L2ELCapable](r, chainID) +} + +// FindL2ELCapableByKey returns the first L2 EL-capable component with the given key and chainID. +// This enables the polymorphic lookup pattern where you want to find a node by key +// regardless of whether it's an L2ELNode, RollupBoostNode, or OPRBuilderNode. +func FindL2ELCapableByKey(r *Registry, key string, chainID eth.ChainID) (L2ELCapable, bool) { + for _, kind := range L2ELCapableKinds() { + id := NewComponentID(kind, key, chainID) + if component, ok := r.Get(id); ok { + if capable, ok := component.(L2ELCapable); ok { + return capable, true + } + } + } + return nil, false +} diff --git a/op-devstack/stack/capabilities_test.go b/op-devstack/stack/capabilities_test.go new file mode 100644 index 00000000000..b69758a0b9a --- /dev/null +++ b/op-devstack/stack/capabilities_test.go @@ -0,0 +1,312 @@ +package stack + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" +) + +// Mock implementations for testing capabilities + +type mockELNode struct { + chainID eth.ChainID +} + +func (m *mockELNode) T() devtest.T { return nil } +func (m *mockELNode) Logger() log.Logger { return nil } +func (m *mockELNode) Label(key string) string { return "" } +func (m *mockELNode) SetLabel(key, value string) {} +func (m *mockELNode) ChainID() eth.ChainID { return m.chainID } +func (m *mockELNode) EthClient() apis.EthClient { return nil } +func (m *mockELNode) TransactionTimeout() time.Duration { return 0 } + +type mockL2ELNode struct { + mockELNode + id L2ELNodeID +} + +func (m *mockL2ELNode) ID() L2ELNodeID { return m.id } +func (m *mockL2ELNode) L2EthClient() apis.L2EthClient { return nil } +func (m *mockL2ELNode) L2EngineClient() apis.EngineClient { return nil } +func (m *mockL2ELNode) RegistryID() ComponentID { return ConvertL2ELNodeID(m.id).ComponentID } + +var _ L2ELNode = (*mockL2ELNode)(nil) +var _ L2ELCapable = (*mockL2ELNode)(nil) +var _ Registrable = (*mockL2ELNode)(nil) + +type mockRollupBoostNode struct { + mockELNode + id RollupBoostNodeID +} + +func (m *mockRollupBoostNode) ID() RollupBoostNodeID { return m.id } +func (m *mockRollupBoostNode) L2EthClient() apis.L2EthClient { return nil } +func (m *mockRollupBoostNode) L2EngineClient() apis.EngineClient { return nil } +func (m *mockRollupBoostNode) FlashblocksClient() *client.WSClient { return nil } +func (m *mockRollupBoostNode) RegistryID() ComponentID { + return ConvertRollupBoostNodeID(m.id).ComponentID +} + +var _ RollupBoostNode = (*mockRollupBoostNode)(nil) +var _ L2ELCapable = (*mockRollupBoostNode)(nil) +var _ Registrable = (*mockRollupBoostNode)(nil) + +type mockOPRBuilderNode struct { + mockELNode + id OPRBuilderNodeID +} + +func (m *mockOPRBuilderNode) ID() OPRBuilderNodeID { return m.id } +func (m *mockOPRBuilderNode) L2EthClient() apis.L2EthClient { return nil } +func (m *mockOPRBuilderNode) L2EngineClient() apis.EngineClient { return nil } +func (m *mockOPRBuilderNode) FlashblocksClient() *client.WSClient { return nil } +func (m *mockOPRBuilderNode) RegistryID() ComponentID { + return ConvertOPRBuilderNodeID(m.id).ComponentID +} + +var _ OPRBuilderNode = (*mockOPRBuilderNode)(nil) +var _ L2ELCapable = (*mockOPRBuilderNode)(nil) +var _ Registrable = (*mockOPRBuilderNode)(nil) + +func TestL2ELCapableKinds(t *testing.T) { + kinds := L2ELCapableKinds() + require.Len(t, kinds, 3) + require.Contains(t, kinds, KindL2ELNode) + require.Contains(t, kinds, KindRollupBoostNode) + require.Contains(t, kinds, KindOPRBuilderNode) +} + +func TestRegistryFindByCapability(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + // Register different L2 EL-capable nodes + l2el := &mockL2ELNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewL2ELNodeID("sequencer", chainID), + } + rollupBoost := &mockRollupBoostNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewRollupBoostNodeID("boost", chainID), + } + oprBuilder := &mockOPRBuilderNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewOPRBuilderNodeID("builder", chainID), + } + + r.RegisterComponent(l2el) + r.RegisterComponent(rollupBoost) + r.RegisterComponent(oprBuilder) + + // Also register a non-L2EL component + r.Register(NewComponentID(KindL2Batcher, "batcher", chainID), "not-l2el-capable") + + // Find all L2ELCapable + capable := RegistryFindByCapability[L2ELCapable](r) + require.Len(t, capable, 3) +} + +func TestRegistryFindByCapabilityOnChain(t *testing.T) { + r := NewRegistry() + + chainID1 := eth.ChainIDFromUInt64(420) + chainID2 := eth.ChainIDFromUInt64(421) + + // Nodes on chain 420 + l2el1 := &mockL2ELNode{ + mockELNode: mockELNode{chainID: chainID1}, + id: NewL2ELNodeID("sequencer", chainID1), + } + rollupBoost1 := &mockRollupBoostNode{ + mockELNode: mockELNode{chainID: chainID1}, + id: NewRollupBoostNodeID("boost", chainID1), + } + + // Node on chain 421 + l2el2 := &mockL2ELNode{ + mockELNode: mockELNode{chainID: chainID2}, + id: NewL2ELNodeID("sequencer", chainID2), + } + + r.RegisterComponent(l2el1) + r.RegisterComponent(rollupBoost1) + r.RegisterComponent(l2el2) + + // Find on chain 420 + chain420 := RegistryFindByCapabilityOnChain[L2ELCapable](r, chainID1) + require.Len(t, chain420, 2) + + // Find on chain 421 + chain421 := RegistryFindByCapabilityOnChain[L2ELCapable](r, chainID2) + require.Len(t, chain421, 1) +} + +func TestFindL2ELCapable(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + l2el := &mockL2ELNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewL2ELNodeID("sequencer", chainID), + } + rollupBoost := &mockRollupBoostNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewRollupBoostNodeID("boost", chainID), + } + + r.RegisterComponent(l2el) + r.RegisterComponent(rollupBoost) + + capable := FindL2ELCapable(r) + require.Len(t, capable, 2) +} + +func TestFindL2ELCapableOnChain(t *testing.T) { + r := NewRegistry() + + chainID1 := eth.ChainIDFromUInt64(420) + chainID2 := eth.ChainIDFromUInt64(421) + + l2el1 := &mockL2ELNode{ + mockELNode: mockELNode{chainID: chainID1}, + id: NewL2ELNodeID("sequencer", chainID1), + } + l2el2 := &mockL2ELNode{ + mockELNode: mockELNode{chainID: chainID2}, + id: NewL2ELNodeID("sequencer", chainID2), + } + + r.RegisterComponent(l2el1) + r.RegisterComponent(l2el2) + + chain420 := FindL2ELCapableOnChain(r, chainID1) + require.Len(t, chain420, 1) + require.Equal(t, chainID1, chain420[0].ChainID()) +} + +func TestFindL2ELCapableByKey(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + // Register a RollupBoostNode with key "sequencer" + rollupBoost := &mockRollupBoostNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewRollupBoostNodeID("sequencer", chainID), + } + r.RegisterComponent(rollupBoost) + + // Should find it by key, even though it's not an L2ELNode + found, ok := FindL2ELCapableByKey(r, "sequencer", chainID) + require.True(t, ok) + require.NotNil(t, found) + require.Equal(t, chainID, found.ChainID()) + + // Should not find non-existent key + _, ok = FindL2ELCapableByKey(r, "nonexistent", chainID) + require.False(t, ok) +} + +func TestFindL2ELCapableByKey_PrefersL2ELNode(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + // Register both L2ELNode and RollupBoostNode with same key + l2el := &mockL2ELNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewL2ELNodeID("sequencer", chainID), + } + rollupBoost := &mockRollupBoostNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewRollupBoostNodeID("sequencer", chainID), + } + + r.RegisterComponent(l2el) + r.RegisterComponent(rollupBoost) + + // Should find L2ELNode first (it's first in L2ELCapableKinds) + found, ok := FindL2ELCapableByKey(r, "sequencer", chainID) + require.True(t, ok) + // Verify it's the L2ELNode by checking it's the right mock type + _, isL2EL := found.(*mockL2ELNode) + require.True(t, isL2EL, "expected to find L2ELNode first") +} + +func TestRegistryFindByKindsTyped(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + l2el := &mockL2ELNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewL2ELNodeID("sequencer", chainID), + } + rollupBoost := &mockRollupBoostNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewRollupBoostNodeID("boost", chainID), + } + + r.RegisterComponent(l2el) + r.RegisterComponent(rollupBoost) + + // Find only L2ELNode kind + l2els := RegistryFindByKindsTyped[L2ELCapable](r, []ComponentKind{KindL2ELNode}) + require.Len(t, l2els, 1) + + // Find both kinds + both := RegistryFindByKindsTyped[L2ELCapable](r, []ComponentKind{KindL2ELNode, KindRollupBoostNode}) + require.Len(t, both, 2) +} + +// TestPolymorphicLookupScenario demonstrates the polymorphic lookup use case +// that Phase 3 is designed to solve. +func TestPolymorphicLookupScenario(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + // Scenario: A test wants to find an L2 EL node by key "sequencer" + // The actual node could be L2ELNode, RollupBoostNode, or OPRBuilderNode + // depending on the test configuration. + + // Configuration 1: Using RollupBoost + rollupBoost := &mockRollupBoostNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewRollupBoostNodeID("sequencer", chainID), + } + r.RegisterComponent(rollupBoost) + + // The polymorphic lookup finds the sequencer regardless of its concrete type + sequencer, ok := FindL2ELCapableByKey(r, "sequencer", chainID) + require.True(t, ok) + require.NotNil(t, sequencer) + + // Can use it as L2ELCapable + require.Equal(t, chainID, sequencer.ChainID()) + // Could call sequencer.L2EthClient(), sequencer.L2EngineClient(), etc. + + // Clear and try with OPRBuilder + r.Clear() + + oprBuilder := &mockOPRBuilderNode{ + mockELNode: mockELNode{chainID: chainID}, + id: NewOPRBuilderNodeID("sequencer", chainID), + } + r.RegisterComponent(oprBuilder) + + // Same lookup code works + sequencer, ok = FindL2ELCapableByKey(r, "sequencer", chainID) + require.True(t, ok) + require.NotNil(t, sequencer) + require.Equal(t, chainID, sequencer.ChainID()) +}