Skip to content

feat(devnet-sdk): add chain descriptor validation #16523

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

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
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
85 changes: 83 additions & 2 deletions devnet-sdk/system/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,81 @@ func (c *chain) SupportsEIP(ctx context.Context, eip uint64) bool {
return c.nodes[0].SupportsEIP(ctx, eip)
}

// validateChainDescriptor validates a Chain descriptor for common issues
func validateChainDescriptor(d *descriptors.Chain) error {
if d == nil {
return fmt.Errorf("chain descriptor is nil")
}

if d.ID == "" {
return fmt.Errorf("chain ID is empty")
}

if len(d.Nodes) == 0 {
return fmt.Errorf("chain has no nodes")
}

for i, node := range d.Nodes {
if err := validateNodeDescriptor(&node, i); err != nil {
return fmt.Errorf("node %d validation failed: %w", i, err)
}
}

return nil
}

// validateNodeDescriptor validates a Node descriptor
func validateNodeDescriptor(node *descriptors.Node, index int) error {
if node == nil {
return fmt.Errorf("node descriptor is nil")
}

elService, exists := node.Services["el"]
if !exists {
return fmt.Errorf("node missing required 'el' service")
}

if elService == nil {
return fmt.Errorf("'el' service is nil")
}

rpcEndpoint, exists := elService.Endpoints["rpc"]
if !exists {
return fmt.Errorf("'el' service missing required 'rpc' endpoint")
}

if rpcEndpoint == nil {
return fmt.Errorf("'rpc' endpoint is nil")
}

if rpcEndpoint.Host == "" {
return fmt.Errorf("'rpc' endpoint host is empty")
}

if rpcEndpoint.Port <= 0 {
return fmt.Errorf("'rpc' endpoint port must be positive, got %d", rpcEndpoint.Port)
}

return nil
}

// validateL2ChainDescriptor validates an L2Chain descriptor
func validateL2ChainDescriptor(d *descriptors.L2Chain) error {
if d == nil {
return fmt.Errorf("L2 chain descriptor is nil")
}

if d.Chain == nil {
return fmt.Errorf("L2 chain descriptor missing embedded Chain")
}

if err := validateChainDescriptor(d.Chain); err != nil {
return fmt.Errorf("embedded chain validation failed: %w", err)
}

return nil
}

func checkHeader(ctx context.Context, client *sources.EthClient, check func(eth.BlockInfo) bool) bool {
info, err := client.InfoByLabel(ctx, eth.Unsafe)
if err != nil {
Expand All @@ -195,7 +270,10 @@ func newNodesFromDescriptor(d *descriptors.Chain) []Node {
}

func newChainFromDescriptor(d *descriptors.Chain) (*chain, error) {
// TODO: handle incorrect descriptors better. We could panic here.
// Validate descriptor before proceeding
if err := validateChainDescriptor(d); err != nil {
return nil, fmt.Errorf("invalid chain descriptor: %w", err)
}

nodes := newNodesFromDescriptor(d)
c := newChain(d.ID, nil, d.Config, AddressMap(d.Addresses), nodes) // Create chain first
Expand All @@ -221,7 +299,10 @@ func newChain(chainID string, wallets WalletMap, chainConfig *params.ChainConfig
}

func newL2ChainFromDescriptor(d *descriptors.L2Chain) (*l2Chain, error) {
// TODO: handle incorrect descriptors better. We could panic here.
// Validate descriptor before proceeding
if err := validateL2ChainDescriptor(d); err != nil {
return nil, fmt.Errorf("invalid L2 chain descriptor: %w", err)
}

nodes := newNodesFromDescriptor(d.Chain)
c := newL2Chain(d.ID, nil, nil, d.Config, AddressMap(d.L1Addresses), AddressMap(d.Addresses), nodes) // Create chain first
Expand Down
191 changes: 189 additions & 2 deletions devnet-sdk/system/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestChainFromDescriptor(t *testing.T) {
// Compare the underlying big.Int values
chainID := chain.ID()
expectedID := big.NewInt(1)
assert.Equal(t, 0, expectedID.Cmp(chainID))
assert.Equal(t, expectedID.Cmp(chainID), 0)
}

func TestL2ChainFromDescriptor(t *testing.T) {
Expand Down Expand Up @@ -130,7 +130,7 @@ func TestL2ChainFromDescriptor(t *testing.T) {
// Compare the underlying big.Int values
chainID := chain.ID()
expectedID := big.NewInt(1)
assert.Equal(t, 0, expectedID.Cmp(chainID))
assert.Equal(t, expectedID.Cmp(chainID), 0)
}

func TestChainWallet(t *testing.T) {
Expand Down Expand Up @@ -247,3 +247,190 @@ func TestContractsRegistry(t *testing.T) {
assert.Same(t, registry1, registry2)
})
}

// TestChainFromDescriptorValidation tests validation of invalid chain descriptors
func TestChainFromDescriptorValidation(t *testing.T) {
t.Run("nil descriptor", func(t *testing.T) {
_, err := newChainFromDescriptor(nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "chain descriptor is nil")
})

t.Run("empty chain ID", func(t *testing.T) {
descriptor := &descriptors.Chain{
ID: "",
Nodes: []descriptors.Node{
{
Services: descriptors.ServiceMap{
"el": &descriptors.Service{
Endpoints: descriptors.EndpointMap{
"rpc": &descriptors.PortInfo{
Host: "localhost",
Port: 8545,
},
},
},
},
},
},
}
_, err := newChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "chain ID is empty")
})

t.Run("no nodes", func(t *testing.T) {
descriptor := &descriptors.Chain{
ID: "1",
Nodes: []descriptors.Node{},
}
_, err := newChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "chain has no nodes")
})

t.Run("missing el service", func(t *testing.T) {
descriptor := &descriptors.Chain{
ID: "1",
Nodes: []descriptors.Node{
{
Services: descriptors.ServiceMap{},
},
},
}
_, err := newChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "node missing required 'el' service")
})

t.Run("nil el service", func(t *testing.T) {
descriptor := &descriptors.Chain{
ID: "1",
Nodes: []descriptors.Node{
{
Services: descriptors.ServiceMap{
"el": nil,
},
},
},
}
_, err := newChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "'el' service is nil")
})

t.Run("missing rpc endpoint", func(t *testing.T) {
descriptor := &descriptors.Chain{
ID: "1",
Nodes: []descriptors.Node{
{
Services: descriptors.ServiceMap{
"el": &descriptors.Service{
Endpoints: descriptors.EndpointMap{},
},
},
},
},
}
_, err := newChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "'el' service missing required 'rpc' endpoint")
})

