From 6c7c3399724e07de0431c1917a7eea9d35ed5046 Mon Sep 17 00:00:00 2001 From: Teddy Knox Date: Tue, 20 Jan 2026 11:49:12 -0500 Subject: [PATCH] op-devstack: add capability interfaces for polymorphic lookups (Phase 3) Introduce L2ELCapable interface that captures shared behavior across L2ELNode, RollupBoostNode, and OPRBuilderNode without requiring them to share an ID() method signature. This enables polymorphic lookups where code can find any L2 EL-capable component by key+chainID, regardless of concrete type: sequencer, ok := FindL2ELCapableByKey(registry, "sequencer", chainID) Previously this required manual multi-registry lookups checking each type separately. --- op-devstack/stack/capabilities.go | 134 +++++++++++ op-devstack/stack/capabilities_test.go | 312 +++++++++++++++++++++++++ 2 files changed, 446 insertions(+) create mode 100644 op-devstack/stack/capabilities.go create mode 100644 op-devstack/stack/capabilities_test.go 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()) +}