rpc, ui/qt/qwhisper, whisper, xeth: introduce complex topic filters

This commit is contained in:
Péter Szilágyi 2015-04-21 18:31:08 +03:00
parent 5f9a6cf972
commit 2d8bbab6f6
5 changed files with 315 additions and 34 deletions

View File

@ -2,12 +2,55 @@
package whisper package whisper
import "crypto/ecdsa" import (
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/event/filter"
)
// Filter is used to subscribe to specific types of whisper messages. // Filter is used to subscribe to specific types of whisper messages.
type Filter struct { type Filter struct {
To *ecdsa.PublicKey // Recipient of the message To *ecdsa.PublicKey // Recipient of the message
From *ecdsa.PublicKey // Sender of the message From *ecdsa.PublicKey // Sender of the message
Topics []Topic // Topics to watch messages on Topics [][]Topic // Topics to filter messages with
Fn func(*Message) // Handler in case of a match Fn func(msg *Message) // Handler in case of a match
}
// filterer is the internal, fully initialized filter ready to match inbound
// messages to a variety of criteria.
type filterer struct {
to string // Recipient of the message
from string // Sender of the message
matcher *topicMatcher // Topics to filter messages with
fn func(data interface{}) // Handler in case of a match
}
// Compare checks if the specified filter matches the current one.
func (self filterer) Compare(f filter.Filter) bool {
filter := f.(filterer)
// Check the message sender and recipient
if len(self.to) > 0 && self.to != filter.to {
return false
}
if len(self.from) > 0 && self.from != filter.from {
return false
}
// Check the topic filtering
topics := make([]Topic, len(filter.matcher.conditions))
for i, group := range filter.matcher.conditions {
// Message should contain a single topic entry, extract
for topics[i], _ = range group {
break
}
}
if !self.matcher.Matches(topics) {
return false
}
return true
}
// Trigger is called when a filter successfully matches an inbound message.
func (self filterer) Trigger(data interface{}) {
self.fn(data)
} }

121
topic.go
View File

