Repository: hyperledger/firefly (go)

Issue:
Database is locked
From Discord thread "Database is locked": https://discord.com/channels/905194001349627914/997135847797305484

Sometimes when running with the sqlite database, an HTTP request will be met with `{"error":"FF10116: Database insert failed: database is locked"}`. This seems to happen most frequently with the tokens E2E tests, although it's not necessarily specific to tokens. Might just be an issue with too many concurrent database routines on the sqlite database.

Pull request:
Wait up to 1000ms to lock sqlite database
Might fix #925

Patch:
diff --git a/docs/reference/config.md b/docs/reference/config.md
index 72c388127..e1009783d 100644
--- a/docs/reference/config.md
+++ b/docs/reference/config.md
@@ -30,7 +30,7 @@ nav_order: 2
 
 |Key|Description|Type|Default Value|
 |---|-----------|----|-------------|
-|keynormalization|Mechanism to normalize keys before using them. Valid options are `blockchain_plugin` - use blockchain plugin (default) or `none` - do not attempt normalization (deprecated - use namespaces.predefined[].asset.manager.keynormalization)|`string`|`<nil>`
+|keyNormalization|Mechanism to normalize keys before using them. Valid options are `blockchain_plugin` - use blockchain plugin (default) or `none` - do not attempt normalization|`string`|`<nil>`
 
 ## batch.cache
 
