package params import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/url" "os" "path/filepath" "strings" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/discv5" "github.com/ethereum/go-ethereum/params" "github.com/status-im/status-go/static" whisper "github.com/status-im/whisper/whisperv6" validator "gopkg.in/go-playground/validator.v9" ) // ---------- // 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 // DatabaseCache is memory (in MBs) allocated to internal caching (min 16MB / database forced) DatabaseCache int // TrustedNodes is a list of trusted servers TrustedNodes []string //MinTrustedFraction is minimum percentage of connected trusted servers to validate header(1-100) MinTrustedFraction int } // ---------- // WhisperConfig // ---------- // WhisperConfig holds SHH-related configuration type WhisperConfig struct { // Enabled flag specifies whether protocol is enabled Enabled bool // 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 // MailServerPassword for symmetric encryption of whisper message history requests. // (if no account file selected, then this password is used for symmetric encryption). MailServerPassword string // MailServerAsymKey is an hex-encoded asymmetric key to decrypt messages sent to MailServer. MailServerAsymKey string // MailServerRateLimit minimum time between queries to mail server per peer. MailServerRateLimit int // MailServerDataRetention is a number of days data should be stored by MailServer. MailServerDataRetention int // TTL time to live for messages, in seconds TTL int // EnableNTPSync enables NTP synchronizations EnableNTPSync bool // MaxMessageSize is a maximum size of a devp2p packet handled by the Whisper protocol, // not only the size of envelopes sent in that packet. MaxMessageSize uint32 // DatabaseConfig is configuration for which datastore we use DatabaseConfig DatabaseConfig } type DatabaseConfig struct { PGConfig PGConfig } type PGConfig struct { // Enabled whether we should use a posgres instance Enabled bool // The URI of the server URI string } // String dumps config object as nicely indented JSON func (c *WhisperConfig) String() string { data, _ := json.MarshalIndent(c, "", " ") // nolint: gas return string(data) } // IncentivisationConfig holds incentivisation-related configuration type IncentivisationConfig struct { // Enabled flag specifies whether protocol is enabled Enabled bool `validate:"required"` // Endpoint for the RPC calls RPCEndpoint string `validate:"required"` // Contract address ContractAddress string `validate:"required"` // IP address that is used IP string `validate:"required"` // Port Port uint16 `validate:"required"` } // String dumps config object as nicely indented JSON func (c *IncentivisationConfig) String() string { data, _ := json.MarshalIndent(c, "", " ") // nolint: gas return string(data) } // Validate validates the IncentivisationConfig struct and returns an error if inconsistent values are found func (c *IncentivisationConfig) Validate(validate *validator.Validate) error { if err := validate.Struct(c); err != nil { return err } return nil } // ---------- // 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 that nodes in this configuration are taken into account. Enabled bool // Fleet is a name of a selected fleet. If it has a value, nodes are loaded // from a file, namely `fleet-*.{{ .Fleet }}.json`. Nodes can be added to any list // in `ClusterConfig`. Fleet string // StaticNodes is a list of static nodes. StaticNodes []string // BootNodes is a list of bootnodes. BootNodes []string // TrustedMailServers is a list of verified and trusted Mail Server nodes. TrustedMailServers []string // RendezvousNodes is a list rendezvous discovery nodes. RendezvousNodes []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. KeyStoreDir string `validate:"required"` // NodeKey is the hex-encoded node ID (private key). Should be a valid secp256k1 private key that will be used for both // remote peer identification as well as network traffic encryption. NodeKey string // NoDiscovery set to true will disable discovery protocol. NoDiscovery bool // Rendezvous enables discovery protocol. Rendezvous bool // ListenAddr is an IP address and port of this node (e.g. 127.0.0.1:30303). ListenAddr string // AdvertiseAddr is a public IP address the node wants to be found with. // It is especially useful when using floating IPs attached to a server. AdvertiseAddr 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 // HTTPEnabled specifies whether the http RPC server is to be enabled by default. HTTPEnabled bool // 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 // HTTPPort is the TCP port number on which to start the Geth's HTTP RPC server. HTTPPort int // HTTPVirtualHosts is the list of virtual hostnames which are allowed on incoming requests. // This is by default {'localhost'}. Using this prevents attacks like // DNS rebinding, which bypasses SOP by simply masquerading as being within the same // origin. These attacks do not utilize CORS, since they are not cross-domain. // By explicitly checking the Host-header, the server will not allow requests // made against the server with a malicious host domain. // Requests using an IP address directly are not affected. HTTPVirtualHosts []string // HTTPCors is the Cross-Origin Resource Sharing header to send to requesting // clients. Please be aware that CORS is a browser enforced security, it's fully // useless for custom HTTP clients. HTTPCors []string // IPCEnabled specifies whether IPC-RPC Server is enabled or not IPCEnabled bool // IPCFile is filename of exposed IPC RPC Server IPCFile string // 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"` // LogMobileSystem enables log redirection to android/ios system logger. LogMobileSystem bool // 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"` // LogMaxBackups defines number of rotated log files that will be stored. LogMaxBackups int // LogMaxSize in megabytes after current size is reached log file will be rotated. LogMaxSize int // LogCompressRotated if true all rotated files will be gzipped. LogCompressRotated bool // LogToStderr defines whether logged info should also be output to os.Stderr LogToStderr bool // EnableStatusService should be true to enable methods under status namespace. EnableStatusService bool // UpstreamConfig extra config for providing upstream infura server. UpstreamConfig UpstreamRPCConfig `json:"UpstreamConfig"` // 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"` // IncentivisationConfig extra configuration for incentivisation service IncentivisationConfig IncentivisationConfig `json:"IncentivisationConfig," validate:"structonly"` // ShhextConfig keeps configuration for service running under shhext namespace. ShhextConfig ShhextConfig `json:"ShhextConfig," validate:"structonly"` // SwarmConfig extra configuration for Swarm and ENS SwarmConfig SwarmConfig `json:"SwarmConfig," validate:"structonly"` // RegisterTopics a list of specific topics where the peer wants to be // discoverable. RegisterTopics []discv5.Topic `json:"RegisterTopics"` // RequiredTopics list of topics where a client wants to search for // discoverable peers with the discovery limits. RequireTopics map[discv5.Topic]Limits `json:"RequireTopics"` // MailServerRegistryAddress is the MailServerRegistry contract address MailServerRegistryAddress string } // ShhextConfig defines options used by shhext service. type ShhextConfig struct { PFSEnabled bool // BackupDisabledDataDir is the file system folder the node should use for any data storage needs that it doesn't want backed up. BackupDisabledDataDir string // InstallationId id of the current installation InstallationID string // MailServerConfirmations should be true if client wants to receive confirmatons only from a selected mail servers. MailServerConfirmations bool // EnableConnectionManager turns on management of the mail server connections if true. EnableConnectionManager bool // EnableLastUsedMonitor guarantees that last used mail server will be tracked and persisted into the storage. EnableLastUsedMonitor bool // ConnectionTarget will be used by connection manager. It will ensure that we connected with configured number of servers. ConnectionTarget int // RequestsDelay used to ensure that no similar requests are sent within short periods of time. RequestsDelay time.Duration // MaxServerFailures defines maximum allowed expired requests before server will be swapped to another one. MaxServerFailures int // MaxMessageDeliveryAttempts defines how many times we will try to deliver not-acknowledged envelopes. MaxMessageDeliveryAttempts int // WhisperCacheDir is a folder where whisper filters may persist messages before delivering them // to a client. WhisperCacheDir string } // Validate validates the ShhextConfig struct and returns an error if inconsistent values are found func (c *ShhextConfig) Validate(validate *validator.Validate) error { if err := validate.Struct(c); err != nil { return err } if c.PFSEnabled && len(c.BackupDisabledDataDir) == 0 { return errors.New("field BackupDisabledDataDir is required if PFSEnabled is true") } return nil } // Option is an additional setting when creating a NodeConfig // using NewNodeConfigWithDefaults. type Option func(*NodeConfig) error // WithFleet loads one of the preconfigured Status fleets. func WithFleet(fleet string) Option { return func(c *NodeConfig) error { if fleet == FleetUndefined { return nil } c.NoDiscovery = false c.ClusterConfig.Enabled = true return loadConfigFromAsset(fmt.Sprintf("../config/cli/fleet-%s.json", fleet), c) } } // WithLES enabled LES protocol. func WithLES() Option { return func(c *NodeConfig) error { return loadConfigFromAsset("../config/cli/les-enabled.json", c) } } // WithMailserver enables MailServer. func WithMailserver() Option { return func(c *NodeConfig) error { return loadConfigFromAsset("../config/cli/mailserver-enabled.json", c) } } // NewNodeConfigWithDefaults creates new node configuration object // with some defaults suitable for adhoc use. func NewNodeConfigWithDefaults(dataDir string, networkID uint64, opts ...Option) (*NodeConfig, error) { c, err := NewNodeConfig(dataDir, networkID) if err != nil { return nil, err } c.NoDiscovery = true c.HTTPHost = "" c.ListenAddr = ":30303" c.LogEnabled = true c.LogLevel = "INFO" c.LogMaxSize = 100 c.LogCompressRotated = true c.LogMaxBackups = 3 c.LogToStderr = true c.WhisperConfig.Enabled = true c.WhisperConfig.EnableNTPSync = true for _, opt := range opts { if err := opt(c); err != nil { return nil, err } } c.updatePeerLimits() if err := c.Validate(); err != nil { return nil, err } return c, nil } // NewNodeConfigWithDefaultsAndFiles creates new node configuration object // with some defaults suitable for adhoc use and applies config files on top. func NewNodeConfigWithDefaultsAndFiles( dataDir string, networkID uint64, opts []Option, files []string, ) (*NodeConfig, error) { c, err := NewNodeConfigWithDefaults(dataDir, networkID, opts...) if err != nil { return nil, err } for _, file := range files { if err := loadConfigConfigFromFile(file, c); err != nil { return nil, err } } c.updatePeerLimits() if err := c.Validate(); err != nil { return nil, err } return c, nil } // updatePeerLimits will set default peer limits expectations based on enabled services. func (c *NodeConfig) updatePeerLimits() { if c.NoDiscovery && !c.Rendezvous { return } if c.WhisperConfig.Enabled { c.RequireTopics[WhisperDiscv5Topic] = WhisperDiscv5Limits // TODO(dshulyak) register mailserver limits when we will change how they are handled. } if c.LightEthConfig.Enabled { c.RequireTopics[discv5.Topic(LesTopic(int(c.NetworkID)))] = LesDiscoveryLimits } } // NewNodeConfig creates new node configuration object with bare-minimum defaults. // Important: the returned config is not validated. func NewNodeConfig(dataDir string, networkID uint64) (*NodeConfig, error) { var keyStoreDir, wnodeDir string if dataDir != "" { keyStoreDir = filepath.Join(dataDir, "keystore") } if dataDir != "" { wnodeDir = filepath.Join(dataDir, "wnode") } config := &NodeConfig{ NetworkID: networkID, DataDir: dataDir, KeyStoreDir: keyStoreDir, Version: Version, HTTPHost: "localhost", HTTPPort: 8545, HTTPVirtualHosts: []string{"localhost"}, ListenAddr: ":0", APIModules: "eth,net,web3,peer", MaxPeers: 25, MaxPendingPeers: 0, IPCFile: "geth.ipc", log: log.New("package", "status-go/params.NodeConfig"), LogFile: "", LogLevel: "ERROR", NoDiscovery: true, UpstreamConfig: UpstreamRPCConfig{ URL: getUpstreamURL(networkID), }, LightEthConfig: LightEthConfig{ DatabaseCache: 16, }, WhisperConfig: WhisperConfig{ DataDir: wnodeDir, MinimumPoW: WhisperMinimumPoW, TTL: WhisperTTL, MaxMessageSize: whisper.DefaultMaxMessageSize, }, ShhextConfig: ShhextConfig{ BackupDisabledDataDir: dataDir, }, SwarmConfig: SwarmConfig{}, RegisterTopics: []discv5.Topic{}, RequireTopics: map[discv5.Topic]Limits{}, } return config, nil } // NewConfigFromJSON parses incoming JSON and returned it as Config func NewConfigFromJSON(configJSON string) (*NodeConfig, error) { config, err := NewNodeConfig("", 0) if err != nil { return nil, err } if err := loadConfigFromJSON(configJSON, config); err != nil { return nil, err } if err := config.Validate(); err != nil { return nil, err } return config, nil } func loadConfigFromJSON(configJSON string, nodeConfig *NodeConfig) error { decoder := json.NewDecoder(strings.NewReader(configJSON)) // override default configuration with values by JSON input return decoder.Decode(&nodeConfig) } func loadConfigConfigFromFile(path string, config *NodeConfig) error { jsonConfig, err := ioutil.ReadFile(path) if err != nil { return err } return loadConfigFromJSON(string(jsonConfig), config) } func loadConfigFromAsset(name string, config *NodeConfig) error { data, err := static.Asset(name) if err != nil { return err } return loadConfigFromJSON(string(data), config) } // 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 // // nolint: gocyclo func (c *NodeConfig) Validate() error { validate := NewValidator() if err := validate.Struct(c); err != nil { return err } if c.NodeKey != "" { if _, err := crypto.HexToECDSA(c.NodeKey); err != nil { return fmt.Errorf("NodeKey is invalid (%s): %v", c.NodeKey, err) } } if c.UpstreamConfig.Enabled && c.LightEthConfig.Enabled { return fmt.Errorf("both UpstreamConfig and LightEthConfig are enabled, but they are mutually exclusive") } if err := c.validateChildStructs(validate); err != nil { return err } // Whisper's data directory must be relative to the main data directory // if EnableMailServer is true. if c.WhisperConfig.Enabled && c.WhisperConfig.EnableMailServer { if !strings.HasPrefix(c.WhisperConfig.DataDir, c.DataDir) { return fmt.Errorf("WhisperConfig.DataDir must start with DataDir fragment") } } if !c.NoDiscovery && len(c.ClusterConfig.BootNodes) == 0 { // No point in running discovery if we don't have bootnodes. // In case we do have bootnodes, NoDiscovery should be true. return fmt.Errorf("NoDiscovery is false, but ClusterConfig.BootNodes is empty") } if c.ShhextConfig.PFSEnabled && len(c.ShhextConfig.InstallationID) == 0 { return fmt.Errorf("PFSEnabled is true, but InstallationID is empty") } if len(c.ClusterConfig.RendezvousNodes) == 0 { if c.Rendezvous { return fmt.Errorf("Rendezvous is enabled, but ClusterConfig.RendezvousNodes is empty") } } else if !c.Rendezvous { return fmt.Errorf("Rendezvous is disabled, but ClusterConfig.RendezvousNodes is not empty") } return nil } func (c *NodeConfig) validateChildStructs(validate *validator.Validate) error { // Validate child structs if err := c.UpstreamConfig.Validate(validate); err != nil { return err } if err := c.ClusterConfig.Validate(validate); err != nil { return err } if err := c.LightEthConfig.Validate(validate); err != nil { return err } if err := c.WhisperConfig.Validate(validate); err != nil { return err } if err := c.SwarmConfig.Validate(validate); err != nil { return err } if err := c.ShhextConfig.Validate(validate); err != nil { return err } if c.IncentivisationConfig.Enabled { if err := c.IncentivisationConfig.Validate(validate); err != nil { return err } } return nil } // Validate validates the UpstreamRPCConfig struct and returns an error if inconsistent values are found func (c *UpstreamRPCConfig) Validate(validate *validator.Validate) error { if !c.Enabled { return nil } if err := validate.Struct(c); err != nil { return err } if _, err := url.ParseRequestURI(c.URL); err != nil { return fmt.Errorf("UpstreamRPCConfig.URL '%s' is invalid: %v", c.URL, err.Error()) } return nil } // Validate validates the ClusterConfig struct and returns an error if inconsistent values are found func (c *ClusterConfig) Validate(validate *validator.Validate) error { if !c.Enabled { return nil } if err := validate.Struct(c); err != nil { return err } return nil } // Validate validates the LightEthConfig struct and returns an error if inconsistent values are found func (c *LightEthConfig) Validate(validate *validator.Validate) error { if !c.Enabled { return nil } if err := validate.Struct(c); err != nil { return err } return nil } // Validate validates the WhisperConfig struct and returns an error if inconsistent values are found func (c *WhisperConfig) Validate(validate *validator.Validate) error { if !c.Enabled { return nil } if err := validate.Struct(c); err != nil { return err } if c.EnableMailServer { if c.DataDir == "" { return fmt.Errorf("WhisperConfig.DataDir must be specified when WhisperConfig.EnableMailServer is true") } if c.MailServerPassword == "" && c.MailServerAsymKey == "" { return fmt.Errorf("WhisperConfig.MailServerPassword or WhisperConfig.MailServerAsymKey must be specified when WhisperConfig.EnableMailServer is true") } if c.MailServerAsymKey != "" { if _, err := crypto.HexToECDSA(c.MailServerAsymKey); err != nil { return fmt.Errorf("WhisperConfig.MailServerAsymKey is invalid: %s", c.MailServerAsymKey) } } } return nil } // Validate validates the SwarmConfig struct and returns an error if inconsistent values are found func (c *SwarmConfig) Validate(validate *validator.Validate) error { if !c.Enabled { return nil } if err := validate.Struct(c); err != nil { return err } return nil } func getUpstreamURL(networkID uint64) string { switch networkID { case MainNetworkID: return MainnetEthereumNetworkURL case RopstenNetworkID: return RopstenEthereumNetworkURL case RinkebyNetworkID: return RinkebyEthereumNetworkURL case GoerliNetworkID: return GoerliEthereumNetworkURL } return "" } // 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 } // 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) } // LesTopic returns discovery v5 topic derived from genesis of the provided network. // 1 - mainnet, 3 - ropsten, 4 - rinkeby func LesTopic(netid int) string { switch netid { case 1: return LESDiscoveryIdentifier + common.Bytes2Hex(params.MainnetGenesisHash.Bytes()[:8]) case 3: return LESDiscoveryIdentifier + common.Bytes2Hex(params.TestnetGenesisHash.Bytes()[:8]) case 4: return LESDiscoveryIdentifier + common.Bytes2Hex(params.RinkebyGenesisHash.Bytes()[:8]) default: return "" } }