diff --git a/devnet-sdk/system/chain.go b/devnet-sdk/system/chain.go index 14dfad31d3e3a..ef2d3e7f0570b 100644 --- a/devnet-sdk/system/chain.go +++ b/devnet-sdk/system/chain.go @@ -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 { @@ -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 @@ -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 diff --git a/devnet-sdk/system/chain_test.go b/devnet-sdk/system/chain_test.go index 5f9f7332e470a..998804fc62d46 100644 --- a/devnet-sdk/system/chain_test.go +++ b/devnet-sdk/system/chain_test.go @@ -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) { @@ -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) { @@ -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") + }) +}