diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 4025964b2..1273bd18f 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -1372,6 +1372,10 @@ func (o *Community) Description() *protobuf.CommunityDescription { return o.config.CommunityDescription } +func (o *Community) DescriptionProtocolMessage() []byte { + return o.config.CommunityDescriptionProtocolMessage +} + func (o *Community) marshaledDescription() ([]byte, error) { clone := proto.Clone(o.config.CommunityDescription).(*protobuf.CommunityDescription) diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 217732540..a3234d74c 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -1638,7 +1638,12 @@ func (m *Manager) HandleCommunityDescriptionMessage(signer *ecdsa.PublicKey, des if hasTokenOwnership && verifiedOwner != nil { // Override verified owner - m.logger.Info("updating verified owner", zap.String("communityID", community.IDString()), zap.String("owner", common.PubkeyToHex(verifiedOwner))) + m.logger.Info("updating verified owner", + zap.String("communityID", community.IDString()), + zap.String("verifiedOwner", common.PubkeyToHex(verifiedOwner)), + zap.String("signer", common.PubkeyToHex(signer)), + zap.String("controlNode", common.PubkeyToHex(community.ControlNode())), + ) // If we are not the verified owner anymore, drop the private key if !common.IsPubKeyEqual(verifiedOwner, &m.identity.PublicKey) { diff --git a/protocol/communities_messenger_helpers_test.go b/protocol/communities_messenger_helpers_test.go index b5917f5fb..7aaaa41ae 100644 --- a/protocol/communities_messenger_helpers_test.go +++ b/protocol/communities_messenger_helpers_test.go @@ -331,7 +331,8 @@ func advertiseCommunityTo(s *suite.Suite, community *communities.Community, owne messageState := user.buildMessageState() messageState.CurrentMessageState = &CurrentMessageState{} messageState.CurrentMessageState.PublicKey = &user.identity.PublicKey - err = user.handleCommunityDescription(messageState, signer, description, wrappedCommunity, nil) + // TODO: handle shards? + err = user.handleCommunityDescription(messageState, signer, description, wrappedCommunity, nil, nil) s.Require().NoError(err) } diff --git a/protocol/communities_messenger_signers_test.go b/protocol/communities_messenger_signers_test.go index 6f88fc269..3ca223eae 100644 --- a/protocol/communities_messenger_signers_test.go +++ b/protocol/communities_messenger_signers_test.go @@ -1,16 +1,20 @@ package protocol import ( + "bytes" "context" "testing" "time" + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/suite" "go.uber.org/zap" gethcommon "github.com/ethereum/go-ethereum/common" hexutil "github.com/ethereum/go-ethereum/common/hexutil" + utils "github.com/status-im/status-go/common" gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" @@ -581,3 +585,159 @@ func (s *MessengerCommunitiesSignersSuite) TestNewOwnerAcceptRequestToJoin() { s.joinCommunity(s.alice, community, s.bob) } + +func (s *MessengerCommunitiesSignersSuite) testDescriptionSignature(description []byte) { + var amm protobuf.ApplicationMetadataMessage + err := proto.Unmarshal(description, &amm) + s.Require().NoError(err) + + signer, err := utils.RecoverKey(&amm) + s.Require().NoError(err) + s.NotNil(signer) +} + +func (s *MessengerCommunitiesSignersSuite) forceCommunityChange(community *communities.Community, owner *Messenger, user *Messenger) { + newDescription := community.DescriptionText() + " new" + _, err := owner.EditCommunity(&requests.EditCommunity{ + CommunityID: community.ID(), + CreateCommunity: requests.CreateCommunity{ + Membership: protobuf.CommunityPermissions_AUTO_ACCEPT, + Name: community.Name(), + Color: community.Color(), + Description: newDescription, + }, + }) + s.Require().NoError(err) + + // alice receives new description + _, err = WaitOnMessengerResponse(user, func(r *MessengerResponse) bool { + return len(r.Communities()) > 0 && r.Communities()[0].DescriptionText() == newDescription + }, "new description not received") + s.Require().NoError(err) +} + +func (s *MessengerCommunitiesSignersSuite) testSyncCommunity(mintOwnerToken bool) { + + community := s.createCommunity(s.john) + s.advertiseCommunityTo(s.john, community, s.alice) + s.joinCommunity(s.john, community, s.alice) + + // FIXME: Remove this workaround when fixed: + // https://github.com/status-im/status-go/issues/4413 + s.forceCommunityChange(community, s.john, s.alice) + + aliceCommunity, err := s.alice.GetCommunityByID(community.ID()) + s.Require().NoError(err) + s.testDescriptionSignature(aliceCommunity.DescriptionProtocolMessage()) + + if mintOwnerToken { + // john mints owner token + var chainID uint64 = 1 + tokenAddress := "token-address" + tokenName := "tokenName" + tokenSymbol := "TSM" + _, err := s.john.SaveCommunityToken(&token.CommunityToken{ + TokenType: protobuf.CommunityTokenType_ERC721, + CommunityID: community.IDString(), + Address: tokenAddress, + ChainID: int(chainID), + Name: tokenName, + Supply: &bigint.BigInt{}, + Symbol: tokenSymbol, + PrivilegesLevel: token.OwnerLevel, + }, nil) + s.Require().NoError(err) + + // john adds minted owner token to community + err = s.john.AddCommunityToken(community.IDString(), int(chainID), tokenAddress) + s.Require().NoError(err) + + // update mock - the signer for the community returned by the contracts should be john + s.collectiblesServiceMock.SetSignerPubkeyForCommunity(community.ID(), common.PubkeyToHex(&s.john.identity.PublicKey)) + s.collectiblesServiceMock.SetMockCollectibleContractData(chainID, tokenAddress, + &communitytokens.CollectibleContractData{TotalSupply: &bigint.BigInt{}}) + + // alice accepts community update + _, err = WaitOnSignaledMessengerResponse( + s.alice, + func(r *MessengerResponse) bool { + return len(r.Communities()) > 0 && len(r.Communities()[0].TokenPermissions()) == 1 + }, + "no communities", + ) + s.Require().NoError(err) + } + + // Create alice second instance + alice2, err := newMessengerWithKey( + s.shh, + s.alice.identity, + s.logger.With(zap.String("name", "alice-2")), + nil) + + s.Require().NoError(err) + + _, err = alice2.Start() + s.Require().NoError(err) + defer alice2.Shutdown() // nolint: errcheck + + // Create communities backup + + clock, _ := s.alice.getLastClockWithRelatedChat() + communitiesBackup, err := s.alice.backupCommunities(context.Background(), clock) + s.Require().NoError(err) + + // Find wanted communities in the backup + + var syncCommunityMessages []*protobuf.SyncInstallationCommunity + + for _, b := range communitiesBackup { + for _, c := range b.Communities { + if bytes.Equal(c.Id, community.ID()) { + syncCommunityMessages = append(syncCommunityMessages, c) + } + } + } + s.Require().Len(syncCommunityMessages, 1) + + s.testDescriptionSignature(syncCommunityMessages[0].Description) + + // Push the backup into second instance + + messageState := alice2.buildMessageState() + err = alice2.HandleSyncInstallationCommunity(messageState, syncCommunityMessages[0], nil) + + s.Require().NoError(err) + s.Require().Len(messageState.Response.Communities(), 1) + + expectedControlNode := community.PublicKey() + if mintOwnerToken { + expectedControlNode = &s.john.identity.PublicKey + } + + responseCommunity := messageState.Response.Communities()[0] + s.Require().Equal(community.IDString(), responseCommunity.IDString()) + s.Require().True(common.IsPubKeyEqual(expectedControlNode, responseCommunity.ControlNode())) +} + +func (s *MessengerCommunitiesSignersSuite) TestSyncTokenGatedCommunity() { + testCases := []struct { + name string + mintOwnerToken bool + }{ + { + name: "general community sync", + mintOwnerToken: false, + }, + { + name: "community with token ownership", + mintOwnerToken: true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.testSyncCommunity(tc.mintOwnerToken) + }) + } +} diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index fa8490889..6544c2874 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -20,6 +20,8 @@ import ( "go.uber.org/zap" + utils "github.com/status-im/status-go/common" + "github.com/status-im/status-go/account" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" @@ -2603,8 +2605,8 @@ func (m *Messenger) passStoredCommunityInfoToSignalHandler(community *communitie } // handleCommunityDescription handles an community description -func (m *Messenger) handleCommunityDescription(state *ReceivedMessageState, signer *ecdsa.PublicKey, description *protobuf.CommunityDescription, rawPayload []byte, shard *protobuf.Shard) error { - communityResponse, err := m.communitiesManager.HandleCommunityDescriptionMessage(signer, description, rawPayload, nil, shard) +func (m *Messenger) handleCommunityDescription(state *ReceivedMessageState, signer *ecdsa.PublicKey, description *protobuf.CommunityDescription, rawPayload []byte, verifiedOwner *ecdsa.PublicKey, shard *protobuf.Shard) error { + communityResponse, err := m.communitiesManager.HandleCommunityDescriptionMessage(signer, description, rawPayload, verifiedOwner, shard) if err != nil { return err } @@ -2945,7 +2947,7 @@ func (m *Messenger) HandleSyncInstallationCommunity(messageState *ReceivedMessag } func (m *Messenger) handleSyncInstallationCommunity(messageState *ReceivedMessageState, syncCommunity *protobuf.SyncInstallationCommunity, statusMessage *v1protocol.StatusMessage) error { - logger := m.logger.Named("handleSyncCommunity") + logger := m.logger.Named("handleSyncInstallationCommunity") // Should handle community shouldHandle, err := m.communitiesManager.ShouldHandleSyncCommunity(syncCommunity) @@ -3008,8 +3010,17 @@ func (m *Messenger) handleSyncInstallationCommunity(messageState *ReceivedMessag return err } + // This is our own message, so we can trust the set community owner + // This is good to do so that we don't have to queue all the actions done after the handled community description. + // `signer` is `communityID` for a community with no owner token and `owner public key` otherwise + signer, err := utils.RecoverKey(&amm) + if err != nil { + logger.Debug("failed to recover community description signer", zap.Error(err)) + return err + } + // TODO: handle shard - err = m.handleCommunityDescription(messageState, orgPubKey, &cd, syncCommunity.Description, nil) + err = m.handleCommunityDescription(messageState, signer, &cd, syncCommunity.Description, signer, nil) if err != nil { logger.Debug("m.handleCommunityDescription error", zap.Error(err)) return err @@ -3031,22 +3042,6 @@ func (m *Messenger) handleSyncInstallationCommunity(messageState *ReceivedMessag } } - savedCommunity, err := m.communitiesManager.GetByID(syncCommunity.Id) - if err != nil { - return err - } - - // TODO: if the community is token gated, it will be validated asynchronously - // syncing needs to be adjusted in this case - if savedCommunity == nil { - return nil - } - - if err := m.handleCommunityTokensMetadataByPrivilegedMembers(savedCommunity); err != nil { - logger.Debug("m.handleCommunityTokensMetadataByPrivilegedMembers", zap.Error(err)) - return err - } - // if we are not waiting for approval, join or leave the community if !pending { var mr *MessengerResponse @@ -3102,10 +3097,6 @@ func (m *Messenger) HandleSyncCommunitySettings(messageState *ReceivedMessageSta return nil } -func (m *Messenger) handleCommunityTokensMetadataByPrivilegedMembers(community *communities.Community) error { - return m.communitiesManager.HandleCommunityTokensMetadataByPrivilegedMembers(community) -} - func (m *Messenger) InitHistoryArchiveTasks(communities []*communities.Community) { m.communitiesManager.LogStdout("initializing history archive tasks") diff --git a/protocol/messenger_handler.go b/protocol/messenger_handler.go index 44dba993e..fdb919bd0 100644 --- a/protocol/messenger_handler.go +++ b/protocol/messenger_handler.go @@ -2324,7 +2324,7 @@ func (m *Messenger) handleChatMessage(state *ReceivedMessageState, forceSeen boo return err } - err = m.handleCommunityDescription(state, signer, description, receivedMessage.GetCommunity(), receivedMessage.GetShard()) + err = m.handleCommunityDescription(state, signer, description, receivedMessage.GetCommunity(), nil, receivedMessage.GetShard()) if err != nil { return err } @@ -3667,7 +3667,7 @@ func (m *Messenger) HandlePushNotificationRequest(state *ReceivedMessageState, m func (m *Messenger) HandleCommunityDescription(state *ReceivedMessageState, message *protobuf.CommunityDescription, statusMessage *v1protocol.StatusMessage) error { // TODO: handle shard - err := m.handleCommunityDescription(state, state.CurrentMessageState.PublicKey, message, statusMessage.EncryptionLayer.Payload, nil) + err := m.handleCommunityDescription(state, state.CurrentMessageState.PublicKey, message, statusMessage.EncryptionLayer.Payload, nil, nil) if err != nil { m.logger.Warn("failed to handle CommunityDescription", zap.Error(err)) return err