@@ -132,10 +132,10 @@ nav_order: 2
 |batchTimeout|How long Ethconnect should wait for new events to arrive and fill a batch, before sending the batch to FireFly core. Only applies when automatically creating a new event stream|[`time.Duration`](https://pkg.go.dev/time#Duration)|`500`
 |connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
 |expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s`
-|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use namespaces.predefined[].multiparty.contract[].location.firstEvent)|Address `string`|`0`
+|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use namespace.predefined[].multiparty.contract[].location.firstEvent)|Address `string`|`0`
 |headers|Adds custom headers to HTTP requests|`map[string]string`|`<nil>`
 |idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
-|instance|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use namespaces.predefined[].multiparty.contract[].location.address)|Address `string`|`<nil>`
+|instance|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use namespace.predefined[].multiparty.contract[].location.address)|Address `string`|`<nil>`
 |maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
 |prefixLong|The prefix that will be used for Ethconnect specific HTTP headers when FireFly makes requests to Ethconnect|`string`|`firefly`
 |prefixShort|The prefix that will be used for Ethconnect specific query parameters when FireFly makes requests to Ethconnect|`string`|`fly`
@@ -217,8 +217,8 @@ nav_order: 2
 |---|-----------|----|-------------|
 |batchSize|The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream|`int`|`50`
 |batchTimeout|The maximum amount of time to wait for a batch to complete|[`time.Duration`](https://pkg.go.dev/time#Duration)|`500`
-|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use namespaces.predefined[].multiparty.contract[].location.chaincode)|`string`|`<nil>`
-|channel|The Fabric channel that FireFly will use for BatchPin transactions (deprecated - use namespaces.predefined[].multiparty.contract[].location.channel)|`string`|`<nil>`
+|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use namespace.predefined[].multiparty.contract[].location.chaincode)|`string`|`<nil>`
+|channel|The Fabric channel that FireFly will use for BatchPin transactions (deprecated - use namespace.predefined[].multiparty.contract[].location.channel)|`string`|`<nil>`
 |connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
 |expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s`
 |headers|Adds custom headers to HTTP requests|`map[string]string`|`<nil>`
@@ -622,12 +622,6 @@ nav_order: 2
 |plugins|The list of plugins for this namespace|`string`|`<nil>`
 |remotename|The namespace name to be sent in plugin calls, if it differs from namespace name|`string`|`<nil>`
 
-## namespaces.predefined[].asset.manager
-
-|Key|Description|Type|Default Value|
-|---|-----------|----|-------------|
-|keynormalization|Mechanism to normalize keys before using them. Valid options are `blockchain_plugin` - use blockchain plugin (default) or `none` - do not attempt normalization|`string`|`<nil>`
-
 ## namespaces.predefined[].multiparty
 
 |Key|Description|Type|Default Value|
diff --git a/docs/tutorials/broadcast_data.md b/docs/tutorials/broadcast_data.md
index 2e90b5a26..890859d71 100644
--- a/docs/tutorials/broadcast_data.md
+++ b/docs/tutorials/broadcast_data.md
@@ -198,7 +198,7 @@ At this point the data has not be shared with anyone else in the network
 
 Just include a reference to the `id` returned from the upload.
 
-`POST` `/api/v1/namespaces/default/messages/broadcast`
+`POST` `/api/v1/namespaces/default/broadcast/message`
 
 ```json
 {
diff --git a/internal/assets/manager.go b/internal/assets/manager.go
index fc05916fe..2d9228439 100644
--- a/internal/assets/manager.go
+++ b/internal/assets/manager.go
@@ -19,9 +19,11 @@ package assets
 import (
 	"context"
 
+	"github.com/hyperledger/firefly-common/pkg/config"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly-common/pkg/i18n"
 	"github.com/hyperledger/firefly/internal/broadcast"
+	"github.com/hyperledger/firefly/internal/coreconfig"
 	"github.com/hyperledger/firefly/internal/coremsgs"
 	"github.com/hyperledger/firefly/internal/identity"
 	"github.com/hyperledger/firefly/internal/metrics"
@@ -82,7 +84,7 @@ type assetManager struct {
 	keyNormalization int
 }
 
-func NewAssetManager(ctx context.Context, ns, keyNormalization string, di database.Plugin, ti map[string]tokens.Plugin, im identity.Manager, sa syncasync.Bridge, bm broadcast.Manager, pm privatemessaging.Manager, mm metrics.Manager, om operations.Manager, txHelper txcommon.Helper) (Manager, error) {
+func NewAssetManager(ctx context.Context, ns string, di database.Plugin, ti map[string]tokens.Plugin, im identity.Manager, sa syncasync.Bridge, bm broadcast.Manager, pm privatemessaging.Manager, mm metrics.Manager, om operations.Manager, txHelper txcommon.Helper) (Manager, error) {
 	if di == nil || im == nil || sa == nil || ti == nil || mm == nil || om == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "AssetManager")
 	}
@@ -96,7 +98,7 @@ func NewAssetManager(ctx context.Context, ns, keyNormalization string, di databa
 		broadcast:        bm,
 		messaging:        pm,
 		tokens:           ti,
-		keyNormalization: identity.ParseKeyNormalizationConfig(keyNormalization),
+		keyNormalization: identity.ParseKeyNormalizationConfig(config.GetString(coreconfig.AssetManagerKeyNormalization)),
 		metrics:          mm,
 		operations:       om,
 	}
diff --git a/internal/coreconfig/coreconfig.go b/internal/coreconfig/coreconfig.go
index e3b3498fd..6e04aa28d 100644
--- a/internal/coreconfig/coreconfig.go
+++ b/internal/coreconfig/coreconfig.go
@@ -41,8 +41,6 @@ const (
 	NamespacePlugins = "plugins"
 	// NamespaceDefaultKey is the default signing key for blockchain transactions within this namespace
 	NamespaceDefaultKey = "defaultkey"
-	// NamespaceAssetKeyNormalization mechanism to normalize keys before using them. Valid options: "blockchain_plugin" - use blockchain plugin (default), "none" - do not attempt normalization
-	NamespaceAssetKeyNormalization = "asset.manager.keynormalization"
 	// NamespaceMultiparty contains the multiparty configuration for a namespace
 	NamespaceMultiparty = "multiparty"
 	// NamespaceMultipartyEnabled specifies if multi-party mode is enabled for a namespace
@@ -289,7 +287,7 @@ var (
 	// TransactionCacheTTL
 	TransactionCacheTTL = ffc("transaction.cache.ttl")
 	// AssetManagerKeyNormalization mechanism to normalize keys before using them. Valid options: "blockchain_plugin" - use blockchain plugin (default), "none" - do not attempt normalization
-	AssetManagerKeyNormalization = ffc("asset.manager.keynormalization")
+	AssetManagerKeyNormalization = ffc("asset.manager.keyNormalization")
 	// UIEnabled set to false to disable the UI (default is true, so UI will be enabled if ui.path is valid)
 	UIEnabled = ffc("ui.enabled")
 	// UIPath the path on which to serve the UI
diff --git a/internal/coremsgs/en_config_descriptions.go b/internal/coremsgs/en_config_descriptions.go
index c9c062fbd..cd26b5283 100644
--- a/internal/coremsgs/en_config_descriptions.go
+++ b/internal/coremsgs/en_config_descriptions.go
@@ -41,7 +41,7 @@ var (
 	ConfigAPIDefaultFilterLimit        = ffc("config.api.defaultFilterLimit", "The maximum number of rows to return if no limit is specified on an API request", i18n.IntType)
 	ConfigAPIMaxFilterLimit            = ffc("config.api.maxFilterLimit", "The largest value of `limit` that an HTTP client can specify in a request", i18n.IntType)
 	ConfigAPIRequestMaxTimeout         = ffc("config.api.requestMaxTimeout", "The maximum amount of time that an HTTP client can specify in a `Request-Timeout` header to keep a specific request open", i18n.TimeDurationType)
-	ConfigAssetManagerKeyNormalization = ffc("config.asset.manager.keynormalization", "Mechanism to normalize keys before using them. Valid options are `blockchain_plugin` - use blockchain plugin (default) or `none` - do not attempt normalization (deprecated - use namespaces.predefined[].asset.manager.keynormalization)", i18n.StringType)
+	ConfigAssetManagerKeyNormalization = ffc("config.asset.manager.keyNormalization", "Mechanism to normalize keys before using them. Valid options are `blockchain_plugin` - use blockchain plugin (default) or `none` - do not attempt normalization", i18n.StringType)
 
 	ConfigBatchManagerMinimumPollDelay = ffc("config.batch.manager.minimumPollDelay", "The minimum time the batch manager waits between polls on the DB - to prevent thrashing", i18n.TimeDurationType)
 	ConfigBatchManagerPollTimeout      = ffc("config.batch.manager.pollTimeout", "How long to wait without any notifications of new messages before doing a page query", i18n.TimeDurationType)
@@ -70,8 +70,8 @@ var (
 
 	ConfigBlockchainEthereumEthconnectBatchSize    = ffc("config.blockchain.ethereum.ethconnect.batchSize", "The number of events Ethconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType)
 	ConfigBlockchainEthereumEthconnectBatchTimeout = ffc("config.blockchain.ethereum.ethconnect.batchTimeout", "How long Ethconnect should wait for new events to arrive and fill a batch, before sending the batch to FireFly core. Only applies when automatically creating a new event stream", i18n.TimeDurationType)
-	ConfigBlockchainEthereumEthconnectInstance     = ffc("config.blockchain.ethereum.ethconnect.instance", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use namespaces.predefined[].multiparty.contract[].location.address)", "Address "+i18n.StringType)
-	ConfigBlockchainEthereumEthconnectFromBlock    = ffc("config.blockchain.ethereum.ethconnect.fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use namespaces.predefined[].multiparty.contract[].location.firstEvent)", "Address "+i18n.StringType)
+	ConfigBlockchainEthereumEthconnectInstance     = ffc("config.blockchain.ethereum.ethconnect.instance", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use namespace.predefined[].multiparty.contract[].location.address)", "Address "+i18n.StringType)
+	ConfigBlockchainEthereumEthconnectFromBlock    = ffc("config.blockchain.ethereum.ethconnect.fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use namespace.predefined[].multiparty.contract[].location.firstEvent)", "Address "+i18n.StringType)
 	ConfigBlockchainEthereumEthconnectPrefixLong   = ffc("config.blockchain.ethereum.ethconnect.prefixLong", "The prefix that will be used for Ethconnect specific HTTP headers when FireFly makes requests to Ethconnect", i18n.StringType)
 	ConfigBlockchainEthereumEthconnectPrefixShort  = ffc("config.blockchain.ethereum.ethconnect.prefixShort", "The prefix that will be used for Ethconnect specific query parameters when FireFly makes requests to Ethconnect", i18n.StringType)
 	ConfigBlockchainEthereumEthconnectTopic        = ffc("config.blockchain.ethereum.ethconnect.topic", "The websocket listen topic that the node should register on, which is important if there are multiple nodes using a single ethconnect", i18n.StringType)
@@ -83,8 +83,8 @@ var (
 
 	ConfigBlockchainFabricFabconnectBatchSize    = ffc("config.blockchain.fabric.fabconnect.batchSize", "The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType)
 	ConfigBlockchainFabricFabconnectBatchTimeout = ffc("config.blockchain.fabric.fabconnect.batchTimeout", "The maximum amount of time to wait for a batch to complete", i18n.TimeDurationType)
-	ConfigBlockchainFabricFabconnectChaincode    = ffc("config.blockchain.fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use namespaces.predefined[].multiparty.contract[].location.chaincode)", i18n.StringType)
-	ConfigBlockchainFabricFabconnectChannel      = ffc("config.blockchain.fabric.fabconnect.channel", "The Fabric channel that FireFly will use for BatchPin transactions (deprecated - use namespaces.predefined[].multiparty.contract[].location.channel)", i18n.StringType)
+	ConfigBlockchainFabricFabconnectChaincode    = ffc("config.blockchain.fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use namespace.predefined[].multiparty.contract[].location.chaincode)", i18n.StringType)
+	ConfigBlockchainFabricFabconnectChannel      = ffc("config.blockchain.fabric.fabconnect.channel", "The Fabric channel that FireFly will use for BatchPin transactions (deprecated - use namespace.predefined[].multiparty.contract[].location.channel)", i18n.StringType)
 	ConfigBlockchainFabricFabconnectPrefixLong   = ffc("config.blockchain.fabric.fabconnect.prefixLong", "The prefix that will be used for Fabconnect specific HTTP headers when FireFly makes requests to Fabconnect", i18n.StringType)
 	ConfigBlockchainFabricFabconnectPrefixShort  = ffc("config.blockchain.fabric.fabconnect.prefixShort", "The prefix that will be used for Fabconnect specific query parameters when FireFly makes requests to Fabconnect", i18n.StringType)
 	ConfigBlockchainFabricFabconnectSigner       = ffc("config.blockchain.fabric.fabconnect.signer", "The Fabric signing key to use when submitting transactions to Fabconnect", i18n.StringType)
@@ -257,7 +257,6 @@ var (
 	ConfigNamespacesPredefinedPlugins            = ffc("config.namespaces.predefined[].plugins", "The list of plugins for this namespace", i18n.StringType)
 	ConfigNamespacesPredefinedRemoteName         = ffc("config.namespaces.predefined[].remotename", "The namespace name to be sent in plugin calls, if it differs from namespace name", i18n.StringType)
 	ConfigNamespacesPredefinedDefaultKey         = ffc("config.namespaces.predefined[].defaultkey", "A default signing key for blockchain transactions within this namespace", i18n.StringType)
-	ConfigNamespacesPredefinedKeyNormalization   = ffc("config.namespaces.predefined[].asset.manager.keynormalization", "Mechanism to normalize keys before using them. Valid options are `blockchain_plugin` - use blockchain plugin (default) or `none` - do not attempt normalization", i18n.StringType)
 	ConfigNamespacesMultipartyEnabled            = ffc("config.namespaces.predefined[].multiparty.enabled", "Enables multi-party mode for this namespace (defaults to true if an org name or key is configured, either here or at the root level)", i18n.BooleanType)
 	ConfigNamespacesMultipartyOrgName            = ffc("config.namespaces.predefined[].multiparty.org.name", "A short name for the local root organization within this namespace", i18n.StringType)
 	ConfigNamespacesMultipartyOrgDesc            = ffc("config.namespaces.predefined[].multiparty.org.description", "A description for the local root organization within this namespace", i18n.StringType)
diff --git a/internal/database/sqlite3/sqlite3.go b/internal/database/sqlite3/sqlite3.go
index ecbbaa46a..b8624b593 100644
--- a/internal/database/sqlite3/sqlite3.go
+++ b/internal/database/sqlite3/sqlite3.go
@@ -42,7 +42,10 @@ type SQLite3 struct {
 }
 
 func connHook(conn *sqlite3.SQLiteConn) error {
-	_, err := conn.Exec("PRAGMA case_sensitive_like=ON;", nil)
+	_, err := conn.Exec(`
+		PRAGMA case_sensitive_like=ON;
+		PRAGMA busy_timeout=1000;
+	`, nil)
 	return err
 }
 
diff --git a/internal/identity/identitymanager.go b/internal/identity/identitymanager.go
index 5c862b108..38dd73318 100644
--- a/internal/identity/identitymanager.go
+++ b/internal/identity/identitymanager.go
@@ -135,7 +135,7 @@ func (im *identityManager) NormalizeSigningKey(ctx context.Context, inputKey str
 	}
 	// If the caller is not confident that the blockchain plugin/connector should be used to resolve,
 	// for example it might be a different blockchain (Eth vs Fabric etc.), or it has its own
-	// verification/management of keys, it should set `namespaces.predefined[].asset.manager.keynormalization: "none"` in the config.
+	// verification/management of keys, it should set `assets.keyNormalization: "none"` in the config.
 	if keyNormalizationMode != KeyNormalizationBlockchainPlugin {
 		return inputKey, nil
 	}
diff --git a/internal/namespace/config.go b/internal/namespace/config.go
index c8d57abcd..1a8909464 100644
--- a/internal/namespace/config.go
+++ b/internal/namespace/config.go
@@ -39,7 +39,6 @@ func InitConfig(withDefaults bool) {
 	namespacePredefined.AddKnownKey(coreconfig.NamespaceRemoteName)
 	namespacePredefined.AddKnownKey(coreconfig.NamespacePlugins)
 	namespacePredefined.AddKnownKey(coreconfig.NamespaceDefaultKey)
-	namespacePredefined.AddKnownKey(coreconfig.NamespaceAssetKeyNormalization)
 
 	multipartyConf := namespacePredefined.SubSection(coreconfig.NamespaceMultiparty)
 	multipartyConf.AddKnownKey(coreconfig.NamespaceMultipartyEnabled)
@@ -56,6 +55,5 @@ func InitConfig(withDefaults bool) {
 	if withDefaults {
 		namespaceConfig.AddKnownKey(NamespacePredefined+".0."+coreconfig.NamespaceName, "default")
 		namespaceConfig.AddKnownKey(NamespacePredefined+".0."+coreconfig.NamespaceDescription, "Default predefined namespace")
-		namespaceConfig.AddKnownKey(NamespacePredefined+".0."+coreconfig.NamespaceAssetKeyNormalization, "blockchain_plugin")
 	}
 }
diff --git a/internal/namespace/manager.go b/internal/namespace/manager.go
index 2dca5d5a0..07e22cad9 100644
--- a/internal/namespace/manager.go
+++ b/internal/namespace/manager.go
@@ -791,11 +791,6 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 		remoteName = name
 	}
 
-	keyNormalization := conf.GetString(coreconfig.NamespaceAssetKeyNormalization)
-	if keyNormalization == "" {
-		keyNormalization = config.GetString(coreconfig.AssetManagerKeyNormalization)
-	}
-
 	multipartyConf := conf.SubSection(coreconfig.NamespaceMultiparty)
 	// If any multiparty org information is configured (here or at the root), assume multiparty mode by default
 	orgName := multipartyConf.GetString(coreconfig.NamespaceMultipartyOrgName)
@@ -864,7 +859,6 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 	config := orchestrator.Config{
 		DefaultKey:       conf.GetString(coreconfig.NamespaceDefaultKey),
 		TokenRemoteNames: nm.tokenRemoteNames,
-		KeyNormalization: keyNormalization,
 	}
 	if multipartyEnabled.(bool) {
 		contractsConf := multipartyConf.SubArray(coreconfig.NamespaceMultipartyContract)
diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
index 21ed04f27..5f44e3b0d 100644
--- a/internal/orchestrator/orchestrator.go
+++ b/internal/orchestrator/orchestrator.go
@@ -170,7 +170,6 @@ type Plugins struct {
 
 type Config struct {
 	DefaultKey       string
-	KeyNormalization string
 	Multiparty       multiparty.Config
 	TokenRemoteNames map[string]string
 }
@@ -450,7 +449,7 @@ func (or *orchestrator) initManagers(ctx context.Context) (err error) {
 	}
 
 	if or.assets == nil {
-		or.assets, err = assets.NewAssetManager(ctx, or.namespace.LocalName, or.config.KeyNormalization, or.database(), or.tokens(), or.identity, or.syncasync, or.broadcast, or.messaging, or.metrics, or.operations, or.txHelper)
+		or.assets, err = assets.NewAssetManager(ctx, or.namespace.LocalName, or.database(), or.tokens(), or.identity, or.syncasync, or.broadcast, or.messaging, or.metrics, or.operations, or.txHelper)
 		if err != nil {
 			return err
 		}


Test patch:
diff --git a/internal/assets/manager_test.go b/internal/assets/manager_test.go
index 851cf54ff..6cae98b09 100644
--- a/internal/assets/manager_test.go
+++ b/internal/assets/manager_test.go
@@ -63,7 +63,7 @@ func newTestAssetsCommon(t *testing.T, metrics bool) (*assetManager, func()) {
 	mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything)
 	mti.On("Name").Return("ut").Maybe()
 	ctx, cancel := context.WithCancel(context.Background())
-	a, err := NewAssetManager(ctx, "ns1", "blockchain_plugin", mdi, map[string]tokens.Plugin{"magic-tokens": mti}, mim, msa, mbm, mpm, mm, mom, txHelper)
+	a, err := NewAssetManager(ctx, "ns1", mdi, map[string]tokens.Plugin{"magic-tokens": mti}, mim, msa, mbm, mpm, mm, mom, txHelper)
 	rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe()
 	rag.RunFn = func(a mock.Arguments) {
 		rag.ReturnArguments = mock.Arguments{a[1].(func(context.Context) error)(a[0].(context.Context))}
@@ -75,7 +75,7 @@ func newTestAssetsCommon(t *testing.T, metrics bool) (*assetManager, func()) {
 }
 
 func TestInitFail(t *testing.T) {
-	_, err := NewAssetManager(context.Background(), "", "", nil, nil, nil, nil, nil, nil, nil, nil, nil)
+	_, err := NewAssetManager(context.Background(), "", nil, nil, nil, nil, nil, nil, nil, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 }
 
diff --git a/test/e2e/common.go b/test/e2e/common.go
deleted file mode 100644
index d975a44dc..000000000
--- a/test/e2e/common.go
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright © 2022 Kaleido, Inc.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-// 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 e2e
-
-import (
-	"crypto/rand"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"testing"
-
-	"github.com/go-resty/resty/v2"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"gopkg.in/yaml.v2"
-)
-
-func ReadConfig(t *testing.T, configFile string) map[string]interface{} {
-	yfile, err := ioutil.ReadFile(configFile)
-	assert.NoError(t, err)
-	data := make(map[string]interface{})
-	err = yaml.Unmarshal(yfile, &data)
-	assert.NoError(t, err)
-	return data
-}
-
-func WriteConfig(t *testing.T, configFile string, data map[string]interface{}) {
-	out, err := yaml.Marshal(data)
-	assert.NoError(t, err)
-	f, err := os.Create(configFile)
-	assert.NoError(t, err)
-	_, err = f.Write(out)
-	assert.NoError(t, err)
-	f.Close()
-}
-
-func AddNamespace(data map[string]interface{}, ns map[string]interface{}) {
-	namespaces := data["namespaces"].(map[interface{}]interface{})
-	predefined := namespaces["predefined"].([]interface{})
-	namespaces["predefined"] = append(predefined, ns)
-}
-
-func ResetFireFly(t *testing.T, client *resty.Client) {
-	resp, err := client.R().
-		SetBody(map[string]interface{}{}).
-		Post("/reset")
-	require.NoError(t, err)
-	assert.Equal(t, 204, resp.StatusCode())
-}
-
-func RandomName(t *testing.T) string {
-	b := make([]byte, 5)
-	_, err := rand.Read(b)
-	assert.NoError(t, err)
-	return fmt.Sprintf("e2e_%x", b)
-}
diff --git a/test/e2e/gateway/common.go b/test/e2e/gateway/common.go
index 9b707a288..8f1fef2b9 100644
--- a/test/e2e/gateway/common.go
+++ b/test/e2e/gateway/common.go
@@ -17,17 +17,18 @@
 package gateway
 
 import (
+	"crypto/rand"
 	"encoding/base64"
 	"fmt"
 	"net/http"
 	"os"
-	"path/filepath"
 	"testing"
 	"time"
 
 	"github.com/gorilla/websocket"
 	"github.com/hyperledger/firefly/test/e2e"
 	"github.com/hyperledger/firefly/test/e2e/client"
+	"github.com/stretchr/testify/assert"
 )
 
 const (
@@ -43,9 +44,6 @@ type testState struct {
 	client1              *client.FireFlyClient
 	unregisteredAccounts []interface{}
 	namespace            string
-	stackName            string
-	adminHost1           string
-	configFile1          string
 }
 
 func (m *testState) T() *testing.T {
@@ -61,10 +59,6 @@ func (m *testState) Done() func() {
 }
 
 func beforeE2ETest(t *testing.T) *testState {
-	stackDir := os.Getenv("STACK_DIR")
-	if stackDir == "" {
-		t.Fatal("STACK_DIR must be set")
-	}
 	stack := e2e.ReadStack(t)
 	stackState := e2e.ReadStackState(t)
 
@@ -80,9 +74,7 @@ func beforeE2ETest(t *testing.T) *testState {
 		member0WithPort = fmt.Sprintf(":%d", stack.Members[0].ExposedFireflyPort)
 	}
 
-	base1 := fmt.Sprintf("%s://%s%s", httpProtocolClient1, stack.Members[0].FireflyHostname, member0WithPort)
-	admin1 := fmt.Sprintf("%s://%s:%d", httpProtocolClient1, stack.Members[0].FireflyHostname, stack.Members[0].ExposedAdminPort)
-
+	baseURL := fmt.Sprintf("%s://%s%s", httpProtocolClient1, stack.Members[0].FireflyHostname, member0WithPort)
 	namespace := os.Getenv("NAMESPACE")
 	if namespace == "" {
 		namespace = "default"
@@ -90,13 +82,10 @@ func beforeE2ETest(t *testing.T) *testState {
 
 	ts := &testState{
 		t:                    t,
-		stackName:            stack.Name,
 		startTime:            time.Now(),
-		client1:              client.NewFireFly(t, base1, namespace),
+		client1:              client.NewFireFly(t, baseURL, namespace),
 		unregisteredAccounts: stackState.Accounts[2:],
 		namespace:            namespace,
-		adminHost1:           admin1,
-		configFile1:          filepath.Join(stackDir, "runtime", "config", "firefly_core_0.yml"),
 	}
 
 	t.Logf("Blockchain provider: %s", stack.BlockchainProvider)
@@ -128,3 +117,10 @@ func beforeE2ETest(t *testing.T) *testState {
 	}
 	return ts
 }
+
+func randomName(t *testing.T) string {
+	b := make([]byte, 5)
+	_, err := rand.Read(b)
+	assert.NoError(t, err)
+	return fmt.Sprintf("e2e_%x", b)
+}
diff --git a/test/e2e/gateway/tokens.go b/test/e2e/gateway/tokens.go
index 8e67a2711..81cb49a22 100644
--- a/test/e2e/gateway/tokens.go
+++ b/test/e2e/gateway/tokens.go
@@ -56,7 +56,7 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 
 	received1 := e2e.WsReader(suite.testState.ws1)
 
-	poolName := fmt.Sprintf("pool_%s", e2e.RandomName(suite.T()))
+	poolName := fmt.Sprintf("pool_%s", randomName(suite.T()))
 	suite.T().Logf("Pool name: %s", poolName)
 
 	pool := &core.TokenPool{
diff --git a/test/e2e/gateway/tokens_only.go b/test/e2e/gateway/tokens_only.go
deleted file mode 100644
index 3b49060ad..000000000
--- a/test/e2e/gateway/tokens_only.go
+++ /dev/null
@@ -1,128 +0,0 @@
-// Copyright © 2022 Kaleido, Inc.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-// 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 gateway
-
-import (
-	"fmt"
-
-	"github.com/hyperledger/firefly-common/pkg/fftypes"
-	"github.com/hyperledger/firefly/pkg/core"
-	"github.com/hyperledger/firefly/test/e2e"
-	"github.com/hyperledger/firefly/test/e2e/client"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/suite"
-)
-
-type TokensOnlyTestSuite struct {
-	suite.Suite
-	testState *testState
-	connector string
-	db        string
-	key       string
-}
-
-func (suite *TokensOnlyTestSuite) SetupSuite() {
-	suite.testState = beforeE2ETest(suite.T())
-	stack := e2e.ReadStack(suite.T())
-	stackState := e2e.ReadStackState(suite.T())
-
-	suite.connector = stack.TokenProviders[0]
-	suite.db = stack.Database
-	account := stackState.Accounts[0].(map[string]interface{})
-	suite.key = account["address"].(string)
-}
-
-func (suite *TokensOnlyTestSuite) BeforeTest(suiteName, testName string) {
-	suite.testState = beforeE2ETest(suite.T())
-}
-
-func (suite *TokensOnlyTestSuite) AfterTest(suiteName, testName string) {
-	e2e.VerifyAllOperationsSucceeded(suite.T(), []*client.FireFlyClient{suite.testState.client1}, suite.testState.startTime)
-}
-
-func (suite *TokensOnlyTestSuite) TestTokensOnlyNamespaces() {
-	defer suite.testState.Done()
-	testNamespace := e2e.RandomName(suite.T())
-	suite.T().Logf("Test namespace: %s", testNamespace)
-
-	namespaceInfo := map[string]interface{}{
-		"name": testNamespace,
-		"asset": map[string]interface{}{
-			"manager": map[string]interface{}{
-				"keynormalization": "none",
-			},
-		},
-		"plugins": []string{"database0", suite.connector},
-	}
-
-	// Add the new namespace
-	data1 := e2e.ReadConfig(suite.T(), suite.testState.configFile1)
-	e2e.AddNamespace(data1, namespaceInfo)
-	e2e.WriteConfig(suite.T(), suite.testState.configFile1, data1)
-
-	admin1 := client.NewResty(suite.T())
-	admin1.SetBaseURL(suite.testState.adminHost1 + "/spi/v1")
-
-	e2e.ResetFireFly(suite.T(), admin1)
-	e2e.PollForUp(suite.T(), suite.testState.client1)
-
-	client1 := client.NewFireFly(suite.T(), suite.testState.client1.Hostname, testNamespace)
-	eventNames := "token_pool_confirmed|token_transfer_confirmed"
-	queryString := fmt.Sprintf("namespace=%s&ephemeral&autoack&filter.events=%s", testNamespace, eventNames)
-	received1 := e2e.WsReader(client1.WebSocket(suite.T(), queryString, nil))
-
-	// Attempt async token operations on new namespace
-	poolName := fmt.Sprintf("pool_%s", e2e.RandomName(suite.T()))
-	suite.T().Logf("Pool name: %s", poolName)
-
-	pool := &core.TokenPool{
-		Name: poolName,
-		Key:  suite.key,
-		Type: core.TokenTypeFungible,
-	}
-	poolResp := client1.CreateTokenPool(suite.T(), pool, false)
-	poolID := poolResp.ID
-
-	e2e.WaitForEvent(suite.T(), received1, core.EventTypePoolConfirmed, poolID)
-	pools := client1.GetTokenPools(suite.T(), suite.testState.startTime)
-	assert.Equal(suite.T(), 1, len(pools))
-	assert.Equal(suite.T(), testNamespace, pools[0].Namespace)
-	assert.Equal(suite.T(), suite.connector, pools[0].Connector)
-	assert.Equal(suite.T(), poolName, pools[0].Name)
-	assert.Equal(suite.T(), core.TokenTypeFungible, pools[0].Type)
-	assert.NotEmpty(suite.T(), pools[0].Locator)
-
-	transfer := &core.TokenTransferInput{
-		TokenTransfer: core.TokenTransfer{Amount: *fftypes.NewFFBigInt(1), Key: suite.key},
-		Pool:          poolName,
-	}
-	transferOut := client1.MintTokens(suite.T(), transfer, false)
-	e2e.WaitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, transferOut.LocalID)
-	e2e.ValidateAccountBalances(suite.T(), client1, poolID, "", map[string]int64{
-		suite.key: 1,
-	})
-
-	transfer = &core.TokenTransferInput{
-		TokenTransfer: core.TokenTransfer{Amount: *fftypes.NewFFBigInt(1), Key: suite.key},
-		Pool:          poolName,
-	}
-	transferOut = client1.BurnTokens(suite.T(), transfer, false)
-	e2e.WaitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, transferOut.LocalID)
-	e2e.ValidateAccountBalances(suite.T(), client1, poolID, "", map[string]int64{
-		suite.key: 0,
-	})
-}
diff --git a/test/e2e/multiparty/common.go b/test/e2e/multiparty/common.go
index c890cb28c..d1197a71a 100644
--- a/test/e2e/multiparty/common.go
+++ b/test/e2e/multiparty/common.go
@@ -18,21 +18,26 @@ package multiparty
 
 import (
 	"context"
+	"crypto/rand"
 	"crypto/sha256"
 	"encoding/base64"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"os"
 	"path/filepath"
 	"testing"
 	"time"
 
+	"github.com/go-resty/resty/v2"
 	"github.com/gorilla/websocket"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/hyperledger/firefly/test/e2e"
 	"github.com/hyperledger/firefly/test/e2e/client"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v2"
 )
 
 const (
@@ -251,3 +256,43 @@ func validateReceivedMessages(ts *testState, client *client.FireFlyClient, topic
 	// Flip data (returned in most recent order) into delivery order
 	return returnData
 }
+
+func randomName(t *testing.T) string {
+	b := make([]byte, 5)
+	_, err := rand.Read(b)
+	assert.NoError(t, err)
+	return fmt.Sprintf("e2e_%x", b)
+}
+
+func readConfig(t *testing.T, configFile string) map[string]interface{} {
+	yfile, err := ioutil.ReadFile(configFile)
+	assert.NoError(t, err)
+	data := make(map[string]interface{})
+	err = yaml.Unmarshal(yfile, &data)
+	assert.NoError(t, err)
+	return data
+}
+
+func writeConfig(t *testing.T, configFile string, data map[string]interface{}) {
+	out, err := yaml.Marshal(data)
+	assert.NoError(t, err)
+	f, err := os.Create(configFile)
+	assert.NoError(t, err)
+	_, err = f.Write(out)
+	assert.NoError(t, err)
+	f.Close()
+}
+
+func addNamespace(data map[string]interface{}, ns map[string]interface{}) {
+	namespaces := data["namespaces"].(map[interface{}]interface{})
+	predefined := namespaces["predefined"].([]interface{})
+	namespaces["predefined"] = append(predefined, ns)
+}
+
+func resetFireFly(t *testing.T, client *resty.Client) {
+	resp, err := client.R().
+		SetBody(map[string]interface{}{}).
+		Post("/reset")
+	require.NoError(t, err)
+	assert.Equal(t, 204, resp.StatusCode())
+}
diff --git a/test/e2e/multiparty/contract_migration.go b/test/e2e/multiparty/contract_migration.go
index 3205f768d..d9d1ac114 100644
--- a/test/e2e/multiparty/contract_migration.go
+++ b/test/e2e/multiparty/contract_migration.go
@@ -55,7 +55,7 @@ func (suite *ContractMigrationTestSuite) TestContractMigration() {
 }
 
 func runMigrationTest(suite *ContractMigrationTestSuite, address1, address2 string, startOnV1 bool) {
-	testNamespace := e2e.RandomName(suite.T())
+	testNamespace := randomName(suite.T())
 	suite.T().Logf("Test namespace: %s", testNamespace)
 
 	org := map[string]interface{}{}
@@ -77,17 +77,17 @@ func runMigrationTest(suite *ContractMigrationTestSuite, address1, address2 stri
 	data := &core.DataRefOrValue{Value: fftypes.JSONAnyPtr(`"test"`)}
 
 	// Add the new namespace to both config files
-	data1 := e2e.ReadConfig(suite.T(), suite.testState.configFile1)
+	data1 := readConfig(suite.T(), suite.testState.configFile1)
 	org["name"] = suite.testState.org1.Name
 	org["key"] = suite.testState.org1key.Value
-	e2e.AddNamespace(data1, namespaceInfo)
-	e2e.WriteConfig(suite.T(), suite.testState.configFile1, data1)
+	addNamespace(data1, namespaceInfo)
+	writeConfig(suite.T(), suite.testState.configFile1, data1)
 
-	data2 := e2e.ReadConfig(suite.T(), suite.testState.configFile2)
+	data2 := readConfig(suite.T(), suite.testState.configFile2)
 	org["name"] = suite.testState.org2.Name
 	org["key"] = suite.testState.org2key.Value
-	e2e.AddNamespace(data2, namespaceInfo)
-	e2e.WriteConfig(suite.T(), suite.testState.configFile2, data2)
+	addNamespace(data2, namespaceInfo)
+	writeConfig(suite.T(), suite.testState.configFile2, data2)
 
 	admin1 := client.NewResty(suite.T())
 	admin2 := client.NewResty(suite.T())
@@ -95,8 +95,8 @@ func runMigrationTest(suite *ContractMigrationTestSuite, address1, address2 stri
 	admin2.SetBaseURL(suite.testState.adminHost2 + "/spi/v1")
 
 	// Reset both nodes to pick up the new namespace
-	e2e.ResetFireFly(suite.T(), admin1)
-	e2e.ResetFireFly(suite.T(), admin2)
+	resetFireFly(suite.T(), admin1)
+	resetFireFly(suite.T(), admin2)
 	e2e.PollForUp(suite.T(), suite.testState.client1)
 	e2e.PollForUp(suite.T(), suite.testState.client2)
 
diff --git a/test/e2e/multiparty/namespace_alias.go b/test/e2e/multiparty/namespace_alias.go
index c203221e2..7ba36a7fc 100644
--- a/test/e2e/multiparty/namespace_alias.go
+++ b/test/e2e/multiparty/namespace_alias.go
@@ -49,9 +49,9 @@ func (suite *NamespaceAliasSuite) TestNamespaceMapping() {
 	defer suite.testState.Done()
 
 	address := deployContract(suite.T(), suite.testState.stackName, "firefly/Firefly.json")
-	localNamespace1 := e2e.RandomName(suite.T())
-	localNamespace2 := e2e.RandomName(suite.T())
-	remoteNamespace := e2e.RandomName(suite.T())
+	localNamespace1 := randomName(suite.T())
+	localNamespace2 := randomName(suite.T())
+	remoteNamespace := randomName(suite.T())
 	suite.T().Logf("Test namespace: local1=%s, local2=%s, remote=%s", localNamespace1, localNamespace2, remoteNamespace)
 
 	org := map[string]interface{}{}
@@ -70,19 +70,19 @@ func (suite *NamespaceAliasSuite) TestNamespaceMapping() {
 	data := &core.DataRefOrValue{Value: fftypes.JSONAnyPtr(`"test"`)}
 
 	// Add the new namespace to both config files
-	data1 := e2e.ReadConfig(suite.T(), suite.testState.configFile1)
+	data1 := readConfig(suite.T(), suite.testState.configFile1)
 	namespaceInfo["name"] = localNamespace1
 	org["name"] = suite.testState.org1.Name
 	org["key"] = suite.testState.org1key.Value
-	e2e.AddNamespace(data1, namespaceInfo)
-	e2e.WriteConfig(suite.T(), suite.testState.configFile1, data1)
+	addNamespace(data1, namespaceInfo)
+	writeConfig(suite.T(), suite.testState.configFile1, data1)
 
-	data2 := e2e.ReadConfig(suite.T(), suite.testState.configFile2)
+	data2 := readConfig(suite.T(), suite.testState.configFile2)
 	namespaceInfo["name"] = localNamespace2
 	org["name"] = suite.testState.org2.Name
 	org["key"] = suite.testState.org2key.Value
-	e2e.AddNamespace(data2, namespaceInfo)
-	e2e.WriteConfig(suite.T(), suite.testState.configFile2, data2)
+	addNamespace(data2, namespaceInfo)
+	writeConfig(suite.T(), suite.testState.configFile2, data2)
 
 	admin1 := client.NewResty(suite.T())
 	admin2 := client.NewResty(suite.T())
@@ -90,8 +90,8 @@ func (suite *NamespaceAliasSuite) TestNamespaceMapping() {
 	admin2.SetBaseURL(suite.testState.adminHost2 + "/spi/v1")
 
 	// Reset both nodes to pick up the new namespace
-	e2e.ResetFireFly(suite.T(), admin1)
-	e2e.ResetFireFly(suite.T(), admin2)
+	resetFireFly(suite.T(), admin1)
+	resetFireFly(suite.T(), admin2)
 	e2e.PollForUp(suite.T(), suite.testState.client1)
 	e2e.PollForUp(suite.T(), suite.testState.client2)
 
diff --git a/test/e2e/multiparty/tokens.go b/test/e2e/multiparty/tokens.go
index e0d4e9ba4..af02d12f2 100644
--- a/test/e2e/multiparty/tokens.go
+++ b/test/e2e/multiparty/tokens.go
@@ -53,7 +53,7 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 	received1 := e2e.WsReader(suite.testState.ws1)
 	received2 := e2e.WsReader(suite.testState.ws2)
 
-	poolName := fmt.Sprintf("pool_%s", e2e.RandomName(suite.T()))
+	poolName := fmt.Sprintf("pool_%s", randomName(suite.T()))
 	suite.T().Logf("Pool name: %s", poolName)
 
 	pool := &core.TokenPool{
@@ -219,7 +219,7 @@ func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 	received1 := e2e.WsReader(suite.testState.ws1)
 	received2 := e2e.WsReader(suite.testState.ws2)
 
-	poolName := fmt.Sprintf("pool_%s", e2e.RandomName(suite.T()))
+	poolName := fmt.Sprintf("pool_%s", randomName(suite.T()))
 	suite.T().Logf("Pool name: %s", poolName)
 
 	pool := &core.TokenPool{
diff --git a/test/e2e/runners/ethereum_gateway_test.go b/test/e2e/runners/ethereum_gateway_test.go
index 91afbd057..d0f53cfbb 100644
--- a/test/e2e/runners/ethereum_gateway_test.go
+++ b/test/e2e/runners/ethereum_gateway_test.go
@@ -26,5 +26,4 @@ import (
 func TestEthereumGatewayE2ESuite(t *testing.T) {
 	suite.Run(t, new(gateway.TokensTestSuite))
 	suite.Run(t, new(gateway.EthereumContractTestSuite))
-	suite.Run(t, new(gateway.TokensOnlyTestSuite))
 }
diff --git a/test/e2e/stack.go b/test/e2e/stack.go
index 74f322511..1d9392194 100644
--- a/test/e2e/stack.go
+++ b/test/e2e/stack.go
@@ -27,7 +27,6 @@ type Stack struct {
 	BlockchainProvider    string    `json:"blockchainProvider"`
 	TokenProviders        []string  `json:"tokenProviders"`
 	Members               []*Member `json:"members,omitempty"`
-	Database              string    `json:"database"`
 }
 
 type StackState struct {