Repository: hyperledger/firefly
Pull request #833 (2022-05-24 15:42:32)

Namespace config validation
Adding additional configuration validation to the namespace manager, as specified in [FIR-12](https://github.com/hyperledger/firefly-fir/pull/12)


* `name` must be unique on this node
 * for historical reasons, "ff_system" is a reserved string and cannot be used as a `name` or `remoteName`
 * a `database` plugin is required for every namespace
 * if `mode: multiparty` is specified, plugins _must_ include one each of `blockchain`,
  `dataexchange`, and `sharedstorage`
 * if `mode: gateway` is speicified, plugins _must not_ include `dataexchange` or `sharedstorage`
 * at most one of each type of plugin is allowed per namespace, except for tokens (which
  may have many per namespace)

depends on changes in https://github.com/hyperledger/firefly-common/pull/14

codecov-commenter: # [Codecov](https://codecov.io/gh/hyperledger/firefly/pull/833?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) Report
> Merging [#833](https://codecov.io/gh/hyperledger/firefly/pull/833?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) (c6fd142) into [main](https://codecov.io/gh/hyperledger/firefly/commit/a425fdece91e697934f1c1b205b30626a8819c50?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) (a425fde) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head c6fd142 differs from pull request most recent head b45af09. Consider uploading reports for the commit b45af09 to get more accurate results

```diff
@@             Coverage Diff             @@
##             main      #833      +/-   ##
===========================================
+ Coverage   99.98%   100.00%   +0.01%     
===========================================
  Files         299       299              
  Lines       19457     19543      +86     
===========================================
+ Hits        19455     19543      +88     
+ Misses          1         0       -1     
+ Partials        1         0       -1     
```


| [Impacted Files](https://codecov.io/gh/hyperledger/firefly/pull/833?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) | Coverage Δ | |
|---|---|---|
| [internal/coreconfig/coreconfig.go](https://codecov.io/gh/hyperledger/firefly/pull/833/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvY29yZWNvbmZpZy9jb3JlY29uZmlnLmdv) | `100.00% <ø> (ø)` | |
| [internal/namespace/config.go](https://codecov.io/gh/hyperledger/firefly/pull/833/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvbmFtZXNwYWNlL2NvbmZpZy5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/namespace/manager.go](https://codecov.io/gh/hyperledger/firefly/pull/833/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvbmFtZXNwYWNlL21hbmFnZXIuZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/orchestrator/orchestrator.go](https://codecov.io/gh/hyperledger/firefly/pull/833/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvb3JjaGVzdHJhdG9yL29yY2hlc3RyYXRvci5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/apiserver/server.go](https://codecov.io/gh/hyperledger/firefly/pull/833/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYXBpc2VydmVyL3NlcnZlci5nbw==) | `100.00% <0.00%> (+0.61%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/hyperledger/firefly/pull/833?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/hyperledger/firefly/pull/833?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger). Last update [a425fde...b45af09](https://codecov.io/gh/hyperledger/firefly/pull/833?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger).


Patch:
diff --git a/docs/reference/config.md b/docs/reference/config.md
index 41ba62a32..072a0b98c 100644
--- a/docs/reference/config.md
+++ b/docs/reference/config.md
@@ -617,7 +617,10 @@ nav_order: 3
 |Key|Description|Type|Default Value|
 |---|-----------|----|-------------|
 |description|A description for the namespace|`string`|`<nil>`
+|mode|The namespace mode. Valid values: gateway, multiparty|`string`|`<nil>`
 |name|The name of the namespace (must be unique)|`string`|`<nil>`
+|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>`
 
 ## node
 
diff --git a/docs/reference/types/_includes/blockchainevent_description.md b/docs/reference/types/_includes/blockchainevent_description.md
index 277850b22..554153420 100644
--- a/docs/reference/types/_includes/blockchainevent_description.md
+++ b/docs/reference/types/_includes/blockchainevent_description.md
@@ -1,9 +1,9 @@
 Blockchain Events are detected by the blockchain plugin:
 
-1. When a [ContractListener](./contractlistener.html) has been
+1. When a [ContractListener](./(contractlistener.md) has been
    configured against any custom smart contract through the FireFly API
 2. Indirectly via a Token Connector, which understands the correct events
-   to listen to for a [Token Pool](./tokenpool.html) configured against a
+   to listen to for a [Token Pool](./(tokenpool.md) configured against a
    standard such as ERC-20/ERC-721/ERC-1155
 3. Automatically by FireFly core, for the BatchPin contract that can
    be used for high throughput batched pinning of off-chain data transfers
diff --git a/docs/reference/types/_includes/event_description.md b/docs/reference/types/_includes/event_description.md
index abecba752..709fad9e8 100644
--- a/docs/reference/types/_includes/event_description.md
+++ b/docs/reference/types/_includes/event_description.md
@@ -13,8 +13,8 @@ order that they are delivered to your application.
 ### Reference
 
 Events have a `reference` to the UUID of an object that is the subject of the event,
-such as a detailed [Blockchain Event](./blockchainevent.html), or an off-chain
-[Message](./message.html).
+such as a detailed [Blockchain Event](./(blockchainevent.md), or an off-chain
+[Message](./(message.md).
 
 When events are delivered to your application, the `reference` field is
 automatically retrieved and included in the JSON payload
@@ -44,14 +44,14 @@ the type of event. This is intended to be a property you would use to
 filter events to your application, or query all historical events
 associated with a given business data stream.
 
-For example when you send a [Message](./message.html), you set the `topics`
+For example when you send a [Message](./(message.md), you set the `topics`
 you want that message to apply to, and FireFly ensures a consistent global
 order between all parties that receive that message.
 
 ### Transaction
 
 When actions are submitted by a FireFly node, they are performed
-within a FireFly [Transaction](./transaction.html). The events that occur
+within a FireFly [Transaction](./(transaction.md). The events that occur
 as a direct result of that transaction, are tagged with the transaction
 ID so that they can be grouped together.
 
@@ -74,24 +74,24 @@ Note that some events cannot be tagged with a Transaction ID:
 
 | Types                                       | Reference                            | Topic                       | Correlator              |
 |---------------------------------------------|--------------------------------------|-----------------------------|-------------------------|
-| `transaction_submitted`                     | [Transaction](./transaction.html)         | `transaction.type`          |                         |
-| `message_confirmed`<br/>`message_rejected`  | [Message](./message.html)                 | `message.header.topics[i]`* | `message.header.cid`    |
-| `token_pool_confirmed`                      | [TokenPool](./tokenpool.html)             | `tokenPool.id`              |                         |
-| `token_pool_op_failed`                      | [Operation](./operation.html)             | `tokenPool.id`              | `tokenPool.id`          |
-| `token_transfer_confirmed`                  | [TokenTransfer](./tokentransfer.html)     | `tokenPool.id`              |                         |
-| `token_transfer_op_failed`                  | [Operation](./operation.html)             | `tokenPool.id`              | `tokenTransfer.localId` |
-| `token_approval_confirmed`                  | [TokenApproval](./tokenapproval.html)     | `tokenPool.id`              |                         |
-| `token_approval_op_failed`                  | [Operation](./operation.html)             | `tokenPool.id`              | `tokenApproval.localId` |
-| `namespace_confirmed`                       | [Namespace](./namespace.html)             | `"ff_definition"`           |                         |
-| `datatype_confirmed`                        | [Datatype](./datatype.html)               | `"ff_definition"`           |                         |
-| `identity_confirmed`<br/>`identity_updated` | [Identity](./identity.html)               | `"ff_definition"`           |                         |
-| `contract_interface_confirmed`              | [FFI](./ffi.html)                         | `"ff_definition"`           |                         |
-| `contract_api_confirmed`                    | [ContractAPI](./contractapi.html)         | `"ff_definition"`           |                         |
-| `blockchain_event_received`                 | [BlockchainEvent](./blockchainevent.html) | From listener **            |                         |
-| `blockchain_invoke_op_succeeded`            | [Operation](./operation.html)             |                             |                         |
-| `blockchain_invoke_op_failed`               | [Operation](./operation.html)             |                             |                         |
-
-> * A separate event is emitted for _each topic_ associated with a [Message](./message.html).
+| `transaction_submitted`                     | [Transaction](./(transaction.md)         | `transaction.type`          |                         |
+| `message_confirmed`<br/>`message_rejected`  | [Message](./(message.md)                 | `message.header.topics[i]`* | `message.header.cid`    |
+| `token_pool_confirmed`                      | [TokenPool](./(tokenpool.md)             | `tokenPool.id`              |                         |
+| `token_pool_op_failed`                      | [Operation](./(operation.md)             | `tokenPool.id`              | `tokenPool.id`          |
+| `token_transfer_confirmed`                  | [TokenTransfer](./(tokentransfer.md)     | `tokenPool.id`              |                         |
+| `token_transfer_op_failed`                  | [Operation](./(operation.md)             | `tokenPool.id`              | `tokenTransfer.localId` |
+| `token_approval_confirmed`                  | [TokenApproval](./(tokenapproval.md)     | `tokenPool.id`              |                         |
+| `token_approval_op_failed`                  | [Operation](./(operation.md)             | `tokenPool.id`              | `tokenApproval.localId` |
+| `namespace_confirmed`                       | [Namespace](./(namespace.md)             | `"ff_definition"`           |                         |
+| `datatype_confirmed`                        | [Datatype](./(datatype.md)               | `"ff_definition"`           |                         |
+| `identity_confirmed`<br/>`identity_updated` | [Identity](./(identity.md)               | `"ff_definition"`           |                         |
+| `contract_interface_confirmed`              | [FFI](./(ffi.md)                         | `"ff_definition"`           |                         |
+| `contract_api_confirmed`                    | [ContractAPI](./(contractapi.md)         | `"ff_definition"`           |                         |
+| `blockchain_event_received`                 | [BlockchainEvent](./(blockchainevent.md) | From listener **            |                         |
+| `blockchain_invoke_op_succeeded`            | [Operation](./(operation.md)             |                             |                         |
+| `blockchain_invoke_op_failed`               | [Operation](./(operation.md)             |                             |                         |
+
+> * A separate event is emitted for _each topic_ associated with a [Message](./(message.md).
 
 > ** The topic for a blockchain event is inherited from the blockchain listener,
 >    allowing you to create multiple blockchain listeners that all deliver messages
diff --git a/docs/reference/types/_includes/identity_description.md b/docs/reference/types/_includes/identity_description.md
index 95fe8b226..ae129710d 100644
--- a/docs/reference/types/_includes/identity_description.md
+++ b/docs/reference/types/_includes/identity_description.md
@@ -10,12 +10,12 @@ Root identities are registered with only a `claim` - which is a signed
 transaction from a particular blockchain account, to bind a DID with a
 `name` that is unique within the network, to that signing key.
 
-The signing key then becomes a [Verifier](./verifier.html) for that identity.
+The signing key then becomes a [Verifier](./(verifier.md) for that identity.
 Using that key the root identity can be used to register a new FireFly node
 in the network, send and receive messages, and register child identities.
 
 When child identities are registered, a `claim` using a key that is going
-to be the [Verifier](./verifier.html) for that child identity is required.
+to be the [Verifier](./(verifier.md) for that child identity is required.
 However, this is insufficient to establish that identity as a child identity
 of the parent. There must be an additional `verification` that references
 the `claim` (by UUID) using the key verifier of the parent identity.
diff --git a/docs/reference/types/_includes/message_description.md b/docs/reference/types/_includes/message_description.md
index a5670ad5f..8821d7fee 100644
--- a/docs/reference/types/_includes/message_description.md
+++ b/docs/reference/types/_includes/message_description.md
@@ -18,7 +18,7 @@ The hash is a function of the header, and all of the data payloads. Calculated a
   - JSON data is serialized without whitespace to hash it.
   - The hashing algorithm is SHA-256
 
-Each node independently calculates the hash, and the hash is included in the manifest of the [Batch](./batch.html) by the
+Each node independently calculates the hash, and the hash is included in the manifest of the [Batch](./(batch.md) by the
 node that sends the message.
 Because the hash of that batch manifest is included in the blockchain transaction, a message transferred to
 a node that does not match the original message hash is rejected.
@@ -95,7 +95,7 @@ Some examples:
 
 ### Transaction type
 
-By default messages are pinned to the blockchain, within a [Batch](./batch.html).
+By default messages are pinned to the blockchain, within a [Batch](./(batch.md).
 
 For private messages, you can choose to disable this pinning by setting `header.txtype: "unpinned"`.
 
@@ -103,7 +103,7 @@ Broadcast messages must be pinned to the blockchain.
 
 ### In-line data
 
-When sending a message you can specify the array of [Data](./data.html) attachments in-line, as part of the same JSON payload.
+When sending a message you can specify the array of [Data](./(data.md) attachments in-line, as part of the same JSON payload.
 
 For example, a minimal broadcast message could be:
 
diff --git a/docs/reference/types/_includes/operation_description.md b/docs/reference/types/_includes/operation_description.md
index 7ccf60ae9..5d0af0d63 100644
--- a/docs/reference/types/_includes/operation_description.md
+++ b/docs/reference/types/_includes/operation_description.md
@@ -3,6 +3,6 @@ They are grouped into Transactions in order to accomplish a single logical task.
 
 The diagram below shows the different types of operation that are performed by each
 FireFly plugin type. The color coding (and numbers) map those different types of operation
-to the [Transaction](./transaction.html) types that include those operations.
+to the [Transaction](./(transaction.md) types that include those operations.
 
 [![FireFly operations by transaction type](../../images/operations_by_transaction_type.jpg)](../../images/operations_by_transaction_type.jpg)
\ No newline at end of file
diff --git a/docs/reference/types/_includes/subscription_description.md b/docs/reference/types/_includes/subscription_description.md
index e38fddf82..15a82b11f 100644
--- a/docs/reference/types/_includes/subscription_description.md
+++ b/docs/reference/types/_includes/subscription_description.md
@@ -75,12 +75,12 @@ FireFly has a simple protocol on top of WebSockets:
 1. Each time you connect/reconnect you need to tell FireFly to start
    sending you events on a particular subscription. You can do this in two
    ways (described in detail below):
-    1. Send a [WSStart](./wsstart.html) JSON payload
+    1. Send a [WSStart](./(wsstart.md) JSON payload
     2. Include a `namespace` and `name` query parameter in the URL when you
-       connect, along with query params for other fields of [WSStart](./wsstart.html)
+       connect, along with query params for other fields of [WSStart](./(wsstart.md)
 2. One you have started your subscription, each event flows from
-   the server, to your application as a JSON [Event](./event.html) payload
-3. For each event you receive, you need to send a [WSAck](./wsack.html) payload.
+   the server, to your application as a JSON [Event](./(event.md) payload
+3. For each event you receive, you need to send a [WSAck](./(wsack.md) payload.
     - Unless you specified `autoack` in step (1)
 
 > The SDK libraries for FireFly help you ensure you send the `start`
diff --git a/docs/reference/types/_includes/transaction_description.md b/docs/reference/types/_includes/transaction_description.md
index 70fc4e6c5..b87790325 100644
--- a/docs/reference/types/_includes/transaction_description.md
+++ b/docs/reference/types/_includes/transaction_description.md
@@ -1,4 +1,4 @@
-FireFly Transactions are a grouping construct for a number of [Operations](./operation.html) and [Events](./event.html)
+FireFly Transactions are a grouping construct for a number of [Operations](./(operations.md) and [Events](./(events.md)
 that need to complete or fail as unit.
 
 > FireFly Transactions are not themselves Blockchain transactions, but in many cases there is
@@ -24,7 +24,7 @@ event as well.
 [![FireFly Transactions - Explorer View](../../images/firefly_transactions_explorer_view.png)](../../images/firefly_transactions_explorer_view.png)
 
 Each FireFly transaction has a UUID. This UUID is propagated through to all participants in a FireFly transaction.
-For example in a [Token Transfer](./tokentransfer.html) that is coordinated with an off-chain private [Message](./message.html),
+For example in a [Token Transfer](./(tokentransfer.md) that is coordinated with an off-chain private [Message](./(message.md),
 the transaction ID is propagated to all parties who are part of that transaction. So the same UUID can be used
 to find the transaction in the FireFly Explorer of any member who has access to the message.
 This is possible because hash-pinned off-chain data is associated with that on-chain transfer.
diff --git a/docs/reference/types/_includes/wsack_description.md b/docs/reference/types/_includes/wsack_description.md
index b9ca8c53c..a20218330 100644
--- a/docs/reference/types/_includes/wsack_description.md
+++ b/docs/reference/types/_includes/wsack_description.md
@@ -1,6 +1,6 @@
 An `ack` must be sent on a WebSocket for each event delivered to an application.
 
-> Unless `autoack` is set in the [WSStart](./wsstart.html) payload/URL parameters to cause
+> Unless `autoack` is set in the [WSStart](./(wsstart.md) payload/URL parameters to cause
 > automatic acknowledgement.
 
 Your application should specify the `id` of each event that it acknowledges.
@@ -11,5 +11,5 @@ application that has not been acknowledged is the one the `ack` is associated wi
 If multiple subscriptions are started on a WebSocket, then you need to specify the
 subscription `namespace`+`name` as part of each `ack`.
 
-If you send an acknowledgement that cannot be correlated, then a [WSError](./wserror.html)
+If you send an acknowledgement that cannot be correlated, then a [WSError](./(wserror.md)
 payload will be sent to the application.
diff --git a/go.mod b/go.mod
index c5a00b52d..650770da6 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/golang-migrate/migrate/v4 v4.15.1
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/websocket v1.5.0
-	github.com/hyperledger/firefly-common v0.1.5
+	github.com/hyperledger/firefly-common v0.1.6
 	github.com/jarcoal/httpmock v1.1.0
 	github.com/karlseguin/ccache v2.0.3+incompatible
 	github.com/lib/pq v1.10.4
diff --git a/go.sum b/go.sum
index d39901416..652bbf1b1 100644
--- a/go.sum
+++ b/go.sum
@@ -637,6 +637,8 @@ github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/hyperledger/firefly-common v0.1.5 h1:66RXEAc/dsSEYPRye3G48Lk9sN+AmYGPf1FkrkbQGWs=
 github.com/hyperledger/firefly-common v0.1.5/go.mod h1:qGy7i8eWlE8Ed7jFn2Hpn8bYaflL204j4NJB1mAfTms=
+github.com/hyperledger/firefly-common v0.1.6 h1:I6t7N5D3YlaPSGknYF2xLurfEO+UQsHV4VFYeR/3UI4=
+github.com/hyperledger/firefly-common v0.1.6/go.mod h1:qGy7i8eWlE8Ed7jFn2Hpn8bYaflL204j4NJB1mAfTms=
 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
diff --git a/internal/coreconfig/coreconfig.go b/internal/coreconfig/coreconfig.go
index 382c13f45..d0ee9394a 100644
--- a/internal/coreconfig/coreconfig.go
+++ b/internal/coreconfig/coreconfig.go
@@ -29,9 +29,15 @@ const (
 	PluginConfigName = "name"
 	// PluginConfigType is the type of the plugin to be loaded
 	PluginConfigType = "type"
-	// NamespaceName is a short name for a pre-defined namespace
+	// NamespaceMode is the configured mode of a namespace
+	NamespaceMode = "mode"
+	// NamespaceRemoteName is the namespace name to be sent in plugin calls
+	NamespaceRemoteName = "remoteName"
+	// NamespacePlugins is the list of namespace plugins
+	NamespacePlugins = "plugins"
+	// NamespaceName is the short name for a pre-defined namespace
 	NamespaceName = "name"
-	// NamespaceName is a long description for a pre-defined namespace
+	// NamespaceName is the long description for a pre-defined namespace
 	NamespaceDescription = "description"
 )
 
diff --git a/internal/coremsgs/en_config_descriptions.go b/internal/coremsgs/en_config_descriptions.go
index 7550426a1..4ad572d7e 100644
--- a/internal/coremsgs/en_config_descriptions.go
+++ b/internal/coremsgs/en_config_descriptions.go
@@ -253,6 +253,9 @@ var (
 	ConfigNamespacesDefault               = ffc("config.namespaces.default", "The default namespace - must be in the predefined list", i18n.StringType)
 	ConfigNamespacesPredefined            = ffc("config.namespaces.predefined", "A list of namespaces to ensure exists, without requiring a broadcast from the network", "List "+i18n.StringType)
 	ConfigNamespacesPredefinedName        = ffc("config.namespaces.predefined[].name", "The name of the namespace (must be unique)", i18n.StringType)
+	ConfigNamespacesPredefinedPlugins     = ffc("config.namespaces.predefined[].plugins", "The list of plugins for this namespace", i18n.StringType)
+	ConfigNamespacesPredefinedMode        = ffc("config.namespaces.predefined[].mode", "The namespace mode. Valid values: gateway, multiparty", 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)
 	ConfigNamespacesPredefinedDescription = ffc("config.namespaces.predefined[].description", "A description for the namespace", i18n.StringType)
 
 	ConfigNodeDescription = ffc("config.node.description", "The description of this FireFly node", i18n.StringType)
diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go
index 6de23ff5f..0a2d19f94 100644
--- a/internal/coremsgs/en_error_messages.go
+++ b/internal/coremsgs/en_error_messages.go
@@ -27,211 +27,219 @@ var ffe = func(key, translation string, statusHint ...int) i18n.ErrorMessageKey
 
 //revive:disable
 var (
-	MsgConfigFailed                 = ffe("FF10101", "Failed to read config")
-	MsgJSONDecodeFailed             = ffe("FF10103", "Failed to decode input JSON")
-	MsgTLSConfigFailed              = ffe("FF10105", "Failed to initialize TLS configuration")
-	MsgResponseMarshalError         = ffe("FF10107", "Failed to serialize response data", 400)
-	MsgWebsocketClientError         = ffe("FF10108", "Error received from WebSocket client: %s")
-	Msg404NotFound                  = ffe("FF10109", "Not found", 404)
-	MsgUnknownBlockchainPlugin      = ffe("FF10110", "Unknown blockchain plugin: %s")
-	MsgEthconnectRESTErr            = ffe("FF10111", "Error from ethconnect: %s")
-	MsgDBInitFailed                 = ffe("FF10112", "Database initialization failed")
-	MsgDBQueryBuildFailed           = ffe("FF10113", "Database query builder failed")
-	MsgDBBeginFailed                = ffe("FF10114", "Database begin transaction failed")
-	MsgDBQueryFailed                = ffe("FF10115", "Database query failed")
-	MsgDBInsertFailed               = ffe("FF10116", "Database insert failed")
-	MsgDBUpdateFailed               = ffe("FF10117", "Database update failed")
-	MsgDBDeleteFailed               = ffe("FF10118", "Database delete failed")
-	MsgDBCommitFailed               = ffe("FF10119", "Database commit failed")
-	MsgDBMissingJoin                = ffe("FF10120", "Database missing expected join entry in table '%s' for id '%s'")
-	MsgDBReadErr                    = ffe("FF10121", "Database resultset read error from table '%s'")
-	MsgUnknownDatabasePlugin        = ffe("FF10122", "Unknown database plugin '%s'")
-	MsgNullDataReferenceID          = ffe("FF10123", "Data id is null in message data reference %d")
-	MsgDupDataReferenceID           = ffe("FF10124", "Duplicate data ID in message '%s'")
-	MsgScanFailed                   = ffe("FF10125", "Failed to restore type '%T' into '%T'")
-	MsgUnregisteredBatchType        = ffe("FF10126", "Unregistered batch type '%s'")
-	MsgBatchDispatchTimeout         = ffe("FF10127", "Timed out dispatching work to batch")
-	MsgInitializationNilDepError    = ffe("FF10128", "Initialization failed in %s due to unmet dependency")
-	MsgNilResponseNon204            = ffe("FF10129", "No output from API call")
-	MsgInvalidContentType           = ffe("FF10130", "Invalid content type", 415)
-	MsgDataNotFound                 = ffe("FF10133", "Data not found for message %s", 400)
-	MsgUnknownSharedStoragePlugin   = ffe("FF10134", "Unknown Shared Storage plugin '%s'")
-	MsgIPFSHashDecodeFailed         = ffe("FF10135", "Failed to decode IPFS hash into 32byte value '%s'")
-	MsgIPFSRESTErr                  = ffe("FF10136", "Error from IPFS: %s")
-	MsgSerializationFailed          = ffe("FF10137", "Serialization failed")
-	MsgMissingPluginConfig          = ffe("FF10138", "Missing configuration '%s' for %s")
-	MsgMissingDataHashIndex         = ffe("FF10139", "Missing data hash for index '%d' in message", 400)
-	MsgInvalidEthAddress            = ffe("FF10141", "Supplied ethereum address is invalid", 400)
-	Msg404NoResult                  = ffe("FF10143", "No result found", 404)
-	MsgUnsupportedSQLOpInFilter     = ffe("FF10150", "No SQL mapping implemented for filter operator '%s'", 400)
-	MsgFilterSortDesc               = ffe("FF10154", "Sort field. For multi-field sort use comma separated values (or multiple query values) with '-' prefix for descending")
-	MsgContextCanceled              = ffe("FF00154", "Context cancelled")
-	MsgDBMigrationFailed            = ffe("FF10163", "Database migration failed")
-	MsgHashMismatch                 = ffe("FF10164", "Hash mismatch")
-	MsgDefaultNamespaceNotFound     = ffe("FF10166", "namespaces.default '%s' must be included in the namespaces.predefined configuration")
-	MsgEventTypesParseFail          = ffe("FF10168", "Unable to parse list of event types", 400)
-	MsgUnknownEventType             = ffe("FF10169", "Unknown event type '%s'", 400)
-	MsgIDMismatch                   = ffe("FF10170", "ID mismatch")
-	MsgRegexpCompileFailed          = ffe("FF10171", "Unable to compile '%s' regexp '%s'")
-	MsgUnknownEventTransportPlugin  = ffe("FF10172", "Unknown event transport plugin: %s")
-	MsgWSConnectionNotActive        = ffe("FF10173", "Websocket connection '%s' no longer active")
-	MsgWSSubAlreadyInFlight         = ffe("FF10174", "Websocket subscription '%s' already has a message in flight")
-	MsgWSMsgSubNotMatched           = ffe("FF10175", "Acknowledgment does not match an inflight event + subscription")
-	MsgWSClientSentInvalidData      = ffe("FF10176", "Invalid data")
-	MsgWSClientUnknownAction        = ffe("FF10177", "Unknown action '%s'")
-	MsgWSInvalidStartAction         = ffe("FF10178", "A start action must set namespace and either a name or ephemeral=true")
-	MsgWSAutoAckChanged             = ffe("FF10179", "The autoack option must be set consistently on all start requests")
-	MsgWSAutoAckEnabled             = ffe("FF10180", "The autoack option is enabled on this connection")
-	MsgConnSubscriptionNotStarted   = ffe("FF10181", "Subscription %v is not started on connection")
-	MsgDispatcherClosing            = ffe("FF10182", "Event dispatcher closing")
-	MsgMaxFilterSkip                = ffe("FF10183", "You have reached the maximum pagination limit for this query (%d)")
-	MsgMaxFilterLimit               = ffe("FF10184", "Your query exceeds the maximum filter limit (%d)")
-	MsgAPIServerStaticFail          = ffe("FF10185", "An error occurred loading static content", 500)
-	MsgEventListenerClosing         = ffe("FF10186", "Event listener closing")
-	MsgNamespaceNotExist            = ffe("FF10187", "Namespace does not exist")
-	MsgInvalidSubscription          = ffe("FF10189", "Invalid subscription", 400)
-	MsgMismatchedTransport          = ffe("FF10190", "Connection ID '%s' appears not to be unique between transport '%s' and '%s'", 400)
-	MsgInvalidFirstEvent            = ffe("FF10191", "Invalid firstEvent definition - must be 'newest','oldest' or a sequence number", 400)
-	MsgNumberMustBeGreaterEqual     = ffe("FF10192", "Number must be greater than or equal to %d", 400)
-	MsgAlreadyExists                = ffe("FF10193", "A %s with name '%s:%s' already exists", 409)
-	MsgJSONValidatorBadRef          = ffe("FF10194", "Cannot use JSON validator for data with type '%s' and validator reference '%v'", 400)
-	MsgDatatypeNotFound             = ffe("FF10195", "Datatype '%v' not found", 400)
-	MsgSchemaLoadFailed             = ffe("FF10196", "Datatype '%s' schema invalid", 400)
-	MsgDataCannotBeValidated        = ffe("FF10197", "Data cannot be validated", 400)
-	MsgJSONDataInvalidPerSchema     = ffe("FF10198", "Data does not conform to the JSON schema of datatype '%s': %s", 400)
-	MsgDataValueIsNull              = ffe("FF10199", "Data value is null", 400)
-	MsgDataInvalidHash              = ffe("FF10201", "Invalid data: hashes do not match Hash=%s Expected=%s", 400)
-	MsgDataReferenceUnresolvable    = ffe("FF10204", "Data reference %d cannot be resolved", 400)
-	MsgDataMissing                  = ffe("FF10205", "Data entry %d has neither 'id' to refer to existing data, or 'value' to include in-line JSON data", 400)
-	MsgAuthorInvalid                = ffe("FF10206", "Invalid author specified", 400)
-	MsgNoTransaction                = ffe("FF10207", "Message does not have a transaction", 404)
-	MsgBatchNotSet                  = ffe("FF10208", "Message does not have an assigned batch", 404)
-	MsgBatchNotFound                = ffe("FF10209", "Batch '%s' not found for message", 500)
-	MsgBatchTXNotSet                = ffe("FF10210", "Batch '%s' does not have an assigned transaction", 404)
-	MsgOwnerMissing                 = ffe("FF10211", "Owner missing", 400)
-	MsgUnknownIdentityPlugin        = ffe("FF10212", "Unknown Identity plugin '%s'")
-	MsgUnknownDataExchangePlugin    = ffe("FF10213", "Unknown Data Exchange plugin '%s'")
-	MsgParentIdentityNotFound       = ffe("FF10214", "Identity '%s' not found in identity chain for %s '%s'")
-	MsgInvalidSigningIdentity       = ffe("FF10215", "Invalid signing identity")
-	MsgNodeAndOrgIDMustBeSet        = ffe("FF10216", "node.name, org.name and org.identity must be configured first", 409)
-	MsgBlobStreamingFailed          = ffe("FF10217", "Blob streaming terminated with error", 500)
-	MsgMultiPartFormReadError       = ffe("FF10218", "Error reading multi-part form input", 400)
-	MsgNodeNotFound                 = ffe("FF10224", "Node with name or identity '%s' not found", 400)
-	MsgLocalNodeResolveFailed       = ffe("FF10225", "Unable to find local node to add to group. Check the status API to confirm the node is registered", 500)
-	MsgGroupNotFound                = ffe("FF10226", "Group '%s' not found", 404)
-	MsgDXRESTErr                    = ffe("FF10229", "Error from data exchange: %s")
-	MsgInvalidHex                   = ffe("FF10231", "Invalid hex supplied", 400)
-	MsgInvalidWrongLenB32           = ffe("FF00107", "Byte length must be 32 (64 hex characters)", 400)
-	MsgNodeNotFoundInOrg            = ffe("FF10233", "Unable to find any nodes owned by org '%s', or parent orgs", 400)
-	MsgFieldsAfterFile              = ffe("FF10236", "Additional form field sent after file in multi-part form (ignored): '%s'", 400)
-	MsgDXBadResponse                = ffe("FF10237", "Unexpected '%s' in data exchange response: %s")
-	MsgDXBadHash                    = ffe("FF10238", "Unexpected hash returned from data exchange upload. Hash=%s Expected=%s")
-	MsgBlobNotFound                 = ffe("FF10239", "No blob has been uploaded or confirmed received, with hash=%s", 404)
-	MsgDownloadBlobFailed           = ffe("FF10240", "Error download blob with reference '%s' from local data exchange")
-	MsgDataDoesNotHaveBlob          = ffe("FF10241", "Data does not have a blob attachment", 404)
-	MsgWebhookURLEmpty              = ffe("FF10242", "Webhook subscription option 'url' cannot be empty", 400)
-	MsgWebhookInvalidStringMap      = ffe("FF10243", "Webhook subscription option '%s' must be map of string values. %s=%T", 400)
-	MsgWebsocketsNoData             = ffe("FF10244", "Websockets subscriptions do not support streaming the full data payload, just the references (withData must be false)", 400)
-	MsgWebhooksWithData             = ffe("FF10245", "Webhook subscriptions require the full data payload (withData must be true)", 400)
-	MsgWebhooksReplyBadJSON         = ffe("FF10257", "Failed to process reply from webhook as JSON")
-	MsgRequestTimeout               = ffe("FF10260", "The request with id '%s' timed out after %.2fms", 408)
-	MsgRequestReplyTagRequired      = ffe("FF10261", "For request messages 'header.tag' must be set on the request message to route it to a suitable responder", 400)
-	MsgRequestCannotHaveCID         = ffe("FF10262", "For request messages 'header.cid' must be unset", 400)
-	MsgSystemTransportInternal      = ffe("FF10266", "You cannot create subscriptions on the system events transport")
-	MsgFilterCountNotSupported      = ffe("FF10267", "This query does not support generating a count of all results")
-	MsgRejected                     = ffe("FF10269", "Message with ID '%s' was rejected. Please check the FireFly logs for more information")
-	MsgRequestMustBePrivate         = ffe("FF10271", "For request messages you must specify a group of private recipients", 400)
-	MsgUnknownTokensPlugin          = ffe("FF10272", "Unknown tokens plugin '%s'", 400)
-	MsgMissingTokensPluginConfig    = ffe("FF10273", "Invalid tokens configuration - name and plugin are required", 400)
-	MsgTokensRESTErr                = ffe("FF10274", "Error from tokens service: %s")
-	MsgTokenPoolDuplicate           = ffe("FF10275", "Duplicate token pool: %s")
-	MsgTokenPoolRejected            = ffe("FF10276", "Token pool with ID '%s' was rejected. Please check the FireFly logs for more information")
-	MsgIdentityNotFoundByString     = ffe("FF10277", "Identity could not be resolved via lookup string '%s'")
-	MsgAuthorOrgSigningKeyMismatch  = ffe("FF10279", "Author organization '%s' is not associated with signing key '%s'")
-	MsgCannotTransferToSelf         = ffe("FF10280", "From and to addresses must be different", 400)
-	MsgLocalOrgLookupFailed         = ffe("FF10281", "Unable resolve the local org '%s' by the configured signing key on the node. Please confirm the org is registered with key '%s'", 500)
-	MsgFabconnectRESTErr            = ffe("FF10284", "Error from fabconnect: %s")
-	MsgInvalidIdentity              = ffe("FF10285", "Supplied Fabric signer identity is invalid", 400)
-	MsgFailedToDecodeCertificate    = ffe("FF10286", "Failed to decode certificate: %s", 500)
-	MsgInvalidMessageType           = ffe("FF10287", "Invalid message type - allowed types are %s", 400)
-	MsgWSClosed                     = ffe("FF10290", "Websocket closed")
-	MsgFieldNotSpecified            = ffe("FF10292", "Field '%s' must be specified", 400)
-	MsgTokenPoolNotConfirmed        = ffe("FF10293", "Token pool is not yet confirmed")
-	MsgHistogramCollectionParam     = ffe("FF10297", "Collection to fetch")
-	MsgInvalidNumberOfIntervals     = ffe("FF10298", "Number of time intervals must be between %d and %d", 400)
-	MsgInvalidChartNumberParam      = ffe("FF10299", "Invalid %s. Must be a number.", 400)
-	MsgHistogramInvalidTimes        = ffe("FF10300", "Start time must be before end time", 400)
-	MsgUnsupportedCollection        = ffe("FF10301", "%s collection is not supported", 400)
-	MsgContractInterfaceExists      = ffe("FF10302", "A contract interface already exists in the namespace: '%s' with name: '%s' and version: '%s'", 409)
-	MsgContractInterfaceNotFound    = ffe("FF10303", "Contract interface %s not found", 404)
-	MsgContractMissingInputArgument = ffe("FF10304", "Missing required input argument '%s'", 400)
-	MsgContractWrongInputType       = ffe("FF10305", "Input '%v' is of type '%v' not expected type of '%v'", 400)
-	MsgContractMissingInputField    = ffe("FF10306", "Expected object of type '%v' to contain field named '%v' but it was missing", 400)
-	MsgContractMapInputType         = ffe("FF10307", "Unable to map input type '%v' to known FireFly type - was expecting '%v'", 400)
-	MsgContractByteDecode           = ffe("FF10308", "Unable to decode field '%v' as bytes", 400)
-	MsgContractInternalType         = ffe("FF10309", "Input '%v' of type '%v' is not compatible blockchain internalType of '%v'", 400)
-	MsgContractLocationInvalid      = ffe("FF10310", "Failed to validate contract location: %v", 400)
-	MsgContractParamInvalid         = ffe("FF10311", "Failed to validate contract param: %v", 400)
-	MsgContractListenerNameExists   = ffe("FF10312", "A contract listener already exists in the namespace: '%s' with name: '%s'", 409)
-	MsgContractMethodNotSet         = ffe("FF10313", "Either an interface reference and method path, or in-line method definition, must be supplied on invoke contract request", 400)
-	MsgContractMethodResolveError   = ffe("FF10315", "Unable to resolve contract method: %s", 400)
-	MsgContractLocationExists       = ffe("FF10316", "The contract location cannot be changed after it is created", 400)
-	MsgListenerNoEvent              = ffe("FF10317", "Either an interface reference and event path, or in-line event definition must be supplied when creating a contract listener", 400)
-	MsgListenerEventNotFound        = ffe("FF10318", "No event was found in namespace '%s' with id '%s'", 400)
-	MsgEventNameMustBeSet           = ffe("FF10319", "Event name must be set", 400)
-	MsgMethodNameMustBeSet          = ffe("FF10320", "Method name must be set", 400)
-	MsgContractEventResolveError    = ffe("FF10321", "Unable to resolve contract event", 400)
-	MsgQueryOpUnsupportedMod        = ffe("FF10322", "Operation '%s' on '%s' does not support modifiers", 400)
-	MsgDXBadSize                    = ffe("FF10323", "Unexpected size returned from data exchange upload. Size=%d Expected=%d")
-	MsgTooLargeBroadcast            = ffe("FF10327", "Message size %.2fkb is too large for the max broadcast batch size of %.2fkb", 400)
-	MsgTooLargePrivate              = ffe("FF10328", "Message size %.2fkb is too large for the max private message size of %.2fkb", 400)
-	MsgManifestMismatch             = ffe("FF10329", "Manifest mismatch overriding '%s' status as failure: '%s'", 400)
-	MsgFFIValidationFail            = ffe("FF10331", "Field '%s' does not validate against the provided schema", 400)
-	MsgFFISchemaParseFail           = ffe("FF10332", "Failed to parse schema for param '%s'", 400)
-	MsgFFISchemaCompileFail         = ffe("FF10333", "Failed compile schema for param '%s'", 400)
-	MsgPluginInitializationFailed   = ffe("FF10334", "Plugin initialization error", 500)
-	MsgUnknownTransactionType       = ffe("FF10336", "Unknown transaction type '%s'", 400)
-	MsgGoTemplateCompileFailed      = ffe("FF10337", "Go template compilation for '%s' failed: %s", 500)
-	MsgGoTemplateExecuteFailed      = ffe("FF10338", "Go template execution for '%s' failed: %s", 500)
-	MsgAddressResolveFailed         = ffe("FF10339", "Failed to resolve signing key string '%s': %s", 500)
-	MsgAddressResolveBadStatus      = ffe("FF10340", "Failed to resolve signing key string '%s' [%d]: %s", 500)
-	MsgAddressResolveBadResData     = ffe("FF10341", "Failed to resolve signing key string '%s' - invalid address returned '%s': %s", 500)
-	MsgDXNotInitialized             = ffe("FF10342", "Data exchange is initializing")
-	MsgGroupRequired                = ffe("FF10344", "Group must be set", 400)
-	MsgDBLockFailed                 = ffe("FF10345", "Database lock failed")
-	MsgFFIGenerationFailed          = ffe("FF10346", "Error generating smart contract interface: %s", 400)
-	MsgFFIGenerationUnsupported     = ffe("FF10347", "Smart contract interface generation is not supported by this blockchain plugin", 400)
-	MsgBlobHashMismatch             = ffe("FF10348", "Blob hash mismatch sent=%s received=%s", 400)
-	MsgDIDResolverUnknown           = ffe("FF10349", "DID resolver unknown for DID: %s", 400)
-	MsgIdentityNotOrg               = ffe("FF10350", "Identity '%s' with DID '%s' is not an organization", 400)
-	MsgIdentityNotNode              = ffe("FF10351", "Identity '%s' with DID '%s' is not a node", 400)
-	MsgBlockchainKeyNotSet          = ffe("FF10352", "No blockchain key specified", 400)
-	MsgNoVerifierForIdentity        = ffe("FF10353", "No %s verifier registered for identity %s", 400)
-	MsgNodeMissingBlockchainKey     = ffe("FF10354", "No organization signing key configured on node", 400)
-	MsgAuthorRegistrationMismatch   = ffe("FF10355", "Verifier '%s' cannot be used for signing with author '%s'. Verifier registered to '%s'", 400)
-	MsgAuthorMissingForKey          = ffe("FF10356", "Key '%s' has not been registered by any identity, and a separate 'author' was not supplied", 404)
-	MsgAuthorIncorrectForRootReg    = ffe("FF10357", "Author namespace '%s' and DID '%s' combination invalid for root organization registration", 400)
-	MsgKeyIdentityMissing           = ffe("FF10358", "Identity owner of key '%s' not found", 500)
-	MsgIdentityChainLoop            = ffe("FF10364", "Loop detected on identity %s in chain for %s (%s)", 400)
-	MsgInvalidIdentityParentType    = ffe("FF10365", "Parent %s (%s) of type %s is invalid for child %s (%s) of type", 400)
-	MsgParentIdentityMissingClaim   = ffe("FF10366", "Parent %s (%s) is invalid (missing claim)", 400)
-	MsgDXInfoMissingID              = ffe("FF10367", "Data exchange endpoint info missing 'id' field", 500)
-	MsgEventNotFound                = ffe("FF10370", "Event with name '%s' not found", 400)
-	MsgOperationNotSupported        = ffe("FF10371", "Operation not supported: %s", 400)
-	MsgFailedToRetrieve             = ffe("FF10372", "Failed to retrieve %s %s", 500)
-	MsgBlobMissingPublic            = ffe("FF10373", "Blob for data %s missing public payload reference while flushing batch", 500)
-	MsgDBMultiRowConfigError        = ffe("FF10374", "Database invalid configuration - using multi-row insert on DB plugin that does not support query syntax for input")
-	MsgDBNoSequence                 = ffe("FF10375", "Failed to retrieve sequence for insert row %d (could mean duplicate insert)", 500)
-	MsgDownloadSharedFailed         = ffe("FF10376", "Error downloading data with reference '%s' from shared storage")
-	MsgDownloadBatchMaxBytes        = ffe("FF10377", "Error downloading batch with reference '%s' from shared storage - maximum size limit reached")
-	MsgOperationDataIncorrect       = ffe("FF10378", "Operation data type incorrect: %T", 400)
-	MsgDataMissingBlobHash          = ffe("FF10379", "Blob for data %s cannot be transferred as it is missing a hash", 500)
-	MsgUnexpectedDXMessageType      = ffe("FF10380", "Unexpected websocket event type from DX plugin: %s", 500)
-	MsgFieldDescriptionMissing      = ffe("FF10381", "Field description missing for '%s' on route '%s'")
-	MsgFFStructTagMissing           = ffe("FF10382", "ffstruct tag is missing for '%s' on route '%s'")
-	MsgContractListenerExists       = ffe("FF10383", "A contract listener already exists for this combination of topic + location + event", 409)
-	MsgRouteDescriptionMissing      = ffe("FF10384", "API route description missing for route '%s'")
-	MsgInvalidOutputOption          = ffe("FF10385", "invalid output option '%s'")
-	MsgInvalidPluginConfiguration   = ffe("FF10386", "Invalid %s plugin configuration - name and type are required")
-	MsgReferenceMarkdownMissing     = ffe("FF10387", "Reference markdown file missing: '%s'")
+	MsgConfigFailed                       = ffe("FF10101", "Failed to read config")
+	MsgJSONDecodeFailed                   = ffe("FF10103", "Failed to decode input JSON")
+	MsgTLSConfigFailed                    = ffe("FF10105", "Failed to initialize TLS configuration")
+	MsgResponseMarshalError               = ffe("FF10107", "Failed to serialize response data", 400)
+	MsgWebsocketClientError               = ffe("FF10108", "Error received from WebSocket client: %s")
+	Msg404NotFound                        = ffe("FF10109", "Not found", 404)
+	MsgUnknownBlockchainPlugin            = ffe("FF10110", "Unknown blockchain plugin: %s")
+	MsgEthconnectRESTErr                  = ffe("FF10111", "Error from ethconnect: %s")
+	MsgDBInitFailed                       = ffe("FF10112", "Database initialization failed")
+	MsgDBQueryBuildFailed                 = ffe("FF10113", "Database query builder failed")
+	MsgDBBeginFailed                      = ffe("FF10114", "Database begin transaction failed")
+	MsgDBQueryFailed                      = ffe("FF10115", "Database query failed")
+	MsgDBInsertFailed                     = ffe("FF10116", "Database insert failed")
+	MsgDBUpdateFailed                     = ffe("FF10117", "Database update failed")
+	MsgDBDeleteFailed                     = ffe("FF10118", "Database delete failed")
+	MsgDBCommitFailed                     = ffe("FF10119", "Database commit failed")
+	MsgDBMissingJoin                      = ffe("FF10120", "Database missing expected join entry in table '%s' for id '%s'")
+	MsgDBReadErr                          = ffe("FF10121", "Database resultset read error from table '%s'")
+	MsgUnknownDatabasePlugin              = ffe("FF10122", "Unknown database plugin '%s'")
+	MsgNullDataReferenceID                = ffe("FF10123", "Data id is null in message data reference %d")
+	MsgDupDataReferenceID                 = ffe("FF10124", "Duplicate data ID in message '%s'")
+	MsgScanFailed                         = ffe("FF10125", "Failed to restore type '%T' into '%T'")
+	MsgUnregisteredBatchType              = ffe("FF10126", "Unregistered batch type '%s'")
+	MsgBatchDispatchTimeout               = ffe("FF10127", "Timed out dispatching work to batch")
+	MsgInitializationNilDepError          = ffe("FF10128", "Initialization failed in %s due to unmet dependency")
+	MsgNilResponseNon204                  = ffe("FF10129", "No output from API call")
+	MsgInvalidContentType                 = ffe("FF10130", "Invalid content type", 415)
+	MsgDataNotFound                       = ffe("FF10133", "Data not found for message %s", 400)
+	MsgUnknownSharedStoragePlugin         = ffe("FF10134", "Unknown Shared Storage plugin '%s'")
+	MsgIPFSHashDecodeFailed               = ffe("FF10135", "Failed to decode IPFS hash into 32byte value '%s'")
+	MsgIPFSRESTErr                        = ffe("FF10136", "Error from IPFS: %s")
+	MsgSerializationFailed                = ffe("FF10137", "Serialization failed")
+	MsgMissingPluginConfig                = ffe("FF10138", "Missing configuration '%s' for %s")
+	MsgMissingDataHashIndex               = ffe("FF10139", "Missing data hash for index '%d' in message", 400)
+	MsgInvalidEthAddress                  = ffe("FF10141", "Supplied ethereum address is invalid", 400)
+	Msg404NoResult                        = ffe("FF10143", "No result found", 404)
+	MsgUnsupportedSQLOpInFilter           = ffe("FF10150", "No SQL mapping implemented for filter operator '%s'", 400)
+	MsgFilterSortDesc                     = ffe("FF10154", "Sort field. For multi-field sort use comma separated values (or multiple query values) with '-' prefix for descending")
+	MsgContextCanceled                    = ffe("FF00154", "Context cancelled")
+	MsgDBMigrationFailed                  = ffe("FF10163", "Database migration failed")
+	MsgHashMismatch                       = ffe("FF10164", "Hash mismatch")
+	MsgDefaultNamespaceNotFound           = ffe("FF10166", "namespaces.default '%s' must be included in the namespaces.predefined configuration")
+	MsgEventTypesParseFail                = ffe("FF10168", "Unable to parse list of event types", 400)
+	MsgUnknownEventType                   = ffe("FF10169", "Unknown event type '%s'", 400)
+	MsgIDMismatch                         = ffe("FF10170", "ID mismatch")
+	MsgRegexpCompileFailed                = ffe("FF10171", "Unable to compile '%s' regexp '%s'")
+	MsgUnknownEventTransportPlugin        = ffe("FF10172", "Unknown event transport plugin: %s")
+	MsgWSConnectionNotActive              = ffe("FF10173", "Websocket connection '%s' no longer active")
+	MsgWSSubAlreadyInFlight               = ffe("FF10174", "Websocket subscription '%s' already has a message in flight")
+	MsgWSMsgSubNotMatched                 = ffe("FF10175", "Acknowledgment does not match an inflight event + subscription")
+	MsgWSClientSentInvalidData            = ffe("FF10176", "Invalid data")
+	MsgWSClientUnknownAction              = ffe("FF10177", "Unknown action '%s'")
+	MsgWSInvalidStartAction               = ffe("FF10178", "A start action must set namespace and either a name or ephemeral=true")
+	MsgWSAutoAckChanged                   = ffe("FF10179", "The autoack option must be set consistently on all start requests")
+	MsgWSAutoAckEnabled                   = ffe("FF10180", "The autoack option is enabled on this connection")
+	MsgConnSubscriptionNotStarted         = ffe("FF10181", "Subscription %v is not started on connection")
+	MsgDispatcherClosing                  = ffe("FF10182", "Event dispatcher closing")
+	MsgMaxFilterSkip                      = ffe("FF10183", "You have reached the maximum pagination limit for this query (%d)")
+	MsgMaxFilterLimit                     = ffe("FF10184", "Your query exceeds the maximum filter limit (%d)")
+	MsgAPIServerStaticFail                = ffe("FF10185", "An error occurred loading static content", 500)
+	MsgEventListenerClosing               = ffe("FF10186", "Event listener closing")
+	MsgNamespaceNotExist                  = ffe("FF10187", "Namespace does not exist")
+	MsgInvalidSubscription                = ffe("FF10189", "Invalid subscription", 400)
+	MsgMismatchedTransport                = ffe("FF10190", "Connection ID '%s' appears not to be unique between transport '%s' and '%s'", 400)
+	MsgInvalidFirstEvent                  = ffe("FF10191", "Invalid firstEvent definition - must be 'newest','oldest' or a sequence number", 400)
+	MsgNumberMustBeGreaterEqual           = ffe("FF10192", "Number must be greater than or equal to %d", 400)
+	MsgAlreadyExists                      = ffe("FF10193", "A %s with name '%s:%s' already exists", 409)
+	MsgJSONValidatorBadRef                = ffe("FF10194", "Cannot use JSON validator for data with type '%s' and validator reference '%v'", 400)
+	MsgDatatypeNotFound                   = ffe("FF10195", "Datatype '%v' not found", 400)
+	MsgSchemaLoadFailed                   = ffe("FF10196", "Datatype '%s' schema invalid", 400)
+	MsgDataCannotBeValidated              = ffe("FF10197", "Data cannot be validated", 400)
+	MsgJSONDataInvalidPerSchema           = ffe("FF10198", "Data does not conform to the JSON schema of datatype '%s': %s", 400)
+	MsgDataValueIsNull                    = ffe("FF10199", "Data value is null", 400)
+	MsgDataInvalidHash                    = ffe("FF10201", "Invalid data: hashes do not match Hash=%s Expected=%s", 400)
+	MsgDataReferenceUnresolvable          = ffe("FF10204", "Data reference %d cannot be resolved", 400)
+	MsgDataMissing                        = ffe("FF10205", "Data entry %d has neither 'id' to refer to existing data, or 'value' to include in-line JSON data", 400)
+	MsgAuthorInvalid                      = ffe("FF10206", "Invalid author specified", 400)
+	MsgNoTransaction                      = ffe("FF10207", "Message does not have a transaction", 404)
+	MsgBatchNotSet                        = ffe("FF10208", "Message does not have an assigned batch", 404)
+	MsgBatchNotFound                      = ffe("FF10209", "Batch '%s' not found for message", 500)
+	MsgBatchTXNotSet                      = ffe("FF10210", "Batch '%s' does not have an assigned transaction", 404)
+	MsgOwnerMissing                       = ffe("FF10211", "Owner missing", 400)
+	MsgUnknownIdentityPlugin              = ffe("FF10212", "Unknown Identity plugin '%s'")
+	MsgUnknownDataExchangePlugin          = ffe("FF10213", "Unknown Data Exchange plugin '%s'")
+	MsgParentIdentityNotFound             = ffe("FF10214", "Identity '%s' not found in identity chain for %s '%s'")
+	MsgInvalidSigningIdentity             = ffe("FF10215", "Invalid signing identity")
+	MsgNodeAndOrgIDMustBeSet              = ffe("FF10216", "node.name, org.name and org.identity must be configured first", 409)
+	MsgBlobStreamingFailed                = ffe("FF10217", "Blob streaming terminated with error", 500)
+	MsgMultiPartFormReadError             = ffe("FF10218", "Error reading multi-part form input", 400)
+	MsgNodeNotFound                       = ffe("FF10224", "Node with name or identity '%s' not found", 400)
+	MsgLocalNodeResolveFailed             = ffe("FF10225", "Unable to find local node to add to group. Check the status API to confirm the node is registered", 500)
+	MsgGroupNotFound                      = ffe("FF10226", "Group '%s' not found", 404)
+	MsgDXRESTErr                          = ffe("FF10229", "Error from data exchange: %s")
+	MsgInvalidHex                         = ffe("FF10231", "Invalid hex supplied", 400)
+	MsgInvalidWrongLenB32                 = ffe("FF00107", "Byte length must be 32 (64 hex characters)", 400)
+	MsgNodeNotFoundInOrg                  = ffe("FF10233", "Unable to find any nodes owned by org '%s', or parent orgs", 400)
+	MsgFieldsAfterFile                    = ffe("FF10236", "Additional form field sent after file in multi-part form (ignored): '%s'", 400)
+	MsgDXBadResponse                      = ffe("FF10237", "Unexpected '%s' in data exchange response: %s")
+	MsgDXBadHash                          = ffe("FF10238", "Unexpected hash returned from data exchange upload. Hash=%s Expected=%s")
+	MsgBlobNotFound                       = ffe("FF10239", "No blob has been uploaded or confirmed received, with hash=%s", 404)
+	MsgDownloadBlobFailed                 = ffe("FF10240", "Error download blob with reference '%s' from local data exchange")
+	MsgDataDoesNotHaveBlob                = ffe("FF10241", "Data does not have a blob attachment", 404)
+	MsgWebhookURLEmpty                    = ffe("FF10242", "Webhook subscription option 'url' cannot be empty", 400)
+	MsgWebhookInvalidStringMap            = ffe("FF10243", "Webhook subscription option '%s' must be map of string values. %s=%T", 400)
+	MsgWebsocketsNoData                   = ffe("FF10244", "Websockets subscriptions do not support streaming the full data payload, just the references (withData must be false)", 400)
+	MsgWebhooksWithData                   = ffe("FF10245", "Webhook subscriptions require the full data payload (withData must be true)", 400)
+	MsgWebhooksReplyBadJSON               = ffe("FF10257", "Failed to process reply from webhook as JSON")
+	MsgRequestTimeout                     = ffe("FF10260", "The request with id '%s' timed out after %.2fms", 408)
+	MsgRequestReplyTagRequired            = ffe("FF10261", "For request messages 'header.tag' must be set on the request message to route it to a suitable responder", 400)
+	MsgRequestCannotHaveCID               = ffe("FF10262", "For request messages 'header.cid' must be unset", 400)
+	MsgSystemTransportInternal            = ffe("FF10266", "You cannot create subscriptions on the system events transport")
+	MsgFilterCountNotSupported            = ffe("FF10267", "This query does not support generating a count of all results")
+	MsgRejected                           = ffe("FF10269", "Message with ID '%s' was rejected. Please check the FireFly logs for more information")
+	MsgRequestMustBePrivate               = ffe("FF10271", "For request messages you must specify a group of private recipients", 400)
+	MsgUnknownTokensPlugin                = ffe("FF10272", "Unknown tokens plugin '%s'", 400)
+	MsgMissingTokensPluginConfig          = ffe("FF10273", "Invalid tokens configuration - name and plugin are required", 400)
+	MsgTokensRESTErr                      = ffe("FF10274", "Error from tokens service: %s")
+	MsgTokenPoolDuplicate                 = ffe("FF10275", "Duplicate token pool: %s")
+	MsgTokenPoolRejected                  = ffe("FF10276", "Token pool with ID '%s' was rejected. Please check the FireFly logs for more information")
+	MsgIdentityNotFoundByString           = ffe("FF10277", "Identity could not be resolved via lookup string '%s'")
+	MsgAuthorOrgSigningKeyMismatch        = ffe("FF10279", "Author organization '%s' is not associated with signing key '%s'")
+	MsgCannotTransferToSelf               = ffe("FF10280", "From and to addresses must be different", 400)
+	MsgLocalOrgLookupFailed               = ffe("FF10281", "Unable resolve the local org '%s' by the configured signing key on the node. Please confirm the org is registered with key '%s'", 500)
+	MsgFabconnectRESTErr                  = ffe("FF10284", "Error from fabconnect: %s")
+	MsgInvalidIdentity                    = ffe("FF10285", "Supplied Fabric signer identity is invalid", 400)
+	MsgFailedToDecodeCertificate          = ffe("FF10286", "Failed to decode certificate: %s", 500)
+	MsgInvalidMessageType                 = ffe("FF10287", "Invalid message type - allowed types are %s", 400)
+	MsgWSClosed                           = ffe("FF10290", "Websocket closed")
+	MsgFieldNotSpecified                  = ffe("FF10292", "Field '%s' must be specified", 400)
+	MsgTokenPoolNotConfirmed              = ffe("FF10293", "Token pool is not yet confirmed")
+	MsgHistogramCollectionParam           = ffe("FF10297", "Collection to fetch")
+	MsgInvalidNumberOfIntervals           = ffe("FF10298", "Number of time intervals must be between %d and %d", 400)
+	MsgInvalidChartNumberParam            = ffe("FF10299", "Invalid %s. Must be a number.", 400)
+	MsgHistogramInvalidTimes              = ffe("FF10300", "Start time must be before end time", 400)
+	MsgUnsupportedCollection              = ffe("FF10301", "%s collection is not supported", 400)
+	MsgContractInterfaceExists            = ffe("FF10302", "A contract interface already exists in the namespace: '%s' with name: '%s' and version: '%s'", 409)
+	MsgContractInterfaceNotFound          = ffe("FF10303", "Contract interface %s not found", 404)
+	MsgContractMissingInputArgument       = ffe("FF10304", "Missing required input argument '%s'", 400)
+	MsgContractWrongInputType             = ffe("FF10305", "Input '%v' is of type '%v' not expected type of '%v'", 400)
+	MsgContractMissingInputField          = ffe("FF10306", "Expected object of type '%v' to contain field named '%v' but it was missing", 400)
+	MsgContractMapInputType               = ffe("FF10307", "Unable to map input type '%v' to known FireFly type - was expecting '%v'", 400)
+	MsgContractByteDecode                 = ffe("FF10308", "Unable to decode field '%v' as bytes", 400)
+	MsgContractInternalType               = ffe("FF10309", "Input '%v' of type '%v' is not compatible blockchain internalType of '%v'", 400)
+	MsgContractLocationInvalid            = ffe("FF10310", "Failed to validate contract location: %v", 400)
+	MsgContractParamInvalid               = ffe("FF10311", "Failed to validate contract param: %v", 400)
+	MsgContractListenerNameExists         = ffe("FF10312", "A contract listener already exists in the namespace: '%s' with name: '%s'", 409)
+	MsgContractMethodNotSet               = ffe("FF10313", "Either an interface reference and method path, or in-line method definition, must be supplied on invoke contract request", 400)
+	MsgContractMethodResolveError         = ffe("FF10315", "Unable to resolve contract method: %s", 400)
+	MsgContractLocationExists             = ffe("FF10316", "The contract location cannot be changed after it is created", 400)
+	MsgListenerNoEvent                    = ffe("FF10317", "Either an interface reference and event path, or in-line event definition must be supplied when creating a contract listener", 400)
+	MsgListenerEventNotFound              = ffe("FF10318", "No event was found in namespace '%s' with id '%s'", 400)
+	MsgEventNameMustBeSet                 = ffe("FF10319", "Event name must be set", 400)
+	MsgMethodNameMustBeSet                = ffe("FF10320", "Method name must be set", 400)
+	MsgContractEventResolveError          = ffe("FF10321", "Unable to resolve contract event", 400)
+	MsgQueryOpUnsupportedMod              = ffe("FF10322", "Operation '%s' on '%s' does not support modifiers", 400)
+	MsgDXBadSize                          = ffe("FF10323", "Unexpected size returned from data exchange upload. Size=%d Expected=%d")
+	MsgTooLargeBroadcast                  = ffe("FF10327", "Message size %.2fkb is too large for the max broadcast batch size of %.2fkb", 400)
+	MsgTooLargePrivate                    = ffe("FF10328", "Message size %.2fkb is too large for the max private message size of %.2fkb", 400)
+	MsgManifestMismatch                   = ffe("FF10329", "Manifest mismatch overriding '%s' status as failure: '%s'", 400)
+	MsgFFIValidationFail                  = ffe("FF10331", "Field '%s' does not validate against the provided schema", 400)
+	MsgFFISchemaParseFail                 = ffe("FF10332", "Failed to parse schema for param '%s'", 400)
+	MsgFFISchemaCompileFail               = ffe("FF10333", "Failed compile schema for param '%s'", 400)
+	MsgPluginInitializationFailed         = ffe("FF10334", "Plugin initialization error", 500)
+	MsgUnknownTransactionType             = ffe("FF10336", "Unknown transaction type '%s'", 400)
+	MsgGoTemplateCompileFailed            = ffe("FF10337", "Go template compilation for '%s' failed: %s", 500)
+	MsgGoTemplateExecuteFailed            = ffe("FF10338", "Go template execution for '%s' failed: %s", 500)
+	MsgAddressResolveFailed               = ffe("FF10339", "Failed to resolve signing key string '%s': %s", 500)
+	MsgAddressResolveBadStatus            = ffe("FF10340", "Failed to resolve signing key string '%s' [%d]: %s", 500)
+	MsgAddressResolveBadResData           = ffe("FF10341", "Failed to resolve signing key string '%s' - invalid address returned '%s': %s", 500)
+	MsgDXNotInitialized                   = ffe("FF10342", "Data exchange is initializing")
+	MsgGroupRequired                      = ffe("FF10344", "Group must be set", 400)
+	MsgDBLockFailed                       = ffe("FF10345", "Database lock failed")
+	MsgFFIGenerationFailed                = ffe("FF10346", "Error generating smart contract interface: %s", 400)
+	MsgFFIGenerationUnsupported           = ffe("FF10347", "Smart contract interface generation is not supported by this blockchain plugin", 400)
+	MsgBlobHashMismatch                   = ffe("FF10348", "Blob hash mismatch sent=%s received=%s", 400)
+	MsgDIDResolverUnknown                 = ffe("FF10349", "DID resolver unknown for DID: %s", 400)
+	MsgIdentityNotOrg                     = ffe("FF10350", "Identity '%s' with DID '%s' is not an organization", 400)
+	MsgIdentityNotNode                    = ffe("FF10351", "Identity '%s' with DID '%s' is not a node", 400)
+	MsgBlockchainKeyNotSet                = ffe("FF10352", "No blockchain key specified", 400)
+	MsgNoVerifierForIdentity              = ffe("FF10353", "No %s verifier registered for identity %s", 400)
+	MsgNodeMissingBlockchainKey           = ffe("FF10354", "No organization signing key configured on node", 400)
+	MsgAuthorRegistrationMismatch         = ffe("FF10355", "Verifier '%s' cannot be used for signing with author '%s'. Verifier registered to '%s'", 400)
+	MsgAuthorMissingForKey                = ffe("FF10356", "Key '%s' has not been registered by any identity, and a separate 'author' was not supplied", 404)
+	MsgAuthorIncorrectForRootReg          = ffe("FF10357", "Author namespace '%s' and DID '%s' combination invalid for root organization registration", 400)
+	MsgKeyIdentityMissing                 = ffe("FF10358", "Identity owner of key '%s' not found", 500)
+	MsgIdentityChainLoop                  = ffe("FF10364", "Loop detected on identity %s in chain for %s (%s)", 400)
+	MsgInvalidIdentityParentType          = ffe("FF10365", "Parent %s (%s) of type %s is invalid for child %s (%s) of type", 400)
+	MsgParentIdentityMissingClaim         = ffe("FF10366", "Parent %s (%s) is invalid (missing claim)", 400)
+	MsgDXInfoMissingID                    = ffe("FF10367", "Data exchange endpoint info missing 'id' field", 500)
+	MsgEventNotFound                      = ffe("FF10370", "Event with name '%s' not found", 400)
+	MsgOperationNotSupported              = ffe("FF10371", "Operation not supported: %s", 400)
+	MsgFailedToRetrieve                   = ffe("FF10372", "Failed to retrieve %s %s", 500)
+	MsgBlobMissingPublic                  = ffe("FF10373", "Blob for data %s missing public payload reference while flushing batch", 500)
+	MsgDBMultiRowConfigError              = ffe("FF10374", "Database invalid configuration - using multi-row insert on DB plugin that does not support query syntax for input")
+	MsgDBNoSequence                       = ffe("FF10375", "Failed to retrieve sequence for insert row %d (could mean duplicate insert)", 500)
+	MsgDownloadSharedFailed               = ffe("FF10376", "Error downloading data with reference '%s' from shared storage")
+	MsgDownloadBatchMaxBytes              = ffe("FF10377", "Error downloading batch with reference '%s' from shared storage - maximum size limit reached")
+	MsgOperationDataIncorrect             = ffe("FF10378", "Operation data type incorrect: %T", 400)
+	MsgDataMissingBlobHash                = ffe("FF10379", "Blob for data %s cannot be transferred as it is missing a hash", 500)
+	MsgUnexpectedDXMessageType            = ffe("FF10380", "Unexpected websocket event type from DX plugin: %s", 500)
+	MsgFieldDescriptionMissing            = ffe("FF10381", "Field description missing for '%s' on route '%s'")
+	MsgFFStructTagMissing                 = ffe("FF10382", "ffstruct tag is missing for '%s' on route '%s'")
+	MsgContractListenerExists             = ffe("FF10383", "A contract listener already exists for this combination of topic + location + event", 409)
+	MsgRouteDescriptionMissing            = ffe("FF10384", "API route description missing for route '%s'")
+	MsgInvalidOutputOption                = ffe("FF10385", "invalid output option '%s'")
+	MsgInvalidPluginConfiguration         = ffe("FF10386", "Invalid %s plugin configuration - name and type are required")
+	MsgReferenceMarkdownMissing           = ffe("FF10387", "Reference markdown file missing: '%s'")
+	MsgFFSystemReservedName               = ffe("FF10388", "Invalid namespace configuration - %s is a reserved name")
+	MsgInvalidNamespaceMode               = ffe("FF10389", "Invalid %s namespace configuration - unknown mode")
+	MsgNamespaceUnknownPlugin             = ffe("FF10390", "Invalid %s namespace configuration - unknown plugin %s")
+	MsgNamespaceMultipartyConfiguration   = ffe("FF10391", "Invalid %s multiparty namespace configuration - database, blockchain, shared storage, and data exchange plugins are required")
+	MsgNamespaceGatewayNoDB               = ffe("FF10392", "Invalid %s gateway namespace configuration - a database plugin is required")
+	MsgNamespaceGatewayInvalidPlugins     = ffe("FF10393", "Invalid %s gateway namespace configuration - cannot specify dataexchange or shared storage plugins")
+	MsgNamespaceGatewayMultiplePluginType = ffe("FF10394", "Invalid %s namespace configuration - multiple %s plugins provided")
+	MsgDuplicatePluginName                = ffe("FF10395", "Invalid plugin configuration - plugin with name %s already exists")
 )
diff --git a/internal/namespace/config.go b/internal/namespace/config.go
index 52c0cc450..c6b5e7385 100644
--- a/internal/namespace/config.go
+++ b/internal/namespace/config.go
@@ -34,6 +34,9 @@ var (
 func InitConfig(withDefaults bool) {
 	namespacePredefined.AddKnownKey(coreconfig.NamespaceName)
 	namespacePredefined.AddKnownKey(coreconfig.NamespaceDescription)
+	namespacePredefined.AddKnownKey(coreconfig.NamespaceRemoteName)
+	namespacePredefined.AddKnownKey(coreconfig.NamespacePlugins)
+	namespacePredefined.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
 	if withDefaults {
 		namespaceConfig.AddKnownKey(NamespacePredefined+".0."+coreconfig.NamespaceName, "default")
 		namespaceConfig.AddKnownKey(NamespacePredefined+".0."+coreconfig.NamespaceDescription, "Default predefined namespace")
diff --git a/internal/namespace/manager.go b/internal/namespace/manager.go
index c7981bc8b..4d7c4514c 100644
--- a/internal/namespace/manager.go
+++ b/internal/namespace/manager.go
@@ -26,8 +26,12 @@ import (
 	"github.com/hyperledger/firefly-common/pkg/log"
 	"github.com/hyperledger/firefly/internal/coreconfig"
 	"github.com/hyperledger/firefly/internal/coremsgs"
+	"github.com/hyperledger/firefly/pkg/blockchain"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/hyperledger/firefly/pkg/database"
+	"github.com/hyperledger/firefly/pkg/dataexchange"
+	"github.com/hyperledger/firefly/pkg/sharedstorage"
+	"github.com/hyperledger/firefly/pkg/tokens"
 )
 
 type Manager interface {
@@ -36,14 +40,24 @@ type Manager interface {
 }
 
 type namespaceManager struct {
-	ctx      context.Context
-	nsConfig map[string]config.Section
+	ctx           context.Context
+	nsConfig      map[string]config.Section
+	bcPlugins     map[string]blockchain.Plugin
+	dbPlugins     map[string]database.Plugin
+	dxPlugins     map[string]dataexchange.Plugin
+	ssPlugins     map[string]sharedstorage.Plugin
+	tokensPlugins map[string]tokens.Plugin
 }
 
-func NewNamespaceManager(ctx context.Context) Manager {
+func NewNamespaceManager(ctx context.Context, bc map[string]blockchain.Plugin, db map[string]database.Plugin, dx map[string]dataexchange.Plugin, ss map[string]sharedstorage.Plugin, tokens map[string]tokens.Plugin) Manager {
 	nm := &namespaceManager{
-		ctx:      ctx,
-		nsConfig: buildNamespaceMap(ctx),
+		ctx:           ctx,
+		nsConfig:      buildNamespaceMap(ctx),
+		bcPlugins:     bc,
+		dbPlugins:     db,
+		dxPlugins:     dx,
+		ssPlugins:     ss,
+		tokensPlugins: tokens,
 	}
 	return nm
 }
@@ -80,7 +94,7 @@ func (nm *namespaceManager) getPredefinedNamespaces(ctx context.Context) ([]*cor
 	i := 0
 	foundDefault := false
 	for name, nsObject := range nm.nsConfig {
-		if err := core.ValidateFFNameField(ctx, name, fmt.Sprintf("namespaces.predefined[%d].name", i)); err != nil {
+		if err := nm.validateNamespaceConfig(ctx, name, i, nsObject); err != nil {
 			return nil, err
 		}
 		i++
@@ -124,3 +138,138 @@ func (nm *namespaceManager) initNamespaces(ctx context.Context, di database.Plug
 	}
 	return nil
 }
+
+func (nm *namespaceManager) validateNamespaceConfig(ctx context.Context, name string, index int, conf config.Section) error {
+	if err := core.ValidateFFNameField(ctx, name, fmt.Sprintf("namespaces.predefined[%d].name", index)); err != nil {
+		return err
+	}
+
+	if name == core.SystemNamespace || conf.GetString(coreconfig.NamespaceRemoteName) == core.SystemNamespace {
+		return i18n.NewError(ctx, coremsgs.MsgFFSystemReservedName, core.SystemNamespace)
+	}
+
+	mode := conf.GetString(coreconfig.NamespaceMode)
+	plugins := conf.GetStringSlice(coreconfig.NamespacePlugins)
+
+	// If no plugins are found when querying the config, assume older config file
+	if len(plugins) == 0 {
+		for plugin := range nm.bcPlugins {
+			plugins = append(plugins, plugin)
+		}
+
+		for plugin := range nm.dxPlugins {
+			plugins = append(plugins, plugin)
+		}
+
+		for plugin := range nm.ssPlugins {
+			plugins = append(plugins, plugin)
+		}
+
+		for plugin := range nm.dbPlugins {
+			plugins = append(plugins, plugin)
+		}
+	}
+
+	switch mode {
+	// Multiparty is the default mode when none is provided
+	case "multiparty":
+		if err := nm.validateMultiPartyConfig(ctx, name, plugins); err != nil {
+			return err
+		}
+	case "gateway":
+		if err := nm.validateGatewayConfig(ctx, name, plugins); err != nil {
+			return err
+		}
+	default:
+		return i18n.NewError(ctx, coremsgs.MsgInvalidNamespaceMode, name)
+	}
+	return nil
+}
+
+func (nm *namespaceManager) validateMultiPartyConfig(ctx context.Context, name string, plugins []string) error {
+	var dbPlugin bool
+	var ssPlugin bool
+	var dxPlugin bool
+	var bcPlugin bool
+
+	for _, pluginName := range plugins {
+		if _, ok := nm.bcPlugins[pluginName]; ok {
+			if bcPlugin {
+				return i18n.NewError(ctx, coremsgs.MsgNamespaceGatewayMultiplePluginType, name, "blockchain")
+			}
+			bcPlugin = true
+			continue
+		}
+		if _, ok := nm.dxPlugins[pluginName]; ok {
+			if dxPlugin {
+				return i18n.NewError(ctx, coremsgs.MsgNamespaceGatewayMultiplePluginType, name, "dataexchange")
+			}
+			dxPlugin = true
+			continue
+		}
+		if _, ok := nm.ssPlugins[pluginName]; ok {
+			if ssPlugin {
+				return i18n.NewError(ctx, coremsgs.MsgNamespaceGatewayMultiplePluginType, name, "sharedstorage")
+			}
+			ssPlugin = true
+			continue
+		}
+		if _, ok := nm.dbPlugins[pluginName]; ok {
+			if dbPlugin {
+				return i18n.NewError(ctx, coremsgs.MsgNamespaceGatewayMultiplePluginType, name, "database")
+			}
+			dbPlugin = true
+			continue
+		}
+		if _, ok := nm.tokensPlugins[pluginName]; ok {
+			continue
+		}
+
+		return i18n.NewError(ctx, coremsgs.MsgNamespaceUnknownPlugin, name, pluginName)
+	}
+
+	if !dbPlugin || !ssPlugin || !dxPlugin || !bcPlugin {
+		return i18n.NewError(ctx, coremsgs.MsgNamespaceMultipartyConfiguration, name)
+	}
+
+	return nil
+}
+
+func (nm *namespaceManager) validateGatewayConfig(ctx context.Context, name string, plugins []string) error {
+	var dbPlugin bool
+	var bcPlugin bool
+
+	for _, pluginName := range plugins {
+		if _, ok := nm.bcPlugins[pluginName]; ok {
+			if bcPlugin {
+				return i18n.NewError(ctx, coremsgs.MsgNamespaceGatewayMultiplePluginType, name, "blockchain")
+			}
+			bcPlugin = true
+			continue
+		}
+		if _, ok := nm.dxPlugins[pluginName]; ok {
+			return i18n.NewError(ctx, coremsgs.MsgNamespaceGatewayInvalidPlugins, name)
+		}
+		if _, ok := nm.ssPlugins[pluginName]; ok {
+			return i18n.NewError(ctx, coremsgs.MsgNamespaceGatewayInvalidPlugins, name)
+		}
+		if _, ok := nm.dbPlugins[pluginName]; ok {
+			if dbPlugin {
+				return i18n.NewError(ctx, coremsgs.MsgNamespaceGatewayMultiplePluginType, name, "database")
+			}
+			dbPlugin = true
+			continue
+		}
+		if _, ok := nm.tokensPlugins[pluginName]; ok {
+			continue
+		}
+
+		return i18n.NewError(ctx, coremsgs.MsgNamespaceUnknownPlugin, name, pluginName)
+	}
+
+	if !dbPlugin {
+		return i18n.NewError(ctx, coremsgs.MsgNamespaceGatewayNoDB, name)
+	}
+
+	return nil
+}
diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
index be19fc3a8..5b41be9da 100644
--- a/internal/orchestrator/orchestrator.go
+++ b/internal/orchestrator/orchestrator.go
@@ -176,6 +176,8 @@ type orchestrator struct {
 	sharedDownload       shareddownload.Manager
 	txHelper             txcommon.Helper
 	namespace            namespace.Manager
+	// Used to detect duplicate plugin names
+	pluginNames map[string]bool
 }
 
 func NewOrchestrator(withDefaults bool) Orchestrator {
@@ -369,9 +371,9 @@ func (or *orchestrator) initDatabasePlugins(ctx context.Context, plugins []datab
 
 func (or *orchestrator) validatePluginConfig(ctx context.Context, config config.Section, sectionName string) error {
 	name := config.GetString(coreconfig.PluginConfigName)
-	dxType := config.GetString(coreconfig.PluginConfigType)
+	pluginType := config.GetString(coreconfig.PluginConfigType)
 
-	if name == "" || dxType == "" {
+	if name == "" || pluginType == "" {
 		return i18n.NewError(ctx, coremsgs.MsgInvalidPluginConfiguration, sectionName)
 	}
 
@@ -379,6 +381,11 @@ func (or *orchestrator) validatePluginConfig(ctx context.Context, config config.
 		return err
 	}
 
+	if _, ok := or.pluginNames[name]; ok {
+		return i18n.NewError(ctx, coremsgs.MsgDuplicatePluginName, name)
+	}
+	or.pluginNames[name] = true
+
 	return nil
 }
 
@@ -444,6 +451,7 @@ func (or *orchestrator) initDataExchange(ctx context.Context) (err error) {
 }
 
 func (or *orchestrator) initPlugins(ctx context.Context) (err error) {
+	or.pluginNames = make(map[string]bool)
 	if or.metrics == nil {
 		or.metrics = metrics.NewMetricsManager(ctx)
 	}
@@ -862,7 +870,7 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) {
 
 func (or *orchestrator) initNamespaces(ctx context.Context) (err error) {
 	if or.namespace == nil {
-		or.namespace = namespace.NewNamespaceManager(ctx)
+		or.namespace = namespace.NewNamespaceManager(ctx, or.blockchains, or.databases, or.dataexchangePlugins, or.sharedstoragePlugins, or.tokens)
 	}
 	return or.namespace.Init(ctx, or.database)
 }


Test patch:
diff --git a/internal/namespace/manager_test.go b/internal/namespace/manager_test.go
index d3fd776f1..f6b35a66b 100644
--- a/internal/namespace/manager_test.go
+++ b/internal/namespace/manager_test.go
@@ -19,13 +19,24 @@ package namespace
 import (
 	"context"
 	"fmt"
+	"strings"
 	"testing"
 
 	"github.com/hyperledger/firefly-common/pkg/config"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/internal/coreconfig"
+	"github.com/hyperledger/firefly/mocks/blockchainmocks"
 	"github.com/hyperledger/firefly/mocks/databasemocks"
+	"github.com/hyperledger/firefly/mocks/dataexchangemocks"
+	"github.com/hyperledger/firefly/mocks/sharedstoragemocks"
+	"github.com/hyperledger/firefly/mocks/tokenmocks"
+	"github.com/hyperledger/firefly/pkg/blockchain"
 	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/pkg/database"
+	"github.com/hyperledger/firefly/pkg/dataexchange"
+	"github.com/hyperledger/firefly/pkg/sharedstorage"
+	"github.com/hyperledger/firefly/pkg/tokens"
+	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 )
@@ -33,6 +44,10 @@ import (
 type testNamespaceManager struct {
 	namespaceManager
 	mdi *databasemocks.Plugin
+	mbi *blockchainmocks.Plugin
+	mps *sharedstoragemocks.Plugin
+	mdx *dataexchangemocks.Plugin
+	mti *tokenmocks.Plugin
 }
 
 func (nm *testNamespaceManager) cleanup(t *testing.T) {
@@ -47,14 +62,25 @@ func newTestNamespaceManager(resetConfig bool) *testNamespaceManager {
 	nm := &testNamespaceManager{
 		mdi: &databasemocks.Plugin{},
 		namespaceManager: namespaceManager{
-			nsConfig: buildNamespaceMap(context.Background()),
+			nsConfig:      buildNamespaceMap(context.Background()),
+			bcPlugins:     map[string]blockchain.Plugin{"ethereum": &blockchainmocks.Plugin{}, "fabric": &blockchainmocks.Plugin{}},
+			dbPlugins:     map[string]database.Plugin{"postgres": &databasemocks.Plugin{}, "sqlite3": &databasemocks.Plugin{}},
+			dxPlugins:     map[string]dataexchange.Plugin{"ffdx": &dataexchangemocks.Plugin{}, "ffdx2": &dataexchangemocks.Plugin{}},
+			ssPlugins:     map[string]sharedstorage.Plugin{"ipfs": &sharedstoragemocks.Plugin{}, "ipfs2": &sharedstoragemocks.Plugin{}},
+			tokensPlugins: map[string]tokens.Plugin{"erc721": &tokenmocks.Plugin{}},
 		},
 	}
 	return nm
 }
 
 func TestNewNamespaceManager(t *testing.T) {
-	nm := NewNamespaceManager(context.Background())
+	bc := map[string]blockchain.Plugin{"ethereum": &blockchainmocks.Plugin{}}
+	db := map[string]database.Plugin{"postgres": &databasemocks.Plugin{}}
+	dx := map[string]dataexchange.Plugin{"ffdx": &dataexchangemocks.Plugin{}}
+	ss := map[string]sharedstorage.Plugin{"ipfs": &sharedstoragemocks.Plugin{}}
+	tokens := map[string]tokens.Plugin{"erc721": &tokenmocks.Plugin{}}
+
+	nm := NewNamespaceManager(context.Background(), bc, db, dx, ss, tokens)
 	assert.NotNil(t, nm)
 }
 
@@ -69,18 +95,52 @@ func TestInit(t *testing.T) {
 func TestInitNamespacesBadName(t *testing.T) {
 	coreconfig.Reset()
 	namespaceConfig.AddKnownKey("predefined.0."+coreconfig.NamespaceName, "!Badness")
-
 	nm := newTestNamespaceManager(false)
 	defer nm.cleanup(t)
 
+	utTestConfig := config.RootSection("test")
+
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "!Badness")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "test")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceDescription, "test description")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721"})
+	nm.nsConfig = map[string]config.Section{"!Badness": utTestConfig}
+
 	err := nm.initNamespaces(context.Background(), nm.mdi)
 	assert.Regexp(t, "FF00140", err)
 }
 
+func TestInitNamespacesReservedName(t *testing.T) {
+	coreconfig.Reset()
+	nm := newTestNamespaceManager(false)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("test")
+
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "ff_system")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "test")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceDescription, "test description")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721"})
+	nm.nsConfig = map[string]config.Section{"ff_system": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10388", err)
+}
+
 func TestInitNamespacesGetFail(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceDescription, "test description")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
 	nm.mdi.On("GetNamespace", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("pop"))
 	err := nm.initNamespaces(context.Background(), nm.mdi)
 	assert.Regexp(t, "pop", err)
@@ -90,16 +150,245 @@ func TestInitNamespacesUpsertFail(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
 	nm.mdi.On("GetNamespace", mock.Anything, mock.Anything).Return(nil, nil)
 	nm.mdi.On("UpsertNamespace", mock.Anything, mock.Anything, true).Return(fmt.Errorf("pop"))
 	err := nm.initNamespaces(context.Background(), nm.mdi)
 	assert.Regexp(t, "pop", err)
 }
 
+func TestInitNamespacesMultipartyUnknownPlugin(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721", "bad_unknown_plugin"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10390.*unknown", err)
+}
+
+func TestInitNamespacesMultipartyMultipleBlockchains(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721", "fabric"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10394", err)
+}
+
+func TestInitNamespacesMultipartyMultipleDX(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721", "ffdx2"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10394", err)
+}
+
+func TestInitNamespacesMultipartyMultipleSS(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721", "ipfs2"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10394", err)
+}
+
+func TestInitNamespacesMultipartyMultipleDB(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721", "sqlite3"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10394", err)
+}
+
+func TestInitNamespacesDeprecatedConfigMultipleBlockchains(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceDescription)
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	// nm.mdi.On("GetNamespace", mock.Anything, mock.Anything).Return(nil, nil)
+	// nm.mdi.On("UpsertNamespace", mock.Anything, mock.Anything, true).Return(nil)
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10394", err)
+}
+
+func TestInitNamespacesGatewayMultipleDB(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "gateway")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "sqlite3"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10394", err)
+}
+
+func TestInitNamespacesGatewayMultipleBlockchains(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "gateway")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "fabric"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10394", err)
+}
+
+func TestInitNamespacesMultipartyMissingPlugins(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10391", err)
+}
+
+func TestInitNamespacesGatewayWithDX(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "gateway")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "ffdx"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10393", err)
+}
+
+func TestInitNamespacesGatewayWithSharedStorage(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "gateway")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "ipfs"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10393", err)
+}
+
+func TestInitNamespacesGatewayUnknownPlugin(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "gateway")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "bad_unknown_plugin"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10390.*unknown", err)
+}
+
+func TestInitNamespacesGatewayNoDB(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "gateway")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10392", err)
+}
+
+func TestInitNamespacesUnknownMode(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "not a real mode")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
+	err := nm.initNamespaces(context.Background(), nm.mdi)
+	assert.Regexp(t, "FF10389", err)
+}
+
 func TestInitNamespacesUpsertNotNeeded(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
+	utTestConfig := config.RootSection("default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceRemoteName, "default")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceMode, "multiparty")
+	utTestConfig.AddKnownKey(coreconfig.NamespaceDescription, "test description")
+	utTestConfig.AddKnownKey(coreconfig.NamespacePlugins, []string{"ethereum", "postgres", "ffdx", "ipfs", "erc721"})
+	nm.nsConfig = map[string]config.Section{"default": utTestConfig}
+
 	nm.mdi.On("GetNamespace", mock.Anything, mock.Anything).Return(&core.Namespace{
 		Type: core.NamespaceTypeBroadcast, // any broadcasted NS will not be updated
 	}, nil)
@@ -120,12 +409,42 @@ func TestInitNamespacesDefaultMissing(t *testing.T) {
 
 func TestInitNamespacesDupName(t *testing.T) {
 	coreconfig.Reset()
-	InitConfig(true)
+	InitConfig(false)
 
-	namespaceConfig.AddKnownKey("predefined.0.name", "ns1")
-	namespaceConfig.AddKnownKey("predefined.1.name", "ns2")
-	namespaceConfig.AddKnownKey("predefined.2.name", "ns2")
-	config.Set(coreconfig.NamespacesDefault, "ns1")
+	viper.SetConfigType("yaml")
+	err := viper.ReadConfig(strings.NewReader(`
+  namespaces:
+    default: ns1
+    predefined:
+    - name: ns1
+      remoteName: ns1
+      mode: gateway
+      org:
+        key: 0x123456
+      plugins:
+      - sqlite3
+      - ethereum
+      - erc721
+    - name: ns2
+      remoteName: ns2
+      mode: gateway
+      org:
+        key: 0x223456
+      plugins:
+      - sqlite3
+      - ethereum
+      - erc721
+    - name: ns2
+      remoteName: ns2
+      mode: gateway
+      org:
+        key: 0x223456
+      plugins:
+      - sqlite3
+      - ethereum
+      - erc721
+  `))
+	assert.NoError(t, err)
 
 	nm := newTestNamespaceManager(false)
 	defer nm.cleanup(t)
diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go
index a4a2a5bdf..cf704956d 100644
--- a/internal/orchestrator/orchestrator_test.go
+++ b/internal/orchestrator/orchestrator_test.go
@@ -126,8 +126,9 @@ func newTestOrchestrator() *testOrchestrator {
 	ctx, cancel := context.WithCancel(context.Background())
 	tor := &testOrchestrator{
 		orchestrator: orchestrator{
-			ctx:       ctx,
-			cancelCtx: cancel,
+			ctx:         ctx,
+			cancelCtx:   cancel,
+			pluginNames: make(map[string]bool),
 		},
 		mdi: &databasemocks.Plugin{},
 		mdm: &datamocks.Manager{},
@@ -747,6 +748,18 @@ func TestBadTokensPlugin(t *testing.T) {
 	assert.Error(t, err)
 }
 
+func TestDuplicatePluginNames(t *testing.T) {
+	or := newTestOrchestrator()
+	or.pluginNames["duplicate"] = true
+	tifactory.InitConfig(tokensConfig)
+	utConfig := config.RootSection("test")
+	utConfig.AddKnownKey(coreconfig.PluginConfigName, "duplicate")
+	utConfig.AddKnownKey(coreconfig.PluginConfigType, "fftokens")
+	ctx := context.Background()
+	err := or.validatePluginConfig(ctx, utConfig, "test")
+	assert.Regexp(t, "FF10395", err)
+}
+
 func TestGoodTokensPlugin(t *testing.T) {
 	or := newTestOrchestrator()
 	defer or.cleanup(t)