diff --git a/whisper/envelope.go b/whisper/envelope.go index c51c6e600..93e3ea1a3 100644 --- a/whisper/envelope.go +++ b/whisper/envelope.go @@ -19,7 +19,7 @@ import ( type Envelope struct { Expiry uint32 // Whisper protocol specifies int32, really should be int64 TTL uint32 // ^^^^^^ - Topics [][]byte + Topics []Topic Data []byte Nonce uint32 @@ -28,7 +28,7 @@ type Envelope struct { // NewEnvelope wraps a Whisper message with expiration and destination data // included into an envelope for network forwarding. -func NewEnvelope(ttl time.Duration, topics [][]byte, msg *Message) *Envelope { +func NewEnvelope(ttl time.Duration, topics []Topic, msg *Message) *Envelope { return &Envelope{ Expiry: uint32(time.Now().Add(ttl).Unix()), TTL: uint32(ttl.Seconds()), diff --git a/whisper/filter.go b/whisper/filter.go index b33f2c1a2..7258de3e7 100644 --- a/whisper/filter.go +++ b/whisper/filter.go @@ -5,6 +5,6 @@ import "crypto/ecdsa" type Filter struct { To *ecdsa.PublicKey From *ecdsa.PublicKey - Topics [][]byte + Topics []Topic Fn func(*Message) } diff --git a/whisper/message.go b/whisper/message.go index 457cf6def..ad31aa592 100644 --- a/whisper/message.go +++ b/whisper/message.go @@ -75,8 +75,13 @@ func (self *Message) Wrap(pow time.Duration, options Options) (*Envelope, error) return nil, err } } + // Convert the user topic into whisper ones + topics := make([]Topic, len(options.Topics)) + for i, topic := range options.Topics { + topics[i] = NewTopic(topic) + } // Wrap the processed message, seal it and return - envelope := NewEnvelope(options.TTL, options.Topics, self) + envelope := NewEnvelope(options.TTL, topics, self) envelope.Seal(pow) return envelope, nil diff --git a/whisper/topic.go b/whisper/topic.go new file mode 100644 index 000000000..10069c902 --- /dev/null +++ b/whisper/topic.go @@ -0,0 +1,35 @@ +// Contains the Whisper protocol Topic element. For formal details please see +// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#topics. + +package whisper + +import "github.com/ethereum/go-ethereum/crypto" + +// Topic represents a cryptographically secure, probabilistic partial +// classifications of a message, determined as the first (left) 4 bytes of the +// SHA3 hash of some arbitrary data given by the original author of the message. +type Topic [4]byte + +// NewTopic creates a topic from the 4 byte prefix of the SHA3 hash of the data. +func NewTopic(data []byte) Topic { + prefix := [4]byte{} + copy(prefix[:], crypto.Sha3(data)[:4]) + return Topic(prefix) +} + +// String converts a topic byte array to a string representation. +func (self *Topic) String() string { + return string(self[:]) +} + +// TopicSet represents a hash set to check if a topic exists or not. +type TopicSet map[string]struct{} + +// NewTopicSet creates a topic hash set from a slice of topics. +func NewTopicSet(topics []Topic) TopicSet { + set := make(map[string]struct{}) + for _, topic := range topics { + set[topic.String()] = struct{}{} + } + return TopicSet(set) +} diff --git a/whisper/topic_test.go b/whisper/topic_test.go new file mode 100644 index 000000000..4626e2ae5 --- /dev/null +++ b/whisper/topic_test.go @@ -0,0 +1,38 @@ +package whisper + +import ( + "bytes" + "testing" +) + +var topicCreationTests = []struct { + data []byte + hash [4]byte +}{ + {hash: [4]byte{0xc5, 0xd2, 0x46, 0x01}, data: nil}, + {hash: [4]byte{0xc5, 0xd2, 0x46, 0x01}, data: []byte{}}, + {hash: [4]byte{0x8f, 0x9a, 0x2b, 0x7d}, data: []byte("test name")}, +} + +func TestTopicCreation(t *testing.T) { + for i, tt := range topicCreationTests { + topic := NewTopic(tt.data) + if bytes.Compare(topic[:], tt.hash[:]) != 0 { + t.Errorf("test %d: hash mismatch: have %v, want %v.", i, topic, tt.hash) + } + } +} + +func TestTopicSetCreation(t *testing.T) { + topics := make([]Topic, len(topicCreationTests)) + for i, tt := range topicCreationTests { + topics[i] = NewTopic(tt.data) + } + set := NewTopicSet(topics) + for i, tt := range topicCreationTests { + topic := NewTopic(tt.data) + if _, ok := set[topic.String()]; !ok { + t.Errorf("topic %d: not found in set", i) + } + } +} diff --git a/whisper/util.go b/whisper/util.go deleted file mode 100644 index 7a222395f..000000000 --- a/whisper/util.go +++ /dev/null @@ -1,36 +0,0 @@ -package whisper - -import "github.com/ethereum/go-ethereum/crypto" - -func hashTopic(topic []byte) []byte { - return crypto.Sha3(topic)[:4] -} - -// NOTE this isn't DRY, but I don't want to iterate twice. - -// Returns a formatted topics byte slice. -// data: unformatted data (e.g., no hashes needed) -func Topics(data [][]byte) [][]byte { - d := make([][]byte, len(data)) - for i, byts := range data { - d[i] = hashTopic(byts) - } - return d -} - -func TopicsFromString(data ...string) [][]byte { - d := make([][]byte, len(data)) - for i, str := range data { - d[i] = hashTopic([]byte(str)) - } - return d -} - -func bytesToMap(s [][]byte) map[string]struct{} { - m := make(map[string]struct{}) - for _, topic := range s { - m[string(topic)] = struct{}{} - } - - return m -} diff --git a/whisper/whisper.go b/whisper/whisper.go index ad29fe16a..d9affe09b 100644 --- a/whisper/whisper.go +++ b/whisper/whisper.go @@ -119,7 +119,7 @@ func (self *Whisper) Watch(opts Filter) int { return self.filters.Install(filter.Generic{ Str1: string(crypto.FromECDSAPub(opts.To)), Str2: string(crypto.FromECDSAPub(opts.From)), - Data: bytesToMap(opts.Topics), + Data: NewTopicSet(opts.Topics), Fn: func(data interface{}) { opts.Fn(data.(*Message)) }, @@ -272,9 +272,9 @@ func (self *Whisper) Protocol() p2p.Protocol { return self.protocol } -func createFilter(message *Message, topics [][]byte, key *ecdsa.PrivateKey) filter.Filter { +func createFilter(message *Message, topics []Topic, key *ecdsa.PrivateKey) filter.Filter { return filter.Generic{ Str1: string(crypto.FromECDSAPub(&key.PublicKey)), Str2: string(crypto.FromECDSAPub(message.Recover())), - Data: bytesToMap(topics), + Data: NewTopicSet(topics), } }