diff --git a/cli/GENERATE_CONFIG.md b/cli/GENERATE_CONFIG.md new file mode 100644 index 0000000000..470b63d28a --- /dev/null +++ b/cli/GENERATE_CONFIG.md @@ -0,0 +1,160 @@ +# SSV Config Generator + +**SSV Config Generator** is a command-line tool designed to generate configuration file for SSV Node. It streamlines the setup process by allowing users to specify various parameters through flags, which are then compiled into a YAML configuration file. This tool ensures that the SSV network setup is consistent, customizable, and easy to manage. + +## Table of Contents + +- [Usage](#usage) + - [Flags](#flags) +- [Examples](#examples) +- [Configuration](#configuration) + +## Usage + +The `generate-config` command allows you to generate a YAML configuration file by specifying various parameters through command-line flags. + +### Syntax + +```bash + ssvnode generate-config [flags] +``` + +### Flags + +| Flag | Type | Default | Description | +|--------------------------------------|--------|-----------------------------------|----------------------------------------------------------------------------| +| `--output-path` | string | `./config/config.local.yaml` | Output path for the generated configuration file. | +| `--log-level` | string | `info` | Sets the logging level (e.g., `debug`, `info`, `warn`, `error`). | +| `--db-path` | string | `./data/db` | Path to the database directory. | +| `--discovery` | string | `mdns` | Discovery method. | +| `--consensus-client` | string | _Mandatory_ | Address of the consensus client (e.g., `http://localhost:9000`). | +| `--execution-client` | string | _Mandatory_ | Address of the execution client (e.g., `http://localhost:8545`). | +| `--operator-private-key` | string | | Secret key for the operator. | +| `--metrics-api-port` | int | `0` | Port number for the Metrics API (set to `0` to disable). | +| `--ssv-domain` | string | Derived from local testnet config | Hex-encoded domain type (prefixed with `0x`). | +| `--ssv-registry-sync-offset` | uint64 | Derived from local testnet config | Registry sync offset for the network. | +| `--ssv-registry-contract-addr` | string | Derived from local testnet config | Ethereum address of the network registry contract (e.g., `0xYourAddress`). | +| `--ssv-bootnodes` | string | Derived from local testnet config | Comma-separated list of network bootnodes. | +| `--ssv-discovery-protocol-id` | string | Derived from local testnet config | Hex-encoded discovery protocol ID (prefixed with `0x`). | +| `--ssv-alan-fork-epoch` | uint64 | Derived from local testnet config | Epoch at which the Alan fork occurs in the network. | +| `--ssv-max-validators-per-committee` | int | Derived from local testnet config | Max validators per committee. | + + +**Note:** The `--consensus-client` and `--execution-client` flags are mandatory and must be provided when running the CLI. + +## Examples + +### Basic Configuration + +Generate a configuration file with default settings, specifying only the mandatory flags: + +```bash +ssvnode generate-config \ + --consensus-client "http://localhost:9000" \ + --execution-client "http://localhost:8545" +``` + +This command generates a `config.local.yaml` file in the `./config` directory with default settings for all other parameters. + +### Custom Output Path and Log Level + +Generate a configuration file with a custom output path and set the log level to `debug`: + +```bash +ssvnode generate-config \ + --consensus-client "http://consensus.example.com:9000" \ + --execution-client "http://execution.example.com:8545" \ + --output-path "/etc/ssv/config.yaml" \ + --log-level "debug" +``` + +### Specify Operator Private Key and Enable Metrics API + +Generate a configuration with an operator's private key and enable the Metrics API on port `8080`: + +```bash +ssvnode generate-config \ + --consensus-client "http://consensus.example.com:9000" \ + --execution-client "http://execution.example.com:8545" \ + --operator-private-key "your-operator-private-key" \ + --metrics-api-port 8080 +``` + +### Advanced Network Configuration + +Customize network settings such as bootnodes, discovery protocol ID, fork epoch, etc: + +```bash +ssvnode generate-config \ + --consensus-client "http://consensus.example.com:9000" \ + --execution-client "http://execution.example.com:8545" \ + --ssv-domain "0x12345678" \ + --ssv-registry-sync-offset 50 \ + --ssv-registry-contract-addr "0xYourRegistryContractAddress" \ + --ssv-bootnodes "enode://bootnode1@127.0.0.1:30303,enode://bootnode2@127.0.0.1:30304" \ + --ssv-discovery-protocol-id "0x1234567890ab" \ + --ssv-max-validators-per-committee 560 +``` + +## Configuration + +The generated YAML configuration file (`config.local.yaml` by default) follows the structure defined by the `Config` struct. Below is an overview of each section and its corresponding fields: + +### YAML Structure + +```yaml +global: + LogLevel: info +db: + Path: ./data/db +eth2: + BeaconNodeAddr: http://localhost:9000 +eth1: + ETH1Addr: http://localhost:8545 +p2p: + Discovery: mdns +ssv: + Network: LocalTestnetSSV + CustomNetwork: + DomainType: "0x12345678" + RegistrySyncOffset: 0 + RegistryContractAddr: "0xYourRegistryContractAddress" + Bootnodes: + - "enode://bootnode1@127.0.0.1:30303" + - "enode://bootnode2@127.0.0.1:30304" + DiscoveryProtocolID: "0x1234567890ab" +OperatorPrivateKey: your-operator-private-key +MetricsAPIPort: 8080 +``` + +### Sections + +- **global** + - `LogLevel`: Specifies the logging level (e.g., `debug`, `info`, `warn`, `error`). + +- **db** + - `Path`: Path to the database directory. + +- **eth2** + - `BeaconNodeAddr`: Address of the consensus client (Beacon Node). + +- **eth1** + - `ETH1Addr`: Address of the execution client (ETH1 Node). + +- **p2p** + - `Discovery`: Peer-to-peer discovery method (e.g., `mdns`). + +- **ssv** + - `Network`: Name of the network. + - `CustomNetwork`: Contains custom network parameters. + - `DomainType`: Hex-encoded domain type (prefixed with `0x`). + - `RegistrySyncOffset`: Registry sync offset for the network. + - `RegistryContractAddr`: Ethereum address of the network registry contract. + - `Bootnodes`: List of network bootnodes. + - `DiscoveryProtocolID`: Hex-encoded discovery protocol ID (prefixed with `0x`). + +- **OperatorPrivateKey** + - `OperatorPrivateKey`: Secret key for the operator. + +- **MetricsAPIPort** + - `MetricsAPIPort`: Port number for the Metrics API. \ No newline at end of file diff --git a/cli/generate_config.go b/cli/generate_config.go new file mode 100644 index 0000000000..41de6e6d58 --- /dev/null +++ b/cli/generate_config.go @@ -0,0 +1,149 @@ +package cli + +import ( + "encoding/hex" + "fmt" + "log" + "math/big" + "os" + "strings" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" + spectypes "github.com/ssvlabs/ssv-spec/types" + "gopkg.in/yaml.v3" + + "github.com/ssvlabs/ssv/networkconfig" +) + +const ( + defaultOutputPath = "./config/config.local.yaml" + defaultLogLevel = "info" + defaultDBPath = "./data/db" + defaultDiscovery = "mdns" + sliceSeparator = "," + configFilePermissions = 0644 +) + +var ( + defaultNetwork = networkconfig.LocalTestnet +) + +var ( + outputPath string + logLevel string + dbPath string + discovery string + consensusClient string + executionClient string + operatorPrivateKey string + metricsAPIPort int + ssvDomain string + ssvRegistrySyncOffset uint64 + ssvRegistryContractAddr string + ssvBootnodes string + ssvDiscoveryProtocolID string +) + +type SSVConfig struct { + Global struct { + LogLevel string `yaml:"LogLevel,omitempty"` + } `yaml:"global,omitempty"` + DB struct { + Path string `yaml:"Path,omitempty"` + } `yaml:"db,omitempty"` + ConsensusClient struct { + Address string `yaml:"BeaconNodeAddr,omitempty"` + } `yaml:"eth2,omitempty"` + ExecutionClient struct { + Address string `yaml:"ETH1Addr,omitempty"` + } `yaml:"eth1,omitempty"` + P2P struct { + Discovery string `yaml:"Discovery,omitempty"` + } `yaml:"p2p,omitempty"` + SSV struct { + NetworkName string `yaml:"Network,omitempty" env:"NETWORK" env-description:"Network is the network of this node,omitempty"` + CustomNetwork *networkconfig.SSVConfig `yaml:"CustomNetwork,omitempty" env:"CUSTOM_NETWORK" env-description:"Custom network parameters,omitempty"` + } `yaml:"ssv,omitempty"` + OperatorPrivateKey string `yaml:"OperatorPrivateKey,omitempty"` + MetricsAPIPort int `yaml:"MetricsAPIPort,omitempty"` +} + +// generateConfigCmd is the command to generate ssv operator config. +var generateConfigCmd = &cobra.Command{ + Use: "generate-config", + Short: "generates ssv operator config", + Run: func(cmd *cobra.Command, args []string) { + parsedDomain, err := hex.DecodeString(strings.TrimPrefix(ssvDomain, "0x")) + if err != nil { + log.Fatalf("Failed to decode network domain: %v", err) + } + + parsedDiscoveryProtocolID, err := hex.DecodeString(strings.TrimPrefix(ssvDiscoveryProtocolID, "0x")) + if err != nil { + log.Fatalf("Failed to decode discovery protocol ID: %v", err) + } + + var parsedDiscoveryProtocolIDArr [6]byte + if len(parsedDiscoveryProtocolID) != 0 { + parsedDiscoveryProtocolIDArr = [6]byte(parsedDiscoveryProtocolID) + } + + var bootnodes []string + if ssvBootnodes != "" { + bootnodes = strings.Split(ssvBootnodes, sliceSeparator) + } + + var config SSVConfig + config.Global.LogLevel = logLevel + config.DB.Path = dbPath + config.ConsensusClient.Address = consensusClient + config.ExecutionClient.Address = executionClient + config.P2P.Discovery = discovery + config.OperatorPrivateKey = operatorPrivateKey + config.MetricsAPIPort = metricsAPIPort + config.SSV.CustomNetwork = &networkconfig.SSVConfig{ + DomainType: spectypes.DomainType(parsedDomain), + RegistrySyncOffset: new(big.Int).SetUint64(ssvRegistrySyncOffset), + RegistryContractAddr: ethcommon.HexToAddress(ssvRegistryContractAddr), + Bootnodes: bootnodes, + DiscoveryProtocolID: parsedDiscoveryProtocolIDArr, + } + + data, err := yaml.Marshal(&config) + if err != nil { + log.Fatalf("Failed to marshal YAML: %v", err) + } + + err = os.WriteFile(outputPath, data, configFilePermissions) + if err != nil { + log.Fatalf("Failed to write file: %v", err) + } + + log.Printf("Saved config into '%s':", outputPath) + fmt.Println(string(data)) + }, +} + +func init() { + generateConfigCmd.Flags().StringVarP(&outputPath, "output-path", "o", defaultOutputPath, "Output path for generated config") + generateConfigCmd.Flags().StringVar(&logLevel, "log-level", defaultLogLevel, "Log level") + generateConfigCmd.Flags().StringVar(&dbPath, "db-path", defaultDBPath, "DB path") + generateConfigCmd.Flags().StringVar(&discovery, "discovery", defaultDiscovery, "Discovery") + generateConfigCmd.Flags().StringVar(&consensusClient, "consensus-client", "", "Consensus client (required)") + _ = generateConfigCmd.MarkFlagRequired("consensus-client") + generateConfigCmd.Flags().StringVar(&executionClient, "execution-client", "", "Execution client (required)") + _ = generateConfigCmd.MarkFlagRequired("execution-client") + generateConfigCmd.Flags().StringVar(&operatorPrivateKey, "operator-private-key", "", "Secret key") + generateConfigCmd.Flags().IntVar(&metricsAPIPort, "metrics-api-port", 0, "Metrics API port") + + ssvDomainDefault := "0x" + hex.EncodeToString(defaultNetwork.DomainType[:]) + generateConfigCmd.Flags().StringVar(&ssvDomain, "ssv-domain", ssvDomainDefault, "SSV domain type") + generateConfigCmd.Flags().Uint64Var(&ssvRegistrySyncOffset, "ssv-registry-sync-offset", defaultNetwork.RegistrySyncOffset.Uint64(), "SSV registry sync offset") + generateConfigCmd.Flags().StringVar(&ssvRegistryContractAddr, "ssv-registry-contract-addr", defaultNetwork.RegistryContractAddr.String(), "SSV registry contract addr") + generateConfigCmd.Flags().StringVar(&ssvBootnodes, "ssv-bootnodes", strings.Join(defaultNetwork.Bootnodes, sliceSeparator), "SSV bootnodes (comma-separated)") + ssvDiscoveryProtocolIDDefault := "0x" + hex.EncodeToString(defaultNetwork.DiscoveryProtocolID[:]) + generateConfigCmd.Flags().StringVar(&ssvDiscoveryProtocolID, "ssv-discovery-protocol-id", ssvDiscoveryProtocolIDDefault, "SSV discovery protocol ID") + + RootCmd.AddCommand(generateConfigCmd) +} diff --git a/cli/operator/node.go b/cli/operator/node.go index 28cb923f5d..a08f4626f2 100644 --- a/cli/operator/node.go +++ b/cli/operator/node.go @@ -16,7 +16,6 @@ import ( "time" "github.com/attestantio/go-eth2-client/spec/phase0" - ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ilyakaznacheev/cleanenv" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -287,7 +286,7 @@ var StartNodeCmd = &cobra.Command{ ec, err := executionclient.New( cmd.Context(), executionAddrList[0], - ethcommon.HexToAddress(ssvNetworkConfig.RegistryContractAddr), + ssvNetworkConfig.RegistryContractAddr, executionclient.WithLogger(logger), executionclient.WithFollowDistance(executionclient.DefaultFollowDistance), executionclient.WithConnectionTimeout(cfg.ExecutionClient.ConnectionTimeout), @@ -305,7 +304,7 @@ var StartNodeCmd = &cobra.Command{ ec, err := executionclient.NewMulti( cmd.Context(), executionAddrList, - ethcommon.HexToAddress(ssvNetworkConfig.RegistryContractAddr), + ssvNetworkConfig.RegistryContractAddr, executionclient.WithLoggerMulti(logger), executionclient.WithFollowDistanceMulti(executionclient.DefaultFollowDistance), executionclient.WithConnectionTimeoutMulti(cfg.ExecutionClient.ConnectionTimeout), @@ -714,7 +713,6 @@ func validateConfig(nodeStorage operatorstorage.Storage, networkName string, usi return fmt.Errorf("incompatible config change: %w", err) } } else { - if err := nodeStorage.SaveConfig(nil, currentConfig); err != nil { return fmt.Errorf("failed to store config: %w", err) } @@ -902,9 +900,20 @@ func ensureOperatorPubKey(nodeStorage operatorstorage.Storage, operatorPubKeyBas } func setupSSVNetwork(logger *zap.Logger) (networkconfig.SSVConfig, error) { - ssvConfig, err := networkconfig.GetSSVConfigByName(cfg.SSVOptions.NetworkName) - if err != nil { - return networkconfig.SSVConfig{}, err + var ssvConfig networkconfig.SSVConfig + + if cfg.SSVOptions.CustomNetwork != nil { + ssvConfig = *cfg.SSVOptions.CustomNetwork + logger.Info("using custom network config") + } else if cfg.SSVOptions.NetworkName != "" { + snc, err := networkconfig.GetSSVConfigByName(cfg.SSVOptions.NetworkName) + if err != nil { + return ssvConfig, err + } + ssvConfig = snc + logger.Info("found network config by name", + zap.String("name", cfg.SSVOptions.NetworkName), + ) } if cfg.SSVOptions.CustomDomainType != "" { @@ -923,7 +932,7 @@ func setupSSVNetwork(logger *zap.Logger) (networkconfig.SSVConfig, error) { postForkDomain := binary.BigEndian.Uint32(domainBytes) + 1 binary.BigEndian.PutUint32(ssvConfig.DomainType[:], postForkDomain) - logger.Info("running with custom domain type", + logger.Warn("running with custom domain type; it's deprecated, consider using custom network instead", fields.Domain(ssvConfig.DomainType), ) } @@ -936,7 +945,7 @@ func setupSSVNetwork(logger *zap.Logger) (networkconfig.SSVConfig, error) { logger.Info("setting ssv network", zap.Any("config", ssvConfig), zap.String("nodeType", nodeType), - zap.String("registryContract", ssvConfig.RegistryContractAddr), + zap.String("registryContract", ssvConfig.RegistryContractAddr.String()), ) return ssvConfig, nil diff --git a/networkconfig/holesky-e2e.go b/networkconfig/holesky-e2e.go index f464cc13cc..a879cca299 100644 --- a/networkconfig/holesky-e2e.go +++ b/networkconfig/holesky-e2e.go @@ -4,6 +4,7 @@ import ( "math/big" "time" + ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -18,7 +19,7 @@ var HoleskyE2E = NetworkConfig{ }, SSVConfig: SSVConfig{ DomainType: spectypes.DomainType{0x0, 0x0, 0xee, 0x1}, - RegistryContractAddr: "0x58410bef803ecd7e63b23664c586a6db72daf59c", + RegistryContractAddr: ethcommon.HexToAddress("0x58410bef803ecd7e63b23664c586a6db72daf59c"), RegistrySyncOffset: big.NewInt(405579), Bootnodes: []string{}, }, diff --git a/networkconfig/holesky-stage.go b/networkconfig/holesky-stage.go index 97aa69c291..a28cc3cf72 100644 --- a/networkconfig/holesky-stage.go +++ b/networkconfig/holesky-stage.go @@ -4,6 +4,7 @@ import ( "math/big" "time" + ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -19,7 +20,7 @@ var HoleskyStage = NetworkConfig{ SSVConfig: SSVConfig{ DomainType: [4]byte{0x00, 0x00, 0x31, 0x13}, RegistrySyncOffset: new(big.Int).SetInt64(84599), - RegistryContractAddr: "0x0d33801785340072C452b994496B19f196b7eE15", + RegistryContractAddr: ethcommon.HexToAddress("0x0d33801785340072C452b994496B19f196b7eE15"), DiscoveryProtocolID: [6]byte{'s', 's', 'v', 'd', 'v', '5'}, Bootnodes: []string{ // Public bootnode: diff --git a/networkconfig/holesky.go b/networkconfig/holesky.go index 4629c7ce34..801d9c3cad 100644 --- a/networkconfig/holesky.go +++ b/networkconfig/holesky.go @@ -4,6 +4,7 @@ import ( "math/big" "time" + ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -19,7 +20,7 @@ var Holesky = NetworkConfig{ SSVConfig: SSVConfig{ DomainType: spectypes.DomainType{0x0, 0x0, 0x5, 0x2}, RegistrySyncOffset: new(big.Int).SetInt64(181612), - RegistryContractAddr: "0x38A4794cCEd47d3baf7370CcC43B560D3a1beEFA", + RegistryContractAddr: ethcommon.HexToAddress("0x38A4794cCEd47d3baf7370CcC43B560D3a1beEFA"), DiscoveryProtocolID: [6]byte{'s', 's', 'v', 'd', 'v', '5'}, Bootnodes: []string{ // SSV Labs diff --git a/networkconfig/hoodi-stage.go b/networkconfig/hoodi-stage.go index e28511f599..42ffc5d917 100644 --- a/networkconfig/hoodi-stage.go +++ b/networkconfig/hoodi-stage.go @@ -4,6 +4,7 @@ import ( "math/big" "time" + ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -19,7 +20,7 @@ var HoodiStage = NetworkConfig{ SSVConfig: SSVConfig{ DomainType: [4]byte{0x00, 0x00, 0x31, 0x14}, RegistrySyncOffset: new(big.Int).SetInt64(1004), - RegistryContractAddr: "0x0aaace4e8affc47c6834171c88d342a4abd8f105", + RegistryContractAddr: ethcommon.HexToAddress("0x0aaace4e8affc47c6834171c88d342a4abd8f105"), DiscoveryProtocolID: [6]byte{'s', 's', 'v', 'd', 'v', '5'}, Bootnodes: []string{ // SSV Labs diff --git a/networkconfig/hoodi.go b/networkconfig/hoodi.go index 03799d694b..78f0263743 100644 --- a/networkconfig/hoodi.go +++ b/networkconfig/hoodi.go @@ -4,6 +4,7 @@ import ( "math/big" "time" + ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -19,7 +20,7 @@ var Hoodi = NetworkConfig{ SSVConfig: SSVConfig{ DomainType: spectypes.DomainType{0x0, 0x0, 0x5, 0x3}, RegistrySyncOffset: new(big.Int).SetInt64(1065), - RegistryContractAddr: "0x58410Bef803ECd7E63B23664C586A6DB72DAf59c", + RegistryContractAddr: ethcommon.HexToAddress("0x58410Bef803ECd7E63B23664C586A6DB72DAf59c"), DiscoveryProtocolID: [6]byte{'s', 's', 'v', 'd', 'v', '5'}, Bootnodes: []string{ // SSV Labs diff --git a/networkconfig/local-testnet.go b/networkconfig/local-testnet.go index bd94686743..eb998639f9 100644 --- a/networkconfig/local-testnet.go +++ b/networkconfig/local-testnet.go @@ -1,8 +1,10 @@ package networkconfig import ( + "math/big" "time" + ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -17,9 +19,11 @@ var LocalTestnet = NetworkConfig{ }, SSVConfig: SSVConfig{ DomainType: spectypes.DomainType{0x0, 0x0, spectypes.JatoV2NetworkID.Byte(), 0x2}, - RegistryContractAddr: "0xC3CD9A0aE89Fff83b71b58b6512D43F8a41f363D", + RegistrySyncOffset: new(big.Int).SetInt64(0), + RegistryContractAddr: ethcommon.HexToAddress("0xC3CD9A0aE89Fff83b71b58b6512D43F8a41f363D"), Bootnodes: []string{ "enr:-Li4QLR4Y1VbwiqFYKy6m-WFHRNDjhMDZ_qJwIABu2PY9BHjIYwCKpTvvkVmZhu43Q6zVA29sEUhtz10rQjDJkK3Hd-GAYiGrW2Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhCLdu_SJc2VjcDI1NmsxoQJTcI7GHPw-ZqIflPZYYDK_guurp_gsAFF5Erns3-PAvIN0Y3CCE4mDdWRwgg-h", }, + DiscoveryProtocolID: [6]byte{'s', 's', 'v', 'd', 'v', '5'}, }, } diff --git a/networkconfig/mainnet.go b/networkconfig/mainnet.go index d82490ce27..1e475cd140 100644 --- a/networkconfig/mainnet.go +++ b/networkconfig/mainnet.go @@ -4,6 +4,7 @@ import ( "math/big" "time" + ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -19,7 +20,7 @@ var Mainnet = NetworkConfig{ SSVConfig: SSVConfig{ DomainType: spectypes.AlanMainnet, RegistrySyncOffset: new(big.Int).SetInt64(17507487), - RegistryContractAddr: "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + RegistryContractAddr: ethcommon.HexToAddress("0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1"), DiscoveryProtocolID: [6]byte{'s', 's', 'v', 'd', 'v', '5'}, Bootnodes: []string{ // SSV Labs diff --git a/networkconfig/sepolia.go b/networkconfig/sepolia.go index 49c0066177..02ea111d2e 100644 --- a/networkconfig/sepolia.go +++ b/networkconfig/sepolia.go @@ -4,6 +4,7 @@ import ( "math/big" "time" + ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -19,7 +20,7 @@ var Sepolia = NetworkConfig{ SSVConfig: SSVConfig{ DomainType: spectypes.DomainType{0x0, 0x0, 0x5, 0x69}, RegistrySyncOffset: new(big.Int).SetInt64(7795814), - RegistryContractAddr: "0x261419B48F36EdF420743E9f91bABF4856e76f99", + RegistryContractAddr: ethcommon.HexToAddress("0x261419B48F36EdF420743E9f91bABF4856e76f99"), DiscoveryProtocolID: [6]byte{'s', 's', 'v', 'd', 'v', '5'}, Bootnodes: []string{ // SSV Labs diff --git a/networkconfig/ssv.go b/networkconfig/ssv.go index 5ca0ba4a32..61d3d18959 100644 --- a/networkconfig/ssv.go +++ b/networkconfig/ssv.go @@ -5,6 +5,8 @@ import ( "fmt" "math/big" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -36,7 +38,7 @@ type SSV interface { type SSVConfig struct { DomainType spectypes.DomainType RegistrySyncOffset *big.Int - RegistryContractAddr string // TODO: ethcommon.Address + RegistryContractAddr ethcommon.Address Bootnodes []string DiscoveryProtocolID [6]byte } @@ -50,6 +52,74 @@ func (s SSVConfig) String() string { return string(marshaled) } +type marshaledConfig struct { + DomainType hexutil.Bytes `json:"DomainType,omitempty" yaml:"DomainType,omitempty"` + RegistrySyncOffset *big.Int `json:"RegistrySyncOffset,omitempty" yaml:"RegistrySyncOffset,omitempty"` + RegistryContractAddr ethcommon.Address `json:"RegistryContractAddr,omitempty" yaml:"RegistryContractAddr,omitempty"` + Bootnodes []string `json:"Bootnodes,omitempty" yaml:"Bootnodes,omitempty"` + DiscoveryProtocolID hexutil.Bytes `json:"DiscoveryProtocolID,omitempty" yaml:"DiscoveryProtocolID,omitempty"` +} + +// Helper method to avoid duplication between MarshalJSON and MarshalYAML +func (s SSVConfig) marshal() marshaledConfig { + aux := marshaledConfig{ + DomainType: s.DomainType[:], + RegistrySyncOffset: s.RegistrySyncOffset, + RegistryContractAddr: s.RegistryContractAddr, + Bootnodes: s.Bootnodes, + DiscoveryProtocolID: s.DiscoveryProtocolID[:], + } + + return aux +} + +func (s SSVConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(s.marshal()) +} + +func (s SSVConfig) MarshalYAML() (interface{}, error) { + return s.marshal(), nil +} + +// Helper method to avoid duplication between UnmarshalJSON and UnmarshalYAML +func (s *SSVConfig) unmarshalFromConfig(aux marshaledConfig) error { + if len(aux.DomainType) != 4 { + return fmt.Errorf("invalid domain type length: expected 4 bytes, got %d", len(aux.DomainType)) + } + + if len(aux.DiscoveryProtocolID) != 6 { + return fmt.Errorf("invalid discovery protocol ID length: expected 6 bytes, got %d", len(aux.DiscoveryProtocolID)) + } + + *s = SSVConfig{ + DomainType: spectypes.DomainType(aux.DomainType), + RegistrySyncOffset: aux.RegistrySyncOffset, + RegistryContractAddr: aux.RegistryContractAddr, + Bootnodes: aux.Bootnodes, + DiscoveryProtocolID: [6]byte(aux.DiscoveryProtocolID), + } + + return nil +} + +func (s *SSVConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + var aux marshaledConfig + if err := unmarshal(&aux); err != nil { + return err + } + + return s.unmarshalFromConfig(aux) +} + +func (s *SSVConfig) UnmarshalJSON(data []byte) error { + var aux marshaledConfig + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + return s.unmarshalFromConfig(aux) +} + func (s SSVConfig) GetDomainType() spectypes.DomainType { return s.DomainType } diff --git a/networkconfig/ssv_test.go b/networkconfig/ssv_test.go new file mode 100644 index 0000000000..8872d6b4a3 --- /dev/null +++ b/networkconfig/ssv_test.go @@ -0,0 +1,232 @@ +package networkconfig + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "math/big" + "reflect" + "sort" + "testing" + + ethcommon "github.com/ethereum/go-ethereum/common" + spectypes "github.com/ssvlabs/ssv-spec/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestSSVConfig_MarshalUnmarshalJSON(t *testing.T) { + // Create a sample SSVConfig + originalConfig := SSVConfig{ + DomainType: spectypes.DomainType{0x01, 0x02, 0x03, 0x04}, + RegistrySyncOffset: big.NewInt(123456), + RegistryContractAddr: ethcommon.HexToAddress("0x123456789abcdef0123456789abcdef012345678"), + Bootnodes: []string{"bootnode1", "bootnode2"}, + DiscoveryProtocolID: [6]byte{0x05, 0x06, 0x07, 0x08, 0x09, 0x0a}, + } + + // Marshal to JSON + jsonBytes, err := json.Marshal(originalConfig) + require.NoError(t, err) + + // Unmarshal from JSON + var unmarshaledConfig SSVConfig + err = json.Unmarshal(jsonBytes, &unmarshaledConfig) + require.NoError(t, err) + + // Marshal again after unmarshaling + remarshaledBytes, err := json.Marshal(unmarshaledConfig) + require.NoError(t, err) + + // Compare the original and remarshaled JSON bytes + assert.JSONEq(t, string(jsonBytes), string(remarshaledBytes)) + + // Compare the original and unmarshaled structs + assert.Equal(t, originalConfig.DomainType, unmarshaledConfig.DomainType) + assert.Equal(t, originalConfig.RegistrySyncOffset.Int64(), unmarshaledConfig.RegistrySyncOffset.Int64()) + assert.Equal(t, originalConfig.RegistryContractAddr, unmarshaledConfig.RegistryContractAddr) + assert.Equal(t, originalConfig.Bootnodes, unmarshaledConfig.Bootnodes) + assert.Equal(t, originalConfig.DiscoveryProtocolID, unmarshaledConfig.DiscoveryProtocolID) +} + +func TestSSVConfig_MarshalUnmarshalYAML(t *testing.T) { + // Create a sample SSVConfig + originalConfig := SSVConfig{ + DomainType: spectypes.DomainType{0x01, 0x02, 0x03, 0x04}, + RegistrySyncOffset: big.NewInt(123456), + RegistryContractAddr: ethcommon.HexToAddress("0x123456789abcdef0123456789abcdef012345678"), + Bootnodes: []string{"bootnode1", "bootnode2"}, + DiscoveryProtocolID: [6]byte{0x05, 0x06, 0x07, 0x08, 0x09, 0x0a}, + } + + // Marshal to YAML + yamlBytes, err := yaml.Marshal(originalConfig) + require.NoError(t, err) + + // Unmarshal from YAML + var unmarshaledConfig SSVConfig + err = yaml.Unmarshal(yamlBytes, &unmarshaledConfig) + require.NoError(t, err) + + // Marshal again after unmarshaling + remarshaledBytes, err := yaml.Marshal(unmarshaledConfig) + require.NoError(t, err) + + // Compare the original and unmarshaled structs + assert.Equal(t, originalConfig.DomainType, unmarshaledConfig.DomainType) + assert.Equal(t, originalConfig.RegistrySyncOffset.Int64(), unmarshaledConfig.RegistrySyncOffset.Int64()) + assert.Equal(t, originalConfig.RegistryContractAddr, unmarshaledConfig.RegistryContractAddr) + assert.Equal(t, originalConfig.Bootnodes, unmarshaledConfig.Bootnodes) + assert.Equal(t, originalConfig.DiscoveryProtocolID, unmarshaledConfig.DiscoveryProtocolID) + + // Compare the original and remarshaled YAML bytes + // YAML doesn't preserve order by default, so we need to compare the unmarshaled content + var originalYAMLMap map[string]interface{} + var remarshaledYAMLMap map[string]interface{} + + err = yaml.Unmarshal(yamlBytes, &originalYAMLMap) + require.NoError(t, err) + + err = yaml.Unmarshal(remarshaledBytes, &remarshaledYAMLMap) + require.NoError(t, err) + + assert.Equal(t, originalYAMLMap, remarshaledYAMLMap) +} + +// hashStructJSON creates a deterministic hash of a struct by marshaling to sorted JSON +func hashStructJSON(v interface{}) (string, error) { + // Create a JSON encoder that sorts map keys + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", "") + encoder.SetEscapeHTML(false) + + if err := encoder.Encode(v); err != nil { + return "", err + } + + // Compute SHA-256 hash + hasher := sha256.New() + hasher.Write(buffer.Bytes()) + return hex.EncodeToString(hasher.Sum(nil)), nil +} + +// TestFieldPreservation ensures that all fields are properly preserved during +// marshal/unmarshal operations and that we can detect changes to the struct +func TestFieldPreservation(t *testing.T) { + t.Run("test all fields are present after marshaling", func(t *testing.T) { + // Get all field names from SSVConfig + configType := reflect.TypeOf(SSVConfig{}) + marshaledType := reflect.TypeOf(marshaledConfig{}) + + var configFields, marshaledFields []string + + for i := 0; i < configType.NumField(); i++ { + configFields = append(configFields, configType.Field(i).Name) + } + + for i := 0; i < marshaledType.NumField(); i++ { + marshaledFields = append(marshaledFields, marshaledType.Field(i).Name) + } + + // Sort fields for deterministic comparison + sort.Strings(configFields) + sort.Strings(marshaledFields) + + // Ensure the same fields exist in both structs + assert.Equal(t, configFields, marshaledFields, "SSVConfig and marshaledConfig should have the same fields") + }) + + t.Run("hash comparison JSON", func(t *testing.T) { + // Create a sample config + config := SSVConfig{ + DomainType: spectypes.DomainType{0x01, 0x02, 0x03, 0x04}, + RegistrySyncOffset: big.NewInt(123456), + RegistryContractAddr: ethcommon.HexToAddress("0x123456789abcdef0123456789abcdef012345678"), + Bootnodes: []string{"bootnode1", "bootnode2"}, + DiscoveryProtocolID: [6]byte{0x05, 0x06, 0x07, 0x08, 0x09, 0x0a}, + } + + // Marshal and unmarshal to test preservation + jsonBytes, err := json.Marshal(config) + require.NoError(t, err) + + var unmarshaled SSVConfig + err = json.Unmarshal(jsonBytes, &unmarshaled) + require.NoError(t, err) + + // Hash the original and unmarshaled struct + originalHash, err := hashStructJSON(config) + require.NoError(t, err) + + unmarshaledHash, err := hashStructJSON(unmarshaled) + require.NoError(t, err) + + // The hashes should match if all fields are preserved + assert.Equal(t, originalHash, unmarshaledHash, "Hash mismatch indicates fields weren't properly preserved in JSON") + + // Store the expected hash - this will fail if a new field is added without updating the tests + expectedJSONHash := "3afe88f355185266dfd842df18a096ea8f40dd28f8b022710aedca1d09d59cef" + assert.Equal(t, expectedJSONHash, originalHash, + "Hash has changed. If you've added a new field, please update the expected hash in this test.") + }) + + t.Run("hash comparison YAML", func(t *testing.T) { + // Create a sample config + config := SSVConfig{ + DomainType: spectypes.DomainType{0x01, 0x02, 0x03, 0x04}, + RegistrySyncOffset: big.NewInt(123456), + RegistryContractAddr: ethcommon.HexToAddress("0x123456789abcdef0123456789abcdef012345678"), + Bootnodes: []string{"bootnode1", "bootnode2"}, + DiscoveryProtocolID: [6]byte{0x05, 0x06, 0x07, 0x08, 0x09, 0x0a}, + } + + // Marshal and unmarshal to test preservation + yamlBytes, err := yaml.Marshal(config) + require.NoError(t, err) + + var unmarshaled SSVConfig + err = yaml.Unmarshal(yamlBytes, &unmarshaled) + require.NoError(t, err) + + // For YAML, convert to JSON for consistent hashing + originalHash, err := hashStructJSON(config) + require.NoError(t, err) + + unmarshaledHash, err := hashStructJSON(unmarshaled) + require.NoError(t, err) + + // The hashes should match if all fields are preserved + assert.Equal(t, originalHash, unmarshaledHash, "Hash mismatch indicates fields weren't properly preserved in YAML") + }) +} + +// TestExistingNetworkConfigs validates that all predefined network configs +// can be marshaled and unmarshaled correctly +func TestExistingNetworkConfigs(t *testing.T) { + for networkName, config := range supportedSSVConfigs { + t.Run(networkName, func(t *testing.T) { + // JSON test + jsonBytes, err := json.Marshal(config) + require.NoError(t, err) + + var jsonUnmarshaled SSVConfig + err = json.Unmarshal(jsonBytes, &jsonUnmarshaled) + require.NoError(t, err) + + assert.Equal(t, config.DomainType, jsonUnmarshaled.DomainType) + + // YAML test + yamlBytes, err := yaml.Marshal(config) + require.NoError(t, err) + + var yamlUnmarshaled SSVConfig + err = yaml.Unmarshal(yamlBytes, &yamlUnmarshaled) + require.NoError(t, err) + + assert.Equal(t, config.DomainType, yamlUnmarshaled.DomainType) + }) + } +} diff --git a/networkconfig/test-network.go b/networkconfig/test-network.go index 9a3ee5ceff..34e6e6fac2 100644 --- a/networkconfig/test-network.go +++ b/networkconfig/test-network.go @@ -4,6 +4,7 @@ import ( "math/big" "time" + ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" ) @@ -19,7 +20,7 @@ var TestNetwork = NetworkConfig{ SSVConfig: SSVConfig{ DomainType: spectypes.DomainType{0x0, 0x0, spectypes.JatoNetworkID.Byte(), 0x2}, RegistrySyncOffset: new(big.Int).SetInt64(9015219), - RegistryContractAddr: "0x4B133c68A084B8A88f72eDCd7944B69c8D545f03", + RegistryContractAddr: ethcommon.HexToAddress("0x4B133c68A084B8A88f72eDCd7944B69c8D545f03"), Bootnodes: []string{ "enr:-Li4QFIQzamdvTxGJhvcXG_DFmCeyggSffDnllY5DiU47pd_K_1MRnSaJimWtfKJ-MD46jUX9TwgW5Jqe0t4pH41RYWGAYuFnlyth2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhCLdu_SJc2VjcDI1NmsxoQN4v-N9zFYwEqzGPBBX37q24QPFvAVUtokIo1fblIsmTIN0Y3CCE4uDdWRwgg-j", }, diff --git a/operator/node.go b/operator/node.go index 0fcf7f1225..278e812c73 100644 --- a/operator/node.go +++ b/operator/node.go @@ -26,9 +26,9 @@ import ( // Options contains options to create the node type Options struct { - // NetworkName is the network name of this node - NetworkName string `yaml:"Network" env:"NETWORK" env-default:"mainnet" env-description:"Ethereum network to connect to (mainnet, holesky, sepolia, etc.)"` - CustomDomainType string `yaml:"CustomDomainType" env:"CUSTOM_DOMAIN_TYPE" env-default:"" env-description:"Override SSV domain type for network isolation. Warning: Please modify only if you are certain of the implications. This would be incremented by 1 after Alan fork (e.g., 0x01020304 → 0x01020305 post-fork)"` + NetworkName string `yaml:"Network" env:"NETWORK" env-default:"mainnet" env-description:"Ethereum network to connect to (mainnet, holesky, sepolia, etc.). For backwards compatibility it's ignored if CustomNetwork is set"` + CustomNetwork *networkconfig.SSVConfig `yaml:"CustomNetwork" env:"CUSTOM_NETWORK" env-description:"Custom SSV network configuration"` + CustomDomainType string `yaml:"CustomDomainType" env:"CUSTOM_DOMAIN_TYPE" env-default:"" env-description:"Override SSV domain type for network isolation. Warning: Please modify only if you are certain of the implications. This would be incremented by 1 after Alan fork (e.g., 0x01020304 → 0x01020305 post-fork)"` // DEPRECATED: use CustomNetwork instead. NetworkConfig networkconfig.NetworkConfig BeaconNode beaconprotocol.BeaconNode // TODO: consider renaming to ConsensusClient ExecutionClient executionclient.Provider