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  }