From dd5ebf197f825ed68d2d1c9f1977cc33ca3ec130 Mon Sep 17 00:00:00 2001 From: yacovm Date: Mon, 14 May 2018 01:07:08 +0300 Subject: [PATCH] [FAB-10028] Prepare discovery for cc2cc queries This change set refacors the endorsement logic in order to prepare a smoother landing for the cc2cc query support. It also adds stricter validation for chaincode queries. Change-Id: Id17e139fc5c7294ce2ec5fb54e8f1415a74960a3 Signed-off-by: yacovm --- discovery/endorsement/collection.go | 14 ++ discovery/endorsement/collection_test.go | 27 +++ discovery/endorsement/endorsement.go | 203 ++++++++++++++++++----- discovery/service.go | 26 ++- discovery/service_test.go | 45 ++++- 5 files changed, 264 insertions(+), 51 deletions(-) diff --git a/discovery/endorsement/collection.go b/discovery/endorsement/collection.go index 21e454c4991..776689b94be 100644 --- a/discovery/endorsement/collection.go +++ b/discovery/endorsement/collection.go @@ -15,6 +15,20 @@ import ( type filterPrincipalSets func(collectionName string, principalSets policies.PrincipalSets) (policies.PrincipalSets, error) +func (f filterPrincipalSets) forCollections(ccName string, collections ...string) filterFunc { + return func(principalSets policies.PrincipalSets) (policies.PrincipalSets, error) { + var err error + for _, col := range collections { + principalSets, err = f(col, principalSets) + if err != nil { + logger.Warningf("Failed filtering collection for chaincode %s, collection %s: %v", ccName, col, err) + return nil, err + } + } + return principalSets, nil + } +} + func newCollectionFilter(configBytes []byte) (filterPrincipalSets, error) { mapFilter := make(principalSetsByCollectionName) if len(configBytes) == 0 { diff --git a/discovery/endorsement/collection_test.go b/discovery/endorsement/collection_test.go index 669e2c072bb..37ebff1dc3c 100644 --- a/discovery/endorsement/collection_test.go +++ b/discovery/endorsement/collection_test.go @@ -13,9 +13,36 @@ import ( "github.com/hyperledger/fabric/protos/common" "github.com/hyperledger/fabric/protos/msp" "github.com/hyperledger/fabric/protos/utils" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) +func TestForCollections(t *testing.T) { + foos := policies.PrincipalSets{{orgPrincipal("foo")}} + bars := policies.PrincipalSets{{orgPrincipal("bar")}} + f := filterPrincipalSets(func(collectionName string, principalSets policies.PrincipalSets) (policies.PrincipalSets, error) { + switch collectionName { + case "foo": + return foos, nil + case "bar": + return bars, nil + default: + return nil, errors.Errorf("collection %s doesn't exist", collectionName) + } + }) + + res, err := f.forCollections("mycc", "foo")(nil) + assert.NoError(t, err) + assert.Equal(t, foos, res) + + res, err = f.forCollections("mycc", "bar")(nil) + assert.NoError(t, err) + assert.Equal(t, bars, res) + + res, err = f.forCollections("mycc", "baz")(nil) + assert.Equal(t, "collection baz doesn't exist", err.Error()) +} + func TestCollectionFilter(t *testing.T) { org1AndOrg2 := []*msp.MSPPrincipal{orgPrincipal("Org1MSP"), orgPrincipal("Org2MSP")} org1AndOrg3 := []*msp.MSPPrincipal{orgPrincipal("Org1MSP"), orgPrincipal("Org3MSP")} diff --git a/discovery/endorsement/endorsement.go b/discovery/endorsement/endorsement.go index 2c672b2b9fd..4211885b715 100644 --- a/discovery/endorsement/endorsement.go +++ b/discovery/endorsement/endorsement.go @@ -79,14 +79,18 @@ type peerPrincipalEvaluator func(member discovery2.NetworkMember, principal *msp // PeersForEndorsement returns an EndorsementDescriptor for a given set of peers, channel, and chaincode func (ea *endorsementAnalyzer) PeersForEndorsement(chainID common.ChainID, interest *discovery.ChaincodeInterest) (*discovery.EndorsementDescriptor, error) { - chaincode := interest.Chaincodes[0] - loadCollections := len(chaincode.CollectionNames) > 0 - ccMD := ea.Metadata(string(chainID), chaincode.Name, loadCollections) - if ccMD == nil { - return nil, errors.Errorf("No metadata was found for chaincode %s in channel %s", chaincode.Name, string(chainID)) + // For now we only support a single chaincode + if len(interest.Chaincodes) != 1 { + return nil, errors.New("only a single chaincode is supported") + } + interest.Chaincodes = []*discovery.ChaincodeCall{interest.Chaincodes[0]} + + metadataAndCollectionFilters, err := loadMetadataAndFilters(chainID, interest, ea) + if err != nil { + return nil, errors.WithStack(err) } // Filter out peers that don't have the chaincode installed on them - chanMembership := ea.PeersOfChannel(chainID).Filter(peersWithChaincode(ccMD)) + chanMembership := ea.PeersOfChannel(chainID).Filter(peersWithChaincode(metadataAndCollectionFilters.md...)) channelMembersById := chanMembership.ByID() // Choose only the alive messages of those that have joined the channel aliveMembership := ea.Peers().Intersect(chanMembership) @@ -95,50 +99,58 @@ func (ea *endorsementAnalyzer) PeersForEndorsement(chainID common.ChainID, inter identities := ea.IdentityInfo() identitiesOfMembers := computeIdentitiesOfMembers(identities, membersById) - // Retrieve the policy for the chaincode - pol := ea.PolicyByChaincode(string(chainID), chaincode.Name) - if pol == nil { - logger.Debug("Policy for chaincode '", chaincode, "'doesn't exist") - return nil, errors.New("policy not found") + // Retrieve the policies for the chaincodes + pols, err := ea.loadPolicies(chainID, interest) + if err != nil { + return nil, errors.WithStack(err) } + // For now we take the first policy and ignore the rest + pol := pols[0] // Compute the combinations of principals (principal sets) that satisfy the endorsement policy principalsSets := policies.PrincipalSets(pol.SatisfiedBy()) - // Obtain the MSP IDs of the members of the channel that are alive - mspIDsOfChannelPeers := mspIDsOfMembers(membersById, identities.ByID()) // Filter out principal sets that contain MSP IDs for peers that don't have the chaincode(s) installed - principalsSets = principalsSets.ContainingOnly(func(principal *msp.MSPPrincipal) bool { - mspID := ea.MSPOfPrincipal(principal) - _, exists := mspIDsOfChannelPeers[mspID] - return mspID != "" && exists - }) + principalsSets = ea.excludeIfCCNotInstalled(principalsSets, membersById, identities.ByID()) - if loadCollections { - filterByCollections, err := newCollectionFilter(ccMD.CollectionsConfig) - if err != nil { - return nil, errors.Wrap(err, "failed creating collection filter") - } - for _, col := range chaincode.CollectionNames { - principalsSets, err = filterByCollections(col, principalsSets) - if err != nil { - return nil, errors.Wrapf(err, "failed filtering collection %s for chaincode %s", col, chaincode.Name) - } - } + // Filter the principal sets by the collections (if applicable) + principalsSets, err = metadataAndCollectionFilters.filter(principalsSets) + if err != nil { + return nil, errors.WithStack(err) } + return ea.computeEndorsementResponse(&context{ + chaincode: interest.Chaincodes[0].Name, + channel: string(chainID), + principalsSets: principalsSets, + channelMembersById: channelMembersById, + aliveMembership: aliveMembership, + identitiesOfMembers: identitiesOfMembers, + }) +} + +type context struct { + chaincode string + channel string + aliveMembership discovery2.Members + principalsSets []policies.PrincipalSet + channelMembersById map[string]discovery2.NetworkMember + identitiesOfMembers memberIdentities +} + +func (ea *endorsementAnalyzer) computeEndorsementResponse(ctx *context) (*discovery.EndorsementDescriptor, error) { // mapPrincipalsToGroups returns a mapping from principals to their corresponding groups. // groups are just human readable representations that mask the principals behind them - principalGroups := mapPrincipalsToGroups(principalsSets) + principalGroups := mapPrincipalsToGroups(ctx.principalsSets) // principalsToPeersGraph computes a bipartite graph (V1 U V2 , E) // such that V1 is the peers, V2 are the principals, // and each e=(peer,principal) is in E if the peer satisfies the principal satGraph := principalsToPeersGraph(principalAndPeerData{ - members: aliveMembership, + members: ctx.aliveMembership, pGrps: principalGroups, - }, ea.satisfiesPrincipal(string(chainID), identitiesOfMembers)) + }, ea.satisfiesPrincipal(ctx.channel, ctx.identitiesOfMembers)) - layouts := computeLayouts(principalsSets, principalGroups, satGraph) + layouts := computeLayouts(ctx.principalsSets, principalGroups, satGraph) if len(layouts) == 0 { return nil, errors.New("cannot satisfy any principal combination") } @@ -146,17 +158,120 @@ func (ea *endorsementAnalyzer) PeersForEndorsement(chainID common.ChainID, inter criteria := &peerMembershipCriteria{ possibleLayouts: layouts, satGraph: satGraph, - chanMemberById: channelMembersById, - idOfMembers: identitiesOfMembers, + chanMemberById: ctx.channelMembersById, + idOfMembers: ctx.identitiesOfMembers, } return &discovery.EndorsementDescriptor{ - Chaincode: chaincode.Name, + Chaincode: ctx.chaincode, Layouts: layouts, EndorsersByGroups: endorsersByGroup(criteria), }, nil } +func (ea *endorsementAnalyzer) excludeIfCCNotInstalled(principalsSets policies.PrincipalSets, membersById map[string]discovery2.NetworkMember, identitiesByID map[string]api.PeerIdentityInfo) policies.PrincipalSets { + // Obtain the MSP IDs of the members of the channel that are alive + mspIDsOfChannelPeers := mspIDsOfMembers(membersById, identitiesByID) + principalsSets = ea.excludePrincipals(func(principal *msp.MSPPrincipal) bool { + mspID := ea.MSPOfPrincipal(principal) + _, exists := mspIDsOfChannelPeers[mspID] + return mspID != "" && exists + }, principalsSets) + return principalsSets +} + +func (ea *endorsementAnalyzer) excludePrincipals(filter func(principal *msp.MSPPrincipal) bool, sets ...policies.PrincipalSets) policies.PrincipalSets { + var res []policies.PrincipalSets + for _, principalSets := range sets { + principalSets = principalSets.ContainingOnly(filter) + if len(principalSets) == 0 { + continue + } + res = append(res, principalSets) + } + if len(res) == 0 { + return nil + } + return res[0] +} + +func (ea *endorsementAnalyzer) loadPolicies(chainID common.ChainID, interest *discovery.ChaincodeInterest) ([]policies.InquireablePolicy, error) { + var res []policies.InquireablePolicy + for _, chaincode := range interest.Chaincodes { + pol := ea.PolicyByChaincode(string(chainID), chaincode.Name) + if pol == nil { + logger.Debug("Policy for chaincode '", chaincode, "'doesn't exist") + return nil, errors.New("policy not found") + } + res = append(res, pol) + } + return res, nil +} + +type filterFunc func(policies.PrincipalSets) (policies.PrincipalSets, error) + +type filterFunctions []filterFunc + +type metadataAndColFilter struct { + md []*chaincode.Metadata + filter filterFunc +} + +func loadMetadataAndFilters(chainID common.ChainID, interest *discovery.ChaincodeInterest, fetch chaincodeMetadataFetcher) (*metadataAndColFilter, error) { + var metadata []*chaincode.Metadata + var filters filterFunctions + + for _, chaincode := range interest.Chaincodes { + ccMD := fetch.Metadata(string(chainID), chaincode.Name, len(chaincode.CollectionNames) > 0) + if ccMD == nil { + return nil, errors.Errorf("No metadata was found for chaincode %s in channel %s", chaincode.Name, string(chainID)) + } + metadata = append(metadata, ccMD) + if len(chaincode.CollectionNames) == 0 { + continue + } + f, err := newCollectionFilter(ccMD.CollectionsConfig) + if err != nil { + logger.Warningf("Failed initializing collection filter for chaincode %s: %v", chaincode.Name, err) + return nil, errors.WithStack(err) + } + filters = append(filters, f.forCollections(chaincode.Name, chaincode.CollectionNames...)) + } + + return computeFiltersWithMetadata(filters, metadata), nil +} + +func computeFiltersWithMetadata(filters filterFunctions, metadata []*chaincode.Metadata) *metadataAndColFilter { + if len(filters) == 0 { + return &metadataAndColFilter{ + md: metadata, + filter: noopFilter, + } + } + + return &metadataAndColFilter{ + md: metadata, + filter: filters.combine(), + } +} + +func noopFilter(policies policies.PrincipalSets) (policies.PrincipalSets, error) { + return policies, nil +} + +func (filters filterFunctions) combine() filterFunc { + return func(principals policies.PrincipalSets) (policies.PrincipalSets, error) { + var err error + for _, filter := range filters { + principals, err = filter(principals) + if err != nil { + return nil, err + } + } + return principals, nil + } +} + func (ea *endorsementAnalyzer) satisfiesPrincipal(channel string, identitiesOfMembers memberIdentities) peerPrincipalEvaluator { return func(member discovery2.NetworkMember, principal *msp.MSPPrincipal) bool { err := ea.SatisfiesPrincipal(channel, identitiesOfMembers.identityByPKIID(member.PKIid), principal) @@ -373,17 +488,23 @@ func (l layouts) groupsSet() map[string]struct{} { return m } -func peersWithChaincode(ccMD *chaincode.Metadata) func(member discovery2.NetworkMember) bool { +func peersWithChaincode(metadata ...*chaincode.Metadata) func(member discovery2.NetworkMember) bool { return func(member discovery2.NetworkMember) bool { if member.Properties == nil { return false } - for _, cc := range member.Properties.Chaincodes { - if cc.Name == ccMD.Name && cc.Version == ccMD.Version { - return true + for _, ccMD := range metadata { + var found bool + for _, cc := range member.Properties.Chaincodes { + if cc.Name == ccMD.Name && cc.Version == ccMD.Version { + found = true + } + } + if !found { + return false } } - return false + return true } } diff --git a/discovery/service.go b/discovery/service.go index e3b9948ed5a..e185d7ba78e 100644 --- a/discovery/service.go +++ b/discovery/service.go @@ -119,11 +119,11 @@ func (s *service) dispatch(q *discovery.Query) *discovery.QueryResult { } func (s *service) chaincodeQuery(q *discovery.Query) *discovery.QueryResult { + if err := validateCCQuery(q.GetCcQuery()); err != nil { + return wrapError(err) + } var descriptors []*discovery.EndorsementDescriptor for _, interest := range q.GetCcQuery().Interests { - if len(interest.Chaincodes) == 0 || interest.Chaincodes[0] == nil { - return wrapError(errors.Errorf("must include at least one chaincode")) - } desc, err := s.PeersForEndorsement(common2.ChainID(q.Channel), interest) if err != nil { logger.Errorf("Failed constructing descriptor for chaincode %s,: %v", interest, err) @@ -247,6 +247,26 @@ func validateStructure(ctx context.Context, request *discovery.SignedRequest, ad return req, nil } +func validateCCQuery(ccQuery *discovery.ChaincodeQuery) error { + if len(ccQuery.Interests) == 0 { + return errors.New("chaincode query must have at least one chaincode interest") + } + for _, interest := range ccQuery.Interests { + if interest == nil { + return errors.New("chaincode interest is nil") + } + if len(interest.Chaincodes) == 0 { + return errors.New("chaincode interest must contain at least one chaincode") + } + for _, cc := range interest.Chaincodes { + if cc.Name == "" { + return errors.New("chaincode name in interest cannot be empty") + } + } + } + return nil +} + func wrapError(err error) *discovery.QueryResult { return &discovery.QueryResult{ Result: &discovery.QueryResult_Error{ diff --git a/discovery/service_test.go b/discovery/service_test.go index f7bb84682fd..e004dffdd77 100644 --- a/discovery/service_test.go +++ b/discovery/service_test.go @@ -101,9 +101,31 @@ func TestService(t *testing.T) { } resp, err = service.Discover(ctx, toSignedRequest(req)) assert.NoError(t, err) - assert.Contains(t, resp.Results[0].GetError().Content, "must include at least one chaincode") + assert.Contains(t, resp.Results[0].GetError().Content, "chaincode interest must contain at least one chaincode") - // Scenario VI: Request with a CC query where one chaincode is unavailable + // Scenario VI: Request a CC query with no interests at all + req.Queries[0].Query = &discovery.Query_CcQuery{ + CcQuery: &discovery.ChaincodeQuery{ + Interests: []*discovery.ChaincodeInterest{}}, + } + resp, err = service.Discover(ctx, toSignedRequest(req)) + assert.NoError(t, err) + assert.Contains(t, resp.Results[0].GetError().Content, "chaincode query must have at least one chaincode interest") + + // Scenario VII: Request a CC query with a chaincode name that is empty + req.Queries[0].Query = &discovery.Query_CcQuery{ + CcQuery: &discovery.ChaincodeQuery{ + Interests: []*discovery.ChaincodeInterest{{ + Chaincodes: []*discovery.ChaincodeCall{{ + Name: "", + }}, + }}}, + } + resp, err = service.Discover(ctx, toSignedRequest(req)) + assert.NoError(t, err) + assert.Contains(t, resp.Results[0].GetError().Content, "chaincode name in interest cannot be empty") + + // Scenario VIII: Request with a CC query where one chaincode is unavailable req.Queries[0].Query = &discovery.Query_CcQuery{ CcQuery: &discovery.ChaincodeQuery{ Interests: []*discovery.ChaincodeInterest{ @@ -122,7 +144,7 @@ func TestService(t *testing.T) { assert.Contains(t, resp.Results[0].GetError().Content, "failed constructing descriptor") assert.Contains(t, resp.Results[0].GetError().Content, "unknownCC") - // Scenario VII: Request with a CC query where all are available + // Scenario IX: Request with a CC query where all are available req.Queries[0].Query = &discovery.Query_CcQuery{ CcQuery: &discovery.ChaincodeQuery{ Interests: []*discovery.ChaincodeInterest{ @@ -145,7 +167,7 @@ func TestService(t *testing.T) { }) assert.Equal(t, expected, resp) - // Scenario VIII: Request with a config query + // Scenario X: Request with a config query mockSup.On("Config", mock.Anything).Return(nil, errors.New("failed fetching config")).Once() req.Queries[0].Query = &discovery.Query_ConfigQuery{ ConfigQuery: &discovery.ConfigQuery{}, @@ -154,7 +176,7 @@ func TestService(t *testing.T) { assert.NoError(t, err) assert.Contains(t, resp.Results[0].GetError().Content, "failed fetching config for channel channelWithAccessGranted") - // Scenario IX: Request with a config query + // Scenario XI: Request with a config query mockSup.On("Config", mock.Anything).Return(&discovery.ConfigResult{}, nil).Once() req.Queries[0].Query = &discovery.Query_ConfigQuery{ ConfigQuery: &discovery.ConfigQuery{}, @@ -163,7 +185,7 @@ func TestService(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, resp.Results[0].GetConfigResult()) - // Scenario X: Request with a membership query + // Scenario XII: Request with a membership query // Peers in membership view: { p0, p1, p2, p3} // Peers in channel view: {p1, p2, p4} // So that means that the returned peers for the channel should be the intersection @@ -262,7 +284,7 @@ func TestService(t *testing.T) { assert.NoError(t, err) } - // Scenario XI: The client is eligible for channel queries but not for channel-less + // Scenario XIII: The client is eligible for channel queries but not for channel-less // since it's not an admin. It sends a query for a channel-less query but puts a channel in the query. // It should fail because channel-less query types cannot have a channel configured in them. req.Queries = []*discovery.Query{ @@ -374,6 +396,15 @@ func TestValidateStructure(t *testing.T) { }, "", true, extractHash) } +func TestValidateCCQuery(t *testing.T) { + err := validateCCQuery(&discovery.ChaincodeQuery{ + Interests: []*discovery.ChaincodeInterest{ + nil, + }, + }) + assert.Equal(t, "chaincode interest is nil", err.Error()) +} + func wrapResult(responses ...interface{}) *discovery.Response { response := &discovery.Response{} for _, res := range responses {