From 77b149c926deada5bea40c8966b91921d5644fad Mon Sep 17 00:00:00 2001 From: Fedor Partanskiy Date: Wed, 22 Jan 2025 23:37:27 +0300 Subject: [PATCH] add support GetMultipleKeys Signed-off-by: Fedor Partanskiy --- core/chaincode/chaincode_support.go | 4 + core/chaincode/config.go | 40 ++- core/chaincode/config_test.go | 42 +++ core/chaincode/handler.go | 52 +++- core/chaincode/handler_test.go | 363 +++++++++++++++++++++- integration/chaincode/multi/chaincode.go | 47 ++- integration/e2e/write_batch_test.go | 89 ++++++ integration/nwo/network.go | 2 + integration/nwo/template/core_template.go | 2 + internal/peer/node/start.go | 2 + sampleconfig/core.yaml | 14 +- 11 files changed, 628 insertions(+), 29 deletions(-) diff --git a/core/chaincode/chaincode_support.go b/core/chaincode/chaincode_support.go index ffdd503e06d..b5fd1e383fb 100644 --- a/core/chaincode/chaincode_support.go +++ b/core/chaincode/chaincode_support.go @@ -75,6 +75,8 @@ type ChaincodeSupport struct { UserRunsCC bool UseWriteBatch bool MaxSizeWriteBatch uint32 + UseGetMultipleKeys bool + MaxSizeGetMultipleKeys uint32 } // Launch starts executing chaincode if it is not already running. This method @@ -130,6 +132,8 @@ func (cs *ChaincodeSupport) HandleChaincodeStream(stream ccintf.ChaincodeStream) TotalQueryLimit: cs.TotalQueryLimit, UseWriteBatch: cs.UseWriteBatch, MaxSizeWriteBatch: cs.MaxSizeWriteBatch, + UseGetMultipleKeys: cs.UseGetMultipleKeys, + MaxSizeGetMultipleKeys: cs.MaxSizeGetMultipleKeys, } return handler.ProcessStream(stream) diff --git a/core/chaincode/config.go b/core/chaincode/config.go index 05a261aa194..255e7b01a8b 100644 --- a/core/chaincode/config.go +++ b/core/chaincode/config.go @@ -16,24 +16,27 @@ import ( ) const ( - defaultExecutionTimeout = 30 * time.Second - minimumStartupTimeout = 5 * time.Second - defaultMaxSizeWriteBatch = 1000 + defaultExecutionTimeout = 30 * time.Second + minimumStartupTimeout = 5 * time.Second + defaultMaxSizeWriteBatch = 1000 + defaultMaxSizeGetMultipleKeys = 1000 ) type Config struct { - TotalQueryLimit int - TLSEnabled bool - Keepalive time.Duration - ExecuteTimeout time.Duration - InstallTimeout time.Duration - StartupTimeout time.Duration - LogFormat string - LogLevel string - ShimLogLevel string - SCCAllowlist map[string]bool - UseWriteBatch bool - MaxSizeWriteBatch uint32 + TotalQueryLimit int + TLSEnabled bool + Keepalive time.Duration + ExecuteTimeout time.Duration + InstallTimeout time.Duration + StartupTimeout time.Duration + LogFormat string + LogLevel string + ShimLogLevel string + SCCAllowlist map[string]bool + UseWriteBatch bool + MaxSizeWriteBatch uint32 + UseGetMultipleKeys bool + MaxSizeGetMultipleKeys uint32 } func GlobalConfig() *Config { @@ -81,6 +84,13 @@ func (c *Config) load() { if c.MaxSizeWriteBatch <= 0 { c.MaxSizeWriteBatch = defaultMaxSizeWriteBatch } + if viper.IsSet("chaincode.runtimeParams.useGetMultipleKeys") { + c.UseGetMultipleKeys = viper.GetBool("chaincode.runtimeParams.useGetMultipleKeys") + } + c.MaxSizeGetMultipleKeys = viper.GetUint32("chaincode.runtimeParams.maxSizeGetMultipleKeys") + if c.MaxSizeGetMultipleKeys <= 0 { + c.MaxSizeGetMultipleKeys = defaultMaxSizeGetMultipleKeys + } } func parseBool(s string) bool { diff --git a/core/chaincode/config_test.go b/core/chaincode/config_test.go index 3ebcca3c88d..eb62ec0561b 100644 --- a/core/chaincode/config_test.go +++ b/core/chaincode/config_test.go @@ -37,6 +37,10 @@ var _ = Describe("Config", func() { viper.Set("chaincode.logging.level", "warning") viper.Set("chaincode.logging.shim", "warning") viper.Set("chaincode.system.somecc", true) + viper.Set("chaincode.runtimeParams.useWriteBatch", true) + viper.Set("chaincode.runtimeParams.maxSizeWriteBatch", 1001) + viper.Set("chaincode.runtimeParams.useGetMultipleKeys", true) + viper.Set("chaincode.runtimeParams.maxSizeGetMultipleKeys", 1001) config := chaincode.GlobalConfig() Expect(config.TLSEnabled).To(BeTrue()) @@ -48,6 +52,10 @@ var _ = Describe("Config", func() { Expect(config.LogLevel).To(Equal("warn")) Expect(config.ShimLogLevel).To(Equal("warn")) Expect(config.SCCAllowlist).To(Equal(map[string]bool{"somecc": true})) + Expect(config.UseWriteBatch).To(BeTrue()) + Expect(config.MaxSizeWriteBatch).To(Equal(uint32(1001))) + Expect(config.UseGetMultipleKeys).To(BeTrue()) + Expect(config.MaxSizeGetMultipleKeys).To(Equal(uint32(1001))) }) Context("when an invalid keepalive is configured", func() { @@ -95,6 +103,40 @@ var _ = Describe("Config", func() { Expect(config.ShimLogLevel).To(Equal("info")) }) }) + + Context("when an runtime params is false and zero", func() { + BeforeEach(func() { + viper.Set("chaincode.runtimeParams.useWriteBatch", false) + viper.Set("chaincode.runtimeParams.maxSizeWriteBatch", 0) + viper.Set("chaincode.runtimeParams.useGetMultipleKeys", false) + viper.Set("chaincode.runtimeParams.maxSizeGetMultipleKeys", 0) + }) + + It("check runtime params", func() { + config := chaincode.GlobalConfig() + Expect(config.UseWriteBatch).To(BeFalse()) + Expect(config.MaxSizeWriteBatch).To(Equal(uint32(1000))) + Expect(config.UseGetMultipleKeys).To(BeFalse()) + Expect(config.MaxSizeGetMultipleKeys).To(Equal(uint32(1000))) + }) + }) + + Context("when an invalid runtime params", func() { + BeforeEach(func() { + viper.Set("chaincode.runtimeParams.useWriteBatch", "abc") + viper.Set("chaincode.runtimeParams.maxSizeWriteBatch", "abc") + viper.Set("chaincode.runtimeParams.useGetMultipleKeys", "abc") + viper.Set("chaincode.runtimeParams.maxSizeGetMultipleKeys", "abc") + }) + + It("check runtime params", func() { + config := chaincode.GlobalConfig() + Expect(config.UseWriteBatch).To(BeFalse()) + Expect(config.MaxSizeWriteBatch).To(Equal(uint32(1000))) + Expect(config.UseGetMultipleKeys).To(BeFalse()) + Expect(config.MaxSizeGetMultipleKeys).To(Equal(uint32(1000))) + }) + }) }) Describe("IsDevMode", func() { diff --git a/core/chaincode/handler.go b/core/chaincode/handler.go index 1a5eb2e20a1..5dc57ac78bb 100644 --- a/core/chaincode/handler.go +++ b/core/chaincode/handler.go @@ -134,6 +134,10 @@ type Handler struct { UseWriteBatch bool // MaxSizeWriteBatch maximum batch size for the change segment MaxSizeWriteBatch uint32 + // UseGetMultipleKeys an indication that the peer can handle get multiple keys + UseGetMultipleKeys bool + // MaxSizeGetMultipleKeys maximum size of batches with get multiple keys + MaxSizeGetMultipleKeys uint32 // stateLock is used to read and set State. stateLock sync.RWMutex @@ -221,6 +225,8 @@ func (h *Handler) handleMessageReadyState(msg *pb.ChaincodeMessage) error { go h.HandleTransaction(msg, h.HandlePurgePrivateData) case pb.ChaincodeMessage_WRITE_BATCH_STATE: go h.HandleTransaction(msg, h.HandleWriteBatch) + case pb.ChaincodeMessage_GET_STATE_MULTIPLE: + go h.HandleTransaction(msg, h.HandleGetStateMultipleKeys) default: return fmt.Errorf("[%s] Fabric side handler cannot handle message (%s) while in ready state", msg.Txid, msg.Type) } @@ -449,8 +455,10 @@ func (h *Handler) sendReady() error { chaincodeLogger.Debugf("sending READY for chaincode %s", h.chaincodeID) chaincodeAdditionalParams := &pb.ChaincodeAdditionalParams{ - UseWriteBatch: h.UseWriteBatch, - MaxSizeWriteBatch: h.MaxSizeWriteBatch, + UseWriteBatch: h.UseWriteBatch, + MaxSizeWriteBatch: h.MaxSizeWriteBatch, + UseGetMultipleKeys: h.UseGetMultipleKeys, + MaxSizeGetMultipleKeys: h.MaxSizeGetMultipleKeys, } payloadBytes, err := proto.Marshal(chaincodeAdditionalParams) if err != nil { @@ -678,6 +686,46 @@ func (h *Handler) HandleGetState(msg *pb.ChaincodeMessage, txContext *Transactio return &pb.ChaincodeMessage{Type: pb.ChaincodeMessage_RESPONSE, Payload: res, Txid: msg.Txid, ChannelId: msg.ChannelId}, nil } +// HandleGetStateMultipleKeys query to ledger to get state +func (h *Handler) HandleGetStateMultipleKeys(msg *pb.ChaincodeMessage, txContext *TransactionContext) (*pb.ChaincodeMessage, error) { + getState := &pb.GetStateMultiple{} + err := proto.Unmarshal(msg.Payload, getState) + if err != nil { + return nil, errors.Wrap(err, "unmarshal failed") + } + + var res [][]byte + namespaceID := txContext.NamespaceID + collection := getState.GetCollection() + chaincodeLogger.Debugf("[%s] getting state for chaincode %s, keys %v, channel %s", shorttxid(msg.Txid), namespaceID, getState.GetKeys(), txContext.ChannelID) + + if isCollectionSet(collection) { + if txContext.IsInitTransaction { + return nil, errors.New("private data APIs are not allowed in chaincode Init()") + } + if err = errorIfCreatorHasNoReadPermission(namespaceID, collection, txContext); err != nil { + return nil, err + } + res, err = txContext.TXSimulator.GetPrivateDataMultipleKeys(namespaceID, collection, getState.GetKeys()) + } else { + res, err = txContext.TXSimulator.GetStateMultipleKeys(namespaceID, getState.GetKeys()) + } + if err != nil { + return nil, errors.WithStack(err) + } + if len(res) == 0 { + chaincodeLogger.Debugf("[%s] No state associated with keys: %v. Sending %s with an empty payload", shorttxid(msg.Txid), getState.GetKeys(), pb.ChaincodeMessage_RESPONSE) + } + + payloadBytes, err := proto.Marshal(&pb.GetStateMultipleResult{Values: res}) + if err != nil { + return nil, errors.Wrap(err, "marshal failed") + } + + // Send response msg back to chaincode. GetState will not trigger event + return &pb.ChaincodeMessage{Type: pb.ChaincodeMessage_RESPONSE, Payload: payloadBytes, Txid: msg.Txid, ChannelId: msg.ChannelId}, nil +} + func (h *Handler) HandleGetPrivateDataHash(msg *pb.ChaincodeMessage, txContext *TransactionContext) (*pb.ChaincodeMessage, error) { getState := &pb.GetState{} err := proto.Unmarshal(msg.Payload, getState) diff --git a/core/chaincode/handler_test.go b/core/chaincode/handler_test.go index 3810e0cff7f..bae5f7fe34c 100644 --- a/core/chaincode/handler_test.go +++ b/core/chaincode/handler_test.go @@ -123,10 +123,12 @@ var _ = Describe("Handler", func() { UUIDGenerator: chaincode.UUIDGeneratorFunc(func() string { return "generated-query-id" }), - AppConfig: fakeApplicationConfigRetriever, - Metrics: chaincodeMetrics, - UseWriteBatch: true, - MaxSizeWriteBatch: 1000, + AppConfig: fakeApplicationConfigRetriever, + Metrics: chaincodeMetrics, + UseWriteBatch: true, + MaxSizeWriteBatch: 1000, + UseGetMultipleKeys: true, + MaxSizeGetMultipleKeys: 1000, } chaincode.SetHandlerChatStream(handler, fakeChatStream) chaincode.SetHandlerChaincodeID(handler, "test-handler-name:1.0") @@ -914,6 +916,157 @@ var _ = Describe("Handler", func() { }) }) + Describe("HandleWriteBatch", func() { + var incomingMessage *pb.ChaincodeMessage + var request *pb.WriteBatchState + + BeforeEach(func() { + request = &pb.WriteBatchState{ + Rec: []*pb.WriteRecord{ + // SetStateValidationParameter + { + Key: "my-key1", + Metadata: &pb.StateMetadata{Metakey: "0", Value: []byte("my-value1")}, + Type: pb.WriteRecord_PUT_STATE_METADATA, + }, + // SetPrivateDataValidationParameter + { + Key: "my-key2", + Collection: "my-collection2", + Metadata: &pb.StateMetadata{Metakey: "0", Value: []byte("my-value2")}, + Type: pb.WriteRecord_PUT_STATE_METADATA, + }, + // PutState + { + Key: "my-key3", + Value: []byte("my-value3"), + Type: pb.WriteRecord_PUT_STATE, + }, + // DelState + { + Key: "my-key4", + Type: pb.WriteRecord_DEL_STATE, + }, + // PutPrivateData + { + Key: "my-key5", + Collection: "my-collection5", + Value: []byte("my-value5"), + Type: pb.WriteRecord_PUT_STATE, + }, + // DelPrivateData + { + Key: "my-key6", + Collection: "my-collection6", + Type: pb.WriteRecord_DEL_STATE, + }, + // PurgePrivateData + { + Key: "my-key7", + Collection: "my-collection7", + Type: pb.WriteRecord_PURGE_PRIVATE_DATA, + }, + }, + } + payload, err := proto.Marshal(request) + Expect(err).NotTo(HaveOccurred()) + + incomingMessage = &pb.ChaincodeMessage{ + Type: pb.ChaincodeMessage_WRITE_BATCH_STATE, + Payload: payload, + Txid: "tx-id", + ChannelId: "channel-id", + } + + fakeCollectionStore.RetrieveReadWritePermissionReturns(false, true, nil) + }) + + It("returns a response message", func() { + resp, err := handler.HandleWriteBatch(incomingMessage, txContext) + Expect(err).NotTo(HaveOccurred()) + Expect(resp).To(ProtoEqual(&pb.ChaincodeMessage{ + Type: pb.ChaincodeMessage_RESPONSE, + Txid: "tx-id", + ChannelId: "channel-id", + })) + + Expect(fakeTxSimulator.SetStateMetadataCallCount()).To(Equal(1)) + ccname, key, metaValue := fakeTxSimulator.SetStateMetadataArgsForCall(0) + Expect(ccname).To(Equal("cc-instance-name")) + Expect(key).To(Equal("my-key1")) + Expect(metaValue).To(Equal(map[string][]byte{ + "0": []byte("my-value1"), + })) + + Expect(fakeTxSimulator.SetPrivateDataMetadataCallCount()).To(Equal(1)) + ccname, collection, key, metaValue := fakeTxSimulator.SetPrivateDataMetadataArgsForCall(0) + Expect(ccname).To(Equal("cc-instance-name")) + Expect(collection).To(Equal("my-collection2")) + Expect(key).To(Equal("my-key2")) + Expect(metaValue).To(Equal(map[string][]byte{ + "0": []byte("my-value2"), + })) + + Expect(fakeTxSimulator.SetStateCallCount()).To(Equal(1)) + ccname, key, value := fakeTxSimulator.SetStateArgsForCall(0) + Expect(ccname).To(Equal("cc-instance-name")) + Expect(key).To(Equal("my-key3")) + Expect(value).To(Equal([]byte("my-value3"))) + + Expect(fakeTxSimulator.DeleteStateCallCount()).To(Equal(1)) + ccname, key = fakeTxSimulator.DeleteStateArgsForCall(0) + Expect(ccname).To(Equal("cc-instance-name")) + Expect(key).To(Equal("my-key4")) + + Expect(fakeTxSimulator.SetPrivateDataCallCount()).To(Equal(1)) + ccname, collection, key, value = fakeTxSimulator.SetPrivateDataArgsForCall(0) + Expect(ccname).To(Equal("cc-instance-name")) + Expect(collection).To(Equal("my-collection5")) + Expect(key).To(Equal("my-key5")) + Expect(value).To(Equal([]byte("my-value5"))) + + Expect(fakeTxSimulator.DeletePrivateDataCallCount()).To(Equal(1)) + ccname, collection, key = fakeTxSimulator.DeletePrivateDataArgsForCall(0) + Expect(ccname).To(Equal("cc-instance-name")) + Expect(collection).To(Equal("my-collection6")) + Expect(key).To(Equal("my-key6")) + + Expect(fakeTxSimulator.PurgePrivateDataCallCount()).To(Equal(1)) + ccname, collection, key = fakeTxSimulator.PurgePrivateDataArgsForCall(0) + Expect(ccname).To(Equal("cc-instance-name")) + Expect(collection).To(Equal("my-collection7")) + Expect(key).To(Equal("my-key7")) + }) + + Context("when unmarshalling the request fails", func() { + BeforeEach(func() { + incomingMessage.Payload = []byte("this-is-a-bogus-payload") + }) + + It("returns an error", func() { + _, err := handler.HandleWriteBatch(incomingMessage, txContext) + Expect(err).To(Not(BeNil())) + Expect(err.Error()).To(HavePrefix("unmarshal failed:")) + }) + }) + + Context("if a single error occurs, it is an error for the whole batch of changes", func() { + BeforeEach(func() { + payload, err := proto.Marshal(request) + Expect(err).NotTo(HaveOccurred()) + incomingMessage.Payload = payload + fakeCollectionStore.RetrieveReadWritePermissionReturns(false, true, nil) + fakeTxSimulator.DeleteStateReturns(errors.New("my-error")) + }) + + It("calls HandleWriteBatch on the transaction simulator with error", func() { + resp, err := handler.HandleWriteBatch(incomingMessage, txContext) + Expect(err).To(MatchError("my-error")) + Expect(resp).To(BeNil()) + }) + }) + }) + Describe("HandleGetState", func() { var ( incomingMessage *pb.ChaincodeMessage @@ -1106,6 +1259,202 @@ var _ = Describe("Handler", func() { }) }) + Describe("HandleGetStateMultipleKeys", func() { + var ( + incomingMessage *pb.ChaincodeMessage + request *pb.GetStateMultiple + expectedResponse *pb.ChaincodeMessage + response [][]byte + respProto *pb.GetStateMultipleResult + respPayload []byte + ) + + BeforeEach(func() { + request = &pb.GetStateMultiple{ + Keys: []string{"get-state-key1", "get-state-key2", "get-state-key3"}, + } + payload, err := proto.Marshal(request) + Expect(err).NotTo(HaveOccurred()) + + incomingMessage = &pb.ChaincodeMessage{ + Type: pb.ChaincodeMessage_GET_STATE_MULTIPLE, + Payload: payload, + Txid: "tx-id", + ChannelId: "channel-id", + } + + expectedResponse = &pb.ChaincodeMessage{ + Type: pb.ChaincodeMessage_RESPONSE, + Txid: "tx-id", + ChannelId: "channel-id", + } + + response = [][]byte{[]byte("my-value1"), nil, []byte("my-value3")} + respProto = &pb.GetStateMultipleResult{ + Values: response, + } + respPayload, err = proto.Marshal(respProto) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("when unmarshalling the request fails", func() { + BeforeEach(func() { + incomingMessage.Payload = []byte("this-is-a-bogus-payload") + }) + + It("returns an error", func() { + _, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).To(Not(BeNil())) + Expect(err.Error()).To(HavePrefix("unmarshal failed:")) + }) + }) + + Context("when collection is set", func() { + BeforeEach(func() { + request.Collection = "collection-name" + payload, err := proto.Marshal(request) + Expect(err).NotTo(HaveOccurred()) + incomingMessage.Payload = payload + + fakeCollectionStore.RetrieveReadWritePermissionReturns(true, false, nil) + fakeTxSimulator.GetPrivateDataMultipleKeysReturns(response, nil) + expectedResponse.Payload = respPayload + }) + + It("calls GetPrivateDataMultipleKeys on the transaction simulator", func() { + _, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeTxSimulator.GetPrivateDataMultipleKeysCallCount()).To(Equal(1)) + ccname, collection, keys := fakeTxSimulator.GetPrivateDataMultipleKeysArgsForCall(0) + Expect(ccname).To(Equal("cc-instance-name")) + Expect(collection).To(Equal("collection-name")) + Expect(keys).To(Equal([]string{"get-state-key1", "get-state-key2", "get-state-key3"})) + }) + + Context("and GetPrivateDataMultipleKeys fails due to ledger error", func() { + BeforeEach(func() { + fakeTxSimulator.GetPrivateDataMultipleKeysReturns(nil, errors.New("french fries")) + }) + + It("returns the error from GetPrivateDataMultipleKeys", func() { + _, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).To(MatchError("french fries")) + }) + }) + + Context("and GetPrivateDataMultipleKeys fails due to no read access permission", func() { + BeforeEach(func() { + fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil) + }) + + It("returns the error from errorIfCreatorHasNoReadAccess", func() { + _, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).To(MatchError("tx creator does not have read access" + + " permission on privatedata in chaincodeName:cc-instance-name" + + " collectionName: collection-name")) + }) + }) + + Context("and GetPrivateDataMultipleKeys fails due to error in checking the read access permission", func() { + BeforeEach(func() { + fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, errors.New("no collection config")) + }) + + It("returns the error from errorIfCreatorHasNoReadAccess", func() { + _, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).To(MatchError("no collection config")) + }) + }) + + Context("and GetPrivateDataMultipleKeys fails due to Init transaction", func() { + BeforeEach(func() { + txContext.IsInitTransaction = true + }) + + It("returns the error from errorIfInitTransaction", func() { + _, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()")) + }) + }) + + Context("and GetPrivateDataMultipleKeys returns the response message", func() { + It("returns the response message from GetPrivateDataMultipleKeys", func() { + fakeCollectionStore.RetrieveReadWritePermissionReturns(true, false, nil) + resp, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).NotTo(HaveOccurred()) + Expect(resp).To(ProtoEqual(&pb.ChaincodeMessage{ + Type: pb.ChaincodeMessage_RESPONSE, + Payload: respPayload, + Txid: "tx-id", + ChannelId: "channel-id", + })) + // as the cache hit should happen in CollectionACLCache, the following + // RetrieveReadWritePermissionReturns should not be called + fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil) + resp, err = handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).NotTo(HaveOccurred()) + Expect(resp).To(Equal(&pb.ChaincodeMessage{ + Type: pb.ChaincodeMessage_RESPONSE, + Payload: respPayload, + Txid: "tx-id", + ChannelId: "channel-id", + })) + }) + }) + + It("returns the response message from GetPrivateDataMultipleKeys", func() { + resp, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).NotTo(HaveOccurred()) + Expect(resp).To(Equal(&pb.ChaincodeMessage{ + Type: pb.ChaincodeMessage_RESPONSE, + Payload: respPayload, + Txid: "tx-id", + ChannelId: "channel-id", + })) + }) + }) + + Context("when collection is not set", func() { + BeforeEach(func() { + fakeTxSimulator.GetStateMultipleKeysReturns(response, nil) + expectedResponse.Payload = respPayload + }) + + It("calls GetStateMultipleKeys on the transaction simulator", func() { + _, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeTxSimulator.GetStateMultipleKeysCallCount()).To(Equal(1)) + ccname, keys := fakeTxSimulator.GetStateMultipleKeysArgsForCall(0) + Expect(ccname).To(Equal("cc-instance-name")) + Expect(keys).To(Equal([]string{"get-state-key1", "get-state-key2", "get-state-key3"})) + }) + + Context("and GetStateMultipleKeys fails", func() { + BeforeEach(func() { + fakeTxSimulator.GetStateMultipleKeysReturns(nil, errors.New("tomato")) + }) + + It("returns the error from GetStateMultipleKeys", func() { + _, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).To(MatchError("tomato")) + }) + }) + + It("returns the response from GetStateMultipleKeys", func() { + resp, err := handler.HandleGetStateMultipleKeys(incomingMessage, txContext) + Expect(err).NotTo(HaveOccurred()) + Expect(resp).To(ProtoEqual(&pb.ChaincodeMessage{ + Type: pb.ChaincodeMessage_RESPONSE, + Payload: respPayload, + Txid: "tx-id", + ChannelId: "channel-id", + })) + }) + }) + }) + Describe("HandleGetPrivateDataHash", func() { var ( incomingMessage *pb.ChaincodeMessage @@ -2786,8 +3135,10 @@ var _ = Describe("Handler", func() { })) chaincodeAdditionalParams := &pb.ChaincodeAdditionalParams{ - UseWriteBatch: true, - MaxSizeWriteBatch: 1000, + UseWriteBatch: true, + MaxSizeWriteBatch: 1000, + UseGetMultipleKeys: true, + MaxSizeGetMultipleKeys: 1000, } payloadBytes, err := proto.Marshal(chaincodeAdditionalParams) Expect(err).NotTo(HaveOccurred()) diff --git a/integration/chaincode/multi/chaincode.go b/integration/chaincode/multi/chaincode.go index 0ca7bf62202..75b4a270b61 100644 --- a/integration/chaincode/multi/chaincode.go +++ b/integration/chaincode/multi/chaincode.go @@ -52,6 +52,11 @@ func (t *Operations) Invoke(stub shim.ChaincodeStubInterface) *pb.Response { return shim.Error("Incorrect number of arguments. Expecting 1") } return t.putPrivateKey(stub, args[1]) + case "get-multiple-keys": + if len(args) != 1 { + return shim.Error("Incorrect number of arguments. Expecting 1") + } + return t.getMultiple(stub, args[0]) default: // error fmt.Println("invoke did not find func: " + function) @@ -59,7 +64,7 @@ func (t *Operations) Invoke(stub shim.ChaincodeStubInterface) *pb.Response { } } -// both params should be marshalled json data and base64 encoded +// put to state keys from "key0" to "key(cntCall-1)" func (t *Operations) put(stub shim.ChaincodeStubInterface, numberCallsPut string) *pb.Response { cntCall, _ := strconv.Atoi(numberCallsPut) @@ -97,3 +102,43 @@ func (t *Operations) putPrivateKey(stub shim.ChaincodeStubInterface, numberCalls } return shim.Success(nil) } + +// getMultiple - get multiple states +func (t *Operations) getMultiple(stub shim.ChaincodeStubInterface, countKeys string) *pb.Response { + num, _ := strconv.Atoi(countKeys) + + keys := make([]string, 0, num) + + keys = append(keys, "non-exist-key") + for i := range num { + key := "key" + strconv.Itoa(i) + keys = append(keys, key) + } + + resps, err := stub.GetMultipleStates(keys...) + if err != nil { + return shim.Error(err.Error()) + } + + if len(resps) != num+1 { + return shim.Error("number of results is not correct") + } + + // non exist key return nil + if resps[0] != nil { + errStr := fmt.Sprintf("incorrect result %d elem, got %v", 0, string(resps[0])) + return shim.Error(errStr) + } + + if string(resps[1]) != "key"+strconv.Itoa(0) { + errStr := fmt.Sprintf("incorrect result %d elem, got %v", 0, string(resps[1])) + return shim.Error(errStr) + } + + if string(resps[num]) != "key"+strconv.Itoa(num-1) { + errStr := fmt.Sprintf("incorrect result %d elem, got %v", 0, string(resps[num])) + return shim.Error(errStr) + } + + return shim.Success(nil) +} diff --git a/integration/e2e/write_batch_test.go b/integration/e2e/write_batch_test.go index 9a94421d507..cc21ec88467 100644 --- a/integration/e2e/write_batch_test.go +++ b/integration/e2e/write_batch_test.go @@ -201,6 +201,85 @@ var _ = Describe("Network", func() { RunInvoke(network, orderer, peer, "put-private-key", true, 3, 1, []string{"collection testchannel/mycc/col", "could not be found"}) }) }) + + DescribeTableSubtree("benchmark get multiple keys", func(desc string, useGetMultipleKeys bool) { + var network *nwo.Network + var ordererRunner *ginkgomon.Runner + var ordererProcess, peerProcess ifrit.Process + + BeforeEach(func() { + network = nwo.New(nwo.BasicEtcdRaft(), tempDir, client, StartPort(), components) + network.UseGetMultipleKeys = useGetMultipleKeys + + // Generate config and bootstrap the network + network.GenerateConfigTree() + network.Bootstrap() + + // Start all the fabric processes + ordererRunner, ordererProcess, peerProcess = network.StartSingleOrdererNetwork("orderer") + }) + + AfterEach(func() { + if ordererProcess != nil { + ordererProcess.Signal(syscall.SIGTERM) + Eventually(ordererProcess.Wait(), network.EventuallyTimeout).Should(Receive()) + } + + if peerProcess != nil { + peerProcess.Signal(syscall.SIGTERM) + Eventually(peerProcess.Wait(), network.EventuallyTimeout).Should(Receive()) + } + + network.Cleanup() + }) + + It("deploys and executes experiment bench", func() { + orderer := network.Orderer("orderer") + channelparticipation.JoinOrdererJoinPeersAppChannel(network, "testchannel", orderer, ordererRunner) + peer := network.Peer("Org1", "peer0") + + chaincode := nwo.Chaincode{ + Name: "mycc", + Version: "0.0", + Path: "github.com/hyperledger/fabric/integration/chaincode/multi/cmd", + Lang: "golang", + PackageFile: filepath.Join(tempDir, "multi.tar.gz"), + Ctor: `{"Args":["init"]}`, + SignaturePolicy: `AND ('Org1MSP.member','Org2MSP.member')`, + Sequence: "1", + InitRequired: true, + Label: "my_multi_operations_chaincode", + } + + network.VerifyMembership(network.PeersWithChannel("testchannel"), "testchannel") + + nwo.EnableCapabilities( + network, + "testchannel", + "Application", "V2_0", + orderer, + network.PeersWithChannel("testchannel")..., + ) + nwo.DeployChaincode(network, "testchannel", orderer, chaincode) + + RunInvoke(network, orderer, peer, "invoke", true, 10000, 0, nil) + + By("run query get state multiple keys") + experiment := gmeasure.NewExperiment("Get state multiple keys " + desc) + AddReportEntry(experiment.Name, experiment) + + experiment.SampleDuration("invoke N-10 cycle-1000", func(idx int) { + RunGetStateMultipleKeys(network, peer, 1000) + }, gmeasure.SamplingConfig{N: 10}) + + experiment.SampleDuration("invoke N-10 cycle-10000", func(idx int) { + RunGetStateMultipleKeys(network, peer, 10000) + }, gmeasure.SamplingConfig{N: 10}) + }) + }, + Entry("without peer support", "without peer support", false), + Entry("with peer support", "with peer support", true), + ) }) func RunInvoke(n *nwo.Network, orderer *nwo.Orderer, peer *nwo.Peer, fn string, startWriteBatch bool, numberCallsPut int, exitCode int, expectedError []string) { @@ -237,3 +316,13 @@ func RunGetState(n *nwo.Network, peer *nwo.Peer, keyUniq string) { Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0)) Expect(sess).To(gbytes.Say("key" + keyUniq)) } + +func RunGetStateMultipleKeys(n *nwo.Network, peer *nwo.Peer, countKeys int) { + sess, err := n.PeerUserSession(peer, "User1", commands.ChaincodeQuery{ + ChannelID: "testchannel", + Name: "mycc", + Ctor: `{"Args":["get-multiple-keys","` + fmt.Sprint(countKeys) + `"]}`, + }) + Expect(err).NotTo(HaveOccurred()) + Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0)) +} diff --git a/integration/nwo/network.go b/integration/nwo/network.go index 06e5758f7e8..67440b2954b 100644 --- a/integration/nwo/network.go +++ b/integration/nwo/network.go @@ -161,6 +161,7 @@ type Network struct { OrdererReplicationPolicy string PeerDeliveryClientPolicy string UseWriteBatch bool + UseGetMultipleKeys bool PortsByOrdererID map[string]Ports PortsByPeerID map[string]Ports @@ -194,6 +195,7 @@ func New(c *Config, rootDir string, dockerClient *docker.Client, startPort int, PortsByPeerID: map[string]Ports{}, PeerDeliveryClientPolicy: "", UseWriteBatch: true, + UseGetMultipleKeys: true, Organizations: c.Organizations, Consensus: c.Consensus, diff --git a/integration/nwo/template/core_template.go b/integration/nwo/template/core_template.go index 8401e09d7e2..c2158c21872 100644 --- a/integration/nwo/template/core_template.go +++ b/integration/nwo/template/core_template.go @@ -190,6 +190,8 @@ chaincode: runtimeParams: useWriteBatch: {{ .UseWriteBatch }} maxSizeWriteBatch: 1000 + useGetMultipleKeys: {{ .UseGetMultipleKeys }} + maxSizeGetMultipleKeys: 1000 logging: level: info shim: warning diff --git a/internal/peer/node/start.go b/internal/peer/node/start.go index 28f345548a4..2e6e5276ea0 100644 --- a/internal/peer/node/start.go +++ b/internal/peer/node/start.go @@ -695,6 +695,8 @@ func serve(args []string) error { UserRunsCC: userRunsCC, UseWriteBatch: chaincodeConfig.UseWriteBatch, MaxSizeWriteBatch: chaincodeConfig.MaxSizeWriteBatch, + UseGetMultipleKeys: chaincodeConfig.UseGetMultipleKeys, + MaxSizeGetMultipleKeys: chaincodeConfig.MaxSizeGetMultipleKeys, } custodianLauncher := custodianLauncherAdapter{ diff --git a/sampleconfig/core.yaml b/sampleconfig/core.yaml index 378b78e0771..6fecc46244d 100644 --- a/sampleconfig/core.yaml +++ b/sampleconfig/core.yaml @@ -661,11 +661,15 @@ chaincode: # RuntimeParams section for parameters that are passed to chaincode # to specify additional operation modes in which the peer operates. - runtimeParams: - # UseWriteBatch an indication that the peer can accept changes from chaincode in batches - useWriteBatch: true - # MaxSizeWriteBatch maximum batch size for the change segment - maxSizeWriteBatch: 1000 + runtimeParams: + # UseWriteBatch an indication that the peer can accept changes from chaincode in batches + useWriteBatch: true + # MaxSizeWriteBatch maximum batch size for the change segment + maxSizeWriteBatch: 1000 + # UseGetMultipleKeys an indication that the peer can handle get multiple keys + useGetMultipleKeys: true + # MaxSizeGetMultipleKeys maximum size of batches with get multiple keys + maxSizeGetMultipleKeys: 1000 ############################################################################### #