From 069d0c61ccbab5ada008184a8963fa3c4784e457 Mon Sep 17 00:00:00 2001 From: Ishaan-Gpt Date: Tue, 6 Oct 2026 09:17:12 +0530 Subject: [PATCH] Add ParseCollectionConfig to build collection configs from a JSON string Defining private data collections currently requires building the peer.CollectionConfigPackage structure by hand, which is cumbersome and error prone. This adds a ParseCollectionConfig function in pkg/chaincode that accepts a collection configuration string in the same JSON format as the Fabric peer CLI collections configuration file and converts it into a peer.CollectionConfigPackage ready for ChaincodeDefinition.Collections. Member orgs policies reuse the existing signature policy parsing, and the required and maximum peer counts fall back to 0 and 1 when omitted, the same defaults the Fabric peer CLI uses. An optional endorsement policy can be given as a signature policy string or a channel config policy reference. Signed-off-by: Ishaan-Gpt --- pkg/chaincode/collectionconfig.go | 110 +++++++++++++++++++++++++ pkg/chaincode/collectionconfig_test.go | 110 +++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 pkg/chaincode/collectionconfig.go create mode 100644 pkg/chaincode/collectionconfig_test.go diff --git a/pkg/chaincode/collectionconfig.go b/pkg/chaincode/collectionconfig.go new file mode 100644 index 0000000..e4349d8 --- /dev/null +++ b/pkg/chaincode/collectionconfig.go @@ -0,0 +1,110 @@ +/* +Copyright IBM Corp. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package chaincode + +import ( + "encoding/json" + "fmt" + + "github.com/hyperledger/fabric-protos-go-apiv2/peer" +) + +// collectionConfigJSON describes a single private data collection. The format +// matches the collection configuration file accepted by the Fabric peer CLI, +// so existing collections configuration files can be reused as-is. +type collectionConfigJSON struct { + Name string `json:"name"` + Policy string `json:"policy"` + RequiredPeerCount *int32 `json:"requiredPeerCount"` + MaxPeerCount *int32 `json:"maxPeerCount"` + BlockToLive uint64 `json:"blockToLive"` + MemberOnlyRead bool `json:"memberOnlyRead"` + MemberOnlyWrite bool `json:"memberOnlyWrite"` + EndorsementPolicy *endorsementPolicyJSON `json:"endorsementPolicy,omitempty"` +} + +// endorsementPolicyJSON describes the optional endorsement policy of a +// collection. Either a signature policy string or a channel config policy +// reference may be given, but not both. +type endorsementPolicyJSON struct { + SignaturePolicy string `json:"signaturePolicy"` + ChannelConfigPolicy string `json:"channelConfigPolicy"` +} + +// ParseCollectionConfig takes a collection configuration string, using the +// same JSON format as the Fabric peer CLI collections configuration file, +// and converts it into a CollectionConfigPackage that can be used directly +// as ChaincodeDefinition.Collections. +// +// For example: +// +// collections, err := chaincode.ParseCollectionConfig(`[ +// { +// "name": "collectionMarbles", +// "policy": "OR('Org1MSP.member', 'Org2MSP.member')", +// "requiredPeerCount": 0, +// "maxPeerCount": 3, +// "blockToLive": 1000000, +// "memberOnlyRead": true +// } +// ]`) +func ParseCollectionConfig(configString string) (*peer.CollectionConfigPackage, error) { + var configs []collectionConfigJSON + if err := json.Unmarshal([]byte(configString), &configs); err != nil { + return nil, fmt.Errorf("error parsing collection configuration: %w", err) + } + + collectionConfigs := make([]*peer.CollectionConfig, 0, len(configs)) + for _, config := range configs { + memberOrgsPolicy, err := signaturePolicyEnvelopeFromString(config.Policy) + if err != nil { + return nil, fmt.Errorf("error parsing member orgs policy for collection %s: %w", config.Name, err) + } + + var endorsementPolicy *peer.ApplicationPolicy + if config.EndorsementPolicy != nil { + endorsementPolicy, err = NewApplicationPolicy( + config.EndorsementPolicy.SignaturePolicy, + config.EndorsementPolicy.ChannelConfigPolicy, + ) + if err != nil { + return nil, fmt.Errorf("error parsing endorsement policy for collection %s: %w", config.Name, err) + } + } + + // The required and maximum peer counts default to 0 and 1 when they + // are not specified, matching the Fabric peer CLI behavior. + var requiredPeerCount int32 + maxPeerCount := int32(1) + if config.RequiredPeerCount != nil { + requiredPeerCount = *config.RequiredPeerCount + } + if config.MaxPeerCount != nil { + maxPeerCount = *config.MaxPeerCount + } + + collectionConfigs = append(collectionConfigs, &peer.CollectionConfig{ + Payload: &peer.CollectionConfig_StaticCollectionConfig{ + StaticCollectionConfig: &peer.StaticCollectionConfig{ + Name: config.Name, + MemberOrgsPolicy: &peer.CollectionPolicyConfig{ + Payload: &peer.CollectionPolicyConfig_SignaturePolicy{ + SignaturePolicy: memberOrgsPolicy, + }, + }, + RequiredPeerCount: requiredPeerCount, + MaximumPeerCount: maxPeerCount, + BlockToLive: config.BlockToLive, + MemberOnlyRead: config.MemberOnlyRead, + MemberOnlyWrite: config.MemberOnlyWrite, + EndorsementPolicy: endorsementPolicy, + }, + }, + }) + } + + return &peer.CollectionConfigPackage{Config: collectionConfigs}, nil +} diff --git a/pkg/chaincode/collectionconfig_test.go b/pkg/chaincode/collectionconfig_test.go new file mode 100644 index 0000000..151fc56 --- /dev/null +++ b/pkg/chaincode/collectionconfig_test.go @@ -0,0 +1,110 @@ +/* +Copyright IBM Corp. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package chaincode_test + +import ( + "github.com/hyperledger/fabric-admin-sdk/pkg/chaincode" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ParseCollectionConfig", func() { + It("converts a collection configuration JSON string", func() { + collections, err := chaincode.ParseCollectionConfig(`[ + { + "name": "collectionMarbles", + "policy": "OR('Org1MSP.member', 'Org2MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 3, + "blockToLive": 1000000, + "memberOnlyRead": true, + "memberOnlyWrite": true + } + ]`) + Expect(err).NotTo(HaveOccurred()) + Expect(collections.GetConfig()).To(HaveLen(1)) + + config := collections.GetConfig()[0].GetStaticCollectionConfig() + Expect(config.GetName()).To(Equal("collectionMarbles")) + Expect(config.GetRequiredPeerCount()).To(Equal(int32(0))) + Expect(config.GetMaximumPeerCount()).To(Equal(int32(3))) + Expect(config.GetBlockToLive()).To(Equal(uint64(1000000))) + Expect(config.GetMemberOnlyRead()).To(BeTrue()) + Expect(config.GetMemberOnlyWrite()).To(BeTrue()) + Expect(config.GetEndorsementPolicy()).To(BeNil()) + + policy, err := chaincode.SignaturePolicyEnvelopeToString(config.GetMemberOrgsPolicy().GetSignaturePolicy()) + Expect(err).NotTo(HaveOccurred()) + Expect(policy).To(Equal("OR('Org1MSP.member','Org2MSP.member')")) + }) + + It("converts multiple collections", func() { + collections, err := chaincode.ParseCollectionConfig(`[ + { + "name": "collectionOne", + "policy": "OR('Org1MSP.member')" + }, + { + "name": "collectionTwo", + "policy": "OR('Org2MSP.member')" + } + ]`) + Expect(err).NotTo(HaveOccurred()) + Expect(collections.GetConfig()).To(HaveLen(2)) + Expect(collections.GetConfig()[0].GetStaticCollectionConfig().GetName()).To(Equal("collectionOne")) + Expect(collections.GetConfig()[1].GetStaticCollectionConfig().GetName()).To(Equal("collectionTwo")) + }) + + It("defaults the peer counts when they are omitted", func() { + collections, err := chaincode.ParseCollectionConfig(`[ + { + "name": "collectionPrivate", + "policy": "OR('Org1MSP.member')" + } + ]`) + Expect(err).NotTo(HaveOccurred()) + + config := collections.GetConfig()[0].GetStaticCollectionConfig() + Expect(config.GetRequiredPeerCount()).To(Equal(int32(0))) + Expect(config.GetMaximumPeerCount()).To(Equal(int32(1))) + }) + + It("supports an endorsement policy", func() { + collections, err := chaincode.ParseCollectionConfig(`[ + { + "name": "collectionMarbles", + "policy": "OR('Org1MSP.member')", + "endorsementPolicy": { + "signaturePolicy": "OR('Org1MSP.member', 'Org2MSP.member')" + } + } + ]`) + Expect(err).NotTo(HaveOccurred()) + + endorsementPolicy := collections.GetConfig()[0].GetStaticCollectionConfig().GetEndorsementPolicy() + Expect(endorsementPolicy).NotTo(BeNil()) + + policy, err := chaincode.SignaturePolicyEnvelopeToString(endorsementPolicy.GetSignaturePolicy()) + Expect(err).NotTo(HaveOccurred()) + Expect(policy).To(Equal("OR('Org1MSP.member','Org2MSP.member')")) + }) + + It("returns an error for invalid JSON", func() { + _, err := chaincode.ParseCollectionConfig(`not json`) + Expect(err).To(HaveOccurred()) + }) + + It("returns an error naming the collection for an invalid policy", func() { + _, err := chaincode.ParseCollectionConfig(`[ + { + "name": "collectionBad", + "policy": "BOGUS('Org1MSP.member')" + } + ]`) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("collectionBad")) + }) +})