From 9a68fd211f816d92542d51dfda5407fca6b58ebf Mon Sep 17 00:00:00 2001 From: Victor Farazdagi Date: Mon, 10 Apr 2017 01:16:05 +0300 Subject: [PATCH] whisper: `statusd wnode` sub-command, closes #140 --- cmd/statusd/main.go | 43 ++---- cmd/statusd/misccmd.go | 36 +++++ cmd/statusd/wnodecmd.go | 138 ++++++++++++++++++ geth/params/config.go | 80 ++++++++-- geth/params/config_test.go | 4 +- geth/params/defaults.go | 61 ++++---- geth/params/testdata/config.testnet.json | 14 +- geth/utils.go | 38 +++-- .../jail/testdata => static/html}/sample.html | 0 9 files changed, 324 insertions(+), 90 deletions(-) create mode 100644 cmd/statusd/misccmd.go create mode 100644 cmd/statusd/wnodecmd.go rename {geth/jail/testdata => static/html}/sample.html (100%) diff --git a/cmd/statusd/main.go b/cmd/statusd/main.go index 525ea0a71..6299b09df 100644 --- a/cmd/statusd/main.go +++ b/cmd/statusd/main.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "runtime" - "strings" "github.com/status-im/status-go/geth" "github.com/status-im/status-go/geth/params" @@ -19,10 +18,14 @@ var ( ) var ( + NodeKeyFileFlag = cli.StringFlag{ + Name: "nodekey", + Usage: "P2P node key file (private key)", + } DataDirFlag = cli.StringFlag{ Name: "datadir", Usage: "Data directory for the databases and keystore", - Value: params.DefaultDataDir, + Value: params.DataDir, } NetworkIdFlag = cli.IntFlag{ Name: "networkid", @@ -48,7 +51,7 @@ var ( HTTPPortFlag = cli.IntFlag{ Name: "httpport", Usage: "HTTP RPC server's listening port", - Value: params.DefaultHTTPPort, + Value: params.HTTPPort, } IPCEnabledFlag = cli.BoolFlag{ Name: "ipc", @@ -59,14 +62,6 @@ var ( Usage: `Log level, one of: ""ERROR", "WARNING", "INFO", "DEBUG", and "DETAIL"`, Value: "INFO", } - TestAccountKey = cli.StringFlag{ - Name: "accountkey", - Usage: "Test account PK (will be loaded into accounts cache, and injected to Whisper)", - } - TestAccountPasswd = cli.StringFlag{ - Name: "accountpasswd", - Usage: "Test account password", - } ) func init() { @@ -74,14 +69,12 @@ func init() { app.Action = statusd app.HideVersion = true // separate command prints version app.Commands = []cli.Command{ - { - Action: version, - Name: "version", - Usage: "Print app version", - }, + versionCommand, + wnodeCommand, } app.Flags = []cli.Flag{ + NodeKeyFileFlag, DataDirFlag, NetworkIdFlag, LightEthEnabledFlag, @@ -133,6 +126,7 @@ func makeNodeConfig(ctx *cli.Context) (*params.NodeConfig, error) { return nil, err } + nodeConfig.NodeKeyFile = ctx.GlobalString(NodeKeyFileFlag.Name) if !ctx.GlobalBool(HTTPEnabledFlag.Name) { nodeConfig.HTTPHost = "" // HTTP RPC is disabled } @@ -150,23 +144,6 @@ func makeNodeConfig(ctx *cli.Context) (*params.NodeConfig, error) { return nodeConfig, nil } -// version displays app version -func version(ctx *cli.Context) error { - fmt.Println(strings.Title(params.DefaultClientIdentifier)) - fmt.Println("Version:", params.Version) - if gitCommit != "" { - fmt.Println("Git Commit:", gitCommit) - } - - fmt.Println("Network Id:", ctx.GlobalInt(NetworkIdFlag.Name)) - fmt.Println("Go Version:", runtime.Version()) - fmt.Println("OS:", runtime.GOOS) - fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH")) - fmt.Printf("GOROOT=%s\n", runtime.GOROOT()) - - return nil -} - // makeApp creates an app with sane defaults. func makeApp(gitCommit string) *cli.App { app := cli.NewApp() diff --git a/cmd/statusd/misccmd.go b/cmd/statusd/misccmd.go new file mode 100644 index 000000000..b01d4a151 --- /dev/null +++ b/cmd/statusd/misccmd.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "os" + "runtime" + "strings" + + "github.com/status-im/status-go/geth/params" + "gopkg.in/urfave/cli.v1" +) + +var ( + versionCommand = cli.Command{ + Action: version, + Name: "version", + Usage: "Print app version", + } +) + +// version displays app version +func version(ctx *cli.Context) error { + fmt.Println(strings.Title(params.ClientIdentifier)) + fmt.Println("Version:", params.Version) + if gitCommit != "" { + fmt.Println("Git Commit:", gitCommit) + } + + fmt.Println("Network Id:", ctx.GlobalInt(NetworkIdFlag.Name)) + fmt.Println("Go Version:", runtime.Version()) + fmt.Println("OS:", runtime.GOOS) + fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH")) + fmt.Printf("GOROOT=%s\n", runtime.GOROOT()) + + return nil +} diff --git a/cmd/statusd/wnodecmd.go b/cmd/statusd/wnodecmd.go new file mode 100644 index 000000000..59493d167 --- /dev/null +++ b/cmd/statusd/wnodecmd.go @@ -0,0 +1,138 @@ +package main + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/status-im/status-go/geth" + "github.com/status-im/status-go/geth/params" + "gopkg.in/urfave/cli.v1" +) + +var ( + WhisperEchoModeFlag = cli.BoolTFlag{ + Name: "echo", + Usage: "Echo mode, prints some arguments for diagnostics (default: true)", + } + WhisperBootstrapNodeFlag = cli.BoolTFlag{ + Name: "bootstrap", + Usage: "Don't actively connect to peers, wait for incoming connections (default: true)", + } + WhisperNotificationServerNodeFlag = cli.BoolFlag{ + Name: "notify", + Usage: "Node is capable of sending Push Notifications", + } + WhisperForwarderNodeFlag = cli.BoolFlag{ + Name: "forward", + Usage: "Only forward messages, neither send nor decrypt messages", + } + WhisperMailserverNodeFlag = cli.BoolFlag{ + Name: "mailserver", + Usage: "Delivers expired messages on demand", + } + WhisperPassword = cli.StringFlag{ + Name: "password", + Usage: "Password, will be used for topic keys, as Mail & Notification Server password", + } + WhisperPortFlag = cli.IntFlag{ + Name: "port", + Usage: "Whisper node's listening port", + Value: params.WhisperPort, + } + WhisperPoWFlag = cli.Float64Flag{ + Name: "pow", + Usage: "PoW for messages to be added to queue, in float format", + Value: params.WhisperMinimumPoW, + } + WhisperTTLFlag = cli.IntFlag{ + Name: "ttl", + Usage: "Time to live for messages, in seconds", + Value: params.WhisperTTL, + } +) + +var ( + wnodeCommand = cli.Command{ + Action: wnode, + Name: "wnode", + Usage: "Starts Whisper/5 node", + Flags: []cli.Flag{ + WhisperEchoModeFlag, + WhisperBootstrapNodeFlag, + WhisperNotificationServerNodeFlag, + WhisperForwarderNodeFlag, + WhisperMailserverNodeFlag, + WhisperPassword, + WhisperPoWFlag, + WhisperPortFlag, + WhisperTTLFlag, + }, + } +) + +// version displays app version +func wnode(ctx *cli.Context) error { + config, err := makeWhisperNodeConfig(ctx) + if err != nil { + return fmt.Errorf("can not parse config: %v", err) + } + + wnodePrintHeader(config) + + // inject test accounts + geth.ImportTestAccount(filepath.Join(config.DataDir, "keystore"), "test-account1.pk") + geth.ImportTestAccount(filepath.Join(config.DataDir, "keystore"), "test-account2.pk") + + if err := geth.CreateAndRunNode(config); err != nil { + return err + } + + // wait till node has been stopped + geth.NodeManagerInstance().Node().GethStack().Wait() + + return nil +} + +func wnodePrintHeader(nodeConfig *params.NodeConfig) { + fmt.Println("Starting Whisper/5 node..") + + whisperConfig := nodeConfig.WhisperConfig + + if whisperConfig.EchoMode { + fmt.Printf("Whisper Config: %s\n", whisperConfig) + } +} + +// makeWhisperNodeConfig parses incoming CLI options and returns node configuration object +func makeWhisperNodeConfig(ctx *cli.Context) (*params.NodeConfig, error) { + nodeConfig, err := makeNodeConfig(ctx) + if err != nil { + return nil, err + } + + whisperConfig := nodeConfig.WhisperConfig + + whisperConfig.Enabled = true + whisperConfig.EchoMode = ctx.BoolT(WhisperEchoModeFlag.Name) + whisperConfig.BootstrapNode = ctx.BoolT(WhisperBootstrapNodeFlag.Name) + whisperConfig.ForwarderNode = ctx.Bool(WhisperForwarderNodeFlag.Name) + whisperConfig.NotificationServerNode = ctx.Bool(WhisperNotificationServerNodeFlag.Name) + whisperConfig.MailServerNode = ctx.Bool(WhisperMailserverNodeFlag.Name) + whisperConfig.MailServerPassword = ctx.String(WhisperPassword.Name) + whisperConfig.NotificationServerPassword = ctx.String(WhisperPassword.Name) // the same for both mail and notification servers + + whisperConfig.Port = ctx.Int(WhisperPortFlag.Name) + whisperConfig.TTL = ctx.Int(WhisperTTLFlag.Name) + whisperConfig.MinimumPoW = ctx.Float64(WhisperPoWFlag.Name) + + if whisperConfig.MailServerNode && len(whisperConfig.MailServerPassword) == 0 { + return nil, errors.New("mail server requires --password to be specified") + } + + if whisperConfig.NotificationServerNode && len(whisperConfig.NotificationServerPassword) == 0 { + return nil, errors.New("notification server requires --password to be specified") + } + + return nodeConfig, nil +} diff --git a/geth/params/config.go b/geth/params/config.go index cb517bdee..b4f2782f3 100644 --- a/geth/params/config.go +++ b/geth/params/config.go @@ -77,6 +77,39 @@ type LightEthConfig struct { type WhisperConfig struct { // Enabled flag specifies whether protocol is enabled Enabled bool + + // EchoMode if mode is on, prints some arguments for diagnostics + EchoMode bool + + // BootstrapNode whether node doesn't actively connect to peers, and waits for incoming connections + BootstrapNode bool + + // ForwarderNode is mode when node only forwards messages, neither sends nor decrypts messages + ForwarderNode bool + + // MailServerNode is mode when node is capable of delivering expired messages on demand + MailServerNode bool + + // MailServerPassword is password for MailServer's symmetric key + MailServerPassword string + + // NotificationServerNode is mode when node is capable of sending Push (and probably other kinds) Notifications + NotificationServerNode bool + + // NotificationServerPassword is password for NotificationServer's symmetric key (used in discovery) + NotificationServerPassword string + + // DataDir is the file system folder Whisper should use for any data storage needs. + DataDir string + + // Port Whisper node's listening port + Port int + + // MinimumPoW minimum PoW for Whisper messages + MinimumPoW float64 + + // TTL time to live for messages, in seconds + TTL int } // SwarmConfig holds Swarm-related configuration @@ -96,6 +129,11 @@ type NodeConfig struct { // DataDir is the file system folder the node should use for any data storage needs. DataDir string + // PrivateKeyFile 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 + // Name sets the instance name of the node. It must not contain the / character. Name string @@ -166,25 +204,29 @@ func NewNodeConfig(dataDir string, networkId int) (*NodeConfig, error) { nodeConfig := &NodeConfig{ NetworkId: networkId, DataDir: dataDir, - Name: DefaultClientIdentifier, + Name: ClientIdentifier, Version: Version, - HTTPHost: DefaultHTTPHost, - HTTPPort: DefaultHTTPPort, - APIModules: DefaultAPIModules, - WSHost: DefaultWSHost, - WSPort: DefaultWSPort, - MaxPeers: DefaultMaxPeers, - MaxPendingPeers: DefaultMaxPendingPeers, - IPCFile: DefaultIPCFile, - LogFile: DefaultLogFile, - LogLevel: DefaultLogLevel, + HTTPHost: HTTPHost, + HTTPPort: HTTPPort, + APIModules: APIModules, + WSHost: WSHost, + WSPort: WSPort, + MaxPeers: MaxPeers, + MaxPendingPeers: MaxPendingPeers, + IPCFile: IPCFile, + LogFile: LogFile, + LogLevel: LogLevel, ChainConfig: &ChainConfig{}, LightEthConfig: &LightEthConfig{ Enabled: true, - DatabaseCache: DefaultDatabaseCache, + DatabaseCache: DatabaseCache, }, WhisperConfig: &WhisperConfig{ - Enabled: true, + Enabled: true, + DataDir: filepath.Join(dataDir, "wnode"), + Port: WhisperPort, + MinimumPoW: WhisperMinimumPoW, + TTL: WhisperTTL, }, SwarmConfig: &SwarmConfig{}, } @@ -293,3 +335,15 @@ func (c *NodeConfig) String() string { data, _ := json.MarshalIndent(c, "", " ") return string(data) } + +// String dumps config object as nicely indented JSON +func (c *WhisperConfig) String() string { + data, _ := json.MarshalIndent(c, "", " ") + return string(data) +} + +// String dumps config object as nicely indented JSON +func (c *SwarmConfig) String() string { + data, _ := json.MarshalIndent(c, "", " ") + return string(data) +} diff --git a/geth/params/config_test.go b/geth/params/config_test.go index 4957e71e9..05943d568 100644 --- a/geth/params/config_test.go +++ b/geth/params/config_test.go @@ -101,11 +101,11 @@ var loadConfigTestCases = []struct { t.Fatal("wrong Name") } - if nodeConfig.HTTPPort != params.DefaultHTTPPort { + if nodeConfig.HTTPPort != params.HTTPPort { t.Fatal("wrong HTTPPort") } - if nodeConfig.HTTPHost != params.DefaultHTTPHost { + if nodeConfig.HTTPHost != params.HTTPHost { t.Fatal("wrong HTTPHost") } diff --git a/geth/params/defaults.go b/geth/params/defaults.go index 0ba28a285..0d4688272 100644 --- a/geth/params/defaults.go +++ b/geth/params/defaults.go @@ -7,37 +7,37 @@ import ( ) const ( - // DefaultClientIdentifier is client identifier to advertise over the network - DefaultClientIdentifier = "StatusIM" + // ClientIdentifier is client identifier to advertise over the network + ClientIdentifier = "StatusIM" - // DefaultDataDir is default data directory used by statusd executable - DefaultDataDir = "statusd-data" + // DataDir is default data directory used by statusd executable + DataDir = "statusd-data" - // DefaultIPCFile is filename of exposed IPC RPC Server - DefaultIPCFile = "geth.ipc" + // IPCFile is filename of exposed IPC RPC Server + IPCFile = "geth.ipc" - // DefaultHTTPHost is host interface for the HTTP RPC server - DefaultHTTPHost = "localhost" + // HTTPHost is host interface for the HTTP RPC server + HTTPHost = "localhost" - // DefaultHTTPPort is HTTP-RPC port (replaced in unit tests) - DefaultHTTPPort = 8545 + // HTTPPort is HTTP-RPC port (replaced in unit tests) + HTTPPort = 8545 - // DefaultAPIModules is a list of modules to expose vie HTTP RPC + // APIModules is a list of modules to expose vie HTTP RPC // TODO remove "admin" on main net - DefaultAPIModules = "db,eth,net,web3,shh,personal,admin" + APIModules = "db,eth,net,web3,shh,personal,admin" - // DefaultWSHost is a host interface for the websocket RPC server - DefaultWSHost = "localhost" + // WSHost is a host interface for the websocket RPC server + WSHost = "localhost" - // DefaultWSPort is a WS-RPC port (replaced in unit tests) - DefaultWSPort = 8546 + // WSPort is a WS-RPC port (replaced in unit tests) + WSPort = 8546 - // DefaultMaxPeers is the maximum number of global peers - DefaultMaxPeers = 25 + // MaxPeers is the maximum number of global peers + MaxPeers = 25 - // DefaultMaxPendingPeers is the maximum number of peers that can be pending in the + // MaxPendingPeers is the maximum number of peers that can be pending in the // handshake phase, counted separately for inbound and outbound connections. - DefaultMaxPendingPeers = 0 + MaxPendingPeers = 0 // DefaultGas default amount of gas used for transactions DefaultGas = 180000 @@ -45,14 +45,23 @@ const ( // DefaultFileDescriptorLimit is fd limit that database can use DefaultFileDescriptorLimit = uint64(2048) - // DefaultDatabaseCache is memory (in MBs) allocated to internal caching (min 16MB / database forced) - DefaultDatabaseCache = 128 + // DatabaseCache is memory (in MBs) allocated to internal caching (min 16MB / database forced) + DatabaseCache = 128 - // DefaultLogFile defines where to write logs to - DefaultLogFile = "geth.log" + // LogFile defines where to write logs to + LogFile = "geth.log" - // DefaultLogLevel defines the minimum log level to report - DefaultLogLevel = "INFO" + // LogLevel defines the minimum log level to report + LogLevel = "INFO" + + // WhisperPort is Whisper node listening port + WhisperPort = 30379 + + // WhisperMinimumPoW amount of work for Whisper message to be added to sending queue + WhisperMinimumPoW = 0.001 + + // WhisperTTL is time to live for messages, in seconds + WhisperTTL = 120 // TestNetworkId is id of a test network TestNetworkId = 3 diff --git a/geth/params/testdata/config.testnet.json b/geth/params/testdata/config.testnet.json index 8682d1441..911b296cf 100755 --- a/geth/params/testdata/config.testnet.json +++ b/geth/params/testdata/config.testnet.json @@ -2,6 +2,7 @@ "TestNet": true, "NetworkId": 3, "DataDir": "$TMPDIR", + "NodeKeyFile": "", "Name": "StatusIM", "Version": "$VERSION", "APIModules": "db,eth,net,web3,shh,personal,admin", @@ -34,7 +35,18 @@ "DatabaseCache": 128 }, "WhisperConfig": { - "Enabled": true + "Enabled": true, + "EchoMode": false, + "BootstrapNode": false, + "ForwarderNode": false, + "MailServerNode": false, + "MailServerPassword": "", + "NotificationServerNode": false, + "NotificationServerPassword": "", + "DataDir": "$TMPDIR/wnode", + "Port": 30379, + "MinimumPoW": 0.001, + "TTL": 120 }, "SwarmConfig": { "Enabled": false diff --git a/geth/utils.go b/geth/utils.go index 44adce266..609269a5b 100644 --- a/geth/utils.go +++ b/geth/utils.go @@ -168,23 +168,11 @@ func PrepareTestNode() (err error) { return err } - // import test account (with test ether on it) - importTestAccount := func(accountFile string) error { - dst := filepath.Join(TestDataDir, "keystore", accountFile) - if _, err := os.Stat(dst); os.IsNotExist(err) { - err = ioutil.WriteFile(dst, static.MustAsset("keys/"+accountFile), 0644) - if err != nil { - glog.V(logger.Warn).Infof("cannot copy test account PK: %v", err) - return err - } - } - - return nil - } - if err := importTestAccount("test-account1.pk"); err != nil { + // import test accounts (with test ether on it) + if err := ImportTestAccount(filepath.Join(TestDataDir, "keystore"), "test-account1.pk"); err != nil { panic(err) } - if err := importTestAccount("test-account2.pk"); err != nil { + if err := ImportTestAccount(filepath.Join(TestDataDir, "keystore"), "test-account2.pk"); err != nil { panic(err) } @@ -286,3 +274,23 @@ func AddressToDecryptedAccount(address, password string) (accounts.Account, *key return keyStore.AccountDecryptedKey(account, password) } + +// ImportTestAccount checks if test account exists in keystore, and if not +// tries to import it (from static resources, see "static/keys" folder) +func ImportTestAccount(keystoreDir, accountFile string) error { + // make sure that keystore folder exists + if _, err := os.Stat(keystoreDir); os.IsNotExist(err) { + os.MkdirAll(keystoreDir, os.ModePerm) + } + + dst := filepath.Join(keystoreDir, accountFile) + if _, err := os.Stat(dst); os.IsNotExist(err) { + err = ioutil.WriteFile(dst, static.MustAsset("keys/"+accountFile), 0644) + if err != nil { + glog.V(logger.Warn).Infof("cannot copy test account PK: %v", err) + return err + } + } + + return nil +} diff --git a/geth/jail/testdata/sample.html b/static/html/sample.html similarity index 100% rename from geth/jail/testdata/sample.html rename to static/html/sample.html