diff --git a/Makefile b/Makefile index 49ebd7b..4536310 100644 --- a/Makefile +++ b/Makefile @@ -43,10 +43,10 @@ FABRIC_TOOLS_TAG ?= $(ARCH)-$(FABRIC_TOOLS_VERSION) # Fabric peer ext docker image (overridable) FABRIC_PEER_EXT_IMAGE ?= trustbloc/fabric-peer -FABRIC_PEER_EXT_VERSION ?= 0.1.2-snapshot-e21ab2b +FABRIC_PEER_EXT_VERSION ?= 0.1.2-snapshot-fc25d20 FABRIC_PEER_EXT_TAG ?= $(ARCH)-$(FABRIC_PEER_EXT_VERSION) -export FABRIC_CLI_EXT_VERSION ?= 3fd66894726c1afcd904413dcfa3b4d586ea6c92 +export FABRIC_CLI_EXT_VERSION ?= fce574c704389e1741dc9edb40456fd7877d42d5 # Namespace for the blocnode image DOCKER_OUTPUT_NS ?= trustbloc diff --git a/cmd/peer/go.mod b/cmd/peer/go.mod index d53637d..9d501b5 100644 --- a/cmd/peer/go.mod +++ b/cmd/peer/go.mod @@ -13,9 +13,9 @@ require ( replace github.com/hyperledger/fabric => github.com/trustbloc/fabric-mod v0.1.2-0.20200211211900-62c249e072e2 -replace github.com/hyperledger/fabric/extensions => github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200211231405-e21ab2bc259f +replace github.com/hyperledger/fabric/extensions => github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200218213141-fc25d209285c -replace github.com/trustbloc/fabric-peer-ext => github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200211231405-e21ab2bc259f +replace github.com/trustbloc/fabric-peer-ext => github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200218213141-fc25d209285c replace github.com/trustbloc/sidetree-fabric => ../.. diff --git a/cmd/peer/go.sum b/cmd/peer/go.sum index d806963..5698c40 100644 --- a/cmd/peer/go.sum +++ b/cmd/peer/go.sum @@ -340,10 +340,10 @@ github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955u github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/trustbloc/fabric-mod v0.1.2-0.20200211211900-62c249e072e2 h1:gHS0f6VQsWxMJrUshduwpHvQIApmBDCvdnrqKld9wfo= github.com/trustbloc/fabric-mod v0.1.2-0.20200211211900-62c249e072e2/go.mod h1:2UoZyvViPfKpCemDr/N2uyo2S+dSI4KbDqVmMePyOes= -github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200211231405-e21ab2bc259f h1:4MJmpaFGySX+hpX1AnvOqpJITza592hu+ADSfHTGlJg= -github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200211231405-e21ab2bc259f/go.mod h1:zdtIRio2I5i5QxLyKZbTnQqL8xmcF45RdAlopd/Evqs= -github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200211231405-e21ab2bc259f h1:krvThqJoCQpJPvf0lQv+zHesRj5SrbXhnhmrUaF6kdQ= -github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200211231405-e21ab2bc259f/go.mod h1:ot55KksVO/lnRDVVNDF14AtdS7CSs//NE20WU6x/eiw= +github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200218213141-fc25d209285c h1:Jx+movyiTkGV5eB5lVq3XXGv3vqU/af+rBWvIG+VGBs= +github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200218213141-fc25d209285c/go.mod h1:zdtIRio2I5i5QxLyKZbTnQqL8xmcF45RdAlopd/Evqs= +github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200218213141-fc25d209285c h1:8b1bjIJ5X8TTApy/3P6GDKx97qIx6mGyct9GUHlwnGw= +github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200218213141-fc25d209285c/go.mod h1:ot55KksVO/lnRDVVNDF14AtdS7CSs//NE20WU6x/eiw= github.com/trustbloc/fabric-protos-go-ext v0.1.2-0.20200205170340-c69bba6d7b81 h1:iant8lATTlHYUWVdL3llKZuGgZUHBL4q7JUkLmcBQXk= github.com/trustbloc/fabric-protos-go-ext v0.1.2-0.20200205170340-c69bba6d7b81/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= github.com/trustbloc/sidetree-core-go v0.1.2-0.20200214144924-3e7aa7825416 h1:t9FawNEHc0R1PTYdDhWd/Q643KAN9IpJZMdlu2FlBxM= diff --git a/go.mod b/go.mod index 09b3898..546fe0b 100644 --- a/go.mod +++ b/go.mod @@ -28,9 +28,9 @@ require ( replace github.com/hyperledger/fabric => github.com/trustbloc/fabric-mod v0.1.2-0.20200211211900-62c249e072e2 -replace github.com/hyperledger/fabric/extensions => github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200211231405-e21ab2bc259f +replace github.com/hyperledger/fabric/extensions => github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200218213141-fc25d209285c -replace github.com/trustbloc/fabric-peer-ext => github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200211231405-e21ab2bc259f +replace github.com/trustbloc/fabric-peer-ext => github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200218213141-fc25d209285c replace github.com/hyperledger/fabric-protos-go => github.com/trustbloc/fabric-protos-go-ext v0.1.2-0.20200205170340-c69bba6d7b81 diff --git a/go.sum b/go.sum index 339c46f..64bef2e 100644 --- a/go.sum +++ b/go.sum @@ -352,10 +352,10 @@ github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955u github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/trustbloc/fabric-mod v0.1.2-0.20200211211900-62c249e072e2 h1:gHS0f6VQsWxMJrUshduwpHvQIApmBDCvdnrqKld9wfo= github.com/trustbloc/fabric-mod v0.1.2-0.20200211211900-62c249e072e2/go.mod h1:2UoZyvViPfKpCemDr/N2uyo2S+dSI4KbDqVmMePyOes= -github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200211231405-e21ab2bc259f h1:4MJmpaFGySX+hpX1AnvOqpJITza592hu+ADSfHTGlJg= -github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200211231405-e21ab2bc259f/go.mod h1:zdtIRio2I5i5QxLyKZbTnQqL8xmcF45RdAlopd/Evqs= -github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200211231405-e21ab2bc259f h1:krvThqJoCQpJPvf0lQv+zHesRj5SrbXhnhmrUaF6kdQ= -github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200211231405-e21ab2bc259f/go.mod h1:ot55KksVO/lnRDVVNDF14AtdS7CSs//NE20WU6x/eiw= +github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200218213141-fc25d209285c h1:Jx+movyiTkGV5eB5lVq3XXGv3vqU/af+rBWvIG+VGBs= +github.com/trustbloc/fabric-peer-ext v0.1.2-0.20200218213141-fc25d209285c/go.mod h1:zdtIRio2I5i5QxLyKZbTnQqL8xmcF45RdAlopd/Evqs= +github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200218213141-fc25d209285c h1:8b1bjIJ5X8TTApy/3P6GDKx97qIx6mGyct9GUHlwnGw= +github.com/trustbloc/fabric-peer-ext/mod/peer v0.0.0-20200218213141-fc25d209285c/go.mod h1:ot55KksVO/lnRDVVNDF14AtdS7CSs//NE20WU6x/eiw= github.com/trustbloc/fabric-protos-go-ext v0.1.2-0.20200205170340-c69bba6d7b81 h1:iant8lATTlHYUWVdL3llKZuGgZUHBL4q7JUkLmcBQXk= github.com/trustbloc/fabric-protos-go-ext v0.1.2-0.20200205170340-c69bba6d7b81/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= github.com/trustbloc/sidetree-core-go v0.1.2-0.20200214144924-3e7aa7825416 h1:t9FawNEHc0R1PTYdDhWd/Q643KAN9IpJZMdlu2FlBxM= diff --git a/pkg/peer/config/config.go b/pkg/peer/config/config.go index 606508c..c52bb21 100644 --- a/pkg/peer/config/config.go +++ b/pkg/peer/config/config.go @@ -14,14 +14,17 @@ const ( // GlobalMSPID is used as the consortium-wide MSP ID (i.e. non org-specific) GlobalMSPID = "general" + // SidetreeAppVersion is the version of the Sidetree config application + SidetreeAppVersion = "1" + // ProtocolComponentName is the name of the Sidetree protocol config component ProtocolComponentName = "protocol" - // SidetreeAppName is the '=name of the Sidetree config application - SidetreeAppName = "sidetree" + // SidetreePeerAppName is the '=name of the Sidetree config application + SidetreePeerAppName = "sidetree" - // SidetreeAppVersion is the version of the Sidetree config application - SidetreeAppVersion = "1" + // SidetreePeerAppVersion is the version of the Sidetree config application + SidetreePeerAppVersion = "1" ) // Namespace holds Sidetree namespace config diff --git a/pkg/peer/config/mocks/validatorregistry.gen.go b/pkg/peer/config/mocks/validatorregistry.gen.go new file mode 100644 index 0000000..4436dab --- /dev/null +++ b/pkg/peer/config/mocks/validatorregistry.gen.go @@ -0,0 +1,66 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "sync" + + ledgerconfig "github.com/trustbloc/fabric-peer-ext/pkg/config/ledgerconfig/config" +) + +type ValidatorRegistry struct { + RegisterStub func(v ledgerconfig.Validator) + registerMutex sync.RWMutex + registerArgsForCall []struct { + v ledgerconfig.Validator + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *ValidatorRegistry) Register(v ledgerconfig.Validator) { + fake.registerMutex.Lock() + fake.registerArgsForCall = append(fake.registerArgsForCall, struct { + v ledgerconfig.Validator + }{v}) + fake.recordInvocation("Register", []interface{}{v}) + fake.registerMutex.Unlock() + if fake.RegisterStub != nil { + fake.RegisterStub(v) + } +} + +func (fake *ValidatorRegistry) RegisterCallCount() int { + fake.registerMutex.RLock() + defer fake.registerMutex.RUnlock() + return len(fake.registerArgsForCall) +} + +func (fake *ValidatorRegistry) RegisterArgsForCall(i int) ledgerconfig.Validator { + fake.registerMutex.RLock() + defer fake.registerMutex.RUnlock() + return fake.registerArgsForCall[i].v +} + +func (fake *ValidatorRegistry) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.registerMutex.RLock() + defer fake.registerMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *ValidatorRegistry) 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/pkg/peer/config/service.go b/pkg/peer/config/service.go index 8e1b032..b9d1413 100644 --- a/pkg/peer/config/service.go +++ b/pkg/peer/config/service.go @@ -22,6 +22,10 @@ type configServiceProvider interface { ForChannel(channelID string) ledgerconfig.Service } +type validatorRegistry interface { + Register(v ledgerconfig.Validator) +} + // SidetreeProvider manages Sidetree configuration for the various channels type SidetreeProvider struct { configProvider configServiceProvider @@ -35,9 +39,15 @@ type SidetreeService interface { } // NewSidetreeProvider returns a new SidetreeProvider instance -func NewSidetreeProvider(configProvider configServiceProvider) *SidetreeProvider { +func NewSidetreeProvider(configProvider configServiceProvider, registry validatorRegistry) *SidetreeProvider { logger.Info("Creating Sidetree config provider") - return &SidetreeProvider{configProvider: configProvider} + + registry.Register(&sidetreeValidator{}) + registry.Register(&sidetreePeerValidator{}) + + return &SidetreeProvider{ + configProvider: configProvider, + } } // ForChannel returns the service for the given channel @@ -51,7 +61,7 @@ type sidetreeService struct { // LoadSidetree loads the Sidetree configuration for the given namespace func (c *sidetreeService) LoadSidetree(namespace string) (Sidetree, error) { - key := ledgerconfig.NewAppKey(GlobalMSPID, namespace, "1") + key := ledgerconfig.NewAppKey(GlobalMSPID, namespace, SidetreeAppVersion) var sidetreeConfig Sidetree if err := c.load(key, &sidetreeConfig); err != nil { @@ -67,7 +77,7 @@ func (c *sidetreeService) LoadSidetree(namespace string) (Sidetree, error) { // LoadSidetreePeer loads the peer-specific Sidetree configuration func (c *sidetreeService) LoadSidetreePeer(mspID, peerID string) (SidetreePeer, error) { - key := ledgerconfig.NewPeerKey(mspID, peerID, SidetreeAppName, SidetreeAppVersion) + key := ledgerconfig.NewPeerKey(mspID, peerID, SidetreePeerAppName, SidetreePeerAppVersion) var sidetreeConfig SidetreePeer if err := c.load(key, &sidetreeConfig); err != nil { @@ -111,10 +121,14 @@ func (c *sidetreeService) load(key *ledgerconfig.Key, v interface{}) error { return errors.WithMessagef(err, "error getting Sidetree config for key %s", key) } + return unmarshal(cfg, v) +} + +func unmarshal(value *ledgerconfig.Value, v interface{}) error { vp := viper.New() - vp.SetConfigType(string(cfg.Format)) + vp.SetConfigType(string(value.Format)) - if err := vp.ReadConfig(bytes.NewBufferString(cfg.Config)); err != nil { + if err := vp.ReadConfig(bytes.NewBufferString(value.Config)); err != nil { return errors.WithMessage(err, "error reading config") } @@ -126,16 +140,9 @@ func (c *sidetreeService) load(key *ledgerconfig.Key, v interface{}) error { } func unmarshalProtocol(cfg *ledgerconfig.Value) (*protocolApi.Protocol, error) { - v := viper.New() - v.SetConfigType(string(cfg.Format)) - - err := v.ReadConfig(bytes.NewBufferString(cfg.Config)) - if err != nil { - return nil, errors.WithMessage(err, "error reading Sidetree protocol config") - } - protocol := &protocolApi.Protocol{} - if err := v.Unmarshal(protocol); err != nil { + + if err := unmarshal(cfg, protocol); err != nil { return nil, errors.WithMessage(err, "error unmarshalling Sidetree protocol config") } diff --git a/pkg/peer/config/service_test.go b/pkg/peer/config/service_test.go index 13eaa87..afab349 100644 --- a/pkg/peer/config/service_test.go +++ b/pkg/peer/config/service_test.go @@ -18,6 +18,7 @@ import ( //go:generate counterfeiter -o ./mocks/configserviceprovider.gen.go --fake-name ConfigServiceProvider . configServiceProvider //go:generate counterfeiter -o ./mocks/configservice.gen.go --fake-name ConfigService github.com/trustbloc/fabric-peer-ext/pkg/config/ledgerconfig/config.Service +//go:generate counterfeiter -o ./mocks/validatorregistry.gen.go --fake-name ValidatorRegistry . validatorRegistry const ( channelID = "mychannel" @@ -40,7 +41,9 @@ func TestNewSidetreeProvider(t *testing.T) { configProvider := &mocks.ConfigServiceProvider{} configProvider.ForChannelReturns(configService) - p := NewSidetreeProvider(configProvider) + validatorRegistry := &mocks.ValidatorRegistry{} + + p := NewSidetreeProvider(configProvider, validatorRegistry) require.NotNil(t, p) s := p.ForChannel(channelID) @@ -154,7 +157,7 @@ func TestNewSidetreeProvider(t *testing.T) { cfgValue := &ledgercfg.Value{} configService.GetReturns(cfgValue, nil) - err := s.(*sidetreeService).load(ledgercfg.NewAppKey(GlobalMSPID, SidetreeAppName, SidetreeAppVersion), func() {}) + err := s.(*sidetreeService).load(ledgercfg.NewAppKey(GlobalMSPID, SidetreePeerAppName, SidetreePeerAppVersion), func() {}) require.Error(t, err) }) } diff --git a/pkg/peer/config/sidetreepeervalidator.go b/pkg/peer/config/sidetreepeervalidator.go new file mode 100644 index 0000000..2acd4c7 --- /dev/null +++ b/pkg/peer/config/sidetreepeervalidator.go @@ -0,0 +1,69 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package config + +import ( + "github.com/pkg/errors" + "github.com/trustbloc/fabric-peer-ext/pkg/config/ledgerconfig/config" +) + +// sidetreePeerValidator validates the SidetreePeer configuration +type sidetreePeerValidator struct { +} + +func (v *sidetreePeerValidator) Validate(kv *config.KeyValue) error { + if kv.AppName != SidetreePeerAppName { + return nil + } + + logger.Debugf("Validating config %s", kv) + + if kv.ComponentName != "" { + return errors.Errorf("unexpected component [%s] for %s", kv.ComponentName, kv.Key) + } + + if kv.PeerID == "" { + return errors.Errorf("field PeerID required for %s", kv.Key) + } + + if kv.AppVersion != SidetreePeerAppVersion { + return errors.Errorf("unsupported application version [%s] for %s", kv.AppVersion, kv.Key) + } + + var sidetreeCfg SidetreePeer + if err := unmarshal(kv.Value, &sidetreeCfg); err != nil { + return errors.WithMessagef(err, "invalid config %s", kv.Key) + } + + if sidetreeCfg.Monitor.Period == 0 { + logger.Infof("The Sidetree monitor period is set to 0 and therefore will be disabled for peer [%s].", kv.PeerID) + } + + for _, ns := range sidetreeCfg.Namespaces { + if err := v.validateNamespace(kv, ns); err != nil { + return err + } + } + + return nil +} + +func (v *sidetreePeerValidator) validateNamespace(kv *config.KeyValue, ns Namespace) error { + if ns.Namespace == "" { + return errors.Errorf("field 'Namespace' is required for %s", kv.Key) + } + + if ns.BasePath == "" { + return errors.Errorf("field 'BasePath' is required for %s", kv.Key) + } + + if ns.BasePath[0:1] != "/" { + return errors.Errorf("field 'BasePath' must begin with '/' for %s", kv.Key) + } + + return nil +} diff --git a/pkg/peer/config/sidetreepeervalidator_test.go b/pkg/peer/config/sidetreepeervalidator_test.go new file mode 100644 index 0000000..e7e925b --- /dev/null +++ b/pkg/peer/config/sidetreepeervalidator_test.go @@ -0,0 +1,89 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/trustbloc/fabric-peer-ext/pkg/config/ledgerconfig/config" +) + +const ( + txID = "tx1" +) + +const ( + org1Peer1Cfg = `{"Monitor":{"Period":"3s"},"Namespaces":[{"Namespace":"did:sidetree","BasePath":"/document"}]}` + org1Peer1NoNamespaceCfg = `{"Namespaces":[{"BasePath":"/document"}]}` + org1Peer1NoBasePathCfg = `{"Namespaces":[{"Namespace":"did:sidetree"}]}` + org1Peer1InvalidBasePathCfg = `{"Namespaces":[{"Namespace":"did:sidetree","BasePath":"document"}]}` +) + +func TestSidetreePeerValidator_Validate(t *testing.T) { + v := &sidetreePeerValidator{} + + key := config.NewPeerKey(mspID, peerID, SidetreePeerAppName, SidetreePeerAppVersion) + + t.Run("Irrelevant config -> success", func(t *testing.T) { + k1 := config.NewPeerKey(mspID, peerID, "app1", "v1") + require.NoError(t, v.Validate(config.NewKeyValue(k1, config.NewValue(txID, `{}`, config.FormatJSON)))) + }) + + t.Run("Empty config -> success", func(t *testing.T) { + require.NoError(t, v.Validate(config.NewKeyValue(key, config.NewValue(txID, `{}`, config.FormatJSON)))) + }) + + t.Run("Config with apps -> success", func(t *testing.T) { + require.NoError(t, v.Validate(config.NewKeyValue(key, config.NewValue(txID, org1Peer1Cfg, config.FormatJSON)))) + }) + + t.Run("No peer ID -> error", func(t *testing.T) { + k1 := config.NewPeerKey(mspID, "", SidetreePeerAppName, SidetreePeerAppVersion) + err := v.Validate(config.NewKeyValue(k1, config.NewValue(txID, `{}`, config.FormatJSON))) + require.Error(t, err) + require.Contains(t, err.Error(), "field PeerID required") + }) + + t.Run("Unsupported version -> error", func(t *testing.T) { + k1 := config.NewPeerKey(mspID, peerID, SidetreePeerAppName, "v0.2") + err := v.Validate(config.NewKeyValue(k1, config.NewValue(txID, `{}`, config.FormatJSON))) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported application version") + }) + + t.Run("Config with component -> error", func(t *testing.T) { + k1 := config.NewPeerComponentKey(mspID, peerID, SidetreePeerAppName, SidetreePeerAppVersion, "comp1", "v1") + err := v.Validate(config.NewKeyValue(k1, config.NewValue(txID, `{}`, config.FormatJSON))) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected component") + }) + + t.Run("Invalid config -> error", func(t *testing.T) { + err := v.Validate(config.NewKeyValue(key, config.NewValue(txID, `}`, config.FormatJSON))) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid config") + }) + + t.Run("No Namespace -> error", func(t *testing.T) { + err := v.Validate(config.NewKeyValue(key, config.NewValue(txID, org1Peer1NoNamespaceCfg, config.FormatJSON))) + require.Error(t, err) + require.Contains(t, err.Error(), "field 'Namespace' is required") + }) + + t.Run("No BasePath -> error", func(t *testing.T) { + err := v.Validate(config.NewKeyValue(key, config.NewValue(txID, org1Peer1NoBasePathCfg, config.FormatJSON))) + require.Error(t, err) + require.Contains(t, err.Error(), "field 'BasePath' is required") + }) + + t.Run("Invalid BasePath -> error", func(t *testing.T) { + err := v.Validate(config.NewKeyValue(key, config.NewValue(txID, org1Peer1InvalidBasePathCfg, config.FormatJSON))) + require.Error(t, err) + require.Contains(t, err.Error(), "field 'BasePath' must begin with '/'") + }) +} diff --git a/pkg/peer/config/sidetreevalidator.go b/pkg/peer/config/sidetreevalidator.go new file mode 100644 index 0000000..667765b --- /dev/null +++ b/pkg/peer/config/sidetreevalidator.go @@ -0,0 +1,106 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package config + +import ( + "github.com/pkg/errors" + "github.com/trustbloc/fabric-peer-ext/pkg/config/ledgerconfig/config" + "github.com/trustbloc/sidetree-core-go/pkg/docutil" +) + +const ( + sidetreeTag = "sidetree" +) + +// sidetreeValidator validates the Sidetree configuration including Protocols +type sidetreeValidator struct { +} + +func (v *sidetreeValidator) Validate(kv *config.KeyValue) error { + if !canValidate(kv) { + return nil + } + + logger.Debugf("Validating config %s", kv) + + if kv.MspID != GlobalMSPID { + return errors.Errorf("expecting MspID to be set to [%s] for Sidetree config %s", GlobalMSPID, kv.Key) + } + + switch kv.ComponentName { + case "": + return v.validateConfig(kv) + case ProtocolComponentName: + return v.validateProtocol(kv) + default: + return errors.Errorf("unexpected component [%s] for %s", kv.ComponentName, kv.Key) + } +} + +func (v *sidetreeValidator) validateConfig(kv *config.KeyValue) error { + logger.Debugf("Validating Sidetree config %s", kv) + + if kv.AppVersion != SidetreeAppVersion { + return errors.Errorf("unsupported application version [%s] for %s", kv.AppVersion, kv.Key) + } + + var sidetreeCfg Sidetree + if err := unmarshal(kv.Value, &sidetreeCfg); err != nil { + return errors.WithMessagef(err, "invalid config %s", kv.Key) + } + + if sidetreeCfg.BatchWriterTimeout == 0 { + return errors.Errorf("field 'BatchWriterTimeout' must contain a value greater than 0 for %s", kv.Key) + } + + return nil +} + +func (v *sidetreeValidator) validateProtocol(kv *config.KeyValue) error { + logger.Debugf("Validating Sidetree Protocol config %s", kv) + + // Validation of protocol depends on the version. + switch kv.ComponentVersion { + default: + // For now, only one protocol version exists + return v.validateProtocolV0(kv) + } +} + +func (v *sidetreeValidator) validateProtocolV0(kv *config.KeyValue) error { + p, err := unmarshalProtocol(kv.Value) + if err != nil { + return errors.WithMessagef(err, "invalid protocol config for %s", kv.Key) + } + + if _, err := docutil.GetHash(p.HashAlgorithmInMultiHashCode); err != nil { + return errors.WithMessagef(err, "error in Sidetree protocol for %s", kv.Key) + } + + if p.MaxOperationsPerBatch == 0 { + return errors.Errorf("field 'MaxOperationsPerBatch' must contain a value greater than 0 for %s", kv.Key) + } + + if p.MaxOperationByteSize == 0 { + return errors.Errorf("field 'MaxOperationByteSize' must contain a value greater than 0 for %s", kv.Key) + } + + return nil +} + +func canValidate(kv *config.KeyValue) bool { + for _, tag := range kv.Tags { + if tag == sidetreeTag { + logger.Debugf("Found tag [%s] in %s", tag, kv) + return true + } + } + + logger.Debugf("Did not find tag [%s] in %s", sidetreeTag, kv) + + return false +} diff --git a/pkg/peer/config/sidetreevalidator_test.go b/pkg/peer/config/sidetreevalidator_test.go new file mode 100644 index 0000000..c105cab --- /dev/null +++ b/pkg/peer/config/sidetreevalidator_test.go @@ -0,0 +1,106 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/trustbloc/fabric-peer-ext/pkg/config/ledgerconfig/config" +) + +const ( + protocolCfg = `{"startingBlockchainTime":500000,"hashAlgorithmInMultihashCode":18,"maxOperationByteSize":2000,"maxOperationsPerBatch":10}` + protocolInvalidAlgoCfg = `{"startingBlockchainTime":500000,"hashAlgorithmInMultihashCode":2777,"maxOperationByteSize":2000,"maxOperationsPerBatch":10}` + protocolInvalidMaxOperPerBatchCfg = `{"startingBlockchainTime":500000,"hashAlgorithmInMultihashCode":18,"maxOperationByteSize":2000}` + protocolInvalidMaxOperByteSizeCfg = `{"startingBlockchainTime":500000,"hashAlgorithmInMultihashCode":18,"maxOperationsPerBatch":10}` + appCfg = `batchWriterTimeout: 1s` +) + +func TestSidetreeValidator_Validate(t *testing.T) { + v := &sidetreeValidator{} + + appKey := config.NewAppKey(GlobalMSPID, "did:sidetree", SidetreeAppVersion) + + t.Run("Irrelevant config -> success", func(t *testing.T) { + k := config.NewAppKey(mspID, "app1", "v1") + require.NoError(t, v.Validate(config.NewKeyValue(k, config.NewValue(txID, `{}`, config.FormatJSON)))) + }) + + t.Run("Invalid MSP ID -> error", func(t *testing.T) { + k := config.NewAppKey(mspID, "did:sidetree", SidetreeAppVersion) + err := v.Validate(config.NewKeyValue(k, config.NewValue(txID, `{}`, config.FormatJSON, sidetreeTag))) + require.Error(t, err) + require.Contains(t, err.Error(), "expecting MspID to be set to [general] for Sidetree config") + }) + + t.Run("Unexpected component -> error", func(t *testing.T) { + k := config.NewComponentKey(GlobalMSPID, "did:sidetree", SidetreeAppVersion, "comp1", "0.4") + err := v.Validate(config.NewKeyValue(k, config.NewValue(txID, `{}`, config.FormatJSON, sidetreeTag))) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected component") + }) + + t.Run("App config -> success", func(t *testing.T) { + require.NoError(t, v.Validate(config.NewKeyValue(appKey, config.NewValue(txID, appCfg, config.FormatYAML, sidetreeTag)))) + }) + + t.Run("Unsupported app version -> error", func(t *testing.T) { + k := config.NewAppKey(GlobalMSPID, "did:sidetree", "1.7") + err := v.Validate(config.NewKeyValue(k, config.NewValue(txID, appCfg, config.FormatYAML, sidetreeTag))) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported application version") + }) + + t.Run("Invalid config -> error", func(t *testing.T) { + k := config.NewAppKey(GlobalMSPID, "did:sidetree", SidetreeAppVersion) + err := v.Validate(config.NewKeyValue(k, config.NewValue(txID, `{`, config.FormatJSON, sidetreeTag))) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid config") + }) + + t.Run("Invalid BatchWriterTimeout -> error", func(t *testing.T) { + k := config.NewAppKey(GlobalMSPID, "did:sidetree", SidetreeAppVersion) + err := v.Validate(config.NewKeyValue(k, config.NewValue(txID, `{}`, config.FormatJSON, sidetreeTag))) + require.Error(t, err) + require.Contains(t, err.Error(), "field 'BatchWriterTimeout' must contain a value greater than 0") + }) +} + +func TestSidetreeValidator_ValidateProtocol(t *testing.T) { + v := &sidetreeValidator{} + + protocolKey := config.NewComponentKey(GlobalMSPID, "did:sidetree", SidetreeAppVersion, ProtocolComponentName, "0.4") + + t.Run("Protocol -> success", func(t *testing.T) { + require.NoError(t, v.Validate(config.NewKeyValue(protocolKey, config.NewValue(txID, protocolCfg, config.FormatJSON, sidetreeTag)))) + }) + + t.Run("Invalid config -> error", func(t *testing.T) { + err := v.Validate(config.NewKeyValue(protocolKey, config.NewValue(txID, `{`, config.FormatJSON, sidetreeTag))) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid protocol config") + }) + + t.Run("Invalid algorithm -> error", func(t *testing.T) { + err := v.Validate(config.NewKeyValue(protocolKey, config.NewValue(txID, protocolInvalidAlgoCfg, config.FormatJSON, sidetreeTag))) + require.Error(t, err) + require.Contains(t, err.Error(), "algorithm not supported") + }) + + t.Run("Invalid MaxOperationsPerBatch -> error", func(t *testing.T) { + err := v.Validate(config.NewKeyValue(protocolKey, config.NewValue(txID, protocolInvalidMaxOperPerBatchCfg, config.FormatJSON, sidetreeTag))) + require.Error(t, err) + require.Contains(t, err.Error(), "field 'MaxOperationsPerBatch' must contain a value greater than 0") + }) + + t.Run("Invalid MaxOperationsByteSize -> error", func(t *testing.T) { + err := v.Validate(config.NewKeyValue(protocolKey, config.NewValue(txID, protocolInvalidMaxOperByteSizeCfg, config.FormatJSON, sidetreeTag))) + require.Error(t, err) + require.Contains(t, err.Error(), "field 'MaxOperationByteSize' must contain a value greater than 0") + }) +} diff --git a/pkg/peer/init_test.go b/pkg/peer/init_test.go index 1ac39a7..7f61d2d 100644 --- a/pkg/peer/init_test.go +++ b/pkg/peer/init_test.go @@ -99,7 +99,7 @@ var ( didSidetreeProtocol_v0_5_CfgKeyBytes = ledgercfgmgr.MarshalKey(ledgercfg.NewComponentKey(config.GlobalMSPID, didSidetreeNamespace, v1, config.ProtocolComponentName, v0_5)) didSidetreeProtocol_v0_5_CfgValueBytes = marshalConfigValue(tx1, didSidetreeProtocol_V0_5_CfgJSON, "json") - peerCfgKeyBytes = ledgercfgmgr.MarshalKey(ledgercfg.NewPeerKey(mspID, peerID, config.SidetreeAppName, config.SidetreeAppVersion)) + peerCfgKeyBytes = ledgercfgmgr.MarshalKey(ledgercfg.NewPeerKey(mspID, peerID, config.SidetreePeerAppName, config.SidetreePeerAppVersion)) peerSidetreeCfgValueBytes = marshalConfigValue(tx1, peerSidetreeCfgJson, "json") peerTrustblocCfgValueBytes = marshalConfigValue(tx5, peerTrustblocCfgJson, "json") peerBothCfgValueBytes = marshalConfigValue(tx2, peerBothCfgJson, "json") diff --git a/pkg/peer/sidetreesvc/channelctrl.go b/pkg/peer/sidetreesvc/channelctrl.go index ce865da..1cfc7c8 100644 --- a/pkg/peer/sidetreesvc/channelctrl.go +++ b/pkg/peer/sidetreesvc/channelctrl.go @@ -241,7 +241,7 @@ func (c *channelController) isMonitoringNamespace(namespace string) bool { } func (c *channelController) shouldUpdate(kv *ledgerconfig.KeyValue) bool { - if kv.MspID == c.PeerConfig.MSPID() && kv.PeerID == c.PeerConfig.PeerID() && kv.AppName == config.SidetreeAppName { + if kv.MspID == c.PeerConfig.MSPID() && kv.PeerID == c.PeerConfig.PeerID() && kv.AppName == config.SidetreePeerAppName { return true } diff --git a/pkg/peer/sidetreesvc/channelctrl_test.go b/pkg/peer/sidetreesvc/channelctrl_test.go index 6ca8854..f5c3110 100644 --- a/pkg/peer/sidetreesvc/channelctrl_test.go +++ b/pkg/peer/sidetreesvc/channelctrl_test.go @@ -100,7 +100,7 @@ func TestChannelManager(t *testing.T) { t.Run("Update peer sidetreeCfgService -> success", func(t *testing.T) { count := len(ctrl.Invocations()[eventMethod]) m.handleUpdate(&ledgerconfig.KeyValue{ - Key: ledgerconfig.NewPeerKey(msp1, peer1, config.SidetreeAppName, config.SidetreeAppVersion), + Key: ledgerconfig.NewPeerKey(msp1, peer1, config.SidetreePeerAppName, config.SidetreePeerAppVersion), }) time.Sleep(20 * time.Millisecond) diff --git a/test/bddtests/fabriccli.go b/test/bddtests/fabriccli.go index 5f93f09..19d6e60 100644 --- a/test/bddtests/fabriccli.go +++ b/test/bddtests/fabriccli.go @@ -43,11 +43,13 @@ func (cli *FabricCLI) Exec(args ...string) (string, error) { err := cmd.Start() if err != nil { - return er.String(), errors.Errorf("%s: %s", err, er.Bytes()) + return "", errors.New(out.String()) } + err = cmd.Wait() if err != nil { - return er.String(), errors.Errorf("%s: %s", err, er.Bytes()) + return "", errors.New(out.String()) } + return out.String(), nil } diff --git a/test/bddtests/fabriccli_steps.go b/test/bddtests/fabriccli_steps.go index 0d1d1b6..d46e292 100644 --- a/test/bddtests/fabriccli_steps.go +++ b/test/bddtests/fabriccli_steps.go @@ -96,6 +96,21 @@ func (d *FabricCLISteps) execute(strArgs string) error { return nil } +func (d *FabricCLISteps) executeWithError(strArgs, expectedError string) error { + logger.Infof("Executing fabric-cli command with args [%s] and expecting error [%s]", strArgs, expectedError) + + err := d.execute(strArgs) + if err == nil { + return errors.Errorf("expecting error [%s] but got no error", expectedError) + } + + if !strings.Contains(err.Error(), expectedError) { + return errors.Errorf("expecting error [%s] but got [%s]", expectedError, err) + } + + return nil +} + // RegisterSteps registers transient data steps func (d *FabricCLISteps) RegisterSteps(s *godog.Suite) { s.BeforeScenario(d.BDDContext.BeforeScenario) @@ -106,4 +121,5 @@ func (d *FabricCLISteps) RegisterSteps(s *godog.Suite) { s.Step(`^fabric-cli context "([^"]*)" is defined on channel "([^"]*)" with org "([^"]*)", peers "([^"]*)" and user "([^"]*)"$`, d.defineContext) s.Step(`^fabric-cli context "([^"]*)" is used$`, d.useContext) s.Step(`^fabric-cli is executed with args "([^"]*)"$`, d.execute) + s.Step(`^fabric-cli is executed with args "([^"]*)" then the error response should contain "([^"]*)"$`, d.executeWithError) } diff --git a/test/bddtests/features/did-sidetree.feature b/test/bddtests/features/did-sidetree.feature index 7c27070..05936eb 100644 --- a/test/bddtests/features/did-sidetree.feature +++ b/test/bddtests/features/did-sidetree.feature @@ -105,3 +105,10 @@ Feature: When client sends request to "http://localhost:48626/document" to resolve DID document Then check success response contains "#didDocumentHash" + + @invalid_config_update + Scenario: Invalid configuration + Given fabric-cli context "mychannel" is used + When fabric-cli is executed with args "ledgerconfig update --configfile ./fixtures/config/fabric/invalid-protocol-config.json --noprompt" then the error response should contain "algorithm not supported" + When fabric-cli is executed with args "ledgerconfig update --configfile ./fixtures/config/fabric/invalid-sidetree-config.json --noprompt" then the error response should contain "field 'BatchWriterTimeout' must contain a value greater than 0" + When fabric-cli is executed with args "ledgerconfig update --configfile ./fixtures/config/fabric/invalid-sidetree-peer-config.json --noprompt" then the error response should contain "field 'BasePath' must begin with '/'" diff --git a/test/bddtests/features/step_definitions/fabriccli_steps.js b/test/bddtests/features/step_definitions/fabriccli_steps.js index c4f2daf..ccd98ce 100644 --- a/test/bddtests/features/step_definitions/fabriccli_steps.js +++ b/test/bddtests/features/step_definitions/fabriccli_steps.js @@ -22,4 +22,7 @@ defineSupportCode(function ({And, But, Given, Then, When}) { And(/^fabric-cli is executed with args "([^"]*)"$/, function (arg1, callback) { callback.pending(); }); + When(/^fabric-cli is executed with args "([^"]*)" then the error response should contain "([^"]*)"$/, function (arg1, arg2, callback) { + callback.pending(); + }); }); diff --git a/test/bddtests/fixtures/config/fabric/invalid-basepath.json b/test/bddtests/fixtures/config/fabric/invalid-basepath.json new file mode 100644 index 0000000..ce7520d --- /dev/null +++ b/test/bddtests/fixtures/config/fabric/invalid-basepath.json @@ -0,0 +1,11 @@ +{ + "Monitor": { + "Period": "3s" + }, + "Namespaces": [ + { + "Namespace": "did:sidetree", + "BasePath": "document" + } + ] +} \ No newline at end of file diff --git a/test/bddtests/fixtures/config/fabric/invalid-batch-timeout.yaml b/test/bddtests/fixtures/config/fabric/invalid-batch-timeout.yaml new file mode 100644 index 0000000..80d06d2 --- /dev/null +++ b/test/bddtests/fixtures/config/fabric/invalid-batch-timeout.yaml @@ -0,0 +1,7 @@ +# +# Copyright SecureKey Technologies Inc. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +batchWriterTimeout: 0s diff --git a/test/bddtests/fixtures/config/fabric/invalid-hash-algorithm.json b/test/bddtests/fixtures/config/fabric/invalid-hash-algorithm.json new file mode 100644 index 0000000..da45731 --- /dev/null +++ b/test/bddtests/fixtures/config/fabric/invalid-hash-algorithm.json @@ -0,0 +1,6 @@ +{ + "startingBlockchainTime": 500000, + "hashAlgorithmInMultihashCode": 777, + "maxOperationByteSize": 2000, + "maxOperationsPerBatch": 10 +} \ No newline at end of file diff --git a/test/bddtests/fixtures/config/fabric/invalid-protocol-config.json b/test/bddtests/fixtures/config/fabric/invalid-protocol-config.json new file mode 100644 index 0000000..e59bfc9 --- /dev/null +++ b/test/bddtests/fixtures/config/fabric/invalid-protocol-config.json @@ -0,0 +1,18 @@ +{ + "MspID": "general", + "Apps": [ + { + "AppName": "did:sidetree", + "Version": "1", + "Components": [ + { + "Name": "protocol", + "Version": "0.4", + "Config": "file://./invalid-hash-algorithm.json", + "Format": "json", + "Tags": ["sidetree"] + } + ] + } + ] +} diff --git a/test/bddtests/fixtures/config/fabric/invalid-sidetree-config.json b/test/bddtests/fixtures/config/fabric/invalid-sidetree-config.json new file mode 100644 index 0000000..afd1902 --- /dev/null +++ b/test/bddtests/fixtures/config/fabric/invalid-sidetree-config.json @@ -0,0 +1,12 @@ +{ + "MspID": "general", + "Apps": [ + { + "AppName": "did:sidetree", + "Version": "1", + "Config": "file://./invalid-batch-timeout.yaml", + "Format": "yaml", + "Tags": ["sidetree"] + } + ] +} diff --git a/test/bddtests/fixtures/config/fabric/invalid-sidetree-peer-config.json b/test/bddtests/fixtures/config/fabric/invalid-sidetree-peer-config.json new file mode 100644 index 0000000..ccffbb8 --- /dev/null +++ b/test/bddtests/fixtures/config/fabric/invalid-sidetree-peer-config.json @@ -0,0 +1,16 @@ +{ + "MspID": "Org1MSP", + "Peers": [ + { + "PeerID": "peer0.org1.example.com", + "Apps": [ + { + "AppName": "sidetree", + "Version": "1", + "Config": "file://./invalid-basepath.json", + "Format": "json" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/bddtests/fixtures/config/fabric/mychannel-consortium-config.json b/test/bddtests/fixtures/config/fabric/mychannel-consortium-config.json index 6c91d6e..c5d7ba5 100644 --- a/test/bddtests/fixtures/config/fabric/mychannel-consortium-config.json +++ b/test/bddtests/fixtures/config/fabric/mychannel-consortium-config.json @@ -6,24 +6,28 @@ "Version": "1", "Config": "file://./did-sidetree-config.yaml", "Format": "yaml", + "Tags": ["sidetree"], "Components": [ { "Name": "protocol", "Version": "0.4", "Config": "file://./did-sidetree-protocol-v0_4.json", - "Format": "json" + "Format": "json", + "Tags": ["sidetree"] }, { "Name": "protocol", "Version": "0.5", "Config": "file://./did-sidetree-protocol-v0_5.json", - "Format": "json" + "Format": "json", + "Tags": ["sidetree"] } ] }, { "AppName": "did:bloc:trustbloc.dev", "Version": "1", + "Tags": ["sidetree"], "Config": "file://./did-trustbloc-config.yaml", "Format": "yaml", "Components": [ @@ -31,7 +35,8 @@ "Name": "protocol", "Version": "0.5", "Config": "file://./did-trustbloc-protocol-v0_5.json", - "Format": "json" + "Format": "json", + "Tags": ["sidetree"] } ] } diff --git a/test/bddtests/fixtures/config/fabric/yourchannel-consortium-config.json b/test/bddtests/fixtures/config/fabric/yourchannel-consortium-config.json index b971e01..7478453 100644 --- a/test/bddtests/fixtures/config/fabric/yourchannel-consortium-config.json +++ b/test/bddtests/fixtures/config/fabric/yourchannel-consortium-config.json @@ -4,6 +4,7 @@ { "AppName": "did:bloc:yourdomain.com", "Version": "1", + "Tags": ["sidetree"], "Config": "file://./did-yourdomain-config.yaml", "Format": "yaml", "Components": [ @@ -11,7 +12,8 @@ "Name": "protocol", "Version": "0.5", "Config": "file://./did-yourdomain-protocol-v0_5.json", - "Format": "json" + "Format": "json", + "Tags": ["sidetree"] } ] }