github.com/status-im/status-go@v1.1.0/protocol/v1/membership_update_message.go (about)

     1  package protocol
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/ecdsa"
     6  	"fmt"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/golang/protobuf/proto"
    11  	"github.com/google/uuid"
    12  	"github.com/pkg/errors"
    13  
    14  	"github.com/status-im/status-go/eth-node/crypto"
    15  	"github.com/status-im/status-go/eth-node/types"
    16  	"github.com/status-im/status-go/protocol/protobuf"
    17  )
    18  
    19  // MembershipUpdateMessage is a message used to propagate information
    20  // about group membership changes.
    21  // For more information, see https://github.com/status-im/specs/blob/master/status-group-chats-spec.md.
    22  type MembershipUpdateMessage struct {
    23  	ChatID        string                  `json:"chatId"` // UUID concatenated with hex-encoded public key of the creator for the chat
    24  	Events        []MembershipUpdateEvent `json:"events"`
    25  	Message       *protobuf.ChatMessage   `json:"-"`
    26  	EmojiReaction *protobuf.EmojiReaction `json:"-"`
    27  }
    28  
    29  const signatureLength = 65
    30  
    31  func MembershipUpdateEventFromProtobuf(chatID string, raw []byte) (*MembershipUpdateEvent, error) {
    32  	if len(raw) <= signatureLength {
    33  		return nil, errors.New("invalid payload length")
    34  	}
    35  	decodedEvent := protobuf.MembershipUpdateEvent{}
    36  	signature := raw[:signatureLength]
    37  	encodedEvent := raw[signatureLength:]
    38  
    39  	signatureMaterial := append([]byte(chatID), encodedEvent...)
    40  	publicKey, err := crypto.ExtractSignature(signatureMaterial, signature)
    41  	if err != nil {
    42  		return nil, errors.Wrap(err, "failed to extract signature")
    43  	}
    44  
    45  	from := publicKeyToString(publicKey)
    46  
    47  	err = proto.Unmarshal(encodedEvent, &decodedEvent)
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	return &MembershipUpdateEvent{
    52  		ClockValue: decodedEvent.Clock,
    53  		ChatID:     chatID,
    54  		Members:    decodedEvent.Members,
    55  		Name:       decodedEvent.Name,
    56  		Type:       decodedEvent.Type,
    57  		Color:      decodedEvent.Color,
    58  		Image:      decodedEvent.Image,
    59  		Signature:  signature,
    60  		RawPayload: encodedEvent,
    61  		From:       from,
    62  	}, nil
    63  }
    64  
    65  func (m *MembershipUpdateMessage) ToProtobuf() (*protobuf.MembershipUpdateMessage, error) {
    66  	var rawEvents [][]byte
    67  	for _, e := range m.Events {
    68  		var encodedEvent []byte
    69  		encodedEvent = append(encodedEvent, e.Signature...)
    70  		encodedEvent = append(encodedEvent, e.RawPayload...)
    71  		rawEvents = append(rawEvents, encodedEvent)
    72  	}
    73  
    74  	mUM := &protobuf.MembershipUpdateMessage{
    75  		ChatId: m.ChatID,
    76  		Events: rawEvents,
    77  	}
    78  
    79  	// If message is not piggybacking anything, that's a valid case and we just return
    80  	switch {
    81  	case m.Message != nil:
    82  		mUM.ChatEntity = &protobuf.MembershipUpdateMessage_Message{Message: m.Message}
    83  	case m.EmojiReaction != nil:
    84  		mUM.ChatEntity = &protobuf.MembershipUpdateMessage_EmojiReaction{EmojiReaction: m.EmojiReaction}
    85  	}
    86  
    87  	return mUM, nil
    88  }
    89  
    90  func MembershipUpdateMessageFromProtobuf(raw *protobuf.MembershipUpdateMessage) (*MembershipUpdateMessage, error) {
    91  	var events []MembershipUpdateEvent
    92  	for _, e := range raw.Events {
    93  		verifiedEvent, err := MembershipUpdateEventFromProtobuf(raw.ChatId, e)
    94  		if err != nil {
    95  			return nil, err
    96  		}
    97  		events = append(events, *verifiedEvent)
    98  	}
    99  	return &MembershipUpdateMessage{
   100  		ChatID:        raw.ChatId,
   101  		Events:        events,
   102  		Message:       raw.GetMessage(),
   103  		EmojiReaction: raw.GetEmojiReaction(),
   104  	}, nil
   105  }
   106  
   107  // EncodeMembershipUpdateMessage encodes a MembershipUpdateMessage using protobuf serialization.
   108  func EncodeMembershipUpdateMessage(value MembershipUpdateMessage) ([]byte, error) {
   109  	pb, err := value.ToProtobuf()
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	return proto.Marshal(pb)
   115  }
   116  
   117  // MembershipUpdateEvent contains an event information.
   118  // Member and Members are hex-encoded values with 0x prefix.
   119  type MembershipUpdateEvent struct {
   120  	Type       protobuf.MembershipUpdateEvent_EventType `json:"type"`
   121  	ClockValue uint64                                   `json:"clockValue"`
   122  	Members    []string                                 `json:"members,omitempty"` // in "members-added" and "admins-added" events
   123  	Name       string                                   `json:"name,omitempty"`    // name of the group chat
   124  	Color      string                                   `json:"color,omitempty"`   // color of the group chat
   125  	Image      []byte                                   `json:"image,omitempty"`   // image of the group chat
   126  	From       string                                   `json:"from,omitempty"`
   127  	Signature  []byte                                   `json:"signature,omitempty"`
   128  	ChatID     string                                   `json:"chatId"`
   129  	RawPayload []byte                                   `json:"rawPayload"`
   130  }
   131  
   132  func (u *MembershipUpdateEvent) Equal(update MembershipUpdateEvent) bool {
   133  	return bytes.Equal(u.Signature, update.Signature)
   134  }
   135  
   136  func (u *MembershipUpdateEvent) Sign(key *ecdsa.PrivateKey) error {
   137  	if len(u.ChatID) == 0 {
   138  		return errors.New("can't sign with empty chatID")
   139  	}
   140  	encodedEvent, err := proto.Marshal(u.ToProtobuf())
   141  	if err != nil {
   142  		return err
   143  	}
   144  	u.RawPayload = encodedEvent
   145  	var signatureMaterial []byte
   146  	signatureMaterial = append(signatureMaterial, []byte(u.ChatID)...)
   147  	signatureMaterial = crypto.Keccak256(append(signatureMaterial, u.RawPayload...))
   148  	signature, err := crypto.Sign(signatureMaterial, key)
   149  
   150  	if err != nil {
   151  		return err
   152  	}
   153  	u.Signature = signature
   154  	u.From = publicKeyToString(&key.PublicKey)
   155  	return nil
   156  }
   157  
   158  func (u *MembershipUpdateEvent) ToProtobuf() *protobuf.MembershipUpdateEvent {
   159  	return &protobuf.MembershipUpdateEvent{
   160  		Clock:   u.ClockValue,
   161  		Name:    u.Name,
   162  		Color:   u.Color,
   163  		Image:   u.Image,
   164  		Members: u.Members,
   165  		Type:    u.Type,
   166  	}
   167  }
   168  
   169  func MergeMembershipUpdateEvents(dest []MembershipUpdateEvent, src []MembershipUpdateEvent) []MembershipUpdateEvent {
   170  	for _, update := range src {
   171  		var exists bool
   172  		for _, existing := range dest {
   173  			if existing.Equal(update) {
   174  				exists = true
   175  				break
   176  			}
   177  		}
   178  		if !exists {
   179  			dest = append(dest, update)
   180  		}
   181  	}
   182  	return dest
   183  }
   184  
   185  func NewChatCreatedEvent(name string, color string, clock uint64) MembershipUpdateEvent {
   186  	return MembershipUpdateEvent{
   187  		Type:       protobuf.MembershipUpdateEvent_CHAT_CREATED,
   188  		Name:       name,
   189  		ClockValue: clock,
   190  		Color:      color,
   191  	}
   192  }
   193  
   194  func NewNameChangedEvent(name string, clock uint64) MembershipUpdateEvent {
   195  	return MembershipUpdateEvent{
   196  		Type:       protobuf.MembershipUpdateEvent_NAME_CHANGED,
   197  		Name:       name,
   198  		ClockValue: clock,
   199  	}
   200  }
   201  
   202  func NewColorChangedEvent(color string, clock uint64) MembershipUpdateEvent {
   203  	return MembershipUpdateEvent{
   204  		Type:       protobuf.MembershipUpdateEvent_COLOR_CHANGED,
   205  		Color:      color,
   206  		ClockValue: clock,
   207  	}
   208  }
   209  
   210  func NewImageChangedEvent(image []byte, clock uint64) MembershipUpdateEvent {
   211  	return MembershipUpdateEvent{
   212  		Type:       protobuf.MembershipUpdateEvent_IMAGE_CHANGED,
   213  		Image:      image,
   214  		ClockValue: clock,
   215  	}
   216  }
   217  
   218  func NewMembersAddedEvent(members []string, clock uint64) MembershipUpdateEvent {
   219  	return MembershipUpdateEvent{
   220  		Type:       protobuf.MembershipUpdateEvent_MEMBERS_ADDED,
   221  		Members:    members,
   222  		ClockValue: clock,
   223  	}
   224  }
   225  
   226  func NewMemberJoinedEvent(clock uint64) MembershipUpdateEvent {
   227  	return MembershipUpdateEvent{
   228  		Type:       protobuf.MembershipUpdateEvent_MEMBER_JOINED,
   229  		ClockValue: clock,
   230  	}
   231  }
   232  
   233  func NewAdminsAddedEvent(admins []string, clock uint64) MembershipUpdateEvent {
   234  	return MembershipUpdateEvent{
   235  		Type:       protobuf.MembershipUpdateEvent_ADMINS_ADDED,
   236  		Members:    admins,
   237  		ClockValue: clock,
   238  	}
   239  }
   240  
   241  func NewMemberRemovedEvent(member string, clock uint64) MembershipUpdateEvent {
   242  	return MembershipUpdateEvent{
   243  		Type:       protobuf.MembershipUpdateEvent_MEMBER_REMOVED,
   244  		Members:    []string{member},
   245  		ClockValue: clock,
   246  	}
   247  }
   248  
   249  func NewAdminRemovedEvent(admin string, clock uint64) MembershipUpdateEvent {
   250  	return MembershipUpdateEvent{
   251  		Type:       protobuf.MembershipUpdateEvent_ADMIN_REMOVED,
   252  		Members:    []string{admin},
   253  		ClockValue: clock,
   254  	}
   255  }
   256  
   257  type Group struct {
   258  	chatID  string
   259  	name    string
   260  	color   string
   261  	image   []byte
   262  	events  []MembershipUpdateEvent
   263  	admins  *stringSet
   264  	members *stringSet
   265  }
   266  
   267  func groupChatID(creator *ecdsa.PublicKey) string {
   268  	return uuid.New().String() + "-" + publicKeyToString(creator)
   269  }
   270  
   271  func NewGroupWithEvents(chatID string, events []MembershipUpdateEvent) (*Group, error) {
   272  	return newGroup(chatID, events)
   273  }
   274  
   275  func NewGroupWithCreator(name string, color string, clock uint64, creator *ecdsa.PrivateKey) (*Group, error) {
   276  	chatID := groupChatID(&creator.PublicKey)
   277  	chatCreated := NewChatCreatedEvent(name, color, clock)
   278  	chatCreated.ChatID = chatID
   279  	err := chatCreated.Sign(creator)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	return newGroup(chatID, []MembershipUpdateEvent{chatCreated})
   284  }
   285  
   286  func newGroup(chatID string, events []MembershipUpdateEvent) (*Group, error) {
   287  	g := Group{
   288  		chatID:  chatID,
   289  		events:  events,
   290  		admins:  newStringSet(),
   291  		members: newStringSet(),
   292  	}
   293  	if err := g.init(); err != nil {
   294  		return nil, err
   295  	}
   296  	return &g, nil
   297  }
   298  
   299  func (g *Group) init() error {
   300  	g.sortEvents()
   301  
   302  	var chatID string
   303  
   304  	for _, event := range g.events {
   305  		if chatID == "" {
   306  			chatID = event.ChatID
   307  		} else if event.ChatID != chatID {
   308  			return errors.New("updates contain different chat IDs")
   309  		}
   310  		valid := g.validateEvent(event)
   311  		if !valid {
   312  			return fmt.Errorf("invalid event %#+v from %s", event, event.From)
   313  		}
   314  		g.processEvent(event)
   315  	}
   316  
   317  	valid := g.validateChatID(g.chatID)
   318  	if !valid {
   319  		return fmt.Errorf("invalid chat ID: %s", g.chatID)
   320  	}
   321  	if chatID != g.chatID {
   322  		return fmt.Errorf("expected chat ID equal %s, got %s", g.chatID, chatID)
   323  	}
   324  
   325  	return nil
   326  }
   327  
   328  func (g Group) ChatID() string {
   329  	return g.chatID
   330  }
   331  
   332  func (g Group) Name() string {
   333  	return g.name
   334  }
   335  
   336  func (g Group) Color() string {
   337  	return g.color
   338  }
   339  
   340  func (g Group) Image() []byte {
   341  	return g.image
   342  }
   343  
   344  func (g Group) Events() []MembershipUpdateEvent {
   345  	return g.events
   346  }
   347  
   348  // AbridgedEvents returns the minimum set of events for a user to publish a post
   349  // The events we want to keep:
   350  // 1) Chat created
   351  // 2) Latest color changed
   352  // 3) Latest image changed
   353  // 4) For each admin, the latest admins added event that contains them
   354  // 5) For each member, the latest members added event that contains them
   355  // 4 & 5, might bring removed admins or removed members, for those, we also need to
   356  // keep the event that removes them
   357  func (g Group) AbridgedEvents() []MembershipUpdateEvent {
   358  	var events []MembershipUpdateEvent
   359  	var nameChangedEventFound bool
   360  	var colorChangedEventFound bool
   361  	var imageChangedEventFound bool
   362  	removedMembers := make(map[string]*MembershipUpdateEvent)
   363  	addedMembers := make(map[string]bool)
   364  	extraMembers := make(map[string]bool)
   365  	admins := make(map[string]bool)
   366  	// Iterate in reverse
   367  	for i := len(g.events) - 1; i >= 0; i-- {
   368  		event := g.events[i]
   369  		switch event.Type {
   370  		case protobuf.MembershipUpdateEvent_CHAT_CREATED:
   371  			events = append(events, event)
   372  		case protobuf.MembershipUpdateEvent_NAME_CHANGED:
   373  			if nameChangedEventFound {
   374  				continue
   375  			}
   376  			events = append(events, event)
   377  			nameChangedEventFound = true
   378  		case protobuf.MembershipUpdateEvent_COLOR_CHANGED:
   379  			if colorChangedEventFound {
   380  				continue
   381  			}
   382  			events = append(events, event)
   383  			colorChangedEventFound = true
   384  		case protobuf.MembershipUpdateEvent_IMAGE_CHANGED:
   385  			if imageChangedEventFound {
   386  				continue
   387  			}
   388  			events = append(events, event)
   389  			imageChangedEventFound = true
   390  
   391  		case protobuf.MembershipUpdateEvent_MEMBERS_ADDED:
   392  			var shouldAddEvent bool
   393  			for _, m := range event.Members {
   394  				// If it's adding a current user, and we don't have a more
   395  				// recent event
   396  				// if it's an admin, we track it
   397  				if admins[m] || (g.members.Has(m) && !addedMembers[m]) {
   398  					addedMembers[m] = true
   399  					shouldAddEvent = true
   400  				}
   401  			}
   402  			if shouldAddEvent {
   403  				// Append the event and check the not current members that are also
   404  				// added
   405  				for _, m := range event.Members {
   406  					if !g.members.Has(m) && !admins[m] {
   407  						extraMembers[m] = true
   408  					}
   409  				}
   410  				events = append(events, event)
   411  			}
   412  		case protobuf.MembershipUpdateEvent_ADMIN_REMOVED:
   413  			// We add it always for now
   414  			events = append(events, event)
   415  		case protobuf.MembershipUpdateEvent_ADMINS_ADDED:
   416  			// We track admins in full
   417  			admins[event.Members[0]] = true
   418  			events = append(events, event)
   419  		case protobuf.MembershipUpdateEvent_MEMBER_REMOVED:
   420  			// Save member removed events, as we might need it
   421  			// to remove members who have been added but subsequently left
   422  			if removedMembers[event.Members[0]] == nil || removedMembers[event.Members[0]].ClockValue < event.ClockValue {
   423  				removedMembers[event.Members[0]] = &event
   424  			}
   425  
   426  		case protobuf.MembershipUpdateEvent_MEMBER_JOINED:
   427  			if g.members.Has(event.From) {
   428  				events = append(events, event)
   429  			}
   430  
   431  		}
   432  	}
   433  
   434  	for m := range extraMembers {
   435  		if removedMembers[m] != nil {
   436  			events = append(events, *removedMembers[m])
   437  		}
   438  	}
   439  
   440  	sort.Slice(events, func(i, j int) bool {
   441  		return events[i].ClockValue < events[j].ClockValue
   442  	})
   443  
   444  	return events
   445  }
   446  
   447  func (g Group) Members() []string {
   448  	return g.members.List()
   449  }
   450  
   451  func (g Group) MemberPublicKeys() ([]*ecdsa.PublicKey, error) {
   452  	var publicKeys = make([]*ecdsa.PublicKey, 0, len(g.Members()))
   453  	for _, memberPublicKey := range g.Members() {
   454  		publicKey, err := hexToPubkey(memberPublicKey)
   455  		if err != nil {
   456  			return nil, err
   457  		}
   458  		publicKeys = append(publicKeys, publicKey)
   459  	}
   460  	return publicKeys, nil
   461  }
   462  
   463  func hexToPubkey(pk string) (*ecdsa.PublicKey, error) {
   464  	bytes, err := types.DecodeHex(pk)
   465  	if err != nil {
   466  		return nil, err
   467  	}
   468  	return crypto.UnmarshalPubkey(bytes)
   469  }
   470  
   471  func (g Group) Admins() []string {
   472  	return g.admins.List()
   473  }
   474  
   475  func (g *Group) ProcessEvents(events []MembershipUpdateEvent) error {
   476  	for _, event := range events {
   477  		err := g.ProcessEvent(event)
   478  		if err != nil {
   479  			return err
   480  		}
   481  	}
   482  	return nil
   483  }
   484  
   485  func (g *Group) ProcessEvent(event MembershipUpdateEvent) error {
   486  	if !g.validateEvent(event) {
   487  		return fmt.Errorf("invalid event %#+v", event)
   488  	}
   489  	// Check if exists
   490  	g.events = append(g.events, event)
   491  	g.processEvent(event)
   492  	return nil
   493  }
   494  
   495  func (g Group) LastClockValue() uint64 {
   496  	if len(g.events) == 0 {
   497  		return 0
   498  	}
   499  	return g.events[len(g.events)-1].ClockValue
   500  }
   501  
   502  func (g Group) Creator() (string, error) {
   503  	if len(g.events) == 0 {
   504  		return "", errors.New("no events in the group")
   505  	}
   506  	first := g.events[0]
   507  	if first.Type != protobuf.MembershipUpdateEvent_CHAT_CREATED {
   508  		return "", fmt.Errorf("expected first event to be 'chat-created', got %s", first.Type)
   509  	}
   510  	return first.From, nil
   511  }
   512  
   513  func (g Group) isCreator(id string) (bool, error) {
   514  	c, err := g.Creator()
   515  	if err != nil {
   516  		return false, err
   517  	}
   518  
   519  	return id == c, nil
   520  }
   521  
   522  func (g Group) validateChatID(chatID string) bool {
   523  	creator, err := g.Creator()
   524  	if err != nil || creator == "" {
   525  		return false
   526  	}
   527  	// TODO: It does not verify that the prefix is a valid UUID.
   528  	//       Improve it so that the prefix follows UUIDv4 spec.
   529  	return strings.HasSuffix(chatID, creator) && chatID != creator
   530  }
   531  
   532  func (g Group) IsMember(id string) bool {
   533  	return g.members.Has(id)
   534  }
   535  
   536  func (g Group) WasEverMember(id string) (bool, error) {
   537  	isCreator, err := g.isCreator(id)
   538  	if err != nil {
   539  		return false, err
   540  	}
   541  
   542  	if isCreator {
   543  		return true, nil
   544  	}
   545  
   546  	for _, event := range g.events {
   547  		if event.Type == protobuf.MembershipUpdateEvent_MEMBERS_ADDED {
   548  			for _, member := range event.Members {
   549  				if member == id {
   550  					return true, nil
   551  				}
   552  			}
   553  		}
   554  	}
   555  	return false, nil
   556  }
   557  
   558  // validateEvent returns true if a given event is valid.
   559  func (g Group) validateEvent(event MembershipUpdateEvent) bool {
   560  	if len(event.From) == 0 {
   561  		return false
   562  	}
   563  	switch event.Type {
   564  	case protobuf.MembershipUpdateEvent_CHAT_CREATED:
   565  		return g.admins.Empty() && g.members.Empty()
   566  	case protobuf.MembershipUpdateEvent_NAME_CHANGED:
   567  		return (g.admins.Has(event.From) || g.members.Has(event.From)) && len(event.Name) > 0
   568  	case protobuf.MembershipUpdateEvent_COLOR_CHANGED:
   569  		return (g.admins.Has(event.From) || g.members.Has(event.From)) && len(event.Color) > 0
   570  	case protobuf.MembershipUpdateEvent_IMAGE_CHANGED:
   571  		return (g.admins.Has(event.From) || g.members.Has(event.From)) && len(event.Image) > 0
   572  	case protobuf.MembershipUpdateEvent_MEMBERS_ADDED:
   573  		return g.admins.Has(event.From) || g.members.Has(event.From)
   574  	case protobuf.MembershipUpdateEvent_MEMBER_JOINED:
   575  		return g.members.Has(event.From)
   576  	case protobuf.MembershipUpdateEvent_MEMBER_REMOVED:
   577  		// Member can remove themselves or admin can remove a member.
   578  		return len(event.Members) == 1 && (event.From == event.Members[0] || (g.admins.Has(event.From) && !g.admins.Has(event.Members[0])))
   579  	case protobuf.MembershipUpdateEvent_ADMINS_ADDED:
   580  		return g.admins.Has(event.From) && stringSliceSubset(event.Members, g.members.List())
   581  	case protobuf.MembershipUpdateEvent_ADMIN_REMOVED:
   582  		return len(event.Members) == 1 && g.admins.Has(event.From) && event.From == event.Members[0]
   583  	default:
   584  		return false
   585  	}
   586  }
   587  
   588  func (g *Group) processEvent(event MembershipUpdateEvent) {
   589  	switch event.Type {
   590  	case protobuf.MembershipUpdateEvent_CHAT_CREATED:
   591  		g.name = event.Name
   592  		g.color = event.Color
   593  		g.members.Add(event.From)
   594  		g.admins.Add(event.From)
   595  	case protobuf.MembershipUpdateEvent_NAME_CHANGED:
   596  		g.name = event.Name
   597  	case protobuf.MembershipUpdateEvent_COLOR_CHANGED:
   598  		g.color = event.Color
   599  	case protobuf.MembershipUpdateEvent_IMAGE_CHANGED:
   600  		g.image = event.Image
   601  	case protobuf.MembershipUpdateEvent_ADMINS_ADDED:
   602  		g.admins.Add(event.Members...)
   603  	case protobuf.MembershipUpdateEvent_ADMIN_REMOVED:
   604  		g.admins.Remove(event.Members[0])
   605  	case protobuf.MembershipUpdateEvent_MEMBERS_ADDED:
   606  		g.members.Add(event.Members...)
   607  	case protobuf.MembershipUpdateEvent_MEMBER_REMOVED:
   608  		g.admins.Remove(event.Members[0])
   609  		g.members.Remove(event.Members[0])
   610  	}
   611  }
   612  
   613  func (g *Group) sortEvents() {
   614  	sort.Slice(g.events, func(i, j int) bool {
   615  		return g.events[i].ClockValue < g.events[j].ClockValue
   616  	})
   617  }
   618  
   619  func stringSliceSubset(subset []string, set []string) bool {
   620  	for _, item1 := range set {
   621  		var found bool
   622  		for _, item2 := range subset {
   623  			if item1 == item2 {
   624  				found = true
   625  				break
   626  			}
   627  		}
   628  		if found {
   629  			return true
   630  		}
   631  	}
   632  	return false
   633  }
   634  
   635  func publicKeyToString(publicKey *ecdsa.PublicKey) string {
   636  	return types.EncodeHex(crypto.FromECDSAPub(publicKey))
   637  }
   638  
   639  type stringSet struct {
   640  	m     map[string]struct{}
   641  	items []string
   642  }
   643  
   644  func newStringSet() *stringSet {
   645  	return &stringSet{
   646  		m: make(map[string]struct{}),
   647  	}
   648  }
   649  
   650  func newStringSetFromSlice(s []string) *stringSet {
   651  	set := newStringSet()
   652  	if len(s) > 0 {
   653  		set.Add(s...)
   654  	}
   655  	return set
   656  }
   657  
   658  func (s *stringSet) Add(items ...string) {
   659  	for _, item := range items {
   660  		if _, ok := s.m[item]; !ok {
   661  			s.m[item] = struct{}{}
   662  			s.items = append(s.items, item)
   663  		}
   664  	}
   665  }
   666  
   667  func (s *stringSet) Remove(items ...string) {
   668  	for _, item := range items {
   669  		if _, ok := s.m[item]; ok {
   670  			delete(s.m, item)
   671  			s.removeFromItems(item)
   672  		}
   673  	}
   674  }
   675  
   676  func (s *stringSet) Has(item string) bool {
   677  	_, ok := s.m[item]
   678  	return ok
   679  }
   680  
   681  func (s *stringSet) Empty() bool {
   682  	return len(s.items) == 0
   683  }
   684  
   685  func (s *stringSet) List() []string {
   686  	return s.items
   687  }
   688  
   689  func (s *stringSet) removeFromItems(dropped string) {
   690  	n := 0
   691  	for _, item := range s.items {
   692  		if item != dropped {
   693  			s.items[n] = item
   694  			n++
   695  		}
   696  	}
   697  	s.items = s.items[:n]
   698  }