@ -27,6 +27,26 @@ func NewTopics(data ...[]byte) []Topic {
return topics return topics
} }
// NewTopicFilter creates a 2D topic array used by whisper.Filter from binary
// data elements.
func NewTopicFilter(data ...[][]byte) [][]Topic {
filter := make([][]Topic, len(data))
for i, condition := range data {
filter[i] = NewTopics(condition...)
}
return filter
}
// NewTopicFilterFlat creates a 2D topic array used by whisper.Filter from flat
// binary data elements.
func NewTopicFilterFlat(data ...[]byte) [][]Topic {
filter := make([][]Topic, len(data))
for i, element := range data {
filter[i] = []Topic{NewTopic(element)}
}
return filter
}
// NewTopicFromString creates a topic using the binary data contents of the // NewTopicFromString creates a topic using the binary data contents of the
// specified string. // specified string.
func NewTopicFromString(data string) Topic { func NewTopicFromString(data string) Topic {
@ -43,19 +63,100 @@ func NewTopicsFromStrings(data ...string) []Topic {
return topics return topics
} }
// NewTopicFilterFromStrings creates a 2D topic array used by whisper.Filter
// from textual data elements.
func NewTopicFilterFromStrings(data ...[]string) [][]Topic {
filter := make([][]Topic, len(data))
for i, condition := range data {
filter[i] = NewTopicsFromStrings(condition...)
}
return filter
}
// NewTopicFilterFromStringsFlat creates a 2D topic array used by whisper.Filter from flat
// binary data elements.
func NewTopicFilterFromStringsFlat(data ...string) [][]Topic {
filter := make([][]Topic, len(data))
for i, element := range data {
filter[i] = []Topic{NewTopicFromString(element)}
}
return filter
}
// String converts a topic byte array to a string representation. // String converts a topic byte array to a string representation.
func (self *Topic) String() string { func (self *Topic) String() string {
return string(self[:]) return string(self[:])
} }
// TopicSet represents a hash set to check if a topic exists or not. // topicMatcher is a filter expression to verify if a list of topics contained
type topicSet map[string]struct{} // in an arriving message matches some topic conditions. The topic matcher is
// built up of a list of conditions, each of which must be satisfied by the
// NewTopicSet creates a topic hash set from a slice of topics. // corresponding topic in the message. Each condition may require: a) an exact
func newTopicSet(topics []Topic) topicSet { // topic match; b) a match from a set of topics; or c) a wild-card matching all.
set := make(map[string]struct{}) //
for _, topic := range topics { // If a message contains more topics than required by the matcher, those beyond
set[topic.String()] = struct{}{} // the condition count are ignored and assumed to match.
} //
return topicSet(set) // Consider the following sample topic matcher:
// sample := {
// {TopicA1, TopicA2, TopicA3},
// {TopicB},
// nil,
// {TopicD1, TopicD2}
// }
// In order for a message to pass this filter, it should enumerate at least 4
// topics, the first any of [TopicA1, TopicA2, TopicA3], the second mandatory
// "TopicB", the third is ignored by the filter and the fourth either "TopicD1"
// or "TopicD2". If the message contains further topics, the filter will match
// them too.
type topicMatcher struct {
conditions []map[Topic]struct{}
}
// newTopicMatcher create a topic matcher from a list of topic conditions.
func newTopicMatcher(topics ...[]Topic) *topicMatcher {
matcher := make([]map[Topic]struct{}, len(topics))
for i, condition := range topics {
matcher[i] = make(map[Topic]struct{})
for _, topic := range condition {
matcher[i][topic] = struct{}{}
}
}
return &topicMatcher{conditions: matcher}
}
// newTopicMatcherFromBinary create a topic matcher from a list of binary conditions.
func newTopicMatcherFromBinary(data ...[][]byte) *topicMatcher {
topics := make([][]Topic, len(data))
for i, condition := range data {
topics[i] = NewTopics(condition...)
}
return newTopicMatcher(topics...)
}
// newTopicMatcherFromStrings creates a topic matcher from a list of textual
// conditions.
func newTopicMatcherFromStrings(data ...[]string) *topicMatcher {
topics := make([][]Topic, len(data))
for i, condition := range data {
topics[i] = NewTopicsFromStrings(condition...)
}
return newTopicMatcher(topics...)
}
// Matches checks if a list of topics matches this particular condition set.
func (self *topicMatcher) Matches(topics []Topic) bool {
// Mismatch if there aren't enough topics
if len(self.conditions) > len(topics) {
return false
}
// Check each topic condition for existence (skip wild-cards)
for i := 0; i < len(topics) && i < len(self.conditions); i++ {
if len(self.conditions[i]) > 0 {
if _, ok := self.conditions[i][topics[i]]; !ok {
return false
}
}
}
return true
} }

View File

@ -52,16 +52,149 @@ func TestTopicCreation(t *testing.T) {
} }
} }
func TestTopicSetCreation(t *testing.T) { var topicMatcherCreationTest = struct {
topics := make([]Topic, len(topicCreationTests)) binary [][][]byte
for i, tt := range topicCreationTests { textual [][]string
topics[i] = NewTopic(tt.data) matcher []map[[4]byte]struct{}
}{
binary: [][][]byte{
[][]byte{},
[][]byte{
[]byte("Topic A"),
},
[][]byte{
[]byte("Topic B1"),
[]byte("Topic B2"),
[]byte("Topic B3"),
},
},
textual: [][]string{
[]string{},
[]string{"Topic A"},
[]string{"Topic B1", "Topic B2", "Topic B3"},
},
matcher: []map[[4]byte]struct{}{
map[[4]byte]struct{}{},
map[[4]byte]struct{}{
[4]byte{0x25, 0xfc, 0x95, 0x66}: struct{}{},
},
map[[4]byte]struct{}{
[4]byte{0x93, 0x6d, 0xec, 0x09}: struct{}{},
[4]byte{0x25, 0x23, 0x34, 0xd3}: struct{}{},
[4]byte{0x6b, 0xc2, 0x73, 0xd1}: struct{}{},
},
},
}
func TestTopicMatcherCreation(t *testing.T) {
test := topicMatcherCreationTest
matcher := newTopicMatcherFromBinary(test.binary...)
for i, cond := range matcher.conditions {
for topic, _ := range cond {
if _, ok := test.matcher[i][topic]; !ok {
t.Errorf("condition %d; extra topic found: 0x%x", i, topic[:])
}
}
} }
set := newTopicSet(topics) for i, cond := range test.matcher {
for i, tt := range topicCreationTests { for topic, _ := range cond {
topic := NewTopic(tt.data) if _, ok := matcher.conditions[i][topic]; !ok {
if _, ok := set[topic.String()]; !ok { t.Errorf("condition %d; topic not found: 0x%x", i, topic[:])
t.Errorf("topic %d: not found in set", i) }
}
}
matcher = newTopicMatcherFromStrings(test.textual...)
for i, cond := range matcher.conditions {
for topic, _ := range cond {
if _, ok := test.matcher[i][topic]; !ok {
t.Errorf("condition %d; extra topic found: 0x%x", i, topic[:])
}
}
}
for i, cond := range test.matcher {
for topic, _ := range cond {
if _, ok := matcher.conditions[i][topic]; !ok {
t.Errorf("condition %d; topic not found: 0x%x", i, topic[:])
}
}
}
}
var topicMatcherTests = []struct {
filter [][]string
topics []string
match bool
}{
// Empty topic matcher should match everything
{
filter: [][]string{},
topics: []string{},
match: true,
},
{
filter: [][]string{},
topics: []string{"a", "b", "c"},
match: true,
},
// Fixed topic matcher should match strictly, but only prefix
{
filter: [][]string{[]string{"a"}, []string{"b"}},
topics: []string{"a"},
match: false,
},
{
filter: [][]string{[]string{"a"}, []string{"b"}},
topics: []string{"a", "b"},
match: true,
},
{
filter: [][]string{[]string{"a"}, []string{"b"}},
topics: []string{"a", "b", "c"},
match: true,
},
// Multi-matcher should match any from a sub-group
{
filter: [][]string{[]string{"a1", "a2"}},
topics: []string{"a"},
match: false,
},
{
filter: [][]string{[]string{"a1", "a2"}},
topics: []string{"a1"},
match: true,
},
{
filter: [][]string{[]string{"a1", "a2"}},
topics: []string{"a2"},
match: true,
},
// Wild-card condition should match anything
{
filter: [][]string{[]string{}, []string{"b"}},
topics: []string{"a"},
match: false,
},
{
filter: [][]string{[]string{}, []string{"b"}},
topics: []string{"a", "b"},
match: true,
},
{
filter: [][]string{[]string{}, []string{"b"}},
topics: []string{"b", "b"},
match: true,
},
}
func TestTopicMatcher(t *testing.T) {
for i, tt := range topicMatcherTests {
topics := NewTopicsFromStrings(tt.topics...)
matcher := newTopicMatcherFromStrings(tt.filter...)
if match := matcher.Matches(topics); match != tt.match {
t.Errorf("test %d: match mismatch: have %v, want %v", i, match, tt.match)
} }
} }
} }

