From a2ea78978d16953ac01339f2ea847e622a86bd5b Mon Sep 17 00:00:00 2001 From: David Liu Date: Thu, 18 Jan 2024 03:50:27 +0800 Subject: [PATCH 1/2] init commit Signed-off-by: David Liu --- README.md | 1 + protoutil/README.md | 7 + protoutil/blockutils.go | 332 ++++++++++++++++ protoutil/blockutils_test.go | 475 +++++++++++++++++++++++ protoutil/common/crypto/random.go | 44 +++ protoutil/common/util/utils.go | 17 + protoutil/commonutils.go | 303 +++++++++++++++ protoutil/commonutils_test.go | 476 +++++++++++++++++++++++ protoutil/configtxutils.go | 17 + protoutil/configtxutils_test.go | 26 ++ protoutil/fakes/signer_serializer.go | 187 +++++++++ protoutil/go.mod | 24 ++ protoutil/mocks/policy.go | 114 ++++++ protoutil/proputils.go | 402 +++++++++++++++++++ protoutil/proputils_test.go | 419 ++++++++++++++++++++ protoutil/signeddata.go | 116 ++++++ protoutil/signeddata_test.go | 183 +++++++++ protoutil/testdata/peer-expired.pem | 14 + protoutil/txutils.go | 534 +++++++++++++++++++++++++ protoutil/txutils_test.go | 559 +++++++++++++++++++++++++++ protoutil/unmarshalers.go | 259 +++++++++++++ 21 files changed, 4509 insertions(+) create mode 100644 protoutil/README.md create mode 100644 protoutil/blockutils.go create mode 100644 protoutil/blockutils_test.go create mode 100644 protoutil/common/crypto/random.go create mode 100644 protoutil/common/util/utils.go create mode 100644 protoutil/commonutils.go create mode 100644 protoutil/commonutils_test.go create mode 100644 protoutil/configtxutils.go create mode 100644 protoutil/configtxutils_test.go create mode 100644 protoutil/fakes/signer_serializer.go create mode 100644 protoutil/go.mod create mode 100644 protoutil/mocks/policy.go create mode 100644 protoutil/proputils.go create mode 100644 protoutil/proputils_test.go create mode 100644 protoutil/signeddata.go create mode 100644 protoutil/signeddata_test.go create mode 100644 protoutil/testdata/peer-expired.pem create mode 100644 protoutil/txutils.go create mode 100644 protoutil/txutils_test.go create mode 100644 protoutil/unmarshalers.go diff --git a/README.md b/README.md index 1493906..c5f2002 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,4 @@ fabric-lib-go is a home for go code that is common across fabric repositories. It contains: - health check logic that is used by fabric and fabric-ca +- protobuf utilities for fabric core or API developer \ No newline at end of file diff --git a/protoutil/README.md b/protoutil/README.md new file mode 100644 index 0000000..4f7e1b0 --- /dev/null +++ b/protoutil/README.md @@ -0,0 +1,7 @@ +# protoutil + +This originates as a copy of github.com/hyperledger/fabric/protoutil + +## Prepare +to use `counterfeiter` +- install by running `go install github.com/maxbrunsfeld/counterfeiter/v6@latest` outside of go module \ No newline at end of file diff --git a/protoutil/blockutils.go b/protoutil/blockutils.go new file mode 100644 index 0000000..00710d0 --- /dev/null +++ b/protoutil/blockutils.go @@ -0,0 +1,332 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil + +import ( + "bytes" + "crypto/sha256" + "encoding/asn1" + "encoding/base64" + "fmt" + "github.com/hyperledger/fabric-lib-go/protoutil/common/util" + "math/big" + + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/msp" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" +) + +// NewBlock constructs a block with no data and no metadata. +func NewBlock(seqNum uint64, previousHash []byte) *common.Block { + block := &common.Block{} + block.Header = &common.BlockHeader{} + block.Header.Number = seqNum + block.Header.PreviousHash = previousHash + block.Header.DataHash = []byte{} + block.Data = &common.BlockData{} + + var metadataContents [][]byte + for i := 0; i < len(common.BlockMetadataIndex_name); i++ { + metadataContents = append(metadataContents, []byte{}) + } + block.Metadata = &common.BlockMetadata{Metadata: metadataContents} + + return block +} + +type asn1Header struct { + Number *big.Int + PreviousHash []byte + DataHash []byte +} + +func BlockHeaderBytes(b *common.BlockHeader) []byte { + asn1Header := asn1Header{ + PreviousHash: b.PreviousHash, + DataHash: b.DataHash, + Number: new(big.Int).SetUint64(b.Number), + } + result, err := asn1.Marshal(asn1Header) + if err != nil { + // Errors should only arise for types which cannot be encoded, since the + // BlockHeader type is known a-priori to contain only encodable types, an + // error here is fatal and should not be propagated + panic(err) + } + return result +} + +func BlockHeaderHash(b *common.BlockHeader) []byte { + sum := sha256.Sum256(BlockHeaderBytes(b)) + return sum[:] +} + +func BlockDataHash(b *common.BlockData) ([]byte, error) { + if err := VerifyTransactionsAreWellFormed(b); err != nil { + return nil, err + } + return ComputeBlockDataHash(b), nil +} + +func ComputeBlockDataHash(b *common.BlockData) []byte { + sum := sha256.Sum256(bytes.Join(b.Data, nil)) + return sum[:] +} + +// GetChannelIDFromBlockBytes returns channel ID given byte array which represents +// the block +func GetChannelIDFromBlockBytes(bytes []byte) (string, error) { + block, err := UnmarshalBlock(bytes) + if err != nil { + return "", err + } + + return GetChannelIDFromBlock(block) +} + +// GetChannelIDFromBlock returns channel ID in the block +func GetChannelIDFromBlock(block *common.Block) (string, error) { + if block == nil || block.Data == nil || block.Data.Data == nil || len(block.Data.Data) == 0 { + return "", errors.New("failed to retrieve channel id - block is empty") + } + var err error + envelope, err := GetEnvelopeFromBlock(block.Data.Data[0]) + if err != nil { + return "", err + } + payload, err := UnmarshalPayload(envelope.Payload) + if err != nil { + return "", err + } + + if payload.Header == nil { + return "", errors.New("failed to retrieve channel id - payload header is empty") + } + chdr, err := UnmarshalChannelHeader(payload.Header.ChannelHeader) + if err != nil { + return "", err + } + + return chdr.ChannelId, nil +} + +// GetMetadataFromBlock retrieves metadata at the specified index. +func GetMetadataFromBlock(block *common.Block, index common.BlockMetadataIndex) (*common.Metadata, error) { + if block.Metadata == nil { + return nil, errors.New("no metadata in block") + } + + if len(block.Metadata.Metadata) <= int(index) { + return nil, errors.Errorf("no metadata at index [%s]", index) + } + + md := &common.Metadata{} + err := proto.Unmarshal(block.Metadata.Metadata[index], md) + if err != nil { + return nil, errors.Wrapf(err, "error unmarshalling metadata at index [%s]", index) + } + return md, nil +} + +// GetMetadataFromBlockOrPanic retrieves metadata at the specified index, or +// panics on error +func GetMetadataFromBlockOrPanic(block *common.Block, index common.BlockMetadataIndex) *common.Metadata { + md, err := GetMetadataFromBlock(block, index) + if err != nil { + panic(err) + } + return md +} + +// GetConsenterMetadataFromBlock attempts to retrieve consenter metadata from the value +// stored in block metadata at index SIGNATURES (first field). If no consenter metadata +// is found there, it falls back to index ORDERER (third field). +func GetConsenterMetadataFromBlock(block *common.Block) (*common.Metadata, error) { + m, err := GetMetadataFromBlock(block, common.BlockMetadataIndex_SIGNATURES) + if err != nil { + return nil, errors.WithMessage(err, "failed to retrieve metadata") + } + + obm := &common.OrdererBlockMetadata{} + err = proto.Unmarshal(m.Value, obm) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal orderer block metadata") + } + + res := &common.Metadata{} + err = proto.Unmarshal(obm.ConsenterMetadata, res) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal consenter metadata") + } + + return res, nil +} + +// GetLastConfigIndexFromBlock retrieves the index of the last config block as +// encoded in the block metadata +func GetLastConfigIndexFromBlock(block *common.Block) (uint64, error) { + m, err := GetMetadataFromBlock(block, common.BlockMetadataIndex_SIGNATURES) + if err != nil { + return 0, errors.WithMessage(err, "failed to retrieve metadata") + } + + obm := &common.OrdererBlockMetadata{} + err = proto.Unmarshal(m.Value, obm) + if err != nil { + return 0, errors.Wrap(err, "failed to unmarshal orderer block metadata") + } + return obm.LastConfig.Index, nil +} + +// GetLastConfigIndexFromBlockOrPanic retrieves the index of the last config +// block as encoded in the block metadata, or panics on error +func GetLastConfigIndexFromBlockOrPanic(block *common.Block) uint64 { + index, err := GetLastConfigIndexFromBlock(block) + if err != nil { + panic(err) + } + return index +} + +// CopyBlockMetadata copies metadata from one block into another +func CopyBlockMetadata(src *common.Block, dst *common.Block) { + dst.Metadata = src.Metadata + // Once copied initialize with rest of the + // required metadata positions. + InitBlockMetadata(dst) +} + +// InitBlockMetadata initializes metadata structure +func InitBlockMetadata(block *common.Block) { + if block.Metadata == nil { + block.Metadata = &common.BlockMetadata{Metadata: [][]byte{{}, {}, {}, {}, {}}} + } else if len(block.Metadata.Metadata) < int(common.BlockMetadataIndex_COMMIT_HASH+1) { + for i := len(block.Metadata.Metadata); i <= int(common.BlockMetadataIndex_COMMIT_HASH); i++ { + block.Metadata.Metadata = append(block.Metadata.Metadata, []byte{}) + } + } +} + +type VerifierBuilder func(block *common.Block) BlockVerifierFunc + +type BlockVerifierFunc func(header *common.BlockHeader, metadata *common.BlockMetadata) error + +//go:generate counterfeiter -o mocks/policy.go --fake-name Policy . policy +type policy interface { // copied from common.policies to avoid circular import. + // EvaluateSignedData takes a set of SignedData and evaluates whether + // 1) the signatures are valid over the related message + // 2) the signing identities satisfy the policy + EvaluateSignedData(signatureSet []*SignedData) error +} + +func BlockSignatureVerifier(bftEnabled bool, consenters []*common.Consenter, policy policy) BlockVerifierFunc { + return func(header *common.BlockHeader, metadata *common.BlockMetadata) error { + if len(metadata.GetMetadata()) < int(common.BlockMetadataIndex_SIGNATURES)+1 { + return errors.Errorf("no signatures in block metadata") + } + + md := &common.Metadata{} + if err := proto.Unmarshal(metadata.Metadata[common.BlockMetadataIndex_SIGNATURES], md); err != nil { + return errors.Wrapf(err, "error unmarshalling signatures from metadata: %v", err) + } + + var signatureSet []*SignedData + for _, metadataSignature := range md.Signatures { + var signerIdentity []byte + var signedPayload []byte + // if the SignatureHeader is empty and the IdentifierHeader is present, then the consenter expects us to fetch its identity by its numeric identifier + if bftEnabled && len(metadataSignature.GetSignatureHeader()) == 0 && len(metadataSignature.GetIdentifierHeader()) > 0 { + identifierHeader, err := UnmarshalIdentifierHeader(metadataSignature.IdentifierHeader) + if err != nil { + return fmt.Errorf("failed unmarshalling identifier header for block %d: %v", header.GetNumber(), err) + } + identifier := identifierHeader.GetIdentifier() + signerIdentity = searchConsenterIdentityByID(consenters, identifier) + if len(signerIdentity) == 0 { + // The identifier is not within the consenter set + continue + } + signedPayload = util.ConcatenateBytes(md.Value, metadataSignature.IdentifierHeader, BlockHeaderBytes(header)) + } else { + signatureHeader, err := UnmarshalSignatureHeader(metadataSignature.GetSignatureHeader()) + if err != nil { + return fmt.Errorf("failed unmarshalling signature header for block %d: %v", header.GetNumber(), err) + } + + signedPayload = util.ConcatenateBytes(md.Value, metadataSignature.SignatureHeader, BlockHeaderBytes(header)) + + signerIdentity = signatureHeader.Creator + } + + signatureSet = append( + signatureSet, + &SignedData{ + Identity: signerIdentity, + Data: signedPayload, + Signature: metadataSignature.Signature, + }, + ) + } + + return policy.EvaluateSignedData(signatureSet) + } +} + +func searchConsenterIdentityByID(consenters []*common.Consenter, identifier uint32) []byte { + for _, consenter := range consenters { + if consenter.Id == identifier { + return MarshalOrPanic(&msp.SerializedIdentity{ + Mspid: consenter.MspId, + IdBytes: consenter.Identity, + }) + } + } + return nil +} + +func VerifyTransactionsAreWellFormed(bd *common.BlockData) error { + if bd == nil || bd.Data == nil || len(bd.Data) == 0 { + return fmt.Errorf("empty block") + } + + // If we have a single transaction, and the block is a config block, then no need to check + // well formed-ness, because there cannot be another transaction in the original block. + if HasConfigTx(bd) { + return nil + } + + for i, rawTx := range bd.Data { + env := &common.Envelope{} + if err := proto.Unmarshal(rawTx, env); err != nil { + return fmt.Errorf("transaction %d is invalid: %v", i, err) + } + + if len(env.Payload) == 0 { + return fmt.Errorf("transaction %d has no payload", i) + } + + if len(env.Signature) == 0 { + return fmt.Errorf("transaction %d has no signature", i) + } + + expected, err := proto.Marshal(env) + if err != nil { + return fmt.Errorf("failed re-marshaling envelope: %v", err) + } + + if len(expected) < len(rawTx) { + return fmt.Errorf("transaction %d has %d trailing bytes", i, len(rawTx)-len(expected)) + } + if !bytes.Equal(expected, rawTx) { + return fmt.Errorf("transaction %d (%s) does not match its raw form (%s)", i, + base64.StdEncoding.EncodeToString(expected), base64.StdEncoding.EncodeToString(rawTx)) + } + } + + return nil +} diff --git a/protoutil/blockutils_test.go b/protoutil/blockutils_test.go new file mode 100644 index 0000000..b240f5e --- /dev/null +++ b/protoutil/blockutils_test.go @@ -0,0 +1,475 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil_test + +import ( + "crypto/sha256" + "encoding/asn1" + "math" + "testing" + + "github.com/hyperledger/fabric-lib-go/protoutil" + "github.com/hyperledger/fabric-lib-go/protoutil/mocks" + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/msp" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +var testChannelID = "myuniquetestchainid" + +func TestNewBlock(t *testing.T) { + var block *common.Block + require.Nil(t, block.GetHeader()) + require.Nil(t, block.GetData()) + require.Nil(t, block.GetMetadata()) + + data := &common.BlockData{ + Data: [][]byte{{0, 1, 2}}, + } + block = protoutil.NewBlock(uint64(0), []byte("datahash")) + require.Equal(t, []byte("datahash"), block.Header.PreviousHash, "Incorrect previous hash") + require.NotNil(t, block.GetData()) + require.NotNil(t, block.GetMetadata()) + block.GetHeader().DataHash = protoutil.ComputeBlockDataHash(data) + + dataHash := protoutil.ComputeBlockDataHash(data) + + asn1Bytes, err := asn1.Marshal(struct { + Number int64 + PreviousHash []byte + DataHash []byte + }{ + Number: 0, + DataHash: dataHash, + PreviousHash: []byte("datahash"), + }) + headerHash := sha256.Sum256(asn1Bytes) + require.NoError(t, err) + require.Equal(t, asn1Bytes, protoutil.BlockHeaderBytes(block.Header), "Incorrect marshaled blockheader bytes") + require.Equal(t, headerHash[:], protoutil.BlockHeaderHash(block.Header), "Incorrect blockheader hash") +} + +func TestGoodBlockHeaderBytes(t *testing.T) { + goodBlockHeader := &common.BlockHeader{ + Number: 1, + PreviousHash: []byte("foo"), + DataHash: []byte("bar"), + } + + _ = protoutil.BlockHeaderBytes(goodBlockHeader) // Should not panic + + goodBlockHeaderMaxNumber := &common.BlockHeader{ + Number: math.MaxUint64, + PreviousHash: []byte("foo"), + DataHash: []byte("bar"), + } + + _ = protoutil.BlockHeaderBytes(goodBlockHeaderMaxNumber) // Should not panic +} + +func TestGetMetadataFromBlock(t *testing.T) { + t.Run("new block", func(t *testing.T) { + block := protoutil.NewBlock(0, nil) + md, err := protoutil.GetMetadataFromBlock(block, common.BlockMetadataIndex_ORDERER) + require.NoError(t, err, "Unexpected error extracting metadata from new block") + require.Nil(t, md.Value, "Expected metadata field value to be nil") + require.Equal(t, 0, len(md.Value), "Expected length of metadata field value to be 0") + md = protoutil.GetMetadataFromBlockOrPanic(block, common.BlockMetadataIndex_ORDERER) + require.NotNil(t, md, "Expected to get metadata from block") + }) + t.Run("no metadata", func(t *testing.T) { + block := protoutil.NewBlock(0, nil) + block.Metadata = nil + _, err := protoutil.GetMetadataFromBlock(block, common.BlockMetadataIndex_ORDERER) + require.Error(t, err, "Expected error with nil metadata") + require.Contains(t, err.Error(), "no metadata in block") + }) + t.Run("no metadata at index", func(t *testing.T) { + block := protoutil.NewBlock(0, nil) + block.Metadata.Metadata = [][]byte{{1, 2, 3}} + _, err := protoutil.GetMetadataFromBlock(block, common.BlockMetadataIndex_LAST_CONFIG) + require.Error(t, err, "Expected error with nil metadata") + require.Contains(t, err.Error(), "no metadata at index") + }) + t.Run("malformed metadata", func(t *testing.T) { + block := protoutil.NewBlock(0, nil) + block.Metadata.Metadata[common.BlockMetadataIndex_ORDERER] = []byte("bad metadata") + _, err := protoutil.GetMetadataFromBlock(block, common.BlockMetadataIndex_ORDERER) + require.Error(t, err, "Expected error with malformed metadata") + require.Contains(t, err.Error(), "error unmarshalling metadata at index [ORDERER]") + require.Panics(t, func() { + _ = protoutil.GetMetadataFromBlockOrPanic(block, common.BlockMetadataIndex_ORDERER) + }, "Expected panic with malformed metadata") + }) +} + +func TestGetConsenterMetadataFromBlock(t *testing.T) { + cases := []struct { + name string + value []byte + signatures []byte + orderer []byte + pass bool + }{ + { + name: "empty", + value: nil, + signatures: nil, + orderer: nil, + pass: true, + }, + { + name: "signature only", + value: []byte("hello"), + signatures: protoutil.MarshalOrPanic(&common.Metadata{ + Value: protoutil.MarshalOrPanic(&common.OrdererBlockMetadata{ + ConsenterMetadata: protoutil.MarshalOrPanic(&common.Metadata{Value: []byte("hello")}), + }), + }), + orderer: nil, + pass: true, + }, + { + name: "both signatures and orderer", + value: []byte("hello"), + signatures: protoutil.MarshalOrPanic(&common.Metadata{ + Value: protoutil.MarshalOrPanic(&common.OrdererBlockMetadata{ + ConsenterMetadata: protoutil.MarshalOrPanic(&common.Metadata{Value: []byte("hello")}), + }), + }), + orderer: protoutil.MarshalOrPanic(&common.Metadata{Value: []byte("hello")}), + pass: true, + }, + { + name: "malformed OrdererBlockMetadata", + signatures: protoutil.MarshalOrPanic(&common.Metadata{Value: []byte("malformed")}), + orderer: nil, + pass: false, + }, + } + + for _, test := range cases { + block := protoutil.NewBlock(0, nil) + block.Metadata.Metadata[common.BlockMetadataIndex_SIGNATURES] = test.signatures + result, err := protoutil.GetConsenterMetadataFromBlock(block) + + if test.pass { + require.NoError(t, err) + require.Equal(t, result.Value, test.value) + } else { + require.Error(t, err) + } + } +} + +func TestInitBlockMeta(t *testing.T) { + // block with no metadata + block := &common.Block{} + protoutil.InitBlockMetadata(block) + // should have 3 entries + require.Equal(t, 5, len(block.Metadata.Metadata), "Expected block to have 5 metadata entries") + + // block with a single entry + block = &common.Block{ + Metadata: &common.BlockMetadata{}, + } + block.Metadata.Metadata = append(block.Metadata.Metadata, []byte{}) + protoutil.InitBlockMetadata(block) + // should have 3 entries + require.Equal(t, 5, len(block.Metadata.Metadata), "Expected block to have 5 metadata entries") +} + +func TestCopyBlockMetadata(t *testing.T) { + srcBlock := protoutil.NewBlock(0, nil) + dstBlock := &common.Block{} + + metadata, _ := proto.Marshal(&common.Metadata{ + Value: []byte("orderer metadata"), + }) + srcBlock.Metadata.Metadata[common.BlockMetadataIndex_ORDERER] = metadata + protoutil.CopyBlockMetadata(srcBlock, dstBlock) + + // check that the copy worked + require.Equal(t, len(srcBlock.Metadata.Metadata), len(dstBlock.Metadata.Metadata), + "Expected target block to have same number of metadata entries after copy") + require.Equal(t, metadata, dstBlock.Metadata.Metadata[common.BlockMetadataIndex_ORDERER], + "Unexpected metadata from target block") +} + +func TestGetLastConfigIndexFromBlock(t *testing.T) { + index := uint64(2) + block := protoutil.NewBlock(0, nil) + + t.Run("block with last config metadata in signatures field", func(t *testing.T) { + block.Metadata.Metadata[common.BlockMetadataIndex_SIGNATURES] = protoutil.MarshalOrPanic(&common.Metadata{ + Value: protoutil.MarshalOrPanic(&common.OrdererBlockMetadata{ + LastConfig: &common.LastConfig{Index: 2}, + }), + }) + result, err := protoutil.GetLastConfigIndexFromBlock(block) + require.NoError(t, err, "Unexpected error returning last config index") + require.Equal(t, index, result, "Unexpected last config index returned from block") + result = protoutil.GetLastConfigIndexFromBlockOrPanic(block) + require.Equal(t, index, result, "Unexpected last config index returned from block") + }) + + t.Run("block with malformed signatures", func(t *testing.T) { + block.Metadata.Metadata[common.BlockMetadataIndex_SIGNATURES] = []byte("apple") + _, err := protoutil.GetLastConfigIndexFromBlock(block) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to retrieve metadata: error unmarshalling metadata at index [SIGNATURES]") + }) + + t.Run("block with malformed orderer block metadata", func(t *testing.T) { + block.Metadata.Metadata[common.BlockMetadataIndex_SIGNATURES] = protoutil.MarshalOrPanic(&common.Metadata{Value: []byte("banana")}) + _, err := protoutil.GetLastConfigIndexFromBlock(block) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to unmarshal orderer block metadata") + }) + +} + +func TestBlockSignatureVerifierEmptyMetadata(t *testing.T) { + policies := mocks.Policy{} + + verify := protoutil.BlockSignatureVerifier(true, nil, &policies) + + header := &common.BlockHeader{} + md := &common.BlockMetadata{} + + err := verify(header, md) + require.ErrorContains(t, err, "no signatures in block metadata") +} + +func TestBlockSignatureVerifierByIdentifier(t *testing.T) { + consenters := []*common.Consenter{ + { + Id: 1, + Host: "host1", + Port: 8001, + MspId: "msp1", + Identity: []byte("identity1"), + }, + { + Id: 2, + Host: "host2", + Port: 8002, + MspId: "msp2", + Identity: []byte("identity2"), + }, + { + Id: 3, + Host: "host3", + Port: 8003, + MspId: "msp3", + Identity: []byte("identity3"), + }, + } + + policies := mocks.Policy{} + + verify := protoutil.BlockSignatureVerifier(true, consenters, &policies) + + header := &common.BlockHeader{} + md := &common.BlockMetadata{ + Metadata: [][]byte{ + protoutil.MarshalOrPanic(&common.Metadata{Signatures: []*common.MetadataSignature{ + { + Signature: []byte{}, + IdentifierHeader: protoutil.MarshalOrPanic(&common.IdentifierHeader{Identifier: 1}), + }, + { + Signature: []byte{}, + IdentifierHeader: protoutil.MarshalOrPanic(&common.IdentifierHeader{Identifier: 3}), + }, + }}), + }, + } + + err := verify(header, md) + require.NoError(t, err) + signatureSet := policies.EvaluateSignedDataArgsForCall(0) + require.Len(t, signatureSet, 2) + require.Equal(t, protoutil.MarshalOrPanic(&msp.SerializedIdentity{Mspid: "msp1", IdBytes: []byte("identity1")}), signatureSet[0].Identity) + require.Equal(t, protoutil.MarshalOrPanic(&msp.SerializedIdentity{Mspid: "msp3", IdBytes: []byte("identity3")}), signatureSet[1].Identity) +} + +func TestBlockSignatureVerifierByCreator(t *testing.T) { + consenters := []*common.Consenter{ + { + Id: 1, + Host: "host1", + Port: 8001, + MspId: "msp1", + Identity: []byte("identity1"), + }, + { + Id: 2, + Host: "host2", + Port: 8002, + MspId: "msp2", + Identity: []byte("identity2"), + }, + { + Id: 3, + Host: "host3", + Port: 8003, + MspId: "msp3", + Identity: []byte("identity3"), + }, + } + + policies := mocks.Policy{} + + verify := protoutil.BlockSignatureVerifier(true, consenters, &policies) + + header := &common.BlockHeader{} + md := &common.BlockMetadata{ + Metadata: [][]byte{ + protoutil.MarshalOrPanic(&common.Metadata{Signatures: []*common.MetadataSignature{ + { + Signature: []byte{}, + SignatureHeader: protoutil.MarshalOrPanic(&common.SignatureHeader{Creator: []byte("creator1")}), + }, + }}), + }, + } + + err := verify(header, md) + require.NoError(t, err) + signatureSet := policies.EvaluateSignedDataArgsForCall(0) + require.Len(t, signatureSet, 1) + require.Equal(t, []byte("creator1"), signatureSet[0].Identity) +} + +func TestVerifyTransactionsAreWellFormed(t *testing.T) { + originalBlock := &common.Block{ + Data: &common.BlockData{ + Data: [][]byte{ + marshalOrPanic(&common.Envelope{ + Payload: []byte{1, 2, 3}, + Signature: []byte{4, 5, 6}, + }), + marshalOrPanic(&common.Envelope{ + Payload: []byte{7, 8, 9}, + Signature: []byte{10, 11, 12}, + }), + }, + }, + } + + forgedBlock := proto.Clone(originalBlock).(*common.Block) + tmp := make([]byte, len(forgedBlock.Data.Data[0])+len(forgedBlock.Data.Data[1])) + copy(tmp, forgedBlock.Data.Data[0]) + copy(tmp[len(forgedBlock.Data.Data[0]):], forgedBlock.Data.Data[1]) + forgedBlock.Data.Data = [][]byte{tmp} // Replace transactions {0,1} with transaction {0 || 1} + + for _, tst := range []struct { + name string + expectedError string + block *common.Block + }{ + { + name: "config block", + block: &common.Block{Data: &common.BlockData{ + Data: [][]byte{ + protoutil.MarshalOrPanic( + &common.Envelope{ + Payload: protoutil.MarshalOrPanic(&common.Payload{ + Header: &common.Header{ + ChannelHeader: protoutil.MarshalOrPanic(&common.ChannelHeader{ + Type: int32(common.HeaderType_CONFIG), + }), + }, + }), + }), + }, + }}, + }, + { + name: "no block data", + block: &common.Block{}, + expectedError: "empty block", + }, + { + name: "no transactions", + block: &common.Block{Data: &common.BlockData{}}, + expectedError: "empty block", + }, + { + name: "single transaction", + block: &common.Block{Data: &common.BlockData{Data: [][]byte{marshalOrPanic(&common.Envelope{ + Payload: []byte{1, 2, 3}, + Signature: []byte{4, 5, 6}, + })}}}, + }, + { + name: "good block", + block: originalBlock, + }, + { + name: "forged block", + block: forgedBlock, + expectedError: "transaction 0 has 10 trailing bytes", + }, + { + name: "no signature", + expectedError: "transaction 0 has no signature", + block: &common.Block{ + Data: &common.BlockData{ + Data: [][]byte{ + marshalOrPanic(&common.Envelope{ + Payload: []byte{1, 2, 3}, + }), + }, + }, + }, + }, + { + name: "no payload", + expectedError: "transaction 0 has no payload", + block: &common.Block{ + Data: &common.BlockData{ + Data: [][]byte{ + marshalOrPanic(&common.Envelope{ + Signature: []byte{4, 5, 6}, + }), + }, + }, + }, + }, + { + name: "transaction invalid", + expectedError: "cannot parse invalid wire-format data", + block: &common.Block{ + Data: &common.BlockData{ + Data: [][]byte{ + marshalOrPanic(&common.Envelope{ + Payload: []byte{1, 2, 3}, + Signature: []byte{4, 5, 6}, + })[9:], + }, + }, + }, + }, + } { + t.Run(tst.name, func(t *testing.T) { + if tst.block == nil || tst.block.Data == nil { + err := protoutil.VerifyTransactionsAreWellFormed(tst.block.Data) + require.EqualError(t, err, "empty block") + } else { + err := protoutil.VerifyTransactionsAreWellFormed(tst.block.Data) + if tst.expectedError == "" { + require.NoError(t, err) + } else { + require.Contains(t, err.Error(), tst.expectedError) + } + } + }) + } +} diff --git a/protoutil/common/crypto/random.go b/protoutil/common/crypto/random.go new file mode 100644 index 0000000..518ee39 --- /dev/null +++ b/protoutil/common/crypto/random.go @@ -0,0 +1,44 @@ +/* +Copyright IBM Corp. 2016 All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crypto + +import ( + "crypto/rand" + "github.com/pkg/errors" +) + +const ( + // NonceSize is the default NonceSize + NonceSize = 24 +) + +// GetRandomBytes returns len random looking bytes +func GetRandomBytes(len int) ([]byte, error) { + key := make([]byte, len) + + _, err := rand.Read(key) + if err != nil { + return nil, errors.Wrap(err, "error getting random bytes") + } + + return key, nil +} + +// GetRandomNonce returns a random byte array of length NonceSize +func GetRandomNonce() ([]byte, error) { + return GetRandomBytes(NonceSize) +} diff --git a/protoutil/common/util/utils.go b/protoutil/common/util/utils.go new file mode 100644 index 0000000..8dbe65a --- /dev/null +++ b/protoutil/common/util/utils.go @@ -0,0 +1,17 @@ +package util + +// ConcatenateBytes is useful for combining multiple arrays of bytes, especially for +// signatures or digests over multiple fields +// This way is more efficient in speed +func ConcatenateBytes(data ...[]byte) []byte { + finalLength := 0 + for _, slice := range data { + finalLength += len(slice) + } + result := make([]byte, finalLength) + last := 0 + for _, slice := range data { + last += copy(result[last:], slice) + } + return result +} diff --git a/protoutil/commonutils.go b/protoutil/commonutils.go new file mode 100644 index 0000000..5026a1b --- /dev/null +++ b/protoutil/commonutils.go @@ -0,0 +1,303 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil + +import ( + "crypto/rand" + "fmt" + "time" + + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" + timestamp "google.golang.org/protobuf/types/known/timestamppb" +) + +// MarshalOrPanic serializes a protobuf message and panics if this +// operation fails +func MarshalOrPanic(pb proto.Message) []byte { + data, err := proto.Marshal(pb) + if err != nil { + panic(err) + } + return data +} + +// Marshal serializes a protobuf message. +func Marshal(pb proto.Message) ([]byte, error) { + return proto.Marshal(pb) +} + +// CreateNonceOrPanic generates a nonce using the common/crypto package +// and panics if this operation fails. +func CreateNonceOrPanic() []byte { + nonce, err := CreateNonce() + if err != nil { + panic(err) + } + return nonce +} + +// CreateNonce generates a nonce using the common/crypto package. +func CreateNonce() ([]byte, error) { + nonce, err := getRandomNonce() + return nonce, errors.WithMessage(err, "error generating random nonce") +} + +// UnmarshalEnvelopeOfType unmarshal an envelope of the specified type, +// including unmarshalling the payload data +func UnmarshalEnvelopeOfType(envelope *common.Envelope, headerType common.HeaderType, message proto.Message) (*common.ChannelHeader, error) { + payload, err := UnmarshalPayload(envelope.Payload) + if err != nil { + return nil, err + } + + if payload.Header == nil { + return nil, errors.New("envelope must have a Header") + } + + chdr, err := UnmarshalChannelHeader(payload.Header.ChannelHeader) + if err != nil { + return nil, err + } + + if chdr.Type != int32(headerType) { + return nil, errors.Errorf("invalid type %s, expected %s", common.HeaderType(chdr.Type), headerType) + } + + err = proto.Unmarshal(payload.Data, message) + err = errors.Wrapf(err, "error unmarshalling message for type %s", headerType) + return chdr, err +} + +// ExtractEnvelopeOrPanic retrieves the requested envelope from a given block +// and unmarshals it -- it panics if either of these operations fail +func ExtractEnvelopeOrPanic(block *common.Block, index int) *common.Envelope { + envelope, err := ExtractEnvelope(block, index) + if err != nil { + panic(err) + } + return envelope +} + +// ExtractEnvelope retrieves the requested envelope from a given block and +// unmarshals it +func ExtractEnvelope(block *common.Block, index int) (*common.Envelope, error) { + if block.Data == nil { + return nil, errors.New("block data is nil") + } + + envelopeCount := len(block.Data.Data) + if index < 0 || index >= envelopeCount { + return nil, errors.New("envelope index out of bounds") + } + marshaledEnvelope := block.Data.Data[index] + envelope, err := GetEnvelopeFromBlock(marshaledEnvelope) + err = errors.WithMessagef(err, "block data does not carry an envelope at index %d", index) + return envelope, err +} + +// MakeChannelHeader creates a ChannelHeader. +func MakeChannelHeader(headerType common.HeaderType, version int32, chainID string, epoch uint64) *common.ChannelHeader { + return &common.ChannelHeader{ + Type: int32(headerType), + Version: version, + Timestamp: ×tamp.Timestamp{ + Seconds: time.Now().Unix(), + Nanos: 0, + }, + ChannelId: chainID, + Epoch: epoch, + } +} + +// MakeSignatureHeader creates a SignatureHeader. +func MakeSignatureHeader(serializedCreatorCertChain []byte, nonce []byte) *common.SignatureHeader { + return &common.SignatureHeader{ + Creator: serializedCreatorCertChain, + Nonce: nonce, + } +} + +// SetTxID generates a transaction id based on the provided signature header +// and sets the TxId field in the channel header +func SetTxID(channelHeader *common.ChannelHeader, signatureHeader *common.SignatureHeader) { + channelHeader.TxId = ComputeTxID( + signatureHeader.Nonce, + signatureHeader.Creator, + ) +} + +// MakePayloadHeader creates a Payload Header. +func MakePayloadHeader(ch *common.ChannelHeader, sh *common.SignatureHeader) *common.Header { + return &common.Header{ + ChannelHeader: MarshalOrPanic(ch), + SignatureHeader: MarshalOrPanic(sh), + } +} + +// NewSignatureHeader returns a SignatureHeader with a valid nonce. +func NewSignatureHeader(id Signer) (*common.SignatureHeader, error) { + creator, err := id.Serialize() + if err != nil { + return nil, err + } + nonce, err := CreateNonce() + if err != nil { + return nil, err + } + + return &common.SignatureHeader{ + Creator: creator, + Nonce: nonce, + }, nil +} + +// NewSignatureHeaderOrPanic returns a signature header and panics on error. +func NewSignatureHeaderOrPanic(id Signer) *common.SignatureHeader { + if id == nil { + panic(errors.New("invalid signer. cannot be nil")) + } + + signatureHeader, err := NewSignatureHeader(id) + if err != nil { + panic(fmt.Errorf("failed generating a new SignatureHeader: %s", err)) + } + + return signatureHeader +} + +// SignOrPanic signs a message and panics on error. +func SignOrPanic(signer Signer, msg []byte) []byte { + if signer == nil { + panic(errors.New("invalid signer. cannot be nil")) + } + + sigma, err := signer.Sign(msg) + if err != nil { + panic(fmt.Errorf("failed generating signature: %s", err)) + } + return sigma +} + +// IsConfigBlock validates whenever given block contains configuration +// update transaction +func IsConfigBlock(block *common.Block) bool { + if block.Data == nil { + return false + } + + return HasConfigTx(block.Data) +} + +func HasConfigTx(blockdata *common.BlockData) bool { + if blockdata.Data == nil { + return false + } + + if len(blockdata.Data) != 1 { + return false + } + + marshaledEnvelope := blockdata.Data[0] + envelope, err := GetEnvelopeFromBlock(marshaledEnvelope) + if err != nil { + return false + } + + payload, err := UnmarshalPayload(envelope.Payload) + if err != nil { + return false + } + + if payload.Header == nil { + return false + } + + hdr, err := UnmarshalChannelHeader(payload.Header.ChannelHeader) + if err != nil { + return false + } + + return common.HeaderType(hdr.Type) == common.HeaderType_CONFIG +} + +// ChannelHeader returns the *common.ChannelHeader for a given *common.Envelope. +func ChannelHeader(env *common.Envelope) (*common.ChannelHeader, error) { + if env == nil { + return nil, errors.New("Invalid envelope payload. can't be nil") + } + + envPayload, err := UnmarshalPayload(env.Payload) + if err != nil { + return nil, err + } + + if envPayload.Header == nil { + return nil, errors.New("header not set") + } + + if envPayload.Header.ChannelHeader == nil { + return nil, errors.New("channel header not set") + } + + chdr, err := UnmarshalChannelHeader(envPayload.Header.ChannelHeader) + if err != nil { + return nil, errors.WithMessage(err, "error unmarshalling channel header") + } + + return chdr, nil +} + +// ChannelID returns the Channel ID for a given *common.Envelope. +func ChannelID(env *common.Envelope) (string, error) { + chdr, err := ChannelHeader(env) + if err != nil { + return "", errors.WithMessage(err, "error retrieving channel header") + } + + return chdr.ChannelId, nil +} + +// EnvelopeToConfigUpdate is used to extract a ConfigUpdateEnvelope from an envelope of +// type CONFIG_UPDATE +func EnvelopeToConfigUpdate(configtx *common.Envelope) (*common.ConfigUpdateEnvelope, error) { + configUpdateEnv := &common.ConfigUpdateEnvelope{} + _, err := UnmarshalEnvelopeOfType(configtx, common.HeaderType_CONFIG_UPDATE, configUpdateEnv) + if err != nil { + return nil, err + } + return configUpdateEnv, nil +} + +func getRandomNonce() ([]byte, error) { + key := make([]byte, 24) + + _, err := rand.Read(key) + if err != nil { + return nil, errors.Wrap(err, "error getting random bytes") + } + return key, nil +} + +func IsConfigTransaction(envelope *common.Envelope) bool { + payload, err := UnmarshalPayload(envelope.Payload) + if err != nil { + return false + } + + if payload.Header == nil { + return false + } + + hdr, err := UnmarshalChannelHeader(payload.Header.ChannelHeader) + if err != nil { + return false + } + + return common.HeaderType(hdr.Type) == common.HeaderType_CONFIG || common.HeaderType(hdr.Type) == common.HeaderType_ORDERER_TRANSACTION +} diff --git a/protoutil/commonutils_test.go b/protoutil/commonutils_test.go new file mode 100644 index 0000000..156856a --- /dev/null +++ b/protoutil/commonutils_test.go @@ -0,0 +1,476 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil + +import ( + "bytes" + "errors" + "testing" + + "github.com/hyperledger/fabric-lib-go/protoutil/common/crypto" + "github.com/hyperledger/fabric-lib-go/protoutil/fakes" + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/peer" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +//go:generate counterfeiter -o fakes/signer_serializer.go --fake-name SignerSerializer . signerSerializer +type signerSerializer interface { + Signer +} + +func TestNonceRandomness(t *testing.T) { + n1, err := CreateNonce() + if err != nil { + t.Fatal(err) + } + n2, err := CreateNonce() + if err != nil { + t.Fatal(err) + } + if bytes.Equal(n1, n2) { + t.Fatalf("Expected nonces to be different, got %x and %x", n1, n2) + } +} + +func TestNonceLength(t *testing.T) { + n, err := CreateNonce() + if err != nil { + t.Fatal(err) + } + actual := len(n) + expected := crypto.NonceSize + if actual != expected { + t.Fatalf("Expected nonce to be of size %d, got %d instead", expected, actual) + } +} + +func TestUnmarshalPayload(t *testing.T) { + var payload *common.Payload + good, _ := proto.Marshal(&common.Payload{ + Data: []byte("payload"), + }) + payload, err := UnmarshalPayload(good) + require.NoError(t, err, "Unexpected error unmarshalling payload") + require.NotNil(t, payload, "Payload should not be nil") + payload = UnmarshalPayloadOrPanic(good) + require.NotNil(t, payload, "Payload should not be nil") + + bad := []byte("bad payload") + require.Panics(t, func() { + _ = UnmarshalPayloadOrPanic(bad) + }, "Expected panic unmarshalling malformed payload") +} + +func TestUnmarshalSignatureHeader(t *testing.T) { + t.Run("invalid header", func(t *testing.T) { + sighdrBytes := []byte("invalid signature header") + _, err := UnmarshalSignatureHeader(sighdrBytes) + require.Error(t, err, "Expected unmarshalling error") + }) + + t.Run("valid empty header", func(t *testing.T) { + sighdr := &common.SignatureHeader{} + sighdrBytes := MarshalOrPanic(sighdr) + sighdr, err := UnmarshalSignatureHeader(sighdrBytes) + require.NoError(t, err, "Unexpected error unmarshalling signature header") + require.Nil(t, sighdr.Creator) + require.Nil(t, sighdr.Nonce) + }) + + t.Run("valid header", func(t *testing.T) { + sighdr := &common.SignatureHeader{ + Creator: []byte("creator"), + Nonce: []byte("nonce"), + } + sighdrBytes := MarshalOrPanic(sighdr) + sighdr, err := UnmarshalSignatureHeader(sighdrBytes) + require.NoError(t, err, "Unexpected error unmarshalling signature header") + require.Equal(t, []byte("creator"), sighdr.Creator) + require.Equal(t, []byte("nonce"), sighdr.Nonce) + }) +} + +func TestUnmarshalSignatureHeaderOrPanic(t *testing.T) { + t.Run("panic due to invalid header", func(t *testing.T) { + sighdrBytes := []byte("invalid signature header") + require.Panics(t, func() { + UnmarshalSignatureHeaderOrPanic(sighdrBytes) + }, "Expected panic with invalid header") + }) + + t.Run("no panic as the header is valid", func(t *testing.T) { + sighdr := &common.SignatureHeader{} + sighdrBytes := MarshalOrPanic(sighdr) + sighdr = UnmarshalSignatureHeaderOrPanic(sighdrBytes) + require.Nil(t, sighdr.Creator) + require.Nil(t, sighdr.Nonce) + }) +} + +func TestUnmarshalEnvelope(t *testing.T) { + var env *common.Envelope + good, _ := proto.Marshal(&common.Envelope{}) + env, err := UnmarshalEnvelope(good) + require.NoError(t, err, "Unexpected error unmarshalling envelope") + require.NotNil(t, env, "Envelope should not be nil") + env = UnmarshalEnvelopeOrPanic(good) + require.NotNil(t, env, "Envelope should not be nil") + + bad := []byte("bad envelope") + require.Panics(t, func() { + _ = UnmarshalEnvelopeOrPanic(bad) + }, "Expected panic unmarshalling malformed envelope") +} + +func TestUnmarshalBlock(t *testing.T) { + var env *common.Block + good, _ := proto.Marshal(&common.Block{}) + env, err := UnmarshalBlock(good) + require.NoError(t, err, "Unexpected error unmarshalling block") + require.NotNil(t, env, "Block should not be nil") + env = UnmarshalBlockOrPanic(good) + require.NotNil(t, env, "Block should not be nil") + + bad := []byte("bad block") + require.Panics(t, func() { + _ = UnmarshalBlockOrPanic(bad) + }, "Expected panic unmarshalling malformed block") +} + +func TestUnmarshalEnvelopeOfType(t *testing.T) { + env := &common.Envelope{} + + env.Payload = []byte("bad payload") + _, err := UnmarshalEnvelopeOfType(env, common.HeaderType_CONFIG, nil) + require.Error(t, err, "Expected error unmarshalling malformed envelope") + + payload, _ := proto.Marshal(&common.Payload{ + Header: nil, + }) + env.Payload = payload + _, err = UnmarshalEnvelopeOfType(env, common.HeaderType_CONFIG, nil) + require.Error(t, err, "Expected error with missing payload header") + + payload, _ = proto.Marshal(&common.Payload{ + Header: &common.Header{ + ChannelHeader: []byte("bad header"), + }, + }) + env.Payload = payload + _, err = UnmarshalEnvelopeOfType(env, common.HeaderType_CONFIG, nil) + require.Error(t, err, "Expected error for malformed channel header") + + chdr, _ := proto.Marshal(&common.ChannelHeader{ + Type: int32(common.HeaderType_CHAINCODE_PACKAGE), + }) + payload, _ = proto.Marshal(&common.Payload{ + Header: &common.Header{ + ChannelHeader: chdr, + }, + }) + env.Payload = payload + _, err = UnmarshalEnvelopeOfType(env, common.HeaderType_CONFIG, nil) + require.Error(t, err, "Expected error for wrong channel header type") + + chdr, _ = proto.Marshal(&common.ChannelHeader{ + Type: int32(common.HeaderType_CONFIG), + }) + payload, _ = proto.Marshal(&common.Payload{ + Header: &common.Header{ + ChannelHeader: chdr, + }, + Data: []byte("bad data"), + }) + env.Payload = payload + _, err = UnmarshalEnvelopeOfType(env, common.HeaderType_CONFIG, &common.ConfigEnvelope{}) + require.Error(t, err, "Expected error for malformed payload data") + + chdr, _ = proto.Marshal(&common.ChannelHeader{ + Type: int32(common.HeaderType_CONFIG), + }) + configEnv, _ := proto.Marshal(&common.ConfigEnvelope{}) + payload, _ = proto.Marshal(&common.Payload{ + Header: &common.Header{ + ChannelHeader: chdr, + }, + Data: configEnv, + }) + env.Payload = payload + _, err = UnmarshalEnvelopeOfType(env, common.HeaderType_CONFIG, &common.ConfigEnvelope{}) + require.NoError(t, err, "Unexpected error unmarshalling envelope") +} + +func TestExtractEnvelopeNilData(t *testing.T) { + block := &common.Block{} + _, err := ExtractEnvelope(block, 0) + require.Error(t, err, "Nil data") +} + +func TestExtractEnvelopeWrongIndex(t *testing.T) { + block := testBlock() + if _, err := ExtractEnvelope(block, len(block.GetData().Data)); err == nil { + t.Fatal("Expected envelope extraction to fail (wrong index)") + } +} + +func TestExtractEnvelopeWrongIndexOrPanic(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("Expected envelope extraction to panic (wrong index)") + } + }() + + block := testBlock() + ExtractEnvelopeOrPanic(block, len(block.GetData().Data)) +} + +func TestExtractEnvelope(t *testing.T) { + if envelope, err := ExtractEnvelope(testBlock(), 0); err != nil { + t.Fatalf("Expected envelop extraction to succeed: %s", err) + } else if !proto.Equal(envelope, testEnvelope()) { + t.Fatal("Expected extracted envelope to match test envelope") + } +} + +func TestExtractEnvelopeOrPanic(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatal("Expected envelope extraction to succeed") + } + }() + + if !proto.Equal(ExtractEnvelopeOrPanic(testBlock(), 0), testEnvelope()) { + t.Fatal("Expected extracted envelope to match test envelope") + } +} + +func TestExtractPayload(t *testing.T) { + if payload, err := UnmarshalPayload(testEnvelope().Payload); err != nil { + t.Fatalf("Expected payload extraction to succeed: %s", err) + } else if !proto.Equal(payload, testPayload()) { + t.Fatal("Expected extracted payload to match test payload") + } +} + +func TestExtractPayloadOrPanic(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatal("Expected payload extraction to succeed") + } + }() + + if !proto.Equal(UnmarshalPayloadOrPanic(testEnvelope().Payload), testPayload()) { + t.Fatal("Expected extracted payload to match test payload") + } +} + +func TestUnmarshalChaincodeID(t *testing.T) { + ccname := "mychaincode" + ccversion := "myversion" + ccidbytes, _ := proto.Marshal(&peer.ChaincodeID{ + Name: ccname, + Version: ccversion, + }) + ccid, err := UnmarshalChaincodeID(ccidbytes) + require.NoError(t, err) + require.Equal(t, ccname, ccid.Name, "Expected ccid names to match") + require.Equal(t, ccversion, ccid.Version, "Expected ccid versions to match") + + _, err = UnmarshalChaincodeID([]byte("bad chaincodeID")) + require.Error(t, err, "Expected error marshaling malformed chaincode ID") +} + +func TestNewSignatureHeaderOrPanic(t *testing.T) { + var sigHeader *common.SignatureHeader + + id := &fakes.SignerSerializer{} + id.SerializeReturnsOnCall(0, []byte("serialized"), nil) + id.SerializeReturnsOnCall(1, nil, errors.New("serialize failed")) + sigHeader = NewSignatureHeaderOrPanic(id) + require.NotNil(t, sigHeader, "Signature header should not be nil") + + require.Panics(t, func() { + _ = NewSignatureHeaderOrPanic(nil) + }, "Expected panic with nil signer") + + require.Panics(t, func() { + _ = NewSignatureHeaderOrPanic(id) + }, "Expected panic with signature header error") +} + +func TestSignOrPanic(t *testing.T) { + msg := []byte("sign me") + signer := &fakes.SignerSerializer{} + signer.SignReturnsOnCall(0, msg, nil) + signer.SignReturnsOnCall(1, nil, errors.New("bad signature")) + sig := SignOrPanic(signer, msg) + // mock signer returns message to be signed + require.Equal(t, msg, sig, "Signature does not match expected value") + + require.Panics(t, func() { + _ = SignOrPanic(nil, []byte("sign me")) + }, "Expected panic with nil signer") + + require.Panics(t, func() { + _ = SignOrPanic(signer, []byte("sign me")) + }, "Expected panic with sign error") +} + +// Helper functions + +func testPayload() *common.Payload { + return &common.Payload{ + Header: MakePayloadHeader( + MakeChannelHeader(common.HeaderType_MESSAGE, int32(1), "test", 0), + MakeSignatureHeader([]byte("creator"), []byte("nonce"))), + Data: []byte("test"), + } +} + +func testEnvelope() *common.Envelope { + // No need to set the signature + return &common.Envelope{Payload: MarshalOrPanic(testPayload())} +} + +func testBlock() *common.Block { + // No need to set the block's Header, or Metadata + return &common.Block{ + Data: &common.BlockData{ + Data: [][]byte{MarshalOrPanic(testEnvelope())}, + }, + } +} + +func TestChannelHeader(t *testing.T) { + makeEnvelope := func(payload *common.Payload) *common.Envelope { + return &common.Envelope{ + Payload: MarshalOrPanic(payload), + } + } + + _, err := ChannelHeader(makeEnvelope(&common.Payload{ + Header: &common.Header{ + ChannelHeader: MarshalOrPanic(&common.ChannelHeader{ + ChannelId: "foo", + }), + }, + })) + require.NoError(t, err, "Channel header was present") + + _, err = ChannelHeader(makeEnvelope(&common.Payload{ + Header: &common.Header{}, + })) + require.Error(t, err, "ChannelHeader was missing") + + _, err = ChannelHeader(makeEnvelope(&common.Payload{})) + require.Error(t, err, "Header was missing") + + _, err = ChannelHeader(&common.Envelope{}) + require.Error(t, err, "Payload was missing") +} + +func TestIsConfigBlock(t *testing.T) { + newBlock := func(env *common.Envelope) *common.Block { + return &common.Block{ + Data: &common.BlockData{ + Data: [][]byte{MarshalOrPanic(env)}, + }, + } + } + + newConfigEnv := func(envType int32) *common.Envelope { + return &common.Envelope{ + Payload: MarshalOrPanic(&common.Payload{ + Header: &common.Header{ + ChannelHeader: MarshalOrPanic(&common.ChannelHeader{ + Type: envType, + ChannelId: "test-chain", + }), + }, + Data: []byte("test bytes"), + }), // common.Payload + } // LastUpdate + } + + // scenario 1: CONFIG envelope + envType := int32(common.HeaderType_CONFIG) + env := newConfigEnv(envType) + block := newBlock(env) + + result := IsConfigBlock(block) + require.True(t, result, "IsConfigBlock returns true for blocks with CONFIG envelope") + + // scenario 2: ORDERER_TRANSACTION envelope + envType = int32(common.HeaderType_ORDERER_TRANSACTION) + env = newConfigEnv(envType) + block = newBlock(env) + + result = IsConfigBlock(block) + require.False(t, result, "IsConfigBlock returns false for blocks with ORDERER_TRANSACTION envelope since it is no longer supported") + + // scenario 3: MESSAGE envelope + envType = int32(common.HeaderType_MESSAGE) + env = newConfigEnv(envType) + block = newBlock(env) + + result = IsConfigBlock(block) + require.False(t, result, "IsConfigBlock returns false for blocks with MESSAGE envelope") + + // scenario 4: Data with more than one tx + result = IsConfigBlock(&common.Block{ + Header: nil, + Data: &common.BlockData{Data: [][]byte{{1, 2, 3, 4}, {1, 2, 3, 4}}}, + Metadata: nil, + }) + require.False(t, result, "IsConfigBlock returns false for blocks with more than one transaction") + + // scenario 5: nil data + result = IsConfigBlock(&common.Block{ + Header: nil, + Data: nil, + Metadata: nil, + }) + require.False(t, result, "IsConfigBlock returns false for blocks with no data") +} + +func TestEnvelopeToConfigUpdate(t *testing.T) { + makeEnv := func(data []byte) *common.Envelope { + return &common.Envelope{ + Payload: MarshalOrPanic(&common.Payload{ + Header: &common.Header{ + ChannelHeader: MarshalOrPanic(&common.ChannelHeader{ + Type: int32(common.HeaderType_CONFIG_UPDATE), + ChannelId: "test-chain", + }), + }, + Data: data, + }), // common.Payload + } // LastUpdate + } + + // scenario 1: for valid envelopes + configUpdateEnv := &common.ConfigUpdateEnvelope{} + env := makeEnv(MarshalOrPanic(configUpdateEnv)) + result, err := EnvelopeToConfigUpdate(env) + + require.NoError(t, err, "EnvelopeToConfigUpdate runs without error for valid CONFIG_UPDATE envelope") + require.Equal(t, configUpdateEnv, result, "Correct configUpdateEnvelope returned") + + // scenario 2: for invalid envelopes + env = makeEnv([]byte("test bytes")) + _, err = EnvelopeToConfigUpdate(env) + + require.Error(t, err, "EnvelopeToConfigUpdate fails with error for invalid CONFIG_UPDATE envelope") +} + +func TestGetRandomNonce(t *testing.T) { + key1, err := getRandomNonce() + require.NoErrorf(t, err, "error getting random bytes") + require.Len(t, key1, crypto.NonceSize) +} diff --git a/protoutil/configtxutils.go b/protoutil/configtxutils.go new file mode 100644 index 0000000..8f5140e --- /dev/null +++ b/protoutil/configtxutils.go @@ -0,0 +1,17 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil + +import "github.com/hyperledger/fabric-protos-go-apiv2/common" + +func NewConfigGroup() *common.ConfigGroup { + return &common.ConfigGroup{ + Groups: make(map[string]*common.ConfigGroup), + Values: make(map[string]*common.ConfigValue), + Policies: make(map[string]*common.ConfigPolicy), + } +} diff --git a/protoutil/configtxutils_test.go b/protoutil/configtxutils_test.go new file mode 100644 index 0000000..2193d96 --- /dev/null +++ b/protoutil/configtxutils_test.go @@ -0,0 +1,26 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil_test + +import ( + "testing" + + "github.com/hyperledger/fabric-lib-go/protoutil" + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/stretchr/testify/require" +) + +func TestNewConfigGroup(t *testing.T) { + require.Equal(t, + &common.ConfigGroup{ + Groups: make(map[string]*common.ConfigGroup), + Values: make(map[string]*common.ConfigValue), + Policies: make(map[string]*common.ConfigPolicy), + }, + protoutil.NewConfigGroup(), + ) +} diff --git a/protoutil/fakes/signer_serializer.go b/protoutil/fakes/signer_serializer.go new file mode 100644 index 0000000..c720848 --- /dev/null +++ b/protoutil/fakes/signer_serializer.go @@ -0,0 +1,187 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" +) + +type SignerSerializer struct { + SerializeStub func() ([]byte, error) + serializeMutex sync.RWMutex + serializeArgsForCall []struct { + } + serializeReturns struct { + result1 []byte + result2 error + } + serializeReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + SignStub func([]byte) ([]byte, error) + signMutex sync.RWMutex + signArgsForCall []struct { + arg1 []byte + } + signReturns struct { + result1 []byte + result2 error + } + signReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *SignerSerializer) Serialize() ([]byte, error) { + fake.serializeMutex.Lock() + ret, specificReturn := fake.serializeReturnsOnCall[len(fake.serializeArgsForCall)] + fake.serializeArgsForCall = append(fake.serializeArgsForCall, struct { + }{}) + stub := fake.SerializeStub + fakeReturns := fake.serializeReturns + fake.recordInvocation("Serialize", []interface{}{}) + fake.serializeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *SignerSerializer) SerializeCallCount() int { + fake.serializeMutex.RLock() + defer fake.serializeMutex.RUnlock() + return len(fake.serializeArgsForCall) +} + +func (fake *SignerSerializer) SerializeCalls(stub func() ([]byte, error)) { + fake.serializeMutex.Lock() + defer fake.serializeMutex.Unlock() + fake.SerializeStub = stub +} + +func (fake *SignerSerializer) SerializeReturns(result1 []byte, result2 error) { + fake.serializeMutex.Lock() + defer fake.serializeMutex.Unlock() + fake.SerializeStub = nil + fake.serializeReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *SignerSerializer) SerializeReturnsOnCall(i int, result1 []byte, result2 error) { + fake.serializeMutex.Lock() + defer fake.serializeMutex.Unlock() + fake.SerializeStub = nil + if fake.serializeReturnsOnCall == nil { + fake.serializeReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.serializeReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *SignerSerializer) Sign(arg1 []byte) ([]byte, error) { + var arg1Copy []byte + if arg1 != nil { + arg1Copy = make([]byte, len(arg1)) + copy(arg1Copy, arg1) + } + fake.signMutex.Lock() + ret, specificReturn := fake.signReturnsOnCall[len(fake.signArgsForCall)] + fake.signArgsForCall = append(fake.signArgsForCall, struct { + arg1 []byte + }{arg1Copy}) + stub := fake.SignStub + fakeReturns := fake.signReturns + fake.recordInvocation("Sign", []interface{}{arg1Copy}) + fake.signMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *SignerSerializer) SignCallCount() int { + fake.signMutex.RLock() + defer fake.signMutex.RUnlock() + return len(fake.signArgsForCall) +} + +func (fake *SignerSerializer) SignCalls(stub func([]byte) ([]byte, error)) { + fake.signMutex.Lock() + defer fake.signMutex.Unlock() + fake.SignStub = stub +} + +func (fake *SignerSerializer) SignArgsForCall(i int) []byte { + fake.signMutex.RLock() + defer fake.signMutex.RUnlock() + argsForCall := fake.signArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *SignerSerializer) SignReturns(result1 []byte, result2 error) { + fake.signMutex.Lock() + defer fake.signMutex.Unlock() + fake.SignStub = nil + fake.signReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *SignerSerializer) SignReturnsOnCall(i int, result1 []byte, result2 error) { + fake.signMutex.Lock() + defer fake.signMutex.Unlock() + fake.SignStub = nil + if fake.signReturnsOnCall == nil { + fake.signReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.signReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *SignerSerializer) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.serializeMutex.RLock() + defer fake.serializeMutex.RUnlock() + fake.signMutex.RLock() + defer fake.signMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *SignerSerializer) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/protoutil/go.mod b/protoutil/go.mod new file mode 100644 index 0000000..5f0cbdb --- /dev/null +++ b/protoutil/go.mod @@ -0,0 +1,24 @@ +module github.com/hyperledger/fabric-lib-go/protoutil + +go 1.20 + +require ( + github.com/hyperledger/fabric-protos-go-apiv2 v0.3.2 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.8.4 + google.golang.org/protobuf v1.32.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/grpc v1.59.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/protoutil/mocks/policy.go b/protoutil/mocks/policy.go new file mode 100644 index 0000000..6feadf3 --- /dev/null +++ b/protoutil/mocks/policy.go @@ -0,0 +1,114 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "sync" + + "github.com/hyperledger/fabric-lib-go/protoutil" +) + +type Policy struct { + EvaluateSignedDataStub func([]*protoutil.SignedData) error + evaluateSignedDataMutex sync.RWMutex + evaluateSignedDataArgsForCall []struct { + arg1 []*protoutil.SignedData + } + evaluateSignedDataReturns struct { + result1 error + } + evaluateSignedDataReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *Policy) EvaluateSignedData(arg1 []*protoutil.SignedData) error { + var arg1Copy []*protoutil.SignedData + if arg1 != nil { + arg1Copy = make([]*protoutil.SignedData, len(arg1)) + copy(arg1Copy, arg1) + } + fake.evaluateSignedDataMutex.Lock() + ret, specificReturn := fake.evaluateSignedDataReturnsOnCall[len(fake.evaluateSignedDataArgsForCall)] + fake.evaluateSignedDataArgsForCall = append(fake.evaluateSignedDataArgsForCall, struct { + arg1 []*protoutil.SignedData + }{arg1Copy}) + stub := fake.EvaluateSignedDataStub + fakeReturns := fake.evaluateSignedDataReturns + fake.recordInvocation("EvaluateSignedData", []interface{}{arg1Copy}) + fake.evaluateSignedDataMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *Policy) EvaluateSignedDataCallCount() int { + fake.evaluateSignedDataMutex.RLock() + defer fake.evaluateSignedDataMutex.RUnlock() + return len(fake.evaluateSignedDataArgsForCall) +} + +func (fake *Policy) EvaluateSignedDataCalls(stub func([]*protoutil.SignedData) error) { + fake.evaluateSignedDataMutex.Lock() + defer fake.evaluateSignedDataMutex.Unlock() + fake.EvaluateSignedDataStub = stub +} + +func (fake *Policy) EvaluateSignedDataArgsForCall(i int) []*protoutil.SignedData { + fake.evaluateSignedDataMutex.RLock() + defer fake.evaluateSignedDataMutex.RUnlock() + argsForCall := fake.evaluateSignedDataArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *Policy) EvaluateSignedDataReturns(result1 error) { + fake.evaluateSignedDataMutex.Lock() + defer fake.evaluateSignedDataMutex.Unlock() + fake.EvaluateSignedDataStub = nil + fake.evaluateSignedDataReturns = struct { + result1 error + }{result1} +} + +func (fake *Policy) EvaluateSignedDataReturnsOnCall(i int, result1 error) { + fake.evaluateSignedDataMutex.Lock() + defer fake.evaluateSignedDataMutex.Unlock() + fake.EvaluateSignedDataStub = nil + if fake.evaluateSignedDataReturnsOnCall == nil { + fake.evaluateSignedDataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.evaluateSignedDataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *Policy) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.evaluateSignedDataMutex.RLock() + defer fake.evaluateSignedDataMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *Policy) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/protoutil/proputils.go b/protoutil/proputils.go new file mode 100644 index 0000000..d1b2f9a --- /dev/null +++ b/protoutil/proputils.go @@ -0,0 +1,402 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil + +import ( + "crypto/sha256" + "encoding/hex" + + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/peer" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// CreateChaincodeProposal creates a proposal from given input. +// It returns the proposal and the transaction id associated to the proposal +func CreateChaincodeProposal(typ common.HeaderType, channelID string, cis *peer.ChaincodeInvocationSpec, creator []byte) (*peer.Proposal, string, error) { + return CreateChaincodeProposalWithTransient(typ, channelID, cis, creator, nil) +} + +// CreateChaincodeProposalWithTransient creates a proposal from given input +// It returns the proposal and the transaction id associated to the proposal +func CreateChaincodeProposalWithTransient(typ common.HeaderType, channelID string, cis *peer.ChaincodeInvocationSpec, creator []byte, transientMap map[string][]byte) (*peer.Proposal, string, error) { + // generate a random nonce + nonce, err := getRandomNonce() + if err != nil { + return nil, "", err + } + + // compute txid + txid := ComputeTxID(nonce, creator) + + return CreateChaincodeProposalWithTxIDNonceAndTransient(txid, typ, channelID, cis, nonce, creator, transientMap) +} + +// CreateChaincodeProposalWithTxIDAndTransient creates a proposal from given +// input. It returns the proposal and the transaction id associated with the +// proposal +func CreateChaincodeProposalWithTxIDAndTransient(typ common.HeaderType, channelID string, cis *peer.ChaincodeInvocationSpec, creator []byte, txid string, transientMap map[string][]byte) (*peer.Proposal, string, error) { + // generate a random nonce + nonce, err := getRandomNonce() + if err != nil { + return nil, "", err + } + + // compute txid unless provided by tests + if txid == "" { + txid = ComputeTxID(nonce, creator) + } + + return CreateChaincodeProposalWithTxIDNonceAndTransient(txid, typ, channelID, cis, nonce, creator, transientMap) +} + +// CreateChaincodeProposalWithTxIDNonceAndTransient creates a proposal from +// given input +func CreateChaincodeProposalWithTxIDNonceAndTransient(txid string, typ common.HeaderType, channelID string, cis *peer.ChaincodeInvocationSpec, nonce, creator []byte, transientMap map[string][]byte) (*peer.Proposal, string, error) { + ccHdrExt := &peer.ChaincodeHeaderExtension{ChaincodeId: cis.ChaincodeSpec.ChaincodeId} + ccHdrExtBytes, err := proto.Marshal(ccHdrExt) + if err != nil { + return nil, "", errors.Wrap(err, "error marshaling ChaincodeHeaderExtension") + } + + cisBytes, err := proto.Marshal(cis) + if err != nil { + return nil, "", errors.Wrap(err, "error marshaling ChaincodeInvocationSpec") + } + + ccPropPayload := &peer.ChaincodeProposalPayload{Input: cisBytes, TransientMap: transientMap} + ccPropPayloadBytes, err := proto.Marshal(ccPropPayload) + if err != nil { + return nil, "", errors.Wrap(err, "error marshaling ChaincodeProposalPayload") + } + + // TODO: epoch is now set to zero. This must be changed once we get a more appropriate mechanism to handle it in. + var epoch uint64 + + hdr := &common.Header{ + ChannelHeader: MarshalOrPanic( + &common.ChannelHeader{ + Type: int32(typ), + TxId: txid, + Timestamp: timestamppb.Now(), + ChannelId: channelID, + Extension: ccHdrExtBytes, + Epoch: epoch, + }, + ), + SignatureHeader: MarshalOrPanic( + &common.SignatureHeader{ + Nonce: nonce, + Creator: creator, + }, + ), + } + + hdrBytes, err := proto.Marshal(hdr) + if err != nil { + return nil, "", err + } + + prop := &peer.Proposal{ + Header: hdrBytes, + Payload: ccPropPayloadBytes, + } + return prop, txid, nil +} + +// GetBytesProposalResponsePayload gets proposal response payload +func GetBytesProposalResponsePayload(hash []byte, response *peer.Response, result []byte, event []byte, ccid *peer.ChaincodeID) ([]byte, error) { + cAct := &peer.ChaincodeAction{ + Events: event, Results: result, + Response: response, + ChaincodeId: ccid, + } + cActBytes, err := proto.Marshal(cAct) + if err != nil { + return nil, errors.Wrap(err, "error marshaling ChaincodeAction") + } + + prp := &peer.ProposalResponsePayload{ + Extension: cActBytes, + ProposalHash: hash, + } + prpBytes, err := proto.Marshal(prp) + return prpBytes, errors.Wrap(err, "error marshaling ProposalResponsePayload") +} + +// GetBytesChaincodeProposalPayload gets the chaincode proposal payload +func GetBytesChaincodeProposalPayload(cpp *peer.ChaincodeProposalPayload) ([]byte, error) { + cppBytes, err := proto.Marshal(cpp) + return cppBytes, errors.Wrap(err, "error marshaling ChaincodeProposalPayload") +} + +// GetBytesResponse gets the bytes of Response +func GetBytesResponse(res *peer.Response) ([]byte, error) { + resBytes, err := proto.Marshal(res) + return resBytes, errors.Wrap(err, "error marshaling Response") +} + +// GetBytesChaincodeEvent gets the bytes of ChaincodeEvent +func GetBytesChaincodeEvent(event *peer.ChaincodeEvent) ([]byte, error) { + eventBytes, err := proto.Marshal(event) + return eventBytes, errors.Wrap(err, "error marshaling ChaincodeEvent") +} + +// GetBytesChaincodeActionPayload get the bytes of ChaincodeActionPayload from +// the message +func GetBytesChaincodeActionPayload(cap *peer.ChaincodeActionPayload) ([]byte, error) { + capBytes, err := proto.Marshal(cap) + return capBytes, errors.Wrap(err, "error marshaling ChaincodeActionPayload") +} + +// GetBytesProposalResponse gets proposal bytes response +func GetBytesProposalResponse(pr *peer.ProposalResponse) ([]byte, error) { + respBytes, err := proto.Marshal(pr) + return respBytes, errors.Wrap(err, "error marshaling ProposalResponse") +} + +// GetBytesHeader get the bytes of Header from the message +func GetBytesHeader(hdr *common.Header) ([]byte, error) { + bytes, err := proto.Marshal(hdr) + return bytes, errors.Wrap(err, "error marshaling Header") +} + +// GetBytesSignatureHeader get the bytes of SignatureHeader from the message +func GetBytesSignatureHeader(hdr *common.SignatureHeader) ([]byte, error) { + bytes, err := proto.Marshal(hdr) + return bytes, errors.Wrap(err, "error marshaling SignatureHeader") +} + +// GetBytesTransaction get the bytes of Transaction from the message +func GetBytesTransaction(tx *peer.Transaction) ([]byte, error) { + bytes, err := proto.Marshal(tx) + return bytes, errors.Wrap(err, "error unmarshalling Transaction") +} + +// GetBytesPayload get the bytes of Payload from the message +func GetBytesPayload(payl *common.Payload) ([]byte, error) { + bytes, err := proto.Marshal(payl) + return bytes, errors.Wrap(err, "error marshaling Payload") +} + +// GetBytesEnvelope get the bytes of Envelope from the message +func GetBytesEnvelope(env *common.Envelope) ([]byte, error) { + bytes, err := proto.Marshal(env) + return bytes, errors.Wrap(err, "error marshaling Envelope") +} + +// GetActionFromEnvelope extracts a ChaincodeAction message from a +// serialized Envelope +// TODO: fix function name as per FAB-11831 +func GetActionFromEnvelope(envBytes []byte) (*peer.ChaincodeAction, error) { + env, err := GetEnvelopeFromBlock(envBytes) + if err != nil { + return nil, err + } + return GetActionFromEnvelopeMsg(env) +} + +func GetActionFromEnvelopeMsg(env *common.Envelope) (*peer.ChaincodeAction, error) { + payl, err := UnmarshalPayload(env.Payload) + if err != nil { + return nil, err + } + + tx, err := UnmarshalTransaction(payl.Data) + if err != nil { + return nil, err + } + + if len(tx.Actions) == 0 { + return nil, errors.New("at least one TransactionAction required") + } + + _, respPayload, err := GetPayloads(tx.Actions[0]) + return respPayload, err +} + +// CreateProposalFromCISAndTxid returns a proposal given a serialized identity +// and a ChaincodeInvocationSpec +func CreateProposalFromCISAndTxid(txid string, typ common.HeaderType, channelID string, cis *peer.ChaincodeInvocationSpec, creator []byte) (*peer.Proposal, string, error) { + nonce, err := getRandomNonce() + if err != nil { + return nil, "", err + } + return CreateChaincodeProposalWithTxIDNonceAndTransient(txid, typ, channelID, cis, nonce, creator, nil) +} + +// CreateProposalFromCIS returns a proposal given a serialized identity and a +// ChaincodeInvocationSpec +func CreateProposalFromCIS(typ common.HeaderType, channelID string, cis *peer.ChaincodeInvocationSpec, creator []byte) (*peer.Proposal, string, error) { + return CreateChaincodeProposal(typ, channelID, cis, creator) +} + +// CreateGetChaincodesProposal returns a GETCHAINCODES proposal given a +// serialized identity +func CreateGetChaincodesProposal(channelID string, creator []byte) (*peer.Proposal, string, error) { + ccinp := &peer.ChaincodeInput{Args: [][]byte{[]byte("getchaincodes")}} + lsccSpec := &peer.ChaincodeInvocationSpec{ + ChaincodeSpec: &peer.ChaincodeSpec{ + Type: peer.ChaincodeSpec_GOLANG, + ChaincodeId: &peer.ChaincodeID{Name: "lscc"}, + Input: ccinp, + }, + } + return CreateProposalFromCIS(common.HeaderType_ENDORSER_TRANSACTION, channelID, lsccSpec, creator) +} + +// CreateGetInstalledChaincodesProposal returns a GETINSTALLEDCHAINCODES +// proposal given a serialized identity +func CreateGetInstalledChaincodesProposal(creator []byte) (*peer.Proposal, string, error) { + ccinp := &peer.ChaincodeInput{Args: [][]byte{[]byte("getinstalledchaincodes")}} + lsccSpec := &peer.ChaincodeInvocationSpec{ + ChaincodeSpec: &peer.ChaincodeSpec{ + Type: peer.ChaincodeSpec_GOLANG, + ChaincodeId: &peer.ChaincodeID{Name: "lscc"}, + Input: ccinp, + }, + } + return CreateProposalFromCIS(common.HeaderType_ENDORSER_TRANSACTION, "", lsccSpec, creator) +} + +// CreateInstallProposalFromCDS returns a install proposal given a serialized +// identity and a ChaincodeDeploymentSpec +func CreateInstallProposalFromCDS(ccpack proto.Message, creator []byte) (*peer.Proposal, string, error) { + return createProposalFromCDS("", ccpack, creator, "install") +} + +// CreateDeployProposalFromCDS returns a deploy proposal given a serialized +// identity and a ChaincodeDeploymentSpec +func CreateDeployProposalFromCDS( + channelID string, + cds *peer.ChaincodeDeploymentSpec, + creator []byte, + policy []byte, + escc []byte, + vscc []byte, + collectionConfig []byte) (*peer.Proposal, string, error) { + if collectionConfig == nil { + return createProposalFromCDS(channelID, cds, creator, "deploy", policy, escc, vscc) + } + return createProposalFromCDS(channelID, cds, creator, "deploy", policy, escc, vscc, collectionConfig) +} + +// CreateUpgradeProposalFromCDS returns a upgrade proposal given a serialized +// identity and a ChaincodeDeploymentSpec +func CreateUpgradeProposalFromCDS( + channelID string, + cds *peer.ChaincodeDeploymentSpec, + creator []byte, + policy []byte, + escc []byte, + vscc []byte, + collectionConfig []byte) (*peer.Proposal, string, error) { + if collectionConfig == nil { + return createProposalFromCDS(channelID, cds, creator, "upgrade", policy, escc, vscc) + } + return createProposalFromCDS(channelID, cds, creator, "upgrade", policy, escc, vscc, collectionConfig) +} + +// createProposalFromCDS returns a deploy or upgrade proposal given a +// serialized identity and a ChaincodeDeploymentSpec +func createProposalFromCDS(channelID string, msg proto.Message, creator []byte, propType string, args ...[]byte) (*peer.Proposal, string, error) { + // in the new mode, cds will be nil, "deploy" and "upgrade" are instantiates. + var ccinp *peer.ChaincodeInput + var b []byte + var err error + if msg != nil { + b, err = proto.Marshal(msg) + if err != nil { + return nil, "", err + } + } + switch propType { + case "deploy": + fallthrough + case "upgrade": + cds, ok := msg.(*peer.ChaincodeDeploymentSpec) + if !ok || cds == nil { + return nil, "", errors.New("invalid message for creating lifecycle chaincode proposal") + } + Args := [][]byte{[]byte(propType), []byte(channelID), b} + Args = append(Args, args...) + + ccinp = &peer.ChaincodeInput{Args: Args} + case "install": + ccinp = &peer.ChaincodeInput{Args: [][]byte{[]byte(propType), b}} + } + + // wrap the deployment in an invocation spec to lscc... + lsccSpec := &peer.ChaincodeInvocationSpec{ + ChaincodeSpec: &peer.ChaincodeSpec{ + Type: peer.ChaincodeSpec_GOLANG, + ChaincodeId: &peer.ChaincodeID{Name: "lscc"}, + Input: ccinp, + }, + } + + // ...and get the proposal for it + return CreateProposalFromCIS(common.HeaderType_ENDORSER_TRANSACTION, channelID, lsccSpec, creator) +} + +// ComputeTxID computes TxID as the Hash computed +// over the concatenation of nonce and creator. +func ComputeTxID(nonce, creator []byte) string { + // TODO: Get the Hash function to be used from channel configuration + hasher := sha256.New() + hasher.Write(nonce) + hasher.Write(creator) + return hex.EncodeToString(hasher.Sum(nil)) +} + +// CheckTxID checks that txid is equal to the Hash computed +// over the concatenation of nonce and creator. +func CheckTxID(txid string, nonce, creator []byte) error { + computedTxID := ComputeTxID(nonce, creator) + + if txid != computedTxID { + return errors.Errorf("invalid txid. got [%s], expected [%s]", txid, computedTxID) + } + + return nil +} + +// InvokedChaincodeName takes the proposal bytes of a SignedProposal, and unpacks it all the way down, +// until either an error is encountered, or the chaincode name is found. This is useful primarily +// for chaincodes which wish to know the chaincode name originally invoked, in order to deny cc2cc +// invocations (or, perhaps to deny direct invocations and require cc2cc). +func InvokedChaincodeName(proposalBytes []byte) (string, error) { + proposal := &peer.Proposal{} + err := proto.Unmarshal(proposalBytes, proposal) + if err != nil { + return "", errors.WithMessage(err, "could not unmarshal proposal") + } + + proposalPayload := &peer.ChaincodeProposalPayload{} + err = proto.Unmarshal(proposal.Payload, proposalPayload) + if err != nil { + return "", errors.WithMessage(err, "could not unmarshal chaincode proposal payload") + } + + cis := &peer.ChaincodeInvocationSpec{} + err = proto.Unmarshal(proposalPayload.Input, cis) + if err != nil { + return "", errors.WithMessage(err, "could not unmarshal chaincode invocation spec") + } + + if cis.ChaincodeSpec == nil { + return "", errors.Errorf("chaincode spec is nil") + } + + if cis.ChaincodeSpec.ChaincodeId == nil { + return "", errors.Errorf("chaincode id is nil") + } + + return cis.ChaincodeSpec.ChaincodeId.Name, nil +} diff --git a/protoutil/proputils_test.go b/protoutil/proputils_test.go new file mode 100644 index 0000000..8cb3430 --- /dev/null +++ b/protoutil/proputils_test.go @@ -0,0 +1,419 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil_test + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "testing" + + "github.com/hyperledger/fabric-lib-go/protoutil" + "github.com/hyperledger/fabric-protos-go-apiv2/common" + pb "github.com/hyperledger/fabric-protos-go-apiv2/peer" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func createCIS() *pb.ChaincodeInvocationSpec { + return &pb.ChaincodeInvocationSpec{ + ChaincodeSpec: &pb.ChaincodeSpec{ + Type: pb.ChaincodeSpec_GOLANG, + ChaincodeId: &pb.ChaincodeID{Name: "chaincode_name"}, + Input: &pb.ChaincodeInput{Args: [][]byte{[]byte("arg1"), []byte("arg2")}}, + }, + } +} + +func TestGetChaincodeDeploymentSpec(t *testing.T) { + _, err := protoutil.UnmarshalChaincodeDeploymentSpec([]byte("bad spec")) + require.Error(t, err, "Expected error with malformed spec") + + cds, _ := proto.Marshal(&pb.ChaincodeDeploymentSpec{ + ChaincodeSpec: &pb.ChaincodeSpec{ + Type: pb.ChaincodeSpec_GOLANG, + }, + }) + _, err = protoutil.UnmarshalChaincodeDeploymentSpec(cds) + require.NoError(t, err, "Unexpected error getting deployment spec") +} + +func TestCDSProposals(t *testing.T) { + var prop *pb.Proposal + var err error + var txid string + creator := []byte("creator") + cds := &pb.ChaincodeDeploymentSpec{ + ChaincodeSpec: &pb.ChaincodeSpec{ + Type: pb.ChaincodeSpec_GOLANG, + }, + } + policy := []byte("policy") + escc := []byte("escc") + vscc := []byte("vscc") + chainID := "testchannelid" + + // install + prop, txid, err = protoutil.CreateInstallProposalFromCDS(cds, creator) + require.NotNil(t, prop, "Install proposal should not be nil") + require.NoError(t, err, "Unexpected error creating install proposal") + require.NotEqual(t, "", txid, "txid should not be empty") + + // deploy + prop, txid, err = protoutil.CreateDeployProposalFromCDS(chainID, cds, creator, policy, escc, vscc, nil) + require.NotNil(t, prop, "Deploy proposal should not be nil") + require.NoError(t, err, "Unexpected error creating deploy proposal") + require.NotEqual(t, "", txid, "txid should not be empty") + + // upgrade + prop, txid, err = protoutil.CreateUpgradeProposalFromCDS(chainID, cds, creator, policy, escc, vscc, nil) + require.NotNil(t, prop, "Upgrade proposal should not be nil") + require.NoError(t, err, "Unexpected error creating upgrade proposal") + require.NotEqual(t, "", txid, "txid should not be empty") +} + +func TestProposal(t *testing.T) { + // create a proposal from a ChaincodeInvocationSpec + prop, _, err := protoutil.CreateChaincodeProposalWithTransient( + common.HeaderType_ENDORSER_TRANSACTION, + testChannelID, createCIS(), + []byte("creator"), + map[string][]byte{"certx": []byte("transient")}) + if err != nil { + t.Fatalf("Could not create chaincode proposal, err %s\n", err) + return + } + + // serialize the proposal + pBytes, err := proto.Marshal(prop) + if err != nil { + t.Fatalf("Could not serialize the chaincode proposal, err %s\n", err) + return + } + + // deserialize it and expect it to be the same + propBack, err := protoutil.UnmarshalProposal(pBytes) + if err != nil { + t.Fatalf("Could not deserialize the chaincode proposal, err %s\n", err) + return + } + if !proto.Equal(prop, propBack) { + t.Fatalf("Proposal and deserialized proposals don't match\n") + return + } + + // get back the header + hdr, err := protoutil.UnmarshalHeader(prop.Header) + if err != nil { + t.Fatalf("Could not extract the header from the proposal, err %s\n", err) + } + + hdrBytes, err := protoutil.GetBytesHeader(hdr) + if err != nil { + t.Fatalf("Could not marshal the header, err %s\n", err) + } + + hdr, err = protoutil.UnmarshalHeader(hdrBytes) + if err != nil { + t.Fatalf("Could not unmarshal the header, err %s\n", err) + } + + chdr, err := protoutil.UnmarshalChannelHeader(hdr.ChannelHeader) + if err != nil { + t.Fatalf("Could not unmarshal channel header, err %s", err) + } + + shdr, err := protoutil.UnmarshalSignatureHeader(hdr.SignatureHeader) + if err != nil { + t.Fatalf("Could not unmarshal signature header, err %s", err) + } + + _, err = protoutil.GetBytesSignatureHeader(shdr) + if err != nil { + t.Fatalf("Could not marshal signature header, err %s", err) + } + + // sanity check on header + if chdr.Type != int32(common.HeaderType_ENDORSER_TRANSACTION) || + shdr.Nonce == nil || + string(shdr.Creator) != "creator" { + t.Fatalf("Invalid header after unmarshalling\n") + return + } + + // get back the header extension + hdrExt, err := protoutil.UnmarshalChaincodeHeaderExtension(chdr.Extension) + if err != nil { + t.Fatalf("Could not extract the header extensions from the proposal, err %s\n", err) + return + } + + // sanity check on header extension + if string(hdrExt.ChaincodeId.Name) != "chaincode_name" { + t.Fatalf("Invalid header extension after unmarshalling\n") + return + } + + cpp, err := protoutil.UnmarshalChaincodeProposalPayload(prop.Payload) + if err != nil { + t.Fatalf("could not unmarshal proposal payload") + } + + cis, err := protoutil.UnmarshalChaincodeInvocationSpec(cpp.Input) + if err != nil { + t.Fatalf("could not unmarshal proposal chaincode invocation spec") + } + + // sanity check on cis + if cis.ChaincodeSpec.Type != pb.ChaincodeSpec_GOLANG || + cis.ChaincodeSpec.ChaincodeId.Name != "chaincode_name" || + len(cis.ChaincodeSpec.Input.Args) != 2 || + string(cis.ChaincodeSpec.Input.Args[0]) != "arg1" || + string(cis.ChaincodeSpec.Input.Args[1]) != "arg2" { + t.Fatalf("Invalid chaincode invocation spec after unmarshalling\n") + return + } + + if string(shdr.Creator) != "creator" { + t.Fatalf("Failed checking Creator field. Invalid value, expectext 'creator', got [%s]", string(shdr.Creator)) + return + } + value, ok := cpp.TransientMap["certx"] + if !ok || string(value) != "transient" { + t.Fatalf("Failed checking Transient field. Invalid value, expectext 'transient', got [%s]", string(value)) + return + } +} + +func TestProposalWithTxID(t *testing.T) { + // create a proposal from a ChaincodeInvocationSpec + prop, txid, err := protoutil.CreateChaincodeProposalWithTxIDAndTransient( + common.HeaderType_ENDORSER_TRANSACTION, + testChannelID, + createCIS(), + []byte("creator"), + "testtx", + map[string][]byte{"certx": []byte("transient")}, + ) + require.Nil(t, err) + require.NotNil(t, prop) + require.Equal(t, txid, "testtx") + + prop, txid, err = protoutil.CreateChaincodeProposalWithTxIDAndTransient( + common.HeaderType_ENDORSER_TRANSACTION, + testChannelID, + createCIS(), + []byte("creator"), + "", + map[string][]byte{"certx": []byte("transient")}, + ) + require.Nil(t, err) + require.NotNil(t, prop) + require.NotEmpty(t, txid) +} + +func TestProposalResponse(t *testing.T) { + events := &pb.ChaincodeEvent{ + ChaincodeId: "ccid", + EventName: "EventName", + Payload: []byte("EventPayload"), + TxId: "TxID", + } + ccid := &pb.ChaincodeID{ + Name: "ccid", + Version: "v1", + } + + pHashBytes := []byte("proposal_hash") + pResponse := &pb.Response{Status: 200} + results := []byte("results") + eventBytes, err := protoutil.GetBytesChaincodeEvent(events) + if err != nil { + t.Fatalf("Failure while marshalling the ProposalResponsePayload") + return + } + + // get the bytes of the response + pResponseBytes, err := protoutil.GetBytesResponse(pResponse) + if err != nil { + t.Fatalf("Failure while marshalling the Response") + return + } + + // get the response from bytes + _, err = protoutil.UnmarshalResponse(pResponseBytes) + if err != nil { + t.Fatalf("Failure while unmarshalling the Response") + return + } + + // get the bytes of the ProposalResponsePayload + prpBytes, err := protoutil.GetBytesProposalResponsePayload(pHashBytes, pResponse, results, eventBytes, ccid) + if err != nil { + t.Fatalf("Failure while marshalling the ProposalResponsePayload") + return + } + + // get the ProposalResponsePayload message + prp, err := protoutil.UnmarshalProposalResponsePayload(prpBytes) + if err != nil { + t.Fatalf("Failure while unmarshalling the ProposalResponsePayload") + return + } + + // get the ChaincodeAction message + act, err := protoutil.UnmarshalChaincodeAction(prp.Extension) + if err != nil { + t.Fatalf("Failure while unmarshalling the ChaincodeAction") + return + } + + // sanity check on the action + if string(act.Results) != "results" { + t.Fatalf("Invalid actions after unmarshalling") + return + } + + event, err := protoutil.UnmarshalChaincodeEvents(act.Events) + if err != nil { + t.Fatalf("Failure while unmarshalling the ChainCodeEvents") + return + } + + // sanity check on the event + if string(event.ChaincodeId) != "ccid" { + t.Fatalf("Invalid actions after unmarshalling") + return + } + + pr := &pb.ProposalResponse{ + Payload: prpBytes, + Endorsement: &pb.Endorsement{Endorser: []byte("endorser"), Signature: []byte("signature")}, + Version: 1, // TODO: pick right version number + Response: &pb.Response{Status: 200, Message: "OK"}, + } + + // create a proposal response + prBytes, err := protoutil.GetBytesProposalResponse(pr) + if err != nil { + t.Fatalf("Failure while marshalling the ProposalResponse") + return + } + + // get the proposal response message back + prBack, err := protoutil.UnmarshalProposalResponse(prBytes) + if err != nil { + t.Fatalf("Failure while unmarshalling the ProposalResponse") + return + } + + // sanity check on pr + if prBack.Response.Status != 200 || + string(prBack.Endorsement.Signature) != "signature" || + string(prBack.Endorsement.Endorser) != "endorser" || + !bytes.Equal(prBack.Payload, prpBytes) { + t.Fatalf("Invalid ProposalResponse after unmarshalling") + return + } +} + +func TestProposalTxID(t *testing.T) { + nonce := []byte{1} + creator := []byte{2} + + txid := protoutil.ComputeTxID(nonce, creator) + require.NotEmpty(t, txid, "TxID cannot be empty.") + require.Nil(t, protoutil.CheckTxID(txid, nonce, creator)) + require.Error(t, protoutil.CheckTxID("", nonce, creator)) + + txid = protoutil.ComputeTxID(nil, nil) + require.NotEmpty(t, txid, "TxID cannot be empty.") +} + +func TestComputeProposalTxID(t *testing.T) { + txid := protoutil.ComputeTxID([]byte{1}, []byte{1}) + + // Compute the function computed by ComputeTxID, + // namely, base64(sha256(nonce||creator)) + hf := sha256.New() + hf.Write([]byte{1}) + hf.Write([]byte{1}) + hashOut := hf.Sum(nil) + txid2 := hex.EncodeToString(hashOut) + + t.Logf("%x\n", hashOut) + t.Logf("%s\n", txid) + t.Logf("%s\n", txid2) + + require.Equal(t, txid, txid2) +} + +var ( + signerSerialized []byte +) + +func TestInvokedChaincodeName(t *testing.T) { + t.Run("Success", func(t *testing.T) { + name, err := protoutil.InvokedChaincodeName(protoutil.MarshalOrPanic(&pb.Proposal{ + Payload: protoutil.MarshalOrPanic(&pb.ChaincodeProposalPayload{ + Input: protoutil.MarshalOrPanic(&pb.ChaincodeInvocationSpec{ + ChaincodeSpec: &pb.ChaincodeSpec{ + ChaincodeId: &pb.ChaincodeID{ + Name: "cscc", + }, + }, + }), + }), + })) + require.NoError(t, err) + require.Equal(t, "cscc", name) + }) + + t.Run("BadProposalBytes", func(t *testing.T) { + _, err := protoutil.InvokedChaincodeName([]byte("garbage")) + require.Error(t, err) + require.Contains(t, err.Error(), "could not unmarshal proposal") + }) + + t.Run("BadChaincodeProposalBytes", func(t *testing.T) { + _, err := protoutil.InvokedChaincodeName(protoutil.MarshalOrPanic(&pb.Proposal{ + Payload: []byte("garbage"), + })) + require.Error(t, err) + require.Contains(t, err.Error(), "could not unmarshal chaincode proposal payload") + }) + + t.Run("BadChaincodeInvocationSpec", func(t *testing.T) { + _, err := protoutil.InvokedChaincodeName(protoutil.MarshalOrPanic(&pb.Proposal{ + Payload: protoutil.MarshalOrPanic(&pb.ChaincodeProposalPayload{ + Input: []byte("garbage"), + }), + })) + require.Error(t, err) + require.Contains(t, err.Error(), "could not unmarshal chaincode invocation spec") + }) + + t.Run("NilChaincodeSpec", func(t *testing.T) { + _, err := protoutil.InvokedChaincodeName(protoutil.MarshalOrPanic(&pb.Proposal{ + Payload: protoutil.MarshalOrPanic(&pb.ChaincodeProposalPayload{ + Input: protoutil.MarshalOrPanic(&pb.ChaincodeInvocationSpec{}), + }), + })) + require.EqualError(t, err, "chaincode spec is nil") + }) + + t.Run("NilChaincodeID", func(t *testing.T) { + _, err := protoutil.InvokedChaincodeName(protoutil.MarshalOrPanic(&pb.Proposal{ + Payload: protoutil.MarshalOrPanic(&pb.ChaincodeProposalPayload{ + Input: protoutil.MarshalOrPanic(&pb.ChaincodeInvocationSpec{ + ChaincodeSpec: &pb.ChaincodeSpec{}, + }), + }), + })) + require.EqualError(t, err, "chaincode id is nil") + }) +} diff --git a/protoutil/signeddata.go b/protoutil/signeddata.go new file mode 100644 index 0000000..a95a3e8 --- /dev/null +++ b/protoutil/signeddata.go @@ -0,0 +1,116 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "fmt" + "strings" + + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/msp" + "google.golang.org/protobuf/proto" +) + +// SignedData is used to represent the general triplet required to verify a signature +// This is intended to be generic across crypto schemes, while most crypto schemes will +// include the signing identity and a nonce within the Data, this is left to the crypto +// implementation. +type SignedData struct { + Data []byte + Identity []byte + Signature []byte +} + +// ConfigUpdateEnvelopeAsSignedData returns the set of signatures for the +// ConfigUpdateEnvelope as SignedData or an error indicating why this was not +// possible. +func ConfigUpdateEnvelopeAsSignedData(ce *common.ConfigUpdateEnvelope) ([]*SignedData, error) { + if ce == nil { + return nil, fmt.Errorf("No signatures for nil SignedConfigItem") + } + + result := make([]*SignedData, len(ce.Signatures)) + for i, configSig := range ce.Signatures { + sigHeader := &common.SignatureHeader{} + err := proto.Unmarshal(configSig.SignatureHeader, sigHeader) + if err != nil { + return nil, err + } + + result[i] = &SignedData{ + Data: bytes.Join([][]byte{configSig.SignatureHeader, ce.ConfigUpdate}, nil), + Identity: sigHeader.Creator, + Signature: configSig.Signature, + } + + } + + return result, nil +} + +// EnvelopeAsSignedData returns the signatures for the Envelope as SignedData +// slice of length 1 or an error indicating why this was not possible. +func EnvelopeAsSignedData(env *common.Envelope) ([]*SignedData, error) { + if env == nil { + return nil, fmt.Errorf("No signatures for nil Envelope") + } + + payload := &common.Payload{} + err := proto.Unmarshal(env.Payload, payload) + if err != nil { + return nil, err + } + + if payload.Header == nil /* || payload.Header.SignatureHeader == nil */ { + return nil, fmt.Errorf("Missing Header") + } + + shdr := &common.SignatureHeader{} + err = proto.Unmarshal(payload.Header.SignatureHeader, shdr) + if err != nil { + return nil, fmt.Errorf("GetSignatureHeaderFromBytes failed, err %s", err) + } + + return []*SignedData{{ + Data: env.Payload, + Identity: shdr.Creator, + Signature: env.Signature, + }}, nil +} + +// LogMessageForSerializedIdentity returns a string with seriealized identity information, +// or a string indicating why the serialized identity information cannot be returned. +// Any errors are intentially returned in the return strings so that the function can be used in single-line log messages with minimal clutter. +func LogMessageForSerializedIdentity(serializedIdentity []byte) string { + id := &msp.SerializedIdentity{} + err := proto.Unmarshal(serializedIdentity, id) + if err != nil { + return fmt.Sprintf("Could not unmarshal serialized identity: %s", err) + } + pemBlock, _ := pem.Decode(id.IdBytes) + if pemBlock == nil { + // not all identities are certificates so simply log the serialized + // identity bytes + return fmt.Sprintf("serialized-identity=%x", serializedIdentity) + } + cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return fmt.Sprintf("Could not parse certificate: %s", err) + } + return fmt.Sprintf("(mspid=%s subject=%s issuer=%s serialnumber=%d)", id.Mspid, cert.Subject, cert.Issuer, cert.SerialNumber) +} + +func LogMessageForSerializedIdentities(signedData []*SignedData) (logMsg string) { + var identityMessages []string + for _, sd := range signedData { + identityMessages = append(identityMessages, LogMessageForSerializedIdentity(sd.Identity)) + } + return strings.Join(identityMessages, ", ") +} diff --git a/protoutil/signeddata_test.go b/protoutil/signeddata_test.go new file mode 100644 index 0000000..098fc01 --- /dev/null +++ b/protoutil/signeddata_test.go @@ -0,0 +1,183 @@ +/* +Copyright IBM Corp. 2016 All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil_test + +import ( + "bytes" + "encoding/pem" + "os" + "path/filepath" + "testing" + + "github.com/hyperledger/fabric-lib-go/protoutil" + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/msp" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// More duplicate utility which should go away, but the utils are a bit of a mess right now with import cycles +func marshalOrPanic(msg proto.Message) []byte { + data, err := proto.Marshal(msg) + if err != nil { + panic("Error marshaling") + } + return data +} + +func TestNilConfigEnvelopeAsSignedData(t *testing.T) { + var ce *common.ConfigUpdateEnvelope + _, err := protoutil.ConfigUpdateEnvelopeAsSignedData(ce) + if err == nil { + t.Fatalf("Should have errored trying to convert a nil signed config item to signed data") + } +} + +func TestConfigEnvelopeAsSignedData(t *testing.T) { + configBytes := []byte("Foo") + signatures := [][]byte{[]byte("Signature1"), []byte("Signature2")} + identities := [][]byte{[]byte("Identity1"), []byte("Identity2")} + + configSignatures := make([]*common.ConfigSignature, len(signatures)) + for i := range configSignatures { + configSignatures[i] = &common.ConfigSignature{ + SignatureHeader: marshalOrPanic(&common.SignatureHeader{ + Creator: identities[i], + }), + Signature: signatures[i], + } + } + + ce := &common.ConfigUpdateEnvelope{ + ConfigUpdate: configBytes, + Signatures: configSignatures, + } + + signedData, err := protoutil.ConfigUpdateEnvelopeAsSignedData(ce) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + for i, sigData := range signedData { + if !bytes.Equal(sigData.Identity, identities[i]) { + t.Errorf("Expected identity to match at index %d", i) + } + if !bytes.Equal(sigData.Data, append(configSignatures[i].SignatureHeader, configBytes...)) { + t.Errorf("Expected signature over concatenation of config item bytes and signature header") + } + if !bytes.Equal(sigData.Signature, signatures[i]) { + t.Errorf("Expected signature to match at index %d", i) + } + } +} + +func TestNilEnvelopeAsSignedData(t *testing.T) { + var env *common.Envelope + _, err := protoutil.EnvelopeAsSignedData(env) + if err == nil { + t.Fatalf("Should have errored trying to convert a nil envelope") + } +} + +func TestEnvelopeAsSignedData(t *testing.T) { + identity := []byte("Foo") + sig := []byte("Bar") + + shdrbytes, err := proto.Marshal(&common.SignatureHeader{Creator: identity}) + if err != nil { + t.Fatalf("%s", err) + } + + env := &common.Envelope{ + Payload: marshalOrPanic(&common.Payload{ + Header: &common.Header{ + SignatureHeader: shdrbytes, + }, + }), + Signature: sig, + } + + signedData, err := protoutil.EnvelopeAsSignedData(env) + if err != nil { + t.Fatalf("Unexpected error converting envelope to SignedData: %s", err) + } + + if len(signedData) != 1 { + t.Fatalf("Expected 1 entry of signed data, but got %d", len(signedData)) + } + + if !bytes.Equal(signedData[0].Identity, identity) { + t.Errorf("Wrong identity bytes") + } + if !bytes.Equal(signedData[0].Data, env.Payload) { + t.Errorf("Wrong data bytes") + } + if !bytes.Equal(signedData[0].Signature, sig) { + t.Errorf("Wrong data bytes") + } +} + +func TestLogMessageForSerializedIdentity(t *testing.T) { + pem, err := readPemFile(filepath.Join("testdata", "peer-expired.pem")) + require.NoError(t, err, "Unexpected error reading pem file") + + serializedIdentity := &msp.SerializedIdentity{ + Mspid: "MyMSP", + IdBytes: pem, + } + + serializedIdentityBytes, err := proto.Marshal(serializedIdentity) + require.NoError(t, err, "Unexpected error marshaling") + + identityLogMessage := protoutil.LogMessageForSerializedIdentity(serializedIdentityBytes) + + expected := "(mspid=MyMSP subject=CN=peer0.org1.example.com,L=San Francisco,ST=California,C=US issuer=CN=ca.org1.example.com,O=org1.example.com,L=San Francisco,ST=California,C=US serialnumber=216422593083731187380743188920914963441)" + require.Equal(t, expected, identityLogMessage) + + signedDatas := []*protoutil.SignedData{ + { + Data: nil, + Identity: serializedIdentityBytes, + Signature: nil, + }, + { + Data: nil, + Identity: serializedIdentityBytes, + Signature: nil, + }, + } + + identitiesLogMessage := protoutil.LogMessageForSerializedIdentities(signedDatas) + + expected = + "(mspid=MyMSP subject=CN=peer0.org1.example.com,L=San Francisco,ST=California,C=US issuer=CN=ca.org1.example.com,O=org1.example.com,L=San Francisco,ST=California,C=US serialnumber=216422593083731187380743188920914963441), " + + "(mspid=MyMSP subject=CN=peer0.org1.example.com,L=San Francisco,ST=California,C=US issuer=CN=ca.org1.example.com,O=org1.example.com,L=San Francisco,ST=California,C=US serialnumber=216422593083731187380743188920914963441)" + require.Equal(t, expected, identitiesLogMessage) +} + +func readFile(file string) ([]byte, error) { + fileCont, err := os.ReadFile(file) + if err != nil { + return nil, errors.Wrapf(err, "could not read file %s", file) + } + return fileCont, nil +} + +func readPemFile(file string) ([]byte, error) { + bytes, err := readFile(file) + if err != nil { + return nil, errors.Wrapf(err, "reading from file %s failed", file) + } + + b, _ := pem.Decode(bytes) + if b == nil { + return nil, errors.Errorf("no pem content for file %s", file) + } + + return bytes, nil +} diff --git a/protoutil/testdata/peer-expired.pem b/protoutil/testdata/peer-expired.pem new file mode 100644 index 0000000..16b9ae4 --- /dev/null +++ b/protoutil/testdata/peer-expired.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGTCCAcCgAwIBAgIRAKLReasLg2oNMbOafRp0a/EwCgYIKoZIzj0EAwIwczEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG +cmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh +Lm9yZzEuZXhhbXBsZS5jb20wHhcNODkxMjE1MDc1NTAwWhcNODkxMjE1MDgwMDAw +WjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN +U2FuIEZyYW5jaXNjbzEfMB0GA1UEAxMWcGVlcjAub3JnMS5leGFtcGxlLmNvbTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABGRKxsl6MGrNEgyj78c1uVDgR0lqHvuf +jBS/hlMbOqkF9f+oj1Hfr2oAQYMgj6hwiePxzXTRyk+NboqgVgccstujTTBLMA4G +A1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIEIBuSbFuduz +ktspAE6FAP7r1N5ClHZM1B/fSiRh9BXGMAoGCCqGSM49BAMCA0cAMEQCIFWScCx8 +KIAmvO0qN2qPdG8UeeSr10gvdHl7vohRlDMXAiBt1Pks8/McNoUNI1Q5kInsWroH +1pE6XdTNIOsKDKnd5g== +-----END CERTIFICATE----- diff --git a/protoutil/txutils.go b/protoutil/txutils.go new file mode 100644 index 0000000..478ea05 --- /dev/null +++ b/protoutil/txutils.go @@ -0,0 +1,534 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil + +import ( + "bytes" + "crypto/sha256" + b64 "encoding/base64" + + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/peer" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" +) + +// GetPayloads gets the underlying payload objects in a TransactionAction +func GetPayloads(txActions *peer.TransactionAction) (*peer.ChaincodeActionPayload, *peer.ChaincodeAction, error) { + // TODO: pass in the tx type (in what follows we're assuming the + // type is ENDORSER_TRANSACTION) + ccPayload, err := UnmarshalChaincodeActionPayload(txActions.Payload) + if err != nil { + return nil, nil, err + } + + if ccPayload.Action == nil || ccPayload.Action.ProposalResponsePayload == nil { + return nil, nil, errors.New("no payload in ChaincodeActionPayload") + } + pRespPayload, err := UnmarshalProposalResponsePayload(ccPayload.Action.ProposalResponsePayload) + if err != nil { + return nil, nil, err + } + + if pRespPayload.Extension == nil { + return nil, nil, errors.New("response payload is missing extension") + } + + respPayload, err := UnmarshalChaincodeAction(pRespPayload.Extension) + if err != nil { + return ccPayload, nil, err + } + return ccPayload, respPayload, nil +} + +// GetEnvelopeFromBlock gets an envelope from a block's Data field. +func GetEnvelopeFromBlock(data []byte) (*common.Envelope, error) { + // Block always begins with an envelope + var err error + env := &common.Envelope{} + if err = proto.Unmarshal(data, env); err != nil { + return nil, errors.Wrap(err, "error unmarshalling Envelope") + } + + return env, nil +} + +// CreateSignedEnvelope creates a signed envelope of the desired type, with +// marshaled dataMsg and signs it +func CreateSignedEnvelope( + txType common.HeaderType, + channelID string, + signer Signer, + dataMsg proto.Message, + msgVersion int32, + epoch uint64, +) (*common.Envelope, error) { + return CreateSignedEnvelopeWithTLSBinding(txType, channelID, signer, dataMsg, msgVersion, epoch, nil) +} + +// CreateSignedEnvelopeWithTLSBinding creates a signed envelope of the desired +// type, with marshaled dataMsg and signs it. It also includes a TLS cert hash +// into the channel header +func CreateSignedEnvelopeWithTLSBinding( + txType common.HeaderType, + channelID string, + signer Signer, + dataMsg proto.Message, + msgVersion int32, + epoch uint64, + tlsCertHash []byte, +) (*common.Envelope, error) { + payloadChannelHeader := MakeChannelHeader(txType, msgVersion, channelID, epoch) + payloadChannelHeader.TlsCertHash = tlsCertHash + var err error + payloadSignatureHeader := &common.SignatureHeader{} + + if signer != nil { + payloadSignatureHeader, err = NewSignatureHeader(signer) + if err != nil { + return nil, err + } + } + + data, err := proto.Marshal(dataMsg) + if err != nil { + return nil, errors.Wrap(err, "error marshaling") + } + + paylBytes := MarshalOrPanic( + &common.Payload{ + Header: MakePayloadHeader(payloadChannelHeader, payloadSignatureHeader), + Data: data, + }, + ) + + var sig []byte + if signer != nil { + sig, err = signer.Sign(paylBytes) + if err != nil { + return nil, err + } + } + + env := &common.Envelope{ + Payload: paylBytes, + Signature: sig, + } + + return env, nil +} + +// Signer is the interface needed to sign a transaction +type Signer interface { + Sign(msg []byte) ([]byte, error) + Serialize() ([]byte, error) +} + +// CreateSignedTx assembles an Envelope message from proposal, endorsements, +// and a signer. This function should be called by a client when it has +// collected enough endorsements for a proposal to create a transaction and +// submit it to peers for ordering +func CreateSignedTx( + proposal *peer.Proposal, + signer Signer, + resps ...*peer.ProposalResponse, +) (*common.Envelope, error) { + if len(resps) == 0 { + return nil, errors.New("at least one proposal response is required") + } + + if signer == nil { + return nil, errors.New("signer is required when creating a signed transaction") + } + + // the original header + hdr, err := UnmarshalHeader(proposal.Header) + if err != nil { + return nil, err + } + + // the original payload + pPayl, err := UnmarshalChaincodeProposalPayload(proposal.Payload) + if err != nil { + return nil, err + } + + // check that the signer is the same that is referenced in the header + signerBytes, err := signer.Serialize() + if err != nil { + return nil, err + } + + shdr, err := UnmarshalSignatureHeader(hdr.SignatureHeader) + if err != nil { + return nil, err + } + + if !bytes.Equal(signerBytes, shdr.Creator) { + return nil, errors.New("signer must be the same as the one referenced in the header") + } + + // ensure that all actions are bitwise equal and that they are successful + var a1 []byte + for n, r := range resps { + if r.Response.Status < 200 || r.Response.Status >= 400 { + return nil, errors.Errorf("proposal response was not successful, error code %d, msg %s", r.Response.Status, r.Response.Message) + } + + if n == 0 { + a1 = r.Payload + continue + } + + if !bytes.Equal(a1, r.Payload) { + return nil, errors.Errorf("ProposalResponsePayloads do not match (base64): '%s' vs '%s'", + b64.StdEncoding.EncodeToString(r.Payload), b64.StdEncoding.EncodeToString(a1)) + } + } + + // fill endorsements according to their uniqueness + endorsersUsed := make(map[string]struct{}) + var endorsements []*peer.Endorsement + for _, r := range resps { + if r.Endorsement == nil { + continue + } + key := string(r.Endorsement.Endorser) + if _, used := endorsersUsed[key]; used { + continue + } + endorsements = append(endorsements, r.Endorsement) + endorsersUsed[key] = struct{}{} + } + + if len(endorsements) == 0 { + return nil, errors.Errorf("no endorsements") + } + + // create ChaincodeEndorsedAction + cea := &peer.ChaincodeEndorsedAction{ProposalResponsePayload: resps[0].Payload, Endorsements: endorsements} + + // obtain the bytes of the proposal payload that will go to the transaction + propPayloadBytes, err := GetBytesProposalPayloadForTx(pPayl) + if err != nil { + return nil, err + } + + // serialize the chaincode action payload + cap := &peer.ChaincodeActionPayload{ChaincodeProposalPayload: propPayloadBytes, Action: cea} + capBytes, err := GetBytesChaincodeActionPayload(cap) + if err != nil { + return nil, err + } + + // create a transaction + taa := &peer.TransactionAction{Header: hdr.SignatureHeader, Payload: capBytes} + taas := make([]*peer.TransactionAction, 1) + taas[0] = taa + tx := &peer.Transaction{Actions: taas} + + // serialize the tx + txBytes, err := GetBytesTransaction(tx) + if err != nil { + return nil, err + } + + // create the payload + payl := &common.Payload{Header: hdr, Data: txBytes} + paylBytes, err := GetBytesPayload(payl) + if err != nil { + return nil, err + } + + // sign the payload + sig, err := signer.Sign(paylBytes) + if err != nil { + return nil, err + } + + // here's the envelope + return &common.Envelope{Payload: paylBytes, Signature: sig}, nil +} + +// CreateProposalResponse creates a proposal response. +func CreateProposalResponse( + hdrbytes []byte, + payl []byte, + response *peer.Response, + results []byte, + events []byte, + ccid *peer.ChaincodeID, + signingEndorser Signer, +) (*peer.ProposalResponse, error) { + hdr, err := UnmarshalHeader(hdrbytes) + if err != nil { + return nil, err + } + + // obtain the proposal hash given proposal header, payload and the + // requested visibility + pHashBytes, err := GetProposalHash1(hdr, payl) + if err != nil { + return nil, errors.WithMessage(err, "error computing proposal hash") + } + + // get the bytes of the proposal response payload - we need to sign them + prpBytes, err := GetBytesProposalResponsePayload(pHashBytes, response, results, events, ccid) + if err != nil { + return nil, err + } + + // serialize the signing identity + endorser, err := signingEndorser.Serialize() + if err != nil { + return nil, errors.WithMessage(err, "error serializing signing identity") + } + + // sign the concatenation of the proposal response and the serialized + // endorser identity with this endorser's key + signature, err := signingEndorser.Sign(append(prpBytes, endorser...)) + if err != nil { + return nil, errors.WithMessage(err, "could not sign the proposal response payload") + } + + resp := &peer.ProposalResponse{ + // Timestamp: TODO! + Version: 1, // TODO: pick right version number + Endorsement: &peer.Endorsement{ + Signature: signature, + Endorser: endorser, + }, + Payload: prpBytes, + Response: &peer.Response{ + Status: 200, + Message: "OK", + }, + } + + return resp, nil +} + +// CreateProposalResponseFailure creates a proposal response for cases where +// endorsement proposal fails either due to a endorsement failure or a +// chaincode failure (chaincode response status >= shim.ERRORTHRESHOLD) +func CreateProposalResponseFailure( + hdrbytes []byte, + payl []byte, + response *peer.Response, + results []byte, + events []byte, + chaincodeName string, +) (*peer.ProposalResponse, error) { + hdr, err := UnmarshalHeader(hdrbytes) + if err != nil { + return nil, err + } + + // obtain the proposal hash given proposal header, payload and the requested visibility + pHashBytes, err := GetProposalHash1(hdr, payl) + if err != nil { + return nil, errors.WithMessage(err, "error computing proposal hash") + } + + // get the bytes of the proposal response payload + prpBytes, err := GetBytesProposalResponsePayload(pHashBytes, response, results, events, &peer.ChaincodeID{Name: chaincodeName}) + if err != nil { + return nil, err + } + + resp := &peer.ProposalResponse{ + // Timestamp: TODO! + Payload: prpBytes, + Response: response, + } + + return resp, nil +} + +// GetSignedProposal returns a signed proposal given a Proposal message and a +// signing identity +func GetSignedProposal(prop *peer.Proposal, signer Signer) (*peer.SignedProposal, error) { + // check for nil argument + if prop == nil || signer == nil { + return nil, errors.New("nil arguments") + } + + propBytes, err := proto.Marshal(prop) + if err != nil { + return nil, err + } + + signature, err := signer.Sign(propBytes) + if err != nil { + return nil, err + } + + return &peer.SignedProposal{ProposalBytes: propBytes, Signature: signature}, nil +} + +// MockSignedEndorserProposalOrPanic creates a SignedProposal with the +// passed arguments +func MockSignedEndorserProposalOrPanic( + channelID string, + cs *peer.ChaincodeSpec, + creator, + signature []byte, +) (*peer.SignedProposal, *peer.Proposal) { + prop, _, err := CreateChaincodeProposal( + common.HeaderType_ENDORSER_TRANSACTION, + channelID, + &peer.ChaincodeInvocationSpec{ChaincodeSpec: cs}, + creator) + if err != nil { + panic(err) + } + + propBytes, err := proto.Marshal(prop) + if err != nil { + panic(err) + } + + return &peer.SignedProposal{ProposalBytes: propBytes, Signature: signature}, prop +} + +func MockSignedEndorserProposal2OrPanic( + channelID string, + cs *peer.ChaincodeSpec, + signer Signer, +) (*peer.SignedProposal, *peer.Proposal) { + serializedSigner, err := signer.Serialize() + if err != nil { + panic(err) + } + + prop, _, err := CreateChaincodeProposal( + common.HeaderType_ENDORSER_TRANSACTION, + channelID, + &peer.ChaincodeInvocationSpec{ChaincodeSpec: &peer.ChaincodeSpec{}}, + serializedSigner) + if err != nil { + panic(err) + } + + sProp, err := GetSignedProposal(prop, signer) + if err != nil { + panic(err) + } + + return sProp, prop +} + +// GetBytesProposalPayloadForTx takes a ChaincodeProposalPayload and returns +// its serialized version according to the visibility field +func GetBytesProposalPayloadForTx( + payload *peer.ChaincodeProposalPayload, +) ([]byte, error) { + // check for nil argument + if payload == nil { + return nil, errors.New("nil arguments") + } + + // strip the transient bytes off the payload + cppNoTransient := &peer.ChaincodeProposalPayload{Input: payload.Input, TransientMap: nil} + cppBytes, err := GetBytesChaincodeProposalPayload(cppNoTransient) + if err != nil { + return nil, err + } + + return cppBytes, nil +} + +// GetProposalHash2 gets the proposal hash - this version +// is called by the committer where the visibility policy +// has already been enforced and so we already get what +// we have to get in ccPropPayl +func GetProposalHash2(header *common.Header, ccPropPayl []byte) ([]byte, error) { + // check for nil argument + if header == nil || + header.ChannelHeader == nil || + header.SignatureHeader == nil || + ccPropPayl == nil { + return nil, errors.New("nil arguments") + } + + hash := sha256.New() + // hash the serialized Channel Header object + hash.Write(header.ChannelHeader) + // hash the serialized Signature Header object + hash.Write(header.SignatureHeader) + // hash the bytes of the chaincode proposal payload that we are given + hash.Write(ccPropPayl) + return hash.Sum(nil), nil +} + +// GetProposalHash1 gets the proposal hash bytes after sanitizing the +// chaincode proposal payload according to the rules of visibility +func GetProposalHash1(header *common.Header, ccPropPayl []byte) ([]byte, error) { + // check for nil argument + if header == nil || + header.ChannelHeader == nil || + header.SignatureHeader == nil || + ccPropPayl == nil { + return nil, errors.New("nil arguments") + } + + // unmarshal the chaincode proposal payload + cpp, err := UnmarshalChaincodeProposalPayload(ccPropPayl) + if err != nil { + return nil, err + } + + ppBytes, err := GetBytesProposalPayloadForTx(cpp) + if err != nil { + return nil, err + } + + hash2 := sha256.New() + // hash the serialized Channel Header object + hash2.Write(header.ChannelHeader) + // hash the serialized Signature Header object + hash2.Write(header.SignatureHeader) + // hash of the part of the chaincode proposal payload that will go to the tx + hash2.Write(ppBytes) + return hash2.Sum(nil), nil +} + +// GetOrComputeTxIDFromEnvelope gets the txID present in a given transaction +// envelope. If the txID is empty, it constructs the txID from nonce and +// creator fields in the envelope. +func GetOrComputeTxIDFromEnvelope(txEnvelopBytes []byte) (string, error) { + txEnvelope, err := UnmarshalEnvelope(txEnvelopBytes) + if err != nil { + return "", errors.WithMessage(err, "error getting txID from envelope") + } + + txPayload, err := UnmarshalPayload(txEnvelope.Payload) + if err != nil { + return "", errors.WithMessage(err, "error getting txID from payload") + } + + if txPayload.Header == nil { + return "", errors.New("error getting txID from header: payload header is nil") + } + + chdr, err := UnmarshalChannelHeader(txPayload.Header.ChannelHeader) + if err != nil { + return "", errors.WithMessage(err, "error getting txID from channel header") + } + + if chdr.TxId != "" { + return chdr.TxId, nil + } + + sighdr, err := UnmarshalSignatureHeader(txPayload.Header.SignatureHeader) + if err != nil { + return "", errors.WithMessage(err, "error getting nonce and creator for computing txID") + } + + txid := ComputeTxID(sighdr.Nonce, sighdr.Creator) + return txid, nil +} diff --git a/protoutil/txutils_test.go b/protoutil/txutils_test.go new file mode 100644 index 0000000..7d0836b --- /dev/null +++ b/protoutil/txutils_test.go @@ -0,0 +1,559 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil_test + +import ( + "encoding/hex" + "errors" + "strconv" + "strings" + "testing" + + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/peer" + "google.golang.org/protobuf/proto" + + "github.com/hyperledger/fabric-lib-go/protoutil" + "github.com/hyperledger/fabric-lib-go/protoutil/fakes" + "github.com/stretchr/testify/require" +) + +func TestGetPayloads(t *testing.T) { + var txAction *peer.TransactionAction + var err error + + // good + ccActionBytes, _ := proto.Marshal(&peer.ChaincodeAction{ + Results: []byte("results"), + }) + proposalResponsePayload := &peer.ProposalResponsePayload{ + Extension: ccActionBytes, + } + proposalResponseBytes, err := proto.Marshal(proposalResponsePayload) + require.NoError(t, err) + ccActionPayload := &peer.ChaincodeActionPayload{ + Action: &peer.ChaincodeEndorsedAction{ + ProposalResponsePayload: proposalResponseBytes, + }, + } + ccActionPayloadBytes, _ := proto.Marshal(ccActionPayload) + txAction = &peer.TransactionAction{ + Payload: ccActionPayloadBytes, + } + _, _, err = protoutil.GetPayloads(txAction) + require.NoError(t, err, "Unexpected error getting payload bytes") + t.Logf("error1 [%s]", err) + + // nil proposal response extension + proposalResponseBytes, err = proto.Marshal(&peer.ProposalResponsePayload{ + Extension: nil, + }) + require.NoError(t, err) + ccActionPayloadBytes, _ = proto.Marshal(&peer.ChaincodeActionPayload{ + Action: &peer.ChaincodeEndorsedAction{ + ProposalResponsePayload: proposalResponseBytes, + }, + }) + txAction = &peer.TransactionAction{ + Payload: ccActionPayloadBytes, + } + _, _, err = protoutil.GetPayloads(txAction) + require.Error(t, err, "Expected error with nil proposal response extension") + t.Logf("error2 [%s]", err) + + // malformed proposal response payload + ccActionPayloadBytes, _ = proto.Marshal(&peer.ChaincodeActionPayload{ + Action: &peer.ChaincodeEndorsedAction{ + ProposalResponsePayload: []byte("bad payload"), + }, + }) + txAction = &peer.TransactionAction{ + Payload: ccActionPayloadBytes, + } + _, _, err = protoutil.GetPayloads(txAction) + require.Error(t, err, "Expected error with malformed proposal response payload") + t.Logf("error3 [%s]", err) + + // malformed proposal response payload extension + proposalResponseBytes, _ = proto.Marshal(&peer.ProposalResponsePayload{ + Extension: []byte("bad extension"), + }) + ccActionPayloadBytes, _ = proto.Marshal(&peer.ChaincodeActionPayload{ + Action: &peer.ChaincodeEndorsedAction{ + ProposalResponsePayload: proposalResponseBytes, + }, + }) + txAction = &peer.TransactionAction{ + Payload: ccActionPayloadBytes, + } + _, _, err = protoutil.GetPayloads(txAction) + require.Error(t, err, "Expected error with malformed proposal response extension") + t.Logf("error4 [%s]", err) + + // nil proposal response payload extension + proposalResponseBytes, _ = proto.Marshal(&peer.ProposalResponsePayload{ + ProposalHash: []byte("hash"), + }) + ccActionPayloadBytes, _ = proto.Marshal(&peer.ChaincodeActionPayload{ + Action: &peer.ChaincodeEndorsedAction{ + ProposalResponsePayload: proposalResponseBytes, + }, + }) + txAction = &peer.TransactionAction{ + Payload: ccActionPayloadBytes, + } + _, _, err = protoutil.GetPayloads(txAction) + require.Error(t, err, "Expected error with nil proposal response extension") + t.Logf("error5 [%s]", err) + + // malformed transaction action payload + txAction = &peer.TransactionAction{ + Payload: []byte("bad payload"), + } + _, _, err = protoutil.GetPayloads(txAction) + require.Error(t, err, "Expected error with malformed transaction action payload") + t.Logf("error6 [%s]", err) +} + +func TestDeduplicateEndorsements(t *testing.T) { + signID := &fakes.SignerSerializer{} + signID.SerializeReturns([]byte("signer"), nil) + signerBytes, err := signID.Serialize() + require.NoError(t, err, "Unexpected error serializing signing identity") + + proposal := &peer.Proposal{ + Header: protoutil.MarshalOrPanic(&common.Header{ + ChannelHeader: protoutil.MarshalOrPanic(&common.ChannelHeader{ + Extension: protoutil.MarshalOrPanic(&peer.ChaincodeHeaderExtension{}), + }), + SignatureHeader: protoutil.MarshalOrPanic(&common.SignatureHeader{ + Creator: signerBytes, + }), + }), + } + responses := []*peer.ProposalResponse{ + {Payload: []byte("payload"), Endorsement: &peer.Endorsement{Endorser: []byte{5, 4, 3}}, Response: &peer.Response{Status: int32(200)}}, + {Payload: []byte("payload"), Endorsement: &peer.Endorsement{Endorser: []byte{5, 4, 3}}, Response: &peer.Response{Status: int32(200)}}, + } + + transaction, err := protoutil.CreateSignedTx(proposal, signID, responses...) + require.NoError(t, err) + require.True(t, proto.Equal(transaction, transaction), "got: %#v, want: %#v", transaction, transaction) + + pl := protoutil.UnmarshalPayloadOrPanic(transaction.Payload) + tx, err := protoutil.UnmarshalTransaction(pl.Data) + require.NoError(t, err) + ccap, err := protoutil.UnmarshalChaincodeActionPayload(tx.Actions[0].Payload) + require.NoError(t, err) + require.Len(t, ccap.Action.Endorsements, 1) + require.Equal(t, []byte{5, 4, 3}, ccap.Action.Endorsements[0].Endorser) +} + +func TestCreateSignedTx(t *testing.T) { + var err error + prop := &peer.Proposal{} + + signID := &fakes.SignerSerializer{} + signID.SerializeReturns([]byte("signer"), nil) + signerBytes, err := signID.Serialize() + require.NoError(t, err, "Unexpected error serializing signing identity") + + ccHeaderExtensionBytes := protoutil.MarshalOrPanic(&peer.ChaincodeHeaderExtension{}) + chdrBytes := protoutil.MarshalOrPanic(&common.ChannelHeader{ + Extension: ccHeaderExtensionBytes, + }) + shdrBytes := protoutil.MarshalOrPanic(&common.SignatureHeader{ + Creator: signerBytes, + }) + responses := []*peer.ProposalResponse{{}} + + // malformed signature header + headerBytes := protoutil.MarshalOrPanic(&common.Header{ + SignatureHeader: []byte("bad signature header"), + }) + prop.Header = headerBytes + _, err = protoutil.CreateSignedTx(prop, signID, responses...) + require.Error(t, err, "Expected error with malformed signature header") + + // set up the header bytes for the remaining tests + headerBytes, _ = proto.Marshal(&common.Header{ + ChannelHeader: chdrBytes, + SignatureHeader: shdrBytes, + }) + prop.Header = headerBytes + + nonMatchingTests := []struct { + responses []*peer.ProposalResponse + expectedError string + }{ + // good response followed by bad response + { + []*peer.ProposalResponse{ + {Payload: []byte("payload"), Response: &peer.Response{Status: int32(200)}}, + {Payload: []byte{}, Response: &peer.Response{Status: int32(500), Message: "failed to endorse"}}, + }, + "proposal response was not successful, error code 500, msg failed to endorse", + }, + // bad response followed by good response + { + []*peer.ProposalResponse{ + {Payload: []byte{}, Response: &peer.Response{Status: int32(500), Message: "failed to endorse"}}, + {Payload: []byte("payload"), Response: &peer.Response{Status: int32(200)}}, + }, + "proposal response was not successful, error code 500, msg failed to endorse", + }, + } + for i, nonMatchingTest := range nonMatchingTests { + _, err = protoutil.CreateSignedTx(prop, signID, nonMatchingTest.responses...) + require.EqualErrorf(t, err, nonMatchingTest.expectedError, "Expected non-matching response error '%v' for test %d", nonMatchingTest.expectedError, i) + } + + // good responses, but different payloads + responses = []*peer.ProposalResponse{ + {Payload: []byte("payload"), Response: &peer.Response{Status: int32(200)}}, + {Payload: []byte("payload2"), Response: &peer.Response{Status: int32(200)}}, + } + _, err = protoutil.CreateSignedTx(prop, signID, responses...) + if err == nil || strings.HasPrefix(err.Error(), "ProposalResponsePayloads do not match (base64):") == false { + require.FailNow(t, "Error is expected when response payloads do not match") + } + + // no endorsement + responses = []*peer.ProposalResponse{{ + Payload: []byte("payload"), + Response: &peer.Response{ + Status: int32(200), + }, + }} + _, err = protoutil.CreateSignedTx(prop, signID, responses...) + require.Error(t, err, "Expected error with no endorsements") + + // success + responses = []*peer.ProposalResponse{{ + Payload: []byte("payload"), + Endorsement: &peer.Endorsement{}, + Response: &peer.Response{ + Status: int32(200), + }, + }} + _, err = protoutil.CreateSignedTx(prop, signID, responses...) + require.NoError(t, err, "Unexpected error creating signed transaction") + t.Logf("error: [%s]", err) + + // + // + // additional failure cases + prop = &peer.Proposal{} + responses = []*peer.ProposalResponse{} + // no proposal responses + _, err = protoutil.CreateSignedTx(prop, signID, responses...) + require.Error(t, err, "Expected error with no proposal responses") + + // missing proposal header + responses = append(responses, &peer.ProposalResponse{}) + _, err = protoutil.CreateSignedTx(prop, signID, responses...) + require.Error(t, err, "Expected error with no proposal header") + + // bad proposal payload + prop.Payload = []byte("bad payload") + _, err = protoutil.CreateSignedTx(prop, signID, responses...) + require.Error(t, err, "Expected error with malformed proposal payload") + + // bad payload header + prop.Header = []byte("bad header") + _, err = protoutil.CreateSignedTx(prop, signID, responses...) + require.Error(t, err, "Expected error with malformed proposal header") +} + +func TestCreateSignedTxNoSigner(t *testing.T) { + _, err := protoutil.CreateSignedTx(nil, nil, &peer.ProposalResponse{}) + require.ErrorContains(t, err, "signer is required when creating a signed transaction") +} + +func TestCreateSignedTxStatus(t *testing.T) { + serializedExtension, err := proto.Marshal(&peer.ChaincodeHeaderExtension{}) + require.NoError(t, err) + serializedChannelHeader, err := proto.Marshal(&common.ChannelHeader{ + Extension: serializedExtension, + }) + require.NoError(t, err) + + signingID := &fakes.SignerSerializer{} + signingID.SerializeReturns([]byte("signer"), nil) + serializedSigningID, err := signingID.Serialize() + require.NoError(t, err) + serializedSignatureHeader, err := proto.Marshal(&common.SignatureHeader{ + Creator: serializedSigningID, + }) + require.NoError(t, err) + + header := &common.Header{ + ChannelHeader: serializedChannelHeader, + SignatureHeader: serializedSignatureHeader, + } + + serializedHeader, err := proto.Marshal(header) + require.NoError(t, err) + + proposal := &peer.Proposal{ + Header: serializedHeader, + } + + tests := []struct { + status int32 + expectedErr string + }{ + {status: 0, expectedErr: "proposal response was not successful, error code 0, msg response-message"}, + {status: 199, expectedErr: "proposal response was not successful, error code 199, msg response-message"}, + {status: 200, expectedErr: ""}, + {status: 201, expectedErr: ""}, + {status: 399, expectedErr: ""}, + {status: 400, expectedErr: "proposal response was not successful, error code 400, msg response-message"}, + } + for _, tc := range tests { + t.Run(strconv.Itoa(int(tc.status)), func(t *testing.T) { + response := &peer.ProposalResponse{ + Payload: []byte("payload"), + Endorsement: &peer.Endorsement{}, + Response: &peer.Response{ + Status: tc.status, + Message: "response-message", + }, + } + + _, err := protoutil.CreateSignedTx(proposal, signingID, response) + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestCreateSignedEnvelope(t *testing.T) { + var env *common.Envelope + channelID := "mychannelID" + msg := &common.ConfigEnvelope{} + + id := &fakes.SignerSerializer{} + id.SignReturnsOnCall(0, []byte("goodsig"), nil) + id.SignReturnsOnCall(1, nil, errors.New("bad signature")) + env, err := protoutil.CreateSignedEnvelope(common.HeaderType_CONFIG, channelID, + id, msg, int32(1), uint64(1)) + require.NoError(t, err, "Unexpected error creating signed envelope") + require.NotNil(t, env, "Envelope should not be nil") + // mock sign returns the bytes to be signed + require.Equal(t, []byte("goodsig"), env.Signature, "Unexpected signature returned") + payload := &common.Payload{} + err = proto.Unmarshal(env.Payload, payload) + require.NoError(t, err, "Failed to unmarshal payload") + data := &common.ConfigEnvelope{} + err = proto.Unmarshal(payload.Data, data) + require.NoError(t, err, "Expected payload data to be a config envelope") + require.Equal(t, msg, data, "Payload data does not match expected value") + + _, err = protoutil.CreateSignedEnvelope(common.HeaderType_CONFIG, channelID, + id, &common.ConfigEnvelope{}, int32(1), uint64(1)) + require.Error(t, err, "Expected sign error") +} + +func TestCreateSignedEnvelopeNilSigner(t *testing.T) { + var env *common.Envelope + channelID := "mychannelID" + msg := &common.ConfigEnvelope{} + + env, err := protoutil.CreateSignedEnvelope(common.HeaderType_CONFIG, channelID, + nil, msg, int32(1), uint64(1)) + require.NoError(t, err, "Unexpected error creating signed envelope") + require.NotNil(t, env, "Envelope should not be nil") + require.Empty(t, env.Signature, "Signature should have been empty") + payload := &common.Payload{} + err = proto.Unmarshal(env.Payload, payload) + require.NoError(t, err, "Failed to unmarshal payload") + data := &common.ConfigEnvelope{} + err = proto.Unmarshal(payload.Data, data) + require.NoError(t, err, "Expected payload data to be a config envelope") + require.Equal(t, msg, data, "Payload data does not match expected value") +} + +func TestGetSignedProposal(t *testing.T) { + var signedProp *peer.SignedProposal + var err error + + sig := []byte("signature") + + signID := &fakes.SignerSerializer{} + signID.SignReturns(sig, nil) + + prop := &peer.Proposal{} + propBytes, _ := proto.Marshal(prop) + signedProp, err = protoutil.GetSignedProposal(prop, signID) + require.NoError(t, err, "Unexpected error getting signed proposal") + require.Equal(t, propBytes, signedProp.ProposalBytes, + "Proposal bytes did not match expected value") + require.Equal(t, sig, signedProp.Signature, + "Signature did not match expected value") + + _, err = protoutil.GetSignedProposal(nil, signID) + require.Error(t, err, "Expected error with nil proposal") + _, err = protoutil.GetSignedProposal(prop, nil) + require.Error(t, err, "Expected error with nil signing identity") +} + +func TestMockSignedEndorserProposalOrPanic(t *testing.T) { + var prop *peer.Proposal + var signedProp *peer.SignedProposal + + ccProposal := &peer.ChaincodeProposalPayload{} + cis := &peer.ChaincodeInvocationSpec{} + chainID := "testchannelid" + sig := []byte("signature") + creator := []byte("creator") + cs := &peer.ChaincodeSpec{ + ChaincodeId: &peer.ChaincodeID{ + Name: "mychaincode", + }, + } + + signedProp, prop = protoutil.MockSignedEndorserProposalOrPanic(chainID, cs, + creator, sig) + require.Equal(t, sig, signedProp.Signature, + "Signature did not match expected result") + propBytes, _ := proto.Marshal(prop) + require.Equal(t, propBytes, signedProp.ProposalBytes, + "Proposal bytes do not match expected value") + err := proto.Unmarshal(prop.Payload, ccProposal) + require.NoError(t, err, "Expected ChaincodeProposalPayload") + err = proto.Unmarshal(ccProposal.Input, cis) + require.NoError(t, err, "Expected ChaincodeInvocationSpec") + require.Equal(t, cs.ChaincodeId.Name, cis.ChaincodeSpec.ChaincodeId.Name, + "Chaincode name did not match expected value") +} + +func TestMockSignedEndorserProposal2OrPanic(t *testing.T) { + var prop *peer.Proposal + var signedProp *peer.SignedProposal + + ccProposal := &peer.ChaincodeProposalPayload{} + cis := &peer.ChaincodeInvocationSpec{} + chainID := "testchannelid" + sig := []byte("signature") + signID := &fakes.SignerSerializer{} + signID.SignReturns(sig, nil) + + signedProp, prop = protoutil.MockSignedEndorserProposal2OrPanic(chainID, + &peer.ChaincodeSpec{}, signID) + require.Equal(t, sig, signedProp.Signature, + "Signature did not match expected result") + propBytes, _ := proto.Marshal(prop) + require.Equal(t, propBytes, signedProp.ProposalBytes, + "Proposal bytes do not match expected value") + err := proto.Unmarshal(prop.Payload, ccProposal) + require.NoError(t, err, "Expected ChaincodeProposalPayload") + err = proto.Unmarshal(ccProposal.Input, cis) + require.NoError(t, err, "Expected ChaincodeInvocationSpec") +} + +func TestGetBytesProposalPayloadForTx(t *testing.T) { + input := &peer.ChaincodeProposalPayload{ + Input: []byte("input"), + TransientMap: make(map[string][]byte), + } + expected, _ := proto.Marshal(&peer.ChaincodeProposalPayload{ + Input: []byte("input"), + }) + + result, err := protoutil.GetBytesProposalPayloadForTx(input) + require.NoError(t, err, "Unexpected error getting proposal payload") + require.Equal(t, expected, result, "Payload does not match expected value") + + _, err = protoutil.GetBytesProposalPayloadForTx(nil) + require.Error(t, err, "Expected error with nil proposal payload") +} + +func TestGetProposalHash2(t *testing.T) { + expectedHashHex := "7b622ef4e1ab9b7093ec3bbfbca17d5d6f14a437914a6839319978a7034f7960" + expectedHash, _ := hex.DecodeString(expectedHashHex) + hdr := &common.Header{ + ChannelHeader: []byte("chdr"), + SignatureHeader: []byte("shdr"), + } + propHash, err := protoutil.GetProposalHash2(hdr, []byte("ccproppayload")) + require.NoError(t, err, "Unexpected error getting hash2 for proposal") + require.Equal(t, expectedHash, propHash, "Proposal hash did not match expected hash") + + _, err = protoutil.GetProposalHash2(&common.Header{}, []byte("ccproppayload")) + require.Error(t, err, "Expected error with nil arguments") +} + +func TestGetProposalHash1(t *testing.T) { + expectedHashHex := "d4c1e3cac2105da5fddc2cfe776d6ec28e4598cf1e6fa51122c7f70d8076437b" + expectedHash, _ := hex.DecodeString(expectedHashHex) + hdr := &common.Header{ + ChannelHeader: []byte("chdr"), + SignatureHeader: []byte("shdr"), + } + + ccProposal, _ := proto.Marshal(&peer.ChaincodeProposalPayload{}) + + propHash, err := protoutil.GetProposalHash1(hdr, ccProposal) + require.NoError(t, err, "Unexpected error getting hash for proposal") + require.Equal(t, expectedHash, propHash, "Proposal hash did not match expected hash") + + _, err = protoutil.GetProposalHash1(hdr, []byte("ccproppayload")) + require.Error(t, err, "Expected error with malformed chaincode proposal payload") + + _, err = protoutil.GetProposalHash1(&common.Header{}, []byte("ccproppayload")) + require.Error(t, err, "Expected error with nil arguments") +} + +func TestGetorComputeTxIDFromEnvelope(t *testing.T) { + t.Run("txID is present in the envelope", func(t *testing.T) { + txID := "709184f9d24f6ade8fcd4d6521a6eef295fef6c2e67216c58b68ac15e8946492" + envelopeBytes := createSampleTxEnvelopeBytes(txID) + actualTxID, err := protoutil.GetOrComputeTxIDFromEnvelope(envelopeBytes) + require.Nil(t, err) + require.Equal(t, "709184f9d24f6ade8fcd4d6521a6eef295fef6c2e67216c58b68ac15e8946492", actualTxID) + }) + + t.Run("txID is not present in the envelope", func(t *testing.T) { + txID := "" + envelopeBytes := createSampleTxEnvelopeBytes(txID) + actualTxID, err := protoutil.GetOrComputeTxIDFromEnvelope(envelopeBytes) + require.Nil(t, err) + require.Equal(t, "709184f9d24f6ade8fcd4d6521a6eef295fef6c2e67216c58b68ac15e8946492", actualTxID) + }) +} + +func createSampleTxEnvelopeBytes(txID string) []byte { + chdr := &common.ChannelHeader{ + TxId: "709184f9d24f6ade8fcd4d6521a6eef295fef6c2e67216c58b68ac15e8946492", + } + chdrBytes := protoutil.MarshalOrPanic(chdr) + + shdr := &common.SignatureHeader{ + Nonce: []byte("nonce"), + Creator: []byte("creator"), + } + shdrBytes := protoutil.MarshalOrPanic(shdr) + + hdr := &common.Header{ + ChannelHeader: chdrBytes, + SignatureHeader: shdrBytes, + } + + payload := &common.Payload{ + Header: hdr, + } + payloadBytes := protoutil.MarshalOrPanic(payload) + + envelope := &common.Envelope{ + Payload: payloadBytes, + } + return protoutil.MarshalOrPanic(envelope) +} diff --git a/protoutil/unmarshalers.go b/protoutil/unmarshalers.go new file mode 100644 index 0000000..51f9ddb --- /dev/null +++ b/protoutil/unmarshalers.go @@ -0,0 +1,259 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package protoutil + +import ( + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset" + "github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset" + "github.com/hyperledger/fabric-protos-go-apiv2/msp" + "github.com/hyperledger/fabric-protos-go-apiv2/peer" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" +) + +// the implicit contract of all these unmarshalers is that they +// will return a non-nil pointer whenever the error is nil + +// UnmarshalBlock unmarshal bytes to a Block +func UnmarshalBlock(encoded []byte) (*common.Block, error) { + block := &common.Block{} + err := proto.Unmarshal(encoded, block) + return block, errors.Wrap(err, "error unmarshalling Block") +} + +// UnmarshalChaincodeDeploymentSpec unmarshal bytes to a ChaincodeDeploymentSpec +func UnmarshalChaincodeDeploymentSpec(code []byte) (*peer.ChaincodeDeploymentSpec, error) { + cds := &peer.ChaincodeDeploymentSpec{} + err := proto.Unmarshal(code, cds) + return cds, errors.Wrap(err, "error unmarshalling ChaincodeDeploymentSpec") +} + +// UnmarshalChaincodeInvocationSpec unmarshal bytes to a ChaincodeInvocationSpec +func UnmarshalChaincodeInvocationSpec(encoded []byte) (*peer.ChaincodeInvocationSpec, error) { + cis := &peer.ChaincodeInvocationSpec{} + err := proto.Unmarshal(encoded, cis) + return cis, errors.Wrap(err, "error unmarshalling ChaincodeInvocationSpec") +} + +// UnmarshalPayload unmarshal bytes to a Payload +func UnmarshalPayload(encoded []byte) (*common.Payload, error) { + payload := &common.Payload{} + err := proto.Unmarshal(encoded, payload) + return payload, errors.Wrap(err, "error unmarshalling Payload") +} + +// UnmarshalEnvelope unmarshal bytes to a Envelope +func UnmarshalEnvelope(encoded []byte) (*common.Envelope, error) { + envelope := &common.Envelope{} + err := proto.Unmarshal(encoded, envelope) + return envelope, errors.Wrap(err, "error unmarshalling Envelope") +} + +// UnmarshalChannelHeader unmarshal bytes to a ChannelHeader +func UnmarshalChannelHeader(bytes []byte) (*common.ChannelHeader, error) { + chdr := &common.ChannelHeader{} + err := proto.Unmarshal(bytes, chdr) + return chdr, errors.Wrap(err, "error unmarshalling ChannelHeader") +} + +// UnmarshalChaincodeID unmarshal bytes to a ChaincodeID +func UnmarshalChaincodeID(bytes []byte) (*peer.ChaincodeID, error) { + ccid := &peer.ChaincodeID{} + err := proto.Unmarshal(bytes, ccid) + return ccid, errors.Wrap(err, "error unmarshalling ChaincodeID") +} + +// UnmarshalSignatureHeader unmarshal bytes to a SignatureHeader +func UnmarshalSignatureHeader(bytes []byte) (*common.SignatureHeader, error) { + sh := &common.SignatureHeader{} + err := proto.Unmarshal(bytes, sh) + return sh, errors.Wrap(err, "error unmarshalling SignatureHeader") +} + +// UnmarshalIdentifierHeader unmarshal bytes to an IdentifierHeader +func UnmarshalIdentifierHeader(bytes []byte) (*common.IdentifierHeader, error) { + ih := &common.IdentifierHeader{} + err := proto.Unmarshal(bytes, ih) + return ih, errors.Wrap(err, "error unmarshalling IdentifierHeader") +} + +func UnmarshalSerializedIdentity(bytes []byte) (*msp.SerializedIdentity, error) { + sid := &msp.SerializedIdentity{} + err := proto.Unmarshal(bytes, sid) + return sid, errors.Wrap(err, "error unmarshalling SerializedIdentity") +} + +// UnmarshalHeader unmarshal bytes to a Header +func UnmarshalHeader(bytes []byte) (*common.Header, error) { + hdr := &common.Header{} + err := proto.Unmarshal(bytes, hdr) + return hdr, errors.Wrap(err, "error unmarshalling Header") +} + +// UnmarshalConfigEnvelope unmarshal bytes to a ConfigEnvelope +func UnmarshalConfigEnvelope(bytes []byte) (*common.ConfigEnvelope, error) { + cfg := &common.ConfigEnvelope{} + err := proto.Unmarshal(bytes, cfg) + return cfg, errors.Wrap(err, "error unmarshalling ConfigEnvelope") +} + +// UnmarshalChaincodeHeaderExtension unmarshal bytes to a ChaincodeHeaderExtension +func UnmarshalChaincodeHeaderExtension(hdrExtension []byte) (*peer.ChaincodeHeaderExtension, error) { + chaincodeHdrExt := &peer.ChaincodeHeaderExtension{} + err := proto.Unmarshal(hdrExtension, chaincodeHdrExt) + return chaincodeHdrExt, errors.Wrap(err, "error unmarshalling ChaincodeHeaderExtension") +} + +// UnmarshalProposalResponse unmarshal bytes to a ProposalResponse +func UnmarshalProposalResponse(prBytes []byte) (*peer.ProposalResponse, error) { + proposalResponse := &peer.ProposalResponse{} + err := proto.Unmarshal(prBytes, proposalResponse) + return proposalResponse, errors.Wrap(err, "error unmarshalling ProposalResponse") +} + +// UnmarshalChaincodeAction unmarshal bytes to a ChaincodeAction +func UnmarshalChaincodeAction(caBytes []byte) (*peer.ChaincodeAction, error) { + chaincodeAction := &peer.ChaincodeAction{} + err := proto.Unmarshal(caBytes, chaincodeAction) + return chaincodeAction, errors.Wrap(err, "error unmarshalling ChaincodeAction") +} + +// UnmarshalResponse unmarshal bytes to a Response +func UnmarshalResponse(resBytes []byte) (*peer.Response, error) { + response := &peer.Response{} + err := proto.Unmarshal(resBytes, response) + return response, errors.Wrap(err, "error unmarshalling Response") +} + +// UnmarshalChaincodeEvents unmarshal bytes to a ChaincodeEvent +func UnmarshalChaincodeEvents(eBytes []byte) (*peer.ChaincodeEvent, error) { + chaincodeEvent := &peer.ChaincodeEvent{} + err := proto.Unmarshal(eBytes, chaincodeEvent) + return chaincodeEvent, errors.Wrap(err, "error unmarshalling ChaicnodeEvent") +} + +// UnmarshalProposalResponsePayload unmarshal bytes to a ProposalResponsePayload +func UnmarshalProposalResponsePayload(prpBytes []byte) (*peer.ProposalResponsePayload, error) { + prp := &peer.ProposalResponsePayload{} + err := proto.Unmarshal(prpBytes, prp) + return prp, errors.Wrap(err, "error unmarshalling ProposalResponsePayload") +} + +// UnmarshalProposal unmarshal bytes to a Proposal +func UnmarshalProposal(propBytes []byte) (*peer.Proposal, error) { + prop := &peer.Proposal{} + err := proto.Unmarshal(propBytes, prop) + return prop, errors.Wrap(err, "error unmarshalling Proposal") +} + +// UnmarshalTransaction unmarshal bytes to a Transaction +func UnmarshalTransaction(txBytes []byte) (*peer.Transaction, error) { + tx := &peer.Transaction{} + err := proto.Unmarshal(txBytes, tx) + return tx, errors.Wrap(err, "error unmarshalling Transaction") +} + +// UnmarshalChaincodeActionPayload unmarshal bytes to a ChaincodeActionPayload +func UnmarshalChaincodeActionPayload(capBytes []byte) (*peer.ChaincodeActionPayload, error) { + cap := &peer.ChaincodeActionPayload{} + err := proto.Unmarshal(capBytes, cap) + return cap, errors.Wrap(err, "error unmarshalling ChaincodeActionPayload") +} + +// UnmarshalChaincodeProposalPayload unmarshal bytes to a ChaincodeProposalPayload +func UnmarshalChaincodeProposalPayload(bytes []byte) (*peer.ChaincodeProposalPayload, error) { + cpp := &peer.ChaincodeProposalPayload{} + err := proto.Unmarshal(bytes, cpp) + return cpp, errors.Wrap(err, "error unmarshalling ChaincodeProposalPayload") +} + +// UnmarshalTxReadWriteSet unmarshal bytes to a TxReadWriteSet +func UnmarshalTxReadWriteSet(bytes []byte) (*rwset.TxReadWriteSet, error) { + rws := &rwset.TxReadWriteSet{} + err := proto.Unmarshal(bytes, rws) + return rws, errors.Wrap(err, "error unmarshalling TxReadWriteSet") +} + +// UnmarshalKVRWSet unmarshal bytes to a KVRWSet +func UnmarshalKVRWSet(bytes []byte) (*kvrwset.KVRWSet, error) { + rws := &kvrwset.KVRWSet{} + err := proto.Unmarshal(bytes, rws) + return rws, errors.Wrap(err, "error unmarshalling KVRWSet") +} + +// UnmarshalHashedRWSet unmarshal bytes to a HashedRWSet +func UnmarshalHashedRWSet(bytes []byte) (*kvrwset.HashedRWSet, error) { + hrws := &kvrwset.HashedRWSet{} + err := proto.Unmarshal(bytes, hrws) + return hrws, errors.Wrap(err, "error unmarshalling HashedRWSet") +} + +// UnmarshalSignaturePolicy unmarshal 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 unmarshal bytes to a Payload structure or panics +// on error +func UnmarshalPayloadOrPanic(encoded []byte) *common.Payload { + payload, err := UnmarshalPayload(encoded) + if err != nil { + panic(err) + } + return payload +} + +// UnmarshalEnvelopeOrPanic unmarshal bytes to an Envelope structure or panics +// on error +func UnmarshalEnvelopeOrPanic(encoded []byte) *common.Envelope { + envelope, err := UnmarshalEnvelope(encoded) + if err != nil { + panic(err) + } + return envelope +} + +// UnmarshalBlockOrPanic unmarshal bytes to an Block or panics +// on error +func UnmarshalBlockOrPanic(encoded []byte) *common.Block { + block, err := UnmarshalBlock(encoded) + if err != nil { + panic(err) + } + return block +} + +// UnmarshalChannelHeaderOrPanic unmarshal bytes to a ChannelHeader or panics +// on error +func UnmarshalChannelHeaderOrPanic(bytes []byte) *common.ChannelHeader { + chdr, err := UnmarshalChannelHeader(bytes) + if err != nil { + panic(err) + } + return chdr +} + +// UnmarshalSignatureHeaderOrPanic unmarshal bytes to a SignatureHeader or panics +// on error +func UnmarshalSignatureHeaderOrPanic(bytes []byte) *common.SignatureHeader { + sighdr, err := UnmarshalSignatureHeader(bytes) + if err != nil { + panic(err) + } + return sighdr +} +func UnmarshalTransactionActionOrPanic(bytes []byte) *peer.TransactionAction { + transactionAction := &peer.TransactionAction{} + err := proto.Unmarshal(bytes, transactionAction) + if err != nil { + panic(err) + } + return transactionAction +} From baeee2134f7bcb1326102623f62573bf439c4443 Mon Sep 17 00:00:00 2001 From: David Liu Date: Thu, 18 Jan 2024 04:35:24 +0800 Subject: [PATCH 2/2] init commit part2 Signed-off-by: David Liu --- common/crypto/random.go | 45 +++++++++++++++++++++ common/crypto/random_test.go | 25 ++++++++++++ common/go.mod | 15 +++++++ common/go.sum | 16 ++++++++ common/util/utils.go | 76 +++++++++++++++++++++++++++++++++++ common/util/utils_test.go | 78 ++++++++++++++++++++++++++++++++++++ protoutil/go.sum | 41 +++++++++++++++++++ scripts/run-unit-tests.sh | 4 ++ 8 files changed, 300 insertions(+) create mode 100644 common/crypto/random.go create mode 100644 common/crypto/random_test.go create mode 100644 common/go.mod create mode 100644 common/go.sum create mode 100644 common/util/utils.go create mode 100644 common/util/utils_test.go create mode 100644 protoutil/go.sum diff --git a/common/crypto/random.go b/common/crypto/random.go new file mode 100644 index 0000000..cb6fcb9 --- /dev/null +++ b/common/crypto/random.go @@ -0,0 +1,45 @@ +/* +Copyright IBM Corp. 2016 All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crypto + +import ( + "crypto/rand" + + "github.com/pkg/errors" +) + +const ( + // NonceSize is the default NonceSize + NonceSize = 24 +) + +// GetRandomBytes returns len random looking bytes +func GetRandomBytes(len int) ([]byte, error) { + key := make([]byte, len) + + _, err := rand.Read(key) + if err != nil { + return nil, errors.Wrap(err, "error getting random bytes") + } + + return key, nil +} + +// GetRandomNonce returns a random byte array of length NonceSize +func GetRandomNonce() ([]byte, error) { + return GetRandomBytes(NonceSize) +} diff --git a/common/crypto/random_test.go b/common/crypto/random_test.go new file mode 100644 index 0000000..d63ac02 --- /dev/null +++ b/common/crypto/random_test.go @@ -0,0 +1,25 @@ +/* +Copyright IBM Corp. 2017 All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package crypto + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetRandomBytes(t *testing.T) { + _, err := GetRandomBytes(10) + + require.NoError(t, err, "GetRandomBytes fails") +} + +func TestGetRandomNonce(t *testing.T) { + _, err := GetRandomNonce() + + require.NoError(t, err, "GetRandomNonce fails") +} diff --git a/common/go.mod b/common/go.mod new file mode 100644 index 0000000..dbe96ee --- /dev/null +++ b/common/go.mod @@ -0,0 +1,15 @@ +module github.com/hyperledger/fabric-lib-go/common + +go 1.20 + +require ( + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.8.4 + google.golang.org/protobuf v1.32.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/common/go.sum b/common/go.sum new file mode 100644 index 0000000..26f09a0 --- /dev/null +++ b/common/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/common/util/utils.go b/common/util/utils.go new file mode 100644 index 0000000..27c7d9d --- /dev/null +++ b/common/util/utils.go @@ -0,0 +1,76 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package util + +import ( + "crypto/rand" + "fmt" + "io" + "time" + + timestamp "google.golang.org/protobuf/types/known/timestamppb" +) + +// GenerateBytesUUID returns a UUID based on RFC 4122 returning the generated bytes +func GenerateBytesUUID() []byte { + uuid := make([]byte, 16) + _, err := io.ReadFull(rand.Reader, uuid) + if err != nil { + panic(fmt.Sprintf("Error generating UUID: %s", err)) + } + + // variant bits; see section 4.1.1 + uuid[8] = uuid[8]&^0xc0 | 0x80 + + // version 4 (pseudo-random); see section 4.1.3 + uuid[6] = uuid[6]&^0xf0 | 0x40 + + return uuid +} + +// GenerateUUID returns a UUID based on RFC 4122 +func GenerateUUID() string { + uuid := GenerateBytesUUID() + return idBytesToStr(uuid) +} + +// CreateUtcTimestamp returns a Timestamp protobuf in UTC +func CreateUtcTimestamp() *timestamp.Timestamp { + now := time.Now().UTC() + secs := now.Unix() + nanos := int32(now.UnixNano() - (secs * 1000000000)) + return &(timestamp.Timestamp{Seconds: secs, Nanos: nanos}) +} + +func idBytesToStr(id []byte) string { + return fmt.Sprintf("%x-%x-%x-%x-%x", id[0:4], id[4:6], id[6:8], id[8:10], id[10:]) +} + +// ToChaincodeArgs converts string args to []byte args +func ToChaincodeArgs(args ...string) [][]byte { + bargs := make([][]byte, len(args)) + for i, arg := range args { + bargs[i] = []byte(arg) + } + return bargs +} + +// ConcatenateBytes is useful for combining multiple arrays of bytes, especially for +// signatures or digests over multiple fields +// This way is more efficient in speed +func ConcatenateBytes(data ...[]byte) []byte { + finalLength := 0 + for _, slice := range data { + finalLength += len(slice) + } + result := make([]byte, finalLength) + last := 0 + for _, slice := range data { + last += copy(result[last:], slice) + } + return result +} diff --git a/common/util/utils_test.go b/common/util/utils_test.go new file mode 100644 index 0000000..9d5d194 --- /dev/null +++ b/common/util/utils_test.go @@ -0,0 +1,78 @@ +/* +Copyright IBM Corp. 2016 All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "bytes" + "testing" + "time" +) + +func TestUUIDGeneration(t *testing.T) { + uuid := GenerateUUID() + if len(uuid) != 36 { + t.Fatalf("UUID length is not correct. Expected = 36, Got = %d", len(uuid)) + } + uuid2 := GenerateUUID() + if uuid == uuid2 { + t.Fatalf("Two UUIDs are equal. This should never occur") + } +} + +func TestTimestamp(t *testing.T) { + for i := 0; i < 10; i++ { + t.Logf("timestamp now: %v", CreateUtcTimestamp()) + time.Sleep(200 * time.Millisecond) + } +} + +func TestToChaincodeArgs(t *testing.T) { + expected := [][]byte{[]byte("foo"), []byte("bar")} + actual := ToChaincodeArgs("foo", "bar") + if len(expected) != len(actual) { + t.Fatalf("Got %v, expected %v", actual, expected) + } + for i := range expected { + if !bytes.Equal(expected[i], actual[i]) { + t.Fatalf("Got %v, expected %v", actual, expected) + } + } +} + +func TestConcatenateBytesNormal(t *testing.T) { + first := []byte("first") + second := []byte("second") + third := []byte("third") + + result := ConcatenateBytes(first, second, third) + expected := []byte("firstsecondthird") + if !bytes.Equal(result, expected) { + t.Errorf("Did not concatenate bytes correctly, expected %s, got %s", expected, result) + } +} + +func TestConcatenateBytesNil(t *testing.T) { + first := []byte("first") + second := []byte(nil) + third := []byte("third") + + result := ConcatenateBytes(first, second, third) + expected := []byte("firstthird") + if !bytes.Equal(result, expected) { + t.Errorf("Did not concatenate bytes correctly, expected %s, got %s", expected, result) + } +} diff --git a/protoutil/go.sum b/protoutil/go.sum new file mode 100644 index 0000000..92a4882 --- /dev/null +++ b/protoutil/go.sum @@ -0,0 +1,41 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.2 h1:zL93mhCZbO99pzelinxqx+SahGAVafvbVR6nzRJkXts= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.2/go.mod h1:2pq0ui6ZWA0cC8J+eCErgnMDCS1kPOEYVY+06ZAK0qE= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scripts/run-unit-tests.sh b/scripts/run-unit-tests.sh index 91818e1..858bb15 100755 --- a/scripts/run-unit-tests.sh +++ b/scripts/run-unit-tests.sh @@ -6,4 +6,8 @@ # SPDX-License-Identifier: Apache-2.0 # +time go test -race -cover ./... +cd common +time go test -race -cover ./... +cd ../protoutil time go test -race -cover ./... \ No newline at end of file