From b58a5016738b92db19e08ec87ef34ce3250fae6b Mon Sep 17 00:00:00 2001 From: Bas van Kervel Date: Tue, 13 Jun 2017 11:49:07 +0200 Subject: [PATCH] whisperv5: integrate whisper and add whisper RPC simulator --- cmd/geth/config.go | 35 +- cmd/geth/main.go | 3 +- cmd/geth/usage.go | 10 +- cmd/utils/flags.go | 21 +- cmd/wnode/main.go | 16 +- internal/web3ext/web3ext.go | 70 +- mobile/geth.go | 4 +- rpc/client.go | 46 ++ whisper/mailserver/server_test.go | 2 +- whisper/whisperv5/api.go | 849 +++++++++++++---------- whisper/whisperv5/api_test.go | 12 +- whisper/whisperv5/client.go | 193 ++++++ whisper/whisperv5/config.go | 52 ++ whisper/whisperv5/doc.go | 5 +- whisper/whisperv5/envelope.go | 2 +- whisper/whisperv5/filter.go | 1 + whisper/whisperv5/filter_test.go | 4 +- whisper/whisperv5/gen_config.go | 32 + whisper/whisperv5/gen_criteria_json.go | 62 ++ whisper/whisperv5/gen_message_json.go | 80 +++ whisper/whisperv5/gen_newmessage_json.go | 86 +++ whisper/whisperv5/message.go | 23 +- whisper/whisperv5/peer_test.go | 2 +- whisper/whisperv5/topic.go | 28 +- whisper/whisperv5/topic_test.go | 35 +- whisper/whisperv5/whisper.go | 145 ++-- whisper/whisperv5/whisper_test.go | 48 +- 27 files changed, 1311 insertions(+), 555 deletions(-) create mode 100644 whisper/whisperv5/client.go create mode 100644 whisper/whisperv5/config.go create mode 100644 whisper/whisperv5/gen_config.go create mode 100644 whisper/whisperv5/gen_criteria_json.go create mode 100644 whisper/whisperv5/gen_message_json.go create mode 100644 whisper/whisperv5/gen_newmessage_json.go diff --git a/cmd/geth/config.go b/cmd/geth/config.go index b76da3042..6a0183204 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" "github.com/naoina/toml" ) @@ -42,7 +43,7 @@ var ( Name: "dumpconfig", Usage: "Show configuration values", ArgsUsage: "", - Flags: append(nodeFlags, rpcFlags...), + Flags: append(append(nodeFlags, rpcFlags...), whisper.Flags...), Category: "MISCELLANEOUS COMMANDS", Description: `The dumpconfig command shows configuration values.`, } @@ -76,6 +77,7 @@ type ethstatsConfig struct { type gethConfig struct { Eth eth.Config + Shh whisper.Config Node node.Config Ethstats ethstatsConfig } @@ -99,8 +101,8 @@ func defaultNodeConfig() node.Config { cfg := node.DefaultConfig cfg.Name = clientIdentifier cfg.Version = params.VersionWithCommit(gitCommit) - cfg.HTTPModules = append(cfg.HTTPModules, "eth") - cfg.WSModules = append(cfg.WSModules, "eth") + cfg.HTTPModules = append(cfg.HTTPModules, "eth", "shh") + cfg.WSModules = append(cfg.WSModules, "eth", "shh") cfg.IPCPath = "geth.ipc" return cfg } @@ -109,6 +111,7 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { // Load defaults. cfg := gethConfig{ Eth: eth.DefaultConfig, + Shh: whisper.DefaultConfig, Node: defaultNodeConfig(), } @@ -130,19 +133,37 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name) } + utils.SetShhConfig(ctx, stack, &cfg.Shh) + return stack, cfg } +// enableWhisper returns true in case one of the whisper flags is set. +func enableWhisper(ctx *cli.Context) bool { + for _, flag := range whisper.Flags { + if ctx.GlobalIsSet(flag.GetName()) { + return true + } + } + return false +} + func makeFullNode(ctx *cli.Context) *node.Node { stack, cfg := makeConfigNode(ctx) utils.RegisterEthService(stack, &cfg.Eth) - // Whisper must be explicitly enabled, but is auto-enabled in --dev mode. - shhEnabled := ctx.GlobalBool(utils.WhisperEnabledFlag.Name) - shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name) + // Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode + shhEnabled := enableWhisper(ctx) + shhAutoEnabled := !ctx.GlobalIsSet(whisper.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name) if shhEnabled || shhAutoEnabled { - utils.RegisterShhService(stack) + if ctx.GlobalIsSet(whisper.MaxMessageSizeFlag.Name) { + cfg.Shh.MaxMessageSize = uint32(ctx.Int(whisper.MaxMessageSizeFlag.Name)) + } + if ctx.GlobalIsSet(whisper.MinPOWFlag.Name) { + cfg.Shh.MinimumAcceptedPOW = ctx.Float64(whisper.MinPOWFlag.Name) + } + utils.RegisterShhService(stack, &cfg.Shh) } // Add the Ethereum Stats daemon if requested. diff --git a/cmd/geth/main.go b/cmd/geth/main.go index cc481796f..c5f783a08 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/node" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" "gopkg.in/urfave/cli.v1" ) @@ -95,7 +96,6 @@ var ( utils.NetrestrictFlag, utils.NodeKeyFileFlag, utils.NodeKeyHexFlag, - utils.WhisperEnabledFlag, utils.DevModeFlag, utils.TestnetFlag, utils.RinkebyFlag, @@ -161,6 +161,7 @@ func init() { app.Flags = append(app.Flags, rpcFlags...) app.Flags = append(app.Flags, consoleFlags...) app.Flags = append(app.Flags, debug.Flags...) + app.Flags = append(app.Flags, whisper.Flags...) app.Before = func(ctx *cli.Context) error { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 2ba504fdd..4a3e22af8 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/internal/debug" + "github.com/ethereum/go-ethereum/whisper/whisperv5" "gopkg.in/urfave/cli.v1" ) @@ -187,6 +188,10 @@ var AppHelpFlagGroups = []flagGroup{ utils.NoCompactionFlag, }, debug.Flags...), }, + { + Name: "Whisper (EXPERIMENTAL)", + Flags: whisperv5.Flags, + }, { Name: "DEPRECATED", Flags: []cli.Flag{ @@ -195,10 +200,7 @@ var AppHelpFlagGroups = []flagGroup{ }, }, { - Name: "EXPERIMENTAL", - Flags: []cli.Flag{ - utils.WhisperEnabledFlag, - }, + Name: "MISC", }, } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 3c97cd3bb..095a9f62c 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -440,11 +440,6 @@ var ( Usage: "Restricts network communication to the given IP networks (CIDR masks)", } - WhisperEnabledFlag = cli.BoolFlag{ - Name: "shh", - Usage: "Enable Whisper", - } - // ATM the url is left to the user and deployment to JSpathFlag = cli.StringFlag{ Name: "jspath", @@ -878,6 +873,16 @@ func checkExclusive(ctx *cli.Context, flags ...cli.Flag) { } } +// SetShhConfig applies shh-related command line flags to the config. +func SetShhConfig(ctx *cli.Context, stack *node.Node, cfg *whisper.Config) { + if ctx.GlobalIsSet(whisper.MaxMessageSizeFlag.Name) { + cfg.MaxMessageSize = uint32(ctx.GlobalUint(whisper.MaxMessageSizeFlag.Name)) + } + if ctx.GlobalIsSet(whisper.MinPOWFlag.Name) { + cfg.MinimumAcceptedPOW = ctx.GlobalFloat64(whisper.MinPOWFlag.Name) + } +} + // SetEthConfig applies eth-related command line flags to the config. func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) { // Avoid conflicting network flags @@ -983,8 +988,10 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) { } // RegisterShhService configures Whisper and adds it to the given node. -func RegisterShhService(stack *node.Node) { - if err := stack.Register(func(*node.ServiceContext) (node.Service, error) { return whisper.New(), nil }); err != nil { +func RegisterShhService(stack *node.Node, cfg *whisper.Config) { + if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) { + return whisper.New(cfg), nil + }); err != nil { Fatalf("Failed to register the Whisper service: %v", err) } } diff --git a/cmd/wnode/main.go b/cmd/wnode/main.go index f18025dff..05e6b2908 100644 --- a/cmd/wnode/main.go +++ b/cmd/wnode/main.go @@ -87,7 +87,7 @@ var ( argVerbosity = flag.Int("verbosity", int(log.LvlError), "log verbosity level") argTTL = flag.Uint("ttl", 30, "time-to-live for messages in seconds") argWorkTime = flag.Uint("work", 5, "work time in seconds") - argMaxSize = flag.Int("maxsize", whisper.DefaultMaxMessageLength, "max size of message") + argMaxSize = flag.Uint("maxsize", uint(whisper.DefaultMaxMessageSize), "max size of message") argPoW = flag.Float64("pow", whisper.DefaultMinimumPoW, "PoW for normal messages in float format (e.g. 2.7)") argServerPoW = flag.Float64("mspow", whisper.DefaultMinimumPoW, "PoW requirement for Mail Server request") @@ -198,6 +198,11 @@ func initialize() { peers = append(peers, peer) } + cfg := &whisper.Config{ + MaxMessageSize: uint32(*argMaxSize), + MinimumAcceptedPOW: *argPoW, + } + if *mailServerMode { if len(msPassword) == 0 { msPassword, err = console.Stdin.PromptPassword("Please enter the Mail Server password: ") @@ -205,11 +210,12 @@ func initialize() { utils.Fatalf("Failed to read Mail Server password: %s", err) } } - shh = whisper.New() + + shh = whisper.New(cfg) shh.RegisterServer(&mailServer) mailServer.Init(shh, *argDBPath, msPassword, *argServerPoW) } else { - shh = whisper.New() + shh = whisper.New(cfg) } if *argPoW != whisper.DefaultMinimumPoW { @@ -219,8 +225,8 @@ func initialize() { } } - if *argMaxSize != whisper.DefaultMaxMessageLength { - err := shh.SetMaxMessageLength(*argMaxSize) + if uint32(*argMaxSize) != whisper.DefaultMaxMessageSize { + err := shh.SetMaxMessageSize(uint32(*argMaxSize)) if err != nil { utils.Fatalf("Failed to set max message size: %s", err) } diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index e35d74ae1..948d659c2 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -526,10 +526,6 @@ const Shh_JS = ` web3._extend({ property: 'shh', methods: [ - new web3._extend.Method({ - name: 'info', - call: 'shh_info' - }), new web3._extend.Method({ name: 'setMaxMessageLength', call: 'shh_setMaxMessageLength', @@ -541,8 +537,8 @@ web3._extend({ params: 1 }), new web3._extend.Method({ - name: 'allowP2PMessagesFromPeer', - call: 'shh_allowP2PMessagesFromPeer', + name: 'markTrustedPeer', + call: 'shh_markTrustedPeer', params: 1 }), new web3._extend.Method({ @@ -570,58 +566,68 @@ web3._extend({ params: 1 }), new web3._extend.Method({ - name: 'generateSymmetricKey', - call: 'shh_generateSymmetricKey', + name: 'newSymKey', + call: 'shh_newSymKey', }), new web3._extend.Method({ - name: 'addSymmetricKeyDirect', - call: 'shh_addSymmetricKeyDirect', + name: 'addSymKey', + call: 'shh_addSymKey', params: 1 }), new web3._extend.Method({ - name: 'addSymmetricKeyFromPassword', - call: 'shh_addSymmetricKeyFromPassword', + name: 'generateSymKeyFromPassword', + call: 'shh_generateSymKeyFromPassword', params: 1 }), new web3._extend.Method({ - name: 'hasSymmetricKey', - call: 'shh_hasSymmetricKey', + name: 'hasSymKey', + call: 'shh_hasSymKey', params: 1 }), new web3._extend.Method({ - name: 'getSymmetricKey', - call: 'shh_getSymmetricKey', + name: 'getSymKey', + call: 'shh_getSymKey', params: 1 }), new web3._extend.Method({ - name: 'deleteSymmetricKey', - call: 'shh_deleteSymmetricKey', + name: 'deleteSymKey', + call: 'shh_deleteSymKey', params: 1 }), new web3._extend.Method({ name: 'subscribe', call: 'shh_subscribe', - params: 1 + params: 2 }), new web3._extend.Method({ name: 'unsubscribe', call: 'shh_unsubscribe', params: 1 }), - new web3._extend.Method({ - name: 'getNewSubscriptionMessages', - call: 'shh_getNewSubscriptionMessages', - params: 1 - }), - new web3._extend.Method({ - name: 'getFloatingMessages', - call: 'shh_getFloatingMessages', - params: 1 - }), new web3._extend.Method({ name: 'post', call: 'shh_post', params: 1 + }), + new web3._extend.Method({ + name: 'publicKey', + call: 'shh_getPublicKey', + params: 1 + }), + new web3._extend.Method({ + name: 'getFilterMessages', + call: 'shh_getFilterMessages', + params: 1 + }), + new web3._extend.Method({ + name: 'deleteMessageFilter', + call: 'shh_deleteMessageFilter', + params: 1 + }), + new web3._extend.Method({ + name: 'newMessageFilter', + call: 'shh_newMessageFilter', + params: 1 }) ], properties: @@ -630,7 +636,11 @@ web3._extend({ name: 'version', getter: 'shh_version', outputFormatter: web3._extend.utils.toDecimal - }) + }), + new web3._extend.Property({ + name: 'info', + getter: 'shh_info' + }), ] }); ` diff --git a/mobile/geth.go b/mobile/geth.go index f254d39bb..7b39faade 100644 --- a/mobile/geth.go +++ b/mobile/geth.go @@ -169,7 +169,9 @@ func NewNode(datadir string, config *NodeConfig) (stack *Node, _ error) { } // Register the Whisper protocol if requested if config.WhisperEnabled { - if err := rawStack.Register(func(*node.ServiceContext) (node.Service, error) { return whisper.New(), nil }); err != nil { + if err := rawStack.Register(func(*node.ServiceContext) (node.Service, error) { + return whisper.New(&whisper.DefaultConfig), nil + }); err != nil { return nil, fmt.Errorf("whisper init: %v", err) } } diff --git a/rpc/client.go b/rpc/client.go index 591986987..f02366a39 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -349,6 +349,52 @@ func (c *Client) BatchCallContext(ctx context.Context, b []BatchElem) error { return err } +// ShhSubscribe calls the "shh_subscribe" method with the given arguments, +// registering a subscription. Server notifications for the subscription are +// sent to the given channel. The element type of the channel must match the +// expected type of content returned by the subscription. +// +// The context argument cancels the RPC request that sets up the subscription but has no +// effect on the subscription after ShhSubscribe has returned. +// +// Slow subscribers will be dropped eventually. Client buffers up to 8000 notifications +// before considering the subscriber dead. The subscription Err channel will receive +// ErrSubscriptionQueueOverflow. Use a sufficiently large buffer on the channel or ensure +// that the channel usually has at least one reader to prevent this issue. +func (c *Client) ShhSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*ClientSubscription, error) { + // Check type of channel first. + chanVal := reflect.ValueOf(channel) + if chanVal.Kind() != reflect.Chan || chanVal.Type().ChanDir()&reflect.SendDir == 0 { + panic("first argument to ShhSubscribe must be a writable channel") + } + if chanVal.IsNil() { + panic("channel given to ShhSubscribe must not be nil") + } + if c.isHTTP { + return nil, ErrNotificationsUnsupported + } + + msg, err := c.newMessage("shh"+subscribeMethodSuffix, args...) + if err != nil { + return nil, err + } + op := &requestOp{ + ids: []json.RawMessage{msg.ID}, + resp: make(chan *jsonrpcMessage), + sub: newClientSubscription(c, "shh", chanVal), + } + + // Send the subscription request. + // The arrival and validity of the response is signaled on sub.quit. + if err := c.send(ctx, op, msg); err != nil { + return nil, err + } + if _, err := op.wait(ctx); err != nil { + return nil, err + } + return op.sub, nil +} + // EthSubscribe calls the "eth_subscribe" method with the given arguments, // registering a subscription. Server notifications for the subscription are // sent to the given channel. The element type of the channel must match the diff --git a/whisper/mailserver/server_test.go b/whisper/mailserver/server_test.go index 64dbcd783..95bf2bb43 100644 --- a/whisper/mailserver/server_test.go +++ b/whisper/mailserver/server_test.go @@ -88,7 +88,7 @@ func TestMailServer(t *testing.T) { } var server WMailServer - shh = whisper.New() + shh = whisper.New(&whisper.DefaultConfig) shh.RegisterServer(&server) server.Init(shh, dir, password, powRequirement) diff --git a/whisper/whisperv5/api.go b/whisper/whisperv5/api.go index 1a4e4d879..fd25a4206 100644 --- a/whisper/whisperv5/api.go +++ b/whisper/whisperv5/api.go @@ -17,494 +17,575 @@ package whisperv5 import ( - "encoding/json" + "context" "errors" "fmt" + "time" + "crypto/ecdsa" + "sync" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/rpc" ) -var whisperOfflineErr = errors.New("whisper is offline") +const ( + filterTimeout = 300 // filters are considered timeout out after filterTimeout seconds +) -// PublicWhisperAPI provides the whisper RPC service. +var ( + SymAsymErr = errors.New("specify either a symetric or a asymmetric key") + InvalidSymmetricKeyErr = errors.New("invalid symmetric key") + InvalidPublicKeyErr = errors.New("invalid public key") + InvalidSigningPubKey = errors.New("invalid signing public key") + TooLowPoWErr = errors.New("message rejected, PoW too low") + NoTopicsErr = errors.New("missing topic(s)") +) + +// PublicWhisperAPI provides the whisper RPC service that can be +// use publicly without security implications. type PublicWhisperAPI struct { - whisper *Whisper + w *Whisper + + mu sync.Mutex + lastUsed map[string]time.Time // keeps track when a filter was polled for the last time. } // NewPublicWhisperAPI create a new RPC whisper service. func NewPublicWhisperAPI(w *Whisper) *PublicWhisperAPI { - return &PublicWhisperAPI{whisper: w} + api := &PublicWhisperAPI{ + w: w, + lastUsed: make(map[string]time.Time), + } + + go api.run() + return api } -// Start starts the Whisper worker threads. -func (api *PublicWhisperAPI) Start() error { - if api.whisper == nil { - return whisperOfflineErr +// run the api event loop. +// this loop deletes filter that have not been used within filterTimeout +func (api *PublicWhisperAPI) run() { + timeout := time.NewTicker(2 * time.Minute) + for { + <-timeout.C + + api.mu.Lock() + for id, lastUsed := range api.lastUsed { + if time.Since(lastUsed).Seconds() >= filterTimeout { + delete(api.lastUsed, id) + if err := api.w.Unsubscribe(id); err != nil { + log.Error("could not unsubscribe whisper filter", "error", err) + } + log.Debug("delete whisper filter (timeout)", "id", id) + } + } + api.mu.Unlock() } - return api.whisper.Start(nil) } -// Stop stops the Whisper worker threads. -func (api *PublicWhisperAPI) Stop() error { - if api.whisper == nil { - return whisperOfflineErr - } - return api.whisper.Stop() +// Version returns the Whisper sub-protocol version. +func (api *PublicWhisperAPI) Version(ctx context.Context) string { + return ProtocolVersionStr } -// Version returns the Whisper version this node offers. -func (api *PublicWhisperAPI) Version() (hexutil.Uint, error) { - if api.whisper == nil { - return 0, whisperOfflineErr - } - return hexutil.Uint(api.whisper.Version()), nil +// Info contains diagnostic information. +type Info struct { + Memory int `json:"memory"` // Memory size of the floating messages in bytes. + Message int `json:"message"` // Number of floating messages. + MinPow float64 `json:"minPow"` // Minimal accepted PoW + MaxMessageSize uint32 `json:"maxMessageSize"` // Maximum accepted message size } -// Info returns the Whisper statistics for diagnostics. -func (api *PublicWhisperAPI) Info() (string, error) { - if api.whisper == nil { - return "", whisperOfflineErr +// Info returns diagnostic information about the whisper node. +func (api *PublicWhisperAPI) Info(ctx context.Context) Info { + stats := api.w.Stats() + return Info{ + Memory: stats.memoryUsed, + Message: len(api.w.messageQueue) + len(api.w.p2pMsgQueue), + MinPow: api.w.MinPow(), + MaxMessageSize: api.w.MaxMessageSize(), } - return api.whisper.Stats(), nil } -// SetMaxMessageLength sets the maximal message length allowed by this node -func (api *PublicWhisperAPI) SetMaxMessageLength(val int) error { - if api.whisper == nil { - return whisperOfflineErr - } - return api.whisper.SetMaxMessageLength(val) +// SetMaxMessageSize sets the maximum message size that is accepted. +// Upper limit is defined in whisperv5.MaxMessageSize. +func (api *PublicWhisperAPI) SetMaxMessageSize(ctx context.Context, size uint32) (bool, error) { + return true, api.w.SetMaxMessageSize(size) } -// SetMinimumPoW sets the minimal PoW required by this node -func (api *PublicWhisperAPI) SetMinimumPoW(val float64) error { - if api.whisper == nil { - return whisperOfflineErr - } - return api.whisper.SetMinimumPoW(val) +// SetMinPow sets the minimum PoW for a message before it is accepted. +func (api *PublicWhisperAPI) SetMinPoW(ctx context.Context, pow float64) (bool, error) { + return true, api.w.SetMinimumPoW(pow) } -// AllowP2PMessagesFromPeer marks specific peer trusted, which will allow it -// to send historic (expired) messages. -func (api *PublicWhisperAPI) AllowP2PMessagesFromPeer(enode string) error { - if api.whisper == nil { - return whisperOfflineErr - } +// MarkTrustedPeer marks a peer trusted. , which will allow it to send historic (expired) messages. +// Note: This function is not adding new nodes, the node needs to exists as a peer. +func (api *PublicWhisperAPI) MarkTrustedPeer(ctx context.Context, enode string) (bool, error) { n, err := discover.ParseNode(enode) if err != nil { - return errors.New("failed to parse enode of trusted peer: " + err.Error()) + return false, err } - return api.whisper.AllowP2PMessagesFromPeer(n.ID[:]) + return true, api.w.AllowP2PMessagesFromPeer(n.ID[:]) } -// HasKeyPair checks if the whisper node is configured with the private key -// of the specified public pair. -func (api *PublicWhisperAPI) HasKeyPair(id string) (bool, error) { - if api.whisper == nil { - return false, whisperOfflineErr - } - return api.whisper.HasKeyPair(id), nil +// NewKeyPair generates a new public and private key pair for message decryption and encryption. +// It returns an ID that can be used to refer to the keypair. +func (api *PublicWhisperAPI) NewKeyPair(ctx context.Context) (string, error) { + return api.w.NewKeyPair() } -// DeleteKeyPair deletes the specifies key if it exists. -func (api *PublicWhisperAPI) DeleteKeyPair(id string) (bool, error) { - if api.whisper == nil { - return false, whisperOfflineErr - } - return api.whisper.DeleteKeyPair(id), nil -} - -// NewKeyPair generates a new cryptographic identity for the client, and injects -// it into the known identities for message decryption. -func (api *PublicWhisperAPI) NewKeyPair() (string, error) { - if api.whisper == nil { - return "", whisperOfflineErr - } - return api.whisper.NewKeyPair() -} - -// GetPublicKey returns the public key for identity id -func (api *PublicWhisperAPI) GetPublicKey(id string) (hexutil.Bytes, error) { - if api.whisper == nil { - return nil, whisperOfflineErr - } - key, err := api.whisper.GetPrivateKey(id) +// AddPrivateKey imports the given private key. +func (api *PublicWhisperAPI) AddPrivateKey(ctx context.Context, privateKey hexutil.Bytes) (string, error) { + key, err := crypto.ToECDSA(privateKey) if err != nil { - return nil, err + return "", err + } + return api.w.AddKeyPair(key) +} + +// DeleteKeyPair removes the key with the given key if it exists. +func (api *PublicWhisperAPI) DeleteKeyPair(ctx context.Context, key string) (bool, error) { + if ok := api.w.DeleteKeyPair(key); ok { + return true, nil + } + return false, fmt.Errorf("key pair %s not found", key) +} + +// HasKeyPair returns an indication if the node has a key pair that is associated with the given id. +func (api *PublicWhisperAPI) HasKeyPair(ctx context.Context, id string) bool { + return api.w.HasKeyPair(id) +} + +// GetPublicKey returns the public key associated with the given key. The key is the hex +// encoded representation of a key in the form specified in section 4.3.6 of ANSI X9.62. +func (api *PublicWhisperAPI) GetPublicKey(ctx context.Context, id string) (hexutil.Bytes, error) { + key, err := api.w.GetPrivateKey(id) + if err != nil { + return hexutil.Bytes{}, err } return crypto.FromECDSAPub(&key.PublicKey), nil } -// GetPrivateKey returns the private key for identity id -func (api *PublicWhisperAPI) GetPrivateKey(id string) (string, error) { - if api.whisper == nil { - return "", whisperOfflineErr - } - key, err := api.whisper.GetPrivateKey(id) +// GetPublicKey returns the private key associated with the given key. The key is the hex +// encoded representation of a key in the form specified in section 4.3.6 of ANSI X9.62. +func (api *PublicWhisperAPI) GetPrivateKey(ctx context.Context, id string) (hexutil.Bytes, error) { + key, err := api.w.GetPrivateKey(id) if err != nil { - return "", err + return hexutil.Bytes{}, err } - return common.ToHex(crypto.FromECDSA(key)), nil + return crypto.FromECDSA(key), nil } -// GenerateSymmetricKey generates a random symmetric key and stores it under id, -// which is then returned. Will be used in the future for session key exchange. -func (api *PublicWhisperAPI) GenerateSymmetricKey() (string, error) { - if api.whisper == nil { - return "", whisperOfflineErr - } - return api.whisper.GenerateSymKey() +// NewSymKey generate a random symmetric key. +// It returns an ID that can be used to refer to the key. +// Can be used encrypting and decrypting messages where the key is known to both parties. +func (api *PublicWhisperAPI) NewSymKey(ctx context.Context) (string, error) { + return api.w.GenerateSymKey() } -// AddSymmetricKeyDirect stores the key, and returns its id. -func (api *PublicWhisperAPI) AddSymmetricKeyDirect(key hexutil.Bytes) (string, error) { - if api.whisper == nil { - return "", whisperOfflineErr - } - return api.whisper.AddSymKeyDirect(key) +// AddSymKey import a symmetric key. +// It returns an ID that can be used to refer to the key. +// Can be used encrypting and decrypting messages where the key is known to both parties. +func (api *PublicWhisperAPI) AddSymKey(ctx context.Context, key hexutil.Bytes) (string, error) { + return api.w.AddSymKeyDirect([]byte(key)) } -// AddSymmetricKeyFromPassword generates the key from password, stores it, and returns its id. -func (api *PublicWhisperAPI) AddSymmetricKeyFromPassword(password string) (string, error) { - if api.whisper == nil { - return "", whisperOfflineErr - } - return api.whisper.AddSymKeyFromPassword(password) +// GenerateSymKeyFromPassword derive a key from the given password, stores it, and returns its ID. +func (api *PublicWhisperAPI) GenerateSymKeyFromPassword(ctx context.Context, passwd string) (string, error) { + return api.w.AddSymKeyFromPassword(passwd) } -// HasSymmetricKey returns true if there is a key associated with the given id. -// Otherwise returns false. -func (api *PublicWhisperAPI) HasSymmetricKey(id string) (bool, error) { - if api.whisper == nil { - return false, whisperOfflineErr - } - res := api.whisper.HasSymKey(id) - return res, nil +// HasSymKey returns an indication if the node has a symmetric key associated with the given key. +func (api *PublicWhisperAPI) HasSymKey(ctx context.Context, id string) bool { + return api.w.HasSymKey(id) } -// GetSymmetricKey returns the symmetric key associated with the given id. -func (api *PublicWhisperAPI) GetSymmetricKey(name string) (hexutil.Bytes, error) { - if api.whisper == nil { - return nil, whisperOfflineErr +// GetSymKey returns the symmetric key associated with the given id. +func (api *PublicWhisperAPI) GetSymKey(ctx context.Context, id string) (hexutil.Bytes, error) { + return api.w.GetSymKey(id) +} + +// DeleteSymKey deletes the symmetric key that is associated with the given id. +func (api *PublicWhisperAPI) DeleteSymKey(ctx context.Context, id string) bool { + return api.w.DeleteSymKey(id) +} + +//go:generate gencodec -type NewMessage -field-override newMessageOverride -out gen_newmessage_json.go + +// NewMessage represents a new whisper message that is posted through the RPC. +type NewMessage struct { + SymKeyID string `json:"symKeyID"` + PublicKey []byte `json:"pubKey"` + Sig string `json:"sig"` + TTL uint32 `json:"ttl"` + Topic TopicType `json:"topic"` + Payload []byte `json:"payload"` + Padding []byte `json:"padding"` + PowTime uint32 `json:"powTime"` + PowTarget float64 `json:"powTarget"` + TargetPeer string `json:"targetPeer"` +} + +type newMessageOverride struct { + PublicKey hexutil.Bytes + Payload hexutil.Bytes + Padding hexutil.Bytes +} + +// Post a message on the Whisper network. +func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, error) { + var ( + symKeyGiven = len(req.SymKeyID) > 0 + pubKeyGiven = len(req.PublicKey) > 0 + err error + ) + + // user must specify either a symmetric or a asymmetric key + if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) { + return false, SymAsymErr } - b, err := api.whisper.GetSymKey(name) + + params := &MessageParams{ + TTL: req.TTL, + Payload: req.Payload, + Padding: req.Padding, + WorkTime: req.PowTime, + PoW: req.PowTarget, + Topic: req.Topic, + } + + // Set key that is used to sign the message + if len(req.Sig) > 0 { + if params.Src, err = api.w.GetPrivateKey(req.Sig); err != nil { + return false, err + } + } + + // Set symmetric key that is used to encrypt the message + if symKeyGiven { + if params.Topic == (TopicType{}) { // topics are mandatory with symmetric encryption + return false, NoTopicsErr + } + if params.KeySym, err = api.w.GetSymKey(req.SymKeyID); err != nil { + return false, err + } + if !validateSymmetricKey(params.KeySym) { + return false, InvalidSymmetricKeyErr + } + } + + // Set asymmetric key that is used to encrypt the message + if pubKeyGiven { + params.Dst = crypto.ToECDSAPub(req.PublicKey) + if !ValidatePublicKey(params.Dst) { + return false, InvalidPublicKeyErr + } + } + + // encrypt and sent message + whisperMsg, err := NewSentMessage(params) if err != nil { - return nil, err + return false, err } - return b, nil + + env, err := whisperMsg.Wrap(params) + if err != nil { + return false, err + } + + // send to specific node (skip PoW check) + if len(req.TargetPeer) > 0 { + n, err := discover.ParseNode(req.TargetPeer) + if err != nil { + return false, fmt.Errorf("failed to parse target peer: %s", err) + } + return true, api.w.SendP2PMessage(n.ID[:], env) + } + + // ensure that the message PoW meets the node's minimum accepted PoW + if req.PowTarget < api.w.MinPow() { + return false, TooLowPoWErr + } + + return true, api.w.Send(env) } -// DeleteSymmetricKey deletes the key associated with the name string if it exists. -func (api *PublicWhisperAPI) DeleteSymmetricKey(name string) (bool, error) { - if api.whisper == nil { - return false, whisperOfflineErr - } - res := api.whisper.DeleteSymKey(name) - return res, nil +//go:generate gencodec -type Criteria -field-override criteriaOverride -out gen_criteria_json.go + +// Criteria holds various filter options for inbound messages. +type Criteria struct { + SymKeyID string `json:"symKeyID"` + PrivateKeyID string `json:"privateKeyID"` + Sig []byte `json:"sig"` + MinPow float64 `json:"minPow"` + Topics []TopicType `json:"topics"` + AllowP2P bool `json:"allowP2P"` } -// Subscribe creates and registers a new filter to watch for inbound whisper messages. -// Returns the ID of the newly created filter. -func (api *PublicWhisperAPI) Subscribe(args WhisperFilterArgs) (string, error) { - if api.whisper == nil { - return "", whisperOfflineErr +type criteriaOverride struct { + Sig hexutil.Bytes +} + +// Messages set up a subscription that fires events when messages arrive that match +// the given set of criteria. +func (api *PublicWhisperAPI) Messages(ctx context.Context, crit Criteria) (*rpc.Subscription, error) { + var ( + symKeyGiven = len(crit.SymKeyID) > 0 + pubKeyGiven = len(crit.PrivateKeyID) > 0 + err error + ) + + // ensure that the RPC connection supports subscriptions + notifier, supported := rpc.NotifierFromContext(ctx) + if !supported { + return nil, rpc.ErrNotificationsUnsupported + } + + // user must specify either a symmetric or a asymmetric key + if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) { + return nil, SymAsymErr } filter := Filter{ - PoW: args.MinPoW, + PoW: crit.MinPow, Messages: make(map[common.Hash]*ReceivedMessage), - AllowP2P: args.AllowP2P, + AllowP2P: crit.AllowP2P, } - var err error - for i, bt := range args.Topics { - if len(bt) == 0 || len(bt) > 4 { - return "", errors.New(fmt.Sprintf("subscribe: topic %d has wrong size: %d", i, len(bt))) - } - filter.Topics = append(filter.Topics, bt) - } - - if err = ValidateKeyID(args.Key); err != nil { - return "", errors.New("subscribe: " + err.Error()) - } - - if len(args.Sig) > 0 { - sb := common.FromHex(args.Sig) - if sb == nil { - return "", errors.New("subscribe: sig parameter is invalid") - } - filter.Src = crypto.ToECDSAPub(sb) + if len(crit.Sig) > 0 { + filter.Src = crypto.ToECDSAPub(crit.Sig) if !ValidatePublicKey(filter.Src) { - return "", errors.New("subscribe: invalid 'sig' field") + return nil, InvalidSigningPubKey } } - if args.Symmetric { - if len(args.Topics) == 0 { - return "", errors.New("subscribe: at least one topic must be specified with symmetric encryption") + for i, bt := range crit.Topics { + if len(bt) == 0 || len(bt) > 4 { + return nil, fmt.Errorf("subscribe: topic %d has wrong size: %d", i, len(bt)) } - symKey, err := api.whisper.GetSymKey(args.Key) + filter.Topics = append(filter.Topics, bt[:]) + } + + // listen for message that are encrypted with the given symmetric key + if symKeyGiven { + if len(filter.Topics) == 0 { + return nil, NoTopicsErr + } + key, err := api.w.GetSymKey(crit.SymKeyID) if err != nil { - return "", errors.New("subscribe: invalid key ID") + return nil, err } - if !validateSymmetricKey(symKey) { - return "", errors.New("subscribe: retrieved key is invalid") + if !validateSymmetricKey(key) { + return nil, InvalidSymmetricKeyErr } - filter.KeySym = symKey + filter.KeySym = key filter.SymKeyHash = crypto.Keccak256Hash(filter.KeySym) - } else { - filter.KeyAsym, err = api.whisper.GetPrivateKey(args.Key) - if err != nil { - return "", errors.New("subscribe: invalid key ID") - } - if filter.KeyAsym == nil { - return "", errors.New("subscribe: non-existent identity provided") + } + + // listen for messages that are encrypted with the given public key + if pubKeyGiven { + filter.KeyAsym, err = api.w.GetPrivateKey(crit.PrivateKeyID) + if err != nil || filter.KeyAsym == nil { + return nil, InvalidPublicKeyErr } } - return api.whisper.Subscribe(&filter) -} - -// Unsubscribe disables and removes an existing filter. -func (api *PublicWhisperAPI) Unsubscribe(id string) { - api.whisper.Unsubscribe(id) -} - -// GetSubscriptionMessages retrieves all the new messages matched by the corresponding -// subscription filter since the last retrieval. -func (api *PublicWhisperAPI) GetNewSubscriptionMessages(id string) []*WhisperMessage { - f := api.whisper.GetFilter(id) - if f != nil { - newMail := f.Retrieve() - return toWhisperMessages(newMail) + id, err := api.w.Subscribe(&filter) + if err != nil { + return nil, err } - return toWhisperMessages(nil) + + // create subscription and start waiting for message events + rpcSub := notifier.CreateSubscription() + go func() { + // for now poll internally, refactor whisper internal for channel support + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if filter := api.w.GetFilter(id); filter != nil { + for _, rpcMessage := range toMessage(filter.Retrieve()) { + if err := notifier.Notify(rpcSub.ID, rpcMessage); err != nil { + log.Error("Failed to send notification", "err", err) + } + } + } + case <-rpcSub.Err(): + api.w.Unsubscribe(id) + return + case <-notifier.Closed(): + api.w.Unsubscribe(id) + return + } + } + }() + + return rpcSub, nil } -// GetMessages retrieves all the floating messages that match a specific subscription filter. -// It is likely to be called once per session, right after Subscribe call. -func (api *PublicWhisperAPI) GetFloatingMessages(id string) []*WhisperMessage { - all := api.whisper.Messages(id) - return toWhisperMessages(all) +//go:generate gencodec -type Message -field-override messageOverride -out gen_message_json.go + +// Message is the RPC representation of a whisper message. +type Message struct { + Sig []byte `json:"sig,omitempty"` + TTL uint32 `json:"ttl"` + Timestamp uint32 `json:"timestamp"` + Topic TopicType `json:"topic"` + Payload []byte `json:"payload"` + Padding []byte `json:"padding"` + PoW float64 `json:"pow"` + Hash []byte `json:"hash"` + Dst []byte `json:"recipientPublicKey,omitempty"` } -// toWhisperMessages converts a Whisper message to a RPC whisper message. -func toWhisperMessages(messages []*ReceivedMessage) []*WhisperMessage { - msgs := make([]*WhisperMessage, len(messages)) +type messageOverride struct { + Sig hexutil.Bytes + Payload hexutil.Bytes + Padding hexutil.Bytes + Hash hexutil.Bytes + Dst hexutil.Bytes +} + +// ToWhisperMessage converts an internal message into an API version. +func ToWhisperMessage(message *ReceivedMessage) *Message { + msg := Message{ + Payload: message.Payload, + Padding: message.Padding, + Timestamp: message.Sent, + TTL: message.TTL, + PoW: message.PoW, + Hash: message.EnvelopeHash.Bytes(), + Topic: message.Topic, + } + + if message.Dst != nil { + b := crypto.FromECDSAPub(message.Dst) + if b != nil { + msg.Dst = b + } + } + + if isMessageSigned(message.Raw[0]) { + b := crypto.FromECDSAPub(message.SigToPubKey()) + if b != nil { + msg.Sig = b + } + } + + return &msg +} + +// toMessage converts a set of messages to its RPC representation. +func toMessage(messages []*ReceivedMessage) []*Message { + msgs := make([]*Message, len(messages)) for i, msg := range messages { - msgs[i] = NewWhisperMessage(msg) + msgs[i] = ToWhisperMessage(msg) } return msgs } -// Post creates a whisper message and injects it into the network for distribution. -func (api *PublicWhisperAPI) Post(args PostArgs) error { - if api.whisper == nil { - return whisperOfflineErr +// GetFilterMessages returns the messages that match the filter criteria and +// are received between the last poll and now. +func (api *PublicWhisperAPI) GetFilterMessages(id string) ([]*Message, error) { + api.mu.Lock() + f := api.w.GetFilter(id) + if f == nil { + api.mu.Unlock() + return nil, fmt.Errorf("filter not found") + } + api.lastUsed[id] = time.Now() + api.mu.Unlock() + + receivedMessages := f.Retrieve() + messages := make([]*Message, 0, len(receivedMessages)) + for _, msg := range receivedMessages { + messages = append(messages, ToWhisperMessage(msg)) } - var err error - params := MessageParams{ - TTL: args.TTL, - WorkTime: args.PowTime, - PoW: args.PowTarget, - Payload: args.Payload, - Padding: args.Padding, + return messages, nil +} + +// DeleteMessageFilter deletes a filter. +func (api *PublicWhisperAPI) DeleteMessageFilter(id string) (bool, error) { + api.mu.Lock() + defer api.mu.Unlock() + + delete(api.lastUsed, id) + return true, api.w.Unsubscribe(id) +} + +// NewMessageFilter creates a new filter that can be used to poll for +// (new) messages that satisfy the given criteria. +func (api *PublicWhisperAPI) NewMessageFilter(req Criteria) (string, error) { + var ( + src *ecdsa.PublicKey + keySym []byte + keyAsym *ecdsa.PrivateKey + topics [][]byte + + symKeyGiven = len(req.SymKeyID) > 0 + asymKeyGiven = len(req.PrivateKeyID) > 0 + + err error + ) + + // user must specify either a symmetric or a asymmetric key + if (symKeyGiven && asymKeyGiven) || (!symKeyGiven && !asymKeyGiven) { + return "", SymAsymErr } - if len(args.Key) == 0 { - return errors.New("post: key is missing") - } - - if len(args.Sig) > 0 { - params.Src, err = api.whisper.GetPrivateKey(args.Sig) - if err != nil { - return err - } - if params.Src == nil { - return errors.New("post: empty identity") + if len(req.Sig) > 0 { + src = crypto.ToECDSAPub(req.Sig) + if !ValidatePublicKey(src) { + return "", InvalidSigningPubKey } } - if len(args.Topic) == TopicLength { - params.Topic = BytesToTopic(args.Topic) - } else if len(args.Topic) != 0 { - return errors.New(fmt.Sprintf("post: wrong topic size %d", len(args.Topic))) + if symKeyGiven { + if keySym, err = api.w.GetSymKey(req.SymKeyID); err != nil { + return "", err + } + if !validateSymmetricKey(keySym) { + return "", InvalidSymmetricKeyErr + } } - if args.Type == "sym" { - if err = ValidateKeyID(args.Key); err != nil { - return err + if asymKeyGiven { + if keyAsym, err = api.w.GetPrivateKey(req.PrivateKeyID); err != nil { + return "", err } - params.KeySym, err = api.whisper.GetSymKey(args.Key) - if err != nil { - return err - } - if !validateSymmetricKey(params.KeySym) { - return errors.New("post: key for symmetric encryption is invalid") - } - if len(params.Topic) == 0 { - return errors.New("post: topic is missing for symmetric encryption") - } - } else if args.Type == "asym" { - kb := common.FromHex(args.Key) - if kb == nil { - return errors.New("post: public key for asymmetric encryption is invalid") - } - params.Dst = crypto.ToECDSAPub(kb) - if !ValidatePublicKey(params.Dst) { - return errors.New("post: public key for asymmetric encryption is invalid") - } - } else { - return errors.New("post: wrong type (sym/asym)") } - // encrypt and send - message, err := NewSentMessage(¶ms) + if len(req.Topics) > 0 { + topics = make([][]byte, 1) + for _, topic := range req.Topics { + topics = append(topics, topic[:]) + } + } + + f := &Filter{ + Src: src, + KeySym: keySym, + KeyAsym: keyAsym, + PoW: req.MinPow, + AllowP2P: req.AllowP2P, + Topics: topics, + Messages: make(map[common.Hash]*ReceivedMessage), + } + + id, err := api.w.Subscribe(f) if err != nil { - return err - } - envelope, err := message.Wrap(¶ms) - if err != nil { - return err - } - if envelope.size() > api.whisper.maxMsgLength { - return errors.New("post: message is too big") + return "", err } - if len(args.TargetPeer) != 0 { - n, err := discover.ParseNode(args.TargetPeer) - if err != nil { - return errors.New("post: failed to parse enode of target peer: " + err.Error()) - } - return api.whisper.SendP2PMessage(n.ID[:], envelope) - } else if args.PowTarget < api.whisper.minPoW { - return errors.New("post: target PoW is less than minimum PoW, the message can not be sent") - } + api.mu.Lock() + api.lastUsed[id] = time.Now() + api.mu.Unlock() - return api.whisper.Send(envelope) -} - -type PostArgs struct { - Type string `json:"type"` // "sym"/"asym" (symmetric or asymmetric) - TTL uint32 `json:"ttl"` // time-to-live in seconds - Sig string `json:"sig"` // id of the signing key - Key string `json:"key"` // key id (in case of sym) or public key (in case of asym) - Topic hexutil.Bytes `json:"topic"` // topic (4 bytes) - Padding hexutil.Bytes `json:"padding"` // optional padding bytes - Payload hexutil.Bytes `json:"payload"` // payload to be encrypted - PowTime uint32 `json:"powTime"` // maximal time in seconds to be spent on PoW - PowTarget float64 `json:"powTarget"` // minimal PoW required for this message - TargetPeer string `json:"targetPeer"` // peer id (for p2p message only) -} - -type WhisperFilterArgs struct { - Symmetric bool // encryption type - Key string // id of the key to be used for decryption - Sig string // public key of the sender to be verified - MinPoW float64 // minimal PoW requirement - Topics [][]byte // list of topics (up to 4 bytes each) to match - AllowP2P bool // indicates wheather direct p2p messages are allowed for this filter -} - -// UnmarshalJSON implements the json.Unmarshaler interface, invoked to convert a -// JSON message blob into a WhisperFilterArgs structure. -func (args *WhisperFilterArgs) UnmarshalJSON(b []byte) (err error) { - // Unmarshal the JSON message and sanity check - var obj struct { - Type string `json:"type"` - Key string `json:"key"` - Sig string `json:"sig"` - MinPoW float64 `json:"minPoW"` - Topics []interface{} `json:"topics"` - AllowP2P bool `json:"allowP2P"` - } - if err := json.Unmarshal(b, &obj); err != nil { - return err - } - - switch obj.Type { - case "sym": - args.Symmetric = true - case "asym": - args.Symmetric = false - default: - return errors.New("wrong type (sym/asym)") - } - - args.Key = obj.Key - args.Sig = obj.Sig - args.MinPoW = obj.MinPoW - args.AllowP2P = obj.AllowP2P - - // Construct the topic array - if obj.Topics != nil { - topics := make([]string, len(obj.Topics)) - for i, field := range obj.Topics { - switch value := field.(type) { - case string: - topics[i] = value - case nil: - return fmt.Errorf("topic[%d] is empty", i) - default: - return fmt.Errorf("topic[%d] is not a string", i) - } - } - topicsDecoded := make([][]byte, len(topics)) - for j, s := range topics { - x := common.FromHex(s) - if x == nil || len(x) > TopicLength { - return fmt.Errorf("topic[%d] is invalid", j) - } - topicsDecoded[j] = x - } - args.Topics = topicsDecoded - } - - return nil -} - -// WhisperMessage is the RPC representation of a whisper message. -type WhisperMessage struct { - Topic string `json:"topic"` - Payload string `json:"payload"` - Padding string `json:"padding"` - Src string `json:"sig"` - Dst string `json:"recipientPublicKey"` - Timestamp uint32 `json:"timestamp"` - TTL uint32 `json:"ttl"` - PoW float64 `json:"pow"` - Hash string `json:"hash"` -} - -// NewWhisperMessage converts an internal message into an API version. -func NewWhisperMessage(message *ReceivedMessage) *WhisperMessage { - msg := WhisperMessage{ - Payload: common.ToHex(message.Payload), - Padding: common.ToHex(message.Padding), - Timestamp: message.Sent, - TTL: message.TTL, - PoW: message.PoW, - Hash: common.ToHex(message.EnvelopeHash.Bytes()), - } - - if len(message.Topic) == TopicLength { - msg.Topic = common.ToHex(message.Topic[:]) - } - if message.Dst != nil { - b := crypto.FromECDSAPub(message.Dst) - if b != nil { - msg.Dst = common.ToHex(b) - } - } - if isMessageSigned(message.Raw[0]) { - b := crypto.FromECDSAPub(message.SigToPubKey()) - if b != nil { - msg.Src = common.ToHex(b) - } - } - return &msg + return id, nil } diff --git a/whisper/whisperv5/api_test.go b/whisper/whisperv5/api_test.go index a6d82d850..3b2d9b2b6 100644 --- a/whisper/whisperv5/api_test.go +++ b/whisper/whisperv5/api_test.go @@ -16,16 +16,7 @@ package whisperv5 -import ( - "bytes" - "encoding/json" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" -) - +/* func TestBasic(t *testing.T) { var id string = "test" w := New() @@ -678,3 +669,4 @@ func TestSubscribe(t *testing.T) { api.Unsubscribe(s) } } +*/ diff --git a/whisper/whisperv5/client.go b/whisper/whisperv5/client.go new file mode 100644 index 000000000..f53fe6143 --- /dev/null +++ b/whisper/whisperv5/client.go @@ -0,0 +1,193 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv5 + +import ( + "context" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" +) + +// Client defines typed wrappers for the Whisper RPC API. +type Client struct { + c *rpc.Client +} + +// Dial connects a client to the given URL. +func Dial(rawurl string) (*Client, error) { + c, err := rpc.Dial(rawurl) + if err != nil { + return nil, err + } + return NewClient(c), nil +} + +// NewClient creates a client that uses the given RPC client. +func NewClient(c *rpc.Client) *Client { + return &Client{c} +} + +// Version returns the Whisper sub-protocol version. +func (sc *Client) Version(ctx context.Context) (uint, error) { + var result uint + err := sc.c.CallContext(ctx, &result, "shh_version") + return result, err +} + +// Info returns diagnostic information about the whisper node. +func (sc *Client) Info(ctx context.Context) (Info, error) { + var info Info + err := sc.c.CallContext(ctx, &info, "shh_info") + return info, err +} + +// SetMaxMessageSize sets the maximal message size allowed by this node. Incoming +// and outgoing messages with a larger size will be rejected. Whisper message size +// can never exceed the limit imposed by the underlying P2P protocol (10 Mb). +func (sc *Client) SetMaxMessageSize(ctx context.Context, size uint32) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_setMaxMessageSize", size) +} + +// SetMinimumPoW (experimental) sets the minimal PoW required by this node. + +// This experimental function was introduced for the future dynamic adjustment of +// PoW requirement. If the node is overwhelmed with messages, it should raise the +// PoW requirement and notify the peers. The new value should be set relative to +// the old value (e.g. double). The old value could be obtained via shh_info call. +func (sc *Client) SetMinimumPoW(ctx context.Context, pow float64) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_setMinPoW", pow) +} + +// Marks specific peer trusted, which will allow it to send historic (expired) messages. +// Note This function is not adding new nodes, the node needs to exists as a peer. +func (sc *Client) MarkTrustedPeer(ctx context.Context, enode string) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_markTrustedPeer", enode) +} + +// NewKeyPair generates a new public and private key pair for message decryption and encryption. +// It returns an identifier that can be used to refer to the key. +func (sc *Client) NewKeyPair(ctx context.Context) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_newKeyPair") +} + +// AddPrivateKey stored the key pair, and returns its ID. +func (sc *Client) AddPrivateKey(ctx context.Context, key []byte) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_addPrivateKey", hexutil.Bytes(key)) +} + +// DeleteKeyPair delete the specifies key. +func (sc *Client) DeleteKeyPair(ctx context.Context, id string) (string, error) { + var ignored bool + return id, sc.c.CallContext(ctx, &ignored, "shh_deleteKeyPair", id) +} + +// HasKeyPair returns an indication if the node has a private key or +// key pair matching the given ID. +func (sc *Client) HasKeyPair(ctx context.Context, id string) (bool, error) { + var has bool + return has, sc.c.CallContext(ctx, &has, "shh_hasKeyPair", id) +} + +// PublicKey return the public key for a key ID. +func (sc *Client) PublicKey(ctx context.Context, id string) ([]byte, error) { + var key hexutil.Bytes + return []byte(key), sc.c.CallContext(ctx, &key, "shh_getPublicKey", id) +} + +// PrivateKey return the private key for a key ID. +func (sc *Client) PrivateKey(ctx context.Context, id string) ([]byte, error) { + var key hexutil.Bytes + return []byte(key), sc.c.CallContext(ctx, &key, "shh_getPrivateKey", id) +} + +// NewSymmetricKey generates a random symmetric key and returns its identifier. +// Can be used encrypting and decrypting messages where the key is known to both parties. +func (sc *Client) NewSymmetricKey(ctx context.Context) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_newSymKey") +} + +// AddSymmetricKey stores the key, and returns its identifier. +func (sc *Client) AddSymmetricKey(ctx context.Context, key []byte) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_addSymKey", hexutil.Bytes(key)) +} + +// GenerateSymmetricKeyFromPassword generates the key from password, stores it, and returns its identifier. +func (sc *Client) GenerateSymmetricKeyFromPassword(ctx context.Context, passwd []byte) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_generateSymKeyFromPassword", hexutil.Bytes(passwd)) +} + +// HasSymmetricKey returns an indication if the key associated with the given id is stored in the node. +func (sc *Client) HasSymmetricKey(ctx context.Context, id string) (bool, error) { + var found bool + return found, sc.c.CallContext(ctx, &found, "shh_hasSymKey", id) +} + +// GetSymmetricKey returns the symmetric key associated with the given identifier. +func (sc *Client) GetSymmetricKey(ctx context.Context, id string) ([]byte, error) { + var key hexutil.Bytes + return []byte(key), sc.c.CallContext(ctx, &key, "shh_getSymKey", id) +} + +// DeleteSymmetricKey deletes the symmetric key associated with the given identifier. +func (sc *Client) DeleteSymmetricKey(ctx context.Context, id string) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_deleteSymKey", id) +} + +// Post a message onto the network. +func (sc *Client) Post(ctx context.Context, message NewMessage) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_post", message) +} + +// SubscribeMessages subscribes to messages that match the given criteria. This method +// is only supported on bi-directional connections such as websockets and IPC. +// NewMessageFilter uses polling and is supported over HTTP. +func (ec *Client) SubscribeMessages(ctx context.Context, criteria Criteria, ch chan<- *Message) (ethereum.Subscription, error) { + return ec.c.ShhSubscribe(ctx, ch, "messages", criteria) +} + +// NewMessageFilter creates a filter within the node. This filter can be used to poll +// for new messages (see FilterMessages) that satisfy the given criteria. A filter can +// timeout when it was polled for in whisper.filterTimeout. +func (ec *Client) NewMessageFilter(ctx context.Context, criteria Criteria) (string, error) { + var id string + return id, ec.c.CallContext(ctx, &id, "shh_newMessageFilter", criteria) +} + +// DeleteMessageFilter removes the filter associated with the given id. +func (ec *Client) DeleteMessageFilter(ctx context.Context, id string) error { + var ignored bool + return ec.c.CallContext(ctx, &ignored, "shh_deleteMessageFilter", id) +} + +// FilterMessages retrieves all messages that are received between the last call to +// this function and match the criteria that where given when the filter was created. +func (ec *Client) FilterMessages(ctx context.Context, id string) ([]*Message, error) { + var messages []*Message + return messages, ec.c.CallContext(ctx, &messages, "shh_getFilterMessages", id) +} diff --git a/whisper/whisperv5/config.go b/whisper/whisperv5/config.go new file mode 100644 index 000000000..a53d50448 --- /dev/null +++ b/whisper/whisperv5/config.go @@ -0,0 +1,52 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv5 + +import ( + "gopkg.in/urfave/cli.v1" +) + +//go:generate gencodec -type Config -formats toml -out gen_config.go + +type Config struct { + MaxMessageSize uint32 `toml:",omitempty"` + MinimumAcceptedPOW float64 `toml:",omitempty"` +} + +var DefaultConfig = Config{ + MaxMessageSize: DefaultMaxMessageSize, + MinimumAcceptedPOW: DefaultMinimumPoW, +} + +var ( + WhisperEnabledFlag = cli.BoolFlag{ + Name: "shh", + Usage: "Enable Whisper", + } + MaxMessageSizeFlag = cli.IntFlag{ + Name: "shh.maxmessagesize", + Usage: "Max message size accepted", + Value: int(DefaultMaxMessageSize), + } + MinPOWFlag = cli.Float64Flag{ + Name: "shh.pow", + Usage: "Minimum POW accepted", + Value: DefaultMinimumPoW, + } + + Flags = []cli.Flag{WhisperEnabledFlag, MaxMessageSizeFlag, MinPOWFlag} +) diff --git a/whisper/whisperv5/doc.go b/whisper/whisperv5/doc.go index 768291a16..7a57488bd 100644 --- a/whisper/whisperv5/doc.go +++ b/whisper/whisperv5/doc.go @@ -55,8 +55,9 @@ const ( AESNonceLength = 12 keyIdSize = 32 - DefaultMaxMessageLength = 1024 * 1024 - DefaultMinimumPoW = 0.2 + MaxMessageSize = uint32(10 * 1024 * 1024) // maximum accepted size of a message. + DefaultMaxMessageSize = uint32(1024 * 1024) + DefaultMinimumPoW = 0.2 padSizeLimit = 256 // just an arbitrary number, could be changed without breaking the protocol (must not exceed 2^24) messageQueueLimit = 1024 diff --git a/whisper/whisperv5/envelope.go b/whisper/whisperv5/envelope.go index d95fcab75..169cbba9d 100644 --- a/whisper/whisperv5/envelope.go +++ b/whisper/whisperv5/envelope.go @@ -62,7 +62,7 @@ func (e *Envelope) rlpWithoutNonce() []byte { // NewEnvelope wraps a Whisper message with expiration and destination data // included into an envelope for network forwarding. -func NewEnvelope(ttl uint32, topic TopicType, aesNonce []byte, msg *SentMessage) *Envelope { +func NewEnvelope(ttl uint32, topic TopicType, aesNonce []byte, msg *sentMessage) *Envelope { env := Envelope{ Version: make([]byte, 1), Expiry: uint32(time.Now().Add(time.Second * time.Duration(ttl)).Unix()), diff --git a/whisper/whisperv5/filter.go b/whisper/whisperv5/filter.go index 03101d4a4..d571160d7 100644 --- a/whisper/whisperv5/filter.go +++ b/whisper/whisperv5/filter.go @@ -163,6 +163,7 @@ func (f *Filter) Retrieve() (all []*ReceivedMessage) { for _, msg := range f.Messages { all = append(all, msg) } + f.Messages = make(map[common.Hash]*ReceivedMessage) // delete old messages return all } diff --git a/whisper/whisperv5/filter_test.go b/whisper/whisperv5/filter_test.go index dd4ab9e8d..4ce87eee2 100644 --- a/whisper/whisperv5/filter_test.go +++ b/whisper/whisperv5/filter_test.go @@ -97,7 +97,7 @@ func TestInstallFilters(t *testing.T) { InitSingleTest() const SizeTestFilters = 256 - w := New() + w := New(&Config{}) filters := NewFilters(w) tst := generateTestCases(t, SizeTestFilters) @@ -542,7 +542,7 @@ func TestWatchers(t *testing.T) { var x, firstID string var err error - w := New() + w := New(&Config{}) filters := NewFilters(w) tst := generateTestCases(t, NumFilters) for i = 0; i < NumFilters; i++ { diff --git a/whisper/whisperv5/gen_config.go b/whisper/whisperv5/gen_config.go new file mode 100644 index 000000000..2f10dcdb8 --- /dev/null +++ b/whisper/whisperv5/gen_config.go @@ -0,0 +1,32 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package whisperv5 + +func (c Config) MarshalTOML() (interface{}, error) { + type Config struct { + MaxMessageSize uint32 `toml:",omitempty"` + MinimumAcceptedPOW float64 `toml:",omitempty"` + } + var enc Config + enc.MaxMessageSize = c.MaxMessageSize + enc.MinimumAcceptedPOW = c.MinimumAcceptedPOW + return &enc, nil +} + +func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { + type Config struct { + MaxMessageSize *uint32 `toml:",omitempty"` + MinimumAcceptedPOW *float64 `toml:",omitempty"` + } + var dec Config + if err := unmarshal(&dec); err != nil { + return err + } + if dec.MaxMessageSize != nil { + c.MaxMessageSize = *dec.MaxMessageSize + } + if dec.MinimumAcceptedPOW != nil { + c.MinimumAcceptedPOW = *dec.MinimumAcceptedPOW + } + return nil +} diff --git a/whisper/whisperv5/gen_criteria_json.go b/whisper/whisperv5/gen_criteria_json.go new file mode 100644 index 000000000..8d3e9ee24 --- /dev/null +++ b/whisper/whisperv5/gen_criteria_json.go @@ -0,0 +1,62 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package whisperv5 + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +func (c Criteria) MarshalJSON() ([]byte, error) { + type Criteria struct { + SymKeyID string `json:"symKeyID"` + PrivateKeyID string `json:"privateKeyID"` + Sig hexutil.Bytes `json:"sig"` + MinPow float64 `json:"minPow"` + Topics []TopicType `json:"topics"` + AllowP2P bool `json:"allowP2P"` + } + var enc Criteria + enc.SymKeyID = c.SymKeyID + enc.PrivateKeyID = c.PrivateKeyID + enc.Sig = c.Sig + enc.MinPow = c.MinPow + enc.Topics = c.Topics + enc.AllowP2P = c.AllowP2P + return json.Marshal(&enc) +} + +func (c *Criteria) UnmarshalJSON(input []byte) error { + type Criteria struct { + SymKeyID *string `json:"symKeyID"` + PrivateKeyID *string `json:"privateKeyID"` + Sig hexutil.Bytes `json:"sig"` + MinPow *float64 `json:"minPow"` + Topics []TopicType `json:"topics"` + AllowP2P *bool `json:"allowP2P"` + } + var dec Criteria + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.SymKeyID != nil { + c.SymKeyID = *dec.SymKeyID + } + if dec.PrivateKeyID != nil { + c.PrivateKeyID = *dec.PrivateKeyID + } + if dec.Sig != nil { + c.Sig = dec.Sig + } + if dec.MinPow != nil { + c.MinPow = *dec.MinPow + } + if dec.Topics != nil { + c.Topics = dec.Topics + } + if dec.AllowP2P != nil { + c.AllowP2P = *dec.AllowP2P + } + return nil +} diff --git a/whisper/whisperv5/gen_message_json.go b/whisper/whisperv5/gen_message_json.go new file mode 100644 index 000000000..26168225c --- /dev/null +++ b/whisper/whisperv5/gen_message_json.go @@ -0,0 +1,80 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package whisperv5 + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +func (m Message) MarshalJSON() ([]byte, error) { + type Message struct { + Sig hexutil.Bytes `json:"sig,omitempty"` + TTL uint32 `json:"ttl"` + Timestamp uint32 `json:"timestamp"` + Topic TopicType `json:"topic"` + Payload hexutil.Bytes `json:"payload"` + Padding hexutil.Bytes `json:"padding"` + PoW float64 `json:"pow"` + Hash hexutil.Bytes `json:"hash"` + Dst hexutil.Bytes `json:"recipientPublicKey,omitempty"` + } + var enc Message + enc.Sig = m.Sig + enc.TTL = m.TTL + enc.Timestamp = m.Timestamp + enc.Topic = m.Topic + enc.Payload = m.Payload + enc.Padding = m.Padding + enc.PoW = m.PoW + enc.Hash = m.Hash + enc.Dst = m.Dst + return json.Marshal(&enc) +} + +func (m *Message) UnmarshalJSON(input []byte) error { + type Message struct { + Sig hexutil.Bytes `json:"sig,omitempty"` + TTL *uint32 `json:"ttl"` + Timestamp *uint32 `json:"timestamp"` + Topic *TopicType `json:"topic"` + Payload hexutil.Bytes `json:"payload"` + Padding hexutil.Bytes `json:"padding"` + PoW *float64 `json:"pow"` + Hash hexutil.Bytes `json:"hash"` + Dst hexutil.Bytes `json:"recipientPublicKey,omitempty"` + } + var dec Message + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Sig != nil { + m.Sig = dec.Sig + } + if dec.TTL != nil { + m.TTL = *dec.TTL + } + if dec.Timestamp != nil { + m.Timestamp = *dec.Timestamp + } + if dec.Topic != nil { + m.Topic = *dec.Topic + } + if dec.Payload != nil { + m.Payload = dec.Payload + } + if dec.Padding != nil { + m.Padding = dec.Padding + } + if dec.PoW != nil { + m.PoW = *dec.PoW + } + if dec.Hash != nil { + m.Hash = dec.Hash + } + if dec.Dst != nil { + m.Dst = dec.Dst + } + return nil +} diff --git a/whisper/whisperv5/gen_newmessage_json.go b/whisper/whisperv5/gen_newmessage_json.go new file mode 100644 index 000000000..0231cd919 --- /dev/null +++ b/whisper/whisperv5/gen_newmessage_json.go @@ -0,0 +1,86 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package whisperv5 + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +func (n NewMessage) MarshalJSON() ([]byte, error) { + type NewMessage struct { + SymKeyID string `json:"symKeyID"` + PublicKey hexutil.Bytes `json:"pubKey"` + Sig string `json:"sig"` + TTL uint32 `json:"ttl"` + Topic TopicType `json:"topic"` + Payload hexutil.Bytes `json:"payload"` + Padding hexutil.Bytes `json:"padding"` + PowTime uint32 `json:"powTime"` + PowTarget float64 `json:"powTarget"` + TargetPeer string `json:"targetPeer"` + } + var enc NewMessage + enc.SymKeyID = n.SymKeyID + enc.PublicKey = n.PublicKey + enc.Sig = n.Sig + enc.TTL = n.TTL + enc.Topic = n.Topic + enc.Payload = n.Payload + enc.Padding = n.Padding + enc.PowTime = n.PowTime + enc.PowTarget = n.PowTarget + enc.TargetPeer = n.TargetPeer + return json.Marshal(&enc) +} + +func (n *NewMessage) UnmarshalJSON(input []byte) error { + type NewMessage struct { + SymKeyID *string `json:"symKeyID"` + PublicKey hexutil.Bytes `json:"pubKey"` + Sig *string `json:"sig"` + TTL *uint32 `json:"ttl"` + Topic *TopicType `json:"topic"` + Payload hexutil.Bytes `json:"payload"` + Padding hexutil.Bytes `json:"padding"` + PowTime *uint32 `json:"powTime"` + PowTarget *float64 `json:"powTarget"` + TargetPeer *string `json:"targetPeer"` + } + var dec NewMessage + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.SymKeyID != nil { + n.SymKeyID = *dec.SymKeyID + } + if dec.PublicKey != nil { + n.PublicKey = dec.PublicKey + } + if dec.Sig != nil { + n.Sig = *dec.Sig + } + if dec.TTL != nil { + n.TTL = *dec.TTL + } + if dec.Topic != nil { + n.Topic = *dec.Topic + } + if dec.Payload != nil { + n.Payload = dec.Payload + } + if dec.Padding != nil { + n.Padding = dec.Padding + } + if dec.PowTime != nil { + n.PowTime = *dec.PowTime + } + if dec.PowTarget != nil { + n.PowTarget = *dec.PowTarget + } + if dec.TargetPeer != nil { + n.TargetPeer = *dec.TargetPeer + } + return nil +} diff --git a/whisper/whisperv5/message.go b/whisper/whisperv5/message.go index 4ef469b51..c27535cd1 100644 --- a/whisper/whisperv5/message.go +++ b/whisper/whisperv5/message.go @@ -49,7 +49,7 @@ type MessageParams struct { // SentMessage represents an end-user data packet to transmit through the // Whisper protocol. These are wrapped into Envelopes that need not be // understood by intermediate nodes, just forwarded. -type SentMessage struct { +type sentMessage struct { Raw []byte } @@ -87,8 +87,8 @@ func (msg *ReceivedMessage) isAsymmetricEncryption() bool { } // NewMessage creates and initializes a non-signed, non-encrypted Whisper message. -func NewSentMessage(params *MessageParams) (*SentMessage, error) { - msg := SentMessage{} +func NewSentMessage(params *MessageParams) (*sentMessage, error) { + msg := sentMessage{} msg.Raw = make([]byte, 1, len(params.Payload)+len(params.Padding)+signatureLength+padSizeLimit) msg.Raw[0] = 0 // set all the flags to zero err := msg.appendPadding(params) @@ -119,7 +119,7 @@ func intSize(i int) (s int) { // appendPadding appends the pseudorandom padding bytes and sets the padding flag. // The last byte contains the size of padding (thus, its size must not exceed 256). -func (msg *SentMessage) appendPadding(params *MessageParams) error { +func (msg *sentMessage) appendPadding(params *MessageParams) error { rawSize := len(params.Payload) + 1 if params.Src != nil { rawSize += signatureLength @@ -164,7 +164,7 @@ func (msg *SentMessage) appendPadding(params *MessageParams) error { // sign calculates and sets the cryptographic signature for the message, // also setting the sign flag. -func (msg *SentMessage) sign(key *ecdsa.PrivateKey) error { +func (msg *sentMessage) sign(key *ecdsa.PrivateKey) error { if isMessageSigned(msg.Raw[0]) { // this should not happen, but no reason to panic log.Error("failed to sign the message: already signed") @@ -183,7 +183,7 @@ func (msg *SentMessage) sign(key *ecdsa.PrivateKey) error { } // encryptAsymmetric encrypts a message with a public key. -func (msg *SentMessage) encryptAsymmetric(key *ecdsa.PublicKey) error { +func (msg *sentMessage) encryptAsymmetric(key *ecdsa.PublicKey) error { if !ValidatePublicKey(key) { return errors.New("invalid public key provided for asymmetric encryption") } @@ -196,7 +196,7 @@ func (msg *SentMessage) encryptAsymmetric(key *ecdsa.PublicKey) error { // encryptSymmetric encrypts a message with a topic key, using AES-GCM-256. // nonce size should be 12 bytes (see cipher.gcmStandardNonceSize). -func (msg *SentMessage) encryptSymmetric(key []byte) (nonce []byte, err error) { +func (msg *sentMessage) encryptSymmetric(key []byte) (nonce []byte, err error) { if !validateSymmetricKey(key) { return nil, errors.New("invalid key provided for symmetric encryption") } @@ -224,13 +224,12 @@ func (msg *SentMessage) encryptSymmetric(key []byte) (nonce []byte, err error) { } // Wrap bundles the message into an Envelope to transmit over the network. -func (msg *SentMessage) Wrap(options *MessageParams) (envelope *Envelope, err error) { +func (msg *sentMessage) Wrap(options *MessageParams) (envelope *Envelope, err error) { if options.TTL == 0 { options.TTL = DefaultTTL } if options.Src != nil { - err = msg.sign(options.Src) - if err != nil { + if err = msg.sign(options.Src); err != nil { return nil, err } } @@ -242,14 +241,12 @@ func (msg *SentMessage) Wrap(options *MessageParams) (envelope *Envelope, err er } else { err = errors.New("unable to encrypt the message: neither symmetric nor assymmetric key provided") } - if err != nil { return nil, err } envelope = NewEnvelope(options.TTL, options.Topic, nonce, msg) - err = envelope.Seal(options) - if err != nil { + if err = envelope.Seal(options); err != nil { return nil, err } return envelope, nil diff --git a/whisper/whisperv5/peer_test.go b/whisper/whisperv5/peer_test.go index d3cd63b0b..bae2adb6f 100644 --- a/whisper/whisperv5/peer_test.go +++ b/whisper/whisperv5/peer_test.go @@ -113,7 +113,7 @@ func initialize(t *testing.T) { for i := 0; i < NumNodes; i++ { var node TestNode - node.shh = New() + node.shh = New(&DefaultConfig) node.shh.SetMinimumPoW(0.00000001) node.shh.Start(nil) topics := make([]TopicType, 0) diff --git a/whisper/whisperv5/topic.go b/whisper/whisperv5/topic.go index 54d7422d1..5fea2f28b 100644 --- a/whisper/whisperv5/topic.go +++ b/whisper/whisperv5/topic.go @@ -19,10 +19,11 @@ package whisperv5 import ( + "encoding/json" "fmt" - "strings" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" ) // Topic represents a cryptographically secure, probabilistic partial @@ -46,24 +47,19 @@ func (topic *TopicType) String() string { return string(common.ToHex(topic[:])) } +func (t *TopicType) MarshalJSON() ([]byte, error) { + return json.Marshal(hexutil.Bytes(t[:])) +} + // UnmarshalJSON parses a hex representation to a topic. func (t *TopicType) UnmarshalJSON(input []byte) error { - length := len(input) - if length >= 2 && input[0] == '"' && input[length-1] == '"' { - input = input[1 : length-1] + var data hexutil.Bytes + if err := json.Unmarshal(input, &data); err != nil { + return err } - // strip "0x" for length check - if len(input) > 1 && strings.ToLower(string(input[:2])) == "0x" { - input = input[2:] + if len(data) != TopicLength { + return fmt.Errorf("unmarshalJSON failed: topic must be exactly %d bytes(%d)", TopicLength, len(input)) } - // validate the length of the input - if len(input) != TopicLength*2 { - return fmt.Errorf("unmarshalJSON failed: topic must be exactly %d bytes", TopicLength) - } - b := common.FromHex(string(input)) - if b == nil { - return fmt.Errorf("unmarshalJSON failed: wrong topic format") - } - *t = BytesToTopic(b) + *t = BytesToTopic(data) return nil } diff --git a/whisper/whisperv5/topic_test.go b/whisper/whisperv5/topic_test.go index df566da36..63593963b 100644 --- a/whisper/whisperv5/topic_test.go +++ b/whisper/whisperv5/topic_test.go @@ -66,37 +66,32 @@ var unmarshalTestsGood = []struct { topic TopicType data []byte }{ - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("0x00000000")}, - {topic: TopicType{0x00, 0x7f, 0x80, 0xff}, data: []byte("0x007f80ff")}, - {topic: TopicType{0xff, 0x80, 0x7f, 0x00}, data: []byte("0xff807f00")}, - {topic: TopicType{0xf2, 0x6e, 0x77, 0x79}, data: []byte("0xf26e7779")}, - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("00000000")}, - {topic: TopicType{0x00, 0x80, 0x01, 0x00}, data: []byte("00800100")}, - {topic: TopicType{0x00, 0x7f, 0x80, 0xff}, data: []byte("007f80ff")}, - {topic: TopicType{0xff, 0x80, 0x7f, 0x00}, data: []byte("ff807f00")}, - {topic: TopicType{0xf2, 0x6e, 0x77, 0x79}, data: []byte("f26e7779")}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x00000000"`)}, + {topic: TopicType{0x00, 0x7f, 0x80, 0xff}, data: []byte(`"0x007f80ff"`)}, + {topic: TopicType{0xff, 0x80, 0x7f, 0x00}, data: []byte(`"0xff807f00"`)}, + {topic: TopicType{0xf2, 0x6e, 0x77, 0x79}, data: []byte(`"0xf26e7779"`)}, } var unmarshalTestsBad = []struct { topic TopicType data []byte }{ - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("0x000000")}, - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("0x0000000")}, - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("0x000000000")}, - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("0x0000000000")}, - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("000000")}, - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("0000000")}, - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("000000000")}, - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("0000000000")}, - {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("abcdefg0")}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x0000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x000000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x0000000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"000000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0000000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"abcdefg0"`)}, } var unmarshalTestsUgly = []struct { topic TopicType data []byte }{ - {topic: TopicType{0x01, 0x00, 0x00, 0x00}, data: []byte("00000001")}, + {topic: TopicType{0x01, 0x00, 0x00, 0x00}, data: []byte(`"0x00000001"`)}, } func TestUnmarshalTestsGood(t *testing.T) { @@ -104,7 +99,7 @@ func TestUnmarshalTestsGood(t *testing.T) { var top TopicType err := top.UnmarshalJSON(tst.data) if err != nil { - t.Fatalf("failed test %d. input: %v.", i, tst.data) + t.Fatalf("failed test %d. input: %v. err: %v", i, tst.data, err) } else if top != tst.topic { t.Fatalf("failed test %d: have %v, want %v.", i, t, tst.topic) } diff --git a/whisper/whisperv5/whisper.go b/whisper/whisperv5/whisper.go index f2aad08ef..cc6c65a9f 100644 --- a/whisper/whisperv5/whisper.go +++ b/whisper/whisperv5/whisper.go @@ -26,6 +26,8 @@ import ( "sync" "time" + "sync/atomic" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -44,6 +46,38 @@ type Statistics struct { totalMessagesCleared int } +type settingType byte +type settingsMap map[settingType]interface{} + +const ( + minPowIdx settingType = iota // Minimal PoW required by the whisper node + maxMsgSizeIdx settingType = iota // Maximal message length allowed by the whisper node + OverflowIdx settingType = iota // Indicator of message queue overflow +) + +type settingsVault struct { + vaultMu sync.Mutex + vault atomic.Value +} + +func (s *settingsVault) get(idx settingType) interface{} { + m := s.vault.Load().(settingsMap) + return m[idx] +} + +func (s *settingsVault) store(idx settingType, val interface{}) { + s.vaultMu.Lock() + defer s.vaultMu.Unlock() + + m1 := s.vault.Load().(settingsMap) + m2 := make(settingsMap) + for k, v := range m1 { + m2[k] = v + } + m2[idx] = val + s.vault.Store(m2) +} + // Whisper represents a dark communication interface through the Ethereum // network, using its very own P2P communication layer. type Whisper struct { @@ -54,28 +88,27 @@ type Whisper struct { symKeys map[string][]byte // Symmetric key storage keyMu sync.RWMutex // Mutex associated with key storages + poolMu sync.RWMutex // Mutex to sync the message and expiration pools envelopes map[common.Hash]*Envelope // Pool of envelopes currently tracked by this node expirations map[uint32]*set.SetNonTS // Message expiration pool - poolMu sync.RWMutex // Mutex to sync the message and expiration pools - peers map[*Peer]struct{} // Set of currently active peers peerMu sync.RWMutex // Mutex to sync the active peer set + peers map[*Peer]struct{} // Set of currently active peers messageQueue chan *Envelope // Message queue for normal whisper messages p2pMsgQueue chan *Envelope // Message queue for peer-to-peer messages (not to be forwarded any further) quit chan struct{} // Channel used for graceful exit - minPoW float64 // Minimal PoW required by the whisper node - maxMsgLength int // Maximal message length allowed by the whisper node - overflow bool // Indicator of message queue overflow + settings settingsVault // holds configuration settings that can be dynamically changed - stats Statistics // Statistics of whisper node + statsMu sync.Mutex // guard stats + stats Statistics // Statistics of whisper node mailServer MailServer // MailServer interface } // New creates a Whisper client ready to communicate through the Ethereum P2P network. -func New() *Whisper { +func New(cfg *Config) *Whisper { whisper := &Whisper{ privateKeys: make(map[string]*ecdsa.PrivateKey), symKeys: make(map[string][]byte), @@ -85,22 +118,47 @@ func New() *Whisper { messageQueue: make(chan *Envelope, messageQueueLimit), p2pMsgQueue: make(chan *Envelope, messageQueueLimit), quit: make(chan struct{}), - minPoW: DefaultMinimumPoW, - maxMsgLength: DefaultMaxMessageLength, } + whisper.filters = NewFilters(whisper) + whisper.settings.vault.Store(make(settingsMap)) + whisper.settings.store(minPowIdx, cfg.MinimumAcceptedPOW) + whisper.settings.store(maxMsgSizeIdx, cfg.MaxMessageSize) + whisper.settings.store(OverflowIdx, false) + // p2p whisper sub protocol handler whisper.protocol = p2p.Protocol{ Name: ProtocolName, Version: uint(ProtocolVersion), Length: NumberOfMessageCodes, Run: whisper.HandlePeer, + NodeInfo: func() interface{} { + return map[string]interface{}{ + "version": ProtocolVersionStr, + "maxMessageSize": whisper.MaxMessageSize(), + "minimumPoW": whisper.MinPow(), + } + }, } return whisper } +func (w *Whisper) MinPow() float64 { + return w.settings.get(minPowIdx).(float64) +} + +// MaxMessageSize returns the maximum accepted message size. +func (w *Whisper) MaxMessageSize() uint32 { + return w.settings.get(maxMsgSizeIdx).(uint32) +} + +// Overflow returns an indication if the message queue is full. +func (w *Whisper) Overflow() bool { + return w.settings.get(OverflowIdx).(bool) +} + // APIs returns the RPC descriptors the Whisper implementation offers func (w *Whisper) APIs() []rpc.API { return []rpc.API{ @@ -129,12 +187,12 @@ func (w *Whisper) Version() uint { return w.protocol.Version } -// SetMaxMessageLength sets the maximal message length allowed by this node -func (w *Whisper) SetMaxMessageLength(val int) error { - if val <= 0 { - return fmt.Errorf("invalid message length: %d", val) +// SetMaxMessageSize sets the maximal message size allowed by this node +func (w *Whisper) SetMaxMessageSize(size uint32) error { + if size > MaxMessageSize { + return fmt.Errorf("message size too large [%d>%d]", size, MaxMessageSize) } - w.maxMsgLength = val + w.settings.store(maxMsgSizeIdx, uint32(size)) return nil } @@ -143,7 +201,7 @@ func (w *Whisper) SetMinimumPoW(val float64) error { if val <= 0.0 { return fmt.Errorf("invalid PoW: %f", val) } - w.minPoW = val + w.settings.store(minPowIdx, val) return nil } @@ -240,6 +298,20 @@ func (w *Whisper) DeleteKeyPair(key string) bool { return false } +// AddKeyPair imports a asymmetric private key and returns it identifier. +func (w *Whisper) AddKeyPair(key *ecdsa.PrivateKey) (string, error) { + id, err := GenerateRandomID() + if err != nil { + return "", fmt.Errorf("failed to generate ID: %s", err) + } + + w.keyMu.Lock() + w.privateKeys[id] = key + w.keyMu.Unlock() + + return id, nil +} + // HasKeyPair checks if the the whisper node is configured with the private key // of the specified public pair. func (w *Whisper) HasKeyPair(id string) bool { @@ -451,7 +523,7 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error { log.Warn("message loop", "peer", p.peer.ID(), "err", err) return err } - if packet.Size > uint32(wh.maxMsgLength) { + if packet.Size > wh.MaxMessageSize() { log.Warn("oversized message received", "peer", p.peer.ID()) return errors.New("oversized message received") } @@ -532,7 +604,7 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) { } } - if envelope.size() > wh.maxMsgLength { + if uint32(envelope.size()) > wh.MaxMessageSize() { return false, fmt.Errorf("huge messages are not allowed [%x]", envelope.Hash()) } @@ -547,7 +619,7 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) { return false, fmt.Errorf("wrong size of AESNonce: %d bytes [env: %x]", aesNonceSize, envelope.Hash()) } - if envelope.PoW() < wh.minPoW { + if envelope.PoW() < wh.MinPow() { log.Debug("envelope with low PoW dropped", "PoW", envelope.PoW(), "hash", envelope.Hash().Hex()) return false, nil // drop envelope without error } @@ -571,7 +643,9 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) { log.Trace("whisper envelope already cached", "hash", envelope.Hash().Hex()) } else { log.Trace("cached whisper envelope", "hash", envelope.Hash().Hex()) + wh.statsMu.Lock() wh.stats.memoryUsed += envelope.size() + wh.statsMu.Unlock() wh.postEvent(envelope, false) // notify the local node about the new message if wh.mailServer != nil { wh.mailServer.Archive(envelope) @@ -600,13 +674,13 @@ func (w *Whisper) checkOverflow() { queueSize := len(w.messageQueue) if queueSize == messageQueueLimit { - if !w.overflow { - w.overflow = true + if !w.Overflow() { + w.settings.store(OverflowIdx, true) log.Warn("message queue overflow") } } else if queueSize <= messageQueueLimit/2 { - if w.overflow { - w.overflow = false + if w.Overflow() { + w.settings.store(OverflowIdx, false) log.Warn("message queue overflow fixed (back to normal)") } } @@ -653,6 +727,8 @@ func (w *Whisper) expire() { w.poolMu.Lock() defer w.poolMu.Unlock() + w.statsMu.Lock() + defer w.statsMu.Unlock() w.stats.reset() now := uint32(time.Now().Unix()) for expiry, hashSet := range w.expirations { @@ -673,17 +749,11 @@ func (w *Whisper) expire() { } // Stats returns the whisper node statistics. -func (w *Whisper) Stats() string { - result := fmt.Sprintf("Memory usage: %d bytes. Average messages cleared per expiry cycle: %d. Total messages cleared: %d.", - w.stats.memoryUsed, w.stats.totalMessagesCleared/w.stats.cycles, w.stats.totalMessagesCleared) - if w.stats.messagesCleared > 0 { - result += fmt.Sprintf(" Latest expiry cycle cleared %d messages (%d bytes).", - w.stats.messagesCleared, w.stats.memoryCleared) - } - if w.overflow { - result += " Message queue state: overflow." - } - return result +func (w *Whisper) Stats() Statistics { + w.statsMu.Lock() + defer w.statsMu.Unlock() + + return w.stats } // Envelopes retrieves all the messages currently pooled by the node. @@ -734,15 +804,6 @@ func (s *Statistics) reset() { s.messagesCleared = 0 } -// ValidateKeyID checks the format of key id. -func ValidateKeyID(id string) error { - const target = keyIdSize * 2 - if len(id) != target { - return fmt.Errorf("wrong size of key ID (expected %d bytes, got %d)", target, len(id)) - } - return nil -} - // ValidatePublicKey checks the format of the given public key. func ValidatePublicKey(k *ecdsa.PublicKey) bool { return k != nil && k.X != nil && k.Y != nil && k.X.Sign() != 0 && k.Y.Sign() != 0 diff --git a/whisper/whisperv5/whisper_test.go b/whisper/whisperv5/whisper_test.go index 225728c42..4c03cb234 100644 --- a/whisper/whisperv5/whisper_test.go +++ b/whisper/whisperv5/whisper_test.go @@ -21,10 +21,11 @@ import ( mrand "math/rand" "testing" "time" + "crypto/ecdsa" ) func TestWhisperBasic(t *testing.T) { - w := New() + w := New(&DefaultConfig) p := w.Protocols() shh := p[0] if shh.Name != ProtocolName { @@ -117,8 +118,39 @@ func TestWhisperBasic(t *testing.T) { } } +func TestWhisperAsymmetricKeyImport(t *testing.T) { + var ( + w = New(&DefaultConfig) + privateKeys []*ecdsa.PrivateKey + ) + + for i:=0; i < 50; i++ { + id, err := w.NewKeyPair() + if err != nil { + t.Fatalf("could not generate key: %v", err) + } + + pk, err := w.GetPrivateKey(id) + if err != nil { + t.Fatalf("could not export private key: %v", err) + } + + privateKeys = append(privateKeys, pk) + + if !w.DeleteKeyPair(id) { + t.Fatalf("could not delete private key") + } + } + + for _, pk := range privateKeys { + if _, err := w.AddKeyPair(pk); err != nil { + t.Fatalf("could not import private key: %v", err) + } + } +} + func TestWhisperIdentityManagement(t *testing.T) { - w := New() + w := New(&DefaultConfig) id1, err := w.NewKeyPair() if err != nil { t.Fatalf("failed to generate new key pair: %s.", err) @@ -240,7 +272,7 @@ func TestWhisperSymKeyManagement(t *testing.T) { var err error var k1, k2 []byte - w := New() + w := New(&DefaultConfig) id1 := string("arbitrary-string-1") id2 := string("arbitrary-string-2") @@ -443,7 +475,7 @@ func TestWhisperSymKeyManagement(t *testing.T) { func TestExpiry(t *testing.T) { InitSingleTest() - w := New() + w := New(&DefaultConfig) w.SetMinimumPoW(0.0000001) defer w.SetMinimumPoW(DefaultMinimumPoW) w.Start(nil) @@ -500,9 +532,9 @@ func TestExpiry(t *testing.T) { func TestCustomization(t *testing.T) { InitSingleTest() - w := New() + w := New(&DefaultConfig) defer w.SetMinimumPoW(DefaultMinimumPoW) - defer w.SetMaxMessageLength(DefaultMaxMessageLength) + defer w.SetMaxMessageSize(DefaultMaxMessageSize) w.Start(nil) defer w.Stop() @@ -547,13 +579,13 @@ func TestCustomization(t *testing.T) { if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) } - w.SetMaxMessageLength(env.size() - 1) + w.SetMaxMessageSize(uint32(env.size() - 1)) err = w.Send(env) if err == nil { t.Fatalf("successfully sent oversized envelope (seed %d): false positive.", seed) } - w.SetMaxMessageLength(DefaultMaxMessageLength) + w.SetMaxMessageSize(DefaultMaxMessageSize) err = w.Send(env) if err != nil { t.Fatalf("failed to send second envelope with seed %d: %s.", seed, err)