t.Run("nil rpc endpoint", func(t *testing.T) {
descriptor := &descriptors.Chain{
ID: "1",
Nodes: []descriptors.Node{
{
Services: descriptors.ServiceMap{
"el": &descriptors.Service{
Endpoints: descriptors.EndpointMap{
"rpc": nil,
},
},
},
},
},
}
_, err := newChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "'rpc' endpoint is nil")
})

t.Run("empty host", func(t *testing.T) {
descriptor := &descriptors.Chain{
ID: "1",
Nodes: []descriptors.Node{
{
Services: descriptors.ServiceMap{
"el": &descriptors.Service{
Endpoints: descriptors.EndpointMap{
"rpc": &descriptors.PortInfo{
Host: "",
Port: 8545,
},
},
},
},
},
},
}
_, err := newChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "'rpc' endpoint host is empty")
})

t.Run("invalid port", func(t *testing.T) {
descriptor := &descriptors.Chain{
ID: "1",
Nodes: []descriptors.Node{
{
Services: descriptors.ServiceMap{
"el": &descriptors.Service{
Endpoints: descriptors.EndpointMap{
"rpc": &descriptors.PortInfo{
Host: "localhost",
Port: -1,
},
},
},
},
},
},
}
_, err := newChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "'rpc' endpoint port must be positive")
})
}

// TestL2ChainFromDescriptorValidation tests validation of invalid L2 chain descriptors
func TestL2ChainFromDescriptorValidation(t *testing.T) {
t.Run("nil descriptor", func(t *testing.T) {
_, err := newL2ChainFromDescriptor(nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "L2 chain descriptor is nil")
})

t.Run("nil embedded chain", func(t *testing.T) {
descriptor := &descriptors.L2Chain{
Chain: nil,
}
_, err := newL2ChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "L2 chain descriptor missing embedded Chain")
})

t.Run("invalid embedded chain", func(t *testing.T) {
descriptor := &descriptors.L2Chain{
Chain: &descriptors.Chain{
ID: "", // Invalid: empty ID
Nodes: []descriptors.Node{},
},
}
_, err := newL2ChainFromDescriptor(descriptor)
assert.Error(t, err)
assert.Contains(t, err.Error(), "embedded chain validation failed")
assert.Contains(t, err.Error(), "chain ID is empty")
})
}