package params import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "os" "path/filepath" "strings" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/discv5" "github.com/status-im/status-go/static" ) // errors var ( ErrMissingDataDir = errors.New("missing required 'DataDir' parameter") ErrMissingNetworkID = errors.New("missing required 'NetworkID' parameter") ErrEmptyPasswordFile = errors.New("password file cannot be empty") ErrNoPasswordFileValueSet = errors.New("password file path not set") ErrEmptyAuthorizationKeyFile = errors.New("authorization key file cannot be empty") ErrAuthorizationKeyFileNotSet = errors.New("authorization key file is not set") ) // ---------- // LightEthConfig // ---------- // LightEthConfig holds LES-related configuration // Status nodes are always lightweight clients (due to mobile platform constraints) type LightEthConfig struct { // Enabled flag specifies whether protocol is enabled Enabled bool // Genesis is JSON to seed the chain database with Genesis string // DatabaseCache is memory (in MBs) allocated to internal caching (min 16MB / database forced) DatabaseCache int } // ---------- // FirebaseConfig // ---------- // FirebaseConfig holds FCM-related configuration type FirebaseConfig struct { // AuthorizationKeyFile file path that contains FCM authorization key AuthorizationKeyFile string // NotificationTriggerURL URL used to send push notification requests to NotificationTriggerURL string } // ReadAuthorizationKeyFile reads and loads FCM authorization key func (c *FirebaseConfig) ReadAuthorizationKeyFile() ([]byte, error) { if len(c.AuthorizationKeyFile) == 0 { return nil, ErrAuthorizationKeyFileNotSet } key, err := ioutil.ReadFile(c.AuthorizationKeyFile) if err != nil { return nil, err } key = bytes.TrimRight(key, "\n") if len(key) == 0 { return nil, ErrEmptyAuthorizationKeyFile } return key, nil } // ---------- // WhisperConfig // ---------- // WhisperConfig holds SHH-related configuration type WhisperConfig struct { // Enabled flag specifies whether protocol is enabled Enabled bool // PasswordFile contains a password for symmetric encryption with MailServer. PasswordFile string // Password for symmetric encryption with MailServer. // (if no account file selected, then this password is used for symmetric encryption). Password string // LightClient should be true if the node should start with an empty bloom filter and not forward messages from other nodes LightClient bool // EnableMailServer is mode when node is capable of delivering expired messages on demand EnableMailServer bool // DataDir is the file system folder Whisper should use for any data storage needs. // For instance, MailServer will use this directory to store its data. DataDir string // MinimumPoW minimum PoW for Whisper messages MinimumPoW float64 // RateLimit minimum time between queries to mail server per peer MailServerRateLimit int // MailServerCleanupPeriod time in seconds to wait to run mail server cleanup MailServerCleanupPeriod int // TTL time to live for messages, in seconds TTL int // FirebaseConfig extra configuration for Firebase Cloud Messaging FirebaseConfig *FirebaseConfig `json:"FirebaseConfig,"` // EnableNTPSync enables NTP synchronizations EnableNTPSync bool } // ReadPasswordFile reads and returns content of the password file func (c *WhisperConfig) ReadPasswordFile() error { if len(c.PasswordFile) == 0 { return ErrNoPasswordFileValueSet } password, err := ioutil.ReadFile(c.PasswordFile) if err != nil { return err } password = bytes.TrimRight(password, "\n") if len(password) == 0 { return ErrEmptyPasswordFile } c.Password = string(password) return nil } // String dumps config object as nicely indented JSON func (c *WhisperConfig) String() string { data, _ := json.MarshalIndent(c, "", " ") // nolint: gas return string(data) } // ---------- // SwarmConfig // ---------- // SwarmConfig holds Swarm-related configuration type SwarmConfig struct { // Enabled flag specifies whether protocol is enabled Enabled bool } // String dumps config object as nicely indented JSON func (c *SwarmConfig) String() string { data, _ := json.MarshalIndent(c, "", " ") // nolint: gas return string(data) } // ---------- // ClusterConfig // ---------- // ClusterConfig holds configuration for supporting cluster peers, which is a temporary // means for mobile devices to get connected to Ethereum network (UDP-based discovery // may not be available, so we need means to discover the network manually). type ClusterConfig struct { // Enabled flag specifies whether feature is enabled Enabled bool // StaticNodes lists the static nodes taken from compiled or passed cluster.json StaticNodes []string // BootNodes list of cluster peer nodes for a given network (Mainnet, Ropsten, Rinkeby, Homestead), // for a given mode (production vs development) BootNodes []string } // String dumps config object as nicely indented JSON func (c *ClusterConfig) String() string { data, _ := json.MarshalIndent(c, "", " ") // nolint: gas return string(data) } // Limits represent min and max amount of peers type Limits struct { Min, Max int } // NewLimits creates new Limits config with given min and max values. func NewLimits(min, max int) Limits { return Limits{ Min: min, Max: max, } } // ---------- // UpstreamRPCConfig // ---------- // UpstreamRPCConfig stores configuration for upstream rpc connection. type UpstreamRPCConfig struct { // Enabled flag specifies whether feature is enabled Enabled bool // URL sets the rpc upstream host address for communication with // a non-local infura endpoint. URL string } // ---------- // NodeConfig // ---------- // NodeConfig stores configuration options for a node type NodeConfig struct { // NetworkID sets network to use for selecting peers to connect to NetworkID uint64 `json:"NetworkId" validate:"required"` // DataDir is the file system folder the node should use for any data storage needs. DataDir string `validate:"required"` // KeyStoreDir is the file system folder that contains private keys. // If KeyStoreDir is empty, the default location is the "keystore" subdirectory of DataDir. KeyStoreDir string // NodeKeyFile is a filename with node ID (private key) // This file should contain a valid secp256k1 private key that will be used for both // remote peer identification as well as network traffic encryption. NodeKeyFile string // NoDiscovery set to true will disable discovery protocol. NoDiscovery bool // ListenAddr is an IP address and port of this node (e.g. 127.0.0.1:30303). ListenAddr string // Name sets the instance name of the node. It must not contain the / character. Name string `validate:"excludes=/"` // Version exposes program's version. It is used in the devp2p node identifier. Version string // APIModules is a comma-separated list of API modules exposed via *any* (HTTP/WS/IPC) RPC interface. APIModules string // HTTPHost is the host interface on which to start the HTTP RPC server. // Pass empty string if no HTTP RPC interface needs to be started. HTTPHost string // RPCEnabled specifies whether the http RPC server is to be enabled by default. RPCEnabled bool // HTTPPort is the TCP port number on which to start the Geth's HTTP RPC server. HTTPPort int // IPCFile is filename of exposed IPC RPC Server IPCFile string // IPCEnabled specifies whether IPC-RPC Server is enabled or not IPCEnabled bool // TLSEnabled specifies whether TLS support should be enabled on node or not // TLS support is only planned in go-ethereum, so we are using our own patch. TLSEnabled bool // MaxPeers is the maximum number of (global) peers that can be connected. // Set to zero, if only static or trusted peers are allowed to connect. MaxPeers int // MaxPendingPeers is the maximum number of peers that can be pending in the // handshake phase, counted separately for inbound and outbound connections. MaxPendingPeers int log log.Logger // LogEnabled enables the logger LogEnabled bool `json:"LogEnabled"` // LogFile is filename where exposed logs get written to LogFile string // LogLevel defines minimum log level. Valid names are "ERROR", "WARN", "INFO", "DEBUG", and "TRACE". LogLevel string `validate:"eq=ERROR|eq=WARN|eq=INFO|eq=DEBUG|eq=TRACE"` // LogToStderr defines whether logged info should also be output to os.Stderr LogToStderr bool // UpstreamConfig extra config for providing upstream infura server. UpstreamConfig UpstreamRPCConfig `json:"UpstreamConfig"` // ClusterConfigFile contains the file name of the cluster configuration. If // empty the statical configuration data will be taken. ClusterConfigFile string `json:"ClusterConfigFile"` // ClusterConfig extra configuration for supporting cluster peers. ClusterConfig *ClusterConfig `json:"ClusterConfig," validate:"structonly"` // LightEthConfig extra configuration for LES LightEthConfig *LightEthConfig `json:"LightEthConfig," validate:"structonly"` // WhisperConfig extra configuration for SHH WhisperConfig *WhisperConfig `json:"WhisperConfig," validate:"structonly"` // SwarmConfig extra configuration for Swarm and ENS SwarmConfig *SwarmConfig `json:"SwarmConfig," validate:"structonly"` RegisterTopics []discv5.Topic `json:"RegisterTopics"` RequireTopics map[discv5.Topic]Limits `json:"RequireTopics"` // StatusServiceEnabled enables status service api StatusServiceEnabled bool } // NewNodeConfig creates new node configuration object func NewNodeConfig(dataDir string, clstrCfgFile string, networkID uint64) (*NodeConfig, error) { nodeConfig := &NodeConfig{ NetworkID: networkID, DataDir: dataDir, Name: ClientIdentifier, Version: Version, RPCEnabled: RPCEnabledDefault, HTTPHost: HTTPHost, HTTPPort: HTTPPort, ListenAddr: ListenAddr, APIModules: APIModules, MaxPeers: MaxPeers, MaxPendingPeers: MaxPendingPeers, IPCFile: IPCFile, log: log.New("package", "status-go/params.NodeConfig"), LogFile: LogFile, LogLevel: LogLevel, LogToStderr: LogToStderr, ClusterConfigFile: clstrCfgFile, ClusterConfig: &ClusterConfig{ Enabled: true, StaticNodes: []string{}, BootNodes: []string{}, }, LightEthConfig: &LightEthConfig{ Enabled: true, DatabaseCache: DatabaseCache, }, WhisperConfig: &WhisperConfig{ Enabled: true, MinimumPoW: WhisperMinimumPoW, TTL: WhisperTTL, FirebaseConfig: &FirebaseConfig{ NotificationTriggerURL: FirebaseNotificationTriggerURL, }, EnableNTPSync: true, }, SwarmConfig: &SwarmConfig{}, RegisterTopics: []discv5.Topic{}, RequireTopics: map[discv5.Topic]Limits{}, } // adjust dependent values if err := nodeConfig.updateConfig(); err != nil { return nil, err } return nodeConfig, nil } // LoadNodeConfig parses incoming JSON and returned it as Config func LoadNodeConfig(configJSON string) (*NodeConfig, error) { nodeConfig, err := loadNodeConfig(configJSON) if err != nil { return nil, err } if err := nodeConfig.Validate(); err != nil { return nil, err } return nodeConfig, nil } func loadNodeConfig(configJSON string) (*NodeConfig, error) { nodeConfig, err := NewNodeConfig("", "", 0) if err != nil { return nil, err } decoder := json.NewDecoder(strings.NewReader(configJSON)) // override default configuration with values by JSON input if err := decoder.Decode(&nodeConfig); err != nil { return nil, err } // repopulate if err := nodeConfig.updateConfig(); err != nil { return nil, err } return nodeConfig, nil } // Validate checks if NodeConfig fields have valid values. // // It returns nil if there are no errors, otherwise one or more errors // can be returned. Multiple errors are joined with a new line. // // A single error for a struct: // // type TestStruct struct { // TestField string `validate:"required"` // } // // has the following format: // // Key: 'TestStruct.TestField' Error:Field validation for 'TestField' failed on the 'required' tag // func (c *NodeConfig) Validate() error { validate := NewValidator() if err := validate.Struct(c); err != nil { return err } if c.ClusterConfig.Enabled { if err := validate.Struct(c.ClusterConfig); err != nil { return err } } if c.LightEthConfig.Enabled { if err := validate.Struct(c.LightEthConfig); err != nil { return err } } if c.WhisperConfig.Enabled { if err := validate.Struct(c.WhisperConfig); err != nil { return err } } if c.SwarmConfig.Enabled { if err := validate.Struct(c.SwarmConfig); err != nil { return err } } return nil } // Save dumps configuration to the disk func (c *NodeConfig) Save() error { data, err := json.MarshalIndent(c, "", " ") if err != nil { return err } if err := os.MkdirAll(c.DataDir, os.ModePerm); err != nil { return err } configFilePath := filepath.Join(c.DataDir, "config.json") if err := ioutil.WriteFile(configFilePath, data, os.ModePerm); err != nil { return err } c.log.Info("config file saved", "path", configFilePath) return nil } // updateConfig traverses configuration and adjusts dependent fields // (we have a development/production and mobile/full node dependent configurations) func (c *NodeConfig) updateConfig() error { // Update separate configurations. if err := c.updateGenesisConfig(); err != nil { return err } if err := c.updateUpstreamConfig(); err != nil { return err } if err := c.updateClusterConfig(); err != nil { return err } c.updatePeerLimits() return c.updateRelativeDirsConfig() } // updateGenesisConfig does necessary adjustments to config object (depending on network node will be running on) func (c *NodeConfig) updateGenesisConfig() error { var genesis *core.Genesis switch c.NetworkID { case MainNetworkID: genesis = core.DefaultGenesisBlock() case RopstenNetworkID: genesis = core.DefaultTestnetGenesisBlock() case RinkebyNetworkID: genesis = core.DefaultRinkebyGenesisBlock() case StatusChainNetworkID: var err error genesis, err = c.DefaultStatusChainGenesisBlock() if err != nil { return err } default: return nil } // encode the genesis into JSON enc, err := json.Marshal(genesis) if err != nil { return err } c.LightEthConfig.Genesis = string(enc) return nil } // DefaultStatusChainGenesisBlock returns the StatusChain network genesis block. func (c *NodeConfig) DefaultStatusChainGenesisBlock() (*core.Genesis, error) { genesisJSON, err := static.Asset("config/status-chain-genesis.json") if err != nil { return nil, fmt.Errorf("status-chain-genesis.json could not be loaded: %s", err) } var genesis *core.Genesis err = json.Unmarshal(genesisJSON, &genesis) if err != nil { return nil, fmt.Errorf("cannot unmarshal status-chain-genesis.json: %s", err) } return genesis, nil } // updateUpstreamConfig sets the proper UpstreamConfig.URL for the network id being used. func (c *NodeConfig) updateUpstreamConfig() error { // If we have a URL already set then keep URL incase // of custom server. if c.UpstreamConfig.URL != "" { return nil } switch c.NetworkID { case MainNetworkID: c.UpstreamConfig.URL = MainnetEthereumNetworkURL case RopstenNetworkID: c.UpstreamConfig.URL = RopstenEthereumNetworkURL case RinkebyNetworkID: c.UpstreamConfig.URL = RinkebyEthereumNetworkURL } return nil } // updateClusterConfig loads static peer nodes and CHT for a given network and mode. // This is necessary until we have LES protocol support CHT sync, and better node // discovery on mobile devices) func (c *NodeConfig) updateClusterConfig() error { if !c.ClusterConfig.Enabled { return nil } var clusters []cluster if c.ClusterConfigFile != "" { // Load cluster configuration from external file. configFile, err := ioutil.ReadFile(c.ClusterConfigFile) if err != nil { return fmt.Errorf("cluster configuration file '%s' could not be loaded: %s", c.ClusterConfigFile, err) } err = json.Unmarshal(configFile, &clusters) if err != nil { return fmt.Errorf("failed to unmarshal cluster configuration file: %s", err) } } else { clusters = defaultClusters } for _, cluster := range clusters { if cluster.NetworkID == int(c.NetworkID) { if len(c.ClusterConfig.BootNodes) == 0 { c.ClusterConfig.BootNodes = cluster.BootNodes } c.ClusterConfig.StaticNodes = cluster.StaticNodes // no point in running discovery if we don't have bootnodes. // but in case if we do have nodes and NoDiscovery=true we will preserve that value if len(cluster.BootNodes) == 0 { c.NoDiscovery = true } break } } return nil } // updateRelativeDirsConfig updates directories that should be wrt to DataDir func (c *NodeConfig) updateRelativeDirsConfig() error { makeSubDirPath := func(baseDir, subDir string) string { if len(baseDir) == 0 { return "" } return filepath.Join(baseDir, subDir) } if len(c.KeyStoreDir) == 0 { c.KeyStoreDir = makeSubDirPath(c.DataDir, KeyStoreDir) } if len(c.WhisperConfig.DataDir) == 0 { c.WhisperConfig.DataDir = makeSubDirPath(c.DataDir, WhisperDataDir) } return nil } // updatePeerLimits will set default peer limits expectations based on enabled services. func (c *NodeConfig) updatePeerLimits() { if c.NoDiscovery { return } if c.WhisperConfig.Enabled { c.RequireTopics[WhisperDiscv5Topic] = WhisperDiscv5Limits // TODO(dshulyak) register mailserver limits when we will change how they are handled. } } // String dumps config object as nicely indented JSON func (c *NodeConfig) String() string { data, _ := json.MarshalIndent(c, "", " ") return string(data) } // FormatAPIModules returns a slice of APIModules. func (c *NodeConfig) FormatAPIModules() []string { if len(c.APIModules) == 0 { return nil } return strings.Split(c.APIModules, ",") } // AddAPIModule adds a mobule to APIModules func (c *NodeConfig) AddAPIModule(m string) { c.APIModules = fmt.Sprintf("%s,%s", c.APIModules, m) }