562 lines
16 KiB
Go
562 lines
16 KiB
Go
/*
|
|
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:<n_out_of:<n:1 rules:<signed_by:0 > > > identities:<principal:\"orgA\" > ", "invoked-endorser-value", "rule:<n_out_of:<n:1 rules:<signed_by:0 > > > identities:<principal:\"orgB\" > "},
|
|
{"type", "missing metadata write (SBE policy)", "namespace", "ns1", "key", "key2", "name", "VALIDATION_PARAMETER", "initial-endorser-value", "rule:<n_out_of:<n:1 rules:<signed_by:0 > > > identities:<principal:\"orgA\" > ", "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
|
|
}
|