github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/internal/privatemessaging/groupmanager.go (about) 1 // Copyright © 2021 Kaleido, Inc. 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 package privatemessaging 18 19 import ( 20 "context" 21 "encoding/json" 22 "time" 23 24 "github.com/kaleido-io/firefly/internal/data" 25 "github.com/kaleido-io/firefly/internal/i18n" 26 "github.com/kaleido-io/firefly/internal/log" 27 "github.com/kaleido-io/firefly/pkg/database" 28 "github.com/kaleido-io/firefly/pkg/fftypes" 29 "github.com/karlseguin/ccache" 30 ) 31 32 type GroupManager interface { 33 GetGroupByID(ctx context.Context, id string) (*fftypes.Group, error) 34 GetGroups(ctx context.Context, filter database.AndFilter) ([]*fftypes.Group, error) 35 ResolveInitGroup(ctx context.Context, msg *fftypes.Message) (*fftypes.Group, error) 36 } 37 38 type groupManager struct { 39 database database.Plugin 40 data data.Manager 41 groupCacheTTL time.Duration 42 groupCache *ccache.Cache 43 } 44 45 func (gm *groupManager) groupInit(ctx context.Context, signer *fftypes.Identity, group *fftypes.Group) (err error) { 46 47 // Serialize it into a data object, as a piece of data we can write to a message 48 data := &fftypes.Data{ 49 Validator: fftypes.ValidatorTypeSystemDefinition, 50 ID: fftypes.NewUUID(), 51 Namespace: fftypes.SystemNamespace, 52 Created: fftypes.Now(), 53 } 54 data.Value, err = json.Marshal(&group) 55 if err == nil { 56 err = data.Seal(ctx) 57 } 58 if err != nil { 59 return i18n.WrapError(ctx, err, i18n.MsgSerializationFailed) 60 } 61 62 // In the case of groups, we actually write the unconfirmed group directly to our database. 63 // So it can be used straight away. 64 // We're able to do this by making the identifier of the group a hash of the identity fields 65 // (name, ledger and member list), as that is all the group contains. There's no data in there. 66 if err = gm.database.UpsertGroup(ctx, group, true); err != nil { 67 return err 68 } 69 70 // Write as data to the local store 71 if err = gm.database.UpsertData(ctx, data, true, false /* we just generated the ID, so it is new */); err != nil { 72 return err 73 } 74 75 // Create a private send message referring to the data 76 msg := &fftypes.Message{ 77 Header: fftypes.MessageHeader{ 78 Group: group.Hash, 79 Namespace: fftypes.SystemNamespace, 80 Type: fftypes.MessageTypeGroupInit, 81 Author: signer.Identifier, 82 Tag: string(fftypes.SystemTagDefineGroup), 83 Topics: fftypes.FFNameArray{group.Topic()}, 84 TxType: fftypes.TransactionTypeBatchPin, 85 }, 86 Data: fftypes.DataRefs{ 87 {ID: data.ID, Hash: data.Hash}, 88 }, 89 } 90 91 // Seal the message 92 err = msg.Seal(ctx) 93 if err == nil { 94 // Store the message - this asynchronously triggers the next step in process 95 err = gm.database.UpsertMessage(ctx, msg, false /* newly generated UUID in Seal */, false) 96 } 97 98 return err 99 100 } 101 102 func (gm *groupManager) GetGroupByID(ctx context.Context, hash string) (*fftypes.Group, error) { 103 h, err := fftypes.ParseBytes32(ctx, hash) 104 if err != nil { 105 return nil, err 106 } 107 return gm.database.GetGroupByHash(ctx, h) 108 } 109 110 func (gm *groupManager) GetGroups(ctx context.Context, filter database.AndFilter) ([]*fftypes.Group, error) { 111 return gm.database.GetGroups(ctx, filter) 112 } 113 114 func (gm *groupManager) getGroupNodes(ctx context.Context, groupHash *fftypes.Bytes32) ([]*fftypes.Node, error) { 115 116 if cached := gm.groupCache.Get(groupHash.String()); cached != nil { 117 cached.Extend(gm.groupCacheTTL) 118 return cached.Value().([]*fftypes.Node), nil 119 } 120 121 group, err := gm.database.GetGroupByHash(ctx, groupHash) 122 if err != nil { 123 return nil, err 124 } 125 if group == nil { 126 return nil, i18n.NewError(ctx, i18n.MsgGroupNotFound, groupHash) 127 } 128 129 // We de-duplicate nodes in the case that the payload needs to be received by multiple org identities 130 // that share a single node. 131 nodes := make([]*fftypes.Node, 0, len(group.Members)) 132 knownIDs := make(map[fftypes.UUID]bool) 133 for _, r := range group.Members { 134 node, err := gm.database.GetNodeByID(ctx, r.Node) 135 if err != nil { 136 return nil, err 137 } 138 if node == nil { 139 return nil, i18n.NewError(ctx, i18n.MsgNodeNotFound, r.Node) 140 } 141 if !knownIDs[*node.ID] { 142 knownIDs[*node.ID] = true 143 nodes = append(nodes, node) 144 } 145 } 146 147 gm.groupCache.Set(group.Hash.String(), nodes, gm.groupCacheTTL) 148 return nodes, nil 149 } 150 151 // ResolveInitGroup is called when a message comes in as the first private message on a particular context. 152 // If the message is a group creation request, then it is validated and the group is created. 153 // Otherwise, the existing group must exist. 154 // 155 // Errors are only returned for database issues. For validation issues, a nil group is returned without an error. 156 func (gm *groupManager) ResolveInitGroup(ctx context.Context, msg *fftypes.Message) (*fftypes.Group, error) { 157 if msg.Header.Namespace == fftypes.SystemNamespace && msg.Header.Tag == string(fftypes.SystemTagDefineGroup) { 158 // Store the new group 159 data, foundAll, err := gm.data.GetMessageData(ctx, msg, true) 160 if err != nil || !foundAll || len(data) == 0 { 161 log.L(ctx).Warnf("Group %s definition in message %s invalid: missing data", msg.Header.Group, msg.Header.ID) 162 return nil, err 163 } 164 var newGroup fftypes.Group 165 err = json.Unmarshal(data[0].Value, &newGroup) 166 if err != nil { 167 log.L(ctx).Warnf("Group %s definition in message %s invalid: %s", msg.Header.Group, msg.Header.ID, err) 168 return nil, nil 169 } 170 err = newGroup.Validate(ctx, true) 171 if err != nil { 172 log.L(ctx).Warnf("Group %s definition in message %s invalid: %s", msg.Header.Group, msg.Header.ID, err) 173 return nil, nil 174 } 175 if !newGroup.Hash.Equals(msg.Header.Group) { 176 log.L(ctx).Warnf("Group %s definition in message %s invalid: mismatched hash with message '%s'", msg.Header.Group, msg.Header.ID, newGroup.Hash) 177 return nil, nil 178 } 179 newGroup.Message = msg.Header.ID 180 err = gm.database.UpsertGroup(ctx, &newGroup, true) 181 if err != nil { 182 return nil, err 183 } 184 event := fftypes.NewEvent(fftypes.EventTypeGroupConfirmed, newGroup.Namespace, nil, newGroup.Hash) 185 if err = gm.database.UpsertEvent(ctx, event, false); err != nil { 186 return nil, err 187 } 188 return &newGroup, nil 189 } 190 191 // Get the existing group 192 group, err := gm.database.GetGroupByHash(ctx, msg.Header.Group) 193 if err != nil { 194 return group, err 195 } 196 if group == nil { 197 log.L(ctx).Warnf("Group %s not found", msg.Header.Group) 198 return nil, nil 199 } 200 return group, nil 201 }