github.com/osdi23p228/fabric@v0.0.0-20221218062954-77808885f5db/gossip/privdata/distributor.go (about)

     1  /*
     2  Copyright IBM Corp. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package privdata
     8  
     9  import (
    10  	"bytes"
    11  	"fmt"
    12  	"math/rand"
    13  	"sync"
    14  	"sync/atomic"
    15  	"time"
    16  
    17  	"github.com/golang/protobuf/proto"
    18  	protosgossip "github.com/hyperledger/fabric-protos-go/gossip"
    19  	"github.com/hyperledger/fabric-protos-go/ledger/rwset"
    20  	"github.com/hyperledger/fabric-protos-go/peer"
    21  	"github.com/hyperledger/fabric-protos-go/transientstore"
    22  	"github.com/osdi23p228/fabric/core/common/privdata"
    23  	"github.com/osdi23p228/fabric/gossip/api"
    24  	gossipCommon "github.com/osdi23p228/fabric/gossip/common"
    25  	"github.com/osdi23p228/fabric/gossip/discovery"
    26  	"github.com/osdi23p228/fabric/gossip/filter"
    27  	gossipgossip "github.com/osdi23p228/fabric/gossip/gossip"
    28  	"github.com/osdi23p228/fabric/gossip/metrics"
    29  	"github.com/osdi23p228/fabric/gossip/protoext"
    30  	"github.com/osdi23p228/fabric/gossip/util"
    31  	"github.com/osdi23p228/fabric/msp"
    32  	"github.com/osdi23p228/fabric/protoutil"
    33  	"github.com/pkg/errors"
    34  )
    35  
    36  // gossipAdapter an adapter for API's required from gossip module
    37  type gossipAdapter interface {
    38  	// SendByCriteria sends a given message to all peers that match the given SendCriteria
    39  	SendByCriteria(message *protoext.SignedGossipMessage, criteria gossipgossip.SendCriteria) error
    40  
    41  	// PeerFilter receives a SubChannelSelectionCriteria and returns a RoutingFilter that selects
    42  	// only peer identities that match the given criteria, and that they published their channel participation
    43  	PeerFilter(channel gossipCommon.ChannelID, messagePredicate api.SubChannelSelectionCriteria) (filter.RoutingFilter, error)
    44  
    45  	// IdentityInfo returns information known peer identities
    46  	IdentityInfo() api.PeerIdentitySet
    47  
    48  	// PeersOfChannel returns the NetworkMembers considered alive
    49  	// and also subscribed to the channel given
    50  	PeersOfChannel(gossipCommon.ChannelID) []discovery.NetworkMember
    51  }
    52  
    53  // PvtDataDistributor interface to defines API of distributing private data
    54  type PvtDataDistributor interface {
    55  	// Distribute broadcast reliably private data read write set based on policies
    56  	Distribute(txID string, privData *transientstore.TxPvtReadWriteSetWithConfigInfo, blkHt uint64) error
    57  }
    58  
    59  // IdentityDeserializerFactory is a factory interface to create
    60  // IdentityDeserializer for given channel
    61  type IdentityDeserializerFactory interface {
    62  	// GetIdentityDeserializer returns an IdentityDeserializer
    63  	// instance for the specified chain
    64  	GetIdentityDeserializer(chainID string) msp.IdentityDeserializer
    65  }
    66  
    67  // IdentityDeserializerFactoryFunc is a function adapter for
    68  // IdentityDeserializerFactory.
    69  type IdentityDeserializerFactoryFunc func(chainID string) msp.IdentityDeserializer
    70  
    71  func (i IdentityDeserializerFactoryFunc) GetIdentityDeserializer(chainID string) msp.IdentityDeserializer {
    72  	return i(chainID)
    73  }
    74  
    75  // distributorImpl the implementation of the private data distributor interface
    76  type distributorImpl struct {
    77  	chainID string
    78  	gossipAdapter
    79  	CollectionAccessFactory
    80  	pushAckTimeout time.Duration
    81  	logger         util.Logger
    82  	metrics        *metrics.PrivdataMetrics
    83  }
    84  
    85  //go:generate mockery -dir . -name CollectionAccessFactory -case underscore -output ./mocks/
    86  
    87  // CollectionAccessFactory an interface to generate collection access policy
    88  type CollectionAccessFactory interface {
    89  	// AccessPolicy based on collection configuration
    90  	AccessPolicy(config *peer.CollectionConfig, chainID string) (privdata.CollectionAccessPolicy, error)
    91  }
    92  
    93  //go:generate mockery -dir . -name CollectionAccessPolicy -case underscore -output ./mocks/
    94  
    95  type CollectionAccessPolicy interface {
    96  	privdata.CollectionAccessPolicy
    97  }
    98  
    99  // policyAccessFactory the implementation of CollectionAccessFactory
   100  type policyAccessFactory struct {
   101  	IdentityDeserializerFactory
   102  }
   103  
   104  func (p *policyAccessFactory) AccessPolicy(config *peer.CollectionConfig, chainID string) (privdata.CollectionAccessPolicy, error) {
   105  	colAP := &privdata.SimpleCollection{}
   106  	switch cconf := config.Payload.(type) {
   107  	case *peer.CollectionConfig_StaticCollectionConfig:
   108  		err := colAP.Setup(cconf.StaticCollectionConfig, p.GetIdentityDeserializer(chainID))
   109  		if err != nil {
   110  			return nil, errors.WithMessagef(err, "error setting up collection  %#v", cconf.StaticCollectionConfig.Name)
   111  		}
   112  	default:
   113  		return nil, errors.New("unexpected collection type")
   114  	}
   115  	return colAP, nil
   116  }
   117  
   118  // NewCollectionAccessFactory
   119  func NewCollectionAccessFactory(factory IdentityDeserializerFactory) CollectionAccessFactory {
   120  	return &policyAccessFactory{
   121  		IdentityDeserializerFactory: factory,
   122  	}
   123  }
   124  
   125  // NewDistributor a constructor for private data distributor capable to send
   126  // private read write sets for underlying collection
   127  func NewDistributor(chainID string, gossip gossipAdapter, factory CollectionAccessFactory,
   128  	metrics *metrics.PrivdataMetrics, pushAckTimeout time.Duration) PvtDataDistributor {
   129  	return &distributorImpl{
   130  		chainID:                 chainID,
   131  		gossipAdapter:           gossip,
   132  		CollectionAccessFactory: factory,
   133  		pushAckTimeout:          pushAckTimeout,
   134  		logger:                  logger.With("channel", chainID),
   135  		metrics:                 metrics,
   136  	}
   137  }
   138  
   139  // Distribute broadcast reliably private data read write set based on policies
   140  func (d *distributorImpl) Distribute(txID string, privData *transientstore.TxPvtReadWriteSetWithConfigInfo, blkHt uint64) error {
   141  	disseminationPlan, err := d.computeDisseminationPlan(txID, privData, blkHt)
   142  	if err != nil {
   143  		return errors.WithStack(err)
   144  	}
   145  	return d.disseminate(disseminationPlan)
   146  }
   147  
   148  type dissemination struct {
   149  	msg      *protoext.SignedGossipMessage
   150  	criteria gossipgossip.SendCriteria
   151  }
   152  
   153  func (d *distributorImpl) computeDisseminationPlan(txID string,
   154  	privDataWithConfig *transientstore.TxPvtReadWriteSetWithConfigInfo,
   155  	blkHt uint64) ([]*dissemination, error) {
   156  	privData := privDataWithConfig.PvtRwset
   157  	var disseminationPlan []*dissemination
   158  	for _, pvtRwset := range privData.NsPvtRwset {
   159  		namespace := pvtRwset.Namespace
   160  		configPackage, found := privDataWithConfig.CollectionConfigs[namespace]
   161  		if !found {
   162  			d.logger.Error("Collection config package for", namespace, "chaincode is not provided")
   163  			return nil, errors.New(fmt.Sprint("collection config package for", namespace, "chaincode is not provided"))
   164  		}
   165  
   166  		for _, collection := range pvtRwset.CollectionPvtRwset {
   167  			colCP, err := d.getCollectionConfig(configPackage, collection)
   168  			collectionName := collection.CollectionName
   169  			if err != nil {
   170  				d.logger.Error("Could not find collection access policy for", namespace, " and collection", collectionName, "error", err)
   171  				return nil, errors.WithMessage(err, fmt.Sprint("could not find collection access policy for", namespace, " and collection", collectionName, "error", err))
   172  			}
   173  
   174  			colAP, err := d.AccessPolicy(colCP, d.chainID)
   175  			if err != nil {
   176  				d.logger.Error("Could not obtain collection access policy, collection name", collectionName, "due to", err)
   177  				return nil, errors.Wrap(err, fmt.Sprint("Could not obtain collection access policy, collection name", collectionName, "due to", err))
   178  			}
   179  
   180  			colFilter := colAP.AccessFilter()
   181  			if colFilter == nil {
   182  				d.logger.Error("Collection access policy for", collectionName, "has no filter")
   183  				return nil, errors.Errorf("No collection access policy filter computed for %v", collectionName)
   184  			}
   185  
   186  			pvtDataMsg, err := d.createPrivateDataMessage(txID, namespace, collection, &peer.CollectionConfigPackage{Config: []*peer.CollectionConfig{colCP}}, blkHt)
   187  			if err != nil {
   188  				return nil, errors.WithStack(err)
   189  			}
   190  
   191  			d.logger.Debugf("Computing dissemination plan for collection [%s]", collectionName)
   192  			dPlan, err := d.disseminationPlanForMsg(colAP, colFilter, pvtDataMsg)
   193  			if err != nil {
   194  				return nil, errors.WithMessagef(err, "could not build private data dissemination plan for chaincode %s and collection %s", namespace, collectionName)
   195  			}
   196  			disseminationPlan = append(disseminationPlan, dPlan...)
   197  		}
   198  	}
   199  	return disseminationPlan, nil
   200  }
   201  
   202  func (d *distributorImpl) getCollectionConfig(config *peer.CollectionConfigPackage, collection *rwset.CollectionPvtReadWriteSet) (*peer.CollectionConfig, error) {
   203  	for _, c := range config.Config {
   204  		if staticConfig := c.GetStaticCollectionConfig(); staticConfig != nil {
   205  			if staticConfig.Name == collection.CollectionName {
   206  				return c, nil
   207  			}
   208  		}
   209  	}
   210  	return nil, errors.New(fmt.Sprint("no configuration for collection", collection.CollectionName, "found"))
   211  }
   212  
   213  func (d *distributorImpl) disseminationPlanForMsg(colAP privdata.CollectionAccessPolicy, colFilter privdata.Filter, pvtDataMsg *protoext.SignedGossipMessage) ([]*dissemination, error) {
   214  	var disseminationPlan []*dissemination
   215  
   216  	routingFilter, err := d.gossipAdapter.PeerFilter(gossipCommon.ChannelID(d.chainID), func(signature api.PeerSignature) bool {
   217  		return colFilter(protoutil.SignedData{
   218  			Data:      signature.Message,
   219  			Signature: signature.Signature,
   220  			Identity:  []byte(signature.PeerIdentity),
   221  		})
   222  	})
   223  
   224  	if err != nil {
   225  		d.logger.Error("Failed to retrieve peer routing filter for channel", d.chainID, ":", err)
   226  		return nil, err
   227  	}
   228  
   229  	m := pvtDataMsg.GetPrivateData().Payload
   230  
   231  	eligiblePeers := d.eligiblePeersOfChannel(routingFilter)
   232  
   233  	// With the shift to per peer dissemination in FAB-15389, we must first check
   234  	// that there are enough eligible peers to satisfy RequiredPeerCount.
   235  	if (len(eligiblePeers)) < colAP.RequiredPeerCount() {
   236  		return nil, errors.Errorf("required to disseminate to at least %d peers, but know of only %d eligible peers", colAP.RequiredPeerCount(), len(eligiblePeers))
   237  	}
   238  
   239  	// Group eligible peers by org so that we can disseminate across orgs first
   240  	identitySetsByOrg := d.identitiesOfEligiblePeersByOrg(eligiblePeers, colAP)
   241  
   242  	// peerEndpoints are used for dissemination debug only
   243  	peerEndpoints := map[string]string{}
   244  	for _, peer := range eligiblePeers {
   245  		epToAdd := peer.Endpoint
   246  		if epToAdd == "" {
   247  			epToAdd = peer.InternalEndpoint
   248  		}
   249  		peerEndpoints[string(peer.PKIid)] = epToAdd
   250  	}
   251  
   252  	// Initialize maximumPeerRemainingCount and requiredPeerRemainingCount,
   253  	// these will be decremented until we've selected enough peers for dissemination
   254  	maximumPeerRemainingCount := colAP.MaximumPeerCount()
   255  	requiredPeerRemainingCount := colAP.RequiredPeerCount()
   256  
   257  	remainingPeersAcrossOrgs := []api.PeerIdentityInfo{}
   258  	selectedPeerEndpointsForDebug := []string{}
   259  
   260  	rand.Seed(time.Now().Unix())
   261  
   262  	// PHASE 1 - Select one peer from each eligible org
   263  	if maximumPeerRemainingCount > 0 {
   264  		for _, selectionPeersForOrg := range identitySetsByOrg {
   265  
   266  			// Peers are tagged as a required peer (acksRequired=1) for RequiredPeerCount up front before dissemination.
   267  			// TODO It would be better to attempt dissemination to MaxPeerCount first, and then verify that enough sends were acknowledged to meet RequiredPeerCount.
   268  			acksRequired := 1
   269  			if requiredPeerRemainingCount == 0 {
   270  				acksRequired = 0
   271  			}
   272  
   273  			selectedPeerIndex := rand.Intn(len(selectionPeersForOrg))
   274  			peer2SendPerOrg := selectionPeersForOrg[selectedPeerIndex]
   275  			selectedPeerEndpointsForDebug = append(selectedPeerEndpointsForDebug, peerEndpoints[string(peer2SendPerOrg.PKIId)])
   276  			sc := gossipgossip.SendCriteria{
   277  				Timeout:  d.pushAckTimeout,
   278  				Channel:  gossipCommon.ChannelID(d.chainID),
   279  				MaxPeers: 1,
   280  				MinAck:   acksRequired,
   281  				IsEligible: func(member discovery.NetworkMember) bool {
   282  					return bytes.Equal(member.PKIid, peer2SendPerOrg.PKIId)
   283  				},
   284  			}
   285  			disseminationPlan = append(disseminationPlan, &dissemination{
   286  				criteria: sc,
   287  				msg: &protoext.SignedGossipMessage{
   288  					Envelope:      proto.Clone(pvtDataMsg.Envelope).(*protosgossip.Envelope),
   289  					GossipMessage: proto.Clone(pvtDataMsg.GossipMessage).(*protosgossip.GossipMessage),
   290  				},
   291  			})
   292  
   293  			// Add unselected peers to remainingPeersAcrossOrgs
   294  			for i, peer := range selectionPeersForOrg {
   295  				if i != selectedPeerIndex {
   296  					remainingPeersAcrossOrgs = append(remainingPeersAcrossOrgs, peer)
   297  				}
   298  			}
   299  
   300  			if requiredPeerRemainingCount > 0 {
   301  				requiredPeerRemainingCount--
   302  			}
   303  
   304  			maximumPeerRemainingCount--
   305  			if maximumPeerRemainingCount == 0 {
   306  				d.logger.Debug("MaximumPeerCount satisfied")
   307  				d.logger.Debugf("Disseminating private RWSet for TxID [%s] namespace [%s] collection [%s] to peers: %v", m.TxId, m.Namespace, m.CollectionName, selectedPeerEndpointsForDebug)
   308  				return disseminationPlan, nil
   309  			}
   310  		}
   311  	}
   312  
   313  	// PHASE 2 - Select additional peers to satisfy colAP.MaximumPeerCount() if there are still peers in the remainingPeersAcrossOrgs pool
   314  	numRemainingPeersToSelect := maximumPeerRemainingCount
   315  	if len(remainingPeersAcrossOrgs) < maximumPeerRemainingCount {
   316  		numRemainingPeersToSelect = len(remainingPeersAcrossOrgs)
   317  	}
   318  	if numRemainingPeersToSelect > 0 {
   319  		d.logger.Debugf("MaximumPeerCount not yet satisfied after picking one peer per org, selecting %d more peer(s) for dissemination", numRemainingPeersToSelect)
   320  	}
   321  	for maximumPeerRemainingCount > 0 && len(remainingPeersAcrossOrgs) > 0 {
   322  		required := 1
   323  		if requiredPeerRemainingCount == 0 {
   324  			required = 0
   325  		}
   326  		selectedPeerIndex := rand.Intn(len(remainingPeersAcrossOrgs))
   327  		peer2Send := remainingPeersAcrossOrgs[selectedPeerIndex]
   328  		selectedPeerEndpointsForDebug = append(selectedPeerEndpointsForDebug, peerEndpoints[string(peer2Send.PKIId)])
   329  		sc := gossipgossip.SendCriteria{
   330  			Timeout:  d.pushAckTimeout,
   331  			Channel:  gossipCommon.ChannelID(d.chainID),
   332  			MaxPeers: 1,
   333  			MinAck:   required,
   334  			IsEligible: func(member discovery.NetworkMember) bool {
   335  				return bytes.Equal(member.PKIid, peer2Send.PKIId)
   336  			},
   337  		}
   338  		disseminationPlan = append(disseminationPlan, &dissemination{
   339  			criteria: sc,
   340  			msg: &protoext.SignedGossipMessage{
   341  				Envelope:      proto.Clone(pvtDataMsg.Envelope).(*protosgossip.Envelope),
   342  				GossipMessage: proto.Clone(pvtDataMsg.GossipMessage).(*protosgossip.GossipMessage),
   343  			},
   344  		})
   345  		if requiredPeerRemainingCount > 0 {
   346  			requiredPeerRemainingCount--
   347  		}
   348  
   349  		maximumPeerRemainingCount--
   350  
   351  		// remove the selected peer from remaining peers
   352  		remainingPeersAcrossOrgs = append(remainingPeersAcrossOrgs[:selectedPeerIndex], remainingPeersAcrossOrgs[selectedPeerIndex+1:]...)
   353  	}
   354  
   355  	d.logger.Debugf("Disseminating private RWSet for TxID [%s] namespace [%s] collection [%s] to peers: %v", m.TxId, m.Namespace, m.CollectionName, selectedPeerEndpointsForDebug)
   356  	return disseminationPlan, nil
   357  }
   358  
   359  // identitiesOfEligiblePeersByOrg returns the peers eligible for a collection (aka PeerIdentitySet) grouped in a hash map keyed by orgid
   360  func (d *distributorImpl) identitiesOfEligiblePeersByOrg(eligiblePeers []discovery.NetworkMember, colAP privdata.CollectionAccessPolicy) map[string]api.PeerIdentitySet {
   361  	return d.gossipAdapter.IdentityInfo().
   362  		Filter(func(info api.PeerIdentityInfo) bool {
   363  			if _, ok := colAP.MemberOrgs()[string(info.Organization)]; ok {
   364  				return true
   365  			}
   366  			// peer not in the org
   367  			return false
   368  		}).Filter(func(info api.PeerIdentityInfo) bool {
   369  		for _, peer := range eligiblePeers {
   370  			if bytes.Equal(info.PKIId, peer.PKIid) {
   371  				return true
   372  			}
   373  		}
   374  		// peer not in the channel
   375  		return false
   376  	}).ByOrg()
   377  }
   378  
   379  func (d *distributorImpl) eligiblePeersOfChannel(routingFilter filter.RoutingFilter) []discovery.NetworkMember {
   380  	var eligiblePeers []discovery.NetworkMember
   381  	for _, peer := range d.gossipAdapter.PeersOfChannel(gossipCommon.ChannelID(d.chainID)) {
   382  		if routingFilter(peer) {
   383  			eligiblePeers = append(eligiblePeers, peer)
   384  		}
   385  	}
   386  	return eligiblePeers
   387  }
   388  
   389  func (d *distributorImpl) disseminate(disseminationPlan []*dissemination) error {
   390  	var failures uint32
   391  	var wg sync.WaitGroup
   392  	wg.Add(len(disseminationPlan))
   393  	start := time.Now()
   394  	for _, dis := range disseminationPlan {
   395  		go func(dis *dissemination) {
   396  			defer wg.Done()
   397  			defer d.reportSendDuration(start)
   398  			err := d.SendByCriteria(dis.msg, dis.criteria)
   399  			if err != nil {
   400  				atomic.AddUint32(&failures, 1)
   401  				m := dis.msg.GetPrivateData().Payload
   402  				d.logger.Error("Failed disseminating private RWSet for TxID", m.TxId, ", namespace", m.Namespace, "collection", m.CollectionName, ":", err)
   403  			}
   404  		}(dis)
   405  	}
   406  	wg.Wait()
   407  	failureCount := atomic.LoadUint32(&failures)
   408  	if failureCount != 0 {
   409  		return errors.Errorf("Failed disseminating %d out of %d private dissemination plans", failureCount, len(disseminationPlan))
   410  	}
   411  	return nil
   412  }
   413  
   414  func (d *distributorImpl) reportSendDuration(startTime time.Time) {
   415  	d.metrics.SendDuration.With("channel", d.chainID).Observe(time.Since(startTime).Seconds())
   416  }
   417  
   418  func (d *distributorImpl) createPrivateDataMessage(txID, namespace string,
   419  	collection *rwset.CollectionPvtReadWriteSet,
   420  	ccp *peer.CollectionConfigPackage,
   421  	blkHt uint64) (*protoext.SignedGossipMessage, error) {
   422  	msg := &protosgossip.GossipMessage{
   423  		Channel: []byte(d.chainID),
   424  		Nonce:   util.RandomUInt64(),
   425  		Tag:     protosgossip.GossipMessage_CHAN_ONLY,
   426  		Content: &protosgossip.GossipMessage_PrivateData{
   427  			PrivateData: &protosgossip.PrivateDataMessage{
   428  				Payload: &protosgossip.PrivatePayload{
   429  					Namespace:         namespace,
   430  					CollectionName:    collection.CollectionName,
   431  					TxId:              txID,
   432  					PrivateRwset:      collection.Rwset,
   433  					PrivateSimHeight:  blkHt,
   434  					CollectionConfigs: ccp,
   435  				},
   436  			},
   437  		},
   438  	}
   439  
   440  	pvtDataMsg, err := protoext.NoopSign(msg)
   441  	if err != nil {
   442  		return nil, err
   443  	}
   444  	return pvtDataMsg, nil
   445  }