diff --git a/pkg/fab/chconfig/chconfig.go b/pkg/fab/chconfig/chconfig.go index 5c08f42520..2f7146916b 100644 --- a/pkg/fab/chconfig/chconfig.go +++ b/pkg/fab/chconfig/chconfig.go @@ -8,6 +8,7 @@ package chconfig import ( reqContext "context" + "math/rand" "github.com/golang/protobuf/proto" @@ -33,6 +34,7 @@ var logger = logging.NewLogger("fabsdk/fab") const ( defaultMinResponses = 1 + defaultMaxTargets = 2 ) // Opts contains options for retrieving channel configuration @@ -40,6 +42,7 @@ type Opts struct { Orderer fab.Orderer // if configured, channel config will be retrieved from this orderer Targets []fab.Peer // if configured, channel config will be retrieved from peers (targets) MinResponses int // used with targets option; min number of success responses (from targets/peers) + MaxTargets int //if configured, channel config will be retrieved for these number of random targets } // Option func for each Opts argument @@ -145,18 +148,15 @@ func (c *ChannelConfig) queryPeers(reqCtx reqContext.Context) (*ChannelCfg, erro } targets = append(targets, newPeer) - } + + targets = randomMaxTargets(targets, c.opts.MaxTargets) + } else { targets = peersToTxnProcessors(c.opts.Targets) } - minEndorsers := c.opts.MinResponses - if minEndorsers == 0 { - minEndorsers = defaultMinResponses - } - - configEnvelope, err := l.QueryConfigBlock(reqCtx, targets, minEndorsers) + configEnvelope, err := l.QueryConfigBlock(reqCtx, targets, c.opts.MinResponses) if err != nil { return nil, errors.WithMessage(err, "QueryBlockConfig failed") } @@ -198,6 +198,14 @@ func WithOrderer(orderer fab.Orderer) Option { } } +// WithMaxTargets encapsulates minTargets to Option +func WithMaxTargets(maxTargets int) Option { + return func(opts *Opts) error { + opts.MaxTargets = maxTargets + return nil + } +} + // prepareQueryConfigOpts Reads channel config options from Option array func prepareOpts(options ...Option) (Opts, error) { opts := Opts{} @@ -207,6 +215,15 @@ func prepareOpts(options ...Option) (Opts, error) { return opts, errors.WithMessage(err, "Failed to read query config opts") } } + + //resolve defaults + if opts.MinResponses == 0 { + opts.MinResponses = defaultMinResponses + } + if opts.MaxTargets == 0 { + opts.MaxTargets = defaultMaxTargets + } + return opts, nil } @@ -481,3 +498,15 @@ func peersToTxnProcessors(peers []fab.Peer) []fab.ProposalProcessor { } return tpp } + +//randomMaxTargets returns random sub set of max length targets +func randomMaxTargets(targets []fab.ProposalProcessor, max int) []fab.ProposalProcessor { + if len(targets) <= max { + return targets + } + for i := range targets { + j := rand.Intn(i + 1) + targets[i], targets[j] = targets[j], targets[i] + } + return targets[:max] +} diff --git a/pkg/fab/chconfig/chconfig_test.go b/pkg/fab/chconfig/chconfig_test.go index ce259376c2..0aa987a2c1 100644 --- a/pkg/fab/chconfig/chconfig_test.go +++ b/pkg/fab/chconfig/chconfig_test.go @@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0 package chconfig import ( + reqContext "context" "testing" "time" @@ -17,6 +18,8 @@ import ( "github.com/hyperledger/fabric-sdk-go/pkg/fab/mocks" "github.com/hyperledger/fabric-sdk-go/pkg/fab/orderer" mspmocks "github.com/hyperledger/fabric-sdk-go/pkg/msp/mocks" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" ) @@ -29,7 +32,7 @@ func TestChannelConfigWithPeer(t *testing.T) { ctx := setupTestContext() peer := getPeerWithConfigBlockPayload(t) - channelConfig, err := New(channelID, WithPeers([]fab.Peer{peer}), WithMinResponses(1)) + channelConfig, err := New(channelID, WithPeers([]fab.Peer{peer}), WithMinResponses(1), WithMaxTargets(1)) if err != nil { t.Fatalf("Failed to create new channel client: %s", err) } @@ -87,6 +90,40 @@ func TestChannelConfigWithOrdererError(t *testing.T) { } +func TestRandomMaxTargetsSelections(t *testing.T) { + + testTargets := []fab.ProposalProcessor{ + &mockProposalProcessor{"ONE"}, &mockProposalProcessor{"TWO"}, &mockProposalProcessor{"THREE"}, + &mockProposalProcessor{"FOUR"}, &mockProposalProcessor{"FIVE"}, &mockProposalProcessor{"SIX"}, + &mockProposalProcessor{"SEVEN"}, &mockProposalProcessor{"EIGHT"}, &mockProposalProcessor{"NINE"}, + } + + max := 3 + before := "" + for _, v := range testTargets[:max] { + before = before + v.(*mockProposalProcessor).name + } + + responseTargets := randomMaxTargets(testTargets, max) + assert.True(t, responseTargets != nil && len(responseTargets) == max, "response target not as expected") + + after := "" + for _, v := range responseTargets { + after = after + v.(*mockProposalProcessor).name + } + //make sure it is random + assert.False(t, before == after, "response targets are not random") + + max = 0 //when zero minimum supplied, result should be empty + responseTargets = randomMaxTargets(testTargets, max) + assert.True(t, responseTargets != nil && len(responseTargets) == max, "response target not as expected") + + max = 12 //greater than targets length + responseTargets = randomMaxTargets(testTargets, max) + assert.True(t, responseTargets != nil && len(responseTargets) == len(testTargets), "response target not as expected") + +} + func setupTestContext() context.Client { user := mspmocks.NewMockSigningIdentity("test", "test") ctx := mocks.NewMockContext(user) @@ -121,6 +158,15 @@ func getPeerWithConfigBlockPayload(t *testing.T) fab.Peer { return peer } +//mockProposalProcessor to mock proposal processor for random max target test +type mockProposalProcessor struct { + name string +} + +func (pp *mockProposalProcessor) ProcessTransactionProposal(reqCtx reqContext.Context, request fab.ProcessProposalRequest) (*fab.TransactionProposalResponse, error) { + return nil, errors.New("not implemented, just mock") +} + var validRootCA = `-----BEGIN CERTIFICATE----- MIICYjCCAgmgAwIBAgIUB3CTDOU47sUC5K4kn/Caqnh114YwCgYIKoZIzj0EAwIw fzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh