1127 lines
34 KiB
Go
1127 lines
34 KiB
Go
/*
|
|
Copyright IBM Corp. All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/hyperledger/fabric-protos-go/common"
|
|
"github.com/hyperledger/fabric-protos-go/discovery"
|
|
"github.com/hyperledger/fabric-protos-go/gossip"
|
|
"github.com/hyperledger/fabric-protos-go/msp"
|
|
"github.com/hyperledger/fabric-protos-go/peer"
|
|
"github.com/hyperledger/fabric/common/chaincode"
|
|
"github.com/hyperledger/fabric/common/policies"
|
|
"github.com/hyperledger/fabric/common/policydsl"
|
|
"github.com/hyperledger/fabric/common/util"
|
|
fabricdisc "github.com/hyperledger/fabric/discovery"
|
|
"github.com/hyperledger/fabric/discovery/endorsement"
|
|
"github.com/hyperledger/fabric/gossip/api"
|
|
gossipcommon "github.com/hyperledger/fabric/gossip/common"
|
|
gdisc "github.com/hyperledger/fabric/gossip/discovery"
|
|
"github.com/hyperledger/fabric/gossip/protoext"
|
|
"github.com/hyperledger/fabric/internal/pkg/comm"
|
|
"github.com/hyperledger/fabric/protoutil"
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
)
|
|
|
|
const (
|
|
signerCacheSize uint = 1
|
|
)
|
|
|
|
var (
|
|
ctx = context.Background()
|
|
|
|
orgCombinationsThatSatisfyPolicy = [][]string{
|
|
{"A", "B"}, {"C"}, {"A", "D"},
|
|
}
|
|
|
|
orgCombinationsThatSatisfyPolicy2 = [][]string{
|
|
{"B", "D"},
|
|
}
|
|
|
|
expectedOrgCombinations = []map[string]struct{}{
|
|
{
|
|
"A": {},
|
|
"B": {},
|
|
},
|
|
{
|
|
"C": {},
|
|
},
|
|
{
|
|
"A": {},
|
|
"D": {},
|
|
},
|
|
}
|
|
|
|
expectedOrgCombinations2 = []map[string]struct{}{
|
|
{
|
|
"B": {},
|
|
"C": {},
|
|
"D": {},
|
|
},
|
|
}
|
|
|
|
cc = &gossip.Chaincode{
|
|
Name: "mycc",
|
|
Version: "1.0",
|
|
}
|
|
|
|
cc2 = &gossip.Chaincode{
|
|
Name: "mycc2",
|
|
Version: "1.0",
|
|
}
|
|
|
|
cc3 = &gossip.Chaincode{
|
|
Name: "mycc3",
|
|
Version: "1.0",
|
|
}
|
|
|
|
propertiesWithChaincodes = &gossip.Properties{
|
|
Chaincodes: []*gossip.Chaincode{cc, cc2, cc3},
|
|
}
|
|
|
|
expectedConf = &discovery.ConfigResult{
|
|
Msps: map[string]*msp.FabricMSPConfig{
|
|
"A": {},
|
|
"B": {},
|
|
"C": {},
|
|
"D": {},
|
|
},
|
|
Orderers: map[string]*discovery.Endpoints{
|
|
"A": {},
|
|
"B": {},
|
|
},
|
|
}
|
|
|
|
channelPeersWithChaincodes = gdisc.Members{
|
|
newPeer(0, stateInfoMessage(cc, cc2), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(1, stateInfoMessage(cc, cc2), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(2, stateInfoMessage(cc, cc2), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(3, stateInfoMessage(cc, cc2), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(4, stateInfoMessage(cc, cc2), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(5, stateInfoMessage(cc, cc2), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(6, stateInfoMessage(cc, cc2), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(7, stateInfoMessage(cc, cc2), propertiesWithChaincodes).NetworkMember,
|
|
}
|
|
|
|
channelPeersWithoutChaincodes = gdisc.Members{
|
|
newPeer(0, stateInfoMessage(), nil).NetworkMember,
|
|
newPeer(1, stateInfoMessage(), nil).NetworkMember,
|
|
newPeer(2, stateInfoMessage(), nil).NetworkMember,
|
|
newPeer(3, stateInfoMessage(), nil).NetworkMember,
|
|
newPeer(4, stateInfoMessage(), nil).NetworkMember,
|
|
newPeer(5, stateInfoMessage(), nil).NetworkMember,
|
|
newPeer(6, stateInfoMessage(), nil).NetworkMember,
|
|
newPeer(7, stateInfoMessage(), nil).NetworkMember,
|
|
}
|
|
|
|
channelPeersWithDifferentLedgerHeights = gdisc.Members{
|
|
newPeer(0, stateInfoMessageWithHeight(100, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(1, stateInfoMessageWithHeight(106, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(2, stateInfoMessageWithHeight(107, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(3, stateInfoMessageWithHeight(108, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(4, stateInfoMessageWithHeight(101, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(5, stateInfoMessageWithHeight(108, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(6, stateInfoMessageWithHeight(110, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(7, stateInfoMessageWithHeight(110, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(8, stateInfoMessageWithHeight(100, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(9, stateInfoMessageWithHeight(107, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(10, stateInfoMessageWithHeight(110, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(11, stateInfoMessageWithHeight(111, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(12, stateInfoMessageWithHeight(105, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(13, stateInfoMessageWithHeight(103, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(14, stateInfoMessageWithHeight(109, cc3), propertiesWithChaincodes).NetworkMember,
|
|
newPeer(15, stateInfoMessageWithHeight(111, cc3), propertiesWithChaincodes).NetworkMember,
|
|
}
|
|
|
|
membershipPeers = gdisc.Members{
|
|
newPeer(0, aliveMessage(0), nil).NetworkMember,
|
|
newPeer(1, aliveMessage(1), nil).NetworkMember,
|
|
newPeer(2, aliveMessage(2), nil).NetworkMember,
|
|
newPeer(3, aliveMessage(3), nil).NetworkMember,
|
|
newPeer(4, aliveMessage(4), nil).NetworkMember,
|
|
newPeer(5, aliveMessage(5), nil).NetworkMember,
|
|
newPeer(6, aliveMessage(6), nil).NetworkMember,
|
|
newPeer(7, aliveMessage(7), nil).NetworkMember,
|
|
newPeer(8, aliveMessage(8), nil).NetworkMember,
|
|
newPeer(9, aliveMessage(9), nil).NetworkMember,
|
|
newPeer(10, aliveMessage(10), nil).NetworkMember,
|
|
newPeer(11, aliveMessage(11), nil).NetworkMember,
|
|
newPeer(12, aliveMessage(12), nil).NetworkMember,
|
|
newPeer(13, aliveMessage(13), nil).NetworkMember,
|
|
newPeer(14, aliveMessage(14), nil).NetworkMember,
|
|
newPeer(15, aliveMessage(15), nil).NetworkMember,
|
|
}
|
|
|
|
peerIdentities = api.PeerIdentitySet{
|
|
peerIdentity("A", 0),
|
|
peerIdentity("A", 1),
|
|
peerIdentity("B", 2),
|
|
peerIdentity("B", 3),
|
|
peerIdentity("C", 4),
|
|
peerIdentity("C", 5),
|
|
peerIdentity("D", 6),
|
|
peerIdentity("D", 7),
|
|
peerIdentity("A", 8),
|
|
peerIdentity("A", 9),
|
|
peerIdentity("B", 10),
|
|
peerIdentity("B", 11),
|
|
peerIdentity("C", 12),
|
|
peerIdentity("C", 13),
|
|
peerIdentity("D", 14),
|
|
peerIdentity("D", 15),
|
|
}
|
|
|
|
resultsWithoutEnvelopes = &discovery.QueryResult_CcQueryRes{
|
|
CcQueryRes: &discovery.ChaincodeQueryResult{
|
|
Content: []*discovery.EndorsementDescriptor{
|
|
{
|
|
Chaincode: "mycc",
|
|
EndorsersByGroups: map[string]*discovery.Peers{
|
|
"A": {
|
|
Peers: []*discovery.Peer{
|
|
{},
|
|
},
|
|
},
|
|
},
|
|
Layouts: []*discovery.Layout{
|
|
{
|
|
QuantitiesByGroup: map[string]uint32{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
resultsWithEnvelopesButWithInsufficientPeers = &discovery.QueryResult_CcQueryRes{
|
|
CcQueryRes: &discovery.ChaincodeQueryResult{
|
|
Content: []*discovery.EndorsementDescriptor{
|
|
{
|
|
Chaincode: "mycc",
|
|
EndorsersByGroups: map[string]*discovery.Peers{
|
|
"A": {
|
|
Peers: []*discovery.Peer{
|
|
{
|
|
StateInfo: stateInfoMessage(),
|
|
MembershipInfo: aliveMessage(0),
|
|
Identity: peerIdentity("A", 0).Identity,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Layouts: []*discovery.Layout{
|
|
{
|
|
QuantitiesByGroup: map[string]uint32{
|
|
"A": 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
resultsWithEnvelopesButWithMismatchedLayout = &discovery.QueryResult_CcQueryRes{
|
|
CcQueryRes: &discovery.ChaincodeQueryResult{
|
|
Content: []*discovery.EndorsementDescriptor{
|
|
{
|
|
Chaincode: "mycc",
|
|
EndorsersByGroups: map[string]*discovery.Peers{
|
|
"A": {
|
|
Peers: []*discovery.Peer{
|
|
{
|
|
StateInfo: stateInfoMessage(),
|
|
MembershipInfo: aliveMessage(0),
|
|
Identity: peerIdentity("A", 0).Identity,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Layouts: []*discovery.Layout{
|
|
{
|
|
QuantitiesByGroup: map[string]uint32{
|
|
"B": 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
func loadFileOrPanic(file string) []byte {
|
|
b, err := ioutil.ReadFile(file)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func createGRPCServer(t *testing.T) *comm.GRPCServer {
|
|
serverCert := loadFileOrPanic(filepath.Join("testdata", "server", "cert.pem"))
|
|
serverKey := loadFileOrPanic(filepath.Join("testdata", "server", "key.pem"))
|
|
s, err := comm.NewGRPCServer("localhost:0", comm.ServerConfig{
|
|
SecOpts: comm.SecureOptions{
|
|
UseTLS: true,
|
|
Certificate: serverCert,
|
|
Key: serverKey,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
return s
|
|
}
|
|
|
|
func createConnector(t *testing.T, certificate tls.Certificate, targetPort int) func() (*grpc.ClientConn, error) {
|
|
caCert := loadFileOrPanic(filepath.Join("testdata", "server", "ca.pem"))
|
|
tlsConf := &tls.Config{
|
|
RootCAs: x509.NewCertPool(),
|
|
Certificates: []tls.Certificate{certificate},
|
|
}
|
|
tlsConf.RootCAs.AppendCertsFromPEM(caCert)
|
|
|
|
addr := fmt.Sprintf("localhost:%d", targetPort)
|
|
return func() (*grpc.ClientConn, error) {
|
|
conn, err := grpc.Dial(addr, grpc.WithBlock(), grpc.WithTransportCredentials(credentials.NewTLS(tlsConf)))
|
|
require.NoError(t, err)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return conn, nil
|
|
}
|
|
}
|
|
|
|
func createDiscoveryService(sup *mockSupport) discovery.DiscoveryServer {
|
|
conf := fabricdisc.Config{TLS: true}
|
|
mdf := &ccMetadataFetcher{}
|
|
pe := &principalEvaluator{}
|
|
pf := &policyFetcher{}
|
|
|
|
sigPol, _ := policydsl.FromString("OR(AND('A.member', 'B.member'), 'C.member', AND('A.member', 'D.member'))")
|
|
polBytes, _ := proto.Marshal(sigPol)
|
|
mdf.On("Metadata", "mycc").Return(&chaincode.Metadata{
|
|
Policy: polBytes,
|
|
Name: "mycc",
|
|
Version: "1.0",
|
|
Id: []byte{1, 2, 3},
|
|
})
|
|
|
|
pf.On("PoliciesByChaincode", "mycc").Return(&inquireablePolicy{
|
|
orgCombinations: orgCombinationsThatSatisfyPolicy,
|
|
})
|
|
|
|
sigPol, _ = policydsl.FromString("AND('B.member', 'C.member')")
|
|
polBytes, _ = proto.Marshal(sigPol)
|
|
mdf.On("Metadata", "mycc2").Return(&chaincode.Metadata{
|
|
Policy: polBytes,
|
|
Name: "mycc2",
|
|
Version: "1.0",
|
|
Id: []byte{1, 2, 3},
|
|
CollectionsConfig: buildCollectionConfig(map[string][]*msp.MSPPrincipal{
|
|
"col": {memberPrincipal("B"), memberPrincipal("C"), memberPrincipal("D")},
|
|
}),
|
|
})
|
|
|
|
pf.On("PoliciesByChaincode", "mycc2").Return(&inquireablePolicy{
|
|
orgCombinations: orgCombinationsThatSatisfyPolicy2,
|
|
})
|
|
|
|
sigPol, _ = policydsl.FromString("AND('A.member', 'B.member', 'C.member', 'D.member')")
|
|
polBytes, _ = proto.Marshal(sigPol)
|
|
mdf.On("Metadata", "mycc3").Return(&chaincode.Metadata{
|
|
Policy: polBytes,
|
|
Name: "mycc3",
|
|
Version: "1.0",
|
|
Id: []byte{1, 2, 3},
|
|
})
|
|
|
|
pf.On("PoliciesByChaincode", "mycc3").Return(&inquireablePolicy{
|
|
orgCombinations: [][]string{{"A", "B", "C", "D"}},
|
|
})
|
|
|
|
sup.On("Config", "mychannel").Return(expectedConf)
|
|
sup.On("Peers").Return(membershipPeers)
|
|
sup.endorsementAnalyzer = endorsement.NewEndorsementAnalyzer(sup, pf, pe, mdf)
|
|
sup.On("IdentityInfo").Return(peerIdentities)
|
|
return fabricdisc.NewService(conf, sup)
|
|
}
|
|
|
|
func TestClient(t *testing.T) {
|
|
clientCert := loadFileOrPanic(filepath.Join("testdata", "client", "cert.pem"))
|
|
clientKey := loadFileOrPanic(filepath.Join("testdata", "client", "key.pem"))
|
|
clientTLSCert, err := tls.X509KeyPair(clientCert, clientKey)
|
|
require.NoError(t, err)
|
|
server := createGRPCServer(t)
|
|
sup := &mockSupport{}
|
|
service := createDiscoveryService(sup)
|
|
discovery.RegisterDiscoveryServer(server.Server(), service)
|
|
go server.Start()
|
|
|
|
_, portStr, _ := net.SplitHostPort(server.Address())
|
|
port, _ := strconv.ParseInt(portStr, 10, 64)
|
|
connect := createConnector(t, clientTLSCert, int(port))
|
|
|
|
signer := func(msg []byte) ([]byte, error) {
|
|
return msg, nil
|
|
}
|
|
authInfo := &discovery.AuthInfo{
|
|
ClientIdentity: []byte{1, 2, 3},
|
|
ClientTlsCertHash: util.ComputeSHA256(clientTLSCert.Certificate[0]),
|
|
}
|
|
cl := NewClient(connect, signer, signerCacheSize)
|
|
|
|
sup.On("PeersOfChannel").Return(channelPeersWithoutChaincodes).Times(2)
|
|
req := NewRequest()
|
|
req.OfChannel("mychannel").AddPeersQuery().AddConfigQuery().AddLocalPeersQuery().AddEndorsersQuery(interest("mycc"))
|
|
r, err := cl.Send(ctx, req, authInfo)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Channel mismatch", func(t *testing.T) {
|
|
// Check behavior for channels that we didn't query for.
|
|
fakeChannel := r.ForChannel("fakeChannel")
|
|
peers, err := fakeChannel.Peers()
|
|
require.Equal(t, ErrNotFound, err)
|
|
require.Nil(t, peers)
|
|
|
|
endorsers, err := fakeChannel.Endorsers(ccCall("mycc"), NoFilter)
|
|
require.Equal(t, ErrNotFound, err)
|
|
require.Nil(t, endorsers)
|
|
|
|
conf, err := fakeChannel.Config()
|
|
require.Equal(t, ErrNotFound, err)
|
|
require.Nil(t, conf)
|
|
})
|
|
|
|
t.Run("Peer membership query", func(t *testing.T) {
|
|
// Check response for the correct channel
|
|
mychannel := r.ForChannel("mychannel")
|
|
conf, err := mychannel.Config()
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedConf.Msps, conf.Msps)
|
|
require.Equal(t, expectedConf.Orderers, conf.Orderers)
|
|
peers, err := mychannel.Peers()
|
|
require.NoError(t, err)
|
|
// We should see all peers as provided above
|
|
require.Len(t, peers, 8)
|
|
// Check response for peers when doing a local query
|
|
peers, err = r.ForLocal().Peers()
|
|
require.NoError(t, err)
|
|
require.Len(t, peers, len(peerIdentities))
|
|
})
|
|
|
|
t.Run("Endorser query without chaincode installed", func(t *testing.T) {
|
|
mychannel := r.ForChannel("mychannel")
|
|
endorsers, err := mychannel.Endorsers(ccCall("mycc"), NoFilter)
|
|
// However, since we didn't provide any chaincodes to these peers - the server shouldn't
|
|
// be able to construct the descriptor.
|
|
// Just check that the appropriate error is returned, and nothing crashes.
|
|
require.Contains(t, err.Error(), "failed constructing descriptor for chaincode")
|
|
require.Nil(t, endorsers)
|
|
})
|
|
|
|
t.Run("Endorser query with chaincodes installed", func(t *testing.T) {
|
|
// Next, we check the case when the peers publish chaincode for themselves.
|
|
sup.On("PeersOfChannel").Return(channelPeersWithChaincodes).Times(2)
|
|
req = NewRequest()
|
|
req.OfChannel("mychannel").AddPeersQuery().AddEndorsersQuery(interest("mycc"))
|
|
r, err = cl.Send(ctx, req, authInfo)
|
|
require.NoError(t, err)
|
|
|
|
mychannel := r.ForChannel("mychannel")
|
|
peers, err := mychannel.Peers()
|
|
require.NoError(t, err)
|
|
require.Len(t, peers, 8)
|
|
|
|
// We should get a valid endorsement descriptor from the service
|
|
endorsers, err := mychannel.Endorsers(ccCall("mycc"), NoFilter)
|
|
require.NoError(t, err)
|
|
// The combinations of endorsers should be in the expected combinations
|
|
require.Contains(t, expectedOrgCombinations, getMSPs(endorsers))
|
|
})
|
|
|
|
t.Run("Endorser query with cc2cc and collections", func(t *testing.T) {
|
|
sup.On("PeersOfChannel").Return(channelPeersWithChaincodes).Twice()
|
|
req = NewRequest()
|
|
myccOnly := ccCall("mycc")
|
|
myccAndmycc2 := ccCall("mycc", "mycc2")
|
|
myccAndmycc2[1].CollectionNames = append(myccAndmycc2[1].CollectionNames, "col")
|
|
req.OfChannel("mychannel").AddEndorsersQuery(cc2ccInterests(myccAndmycc2, myccOnly)...)
|
|
r, err = cl.Send(ctx, req, authInfo)
|
|
require.NoError(t, err)
|
|
mychannel := r.ForChannel("mychannel")
|
|
|
|
// Check the endorsers for the non cc2cc call
|
|
endorsers, err := mychannel.Endorsers(ccCall("mycc"), NoFilter)
|
|
require.NoError(t, err)
|
|
require.Contains(t, expectedOrgCombinations, getMSPs(endorsers))
|
|
// Check the endorsers for the cc2cc call with collections
|
|
call := ccCall("mycc", "mycc2")
|
|
call[1].CollectionNames = append(call[1].CollectionNames, "col")
|
|
endorsers, err = mychannel.Endorsers(call, NoFilter)
|
|
require.NoError(t, err)
|
|
require.Contains(t, expectedOrgCombinations2, getMSPs(endorsers))
|
|
})
|
|
|
|
t.Run("Peer membership query with collections and chaincodes", func(t *testing.T) {
|
|
sup.On("PeersOfChannel").Return(channelPeersWithChaincodes).Once()
|
|
interest := ccCall("mycc2")
|
|
interest[0].CollectionNames = append(interest[0].CollectionNames, "col")
|
|
req = NewRequest().OfChannel("mychannel").AddPeersQuery(interest...)
|
|
r, err = cl.Send(ctx, req, authInfo)
|
|
require.NoError(t, err)
|
|
mychannel := r.ForChannel("mychannel")
|
|
peers, err := mychannel.Peers(interest...)
|
|
require.NoError(t, err)
|
|
// We should see all peers that aren't in ORG A since it's not part of the collection
|
|
for _, p := range peers {
|
|
require.NotEqual(t, "A", p.MSPID)
|
|
}
|
|
require.Len(t, peers, 6)
|
|
})
|
|
|
|
t.Run("Endorser query with PrioritiesByHeight selector", func(t *testing.T) {
|
|
sup.On("PeersOfChannel").Return(channelPeersWithDifferentLedgerHeights).Twice()
|
|
req = NewRequest()
|
|
req.OfChannel("mychannel").AddEndorsersQuery(interest("mycc3"))
|
|
r, err = cl.Send(ctx, req, authInfo)
|
|
require.NoError(t, err)
|
|
mychannel := r.ForChannel("mychannel")
|
|
|
|
// acceptablePeers are the ones at the highest ledger height for each org
|
|
acceptablePeers := []string{"p5", "p9", "p11", "p15"}
|
|
used := make(map[string]struct{})
|
|
endorsers, err := mychannel.Endorsers(ccCall("mycc3"), NewFilter(PrioritiesByHeight, NoExclusion))
|
|
require.NoError(t, err)
|
|
names := getNames(endorsers)
|
|
require.Subset(t, acceptablePeers, names)
|
|
for _, name := range names {
|
|
used[name] = struct{}{}
|
|
}
|
|
require.Equalf(t, len(acceptablePeers), len(used), "expecting each endorser to be returned at least once")
|
|
})
|
|
|
|
t.Run("Endorser query with custom filter", func(t *testing.T) {
|
|
sup.On("PeersOfChannel").Return(channelPeersWithDifferentLedgerHeights).Twice()
|
|
req = NewRequest()
|
|
req.OfChannel("mychannel").AddEndorsersQuery(interest("mycc3"))
|
|
r, err = cl.Send(ctx, req, authInfo)
|
|
require.NoError(t, err)
|
|
mychannel := r.ForChannel("mychannel")
|
|
|
|
threshold := uint64(3) // Use peers within 3 of the max height of the org peers
|
|
acceptablePeers := []string{"p1", "p9", "p3", "p5", "p6", "p7", "p10", "p11", "p12", "p14", "p15"}
|
|
used := make(map[string]struct{})
|
|
|
|
for i := 0; i < 90; i++ {
|
|
endorsers, err := mychannel.Endorsers(ccCall("mycc3"), &ledgerHeightFilter{threshold: threshold})
|
|
require.NoError(t, err)
|
|
names := getNames(endorsers)
|
|
require.Subset(t, acceptablePeers, names)
|
|
for _, name := range names {
|
|
used[name] = struct{}{}
|
|
}
|
|
}
|
|
require.Equalf(t, len(acceptablePeers), len(used), "expecting each endorser to be returned at least once")
|
|
|
|
threshold = 0 // only use the peers at the highest ledger height (same as using the PrioritiesByHeight selector)
|
|
acceptablePeers = []string{"p5", "p9", "p11", "p15"}
|
|
used = make(map[string]struct{})
|
|
endorsers, err := mychannel.Endorsers(ccCall("mycc3"), &ledgerHeightFilter{threshold: threshold})
|
|
require.NoError(t, err)
|
|
names := getNames(endorsers)
|
|
require.Subset(t, acceptablePeers, names)
|
|
for _, name := range names {
|
|
used[name] = struct{}{}
|
|
}
|
|
t.Logf("Used peers: %#v\n", used)
|
|
require.Equalf(t, len(acceptablePeers), len(used), "expecting each endorser to be returned at least once")
|
|
})
|
|
}
|
|
|
|
func TestUnableToSign(t *testing.T) {
|
|
signer := func(msg []byte) ([]byte, error) {
|
|
return nil, errors.New("not enough entropy")
|
|
}
|
|
failToConnect := func() (*grpc.ClientConn, error) {
|
|
return nil, nil
|
|
}
|
|
authInfo := &discovery.AuthInfo{
|
|
ClientIdentity: []byte{1, 2, 3},
|
|
}
|
|
cl := NewClient(failToConnect, signer, signerCacheSize)
|
|
req := NewRequest()
|
|
req = req.OfChannel("mychannel")
|
|
resp, err := cl.Send(ctx, req, authInfo)
|
|
require.Nil(t, resp)
|
|
require.Contains(t, err.Error(), "not enough entropy")
|
|
}
|
|
|
|
func TestUnableToConnect(t *testing.T) {
|
|
signer := func(msg []byte) ([]byte, error) {
|
|
return msg, nil
|
|
}
|
|
failToConnect := func() (*grpc.ClientConn, error) {
|
|
return nil, errors.New("unable to connect")
|
|
}
|
|
auth := &discovery.AuthInfo{
|
|
ClientIdentity: []byte{1, 2, 3},
|
|
}
|
|
cl := NewClient(failToConnect, signer, signerCacheSize)
|
|
req := NewRequest()
|
|
req = req.OfChannel("mychannel")
|
|
resp, err := cl.Send(ctx, req, auth)
|
|
require.Nil(t, resp)
|
|
require.Contains(t, err.Error(), "unable to connect")
|
|
}
|
|
|
|
func TestBadResponses(t *testing.T) {
|
|
signer := func(msg []byte) ([]byte, error) {
|
|
return msg, nil
|
|
}
|
|
svc := newMockDiscoveryService()
|
|
t.Logf("Started mock discovery service on port %d", svc.port)
|
|
defer svc.shutdown()
|
|
|
|
connect := func() (*grpc.ClientConn, error) {
|
|
return grpc.Dial(fmt.Sprintf("localhost:%d", svc.port), grpc.WithInsecure())
|
|
}
|
|
|
|
auth := &discovery.AuthInfo{
|
|
ClientIdentity: []byte{1, 2, 3},
|
|
}
|
|
cl := NewClient(connect, signer, signerCacheSize)
|
|
|
|
// Scenario I: discovery service sends back an error
|
|
svc.On("Discover").Return(nil, errors.New("foo")).Once()
|
|
req := NewRequest()
|
|
req.OfChannel("mychannel").AddPeersQuery().AddConfigQuery().AddEndorsersQuery(interest("mycc"))
|
|
r, err := cl.Send(ctx, req, auth)
|
|
require.Contains(t, err.Error(), "foo")
|
|
require.Nil(t, r)
|
|
|
|
// Scenario II: discovery service sends back an empty response
|
|
svc.On("Discover").Return(&discovery.Response{}, nil).Once()
|
|
req = NewRequest()
|
|
req.OfChannel("mychannel").AddPeersQuery().AddConfigQuery().AddEndorsersQuery(interest("mycc"))
|
|
r, err = cl.Send(ctx, req, auth)
|
|
require.Equal(t, "Sent 3 queries but received 0 responses back", err.Error())
|
|
require.Nil(t, r)
|
|
|
|
// Scenario III: discovery service sends back a layout for the wrong chaincode
|
|
svc.On("Discover").Return(&discovery.Response{
|
|
Results: []*discovery.QueryResult{
|
|
{
|
|
Result: &discovery.QueryResult_CcQueryRes{
|
|
CcQueryRes: &discovery.ChaincodeQueryResult{
|
|
Content: []*discovery.EndorsementDescriptor{
|
|
{
|
|
Chaincode: "notmycc",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, nil).Once()
|
|
req = NewRequest()
|
|
req.OfChannel("mychannel").AddEndorsersQuery(interest("mycc"))
|
|
r, err = cl.Send(ctx, req, auth)
|
|
require.Nil(t, r)
|
|
require.Contains(t, err.Error(), "expected chaincode mycc but got endorsement descriptor for notmycc")
|
|
|
|
// Scenario IV: discovery service sends back a layout that has empty envelopes
|
|
svc.On("Discover").Return(&discovery.Response{
|
|
Results: []*discovery.QueryResult{
|
|
{
|
|
Result: resultsWithoutEnvelopes,
|
|
},
|
|
},
|
|
}, nil).Once()
|
|
req = NewRequest()
|
|
req.OfChannel("mychannel").AddEndorsersQuery(interest("mycc"))
|
|
r, err = cl.Send(ctx, req, auth)
|
|
require.Contains(t, err.Error(), "received empty envelope(s) for endorsers for chaincode mycc")
|
|
require.Nil(t, r)
|
|
|
|
// Scenario V: discovery service sends back a layout that has a group that requires more
|
|
// members than are present.
|
|
svc.On("Discover").Return(&discovery.Response{
|
|
Results: []*discovery.QueryResult{
|
|
{
|
|
Result: resultsWithEnvelopesButWithInsufficientPeers,
|
|
},
|
|
},
|
|
}, nil).Once()
|
|
req = NewRequest()
|
|
req.OfChannel("mychannel").AddEndorsersQuery(interest("mycc"))
|
|
r, err = cl.Send(ctx, req, auth)
|
|
require.NoError(t, err)
|
|
mychannel := r.ForChannel("mychannel")
|
|
endorsers, err := mychannel.Endorsers(ccCall("mycc"), NoFilter)
|
|
require.Nil(t, endorsers)
|
|
require.Contains(t, err.Error(), "no endorsement combination can be satisfied")
|
|
|
|
// Scenario VI: discovery service sends back a layout that has a group that doesn't have a matching peer set
|
|
svc.On("Discover").Return(&discovery.Response{
|
|
Results: []*discovery.QueryResult{
|
|
{
|
|
Result: resultsWithEnvelopesButWithMismatchedLayout,
|
|
},
|
|
},
|
|
}, nil).Once()
|
|
req = NewRequest()
|
|
req.OfChannel("mychannel").AddEndorsersQuery(interest("mycc"))
|
|
r, err = cl.Send(ctx, req, auth)
|
|
require.Contains(t, err.Error(), "group B isn't mapped to endorsers, but exists in a layout")
|
|
require.Empty(t, r)
|
|
}
|
|
|
|
func TestAddEndorsersQueryInvalidInput(t *testing.T) {
|
|
_, err := NewRequest().AddEndorsersQuery()
|
|
require.Contains(t, err.Error(), "no chaincode interests given")
|
|
|
|
_, err = NewRequest().AddEndorsersQuery(nil)
|
|
require.Contains(t, err.Error(), "chaincode interest is nil")
|
|
|
|
_, err = NewRequest().AddEndorsersQuery(&peer.ChaincodeInterest{})
|
|
require.Contains(t, err.Error(), "invocation chain should not be empty")
|
|
|
|
_, err = NewRequest().AddEndorsersQuery(&peer.ChaincodeInterest{
|
|
Chaincodes: []*peer.ChaincodeCall{{}},
|
|
})
|
|
require.Contains(t, err.Error(), "chaincode name should not be empty")
|
|
}
|
|
|
|
func TestValidateAliveMessage(t *testing.T) {
|
|
am := aliveMessage(1)
|
|
msg, _ := protoext.EnvelopeToGossipMessage(am)
|
|
|
|
// Scenario I: Valid alive message
|
|
require.NoError(t, validateAliveMessage(msg))
|
|
|
|
// Scenario II: Nullify timestamp
|
|
msg.GetAliveMsg().Timestamp = nil
|
|
err := validateAliveMessage(msg)
|
|
require.Equal(t, "timestamp is nil", err.Error())
|
|
|
|
// Scenario III: Nullify membership
|
|
msg.GetAliveMsg().Membership = nil
|
|
err = validateAliveMessage(msg)
|
|
require.Equal(t, "membership is empty", err.Error())
|
|
|
|
// Scenario IV: Nullify the entire alive message part
|
|
msg.Content = nil
|
|
err = validateAliveMessage(msg)
|
|
require.Equal(t, "message isn't an alive message", err.Error())
|
|
}
|
|
|
|
func TestValidateStateInfoMessage(t *testing.T) {
|
|
si := stateInfoWithHeight(100)
|
|
|
|
// Scenario I: Valid state info message
|
|
require.NoError(t, validateStateInfoMessage(si))
|
|
|
|
// Scenario II: Nullify properties
|
|
si.GetStateInfo().Properties = nil
|
|
err := validateStateInfoMessage(si)
|
|
require.Equal(t, "properties is nil", err.Error())
|
|
|
|
// Scenario III: Nullify timestamp
|
|
si.GetStateInfo().Timestamp = nil
|
|
err = validateStateInfoMessage(si)
|
|
require.Equal(t, "timestamp is nil", err.Error())
|
|
|
|
// Scenario IV: Nullify the state info message part
|
|
si.Content = nil
|
|
err = validateStateInfoMessage(si)
|
|
require.Equal(t, "message isn't a stateInfo message", err.Error())
|
|
}
|
|
|
|
func TestString(t *testing.T) {
|
|
var ic InvocationChain
|
|
ic = append(ic, &peer.ChaincodeCall{
|
|
Name: "foo",
|
|
CollectionNames: []string{"c1", "c2"},
|
|
})
|
|
ic = append(ic, &peer.ChaincodeCall{
|
|
Name: "bar",
|
|
CollectionNames: []string{"c3", "c4"},
|
|
})
|
|
expected := `[{"name":"foo","collection_names":["c1","c2"]},{"name":"bar","collection_names":["c3","c4"]}]`
|
|
require.Equal(t, expected, ic.String())
|
|
}
|
|
|
|
func getMSP(peer *Peer) string {
|
|
endpoint := peer.AliveMessage.GetAliveMsg().Membership.Endpoint
|
|
id, _ := strconv.ParseInt(endpoint[1:], 10, 64)
|
|
switch id / 2 {
|
|
case 0, 4:
|
|
return "A"
|
|
case 1, 5:
|
|
return "B"
|
|
case 2, 6:
|
|
return "C"
|
|
default:
|
|
return "D"
|
|
}
|
|
}
|
|
|
|
func getMSPs(endorsers []*Peer) map[string]struct{} {
|
|
m := make(map[string]struct{})
|
|
for _, endorser := range endorsers {
|
|
m[getMSP(endorser)] = struct{}{}
|
|
}
|
|
return m
|
|
}
|
|
|
|
type ccMetadataFetcher struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (mdf *ccMetadataFetcher) Metadata(channel string, cc string, _ ...string) *chaincode.Metadata {
|
|
return mdf.Called(cc).Get(0).(*chaincode.Metadata)
|
|
}
|
|
|
|
type principalEvaluator struct{}
|
|
|
|
func (pe *principalEvaluator) SatisfiesPrincipal(channel string, identity []byte, principal *msp.MSPPrincipal) error {
|
|
sID := &msp.SerializedIdentity{}
|
|
proto.Unmarshal(identity, sID)
|
|
p := &msp.MSPRole{}
|
|
proto.Unmarshal(principal.Principal, p)
|
|
if sID.Mspid == p.MspIdentifier {
|
|
return nil
|
|
}
|
|
return errors.Errorf("peer %s has MSP %s but should have MSP %s", string(sID.IdBytes), sID.Mspid, p.MspIdentifier)
|
|
}
|
|
|
|
type policyFetcher struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (pf *policyFetcher) PoliciesByChaincode(channel string, cc string, collections ...string) []policies.InquireablePolicy {
|
|
return []policies.InquireablePolicy{pf.Called(cc).Get(0).(policies.InquireablePolicy)}
|
|
}
|
|
|
|
type endorsementAnalyzer interface {
|
|
PeersForEndorsement(chainID gossipcommon.ChannelID, interest *peer.ChaincodeInterest) (*discovery.EndorsementDescriptor, error)
|
|
|
|
PeersAuthorizedByCriteria(chainID gossipcommon.ChannelID, interest *peer.ChaincodeInterest) (gdisc.Members, error)
|
|
}
|
|
|
|
type inquireablePolicy struct {
|
|
principals []*msp.MSPPrincipal
|
|
orgCombinations [][]string
|
|
}
|
|
|
|
func (ip *inquireablePolicy) appendPrincipal(orgName string) {
|
|
ip.principals = append(ip.principals, &msp.MSPPrincipal{
|
|
PrincipalClassification: msp.MSPPrincipal_ROLE,
|
|
Principal: protoutil.MarshalOrPanic(&msp.MSPRole{Role: msp.MSPRole_MEMBER, MspIdentifier: orgName}),
|
|
})
|
|
}
|
|
|
|
func (ip *inquireablePolicy) SatisfiedBy() []policies.PrincipalSet {
|
|
var res []policies.PrincipalSet
|
|
for _, orgs := range ip.orgCombinations {
|
|
for _, org := range orgs {
|
|
ip.appendPrincipal(org)
|
|
}
|
|
res = append(res, ip.principals)
|
|
ip.principals = nil
|
|
}
|
|
return res
|
|
}
|
|
|
|
func peerIdentity(mspID string, i int) api.PeerIdentityInfo {
|
|
p := []byte(fmt.Sprintf("p%d", i))
|
|
sID := &msp.SerializedIdentity{
|
|
Mspid: mspID,
|
|
IdBytes: p,
|
|
}
|
|
b, _ := proto.Marshal(sID)
|
|
return api.PeerIdentityInfo{
|
|
Identity: api.PeerIdentityType(b),
|
|
PKIId: gossipcommon.PKIidType(p),
|
|
Organization: api.OrgIdentityType(mspID),
|
|
}
|
|
}
|
|
|
|
type peerInfo struct {
|
|
identity api.PeerIdentityType
|
|
pkiID gossipcommon.PKIidType
|
|
gdisc.NetworkMember
|
|
}
|
|
|
|
func aliveMessage(id int) *gossip.Envelope {
|
|
g := &gossip.GossipMessage{
|
|
Content: &gossip.GossipMessage_AliveMsg{
|
|
AliveMsg: &gossip.AliveMessage{
|
|
Timestamp: &gossip.PeerTime{
|
|
SeqNum: uint64(id),
|
|
IncNum: uint64(time.Now().UnixNano()),
|
|
},
|
|
Membership: &gossip.Member{
|
|
Endpoint: fmt.Sprintf("p%d", id),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
sMsg, _ := protoext.NoopSign(g)
|
|
return sMsg.Envelope
|
|
}
|
|
|
|
func stateInfoMessage(chaincodes ...*gossip.Chaincode) *gossip.Envelope {
|
|
return stateInfoMessageWithHeight(0, chaincodes...)
|
|
}
|
|
|
|
func stateInfoMessageWithHeight(ledgerHeight uint64, chaincodes ...*gossip.Chaincode) *gossip.Envelope {
|
|
g := &gossip.GossipMessage{
|
|
Content: &gossip.GossipMessage_StateInfo{
|
|
StateInfo: &gossip.StateInfo{
|
|
Timestamp: &gossip.PeerTime{
|
|
SeqNum: 5,
|
|
IncNum: uint64(time.Now().UnixNano()),
|
|
},
|
|
Properties: &gossip.Properties{
|
|
Chaincodes: chaincodes,
|
|
LedgerHeight: ledgerHeight,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
sMsg, _ := protoext.NoopSign(g)
|
|
return sMsg.Envelope
|
|
}
|
|
|
|
func newPeer(i int, env *gossip.Envelope, properties *gossip.Properties) *peerInfo {
|
|
p := fmt.Sprintf("p%d", i)
|
|
return &peerInfo{
|
|
pkiID: gossipcommon.PKIidType(p),
|
|
identity: api.PeerIdentityType(p),
|
|
NetworkMember: gdisc.NetworkMember{
|
|
PKIid: gossipcommon.PKIidType(p),
|
|
Endpoint: p,
|
|
InternalEndpoint: p,
|
|
Envelope: env,
|
|
Properties: properties,
|
|
},
|
|
}
|
|
}
|
|
|
|
type mockSupport struct {
|
|
seq uint64
|
|
mock.Mock
|
|
endorsementAnalyzer
|
|
}
|
|
|
|
func (ms *mockSupport) ConfigSequence(channel string) uint64 {
|
|
// Ensure cache is bypassed
|
|
ms.seq++
|
|
return ms.seq
|
|
}
|
|
|
|
func (ms *mockSupport) IdentityInfo() api.PeerIdentitySet {
|
|
return ms.Called().Get(0).(api.PeerIdentitySet)
|
|
}
|
|
|
|
func (*mockSupport) ChannelExists(channel string) bool {
|
|
return true
|
|
}
|
|
|
|
func (ms *mockSupport) PeersOfChannel(gossipcommon.ChannelID) gdisc.Members {
|
|
return ms.Called().Get(0).(gdisc.Members)
|
|
}
|
|
|
|
func (ms *mockSupport) Peers() gdisc.Members {
|
|
return ms.Called().Get(0).(gdisc.Members)
|
|
}
|
|
|
|
func (ms *mockSupport) PeersForEndorsement(channel gossipcommon.ChannelID, interest *peer.ChaincodeInterest) (*discovery.EndorsementDescriptor, error) {
|
|
return ms.endorsementAnalyzer.PeersForEndorsement(channel, interest)
|
|
}
|
|
|
|
func (ms *mockSupport) PeersAuthorizedByCriteria(channel gossipcommon.ChannelID, interest *peer.ChaincodeInterest) (gdisc.Members, error) {
|
|
return ms.endorsementAnalyzer.PeersAuthorizedByCriteria(channel, interest)
|
|
}
|
|
|
|
func (*mockSupport) EligibleForService(channel string, data protoutil.SignedData) error {
|
|
return nil
|
|
}
|
|
|
|
func (ms *mockSupport) Config(channel string) (*discovery.ConfigResult, error) {
|
|
return ms.Called(channel).Get(0).(*discovery.ConfigResult), nil
|
|
}
|
|
|
|
type mockDiscoveryServer struct {
|
|
mock.Mock
|
|
*grpc.Server
|
|
port int64
|
|
}
|
|
|
|
func newMockDiscoveryService() *mockDiscoveryServer {
|
|
l, err := net.Listen("tcp", "localhost:0")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
s := grpc.NewServer()
|
|
d := &mockDiscoveryServer{
|
|
Server: s,
|
|
}
|
|
discovery.RegisterDiscoveryServer(s, d)
|
|
go s.Serve(l)
|
|
_, portStr, _ := net.SplitHostPort(l.Addr().String())
|
|
d.port, _ = strconv.ParseInt(portStr, 10, 64)
|
|
return d
|
|
}
|
|
|
|
func (ds *mockDiscoveryServer) shutdown() {
|
|
ds.Server.Stop()
|
|
}
|
|
|
|
func (ds *mockDiscoveryServer) Discover(context.Context, *discovery.SignedRequest) (*discovery.Response, error) {
|
|
args := ds.Called()
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*discovery.Response), nil
|
|
}
|
|
|
|
func ccCall(ccNames ...string) []*peer.ChaincodeCall {
|
|
var call []*peer.ChaincodeCall
|
|
for _, ccName := range ccNames {
|
|
call = append(call, &peer.ChaincodeCall{
|
|
Name: ccName,
|
|
})
|
|
}
|
|
return call
|
|
}
|
|
|
|
func cc2ccInterests(invocationsChains ...[]*peer.ChaincodeCall) []*peer.ChaincodeInterest {
|
|
var interests []*peer.ChaincodeInterest
|
|
for _, invocationChain := range invocationsChains {
|
|
interests = append(interests, &peer.ChaincodeInterest{
|
|
Chaincodes: invocationChain,
|
|
})
|
|
}
|
|
return interests
|
|
}
|
|
|
|
func interest(ccNames ...string) *peer.ChaincodeInterest {
|
|
interest := &peer.ChaincodeInterest{
|
|
Chaincodes: []*peer.ChaincodeCall{},
|
|
}
|
|
for _, cc := range ccNames {
|
|
interest.Chaincodes = append(interest.Chaincodes, &peer.ChaincodeCall{
|
|
Name: cc,
|
|
})
|
|
}
|
|
return interest
|
|
}
|
|
|
|
func buildCollectionConfig(col2principals map[string][]*msp.MSPPrincipal) *peer.CollectionConfigPackage {
|
|
collections := &peer.CollectionConfigPackage{}
|
|
for col, principals := range col2principals {
|
|
collections.Config = append(collections.Config, &peer.CollectionConfig{
|
|
Payload: &peer.CollectionConfig_StaticCollectionConfig{
|
|
StaticCollectionConfig: &peer.StaticCollectionConfig{
|
|
Name: col,
|
|
MemberOrgsPolicy: &peer.CollectionPolicyConfig{
|
|
Payload: &peer.CollectionPolicyConfig_SignaturePolicy{
|
|
SignaturePolicy: &common.SignaturePolicyEnvelope{
|
|
Identities: principals,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
return collections
|
|
}
|
|
|
|
func memberPrincipal(mspID string) *msp.MSPPrincipal {
|
|
return &msp.MSPPrincipal{
|
|
PrincipalClassification: msp.MSPPrincipal_ROLE,
|
|
Principal: protoutil.MarshalOrPanic(&msp.MSPRole{
|
|
MspIdentifier: mspID,
|
|
Role: msp.MSPRole_MEMBER,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// ledgerHeightFilter is a filter that uses ledger height to prioritize endorsers, although it provides more
|
|
// even balancing than simply prioritizing by highest ledger height. Certain peers tend to always be at a slightly
|
|
// higher ledger height than others (such as leaders) but we shouldn't always be selecting leaders.
|
|
// This filter treats endorsers that are within a certain block height threshold equally and sorts them randomly.
|
|
type ledgerHeightFilter struct {
|
|
threshold uint64
|
|
}
|
|
|
|
// Filter returns a random set of endorsers that are above the configured ledger height threshold.
|
|
func (f *ledgerHeightFilter) Filter(endorsers Endorsers) Endorsers {
|
|
if len(endorsers) <= 1 {
|
|
return endorsers
|
|
}
|
|
|
|
maxHeight := getMaxLedgerHeight(endorsers)
|
|
|
|
if maxHeight <= f.threshold {
|
|
return endorsers.Shuffle()
|
|
}
|
|
|
|
cutoffHeight := maxHeight - f.threshold
|
|
|
|
var filteredEndorsers Endorsers
|
|
for _, p := range endorsers {
|
|
ledgerHeight := getLedgerHeight(p)
|
|
if ledgerHeight >= cutoffHeight {
|
|
filteredEndorsers = append(filteredEndorsers, p)
|
|
}
|
|
}
|
|
return filteredEndorsers.Shuffle()
|
|
}
|
|
|
|
func getLedgerHeight(endorser *Peer) uint64 {
|
|
return endorser.StateInfoMessage.GetStateInfo().GetProperties().LedgerHeight
|
|
}
|
|
|
|
func getMaxLedgerHeight(endorsers Endorsers) uint64 {
|
|
var maxHeight uint64
|
|
for _, peer := range endorsers {
|
|
height := getLedgerHeight(peer)
|
|
if height > maxHeight {
|
|
maxHeight = height
|
|
}
|
|
}
|
|
return maxHeight
|
|
}
|
|
|
|
func getNames(endorsers Endorsers) []string {
|
|
var names []string
|
|
for _, p := range endorsers {
|
|
names = append(names, p.AliveMessage.GetAliveMsg().Membership.Endpoint)
|
|
}
|
|
return names
|
|
}
|