diff --git a/observer/block_peer_test.go b/observer/block_peer_test.go new file mode 100644 index 0000000..8938672 --- /dev/null +++ b/observer/block_peer_test.go @@ -0,0 +1,128 @@ +package observer_test + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + sdkmocks "github.com/s7techlab/hlf-sdk-go/client/testing" + "github.com/s7techlab/hlf-sdk-go/observer" + testdata "github.com/s7techlab/hlf-sdk-go/testdata/blocks" +) + +var ( + ctx = context.Background() + + channelPeerMock *observer.ChannelPeerMock + blockPeer *observer.BlockPeer + + channelPeerMockConcurrently *observer.ChannelPeerMock + blockPeerConcurrently *observer.BlockPeer + blocksByChannels *observer.BlocksByChannels +) + +var _ = BeforeSuite(func() { + const closeChannelWhenAllRead = true + blockDelivererMock, err := sdkmocks.NewBlocksDelivererMock(fmt.Sprintf("../%s", testdata.Path), closeChannelWhenAllRead) + Expect(err).ShouldNot(HaveOccurred()) + + channelPeerMock = observer.NewChannelPeerMock() + for _, channel := range testdata.Channels { + channelPeerMock.UpdateChannelInfo(&observer.ChannelInfo{Channel: channel}) + } + + blockPeer = observer.NewBlockPeer(channelPeerMock, blockDelivererMock, + observer.WithBlockStopRecreateStream(true), observer.WithBlockPeerObservePeriod(time.Nanosecond)) + + _, err = blockPeer.Observe(ctx) + Expect(err).ShouldNot(HaveOccurred()) + + channelPeerMockConcurrently = observer.NewChannelPeerMock() + for _, channel := range testdata.Channels { + channelPeerMockConcurrently.UpdateChannelInfo(&observer.ChannelInfo{Channel: channel}) + } + + blockPeerConcurrently = observer.NewBlockPeer(channelPeerMockConcurrently, blockDelivererMock, + observer.WithBlockStopRecreateStream(true), observer.WithBlockPeerObservePeriod(time.Nanosecond)) + + blocksByChannels, err = blockPeerConcurrently.ObserveByChannels(ctx) + Expect(err).ShouldNot(HaveOccurred()) +}) + +var _ = Describe("Block Peer", func() { + Context("Channels number check", func() { + Context("Block peer", func() { + It("should return current number of channels", func() { + channelObservers := blockPeer.ChannelObservers() + Expect(channelObservers).To(HaveLen(len(testdata.Channels))) + }) + + It("should add channels to channelPeerMock", func() { + newChannels := []string{"channel1", "channel2", "channel3"} + for _, channel := range newChannels { + channelPeerMock.UpdateChannelInfo(&observer.ChannelInfo{Channel: channel}) + } + + // wait to blockPeer observer + time.Sleep(time.Millisecond * 10) + + channelObservers := blockPeer.ChannelObservers() + Expect(channelObservers).To(HaveLen(len(testdata.Channels) + len(newChannels))) + }) + }) + + Context("Block peer concurrently", func() { + It("should return current number of channels", func() { + channelObservers := blockPeerConcurrently.ChannelObservers() + Expect(channelObservers).To(HaveLen(len(testdata.Channels))) + + channelsWithBlocks := blocksByChannels.Observe() + + for i := 0; i < len(testdata.Channels); i++ { + sampleOrFabcarChannelBlocks := <-channelsWithBlocks + if sampleOrFabcarChannelBlocks.Name == testdata.SampleChannel { + Expect(sampleOrFabcarChannelBlocks.Name).To(Equal(testdata.SampleChannel)) + } else { + Expect(sampleOrFabcarChannelBlocks.Name).To(Equal(testdata.FabcarChannel)) + } + + Expect(sampleOrFabcarChannelBlocks.Blocks).NotTo(BeNil()) + } + }) + + It("should add channels to channelPeerMock", func() { + channel4, channel5, channel6 := "channel4", "channel5", "channel6" + newChannels := []string{channel4, channel5, channel6} + for _, channel := range newChannels { + channelPeerMockConcurrently.UpdateChannelInfo(&observer.ChannelInfo{Channel: channel}) + } + + // wait to blockPeer observer + time.Sleep(time.Millisecond * 200) + + channelObservers := blockPeerConcurrently.ChannelObservers() + Expect(channelObservers).To(HaveLen(len(testdata.Channels) + len(newChannels))) + + channelsWithBlocks := blocksByChannels.Observe() + + for i := 0; i < len(newChannels); i++ { + channel4Or5Or6Blocks := <-channelsWithBlocks + + if channel4Or5Or6Blocks.Name == channel4 { + Expect(channel4Or5Or6Blocks.Name).To(Equal(channel4)) + Expect(channel4Or5Or6Blocks.Blocks).NotTo(BeNil()) + } else if channel4Or5Or6Blocks.Name == channel5 { + Expect(channel4Or5Or6Blocks.Name).To(Equal(channel5)) + Expect(channel4Or5Or6Blocks.Blocks).NotTo(BeNil()) + } else { + Expect(channel4Or5Or6Blocks.Name).To(Equal(channel6)) + Expect(channel4Or5Or6Blocks.Blocks).NotTo(BeNil()) + } + } + }) + }) + }) +}) diff --git a/observer/channel_peer_test.go b/observer/channel_peer_test.go new file mode 100644 index 0000000..1e61264 --- /dev/null +++ b/observer/channel_peer_test.go @@ -0,0 +1,60 @@ +package observer_test + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/s7techlab/hlf-sdk-go/observer" + testdata "github.com/s7techlab/hlf-sdk-go/testdata/blocks" +) + +var _ = Describe("Channel peer", func() { + var ( + channelPeerFetcherMock observer.PeerChannelsFetcher + ) + BeforeEach(func() { + channelPeerFetcherMock = observer.NewChannelPeerFetcherMock(testdata.ChannelsHeights) + }) + + It("default channel peer, no channel matcher", func() { + channelPeer, err := observer.NewChannelPeer(channelPeerFetcherMock) + Expect(err).To(BeNil()) + + channelPeer.Observe(ctx) + time.Sleep(time.Millisecond * 100) + + channelsMap := channelPeer.Channels() + + sampleChannelInfo, exist := channelsMap[testdata.SampleChannel] + Expect(exist).To(BeTrue()) + Expect(sampleChannelInfo.Channel).To(Equal(testdata.SampleChannel)) + Expect(sampleChannelInfo.Height).To(Equal(testdata.SampleChannelHeight)) + + fabcarChannelInfo, exist := channelsMap[testdata.FabcarChannel] + Expect(exist).To(BeTrue()) + Expect(fabcarChannelInfo.Channel).To(Equal(testdata.FabcarChannel)) + Expect(fabcarChannelInfo.Height).To(Equal(testdata.FabcarChannelHeight)) + }) + + It("default channel peer, with channel matcher, exclude Fabcar", func() { + channelPeer, err := observer.NewChannelPeer(channelPeerFetcherMock, + observer.WithChannels([]observer.ChannelToMatch{{MatchPattern: testdata.SampleChannel}})) + Expect(err).To(BeNil()) + + channelPeer.Observe(ctx) + time.Sleep(time.Millisecond * 100) + + channelsMap := channelPeer.Channels() + + sampleChannelInfo, exist := channelsMap[testdata.SampleChannel] + Expect(exist).To(BeTrue()) + Expect(sampleChannelInfo.Channel).To(Equal(testdata.SampleChannel)) + Expect(sampleChannelInfo.Height).To(Equal(testdata.SampleChannelHeight)) + + fabcarChannelInfo, exist := channelsMap[testdata.FabcarChannel] + Expect(exist).To(BeFalse()) + Expect(fabcarChannelInfo).To(BeNil()) + }) +}) diff --git a/observer/observer.test b/observer/observer.test deleted file mode 100644 index 9f4c073..0000000 Binary files a/observer/observer.test and /dev/null differ diff --git a/observer/transform/action.go b/observer/transform/action.go new file mode 100644 index 0000000..7b83c8c --- /dev/null +++ b/observer/transform/action.go @@ -0,0 +1,192 @@ +package transform + +import ( + "fmt" + "regexp" + + "github.com/mohae/deepcopy" + + "github.com/s7techlab/hlf-sdk-go/observer" + hlfproto "github.com/s7techlab/hlf-sdk-go/proto" +) + +type ( + Action struct { + match TxActionMatch + inputArgsTransformers []InputArgsTransformer + kvWriteTransformers []KVWriteTransformer + kvReadTransformers []KVReadTransformer + eventTransformers []EventTransformer + actionPayloadTransformers []ActionPayloadTransformer + } + + ActionOpt func(*Action) + + TxActionMatch func(*hlfproto.TransactionAction) bool + TxActionMutate func(*hlfproto.TransactionAction) +) + +func WithInputArgsTransformer(inputArgsTransformers ...InputArgsTransformer) ActionOpt { + return func(a *Action) { + a.inputArgsTransformers = inputArgsTransformers + } +} + +func WithKVWriteTransformer(kvWriteTransformers ...KVWriteTransformer) ActionOpt { + return func(a *Action) { + a.kvWriteTransformers = kvWriteTransformers + } +} + +func WithKVReadTransformer(kvReadTransformers ...KVReadTransformer) ActionOpt { + return func(a *Action) { + a.kvReadTransformers = kvReadTransformers + } +} + +func WithEventTransformer(eventTransformers ...EventTransformer) ActionOpt { + return func(a *Action) { + a.eventTransformers = eventTransformers + } +} + +func WithActionPayloadTransformer(actionTransformers ...ActionPayloadTransformer) ActionOpt { + return func(a *Action) { + a.actionPayloadTransformers = actionTransformers + } +} + +func NewAction(actionMach TxActionMatch, opts ...ActionOpt) *Action { + a := &Action{ + match: actionMach, + } + + for _, opt := range opts { + opt(a) + } + + return a +} + +func (s *Action) Transform(block *observer.Block) error { + if block.Block == nil { + return nil + } + + // if block is transformed, copy of block will be saved to block.BlockOriginal + blockCopy := deepcopy.Copy(block.Block).(*hlfproto.Block) + blockIsTransformed := false + + for _, envelope := range block.Block.Envelopes { + if envelope.Transaction == nil { + continue + } + + for _, txAction := range envelope.Transaction.Actions { + if !s.match(txAction) { + continue + } + + for _, argsTransformer := range s.inputArgsTransformers { + if err := argsTransformer.Transform(txAction.ChaincodeInvocationSpec.ChaincodeSpec.Input.Args); err != nil { + return fmt.Errorf(`args transformer: %w`, err) + } + } + + for _, eventTransformer := range s.eventTransformers { + if err := eventTransformer.Transform(txAction.Event); err != nil { + return fmt.Errorf(`event transformer: %w`, err) + } + } + + for _, rwSet := range txAction.ReadWriteSets { + for _, write := range rwSet.Writes { + for _, kvWriteTransformer := range s.kvWriteTransformers { + origKey := write.Key + if err := kvWriteTransformer.Transform(write); err != nil { + return fmt.Errorf(`KV write transformer with key: %s: %w`, write.Key, err) + } + + if origKey != write.Key { + blockIsTransformed = true + } + } + } + + for _, read := range rwSet.Reads { + for _, kvReadTransform := range s.kvReadTransformers { + origKey := read.Key + if err := kvReadTransform.Transform(read); err != nil { + return fmt.Errorf(`KV read transformer with key: %s: %w`, read.Key, err) + } + if origKey != read.Key { + blockIsTransformed = true + } + } + } + + for _, actionPayloadTransform := range s.actionPayloadTransformers { + if err := actionPayloadTransform.Transform(txAction); err != nil { + return fmt.Errorf(`action payload transform: %w`, err) + } + } + } + } + } + + if blockIsTransformed { + block.BlockOriginal = blockCopy + } + + return nil +} + +func TxChaincodeIDMatch(chaincode string) TxActionMatch { + return func(action *hlfproto.TransactionAction) bool { + return action.ChaincodeInvocationSpec.ChaincodeSpec.ChaincodeId.Name == chaincode + } +} +func TxChaincodesIDMatch(chaincodes ...string) TxActionMatch { + return func(action *hlfproto.TransactionAction) bool { + for k := range chaincodes { + if action.ChaincodeInvocationSpec.ChaincodeSpec.ChaincodeId.Name == chaincodes[k] { + return true + } + } + return false + } +} + +func TxChaincodesIDRegexp(chaincodePattern string) TxActionMatch { + return func(action *hlfproto.TransactionAction) bool { + matched, _ := regexp.MatchString(chaincodePattern, action.ChaincodeInvocationSpec.ChaincodeSpec.ChaincodeId.Name) + + return matched + } +} +func TxChaincodesIDRegexpExclude(chaincodePattern string) TxActionMatch { + return func(action *hlfproto.TransactionAction) bool { + matched, _ := regexp.MatchString(chaincodePattern, action.ChaincodeInvocationSpec.ChaincodeSpec.ChaincodeId.Name) + + return !matched + } +} + +func TxChaincodePatternsIDRegexpExclude(chaincodePatterns ...string) TxActionMatch { + return func(action *hlfproto.TransactionAction) bool { + isInSlice := false + for key := range chaincodePatterns { + matched, _ := regexp.MatchString(chaincodePatterns[key], action.ChaincodeInvocationSpec.ChaincodeSpec.ChaincodeId.Name) + if matched { + isInSlice = true + } + } + return !isInSlice + } +} + +func TxChaincodeAnyMatch() TxActionMatch { + return func(action *hlfproto.TransactionAction) bool { + return true + } +} diff --git a/observer/transform/action_payload.go b/observer/transform/action_payload.go new file mode 100644 index 0000000..f8b2d52 --- /dev/null +++ b/observer/transform/action_payload.go @@ -0,0 +1,63 @@ +package transform + +import ( + "fmt" + + "github.com/golang/protobuf/proto" + + hlfproto "github.com/s7techlab/hlf-sdk-go/proto" +) + +type ( + ActionPayloadTransformer interface { + Transform(*hlfproto.TransactionAction) error + } + ActionPayloadMatch func(*hlfproto.TransactionAction) bool + ActionPayloadMutate func(*hlfproto.TransactionAction) error + + ActionPayload struct { + match ActionPayloadMatch + mutators []ActionPayloadMutate + } +) + +func NewActionPayload(match ActionPayloadMatch, mutators ...ActionPayloadMutate) *ActionPayload { + return &ActionPayload{ + match: match, + mutators: mutators, + } +} + +func (action *ActionPayload) Transform(txAction *hlfproto.TransactionAction) error { + if !action.match(txAction) { + return nil + } + for _, mutate := range action.mutators { + if err := mutate(txAction); err != nil { + return fmt.Errorf(`kv write mutate: %w`, err) + } + } + return nil +} + +func ActionPayloadMatchNil(txAction *hlfproto.TransactionAction) bool { + return txAction != nil +} + +func ActionPayloadMutateProto(target proto.Message) ActionPayloadMutate { + return func(txAction *hlfproto.TransactionAction) error { + payloadJSON, err := Proto2JSON(txAction.Payload, target) + if err != nil { + return err + } + txAction.Payload = payloadJSON + return nil + } +} + +func ActionPayloadProto(txAction proto.Message) *ActionPayload { + return NewActionPayload( + ActionPayloadMatchNil, + ActionPayloadMutateProto(txAction), + ) +} diff --git a/observer/transform/args.go b/observer/transform/args.go new file mode 100644 index 0000000..72301d5 --- /dev/null +++ b/observer/transform/args.go @@ -0,0 +1,73 @@ +package transform + +import ( + "fmt" + + "github.com/golang/protobuf/proto" +) + +type ( + InputArgsTransformer interface { + Transform([][]byte) error + } + InputArgsMatch func([][]byte) bool + InputArgsMutate func([][]byte) error + + InputArgs struct { + match InputArgsMatch + mutate InputArgsMutate + } +) + +func NewInputArgs(match InputArgsMatch, mutate InputArgsMutate) *InputArgs { + return &InputArgs{ + match: match, + mutate: mutate, + } +} + +func (tr *InputArgs) Transform(args [][]byte) error { + if tr.match(args) { + return tr.mutate(args) + } + return nil +} + +func InputArgsProto(fn string, target proto.Message) *InputArgs { + return NewInputArgs( + InputArgsMatchFunc(fn), + InputArgsMutateProto(target), + ) +} + +func InputArgsMatchFunc(fn string) InputArgsMatch { + return InputArgsMatchString(0, fn) // fn is on pos=0 +} + +func InputArgsMatchString(pos int, str string) InputArgsMatch { + return func(args [][]byte) bool { + if len(args) > pos && string(args[pos]) == str { + return true + } + return false + } +} + +func InputArgsMutateProtoAtPos(target proto.Message, pos int) InputArgsMutate { + return func(args [][]byte) error { + if len(args) < pos+1 || len(args[pos]) == 0 { + return nil + } + arg, err := Proto2JSON(args[pos], target) + if err != nil { + return fmt.Errorf(`args mutator pos=%d: %w`, pos, err) + } + + args[pos] = arg + return nil + } +} + +func InputArgsMutateProto(target proto.Message) InputArgsMutate { + return InputArgsMutateProtoAtPos(target, 1) // args[0] - func name, args[1] - proto by default +} diff --git a/observer/transform/event.go b/observer/transform/event.go new file mode 100644 index 0000000..970cd2e --- /dev/null +++ b/observer/transform/event.go @@ -0,0 +1,60 @@ +package transform + +import ( + "fmt" + + "github.com/golang/protobuf/proto" + "github.com/hyperledger/fabric-protos-go/peer" +) + +type ( + EventTransformer interface { + Transform(*peer.ChaincodeEvent) error + } + EventMatch func(string) bool + EventMutate func(*peer.ChaincodeEvent) error + + Event struct { + match EventMatch + mutate EventMutate + } +) + +func NewEvent(match EventMatch, mutate EventMutate) *Event { + return &Event{ + match: match, + mutate: mutate, + } +} + +func (e *Event) Transform(event *peer.ChaincodeEvent) error { + if e.match(event.EventName) { + return e.mutate(event) + } + return nil +} + +func EventProto(eventName string, target proto.Message) *Event { + return NewEvent( + EventMatchFunc(eventName), + EventMutateProto(target), + ) +} + +func EventMatchFunc(str string) EventMatch { + return func(eventName string) bool { + return eventName == str + } +} + +func EventMutateProto(target proto.Message) EventMutate { + return func(event *peer.ChaincodeEvent) error { + payloadJSON, err := Proto2JSON(event.Payload, target) + if err != nil { + return fmt.Errorf(`event payload mutator: %w`, err) + } + + event.Payload = payloadJSON + return nil + } +} diff --git a/observer/transform/kvread.go b/observer/transform/kvread.go new file mode 100644 index 0000000..afa59b1 --- /dev/null +++ b/observer/transform/kvread.go @@ -0,0 +1,107 @@ +package transform + +import ( + "fmt" + "strings" + + "github.com/golang/protobuf/proto" + "github.com/hyperledger/fabric-protos-go/ledger/rwset/kvrwset" + + "github.com/s7techlab/hlf-sdk-go/util" +) + +type ( + KVReadTransformer interface { + Transform(*kvrwset.KVRead) error + } + + KVReadMatch func(*kvrwset.KVRead) bool + KVReadMutate func(*kvrwset.KVRead) error + + KVRead struct { + match KVReadMatch + mutators []KVReadMutate + } +) + +func NewKVRead(match KVReadMatch, mutators ...KVReadMutate) *KVRead { + return &KVRead{ + match: match, + mutators: mutators, + } +} + +func (kvread *KVRead) Transform(w *kvrwset.KVRead) error { + if !kvread.match(w) { + return nil + } + for _, mutate := range kvread.mutators { + if err := mutate(w); err != nil { + return fmt.Errorf(`kv read mutate: %w`, err) + } + } + return nil +} + +func KVReadMatchKeyPrefix(prefixes ...string) KVReadMatch { + return func(read *kvrwset.KVRead) bool { + keyPrefix, _ := util.SplitCompositeKey(read.Key) + for _, prefix := range prefixes { + if keyPrefix == prefix { + return true + } + } + + return false + } +} + +func KVReadProtoWithKeyPrefix(prefix string, target proto.Message) *KVRead { + return NewKVRead( + KVReadMatchKeyPrefix(prefix), + ) +} + +func KVReadMutatorWithKeyPrefix(prefix string, mutator KVReadMutate) *KVRead { + return NewKVRead( + KVReadMatchKeyPrefix(prefix), + mutator, + ) +} + +func KVReadKeyObjectTypeReplaceByMap(mapping map[string]string, additionalMutators ...KVReadMutate) *KVRead { + return NewKVRead( + KVReadMatchKeyPrefix(MappingPrefixes(mapping)...), + append([]KVReadMutate{KVReadKeyReplacer(mapping)}, additionalMutators...)..., + ) +} + +func KVReadKeyReplacer(mapping map[string]string) KVReadMutate { + return func(read *kvrwset.KVRead) error { + prefix, attributes := util.SplitCompositeKey(read.Key) + if mappedPrefix, ok := mapping[prefix]; ok { + mappedKey, err := util.CreateCompositeKey(mappedPrefix, attributes) + if err != nil { + return fmt.Errorf(`create mapped composite key: %w`, err) + } + read.Key = mappedKey + } + return nil + } +} + +func KVReadKeyReplace(mapping map[string]string, additionalMutators ...KVReadMutate) *KVRead { + return NewKVRead(KVReadMatchKey(MappingPrefixes(mapping)...), additionalMutators...) +} + +func KVReadMatchKey(contents ...string) KVReadMatch { + return func(read *kvrwset.KVRead) bool { + for _, content := range contents { + if strings.Contains(read.Key, content) { + return true + } + } + + return false + } +} diff --git a/observer/transform/kvwrite.go b/observer/transform/kvwrite.go new file mode 100644 index 0000000..308853c --- /dev/null +++ b/observer/transform/kvwrite.go @@ -0,0 +1,119 @@ +package transform + +import ( + "fmt" + "strings" + + "github.com/golang/protobuf/proto" + "github.com/hyperledger/fabric-protos-go/ledger/rwset/kvrwset" + + "github.com/s7techlab/hlf-sdk-go/util" +) + +type ( + KVWriteTransformer interface { + Transform(*kvrwset.KVWrite) error + } + KVWriteMatch func(*kvrwset.KVWrite) bool + KVWriteMutate func(*kvrwset.KVWrite) error + + KVWrite struct { + match KVWriteMatch + mutators []KVWriteMutate + } +) + +func NewKVWrite(match KVWriteMatch, mutators ...KVWriteMutate) *KVWrite { + return &KVWrite{ + match: match, + mutators: mutators, + } +} + +func (kvwrite *KVWrite) Transform(w *kvrwset.KVWrite) error { + if !kvwrite.match(w) { + return nil + } + for _, mutate := range kvwrite.mutators { + if err := mutate(w); err != nil { + return fmt.Errorf(`kv write mutate: %w`, err) + } + } + return nil +} + +func KVWriteMatchKeyPrefix(prefixes ...string) KVWriteMatch { + return func(write *kvrwset.KVWrite) bool { + keyPrefix, _ := util.SplitCompositeKey(write.Key) + for _, prefix := range prefixes { + if keyPrefix == prefix { + return true + } + } + + return false + } +} + +func KVWriteMutateProto(target proto.Message) KVWriteMutate { + return func(write *kvrwset.KVWrite) error { + value, err := Proto2JSON(write.Value, target) + if err != nil { + return fmt.Errorf(`write mutator key=%s: %w`, write.Key, err) + } + + write.Value = value + return nil + } +} + +func KVWriteProtoWithKeyPrefix(prefix string, target proto.Message) *KVWrite { + return NewKVWrite( + KVWriteMatchKeyPrefix(prefix), + KVWriteMutateProto(target), + ) +} + +func KVWriteMutatorWithKeyPrefix(prefix string, mutator KVWriteMutate) *KVWrite { + return NewKVWrite( + KVWriteMatchKeyPrefix(prefix), + mutator, + ) +} + +func KVWriteKeyObjectTypeReplaceByMap(mapping map[string]string, additionalMutators ...KVWriteMutate) *KVWrite { + return NewKVWrite( + KVWriteMatchKeyPrefix(MappingPrefixes(mapping)...), + append([]KVWriteMutate{KVWriteKeyReplacer(mapping)}, additionalMutators...)..., + ) +} + +func KVWriteKeyReplacer(mapping map[string]string) KVWriteMutate { + return func(write *kvrwset.KVWrite) error { + prefix, attributes := util.SplitCompositeKey(write.Key) + if mappedPrefix, ok := mapping[prefix]; ok { + mappedKey, err := util.CreateCompositeKey(mappedPrefix, attributes) + if err != nil { + return fmt.Errorf(`create mapped composite key: %w`, err) + } + write.Key = mappedKey + } + return nil + } +} + +func KVWriteKeyReplace(mapping map[string]string, additionalMutators ...KVWriteMutate) *KVWrite { + return NewKVWrite(KVWriteMatchKey(MappingPrefixes(mapping)...), additionalMutators...) +} + +func KVWriteMatchKey(contents ...string) KVWriteMatch { + return func(write *kvrwset.KVWrite) bool { + for _, content := range contents { + if strings.Contains(write.Key, content) { + return true + } + } + + return false + } +} diff --git a/observer/transform/lifecycle.go b/observer/transform/lifecycle.go new file mode 100644 index 0000000..dec3b10 --- /dev/null +++ b/observer/transform/lifecycle.go @@ -0,0 +1,79 @@ +package transform + +import ( + "fmt" + "strings" + + "github.com/hyperledger/fabric-protos-go/ledger/rwset/kvrwset" + + "github.com/s7techlab/hlf-sdk-go/observer" +) + +const ( + LifecycleChaincodeName = "_lifecycle" + + // MetadataPrefix - это префикс ключа стейта, который хранит информацию о ключах + // в соответствующем неймспейсе. У каждого закомиченного чейнкода в канале есть такой стейт + MetadataPrefix = "namespaces/metadata" + + // FieldsPrefix - префикс ключа стейтов, которые хранят параметры + // закомиченного чейнкода в канале + FieldsPrefix = "namespaces/fields" + + // CollectionField ValidationInfoField EndorsementInfoField SequenceField - суффиксы ключей стейтов, + // которые хранят параметры закомиченного чейнкода в канале + CollectionField = "Collections" + ValidationInfoField = "ValidationInfo" + EndorsementInfoField = "EndorsementInfo" + SequenceField = "Sequence" + + Collection = FieldsPrefix + "/" + CollectionField + ValidationInfo = FieldsPrefix + "/" + ValidationInfoField + EndorsementInfo = FieldsPrefix + "/" + EndorsementInfoField + Sequence = FieldsPrefix + "/" + SequenceField + + strByteZero = string(byte(0)) +) + +func keyReplace(key string) string { + // lifecycle key is look like 'namespaces/metadata/{chaincode_id}' or 'namespaces/fields/{chaincode_id}/{field}' + splitKey := strings.Split(key, "/") + switch splitKey[1] { + case "metadata": + // here 3 elements: [namespaces, metadata, {chaincode_id}] + // make key '{zeroByte}namespaces/metadata/{zeroByte}{chaincode_id}{zeroByte} + key = fmt.Sprintf("%s%s/%s%s%s%s", strByteZero, splitKey[0], splitKey[1], strByteZero, splitKey[2], strByteZero) + + case "fields": + // here 4 elements: [namespaces, fields, {chaincode_id}, {field}] + // make key '{zeroByte}namespaces/fields/{field}{zeroByte}{chaincode_id}{zeroByte} + key = fmt.Sprintf("%s%s/%s/%s%s%s%s", strByteZero, splitKey[0], splitKey[1], splitKey[3], strByteZero, splitKey[2], strByteZero) + } + + return key +} + +var LifecycleTransformers = []observer.BlockTransformer{ + NewAction( + TxChaincodeIDMatch(LifecycleChaincodeName), + WithKVWriteTransformer( + KVWriteKeyReplace(LifecycleStateKeyStrMapping(), func(write *kvrwset.KVWrite) error { + write.Key = keyReplace(write.Key) + return nil + }), + ), + WithKVReadTransformer( + KVReadKeyReplace(LifecycleStateKeyStrMapping(), func(read *kvrwset.KVRead) error { + read.Key = keyReplace(read.Key) + return nil + }), + ), + ), +} + +func LifecycleStateKeyStrMapping() map[string]string { + mapping := make(map[string]string) + mapping[MetadataPrefix] = strByteZero + mapping[FieldsPrefix] = strByteZero + return mapping +} diff --git a/observer/transform/map_prefix.go b/observer/transform/map_prefix.go new file mode 100644 index 0000000..91b2b75 --- /dev/null +++ b/observer/transform/map_prefix.go @@ -0,0 +1,10 @@ +package transform + +func MappingPrefixes(mapping map[string]string) []string { + prefixes := make([]string, 0) + for prefix := range mapping { + prefixes = append(prefixes, prefix) + } + + return prefixes +} diff --git a/observer/transform/proto.go b/observer/transform/proto.go new file mode 100644 index 0000000..569e456 --- /dev/null +++ b/observer/transform/proto.go @@ -0,0 +1,26 @@ +package transform + +import ( + "fmt" + "reflect" + + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/proto" +) + +var jsonpbMarshaler = &jsonpb.Marshaler{EmitDefaults: true} + +func Proto2JSON(serialized []byte, target proto.Message) ([]byte, error) { + m := proto.Clone(target) + + if err := proto.Unmarshal(serialized, m); err != nil { + return nil, fmt.Errorf(`proto unmarshal to=%s: %w`, reflect.TypeOf(target), err) + } + + s, err := jsonpbMarshaler.MarshalToString(m) + if err != nil { + return nil, fmt.Errorf(`json pb marshal: %w`, err) + } + + return []byte(s), nil +}