/* Copyright 2022 IBM All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ package gateway import ( "math" "testing" "github.com/hyperledger/fabric-protos-go/common" "github.com/hyperledger/fabric-protos-go/ledger/rwset" "github.com/hyperledger/fabric-protos-go/ledger/rwset/kvrwset" "github.com/hyperledger/fabric-protos-go/msp" "github.com/hyperledger/fabric-protos-go/peer" "github.com/stretchr/testify/require" ) type readT struct { namespace string key string block uint64 } type writeT struct { namespace string key string value []byte } type metaWriteT struct { namespace string key string name string value []byte } type pvtCollectionT struct { namespace string collection string hash []byte } type responseT struct { status int32 message string payload []byte } type eventT struct { namespace string name string payload []byte txid string } func TestPayloadDifferenceReadVersion(t *testing.T) { rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{ {namespace: "ns1", key: "key1", block: 4}, {namespace: "ns1", key: "key2", block: 4}, }, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{ {namespace: "ns1", key: "key1", block: 4}, {namespace: "ns1", key: "key2", block: 5}, }, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "read value mismatch", "namespace", "ns1", "key", "key2", "initial-endorser-value", "4", "invoked-endorser-value", "5"}, } require.ElementsMatch(t, expected, diff.details()) } func TestPayloadDifferenceReadMissing(t *testing.T) { rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{ {namespace: "ns1", key: "key1", block: 4}, {namespace: "ns1", key: "key2", block: 4}, }, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{ {namespace: "ns1", key: "key1", block: 4}, }, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "missing read", "namespace", "ns1", "key", "key2", "initial-endorser-value", "4", "invoked-endorser-value", "0"}, } require.ElementsMatch(t, expected, diff.details()) } func TestPayloadDifferenceReadExtra(t *testing.T) { rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{ {namespace: "ns1", key: "key1", block: 4}, {namespace: "ns1", key: "key2", block: 5}, {namespace: "ns2", key: "key1b", block: 4}, {namespace: "ns2", key: "key2b", block: 5}, }, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{ {namespace: "ns1", key: "key1", block: 4}, {namespace: "ns1", key: "key2", block: 5}, {namespace: "ns1", key: "key3", block: 3}, {namespace: "ns2", key: "key1b", block: 4}, {namespace: "ns2", key: "key2b", block: 5}, {namespace: "ns2", key: "key3b", block: 5}, }, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "extraneous read", "namespace", "ns1", "key", "key3", "initial-endorser-value", "0", "invoked-endorser-value", "3"}, {"type", "extraneous read", "namespace", "ns2", "key", "key3b", "initial-endorser-value", "0", "invoked-endorser-value", "5"}, } require.ElementsMatch(t, expected, diff.details()) } func TestPayloadDifferenceReadMissingProtos(t *testing.T) { rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{ {namespace: "ns1", key: "key1", block: math.MaxInt64}, {namespace: "ns1"}, }, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{ {namespace: "ns1", key: "key1", block: 4}, }, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "extraneous read", "namespace", "ns1", "key", "key1", "initial-endorser-value", "0", "invoked-endorser-value", "4"}, {"type", "extraneous read", "namespace", "ns1", "key", "", "initial-endorser-value", "0", "invoked-endorser-value", "0"}, } require.ElementsMatch(t, expected, diff.details()) } func TestPayloadDifferenceWriteValue(t *testing.T) { rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{ {namespace: "ns1", key: "key1", value: []byte("value1")}, {namespace: "ns1", key: "key2", value: []byte("value2")}, {namespace: "ns1", key: "key3", value: []byte("value3")}, }, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{ {namespace: "ns1", key: "key1", value: []byte("value1")}, {namespace: "ns1", key: "key2", value: []byte("value3")}, {namespace: "ns1", key: "key4", value: []byte("value4")}, }, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "write value mismatch", "namespace", "ns1", "key", "key2", "initial-endorser-value", "value2", "invoked-endorser-value", "value3"}, {"type", "missing write", "namespace", "ns1", "key", "key3", "initial-endorser-value", "value3", "invoked-endorser-value", ""}, {"type", "extraneous write", "namespace", "ns1", "key", "key4", "initial-endorser-value", "", "invoked-endorser-value", "value4"}, } require.ElementsMatch(t, expected, diff.details()) } func TestPayloadDifferenceMetadata(t *testing.T) { rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{}, []*metaWriteT{ {namespace: "ns1", key: "key1", name: "meta1", value: []byte("value1")}, {namespace: "ns2", key: "key2", name: "meta2", value: []byte("mv1")}, }, []*pvtCollectionT{}, nil, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{}, []*metaWriteT{ {namespace: "ns1", key: "key1", name: "meta1", value: []byte("value2")}, {namespace: "ns3", key: "key2", name: "meta2", value: []byte("mv1")}, }, []*pvtCollectionT{}, nil, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "write metadata mismatch", "namespace", "ns1", "key", "key1", "name", "meta1", "initial-endorser-value", "value1", "invoked-endorser-value", "value2"}, {"type", "missing metadata write", "namespace", "ns2", "key", "key2", "name", "meta2", "initial-endorser-value", "mv1", "invoked-endorser-value", ""}, {"type", "extraneous metadata write", "namespace", "ns3", "key", "key2", "name", "meta2", "initial-endorser-value", "", "invoked-endorser-value", "mv1"}, } require.ElementsMatch(t, expected, diff.details()) } func TestPayloadDifferenceSBEPolicy(t *testing.T) { sbe1 := &common.SignaturePolicyEnvelope{ Rule: &common.SignaturePolicy{ Type: &common.SignaturePolicy_NOutOf_{ NOutOf: &common.SignaturePolicy_NOutOf{ N: 1, Rules: []*common.SignaturePolicy{ {Type: &common.SignaturePolicy_SignedBy{SignedBy: 0}}, }, }, }, }, Identities: []*msp.MSPPrincipal{ {Principal: []byte("orgA")}, }, } sbe2 := &common.SignaturePolicyEnvelope{ Rule: &common.SignaturePolicy{ Type: &common.SignaturePolicy_NOutOf_{ NOutOf: &common.SignaturePolicy_NOutOf{ N: 1, Rules: []*common.SignaturePolicy{ {Type: &common.SignaturePolicy_SignedBy{SignedBy: 0}}, }, }, }, }, Identities: []*msp.MSPPrincipal{ {Principal: []byte("orgB")}, }, } rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{}, []*metaWriteT{ {namespace: "ns1", key: "key1", name: "VALIDATION_PARAMETER", value: marshal(sbe1, t)}, {namespace: "ns1", key: "key2", name: "VALIDATION_PARAMETER", value: marshal(sbe1, t)}, }, []*pvtCollectionT{}, nil, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{}, []*metaWriteT{ {namespace: "ns1", key: "key1", name: "VALIDATION_PARAMETER", value: marshal(sbe2, t)}, }, []*pvtCollectionT{}, nil, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "write metadata mismatch (SBE policy)", "namespace", "ns1", "key", "key1", "name", "VALIDATION_PARAMETER", "initial-endorser-value", "rule: > > identities: ", "invoked-endorser-value", "rule: > > identities: "}, {"type", "missing metadata write (SBE policy)", "namespace", "ns1", "key", "key2", "name", "VALIDATION_PARAMETER", "initial-endorser-value", "rule: > > identities: ", "invoked-endorser-value", ""}, } require.ElementsMatch(t, expected, diff.details()) } func TestPayloadDifferenceChaincodeResponse(t *testing.T) { rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value1"), status: 200, message: "no error"}, []*readT{}, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value2"), status: 200, message: "no error"}, []*readT{}, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, nil, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "chaincode response mismatch", "initial-endorser-response", "status: 200, message: no error, payload: my_value1", "invoked-endorser-response", "status: 200, message: no error, payload: my_value2"}, } require.ElementsMatch(t, expected, diff.details()) } func TestPayloadDifferencePrivateData(t *testing.T) { rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{ {namespace: "ns1", key: "key1", value: []byte("value1")}, }, []*metaWriteT{}, []*pvtCollectionT{ {namespace: "ns1", collection: "collection1", hash: []byte{1, 2, 3}}, }, nil, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{ {namespace: "ns1", key: "key1", value: []byte("value1")}, }, []*metaWriteT{}, []*pvtCollectionT{ {namespace: "ns1", collection: "collection1", hash: []byte{4, 5, 6}}, }, nil, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "private collection hash mismatch", "namespace", "ns1", "collection", "collection1", "initial-endorser-hash", "010203", "invoked-endorser-hash", "040506"}, } require.ElementsMatch(t, expected, diff.details()) } func TestPayloadDifferenceEvent(t *testing.T) { rpl1 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, &eventT{namespace: "ns1", name: "my_event", payload: []byte("my event payload 1")}, ) rpl2 := createProposalResponsePayload( t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"}, []*readT{}, []*writeT{}, []*metaWriteT{}, []*pvtCollectionT{}, &eventT{namespace: "ns1", name: "my_event", payload: []byte("my event payload 2")}, ) rpl1Bytes := marshal(rpl1, t) rpl2Bytes := marshal(rpl2, t) diff, err := payloadDifference(rpl1Bytes, rpl2Bytes) require.NoError(t, err) expected := [][]interface{}{ {"type", "chaincode event mismatch", "initial-endorser-event", "chaincodeId: ns1, name: my_event, value: my event payload 1", "invoked-endorser-event", "chaincodeId: ns1, name: my_event, value: my event payload 2"}, } require.ElementsMatch(t, expected, diff.details()) } func createProposalResponsePayload(t *testing.T, response *responseT, reads []*readT, writes []*writeT, metaWrites []*metaWriteT, pvtData []*pvtCollectionT, event *eventT) *peer.ProposalResponsePayload { resp := &peer.Response{ Status: response.status, Payload: response.payload, Message: response.message, } rwset := &rwset.TxReadWriteSet{ DataModel: rwset.TxReadWriteSet_KV, NsRwset: collateReadWriteSets(t, reads, writes, metaWrites, pvtData), } action := &peer.ChaincodeAction{ Response: resp, Results: marshal(rwset, t), } if event != nil { ccEvent := &peer.ChaincodeEvent{ ChaincodeId: event.namespace, TxId: event.txid, EventName: event.name, Payload: event.payload, } action.Events = marshal(ccEvent, t) } payload := &peer.ProposalResponsePayload{ ProposalHash: []byte{}, Extension: marshal(action, t), } return payload } func collateReadWriteSets(t *testing.T, reads []*readT, writes []*writeT, metaWrites []*metaWriteT, pvtData []*pvtCollectionT) []*rwset.NsReadWriteSet { grouped := map[string]*kvrwset.KVRWSet{} collections := map[string][]*rwset.CollectionHashedReadWriteSet{} for _, r := range reads { rwset := grouped[r.namespace] if rwset == nil { rwset = &kvrwset.KVRWSet{} grouped[r.namespace] = rwset } if r.key == "" && r.block == 0 { // signifies nil Read proto in these tests rwset.Reads = append(rwset.Reads, nil) continue } var version *kvrwset.Version if r.block != math.MaxInt64 { // signifies missing version in these tests version = &kvrwset.Version{BlockNum: r.block} } rwset.Reads = append(rwset.Reads, &kvrwset.KVRead{ Key: r.key, Version: version, }) } for _, w := range writes { rwset := grouped[w.namespace] if rwset == nil { rwset = &kvrwset.KVRWSet{} grouped[w.namespace] = rwset } rwset.Writes = append(rwset.Writes, &kvrwset.KVWrite{ Key: w.key, Value: w.value, }) } for _, mw := range metaWrites { rwset := grouped[mw.namespace] if rwset == nil { rwset = &kvrwset.KVRWSet{} grouped[mw.namespace] = rwset } rwset.MetadataWrites = append(rwset.MetadataWrites, &kvrwset.KVMetadataWrite{ Key: mw.key, Entries: []*kvrwset.KVMetadataEntry{{Name: mw.name, Value: mw.value}}, // support single entry for now }) } for _, pd := range pvtData { collections[pd.namespace] = append(collections[pd.namespace], &rwset.CollectionHashedReadWriteSet{ CollectionName: pd.collection, PvtRwsetHash: pd.hash, }) } var rwsets []*rwset.NsReadWriteSet for ns, rws := range grouped { rwsets = append(rwsets, &rwset.NsReadWriteSet{ Namespace: ns, Rwset: marshal(rws, t), CollectionHashedRwset: collections[ns], }) } return rwsets }