From 9a922fd9c78c01d185bc2b45fb98012a37f7bfb9 Mon Sep 17 00:00:00 2001 From: yacovm Date: Mon, 19 Jul 2021 22:30:47 +0300 Subject: [PATCH] [FAB-18527] Discovery supports state based endorsement queries (#2764) This commit adds support for state based endorsement queries to the discovery service. When state based endorsement policies are passed in the chaincode call, discovery now combines them and then requires their combinations to be satisfied as well as the chaincode endorsement policies. Support for chaincode calls with *only* state based endorsement is future work and not implemented yet because of lack of a way to express this intent in the protobuf definition. Change-Id: I005a6abca77061cd531888cdff544691b25d09ec Signed-off-by: Yacov Manevich --- discovery/endorsement/endorsement.go | 60 ++++++- discovery/endorsement/endorsement_test.go | 200 ++++++++++++++++++++++ 2 files changed, 257 insertions(+), 3 deletions(-) diff --git a/discovery/endorsement/endorsement.go b/discovery/endorsement/endorsement.go index 80ed929e6a7..e3149bd0391 100644 --- a/discovery/endorsement/endorsement.go +++ b/discovery/endorsement/endorsement.go @@ -216,21 +216,64 @@ func filterOutUnsatisfiedLayouts(endorsersByGroup map[string]*discovery.Peers, l return filteredLayouts } +func computeStateBasedPrincipalSets(chaincodes []*peer.ChaincodeCall, logger *flogging.FabricLogger) (inquire.ComparablePrincipalSets, error) { + var stateBasedCPS []inquire.ComparablePrincipalSets + for _, chaincode := range chaincodes { + if len(chaincode.KeyPolicies) == 0 { + continue + } + + logger.Debugf("Chaincode call to %s is satisfied by %d state based policies of %v", + chaincode.Name, len(chaincode.KeyPolicies), chaincode.KeyPolicies) + + for _, stateBasedPolicy := range chaincode.KeyPolicies { + var cmpsets inquire.ComparablePrincipalSets + stateBasedPolicy := inquire.NewInquireableSignaturePolicy(stateBasedPolicy) + for _, ps := range stateBasedPolicy.SatisfiedBy() { + cps := inquire.NewComparablePrincipalSet(ps) + if cps == nil { + return nil, errors.New("failed creating a comparable principal set for state based endorsement") + } + cmpsets = append(cmpsets, cps) + } + if len(cmpsets) == 0 { + return nil, errors.New("state based endorsement policy cannot be satisfied") + } + stateBasedCPS = append(stateBasedCPS, cmpsets) + } + } + + if len(stateBasedCPS) > 0 { + stateBasedPrincipalSet, err := mergePrincipalSets(stateBasedCPS) + if err != nil { + return nil, errors.WithStack(err) + } + + logger.Debugf("Merging state based policies: %v --> %v", stateBasedCPS, stateBasedPrincipalSet) + + return stateBasedPrincipalSet, nil + } + + logger.Debugf("No state based policies requested") + + return nil, nil +} + func (ea *endorsementAnalyzer) computePrincipalSets(channelID common.ChannelID, interest *peer.ChaincodeInterest) (policies.PrincipalSets, error) { sessionLogger := logger.With("channel", string(channelID)) - var inquireablePolicies []policies.InquireablePolicy + var inquireablePoliciesForChaincodeAndCollections []policies.InquireablePolicy for _, chaincode := range interest.Chaincodes { policies := ea.PoliciesByChaincode(string(channelID), chaincode.Name, chaincode.CollectionNames...) if len(policies) == 0 { sessionLogger.Debug("Policy for chaincode '", chaincode, "'doesn't exist") return nil, errors.New("policy not found") } - inquireablePolicies = append(inquireablePolicies, policies...) + inquireablePoliciesForChaincodeAndCollections = append(inquireablePoliciesForChaincodeAndCollections, policies...) } var cpss []inquire.ComparablePrincipalSets - for _, policy := range inquireablePolicies { + for _, policy := range inquireablePoliciesForChaincodeAndCollections { var cmpsets inquire.ComparablePrincipalSets for _, ps := range policy.SatisfiedBy() { cps := inquire.NewComparablePrincipalSet(ps) @@ -245,11 +288,22 @@ func (ea *endorsementAnalyzer) computePrincipalSets(channelID common.ChannelID, cpss = append(cpss, cmpsets) } + stateBasedCPS, err := computeStateBasedPrincipalSets(interest.Chaincodes, sessionLogger) + if err != nil { + return nil, errors.WithStack(err) + } + + if len(stateBasedCPS) > 0 { + cpss = append(cpss, stateBasedCPS) + } + cps, err := mergePrincipalSets(cpss) if err != nil { return nil, errors.WithStack(err) } + sessionLogger.Debugf("Merging principal sets: %v --> %v", cpss, cps) + return cps.ToPrincipalSets(), nil } diff --git a/discovery/endorsement/endorsement_test.go b/discovery/endorsement/endorsement_test.go index 805cb2a6027..a9745ad7627 100644 --- a/discovery/endorsement/endorsement_test.go +++ b/discovery/endorsement/endorsement_test.go @@ -10,6 +10,9 @@ import ( "fmt" "testing" + common2 "github.com/hyperledger/fabric-protos-go/common" + "github.com/hyperledger/fabric/common/policydsl" + "github.com/golang/protobuf/proto" discoveryprotos "github.com/hyperledger/fabric-protos-go/discovery" "github.com/hyperledger/fabric-protos-go/gossip" @@ -551,6 +554,203 @@ func TestPeersForEndorsement(t *testing.T) { peerIdentityString("p6"): {}, }, extractPeers(desc)) }) + + t.Run("Chaincode call with state based endorsement policy I", func(t *testing.T) { + // Scenario XIII: A chaincode call with a state based endorsement policy + // Total organizations are 0, 2, 4, 6, 10, 12 + // and the endorsement policies of the chaincode is: + // cc1: OR(AND(0, 2), AND(6, 10)) + // However the chaincode call is accompanied with a hint + // for a state based endorsement policy for organization 10 + // Therefore, the result should be: 6, 10 + + chanPeers := peerSet{} + for _, id := range []int{0, 2, 4, 6, 10, 12} { + peer := newPeer(id).withChaincode("cc1", "1.0") + chanPeers = append(chanPeers, peer) + } + + g.On("PeersOfChannel").Return(chanPeers.toMembers()).Once() + + mf := &metadataFetcher{} + mf.On("Metadata").Return(&chaincode.Metadata{ + Name: "cc1", + Version: "1.0", + }).Once() + + pb := principalBuilder{} + cc1policy := pb.newSet().addPrincipal(peerRole("p0")).addPrincipal(peerRole("p2")). + newSet().addPrincipal(peerRole("p6")).addPrincipal(peerRole("p10")).buildPolicy() + + pf.On("PoliciesByChaincode", "cc1").Return(cc1policy).Once() + + analyzer := NewEndorsementAnalyzer(g, pf, &principalEvaluatorMock{}, mf) + desc, err := analyzer.PeersForEndorsement(channel, &peer.ChaincodeInterest{ + Chaincodes: []*peer.ChaincodeCall{ + { + Name: "cc1", + KeyPolicies: []*common2.SignaturePolicyEnvelope{ + { + Identities: []*msp.MSPPrincipal{peerRole("p10")}, + Rule: policydsl.SignedBy(0), + }, + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, desc) + require.Len(t, desc.Layouts, 1) + require.Len(t, desc.Layouts[0].QuantitiesByGroup, 2) + require.Equal(t, map[string]struct{}{ + peerIdentityString("p6"): {}, + peerIdentityString("p10"): {}, + }, extractPeers(desc)) + }) + + t.Run("Chaincode call with state based endorsement policy II", func(t *testing.T) { + // Scenario XIV: A chaincode call with a state based endorsement policy + // Total organizations are 0, 2, 4, 6, 10, 12 + // and the endorsement policies of the chaincode is: + // cc1: OR(AND(0, 2), AND(6, 10)) + // However the chaincode call is accompanied with a hint + // for a state based endorsement policy for organization 12 + // Therefore, the result should be: {0, 2, 12} or {6, 10, 12} + + chanPeers := peerSet{} + for _, id := range []int{0, 2, 4, 6, 10, 12} { + peer := newPeer(id).withChaincode("cc1", "1.0") + chanPeers = append(chanPeers, peer) + } + + g.On("PeersOfChannel").Return(chanPeers.toMembers()).Once() + + mf := &metadataFetcher{} + mf.On("Metadata").Return(&chaincode.Metadata{ + Name: "cc1", + Version: "1.0", + }).Once() + + pb := principalBuilder{} + cc1policy := pb.newSet().addPrincipal(peerRole("p0")).addPrincipal(peerRole("p2")). + newSet().addPrincipal(peerRole("p6")).addPrincipal(peerRole("p10")).buildPolicy() + + pf.On("PoliciesByChaincode", "cc1").Return(cc1policy).Once() + + analyzer := NewEndorsementAnalyzer(g, pf, &principalEvaluatorMock{}, mf) + desc, err := analyzer.PeersForEndorsement(channel, &peer.ChaincodeInterest{ + Chaincodes: []*peer.ChaincodeCall{ + { + Name: "cc1", + KeyPolicies: []*common2.SignaturePolicyEnvelope{ + { + Identities: []*msp.MSPPrincipal{peerRole("p12")}, + Rule: policydsl.SignedBy(0), + }, + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, desc) + require.Len(t, desc.Layouts, 2) + require.Len(t, desc.Layouts[0].QuantitiesByGroup, 3) + require.Len(t, desc.Layouts[1].QuantitiesByGroup, 3) + require.Equal(t, map[string]struct{}{ + peerIdentityString("p0"): {}, + peerIdentityString("p2"): {}, + peerIdentityString("p6"): {}, + peerIdentityString("p10"): {}, + peerIdentityString("p12"): {}, + }, extractPeers(desc)) + // Find ID of org 12 + + // Ensure org 12 (and no other org) is found in both layouts + var intersectionSize int + for g1 := range desc.Layouts[0].QuantitiesByGroup { + for g2 := range desc.Layouts[1].QuantitiesByGroup { + if g1 == g2 { + require.Equal(t, intersectionSize, 0) + intersectionSize++ + require.Equal(t, peerIdentityString("p12"), string(desc.EndorsersByGroups[g1].Peers[0].Identity)) + } + } + } + }) + + t.Run("Chaincode call with state based endorsement policy III", func(t *testing.T) { + // Scenario XV: A chaincode call with a state based endorsement policy + // Total organizations are 0, 2, 4, 6, 10, 12 + // and the endorsement policies of the chaincode is: + // cc1: OR(AND(0, 2), AND(6, 10)) + // However the chaincode call is accompanied with a hint + // for a state based endorsement policy for both organizations 2 and 6 + // Therefore, the result should be: {0, 2, 6} or {2, 6, 10} + + chanPeers := peerSet{} + for _, id := range []int{0, 2, 4, 6, 10, 12} { + peer := newPeer(id).withChaincode("cc1", "1.0") + chanPeers = append(chanPeers, peer) + } + + g.On("PeersOfChannel").Return(chanPeers.toMembers()).Once() + + mf := &metadataFetcher{} + mf.On("Metadata").Return(&chaincode.Metadata{ + Name: "cc1", + Version: "1.0", + }).Once() + + pb := principalBuilder{} + cc1policy := pb.newSet().addPrincipal(peerRole("p0")).addPrincipal(peerRole("p2")). + newSet().addPrincipal(peerRole("p6")).addPrincipal(peerRole("p10")).buildPolicy() + + pf.On("PoliciesByChaincode", "cc1").Return(cc1policy).Once() + + analyzer := NewEndorsementAnalyzer(g, pf, &principalEvaluatorMock{}, mf) + desc, err := analyzer.PeersForEndorsement(channel, &peer.ChaincodeInterest{ + Chaincodes: []*peer.ChaincodeCall{ + { + Name: "cc1", + KeyPolicies: []*common2.SignaturePolicyEnvelope{ + { + Identities: []*msp.MSPPrincipal{peerRole("p2")}, + Rule: policydsl.SignedBy(0), + }, + { + Identities: []*msp.MSPPrincipal{peerRole("p6")}, + Rule: policydsl.SignedBy(0), + }, + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, desc) + require.Len(t, desc.Layouts, 2) + require.Len(t, desc.Layouts[0].QuantitiesByGroup, 3) + require.Equal(t, map[string]struct{}{ + peerIdentityString("p0"): {}, + peerIdentityString("p2"): {}, + peerIdentityString("p6"): {}, + peerIdentityString("p10"): {}, + }, extractPeers(desc)) + + // Ensure orgs 2, 6 are found in both layouts + intersection := make(map[string]struct{}) + for g1 := range desc.Layouts[0].QuantitiesByGroup { + for g2 := range desc.Layouts[1].QuantitiesByGroup { + if g1 == g2 { + intersection[string(desc.EndorsersByGroups[g1].Peers[0].Identity)] = struct{}{} + } + } + } + + require.Equal(t, map[string]struct{}{ + peerIdentityString("p2"): {}, + peerIdentityString("p6"): {}, + }, intersection) + }) } func TestPeersAuthorizedByCriteria(t *testing.T) {