2023-07-18 15:06:12 +00:00
|
|
|
package communities
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2023-08-08 13:16:29 +00:00
|
|
|
"crypto/ecdsa"
|
2023-07-18 15:06:12 +00:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"sort"
|
|
|
|
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
|
|
|
2023-08-08 13:16:29 +00:00
|
|
|
"github.com/status-im/status-go/eth-node/crypto"
|
2023-07-18 15:06:12 +00:00
|
|
|
"github.com/status-im/status-go/protocol/protobuf"
|
|
|
|
)
|
|
|
|
|
|
|
|
type CommunityEvent struct {
|
|
|
|
CommunityEventClock uint64 `json:"communityEventClock"`
|
|
|
|
Type protobuf.CommunityEvent_EventType `json:"type"`
|
|
|
|
CommunityConfig *protobuf.CommunityConfig `json:"communityConfig,omitempty"`
|
|
|
|
TokenPermission *protobuf.CommunityTokenPermission `json:"tokenPermissions,omitempty"`
|
|
|
|
CategoryData *protobuf.CategoryData `json:"categoryData,omitempty"`
|
|
|
|
ChannelData *protobuf.ChannelData `json:"channelData,omitempty"`
|
|
|
|
MemberToAction string `json:"memberToAction,omitempty"`
|
|
|
|
MembersAdded map[string]*protobuf.CommunityMember `json:"membersAdded,omitempty"`
|
|
|
|
RejectedRequestsToJoin map[string]*protobuf.CommunityRequestToJoin `json:"rejectedRequestsToJoin,omitempty"`
|
|
|
|
AcceptedRequestsToJoin map[string]*protobuf.CommunityRequestToJoin `json:"acceptedRequestsToJoin,omitempty"`
|
2023-08-04 10:28:46 +00:00
|
|
|
TokenMetadata *protobuf.CommunityTokenMetadata `json:"tokenMetadata,omitempty"`
|
2023-08-08 18:33:29 +00:00
|
|
|
Payload []byte `json:"payload"`
|
|
|
|
Signature []byte `json:"signature"`
|
2023-07-18 15:06:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e *CommunityEvent) ToProtobuf() *protobuf.CommunityEvent {
|
|
|
|
return &protobuf.CommunityEvent{
|
|
|
|
CommunityEventClock: e.CommunityEventClock,
|
|
|
|
Type: e.Type,
|
|
|
|
CommunityConfig: e.CommunityConfig,
|
|
|
|
TokenPermission: e.TokenPermission,
|
|
|
|
CategoryData: e.CategoryData,
|
|
|
|
ChannelData: e.ChannelData,
|
|
|
|
MemberToAction: e.MemberToAction,
|
|
|
|
MembersAdded: e.MembersAdded,
|
|
|
|
RejectedRequestsToJoin: e.RejectedRequestsToJoin,
|
|
|
|
AcceptedRequestsToJoin: e.AcceptedRequestsToJoin,
|
2023-08-04 10:28:46 +00:00
|
|
|
TokenMetadata: e.TokenMetadata,
|
2023-07-18 15:06:12 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-08 13:16:29 +00:00
|
|
|
|
2023-08-08 18:33:29 +00:00
|
|
|
func communityEventFromProtobuf(msg *protobuf.SignedCommunityEvent) (*CommunityEvent, error) {
|
2023-07-18 15:06:12 +00:00
|
|
|
decodedEvent := protobuf.CommunityEvent{}
|
2023-08-08 18:33:29 +00:00
|
|
|
err := proto.Unmarshal(msg.Payload, &decodedEvent)
|
2023-07-18 15:06:12 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-08-08 18:33:29 +00:00
|
|
|
|
2023-07-18 15:06:12 +00:00
|
|
|
return &CommunityEvent{
|
|
|
|
CommunityEventClock: decodedEvent.CommunityEventClock,
|
|
|
|
Type: decodedEvent.Type,
|
|
|
|
CommunityConfig: decodedEvent.CommunityConfig,
|
|
|
|
TokenPermission: decodedEvent.TokenPermission,
|
|
|
|
CategoryData: decodedEvent.CategoryData,
|
|
|
|
ChannelData: decodedEvent.ChannelData,
|
|
|
|
MemberToAction: decodedEvent.MemberToAction,
|
|
|
|
MembersAdded: decodedEvent.MembersAdded,
|
|
|
|
RejectedRequestsToJoin: decodedEvent.RejectedRequestsToJoin,
|
|
|
|
AcceptedRequestsToJoin: decodedEvent.AcceptedRequestsToJoin,
|
2023-08-04 10:28:46 +00:00
|
|
|
TokenMetadata: decodedEvent.TokenMetadata,
|
2023-08-08 18:33:29 +00:00
|
|
|
Payload: msg.Payload,
|
|
|
|
Signature: msg.Signature,
|
2023-07-18 15:06:12 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-08-08 13:16:29 +00:00
|
|
|
func (e *CommunityEvent) RecoverSigner() (*ecdsa.PublicKey, error) {
|
|
|
|
if e.Signature == nil || len(e.Signature) == 0 {
|
|
|
|
return nil, errors.New("missing signature")
|
|
|
|
}
|
|
|
|
|
|
|
|
signer, err := crypto.SigToPub(
|
|
|
|
crypto.Keccak256(e.Payload),
|
|
|
|
e.Signature,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.New("failed to recover signer")
|
|
|
|
}
|
|
|
|
|
|
|
|
return signer, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *CommunityEvent) Sign(pk *ecdsa.PrivateKey) error {
|
|
|
|
sig, err := crypto.Sign(crypto.Keccak256(e.Payload), pk)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
e.Signature = sig
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:06:12 +00:00
|
|
|
type CommunityEventsMessage struct {
|
|
|
|
CommunityID []byte `json:"communityId"`
|
|
|
|
EventsBaseCommunityDescription []byte `json:"eventsBaseCommunityDescription"`
|
|
|
|
Events []CommunityEvent `json:"events,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *CommunityEventsMessage) ToProtobuf() protobuf.CommunityEventsMessage {
|
2023-08-08 18:33:29 +00:00
|
|
|
result := protobuf.CommunityEventsMessage{
|
2023-07-18 15:06:12 +00:00
|
|
|
CommunityId: m.CommunityID,
|
|
|
|
EventsBaseCommunityDescription: m.EventsBaseCommunityDescription,
|
2023-08-08 18:33:29 +00:00
|
|
|
SignedEvents: []*protobuf.SignedCommunityEvent{},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, event := range m.Events {
|
|
|
|
signedEvent := &protobuf.SignedCommunityEvent{
|
|
|
|
Signature: event.Signature,
|
|
|
|
Payload: event.Payload,
|
|
|
|
}
|
|
|
|
result.SignedEvents = append(result.SignedEvents, signedEvent)
|
2023-07-18 15:06:12 +00:00
|
|
|
}
|
2023-08-08 18:33:29 +00:00
|
|
|
|
|
|
|
return result
|
2023-07-18 15:06:12 +00:00
|
|
|
}
|
|
|
|
|
2023-08-08 18:33:29 +00:00
|
|
|
func CommunityEventsMessageFromProtobuf(msg *protobuf.CommunityEventsMessage) (*CommunityEventsMessage, error) {
|
|
|
|
result := &CommunityEventsMessage{
|
|
|
|
CommunityID: msg.CommunityId,
|
|
|
|
EventsBaseCommunityDescription: msg.EventsBaseCommunityDescription,
|
|
|
|
Events: []CommunityEvent{},
|
2023-07-18 15:06:12 +00:00
|
|
|
}
|
|
|
|
|
2023-08-08 18:33:29 +00:00
|
|
|
for _, signedEvent := range msg.SignedEvents {
|
|
|
|
event, err := communityEventFromProtobuf(signedEvent)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result.Events = append(result.Events, *event)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
2023-07-18 15:06:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (m *CommunityEventsMessage) Marshal() ([]byte, error) {
|
|
|
|
pb := m.ToProtobuf()
|
|
|
|
return proto.Marshal(&pb)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Community) mergeCommunityEvents(communityEventMessage *CommunityEventsMessage) {
|
|
|
|
if c.config.EventsData == nil {
|
|
|
|
c.config.EventsData = &EventsData{
|
|
|
|
EventsBaseCommunityDescription: communityEventMessage.EventsBaseCommunityDescription,
|
|
|
|
Events: communityEventMessage.Events,
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, update := range communityEventMessage.Events {
|
|
|
|
var exists bool
|
|
|
|
for _, existing := range c.config.EventsData.Events {
|
|
|
|
if isCommunityEventsEqual(update, existing) {
|
|
|
|
exists = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !exists {
|
|
|
|
c.config.EventsData.Events = append(c.config.EventsData.Events, update)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
c.sortCommunityEvents()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Community) sortCommunityEvents() {
|
|
|
|
sort.Slice(c.config.EventsData.Events, func(i, j int) bool {
|
|
|
|
return c.config.EventsData.Events[i].CommunityEventClock < c.config.EventsData.Events[j].CommunityEventClock
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func validateCommunityEvent(communityEvent *CommunityEvent) error {
|
|
|
|
switch communityEvent.Type {
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_EDIT:
|
|
|
|
if communityEvent.CommunityConfig == nil || communityEvent.CommunityConfig.Identity == nil ||
|
|
|
|
communityEvent.CommunityConfig.Permissions == nil || communityEvent.CommunityConfig.AdminSettings == nil {
|
|
|
|
return errors.New("invalid config change admin event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE:
|
|
|
|
if communityEvent.TokenPermission == nil || len(communityEvent.TokenPermission.Id) == 0 {
|
|
|
|
return errors.New("invalid token permission change event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE:
|
|
|
|
if communityEvent.TokenPermission == nil || len(communityEvent.TokenPermission.Id) == 0 {
|
|
|
|
return errors.New("invalid token permission delete event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_CREATE:
|
|
|
|
if communityEvent.CategoryData == nil || len(communityEvent.CategoryData.CategoryId) == 0 {
|
|
|
|
return errors.New("invalid community category create event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_DELETE:
|
|
|
|
if communityEvent.CategoryData == nil || len(communityEvent.CategoryData.CategoryId) == 0 {
|
|
|
|
return errors.New("invalid community category delete event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_EDIT:
|
|
|
|
if communityEvent.CategoryData == nil || len(communityEvent.CategoryData.CategoryId) == 0 {
|
|
|
|
return errors.New("invalid community category edit event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_CREATE:
|
|
|
|
if communityEvent.ChannelData == nil || len(communityEvent.ChannelData.ChannelId) == 0 ||
|
|
|
|
communityEvent.ChannelData.Channel == nil {
|
|
|
|
return errors.New("invalid community channel create event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_DELETE:
|
|
|
|
if communityEvent.ChannelData == nil || len(communityEvent.ChannelData.ChannelId) == 0 {
|
|
|
|
return errors.New("invalid community channel delete event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_EDIT:
|
|
|
|
if communityEvent.ChannelData == nil || len(communityEvent.ChannelData.ChannelId) == 0 ||
|
|
|
|
communityEvent.ChannelData.Channel == nil {
|
|
|
|
return errors.New("invalid community channel edit event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_REORDER:
|
|
|
|
if communityEvent.ChannelData == nil || len(communityEvent.ChannelData.ChannelId) == 0 {
|
|
|
|
return errors.New("invalid community channel reorder event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_REORDER:
|
|
|
|
if communityEvent.CategoryData == nil || len(communityEvent.CategoryData.CategoryId) == 0 {
|
|
|
|
return errors.New("invalid community category reorder event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_ACCEPT:
|
refactor: EventSenders forward RequestToJoin decision to control node
This is a bigger change in how community membership requests are handled
among admins, token masters, owners, and control nodes.
Prior to this commit, all privileged users, also known as
`EventSenders`, were able to accept and reject community membership
requests and those changes would be applied by all users.
This commit changes this behaviour such that:
1. EventSenders can make a decision (accept, reject), but merely forward
their decision to the control node, which ultimately has to confirm
it
2. EventSenders are no longer removing or adding members to and from
communities
3. When an eventsender signaled a decision, the membership request will
enter a pending state (acceptedPending or rejectedPending)
4. Once a decision was made by one eventsender, no other eventsender can
override that decision
This implementation is covered with a bunch of tests:
- Ensure that decision made by event sender is shared with other event
senders
- `testAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders()`
- `testRejectMemberRequestToJoinResponseSharedWithOtherEventSenders()`
- Ensure memebrship request stays pending, until control node has
confirmed decision by event senders
- `testAcceptMemberRequestToJoinNotConfirmedByControlNode()`
- `testRejectMemberRequestToJoinNotConfirmedByControlNode()`
- Ensure that decision made by event sender cannot be overriden by other
event senders
- `testEventSenderCannotOverrideRequestToJoinState()`
These test cases live in three test suites for different event sender
types respectively
- `OwnerWithoutCommunityKeyCommunityEventsSuite`
- `TokenMasterCommunityEventsSuite`
- `AdminCommunityEventsSuite`
In addition to the changes mentioned above, there's also a smaller
changes that ensures membership requests to *not* attached revealed wallet
addresses when the requests are sent to event senders (in addition to
control nodes).
Requests send to a control node will still include revealed addresses as
the control node needs them to verify token permissions.
This commit does not yet handle the case of event senders attempting to
kick and ban members.
Similar to accepting and rejecting membership requests, kicking and
banning need a new pending state. However, we don't track such state in
local databases yet so those two cases will be handled in future commit
to not have this commit grow larger.
2023-08-02 12:04:47 +00:00
|
|
|
if communityEvent.AcceptedRequestsToJoin == nil {
|
2023-07-18 15:06:12 +00:00
|
|
|
return errors.New("invalid community request to join accepted event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_REJECT:
|
|
|
|
if communityEvent.RejectedRequestsToJoin == nil {
|
|
|
|
return errors.New("invalid community request to join reject event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_MEMBER_KICK:
|
|
|
|
if len(communityEvent.MemberToAction) == 0 {
|
|
|
|
return errors.New("invalid community member kick event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_MEMBER_BAN:
|
|
|
|
if len(communityEvent.MemberToAction) == 0 {
|
|
|
|
return errors.New("invalid community member ban event")
|
|
|
|
}
|
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_MEMBER_UNBAN:
|
|
|
|
if len(communityEvent.MemberToAction) == 0 {
|
|
|
|
return errors.New("invalid community member unban event")
|
|
|
|
}
|
2023-08-04 10:28:46 +00:00
|
|
|
|
|
|
|
case protobuf.CommunityEvent_COMMUNITY_TOKEN_ADD:
|
|
|
|
if communityEvent.TokenMetadata == nil || len(communityEvent.TokenMetadata.ContractAddresses) == 0 {
|
|
|
|
return errors.New("invalid add community token event")
|
|
|
|
}
|
2023-07-18 15:06:12 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func isCommunityEventsEqual(left CommunityEvent, right CommunityEvent) bool {
|
2023-08-08 18:33:29 +00:00
|
|
|
return bytes.Equal(left.Payload, right.Payload)
|
2023-07-18 15:06:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func communityEventsToJSONEncodedBytes(communityEvents []CommunityEvent) ([]byte, error) {
|
|
|
|
return json.Marshal(communityEvents)
|
|
|
|
}
|
|
|
|
|
|
|
|
func communityEventsFromJSONEncodedBytes(jsonEncodedRawEvents []byte) ([]CommunityEvent, error) {
|
|
|
|
var events []CommunityEvent
|
|
|
|
err := json.Unmarshal(jsonEncodedRawEvents, &events)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return events, nil
|
|
|
|
}
|