Skip to content

Commit

Permalink
[DEV-9154] Private data rollback MVCC_READ_CONFLICT
Browse files Browse the repository at this point in the history
Test to ensure that private data is rolled back on an MVCC_READ_CONFLICT

Change-Id: I577ebfa8ebd63e9023667967771c21acfdd5bec7
Signed-off-by: George Aristy <george.aristy@securekey.com>
  • Loading branch information
George Aristy committed Sep 10, 2018
1 parent dd9c90e commit c8fd21d
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 0 deletions.
Expand Up @@ -9,6 +9,7 @@ package main
import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/hyperledger/fabric/core/chaincode/shim"
Expand All @@ -30,6 +31,7 @@ const (
putBothFunc = "putboth"
getAndPutBothFunc = "getandputboth"
invokeCCFunc = "invokecc"
addToIntFunc = "addToInt"
)

// ExampleCC example chaincode that puts and gets state and private data
Expand Down Expand Up @@ -216,6 +218,43 @@ func (cc *ExampleCC) getAndPutBoth(stub shim.ChaincodeStubInterface, args []stri
return shim.Success(nil)
}

// Adds a given int amount to the value stored in the given private collection's key, storing the result using the same key.
// If the given key does not already exist then it will be added and stored with the given amount.
func (cc *ExampleCC) addToInt(stub shim.ChaincodeStubInterface, args[]string) pb.Response {
if len(args) != 3 {
return shim.Error("Invalid args. Expecting collection, key, amountToAdd")
}

coll := args[0]
key := args[1]
amountToAdd, err := strconv.Atoi(args[2])
if err != nil {
return shim.Error("Invalid arg: amountToAdd is not an int")
}

oldValue, err := stub.GetPrivateData(coll, key)
if err != nil {
return shim.Error(fmt.Sprintf("Error getting private data for collection [%s] and key [%s]: %s", coll, key, err))
}

var oldValueInt int
if oldValue != nil {
oldValueInt, err = strconv.Atoi(string(oldValue))
if err != nil {
return shim.Error(fmt.Sprintf("Error parsing existing amount [%s]: %s", string(oldValue), err))
}
} else {
oldValueInt = 0
}

newValueInt := oldValueInt + amountToAdd
if err := stub.PutPrivateData(coll, key, []byte(strconv.Itoa(newValueInt))); err != nil {
return shim.Error(fmt.Sprintf("Error storing new sum [%s] to key [%s] in private collection [%s]: %s", newValueInt, key, coll, err))
}

return shim.Success(nil)
}

type argStruct struct {
Args []string `json:"Args"`
}
Expand Down Expand Up @@ -259,6 +298,7 @@ func (cc *ExampleCC) initRegistry() {
cc.funcRegistry[putBothFunc] = cc.putBoth
cc.funcRegistry[getAndPutBothFunc] = cc.getAndPutBoth
cc.funcRegistry[invokeCCFunc] = cc.invokeCC
cc.funcRegistry[addToIntFunc] = cc.addToInt
}

func (cc *ExampleCC) functions() []string {
Expand Down
74 changes: 74 additions & 0 deletions test/integration/pkg/client/channel/channel_client_pvt_test.go
Expand Up @@ -9,8 +9,14 @@ SPDX-License-Identifier: Apache-2.0
package channel

import (
"strconv"
"strings"
"sync"
"testing"

"github.com/hyperledger/fabric-sdk-go/pkg/common/errors/multi"
"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/require"

"github.com/hyperledger/fabric-sdk-go/pkg/client/channel"
Expand Down Expand Up @@ -137,6 +143,74 @@ func TestPrivateDataWithOrgDown(t *testing.T) {
})
}

// Data in a private data collection must be left untouched if the client receives an MVCC_READ_CONFLICT error.
// We test this by submitting two cumulative changes to a private data collection, ensuring that the MVCC_READ_CONFLICT error
// is reproduced, then asserting that only one of the changes was applied.
func TestChannelClientRollsBackPvtDataIfMvccReadConflict(t *testing.T) {
orgsContext := setupMultiOrgContext(t, mainSDK)
require.NoError(t, integration.EnsureChannelCreatedAndPeersJoined(t, mainSDK, orgChannelID, "orgchannel.tx", orgsContext))
// private data collection used for test
const coll = "collection1"
// collection key used for test
const key = "collection_key"
ccID := integration.GenerateExamplePvtID(true)
collConfig, err := newCollectionConfig(coll, "OR('Org1MSP.member','Org2MSP.member','Org3MSP.member')", 0, 2, 1000)
require.NoError(t, err)
require.NoError(t, integration.InstallExamplePvtChaincode(orgsContext, ccID))
require.NoError(t, integration.InstantiateExamplePvtChaincode(orgsContext, orgChannelID, ccID, "OR('Org1MSP.member','Org2MSP.member','Org3MSP.member')", collConfig))
ctxProvider := mainSDK.ChannelContext(orgChannelID, fabsdk.WithUser(org1User), fabsdk.WithOrg(org1Name))
chClient, err := channel.New(ctxProvider)
require.NoError(t, err)

var errMtx sync.Mutex
errs := multi.Errors{}
var wg sync.WaitGroup

// test function; invokes a CC function that mutates the private data collection
changePvtData := func(amount int) {
defer wg.Done()
_, err := chClient.Execute(
channel.Request{
ChaincodeID: ccID,
Fcn: "addToInt",
Args: [][]byte{[]byte(coll), []byte(key), []byte(strconv.Itoa(amount))},
},
)
if err != nil {
errMtx.Lock()
errs = append(errs, err)
errMtx.Unlock()
return
}
}

// expected value at the end of the test
const expected = 10

wg.Add(2)
go changePvtData(expected)
go changePvtData(expected)
wg.Wait()

// ensure the MVCC_READ_CONFLICT was reproduced
require.Truef(t, len(errs) > 0 && strings.Contains(errs[0].Error(), "MVCC_READ_CONFLICT"), "could not reproduce MVCC_READ_CONFLICT")

// read current value of private data collection
resp, err := chClient.Query(
channel.Request{
ChaincodeID: ccID,
Fcn: "getprivate",
Args: [][]byte{[]byte(coll), []byte(key)},
},
)
require.NoErrorf(t, err, "error attempting to read private data")

actual, err := strconv.Atoi(string(resp.Payload))
require.NoError(t, err)

assert.Truef(t, actual == expected, "Private data not rolled back during MVCC_READ_CONFLICT")
}

func newCollectionConfig(colName, policy string, reqPeerCount, maxPeerCount int32, blockToLive uint64) (*cb.CollectionConfig, error) {
p, err := cauthdsl.FromString(policy)
if err != nil {
Expand Down

0 comments on commit c8fd21d

Please sign in to comment.