diff --git a/core/endorser/endorser.go b/core/endorser/endorser.go index 290e67ff40d..e4377761d01 100644 --- a/core/endorser/endorser.go +++ b/core/endorser/endorser.go @@ -174,7 +174,7 @@ func (e *Endorser) callChaincode(txParams *ccprovider.TransactionParams, input * } // SimulateProposal simulates the proposal by calling the chaincode -func (e *Endorser) SimulateProposal(txParams *ccprovider.TransactionParams, chaincodeName string, chaincodeInput *pb.ChaincodeInput) (*pb.Response, []byte, *pb.ChaincodeEvent, error) { +func (e *Endorser) simulateProposal(txParams *ccprovider.TransactionParams, chaincodeName string, chaincodeInput *pb.ChaincodeInput) (*pb.Response, []byte, *pb.ChaincodeEvent, *pb.ChaincodeInterest, error) { logger := decorateLogger(endorserLogger, txParams) meterLabels := []string{ @@ -186,11 +186,11 @@ func (e *Endorser) SimulateProposal(txParams *ccprovider.TransactionParams, chai res, ccevent, err := e.callChaincode(txParams, chaincodeInput, chaincodeName) if err != nil { logger.Errorf("failed to invoke chaincode %s, error: %+v", chaincodeName, err) - return nil, nil, nil, err + return nil, nil, nil, nil, err } if txParams.TXSimulator == nil { - return res, nil, ccevent, nil + return res, nil, ccevent, nil, nil } // Note, this is a little goofy, as if there is private data, Done() gets called @@ -201,14 +201,14 @@ func (e *Endorser) SimulateProposal(txParams *ccprovider.TransactionParams, chai simResult, err := txParams.TXSimulator.GetTxSimulationResults() if err != nil { e.Metrics.SimulationFailure.With(meterLabels...).Add(1) - return nil, nil, nil, err + return nil, nil, nil, nil, err } if simResult.PvtSimulationResults != nil { if chaincodeName == "lscc" { // TODO: remove once we can store collection configuration outside of LSCC e.Metrics.SimulationFailure.With(meterLabels...).Add(1) - return nil, nil, nil, errors.New("Private data is forbidden to be used in instantiate") + return nil, nil, nil, nil, errors.New("Private data is forbidden to be used in instantiate") } pvtDataWithConfig, err := AssemblePvtRWSet(txParams.ChannelID, simResult.PvtSimulationResults, txParams.TXSimulator, e.Support.GetDeployedCCInfoProvider()) // To read collection config need to read collection updates before @@ -217,12 +217,12 @@ func (e *Endorser) SimulateProposal(txParams *ccprovider.TransactionParams, chai if err != nil { e.Metrics.SimulationFailure.With(meterLabels...).Add(1) - return nil, nil, nil, errors.WithMessage(err, "failed to obtain collections config") + return nil, nil, nil, nil, errors.WithMessage(err, "failed to obtain collections config") } endorsedAt, err := e.Support.GetLedgerHeight(txParams.ChannelID) if err != nil { e.Metrics.SimulationFailure.With(meterLabels...).Add(1) - return nil, nil, nil, errors.WithMessage(err, fmt.Sprintf("failed to obtain ledger height for channel '%s'", txParams.ChannelID)) + return nil, nil, nil, nil, errors.WithMessage(err, fmt.Sprintf("failed to obtain ledger height for channel '%s'", txParams.ChannelID)) } // Add ledger height at which transaction was endorsed, // `endorsedAt` is obtained from the block storage and at times this could be 'endorsement Height + 1'. @@ -232,17 +232,22 @@ func (e *Endorser) SimulateProposal(txParams *ccprovider.TransactionParams, chai pvtDataWithConfig.EndorsedAt = endorsedAt if err := e.PrivateDataDistributor.DistributePrivateData(txParams.ChannelID, txParams.TxID, pvtDataWithConfig, endorsedAt); err != nil { e.Metrics.SimulationFailure.With(meterLabels...).Add(1) - return nil, nil, nil, err + return nil, nil, nil, nil, err } } + ccInterest, err := e.buildChaincodeInterest(simResult) + if err != nil { + return nil, nil, nil, nil, err + } + pubSimResBytes, err := simResult.GetPubSimulationBytes() if err != nil { e.Metrics.SimulationFailure.With(meterLabels...).Add(1) - return nil, nil, nil, err + return nil, nil, nil, nil, err } - return res, pubSimResBytes, ccevent, nil + return res, pubSimResBytes, ccevent, ccInterest, nil } // preProcess checks the tx proposal headers, uniqueness and ACL @@ -394,7 +399,7 @@ func (e *Endorser) ProcessProposalSuccessfullyOrError(up *UnpackedProposal) (*pb } // 1 -- simulate - res, simulationResult, ccevent, err := e.SimulateProposal(txParams, up.ChaincodeName, up.Input) + res, simulationResult, ccevent, ccInterest, err := e.simulateProposal(txParams, up.ChaincodeName, up.Input) if err != nil { return nil, errors.WithMessage(err, "error in simulation") } @@ -424,6 +429,7 @@ func (e *Endorser) ProcessProposalSuccessfullyOrError(up *UnpackedProposal) (*pb return &pb.ProposalResponse{ Response: res, Payload: prpBytes, + Interest: ccInterest, }, nil case up.ChannelID() == "": // Chaincode invocations without a channel ID is a broken concept @@ -458,9 +464,92 @@ func (e *Endorser) ProcessProposalSuccessfullyOrError(up *UnpackedProposal) (*pb Endorsement: endorsement, Payload: mPrpBytes, Response: res, + Interest: ccInterest, }, nil } +// Using the simulation results, build the ChaincodeInterest structure that the client can pass to the discovery service +// to get the correct endorsement policy for the chaincode(s) and any collections encountered. +func (e *Endorser) buildChaincodeInterest(simResult *ledger.TxSimulationResults) (*pb.ChaincodeInterest, error) { + // build a structure that collates all the information needed for the chaincode interest: + // 1. chaincodes in public RWset that don't have collections + // 2. collections in the private write set + // 3. collections in the PrivateReads struct + chaincodes := map[string]map[string]bool{} // map namespace -> collection -> isRead + + // 1. Add any chaincodes (namespace) from the public RW set + if simResult.PubSimulationResults != nil { + for _, nsrws := range simResult.PubSimulationResults.GetNsRwset() { + if e.Support.IsSysCC(nsrws.Namespace) { + // skip system chaincodes + continue + } + chaincodes[nsrws.Namespace] = nil // no collections (maybe added later) + } + } + + // 2. Add any chaincodes/collections from the private write set + if simResult.PvtSimulationResults != nil { + // list collections that are read from (in order to populate NoPublicReads in ChaincodeInterest) + for _, prws := range simResult.PvtSimulationResults.GetNsPvtRwset() { + if e.Support.IsSysCC(prws.Namespace) { + // skip system chaincodes + continue + } + // build a map of collections that are read from or written to - value is true if read from, false if only written to + collections := map[string]bool{} + for _, cprws := range prws.GetCollectionPvtRwset() { + collections[cprws.CollectionName] = false + } + chaincodes[prws.Namespace] = collections + } + } + + // 3. Add/merge collections from the PrivateReads struct + for ns, entry := range simResult.PrivateReads { + if cc := chaincodes[ns]; cc == nil { + // chaincode either not already in the map, or nil entry - add it + chaincodes[ns] = map[string]bool{} + } + for name := range entry { + // even if this already exists from the write-set, it will override the value with true to indicate it was read + chaincodes[ns][name] = true + } + } + + // now build the chaincode interest + ccInterest := &pb.ChaincodeInterest{} + for chaincode, collections := range chaincodes { + if collections == nil { + ccInterest.Chaincodes = append(ccInterest.Chaincodes, &pb.ChaincodeCall{ + Name: chaincode, + }) + } else { + for collectionName, isRead := range collections { + // Since each collection in a chaincode could have different values of the NoPrivateReads flag, create a new Chaincode entry for each. + ccInterest.Chaincodes = append(ccInterest.Chaincodes, &pb.ChaincodeCall{ + Name: chaincode, + CollectionNames: []string{collectionName}, + NoPrivateReads: !isRead, + }) + } + } + } + + // add the key signature policies - at the moment they are grouped together in the first chaincode + // TODO implement DisregardNamespacePolicy hint in ChaincodeCall + if len(ccInterest.Chaincodes) > 0 { + for _, policyBytes := range simResult.KeySignaturePolicies { + policy, err := protoutil.UnmarshalSignaturePolicy(policyBytes) + if err != nil { + return nil, err + } + ccInterest.Chaincodes[0].KeyPolicies = append(ccInterest.Chaincodes[0].KeyPolicies, policy) + } + } + return ccInterest, nil +} + // determine whether or not a transaction simulator should be // obtained for a proposal. func acquireTxSimulator(chainID string, chaincodeName string) bool { diff --git a/core/endorser/endorser_test.go b/core/endorser/endorser_test.go index e23db8db179..512bd7a1590 100644 --- a/core/endorser/endorser_test.go +++ b/core/endorser/endorser_test.go @@ -1019,4 +1019,209 @@ var _ = Describe("Endorser", func() { Expect(fakeSimulateFailure.AddCallCount()).To(Equal(1)) }) }) + + Context("when building the ChaincodeInterest", func() { + var pvtSimResults *rwset.TxPvtReadWriteSet + var pubSimResults *rwset.TxReadWriteSet + + BeforeEach(func() { + ccPkg := &pb.CollectionConfigPackage{ + Config: []*pb.CollectionConfig{ + { + Payload: &pb.CollectionConfig_StaticCollectionConfig{ + StaticCollectionConfig: &pb.StaticCollectionConfig{ + Name: "myCC", + MemberOrgsPolicy: &pb.CollectionPolicyConfig{ + Payload: &pb.CollectionPolicyConfig_SignaturePolicy{ + SignaturePolicy: &cb.SignaturePolicyEnvelope{}, + }, + }, + }, + }, + }, + }, + } + mockDeployedCCInfoProvider := &ledgermock.DeployedChaincodeInfoProvider{} + mockDeployedCCInfoProvider.AllCollectionsConfigPkgReturns(ccPkg, nil) + fakeSupport.GetDeployedCCInfoProviderReturns(mockDeployedCCInfoProvider) + + pubSimResults = &rwset.TxReadWriteSet{ + DataModel: rwset.TxReadWriteSet_KV, + NsRwset: []*rwset.NsReadWriteSet{ + { + Namespace: "myCC", + Rwset: []byte("public RW set"), + }, + }, + } + + pvtSimResults = &rwset.TxPvtReadWriteSet{ + DataModel: rwset.TxReadWriteSet_KV, + NsPvtRwset: []*rwset.NsPvtReadWriteSet{ + { + Namespace: "myCC", + CollectionPvtRwset: []*rwset.CollectionPvtReadWriteSet{ + { + CollectionName: "mycollection-1", + Rwset: []byte("private RW set"), + }, + }, + }, + }, + } + }) + + It("add private collection which gets read", func() { + privateReads := ledger.PrivateReads{} + privateReads.Add("myCC", "mycollection-1") + + fakeTxSimulator.GetTxSimulationResultsReturns( + &ledger.TxSimulationResults{ + PubSimulationResults: &rwset.TxReadWriteSet{}, + PvtSimulationResults: pvtSimResults, + PrivateReads: privateReads, + }, + nil, + ) + + proposalResponse, err := e.ProcessProposal(context.TODO(), signedProposal) + Expect(err).NotTo(HaveOccurred()) + Expect(proposalResponse.Interest).To(Equal(&pb.ChaincodeInterest{ + Chaincodes: []*pb.ChaincodeCall{{ + Name: "myCC", + CollectionNames: []string{"mycollection-1"}, + }}, + })) + }) + + It("add private collection which gets read, but not written", func() { + privateReads := ledger.PrivateReads{} + privateReads.Add("myCC", "mycollection-1") + + fakeTxSimulator.GetTxSimulationResultsReturns( + &ledger.TxSimulationResults{ + PubSimulationResults: &rwset.TxReadWriteSet{}, + PvtSimulationResults: &rwset.TxPvtReadWriteSet{}, + PrivateReads: privateReads, + }, + nil, + ) + + proposalResponse, err := e.ProcessProposal(context.TODO(), signedProposal) + Expect(err).NotTo(HaveOccurred()) + Expect(proposalResponse.Interest).To(Equal(&pb.ChaincodeInterest{ + Chaincodes: []*pb.ChaincodeCall{{ + Name: "myCC", + CollectionNames: []string{"mycollection-1"}, + }}, + })) + }) + + It("add private collection which is not read", func() { + privateReads := ledger.PrivateReads{} + + fakeTxSimulator.GetTxSimulationResultsReturns( + &ledger.TxSimulationResults{ + PubSimulationResults: &rwset.TxReadWriteSet{}, + PvtSimulationResults: pvtSimResults, + PrivateReads: privateReads, + }, + nil, + ) + + proposalResponse, err := e.ProcessProposal(context.TODO(), signedProposal) + Expect(err).NotTo(HaveOccurred()) + Expect(proposalResponse.Interest).To(Equal(&pb.ChaincodeInterest{ + Chaincodes: []*pb.ChaincodeCall{{ + Name: "myCC", + CollectionNames: []string{"mycollection-1"}, + NoPrivateReads: true, + }}, + })) + }) + + It("add private collection and SBE", func() { + privateReads := ledger.PrivateReads{} + sbe1 := &cb.SignaturePolicyEnvelope{ + Rule: &cb.SignaturePolicy{ + Type: &cb.SignaturePolicy_SignedBy{SignedBy: 0}, + }, + } + sbe1Bytes, err := proto.Marshal(sbe1) + Expect(err).NotTo(HaveOccurred()) + + sbe2 := &cb.SignaturePolicyEnvelope{ + Rule: &cb.SignaturePolicy{ + Type: &cb.SignaturePolicy_SignedBy{SignedBy: 1}, + }, + } + sbe2Bytes, err := proto.Marshal(sbe2) + Expect(err).NotTo(HaveOccurred()) + + fakeTxSimulator.GetTxSimulationResultsReturns( + &ledger.TxSimulationResults{ + PubSimulationResults: &rwset.TxReadWriteSet{}, + PvtSimulationResults: pvtSimResults, + PrivateReads: privateReads, + KeySignaturePolicies: [][]byte{sbe1Bytes, sbe2Bytes}, + }, + nil, + ) + + proposalResponse, err := e.ProcessProposal(context.TODO(), signedProposal) + Expect(err).NotTo(HaveOccurred()) + + Expect(proto.Equal( + proposalResponse.Interest, + &pb.ChaincodeInterest{ + Chaincodes: []*pb.ChaincodeCall{{ + Name: "myCC", + CollectionNames: []string{"mycollection-1"}, + NoPrivateReads: true, + KeyPolicies: []*cb.SignaturePolicyEnvelope{sbe1, sbe2}, + }}, + }, + )).To(BeTrue()) + }) + + It("add SBE only", func() { + sbe1 := &cb.SignaturePolicyEnvelope{ + Rule: &cb.SignaturePolicy{ + Type: &cb.SignaturePolicy_SignedBy{SignedBy: 0}, + }, + } + sbe1Bytes, err := proto.Marshal(sbe1) + Expect(err).NotTo(HaveOccurred()) + + sbe2 := &cb.SignaturePolicyEnvelope{ + Rule: &cb.SignaturePolicy{ + Type: &cb.SignaturePolicy_SignedBy{SignedBy: 1}, + }, + } + sbe2Bytes, err := proto.Marshal(sbe2) + Expect(err).NotTo(HaveOccurred()) + + fakeTxSimulator.GetTxSimulationResultsReturns( + &ledger.TxSimulationResults{ + PubSimulationResults: pubSimResults, + PvtSimulationResults: &rwset.TxPvtReadWriteSet{}, + KeySignaturePolicies: [][]byte{sbe1Bytes, sbe2Bytes}, + }, + nil, + ) + + proposalResponse, err := e.ProcessProposal(context.TODO(), signedProposal) + Expect(err).NotTo(HaveOccurred()) + + Expect(proto.Equal( + proposalResponse.Interest, + &pb.ChaincodeInterest{ + Chaincodes: []*pb.ChaincodeCall{{ + Name: "myCC", + KeyPolicies: []*cb.SignaturePolicyEnvelope{sbe1, sbe2}, + }}, + }, + )).To(BeTrue()) + }) + }) }) diff --git a/core/ledger/kvledger/txmgmt/txmgr/query_executor.go b/core/ledger/kvledger/txmgmt/txmgr/query_executor.go index 32891b2679a..6c7c158113c 100644 --- a/core/ledger/kvledger/txmgmt/txmgr/query_executor.go +++ b/core/ledger/kvledger/txmgmt/txmgr/query_executor.go @@ -35,6 +35,7 @@ type queryExecutor struct { doneInvoked bool hasher rwsetutil.HashFunc txid string + privateReads *ledger.PrivateReads } func newQueryExecutor(txmgr *LockBasedTxMgr, @@ -53,6 +54,7 @@ func newQueryExecutor(txmgr *LockBasedTxMgr, qe.hasher = hashFunc validator := newCollNameValidator(txmgr.ledgerid, txmgr.ccInfoProvider, qe, !performCollCheck) qe.collNameValidator = validator + qe.privateReads = &ledger.PrivateReads{} return qe } @@ -219,6 +221,7 @@ func (q *queryExecutor) GetPrivateData(ns, coll, key string) ([]byte, error) { } if q.collectReadset { q.rwsetBuilder.AddToHashedReadSet(ns, coll, key, ver) + q.privateReads.Add(ns, coll) } return val, nil } @@ -319,6 +322,7 @@ func (q *queryExecutor) GetPrivateDataMultipleKeys(ns, coll string, keys []strin val, _, ver := decomposeVersionedValue(versionedValue) if q.collectReadset { q.rwsetBuilder.AddToHashedReadSet(ns, coll, keys[i], ver) + q.privateReads.Add(ns, coll) } values[i] = val } diff --git a/core/ledger/kvledger/txmgmt/txmgr/query_executor_test.go b/core/ledger/kvledger/txmgmt/txmgr/query_executor_test.go index cf1f8876421..af97a725bed 100644 --- a/core/ledger/kvledger/txmgmt/txmgr/query_executor_test.go +++ b/core/ledger/kvledger/txmgmt/txmgr/query_executor_test.go @@ -110,7 +110,7 @@ func testPrivateDataMetadataRetrievalByHash(t *testing.T, env testEnv) { require.NoError(t, s1.SetPrivateData("ns", "coll", key1, value1)) require.NoError(t, s1.SetPrivateDataMetadata("ns", "coll", key1, metadata1)) s1.Done() - blkAndPvtdata1 := prepareNextBlockForTestFromSimulator(t, bg, s1) + blkAndPvtdata1, _ := prepareNextBlockForTestFromSimulator(t, bg, s1) _, _, err := txMgr.ValidateAndPrepare(blkAndPvtdata1, true) require.NoError(t, err) require.NoError(t, txMgr.Commit()) diff --git a/core/ledger/kvledger/txmgmt/txmgr/tx_simulator.go b/core/ledger/kvledger/txmgmt/txmgr/tx_simulator.go index 69432907491..ac41b8bc4f2 100644 --- a/core/ledger/kvledger/txmgmt/txmgr/tx_simulator.go +++ b/core/ledger/kvledger/txmgmt/txmgr/tx_simulator.go @@ -7,9 +7,12 @@ SPDX-License-Identifier: Apache-2.0 package txmgr import ( + "github.com/hyperledger/fabric-protos-go/peer" commonledger "github.com/hyperledger/fabric/common/ledger" "github.com/hyperledger/fabric/core/ledger" "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/rwsetutil" + "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/statemetadata" + "github.com/hyperledger/fabric/core/ledger/util" "github.com/pkg/errors" ) @@ -21,13 +24,14 @@ type txSimulator struct { pvtdataQueriesPerformed bool simulationResultsComputed bool paginatedQueriesPerformed bool + keySignaturePolicies map[string]struct{} } func newTxSimulator(txmgr *LockBasedTxMgr, txid string, hashFunc rwsetutil.HashFunc) (*txSimulator, error) { rwsetBuilder := rwsetutil.NewRWSetBuilder() qe := newQueryExecutor(txmgr, txid, rwsetBuilder, true, hashFunc) logger.Debugf("constructing new tx simulator txid = [%s]", txid) - return &txSimulator{qe, rwsetBuilder, false, false, false, false}, nil + return &txSimulator{qe, rwsetBuilder, false, false, false, false, map[string]struct{}{}}, nil } // SetState implements method in interface `ledger.TxSimulator` @@ -36,6 +40,39 @@ func (s *txSimulator) SetState(ns string, key string, value []byte) error { return err } s.rwsetBuilder.AddToWriteSet(ns, key, value) + // if this has a key level signature policy, add it to the interest + return s.checkKeySignaturePolicy(ns, key) +} + +// If this key has a SBE policy, add that policy to the set +func (s *txSimulator) checkKeySignaturePolicy(ns string, key string) error { + var metadata []byte + var err error + if metadata, err = s.txmgr.db.GetStateMetadata(ns, key); err != nil { + return err + } + return s.addToKeySignaturePolicies(metadata) +} + +// If this private collection key has a SBE policy, add that policy to the set +func (s *txSimulator) checkPrivateKeySignaturePolicy(ns string, coll string, key string) error { + metadata, err := s.txmgr.db.GetPrivateDataMetadataByHash(ns, coll, util.ComputeStringHash(key)) + if err != nil { + return err + } + return s.addToKeySignaturePolicies(metadata) +} + +func (s *txSimulator) addToKeySignaturePolicies(metadata []byte) error { + if metadata != nil { + sm, err := statemetadata.Deserialize(metadata) + if err != nil { + return err + } + if policy, ok := sm[peer.MetaDataKeys_VALIDATION_PARAMETER.String()]; ok { + s.keySignaturePolicies[string(policy)] = struct{}{} + } + } return nil } @@ -60,7 +97,7 @@ func (s *txSimulator) SetStateMetadata(namespace, key string, metadata map[strin return err } s.rwsetBuilder.AddToMetadataWriteSet(namespace, key, metadata) - return nil + return s.checkKeySignaturePolicy(namespace, key) } // DeleteStateMetadata implements method in interface `ledger.TxSimulator` @@ -78,7 +115,7 @@ func (s *txSimulator) SetPrivateData(ns, coll, key string, value []byte) error { } s.writePerformed = true s.rwsetBuilder.AddToPvtAndHashedWriteSet(ns, coll, key, value) - return nil + return s.checkPrivateKeySignaturePolicy(ns, coll, key) } // DeletePrivateData implements method in interface `ledger.TxSimulator` @@ -113,7 +150,7 @@ func (s *txSimulator) SetPrivateDataMetadata(namespace, collection, key string, return err } s.rwsetBuilder.AddToHashedMetadataWriteSet(namespace, collection, key, metadata) - return nil + return s.checkPrivateKeySignaturePolicy(namespace, collection, key) } // DeletePrivateMetadata implements method in interface `ledger.TxSimulator` @@ -157,7 +194,16 @@ func (s *txSimulator) GetTxSimulationResults() (*ledger.TxSimulationResults, err return nil, s.queryExecutor.err } s.queryExecutor.addRangeQueryInfo() - return s.rwsetBuilder.GetTxSimulationResults() + simResults, err := s.rwsetBuilder.GetTxSimulationResults() + if err != nil { + return nil, err + } + // The PrivateReads map needs to be cloned so that subsequent RW set additions don't modify these TX simulation results + simResults.PrivateReads = s.privateReads.Clone() + for policy := range s.keySignaturePolicies { + simResults.KeySignaturePolicies = append(simResults.KeySignaturePolicies, []byte(policy)) + } + return simResults, nil } // ExecuteUpdate implements method in interface `ledger.TxSimulator` diff --git a/core/ledger/kvledger/txmgmt/txmgr/txmgr_test.go b/core/ledger/kvledger/txmgmt/txmgr/txmgr_test.go index 108f02485f3..545ee200015 100644 --- a/core/ledger/kvledger/txmgmt/txmgr/txmgr_test.go +++ b/core/ledger/kvledger/txmgmt/txmgr/txmgr_test.go @@ -14,6 +14,8 @@ import ( "strings" "testing" + "github.com/hyperledger/fabric-protos-go/peer" + "github.com/golang/protobuf/proto" "github.com/hyperledger/fabric-protos-go/ledger/queryresult" "github.com/hyperledger/fabric-protos-go/ledger/rwset" @@ -88,6 +90,10 @@ func TestTxSimulatorGetResults(t *testing.T) { simulationResults1, err := simulator.GetTxSimulationResults() require.NoError(t, err) require.Len(t, simulationResults1.PubSimulationResults.NsRwset, 1) + // verify the Private Read has been captured + expectedPrivateReads := ledger.PrivateReads{} + expectedPrivateReads.Add("ns1", "coll1") + require.Equal(t, expectedPrivateReads, simulationResults1.PrivateReads) // clone freeze simulationResults1 buff1 := new(bytes.Buffer) require.NoError(t, gob.NewEncoder(buff1).Encode(simulationResults1)) @@ -1493,7 +1499,7 @@ func testTxWithPvtdataMetadata(t *testing.T, env testEnv, ns, coll string) { require.NoError(t, s1.SetPrivateDataMetadata(ns, coll, key3, metadata3)) s1.Done() - blkAndPvtdata1 := prepareNextBlockForTestFromSimulator(t, bg, s1) + blkAndPvtdata1, _ := prepareNextBlockForTestFromSimulator(t, bg, s1) _, _, err := txMgr.ValidateAndPrepare(blkAndPvtdata1, true) require.NoError(t, err) require.NoError(t, txMgr.Commit()) @@ -1512,7 +1518,7 @@ func testTxWithPvtdataMetadata(t *testing.T, env testEnv, ns, coll string) { require.NoError(t, s2.DeletePrivateDataMetadata(ns, coll, key2)) s2.Done() - blkAndPvtdata2 := prepareNextBlockForTestFromSimulator(t, bg, s2) + blkAndPvtdata2, _ := prepareNextBlockForTestFromSimulator(t, bg, s2) _, _, err = txMgr.ValidateAndPrepare(blkAndPvtdata2, true) require.NoError(t, err) require.NoError(t, txMgr.Commit()) @@ -1538,17 +1544,18 @@ func prepareNextBlockForTest(t *testing.T, txMgr *LockBasedTxMgr, bg *testutil.B if isMissing { return prepareNextBlockForTestFromSimulatorWithMissingData(t, bg, simulator, txid, 1, "ns", "coll", true) } - return prepareNextBlockForTestFromSimulator(t, bg, simulator) + nb, _ := prepareNextBlockForTestFromSimulator(t, bg, simulator) + return nb } -func prepareNextBlockForTestFromSimulator(t *testing.T, bg *testutil.BlockGenerator, simulator ledger.TxSimulator) *ledger.BlockAndPvtData { +func prepareNextBlockForTestFromSimulator(t *testing.T, bg *testutil.BlockGenerator, simulator ledger.TxSimulator) (*ledger.BlockAndPvtData, *ledger.TxSimulationResults) { simRes, _ := simulator.GetTxSimulationResults() pubSimBytes, _ := simRes.GetPubSimulationBytes() block := bg.NextBlock([][]byte{pubSimBytes}) return &ledger.BlockAndPvtData{ Block: block, PvtData: ledger.TxPvtDataMap{0: {SeqInBlock: 0, WriteSet: simRes.PvtSimulationResults}}, - } + }, simRes } func prepareNextBlockForTestFromSimulatorWithMissingData(t *testing.T, bg *testutil.BlockGenerator, simulator ledger.TxSimulator, @@ -1592,3 +1599,122 @@ func TestName(t *testing.T) { txMgr := testEnv.getTxMgr() require.Equal(t, "state", txMgr.Name()) } + +func TestTxSimulatorWithStateBasedEndorsement(t *testing.T) { + for _, testEnv := range testEnvs { + t.Run(testEnv.getName(), func(t *testing.T) { + testEnv.init(t, "testtxsimulatorwithdtstebasedendorsement", nil) + testTxSimulatorWithStateBasedEndorsement(t, testEnv) + testEnv.cleanup() + }) + } +} + +func testTxSimulatorWithStateBasedEndorsement(t *testing.T, env testEnv) { + txMgr := env.getTxMgr() + txMgrHelper := newTxMgrTestHelper(t, txMgr) + sbe1 := []byte("SBE1") + sbe2 := []byte("SBE2") + + // simulate tx1 + s1, _ := txMgr.NewTxSimulator("test_tx1") + require.NoError(t, s1.SetState("ns1", "key1", []byte("value1"))) + require.NoError(t, s1.SetState("ns1", "key2", []byte("value2"))) + require.NoError(t, s1.SetStateMetadata("ns1", "key2", map[string][]byte{peer.MetaDataKeys_VALIDATION_PARAMETER.String(): sbe1})) + require.NoError(t, s1.SetState("ns2", "key3", []byte("value3"))) + require.NoError(t, s1.SetStateMetadata("ns2", "key3", map[string][]byte{peer.MetaDataKeys_VALIDATION_PARAMETER.String(): sbe2})) + require.NoError(t, s1.SetState("ns2", "key4", []byte("value4"))) + s1.Done() + // validate and commit RWset + txRWSet1, _ := s1.GetTxSimulationResults() + txMgrHelper.validateAndCommitRWSet(txRWSet1.PubSimulationResults) + + // simulate tx2 that make changes to existing data and updates a key policy + s2, _ := txMgr.NewTxSimulator("test_tx2") + sbe3 := []byte("SBE3") + require.NoError(t, s2.SetState("ns1", "key1", []byte("value1b"))) + require.NoError(t, s2.SetState("ns1", "key2", []byte("value2b"))) + require.NoError(t, s2.SetStateMetadata("ns2", "key3", map[string][]byte{peer.MetaDataKeys_VALIDATION_PARAMETER.String(): sbe3})) + require.NoError(t, s2.SetState("ns2", "key4", []byte("value4b"))) + s2.Done() + // validate and commit RWset for tx2 + txRWSet2, err := s2.GetTxSimulationResults() + require.NoError(t, err) + txMgrHelper.validateAndCommitRWSet(txRWSet2.PubSimulationResults) + // check the SBE policies are captured + require.ElementsMatch(t, [][]byte{sbe1, sbe2}, txRWSet2.KeySignaturePolicies) + + // simulate tx3 + s3, _ := txMgr.NewTxSimulator("test_tx3") + require.NoError(t, s3.SetState("ns1", "key1", []byte("value1c"))) + require.NoError(t, s3.SetState("ns1", "key2", []byte("value2c"))) + require.NoError(t, s3.SetState("ns2", "key3", []byte("value3c"))) + require.NoError(t, s3.SetState("ns2", "key4", []byte("value4c"))) + s3.Done() + + // check the updated SBE policies are captured + txRWSet3, err := s3.GetTxSimulationResults() + require.NoError(t, err) + require.ElementsMatch(t, [][]byte{sbe1, sbe3}, txRWSet3.KeySignaturePolicies) +} + +func TestTxSimulatorWithPrivateDataStateBasedEndorsement(t *testing.T) { + ledgerid, ns, coll := "testtxwithprivatedatastatebasedendorsement", "ns1", "coll1" + btlPolicy := btltestutil.SampleBTLPolicy( + map[[2]string]uint64{ + {ns, coll}: 1000, + }, + ) + for _, testEnv := range testEnvs { + t.Logf("Running test for TestEnv = %s", testEnv.getName()) + testEnv.init(t, ledgerid, btlPolicy) + testTxSimulatorWithPrivateDataStateBasedEndorsement(t, testEnv, ns, coll) + testEnv.cleanup() + } +} + +func testTxSimulatorWithPrivateDataStateBasedEndorsement(t *testing.T, env testEnv, ns, coll string) { + ledgerid := "testtxwithprivatedatastatebasedendorsement" + txMgr := env.getTxMgr() + bg, _ := testutil.NewBlockGenerator(t, ledgerid, false) + + populateCollConfigForTest(t, txMgr, []collConfigkey{{ns, coll}}, version.NewHeight(1, 1)) + + sbe1 := []byte("SBE1") + sbe2 := []byte("SBE2") + // Simulate and commit tx1 - set private data and key policy + s1, _ := txMgr.NewTxSimulator("test_tx1") + require.NoError(t, s1.SetPrivateData(ns, coll, "key1", []byte("private_value1"))) + require.NoError(t, s1.SetPrivateDataMetadata(ns, coll, "key1", map[string][]byte{peer.MetaDataKeys_VALIDATION_PARAMETER.String(): sbe1})) + s1.Done() + + blkAndPvtdata1, _ := prepareNextBlockForTestFromSimulator(t, bg, s1) + _, _, err := txMgr.ValidateAndPrepare(blkAndPvtdata1, true) + require.NoError(t, err) + require.NoError(t, txMgr.Commit()) + + // simulate tx2 that make changes to existing data and updates a key policy + s2, _ := txMgr.NewTxSimulator("test_tx2") + require.NoError(t, s2.SetPrivateData(ns, coll, "key1", []byte("private_value2"))) + require.NoError(t, s2.SetPrivateDataMetadata(ns, coll, "key1", map[string][]byte{peer.MetaDataKeys_VALIDATION_PARAMETER.String(): sbe2})) + s2.Done() + + blkAndPvtdata2, simRes2 := prepareNextBlockForTestFromSimulator(t, bg, s2) + _, _, err = txMgr.ValidateAndPrepare(blkAndPvtdata2, true) + require.NoError(t, err) + require.NoError(t, txMgr.Commit()) + // check the SBE policies are captured + require.ElementsMatch(t, [][]byte{sbe1}, simRes2.KeySignaturePolicies) + + // simulate tx3 that make changes to existing data + s3, _ := txMgr.NewTxSimulator("test_tx3") + require.NoError(t, s3.SetPrivateData(ns, coll, "key1", []byte("private_value2"))) + s3.Done() + + blkAndPvtdata3, simRes3 := prepareNextBlockForTestFromSimulator(t, bg, s3) + _, _, err = txMgr.ValidateAndPrepare(blkAndPvtdata3, true) + require.NoError(t, err) + require.NoError(t, txMgr.Commit()) + // check the SBE policies are captured + require.ElementsMatch(t, [][]byte{sbe2}, simRes3.KeySignaturePolicies) +} diff --git a/core/ledger/ledger_interface.go b/core/ledger/ledger_interface.go index 30a3892eba1..150c5f8fd2c 100644 --- a/core/ledger/ledger_interface.go +++ b/core/ledger/ledger_interface.go @@ -470,10 +470,34 @@ func (filter PvtNsCollFilter) Has(ns string, coll string) bool { return collFilter[coll] } +// PrivateReads captures which private data collections are read during TX simulation. +type PrivateReads map[string]map[string]struct{} + +// Add a collection to the set of private data collections that are read by the chaincode. +func (pr PrivateReads) Add(ns, coll string) { + if _, ok := pr[ns]; !ok { + pr[ns] = map[string]struct{}{} + } + pr[ns][coll] = struct{}{} +} + +// Clone returns a copy of this struct. +func (pr PrivateReads) Clone() PrivateReads { + clone := PrivateReads{} + for ns, v := range pr { + for coll := range v { + clone.Add(ns, coll) + } + } + return clone +} + // TxSimulationResults captures the details of the simulation results type TxSimulationResults struct { PubSimulationResults *rwset.TxReadWriteSet PvtSimulationResults *rwset.TxPvtReadWriteSet + PrivateReads PrivateReads + KeySignaturePolicies [][]byte } // GetPubSimulationBytes returns the serialized bytes of public readwrite set diff --git a/protoutil/unmarshalers.go b/protoutil/unmarshalers.go index be1819b4b4a..cc634af60dc 100644 --- a/protoutil/unmarshalers.go +++ b/protoutil/unmarshalers.go @@ -156,6 +156,13 @@ func UnmarshalChaincodeProposalPayload(bytes []byte) (*peer.ChaincodeProposalPay return cpp, errors.Wrap(err, "error unmarshalling ChaincodeProposalPayload") } +// UnmarshalSignaturePolicy unmarshals bytes to a SignaturePolicyEnvelope +func UnmarshalSignaturePolicy(bytes []byte) (*common.SignaturePolicyEnvelope, error) { + sp := &common.SignaturePolicyEnvelope{} + err := proto.Unmarshal(bytes, sp) + return sp, errors.Wrap(err, "error unmarshalling SignaturePolicyEnvelope") +} + // UnmarshalPayloadOrPanic unmarshals bytes to a Payload structure or panics // on error func UnmarshalPayloadOrPanic(encoded []byte) *common.Payload {