Skip to content

fix(kt-devnet): support multiple depsets per devnet #15526

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 24, 2025
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
4 changes: 2 additions & 2 deletions devnet-sdk/descriptors/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@ type DevnetEnvironment struct {
L1 *Chain `json:"l1"`
L2 []*L2Chain `json:"l2"`

Features []string `json:"features,omitempty"`
DepSets []DepSet `json:"dep_sets,omitempty"`
Features []string `json:"features,omitempty"`
DepSets map[string]DepSet `json:"dep_sets,omitempty"`
}
2 changes: 1 addition & 1 deletion kurtosis-devnet/pkg/kurtosis/adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ var _ interfaces.JWTExtractor = (*enclaveJWTAdapter)(nil)

type enclaveDepsetAdapter struct{}

func (a *enclaveDepsetAdapter) ExtractData(ctx context.Context, enclave string) ([]descriptors.DepSet, error) {
func (a *enclaveDepsetAdapter) ExtractData(ctx context.Context, enclave string) (map[string]descriptors.DepSet, error) {
return depset.NewExtractor(enclave).ExtractData(ctx)
}

Expand Down
78 changes: 64 additions & 14 deletions kurtosis-devnet/pkg/kurtosis/endpoints.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package kurtosis

import (
"encoding/json"
"strconv"
"strings"

"github.com/ethereum-optimism/optimism/devnet-sdk/descriptors"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/inspect"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/depset"
)

type ChainSpec struct {
spec.ChainSpec
DepSets map[string]descriptors.DepSet
}

// ServiceFinder is the main entry point for finding services and their endpoints
type ServiceFinder struct {
services inspect.ServiceMap
nodeServices []string
l2ServicePrefix string
l2Networks []string
l2Networks []ChainSpec
globalServices []string
}

// ServiceFinderOption configures a ServiceFinder
Expand All @@ -34,18 +44,26 @@ func WithL2ServicePrefix(prefix string) ServiceFinderOption {
}

// WithL2Networks sets the L2 networks
func WithL2Networks(networks []string) ServiceFinderOption {
func WithL2Networks(networks []ChainSpec) ServiceFinderOption {
return func(f *ServiceFinder) {
f.l2Networks = networks
}
}

// WithGlobalServices sets the global services
func WithGlobalServices(services []string) ServiceFinderOption {
return func(f *ServiceFinder) {
f.globalServices = services
}
}

// NewServiceFinder creates a new ServiceFinder with the given options
func NewServiceFinder(services inspect.ServiceMap, opts ...ServiceFinderOption) *ServiceFinder {
f := &ServiceFinder{
services: services,
nodeServices: []string{"cl", "el"},
l2ServicePrefix: "op-",
globalServices: []string{"op-faucet"},
}
for _, opt := range opts {
opt(f)
Expand All @@ -56,11 +74,13 @@ func NewServiceFinder(services inspect.ServiceMap, opts ...ServiceFinderOption)
// FindL1Services finds L1 nodes.
func (f *ServiceFinder) FindL1Services() ([]descriptors.Node, descriptors.ServiceMap) {
return f.findRPCEndpoints(func(serviceName string) (string, int, bool) {
// Only match services that start with one of the node service identifiers.
// We might have to change this if we need to support L1 services beyond nodes.
for _, service := range f.nodeServices {
// Find node services and global services
allServices := append(f.nodeServices, f.globalServices...)
for _, service := range allServices {
if strings.HasPrefix(serviceName, service) {
tag, idx := f.serviceTag(serviceName)
// strip the L2 prefix if it's there.
name := strings.TrimPrefix(serviceName, f.l2ServicePrefix)
tag, idx := f.serviceTag(name)
return tag, idx, true
}
}
Expand All @@ -69,24 +89,54 @@ func (f *ServiceFinder) FindL1Services() ([]descriptors.Node, descriptors.Servic
}

// FindL2Services finds L2 nodes and services for a specific network
func (f *ServiceFinder) FindL2Services(network string) ([]descriptors.Node, descriptors.ServiceMap) {
networkSuffix := "-" + network
func (f *ServiceFinder) FindL2Services(s ChainSpec) ([]descriptors.Node, descriptors.ServiceMap) {
network := s.Name
networkID := s.NetworkID
return f.findRPCEndpoints(func(serviceName string) (string, int, bool) {
if strings.HasSuffix(serviceName, networkSuffix) {
name := strings.TrimSuffix(serviceName, networkSuffix)
tag, idx := f.serviceTag(strings.TrimPrefix(name, f.l2ServicePrefix))
return tag, idx, true
possibleSuffixes := []string{"-" + network, "-" + networkID}
for _, suffix := range possibleSuffixes {
if strings.HasSuffix(serviceName, suffix) {
name := strings.TrimSuffix(serviceName, suffix)
tag, idx := f.serviceTag(strings.TrimPrefix(name, f.l2ServicePrefix))
return tag, idx, true
}
}

// skip over the other L2 services
for _, l2Network := range f.l2Networks {
if strings.HasSuffix(serviceName, "-"+l2Network) {
if strings.HasSuffix(serviceName, "-"+l2Network.Name) || strings.HasSuffix(serviceName, "-"+l2Network.NetworkID) {
return "", 0, false
}
}

// supervisor is special: itcovers multiple networks, so we need to
// identify the depset this chain belongs to
if strings.HasPrefix(serviceName, "op-supervisor") {
for dsName, ds := range s.DepSets {
suffix := "-" + dsName
if !strings.HasSuffix(serviceName, suffix) {
// not the right depset for this supervisor, skip it
continue
}
var depSet depset.StaticConfigDependencySet
if err := json.Unmarshal(ds, &depSet); err != nil {
return "", 0, false
}
var chainID eth.ChainID
if err := chainID.UnmarshalText([]byte(s.NetworkID)); err != nil {
return "", 0, false
}
if depSet.HasChain(chainID) {
name := strings.TrimSuffix(serviceName, suffix)
tag, idx := f.serviceTag(strings.TrimPrefix(name, f.l2ServicePrefix))
return tag, idx, true
}
}
// this supervisor is irrelevant to this chain, skip it
return "", 0, false
}

// Some services don't have a network suffix, as they span multiple chains
// TODO(14849): ideally we'd need to handle *partial* chain coverage.
if strings.HasPrefix(serviceName, f.l2ServicePrefix) {
tag, idx := f.serviceTag(strings.TrimPrefix(serviceName, f.l2ServicePrefix))
return tag, idx, true
Expand Down
152 changes: 141 additions & 11 deletions kurtosis-devnet/pkg/kurtosis/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/ethereum-optimism/optimism/devnet-sdk/descriptors"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/inspect"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
"github.com/stretchr/testify/assert"
)

Expand All @@ -30,7 +31,7 @@ func TestFindRPCEndpoints(t *testing.T) {
"tcp-discovery": {Port: 53504},
}

testServices["op-el-1-op-geth-op-node-op-kurtosis"] = inspect.PortMap{
testServices["op-el-1-op-geth-op-node-1234"] = inspect.PortMap{
"udp-discovery": {Port: 53233},
"engine-rpc": {Port: 53399},
"metrics": {Port: 53400},
Expand Down Expand Up @@ -95,7 +96,13 @@ func TestFindRPCEndpoints(t *testing.T) {
name: "find op-kurtosis L2 endpoints",
services: testServices,
findFn: func(f *ServiceFinder) ([]descriptors.Node, descriptors.ServiceMap) {
return f.FindL2Services("op-kurtosis")
return f.FindL2Services(ChainSpec{
ChainSpec: spec.ChainSpec{
Name: "op-kurtosis",
NetworkID: "1234",
},
DepSets: map[string]descriptors.DepSet{},
})
},
wantNodes: []descriptors.Node{
{
Expand All @@ -109,7 +116,7 @@ func TestFindRPCEndpoints(t *testing.T) {
},
},
"el": &descriptors.Service{
Name: "op-el-1-op-geth-op-node-op-kurtosis",
Name: "op-el-1-op-geth-op-node-1234",
Endpoints: descriptors.EndpointMap{
"udp-discovery": {Port: 53233},
"engine-rpc": {Port: 53399},
Expand Down Expand Up @@ -139,7 +146,13 @@ func TestFindRPCEndpoints(t *testing.T) {
},
},
findFn: func(f *ServiceFinder) ([]descriptors.Node, descriptors.ServiceMap) {
return f.FindL2Services("custom-host")
return f.FindL2Services(ChainSpec{
ChainSpec: spec.ChainSpec{
Name: "custom-host",
NetworkID: "0000",
},
DepSets: map[string]descriptors.DepSet{},
})
},
wantNodes: nil,
wantServices: descriptors.ServiceMap{
Expand All @@ -155,7 +168,12 @@ func TestFindRPCEndpoints(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
finder := NewServiceFinder(tt.services, WithL2Networks([]string{"op-kurtosis", "network1", "network2", "custom-host"}))
finder := NewServiceFinder(tt.services, WithL2Networks([]ChainSpec{
{ChainSpec: spec.ChainSpec{Name: "op-kurtosis", NetworkID: "1234"}},
{ChainSpec: spec.ChainSpec{Name: "network1", NetworkID: "1111"}},
{ChainSpec: spec.ChainSpec{Name: "network2", NetworkID: "2222"}},
{ChainSpec: spec.ChainSpec{Name: "custom-host", NetworkID: "0000"}},
}))
gotNodes, gotServices := tt.findFn(finder)
assert.Equal(t, tt.wantNodes, gotNodes)
assert.Equal(t, tt.wantServices, gotServices)
Expand All @@ -164,6 +182,28 @@ func TestFindRPCEndpoints(t *testing.T) {
}

func TestFindL2ServicesSkipsOtherNetworks(t *testing.T) {
n1 := ChainSpec{
ChainSpec: spec.ChainSpec{
Name: "network1",
NetworkID: "1111",
},
DepSets: map[string]descriptors.DepSet{},
}
n2 := ChainSpec{
ChainSpec: spec.ChainSpec{
Name: "network2",
NetworkID: "2222",
},
DepSets: map[string]descriptors.DepSet{},
}
n3 := ChainSpec{
ChainSpec: spec.ChainSpec{
Name: "network3",
NetworkID: "3333",
},
DepSets: map[string]descriptors.DepSet{},
}

// Create a service map with services from multiple L2 networks
services := inspect.ServiceMap{
// network1 services
Expand All @@ -181,7 +221,7 @@ func TestFindL2ServicesSkipsOtherNetworks(t *testing.T) {
"op-batcher-network2": inspect.PortMap{
"http": {Port: 8081},
},
"op-proposer-network2": inspect.PortMap{
"op-proposer-2222": inspect.PortMap{
"http": {Port: 8083},
},
"op-cl-1-op-node-op-geth-network2": inspect.PortMap{
Expand All @@ -205,15 +245,14 @@ func TestFindL2ServicesSkipsOtherNetworks(t *testing.T) {
},
}

// Create a service finder with all networks configured
finder := NewServiceFinder(
services,
WithL2Networks([]string{"network1", "network2", "network3"}),
WithL2Networks([]ChainSpec{n1, n2, n3}),
)

// Test finding services for network2
t.Run("find network2 services", func(t *testing.T) {
nodes, serviceMap := finder.FindL2Services("network2")
nodes, serviceMap := finder.FindL2Services(n2)

// Verify nodes
assert.Len(t, nodes, 1)
Expand All @@ -228,7 +267,7 @@ func TestFindL2ServicesSkipsOtherNetworks(t *testing.T) {
assert.Contains(t, serviceMap, "proposer")
assert.Contains(t, serviceMap, "common-service")
assert.Equal(t, "op-batcher-network2", serviceMap["batcher"].Name)
assert.Equal(t, "op-proposer-network2", serviceMap["proposer"].Name)
assert.Equal(t, "op-proposer-2222", serviceMap["proposer"].Name)
assert.Equal(t, "op-common-service", serviceMap["common-service"].Name)

// Verify network1 and network3 services are not included
Expand All @@ -240,7 +279,13 @@ func TestFindL2ServicesSkipsOtherNetworks(t *testing.T) {

// Test with a network that doesn't exist
t.Run("find non-existent network services", func(t *testing.T) {
nodes, serviceMap := finder.FindL2Services("non-existent")
nodes, serviceMap := finder.FindL2Services(ChainSpec{
ChainSpec: spec.ChainSpec{
Name: "non-existent",
NetworkID: "plop",
},
DepSets: map[string]descriptors.DepSet{},
})

// Should only find common services
assert.Len(t, nodes, 0)
Expand Down Expand Up @@ -311,3 +356,88 @@ func TestServiceTag(t *testing.T) {
})
}
}

func TestFindL2ServicesWithSupervisors(t *testing.T) {
// Create dependency sets as raw JSON
depSet1 := []byte(`{"dependencies":{"1111":{"activationTime":0,"chainIndex":"1111","historyMinTime":0}}}`)
depSet2 := []byte(`{"dependencies":{"2222":{"activationTime":0,"chainIndex":"2222","historyMinTime":0}}}`)

// Create test services with supervisors for different networks
services := inspect.ServiceMap{
// Network1 supervisor with depSet1
"op-supervisor-depset1": inspect.PortMap{
"rpc": {Port: 8080},
},
// Network2 supervisor with depSet2
"op-supervisor-depset2": inspect.PortMap{
"rpc": {Port: 8081},
},
// extra supervisor
"op-supervisor-depset3": inspect.PortMap{
"rpc": {Port: 8083},
},
}

// Create chain specs for the networks
n1 := ChainSpec{
ChainSpec: spec.ChainSpec{
Name: "network1",
NetworkID: "1111",
},
DepSets: map[string]descriptors.DepSet{
"depset1": depSet1,
},
}
n2 := ChainSpec{
ChainSpec: spec.ChainSpec{
Name: "network2",
NetworkID: "2222",
},
DepSets: map[string]descriptors.DepSet{
"depset2": depSet2,
},
}

finder := NewServiceFinder(
services,
WithL2Networks([]ChainSpec{n1, n2}),
)

tests := []struct {
name string
chainSpec ChainSpec
wantName string
wantPort int
}{
{
name: "network1 supervisor",
chainSpec: n1,
wantName: "op-supervisor-depset1",
wantPort: 8080,
},
{
name: "network2 supervisor",
chainSpec: n2,
wantName: "op-supervisor-depset2",
wantPort: 8081,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, serviceMap := finder.FindL2Services(tt.chainSpec)

// Debug output
t.Logf("Service map: %+v", serviceMap)
for k, v := range serviceMap {
t.Logf("Service %s: %+v", k, v)
}

// Verify supervisor services
assert.Len(t, serviceMap, 1) // just the supervisor service
assert.Contains(t, serviceMap, "supervisor")
assert.Equal(t, tt.wantName, serviceMap["supervisor"].Name)
assert.Equal(t, tt.wantPort, serviceMap["supervisor"].Endpoints["rpc"].Port)
})
}
}
Loading