View File

@ -118,11 +118,11 @@ func (self *Whisper) GetIdentity(key *ecdsa.PublicKey) *ecdsa.PrivateKey {
// Watch installs a new message handler to run in case a matching packet arrives // Watch installs a new message handler to run in case a matching packet arrives
// from the whisper network. // from the whisper network.
func (self *Whisper) Watch(options Filter) int { func (self *Whisper) Watch(options Filter) int {
filter := filter.Generic{ filter := filterer{
Str1: string(crypto.FromECDSAPub(options.To)), to: string(crypto.FromECDSAPub(options.To)),
Str2: string(crypto.FromECDSAPub(options.From)), from: string(crypto.FromECDSAPub(options.From)),
Data: newTopicSet(options.Topics), matcher: newTopicMatcher(options.Topics...),
Fn: func(data interface{}) { fn: func(data interface{}) {
options.Fn(data.(*Message)) options.Fn(data.(*Message))
}, },
} }
@ -273,10 +273,14 @@ func (self *Whisper) open(envelope *Envelope) *Message {
// createFilter creates a message filter to check against installed handlers. // createFilter creates a message filter to check against installed handlers.
func createFilter(message *Message, topics []Topic) filter.Filter { func createFilter(message *Message, topics []Topic) filter.Filter {
return filter.Generic{ matcher := make([][]Topic, len(topics))
Str1: string(crypto.FromECDSAPub(message.To)), for i, topic := range topics {
Str2: string(crypto.FromECDSAPub(message.Recover())), matcher[i] = []Topic{topic}
Data: newTopicSet(topics), }
return filterer{
to: string(crypto.FromECDSAPub(message.To)),
from: string(crypto.FromECDSAPub(message.Recover())),
matcher: newTopicMatcher(matcher...),
} }
} }

View File

@ -129,7 +129,7 @@ func testBroadcast(anonymous bool, t *testing.T) {
dones[i] = done dones[i] = done
targets[i].Watch(Filter{ targets[i].Watch(Filter{
Topics: NewTopicsFromStrings("broadcast topic"), Topics: NewTopicFilterFromStrings([]string{"broadcast topic"}),
Fn: func(msg *Message) { Fn: func(msg *Message) {
close(done) close(done)
}, },