386 lines
14 KiB
Go
386 lines
14 KiB
Go
/*
|
|
Copyright 2021 IBM All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
pb "github.com/hyperledger/fabric-protos-go/gateway"
|
|
"github.com/hyperledger/fabric-protos-go/peer"
|
|
"github.com/hyperledger/fabric/gossip/common"
|
|
gdiscovery "github.com/hyperledger/fabric/gossip/discovery"
|
|
"github.com/hyperledger/fabric/internal/pkg/gateway/mocks"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func TestEvaluate(t *testing.T) {
|
|
tests := []testDef{
|
|
{
|
|
name: "single endorser",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 5},
|
|
},
|
|
localLedgerHeight: 5,
|
|
expectedEndorsers: []string{"localhost:7051"},
|
|
},
|
|
{
|
|
name: "no endorsers",
|
|
plan: endorsementPlan{},
|
|
members: []networkMember{},
|
|
errCode: codes.FailedPrecondition,
|
|
errString: "no peers available to evaluate chaincode test_chaincode in channel test_channel",
|
|
},
|
|
{
|
|
name: "five endorsers, prefer local org",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 5},
|
|
{"id2", "peer1:8051", "msp1", 6},
|
|
{"id3", "peer2:9051", "msp2", 6},
|
|
{"id4", "peer3:10051", "msp2", 5},
|
|
{"id5", "peer4:11051", "msp3", 6},
|
|
},
|
|
localLedgerHeight: 5,
|
|
expectedEndorsers: []string{"peer1:8051"},
|
|
},
|
|
{
|
|
name: "five endorsers, prefer host peer",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 5},
|
|
{"id2", "peer1:8051", "msp1", 5},
|
|
{"id3", "peer2:9051", "msp2", 6},
|
|
{"id4", "peer3:10051", "msp2", 5},
|
|
{"id5", "peer4:11051", "msp3", 6},
|
|
},
|
|
localLedgerHeight: 5,
|
|
expectedEndorsers: []string{"localhost:7051"},
|
|
},
|
|
{
|
|
name: "five endorsers, prefer host peer despite no endpoint",
|
|
members: []networkMember{
|
|
{"id1", "", "msp1", 5},
|
|
{"id2", "peer1:8051", "msp1", 5},
|
|
{"id3", "peer2:9051", "msp2", 6},
|
|
{"id4", "peer3:10051", "msp2", 5},
|
|
{"id5", "peer4:11051", "msp3", 6},
|
|
},
|
|
localLedgerHeight: 5,
|
|
expectedEndorsers: []string{"localhost:7051"},
|
|
},
|
|
{
|
|
name: "evaluate with targetOrganizations, prefer local org despite block height",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 5},
|
|
{"id2", "peer1:8051", "msp1", 5},
|
|
{"id3", "peer2:9051", "msp2", 6},
|
|
{"id4", "peer3:10051", "msp2", 5},
|
|
{"id5", "peer4:11051", "msp3", 6},
|
|
},
|
|
localLedgerHeight: 5,
|
|
endorsingOrgs: []string{"msp3", "msp1"},
|
|
expectedEndorsers: []string{"localhost:7051"},
|
|
},
|
|
{
|
|
name: "evaluate with targetOrganizations that doesn't include local org, prefer highest block height",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 5},
|
|
{"id2", "peer1:8051", "msp1", 5},
|
|
{"id3", "peer2:9051", "msp2", 6},
|
|
{"id4", "peer3:10051", "msp2", 5},
|
|
{"id5", "peer4:11051", "msp3", 7},
|
|
},
|
|
localLedgerHeight: 5,
|
|
endorsingOrgs: []string{"msp2", "msp3"},
|
|
expectedEndorsers: []string{"peer4:11051"},
|
|
},
|
|
{
|
|
name: "evaluate with transient data should select local org, highest block height",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 4},
|
|
{"id2", "peer1:8051", "msp1", 5},
|
|
{"id3", "peer2:9051", "msp2", 6},
|
|
{"id4", "peer3:10051", "msp2", 5},
|
|
{"id5", "peer4:11051", "msp3", 7},
|
|
},
|
|
localLedgerHeight: 4,
|
|
transientData: map[string][]byte{"transient-key": []byte("transient-value")},
|
|
expectedEndorsers: []string{"peer1:8051"},
|
|
},
|
|
{
|
|
name: "evaluate with transient data should fail if local org not available",
|
|
members: []networkMember{
|
|
{"id3", "peer2:9051", "msp2", 6},
|
|
{"id4", "peer3:10051", "msp2", 5},
|
|
{"id5", "peer4:11051", "msp3", 7},
|
|
},
|
|
transientData: map[string][]byte{"transient-key": []byte("transient-value")},
|
|
errCode: codes.FailedPrecondition,
|
|
errString: "no endorsers found in the gateway's organization; retry specifying target organization(s) to protect transient data: no peers available to evaluate chaincode test_chaincode in channel test_channel",
|
|
},
|
|
{
|
|
name: "evaluate with transient data and target (non-local) orgs should select the highest block height peer",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 11},
|
|
{"id2", "peer1:8051", "msp1", 5},
|
|
{"id3", "peer2:9051", "msp2", 6},
|
|
{"id4", "peer3:10051", "msp2", 9},
|
|
{"id5", "peer4:11051", "msp3", 7},
|
|
},
|
|
localLedgerHeight: 11,
|
|
transientData: map[string][]byte{"transient-key": []byte("transient-value")},
|
|
endorsingOrgs: []string{"msp2", "msp3"},
|
|
expectedEndorsers: []string{"peer3:10051"},
|
|
},
|
|
{
|
|
name: "process proposal fails",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 5},
|
|
},
|
|
localLedgerHeight: 5,
|
|
endpointDefinition: &endpointDef{
|
|
proposalError: status.Error(codes.Aborted, "wibble"),
|
|
},
|
|
errCode: codes.Aborted,
|
|
errString: "failed to evaluate transaction, see attached details for more info",
|
|
errDetails: []*pb.ErrorDetail{{
|
|
Address: "localhost:7051",
|
|
MspId: "msp1",
|
|
Message: "rpc error: code = Aborted desc = wibble",
|
|
}},
|
|
},
|
|
{
|
|
name: "process proposal chaincode error",
|
|
members: []networkMember{
|
|
{"id2", "peer1:8051", "msp1", 5},
|
|
},
|
|
endpointDefinition: &endpointDef{
|
|
proposalResponseStatus: 400,
|
|
proposalResponseMessage: "Mock chaincode error",
|
|
},
|
|
errCode: codes.Unknown,
|
|
errString: "evaluate call to endorser returned error: chaincode response 400, Mock chaincode error",
|
|
errDetails: []*pb.ErrorDetail{{
|
|
Address: "peer1:8051",
|
|
MspId: "msp1",
|
|
Message: "chaincode response 400, Mock chaincode error",
|
|
}},
|
|
},
|
|
{
|
|
name: "evaluate on local org fails - retry in other org",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 4},
|
|
{"id2", "peer1:8051", "msp1", 4},
|
|
{"id3", "peer2:9051", "msp2", 3},
|
|
{"id4", "peer3:10051", "msp2", 4},
|
|
{"id5", "peer4:11051", "msp3", 5},
|
|
},
|
|
localLedgerHeight: 4,
|
|
plan: endorsementPlan{
|
|
"g1": {{endorser: localhostMock, height: 4}, {endorser: peer1Mock, height: 4}}, // msp1
|
|
"g2": {{endorser: peer2Mock, height: 3}, {endorser: peer3Mock, height: 4}}, // msp2
|
|
"g3": {{endorser: peer4Mock, height: 5}}, // msp3
|
|
},
|
|
postSetup: func(t *testing.T, def *preparedTest) {
|
|
def.localEndorser.ProcessProposalReturns(createErrorResponse(t, 500, "bad local endorser", nil), nil)
|
|
peer1Mock.client.(*mocks.EndorserClient).ProcessProposalReturns(createErrorResponse(t, 500, "bad peer1 endorser", nil), nil)
|
|
},
|
|
expectedEndorsers: []string{"peer4:11051"},
|
|
},
|
|
{
|
|
name: "restrict to local org peers - which all fail",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 4},
|
|
{"id2", "peer1:8051", "msp1", 4},
|
|
{"id3", "peer2:9051", "msp2", 3},
|
|
{"id4", "peer3:10051", "msp2", 4},
|
|
{"id5", "peer4:11051", "msp3", 5},
|
|
},
|
|
localLedgerHeight: 4,
|
|
plan: endorsementPlan{
|
|
"g1": {{endorser: localhostMock, height: 4}, {endorser: peer1Mock, height: 4}}, // msp1
|
|
"g2": {{endorser: peer2Mock, height: 3}, {endorser: peer3Mock, height: 4}}, // msp2
|
|
"g3": {{endorser: peer4Mock, height: 5}}, // msp3
|
|
},
|
|
postSetup: func(t *testing.T, def *preparedTest) {
|
|
def.localEndorser.ProcessProposalReturns(createErrorResponse(t, 500, "bad local endorser", nil), nil)
|
|
peer1Mock.client.(*mocks.EndorserClient).ProcessProposalReturns(createErrorResponse(t, 500, "bad peer1 endorser", nil), nil)
|
|
},
|
|
endorsingOrgs: []string{"msp1"},
|
|
errCode: codes.Aborted,
|
|
errString: "failed to evaluate transaction, see attached details for more info",
|
|
errDetails: []*pb.ErrorDetail{
|
|
{
|
|
Address: "localhost:7051",
|
|
MspId: "msp1",
|
|
Message: "bad local endorser",
|
|
},
|
|
{
|
|
Address: "peer1:8051",
|
|
MspId: "msp1",
|
|
Message: "bad peer1 endorser",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "fails due to invalid signature (pre-process check) - does not retry",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 4},
|
|
{"id2", "peer1:8051", "msp1", 4},
|
|
{"id3", "peer2:9051", "msp2", 3},
|
|
{"id4", "peer3:10051", "msp2", 4},
|
|
{"id5", "peer4:11051", "msp3", 5},
|
|
},
|
|
localLedgerHeight: 4,
|
|
plan: endorsementPlan{
|
|
"g1": {{endorser: localhostMock, height: 4}, {endorser: peer1Mock, height: 4}}, // msp1
|
|
"g2": {{endorser: peer2Mock, height: 3}, {endorser: peer3Mock, height: 4}}, // msp2
|
|
"g3": {{endorser: peer4Mock, height: 5}}, // msp3
|
|
},
|
|
postSetup: func(t *testing.T, def *preparedTest) {
|
|
def.localEndorser.ProcessProposalReturns(createErrorResponse(t, 500, "invalid signature", nil), fmt.Errorf("invalid signature"))
|
|
},
|
|
endorsingOrgs: []string{"msp1"},
|
|
errCode: codes.FailedPrecondition, // Code path could fail for reasons other than authentication
|
|
errString: "evaluate call to endorser returned error: invalid signature",
|
|
errDetails: []*pb.ErrorDetail{
|
|
{
|
|
Address: "localhost:7051",
|
|
MspId: "msp1",
|
|
Message: "invalid signature",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "fails due to chaincode panic - retry on next peer",
|
|
members: []networkMember{
|
|
{"id1", "localhost:7051", "msp1", 4},
|
|
{"id2", "peer1:8051", "msp1", 4},
|
|
{"id3", "peer2:9051", "msp2", 3},
|
|
{"id4", "peer3:10051", "msp2", 4},
|
|
{"id5", "peer4:11051", "msp3", 5},
|
|
},
|
|
localLedgerHeight: 4,
|
|
plan: endorsementPlan{
|
|
"g1": {{endorser: localhostMock, height: 4}, {endorser: peer1Mock, height: 4}}, // msp1
|
|
"g2": {{endorser: peer2Mock, height: 3}, {endorser: peer3Mock, height: 4}}, // msp2
|
|
"g3": {{endorser: peer4Mock, height: 5}}, // msp3
|
|
},
|
|
postSetup: func(t *testing.T, def *preparedTest) {
|
|
def.localEndorser.ProcessProposalReturns(createErrorResponse(t, 500, "error in simulation: chaincode stream terminated", nil), nil)
|
|
peer1Mock.client.(*mocks.EndorserClient).ProcessProposalReturns(createErrorResponse(t, 500, "error in simulation: chaincode stream terminated", nil), nil)
|
|
},
|
|
endorsingOrgs: []string{"msp1"},
|
|
errCode: codes.Aborted,
|
|
errString: "failed to evaluate transaction, see attached details for more info",
|
|
errDetails: []*pb.ErrorDetail{
|
|
{
|
|
Address: "localhost:7051",
|
|
MspId: "msp1",
|
|
Message: "error in simulation: chaincode stream terminated",
|
|
},
|
|
{
|
|
Address: "peer1:8051",
|
|
MspId: "msp1",
|
|
Message: "error in simulation: chaincode stream terminated",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "dialing endorser endpoint fails",
|
|
members: []networkMember{
|
|
{"id3", "peer2:9051", "msp2", 5},
|
|
},
|
|
postSetup: func(t *testing.T, def *preparedTest) {
|
|
def.dialer.Calls(func(_ context.Context, target string, _ ...grpc.DialOption) (*grpc.ClientConn, error) {
|
|
if target == "peer2:9051" {
|
|
return nil, fmt.Errorf("endorser not answering")
|
|
}
|
|
return nil, nil
|
|
})
|
|
},
|
|
errCode: codes.Unavailable,
|
|
errString: "failed to create new connection: endorser not answering",
|
|
},
|
|
{
|
|
name: "discovery returns incomplete information - no Properties",
|
|
postSetup: func(t *testing.T, def *preparedTest) {
|
|
def.discovery.PeersOfChannelReturns([]gdiscovery.NetworkMember{{
|
|
Endpoint: "localhost:7051",
|
|
PKIid: []byte("ill-defined"),
|
|
}})
|
|
},
|
|
errCode: codes.FailedPrecondition,
|
|
errString: "no peers available to evaluate chaincode test_chaincode in channel test_channel",
|
|
},
|
|
{
|
|
name: "context timeout during evaluate",
|
|
plan: endorsementPlan{
|
|
"g1": {{endorser: localhostMock, height: 3}}, // msp1
|
|
},
|
|
localLedgerHeight: 3,
|
|
postSetup: func(t *testing.T, def *preparedTest) {
|
|
def.ctx, def.cancel = context.WithTimeout(def.ctx, 100*time.Millisecond)
|
|
|
|
def.localEndorser.ProcessProposalStub = func(ctx context.Context, proposal *peer.SignedProposal, option ...grpc.CallOption) (*peer.ProposalResponse, error) {
|
|
time.Sleep(200 * time.Millisecond)
|
|
return createProposalResponse(t, peer1Mock.address, "mock_response", 200, ""), nil
|
|
}
|
|
},
|
|
postTest: func(t *testing.T, def *preparedTest) {
|
|
def.cancel()
|
|
},
|
|
errCode: codes.DeadlineExceeded,
|
|
errString: "evaluate timeout expired",
|
|
},
|
|
{
|
|
name: "uses local host ledger height",
|
|
members: []networkMember{
|
|
{"id2", "peer1:8051", "msp1", 6},
|
|
{"id1", "localhost:7051", "msp1", 5},
|
|
},
|
|
localLedgerHeight: 7,
|
|
expectedEndorsers: []string{"localhost:7051"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
test := prepareTest(t, &tt)
|
|
|
|
response, err := test.server.Evaluate(test.ctx, &pb.EvaluateRequest{ProposedTransaction: test.signedProposal, TargetOrganizations: tt.endorsingOrgs})
|
|
|
|
if checkError(t, &tt, err) {
|
|
require.Nil(t, response, "response on error")
|
|
return
|
|
}
|
|
|
|
// test the assertions
|
|
|
|
require.NoError(t, err)
|
|
// assert the result is the payload from the proposal response returned by the local endorser
|
|
require.Equal(t, []byte("mock_response"), response.Result.Payload, "Incorrect result")
|
|
|
|
// check the correct endorsers (mock) were called with the right parameters
|
|
checkEndorsers(t, tt.expectedEndorsers, test)
|
|
|
|
// check the discovery service (mock) was invoked as expected
|
|
expectedChannel := common.ChannelID(testChannel)
|
|
require.Equal(t, 2, test.discovery.PeersOfChannelCallCount())
|
|
channel := test.discovery.PeersOfChannelArgsForCall(0)
|
|
require.Equal(t, expectedChannel, channel)
|
|
channel = test.discovery.PeersOfChannelArgsForCall(1)
|
|
require.Equal(t, expectedChannel, channel)
|
|
|
|
require.Equal(t, 1, test.discovery.IdentityInfoCallCount())
|
|
})
|
|
}
|
|
}
|